前端时间有朋友咨询如何实现一个小程序的框架,可以方便的集成到自己的应用中,方便之后前端更新UI。之前自己曾经调研过小程序的技术原理,这里分享这篇博客目的是对小程序移动端的方案进行一些实践。下面就分享下小程序移动端的实践。首先看下小程序整体的一张架构图。
其实小程序的核心思想还是逻辑层和渲染层分离,这样Native可以把逻辑层的代码放到一个单独的线程中,渲染层只负责页面的展示,从而提高了Webview上显示的效率。所以Native的开发核心就是约定逻辑层和渲染层同上层前端代码的协议。
小程序源码的分析
首先可以看下微信小程序的IDE工具:
微信小程序的IDE工具主要是基于NW开源框架开发的,我们这里主要是基于electron这个web技术构建桌面应用框架,写的小程序IDE的demo。其实本质上两个框架是大同小异的。下面看下百度开源的小程序IDE工具的大致架构图:
基于上面的框架,构建了小程序IDE示例程序,demo的源码目录结构如下:
开发者主要是在app目录下开发,基于vue的模版语法进行页面编写,这个demo示例主要有index.cloud、index.css、index.js文件。 index.cloud代码如下:
<page class="container">
<h1>hello</h1>
<p>!</p>
<p></p>
<br />
<button @click="say">666666</button>
<br />
<input :value="content" @change="handleInputChange"></input>
<p></p>
<button @click="go">page 2!</button>
</page>
index.css代码如下:
.container{
width: 100vw;
height: 100vh;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
button{
width: 100%;
padding: 15px;
text-align: center;
font-size: 18px;
background: black;
color: white;
font-weight: 900;
}
input{
box-sizing: border-box;
width: 100%;
padding: 15px;
text-align: center;
font-size: 18px;
font-weight: 900;
}
index.js代码如下:
Page({
data() {
return {
who: 'xxxx',
what: 'uuu',
content: 'empty!'
}
},
watch: {
who(val){
if(val === 'ppppp'){
this.setData('what', 'nnn')
}
if(val === 'aaaa'){
this.setData('what', 'ananana')
}
}
},
mounted(){
this.setData('who', 'ppppp')
},
methods: {
say(){
this.setData('who', Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15));
},
go(){
beacon.navigateTo('page2', { content: this.content, b: 3 })
},
handleInputChange(value){
this.setData('content', value);
}
}
})
然后小程序通过master来解析用户的逻辑代码,生成数据,通过setData的方式传递给小程序slave的渲染层,这个传递的过程在IDE中是通过调用simulator层来模拟的,其实放到客户端的话,就是客户端需要实现的逻辑层js的运行环境。由于百度开源的小程序,移动端的逻辑并没有开放,所以我们根据百度小程序IDE开源的实现,做了一些分析,实现了移动端native的代码逻辑。下面我们就结合我们移动端的demo来看下具体的实现。
ios小程序sdk的实现
首先我们先看下上面的IDE中main.js中注册的一些模拟器方法
slave: {
page: {
ready(...payload){
logger.info(payload)
schemas.simulator.webview.distributeMessage('simulator', ...payload)
},
setData(...payload){
schemas.simulator.webview.distributeMessage('simulator', ...payload)
}
}
},
master: {
page: {
startListen(){
schemas.native.page.navigateTo('native/page/navigateTo', routes[0]);
logger.info(slaveLocations[0]);
master.openDevTools();
},
hook(schema, ...payload){
master.webContents.send('simulator', schema, currPage, ...payload)
},
callFn(schema, ...payload){
master.webContents.send('simulator', schema, currPage, ...payload)
},
prepare(schema){
// 准备页面
master.webContents.send('simulator', schema, currPage);
},
destroy(schema, page) {
logger.info(schema, page)
master.webContents.send('simulator', schema, page);
},
}
},
native: {
page: {
navigateTo(schema, url, payload){
logger.info(`navigateTo ${url}`)
// 获取页面的对象序号,修改当前集中的页面到序号页面
currPage = routes.findIndex((p) => p === url);
// 创建 webview ,获取 webviewid
const query = payload && queryString.stringify(payload);
const location = `${host}${url}.html${query ? `?${query}` :''}`;
logger.info(`navigateTo ${url}`)
schemas.simulator.webview.createWebview(location, currPage);
// 压入页面栈
push({
pageIndex: currPage,
path: routes[url],
location,
query: payload
// page
})
},
navigateBack(schema, payload){
if(isBottom()) return;
schemas.master.page.destroy('master/page/destroy', currPage);
schemas.simulator.webview.destroyWebview(currPage);
const curr = pop();
currPage = curr.pageIndex;
},
getQuery(schema){
master.webContents.send('simulator', schema, currPage, getCurrentCache().query);
}
},
app: {
onLaunch(schema, options){
},
onShow(schema, options){
}
}
}
下面我分析下移动端如何配合前端代码,来实现最终渲染的。首先看下ios端小程序sdk的代码目录结构:
ios的逻辑层代码是基于JSContext实现,渲染层是基于WKWebview实现。jscontext层的核心代码是实现下面的协议:
typedef void (^JSCallBack)(NSString * schema,id args);
@protocol NEBridgeProtocol <NSObject>
@property (nonatomic, readonly) JSValue* exception;
- (void)evalJavascript:(NSString *)script;
- (JSValue *)callJSMethod:(NSString*)method args:(NSArray*)args;
- (void)listenJSEvent:(NSString*)name callback:(JSCallBack)callback;
@end
上面的代码是逻辑层和前端代码沟通的核心,等下我会配合刚才IDE的业务代码分析下如何通信的。
渲染端的核心代码如下:
@protocol NEWebViewBridgeProtocol
@required
- (void)receiveMessageHandler:(void (^ _Nullable)(WKScriptMessage * _Nonnull message))completionHandler;
- (void)evaluateJavaScript:(NSString *_Nullable)javaScriptString;
@end
客户端如何同前端通信
有了前面基础理论的铺垫之后,我们来看下前端如何同客户端通信的,首先我们需要把逻辑层,也就是JSContext的运行环境放到一个单独的线程中,下面我贴出来一部分核心代码:
- (void)runJSCode:(NSString*)code {
@weakify(self);
dispatch_async(self.threadQueue, ^{
@strongify(self);
[self.jsBridge evalJavascript:code];
});
}
- (void)registerEvent:(NSString*)name callback:(JSCallBack)block {
[self.jsBridge listenJSEvent:name callback:block];
}
然后就是注册逻辑层的监听事件,相当于模拟electron工程的simulater的实现。
NEBridgeContext * context = [NEBridgeContext createInstance:@"netease"];
NSString * code = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
@weakify(self);
[context registerEvent:@"startListen" callback:^(NSString *schema,NSDictionary * args) {
@strongify(self);
dispatch_async(dispatch_get_main_queue(), ^{
[self createWebview];
});
}];
[context registerEvent:@"navigateBack" callback:^(NSString *schema,NSDictionary * args) {
@strongify(self);
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController popViewControllerAnimated:YES];
});
}];
[context registerEvent:@"navigateTo" callback:^(NSString *schema,id args) {
@strongify(self);
if ([args isKindOfClass:[NSArray class]]) {
NSArray * paras = (NSArray*)args;
self.queryData = paras[1];
dispatch_async(dispatch_get_main_queue(), ^{
[self navigationTo:paras[0]];
});
}
}];
[context registerEvent:@"setData" callback:^(NSString *schema, id args) {
@strongify(self);
[self handleSchema:schema args:args];
}];
[context registerEvent:@"ready" callback:^(NSString *schema,id args) {
@strongify(self);
[self handleSchema:schema args:args];
}];
[context registerEvent:@"getQuery" callback:^(NSString *schema, id args) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Logic excute : %@",code);
[context runJSCode:code];
});
}];
[context runJSCode:code];
其实上面的逻辑就是刚才提到的master做的主要事情。接收逻辑层的数据,然后通知客户端来创建webview,以及做一些native的控件和动画。之后进入到渲染层的处理,渲染层的代码逻辑如下:
[self.mpView receiveMessageHandler:^(WKScriptMessage * _Nullable message) {
@strongify(self);
[self receiveRenderMessage:message webView:self.mpView];
}];
- (void)receiveRenderMessage:(WKScriptMessage *) message webView:(MNPWebView *) currentWebView {
NSArray * postMessages = (NSArray *)message.body;
NSLog(@"Render message: %@",postMessages);
if (postMessages.count == 0) {
return;
}
NSString * code = [NSString stringWithFormat:@"communicator.emit("];
NSInteger index = 0;
for (id para in postMessages) {
if (index == 0) {
code = [NSString stringWithFormat:@"%@\"%@\",%ld",code,para,(long)currentWebView.pageIndex];
}
else {
code = [NSString stringWithFormat:@"%@,\"%@\"",code,para];
}
index ++;
}
code = [NSString stringWithFormat:@"%@)",code];
NEBridgeContext * context = [NEBridgeContext createInstance:@"netease"];
NSLog(@"Logic excute:%@",code);
[context runJSCode:code];
}
渲染层主要是接收一些用户触发的事件,然后告诉逻辑层来获得数据,逻辑层准备完毕数据后,就再给渲染层,渲染层来渲染页面,最终显示出来。 至此客户端就把逻辑层和渲染层代码分离开了,可以分别的处理不同的事情。下面我们就看下demo的效果。
IDE环境和小程序的Demo
小程序的IDE其实是基于Electron的工程,我们需要启动Electron的模拟器和调试器,用户是通过编写VUE的模版代码和js代码来创建小程序的。如果说要实现客户端动态调试的话,其实是需要客户端创建websocket的连接,前端代码更新的时候,实时通知客户端进行渲染加载,目前demo还没有做这个事情,只是简单的分离了逻辑层和渲染层。下面我们看下我们demo的IDE的长的样子吧。
首先我们通过如下命令
cd simulator-shell/
node launch
启动模拟器的IDE环境,然后进到工程目录中启动当前小程序代码
npm run dev
结果如下图:
小程序demo在编写完毕的时候,通过webpack打包输出master.js、page.js、index.html这些文件。然后客户端分别加载逻辑层master.js的代码和渲染层index.html运行这个demo。
小程序完整方案
逻辑层和渲染层分离只是小程序的一个核心功能,如果要做出来一个完成的小程序,当然还需要很多工程化的事情。这里就说下完整的方案需要的技术能力.
-
Web资源离线缓存能力,小程序往往有大量的静态资源比如webpack打包好的渲染层的代码,以及图片音视频资源,这些如果能做离线缓存,就可以大大提升小程序的性能。
-
静态资源更新的能力,这个就涉及到小程序更新的逻辑。由于静态资源远程加载一般都需要CDN来做加速,CDN节点往往都有资源缓存,所以对静态资源如何做更新哪?一般通用的方法时,更新静态资源版本号的方式,或者对资源做差量计算。这就要根据具体采用的方案进行细化了。
-
小程序的远程调试能力。小程序开发者使用开发工具完成开发后,需要有一定的能力预览上线后的样式,然后可以远程调试定位问题。这里往往需要提供websocket的能力给开发者方便定位问题。
-
native拓展能力,如果开发者,想要用native的代码做一些自定义动画,或者一些自定义控件的话,如果小程序可以方便的把一些native的控件和动画作为插件,集成到小程序的UI库中的话,就可以大大提升小程序的拓展能力。
-
对一些JS语法能力的一些拓展。
小程序前景展望
小程序的重要一点就是可以做到实时发布一些功能。尤其对一些轻量级的页面,比如一些活动页面特别的有用。同时小程序又能让开发者使用一些native的功能,让用户体验上接近native。其实对于很多第三方的app都有开发自己小程序的需求,如果说能够开放出来一个小程序的生态环境,相信会吸引很多开发者入住。目前w3c已经提出了小程序草案。也是希望小程序有一个统一的标准,能更好的的服务广大的开发者。
当然我上面的分析只是针对小程序移动端sdk的一个笼统的介绍,如果需要做一个完善的小程序产品,还需要大量工程化的事情,比如我们要有大量的vue组件库和native的组件库,提供给开发者使用。IDE开发工具肯定也要做一些个性化的更改,比如热更新和调试功能。整个工程下来,可能并不像我这篇文章描述的这么简单,我这里只是分析了下目前小程序可行的一种方案。在技术方案可行的前提下,其实后期就是人力投入和产品打磨的过程了。往往一个成功的产品可能后期的打磨会更关键一些。也想借助这篇博客抛砖引玉,希望能够引起大家对小程序这个产品的兴趣。