3

小程序移动端方案分享

 3 years ago
source link: https://mengtnt.com/2020/05/04/mini-program.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前端时间有朋友咨询如何实现一个小程序的框架,可以方便的集成到自己的应用中,方便之后前端更新UI。之前自己曾经调研过小程序的技术原理,这里分享这篇博客目的是对小程序移动端的方案进行一些实践。下面就分享下小程序移动端的实践。首先看下小程序整体的一张架构图。

小程序架构图.png

其实小程序的核心思想还是逻辑层和渲染层分离,这样Native可以把逻辑层的代码放到一个单独的线程中,渲染层只负责页面的展示,从而提高了Webview上显示的效率。所以Native的开发核心就是约定逻辑层和渲染层同上层前端代码的协议。

小程序源码的分析

首先可以看下微信小程序的IDE工具:

微信小程序.png

微信小程序的IDE工具主要是基于NW开源框架开发的,我们这里主要是基于electron这个web技术构建桌面应用框架,写的小程序IDE的demo。其实本质上两个框架是大同小异的。下面看下百度开源的小程序IDE工具的大致架构图:

百度小程序IED.png
小程序IDE.png

基于上面的框架,构建了小程序IDE示例程序,demo的源码目录结构如下:

IDE目录.png

开发者主要是在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目录结构.png

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

结果如下图:

模拟器.png

小程序demo在编写完毕的时候,通过webpack打包输出master.js、page.js、index.html这些文件。然后客户端分别加载逻辑层master.js的代码和渲染层index.html运行这个demo。

小程序完整方案

逻辑层和渲染层分离只是小程序的一个核心功能,如果要做出来一个完成的小程序,当然还需要很多工程化的事情。这里就说下完整的方案需要的技术能力.

  1. Web资源离线缓存能力,小程序往往有大量的静态资源比如webpack打包好的渲染层的代码,以及图片音视频资源,这些如果能做离线缓存,就可以大大提升小程序的性能。

  2. 静态资源更新的能力,这个就涉及到小程序更新的逻辑。由于静态资源远程加载一般都需要CDN来做加速,CDN节点往往都有资源缓存,所以对静态资源如何做更新哪?一般通用的方法时,更新静态资源版本号的方式,或者对资源做差量计算。这就要根据具体采用的方案进行细化了。

  3. 小程序的远程调试能力。小程序开发者使用开发工具完成开发后,需要有一定的能力预览上线后的样式,然后可以远程调试定位问题。这里往往需要提供websocket的能力给开发者方便定位问题。

  4. native拓展能力,如果开发者,想要用native的代码做一些自定义动画,或者一些自定义控件的话,如果小程序可以方便的把一些native的控件和动画作为插件,集成到小程序的UI库中的话,就可以大大提升小程序的拓展能力。

  5. 对一些JS语法能力的一些拓展。

小程序前景展望

小程序的重要一点就是可以做到实时发布一些功能。尤其对一些轻量级的页面,比如一些活动页面特别的有用。同时小程序又能让开发者使用一些native的功能,让用户体验上接近native。其实对于很多第三方的app都有开发自己小程序的需求,如果说能够开放出来一个小程序的生态环境,相信会吸引很多开发者入住。目前w3c已经提出了小程序草案。也是希望小程序有一个统一的标准,能更好的的服务广大的开发者。

当然我上面的分析只是针对小程序移动端sdk的一个笼统的介绍,如果需要做一个完善的小程序产品,还需要大量工程化的事情,比如我们要有大量的vue组件库和native的组件库,提供给开发者使用。IDE开发工具肯定也要做一些个性化的更改,比如热更新和调试功能。整个工程下来,可能并不像我这篇文章描述的这么简单,我这里只是分析了下目前小程序可行的一种方案。在技术方案可行的前提下,其实后期就是人力投入和产品打磨的过程了。往往一个成功的产品可能后期的打磨会更关键一些。也想借助这篇博客抛砖引玉,希望能够引起大家对小程序这个产品的兴趣。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK