微信小程序算是小程序的鼻祖了,2017年1月9日微信正式上线了小程序。在探究小程序技术架构以前,咱们先看看小程序到底是什么,微信官网对微信小程序的产品定位及功能介绍是: “微信小程序是一种全新的链接用户与服务的方式,它能够在微信内被便捷地获取和传播,同时具备出色的使用体验。”前端
这个介绍有种看了跟没看同样的感受。网上对于微信小程序是什么还有一个介绍的版本: “小程序是一种不须要下载安装便可使用的应用,它实现了应用「触手可及」的梦想,用户扫一扫或搜一下便可打开应用。也体现了「用完即走」的理念,用户不用关心是否安装太多应用的问题。应用将无处不在,随时可用,但又无需安装卸载。”web
这个概念就更清晰一些,能够看出小程序是众多实例运行在一个宿主应用中,小程序自己也是一种可插拔的外接应用。小程序
下面从用户使用交互角度来看一下小程序:微信小程序
iOS:从小程序独立性角度(小程序与小程序之间,小程序与宿主应用之间切换)来讲,BATT 的小程序与招商银行的小程序基本交互类似。 Android:从交互上来看BATT 的小程序均可以看作是独立的应用程序,独立存在于后台(多进程),能够在小程序与小程序之间,小程序与宿主应用之间切换。能够直观的理解为这类小程序为小程序应用。招商银行的的小程序是与宿主应用共存的,也就是在一个进程中,不能在小程序与小程序之间,小程序与宿主应用之间切换。这类小程序能够直观的理解为小程序页面。安全
BATT: 微信,支付宝,头条,百度小程序。因为交互类似,因此并称。微信
因此,从用户使用角度来看,BATT的交互体验更有优点。从对小程序概念的理解来看,各app理解有所差别,但这并不影响功能层面的使用。babel
最先应用小程序的微信为何会创造出小程序这个东西呢?它到底有什么做用?在我看来,主要目的仍是在于管控为目的,使用了多个手段来实现,主要管控在于两个方面:网络
UI管控:以微信为例,微信本身定义了一套DSL,而不是用HTML来开发页面。这样就不能让开发者随意开发,而是在微信的DSL框架中开发。开发者写的DSL具体转换成什么,是经过什么渲染,都是微信平台来决定。基于自定义的这套DSL,能够更好的作代码管控方面的工做,好比:请求白名单,代码扫描等。多线程
服务管控:仍是以微信为例,微信中的宿主平台提供的服务(好比:支付,微信运动,卡券,发票,用户帐号信息等)对于不管是二方使用者仍是三方使用者,都有权限管控的需求。目的也是不能让接入的小程序,在没有受权机制的前提下,随意调用微信基础服务。架构
基于从用户角度的体验需求,以及平台角度的管控需求,咱们来看看BATT系小程序在技术上作了哪些达到了这些目的。
下图是小程序的渲染流程,里面包含了部分技术选型,后面的部分会提到:
在咱们聊DSL以前,咱们先看看编译代码须要作哪些工做。不管是解释性语言(JS,Ruby,Python)仍是编译型语言(Java,C++,C#),都会有一个共同的部分,将源代码解析为AST(抽象语法树)。AST不只可以以结构化的方式呈现源代码,并且在语义分析中起到关键做用。AST不只仅应用在解释器和编译器,并且在静态代码分析中也比较经常使用,好比:咱们在重构代码的时候,但愿提取出公共模块,以便减小重复代码方便复用。这时咱们单纯的用字符串比对的方式会比较片面不能达到效果,这时生成AST就比较有用。另外一个应用例子对于咱们的DSL设计会比较有借鉴意义:代码转换器。下图是一种语言代码转换为另外一种语言代码的主要步骤:
源代码先解析成AST,解析以前它是遵循语言规则的文本,解析以后成为与输入文本彻底相同的树形结构,这个过程是可逆的。而后再对AST遍历以及替换,这个过程对于前端来讲相似于DOM树的生成,最后根据修改后的AST生成编译后的代码。咱们以JS为例,用acorn生成的AST,一样咱们也可使用其余的解析器,例如:babylon,esprima等,下面是一个简单的例子(限于篇幅,右侧的AST树没有彻底展开,读者能够到astexplorer上生成结果):
因为小程序的渲染容器有多是Webview容器,原生Native容器,Flutter容器(虽然Flutter也是Native渲染,为了与原生Native区别,这里把它单独出来,下同),因此咱们能够借鉴前面的代码转换器的思路,用AST抹平具体渲染容器的区别,下图是DSL转换的总体思路:
有了以上的的设计并非大功告成,还须要有不少须要作的工做要作。咱们能够简单的把DSL的处理分为编译时和运行时,编译时负责把DSL代码预编译为目标代码,目标代码能够在相应的运行时环境执行。生成的目标代码的做用是,能够在具体的运行时经过当前环境的参数来执行出实际的代码,简单的理解就是为了在多渲染环境运行的一个适配器。
对于编译时来讲,从零写起确定是不现实的。首先咱们继续上面AST的话题,上面已经提到了几个AST的解析器:acorn,babylon,esprima。固然还有不少其余的例如:cherow,espree,shift等。因此咱们不用再造一个轮子,下面用babylon举例,由于babylon在babel中使用,会与最近的JS功能同步,而且API设计良好易于使用。babel是js编译器(并不只仅是ES6支持的工具包,不然就变成了相似于Android里面的support包了),能够用于代码压缩,语法转换等。对于生成目标代码的过程:解析(babylon),转换(babel-traverse)都有很好的支持。因为不一样的渲染容器有不一样的组件库和API,一样功能的组件或API的使用也不尽相同,因此须要封装出适配层代码。
对于运行时来讲,只须要把编译生成的适配代码生成具体渲染环境的代码执行就能够了。这里比较相似babel把ECMAScript新版代码转换成旧版代码的逻辑比较像。
从性能角度出发,把小程序最终经过Native方式渲染会比用Webview做为渲染容器获得更好的效果。DSL的设计能够很好的屏蔽底层实际渲染的实现,能够用Native,也能够用H5,也可使用二者结合的方式,底层渲染引擎的切换也不会影响到小程序开发者的外部接入。目前移动端跨平台Native渲染的技术很是流行,比较经常使用的有Weex/RN/Flutter。市场上有基于Weex和RN进行小程序的案例,Flutter毕竟是后起之秀,目前尚未见到用Flutter做为渲染引擎的案例。
前面咱们在从“用户角度的小程序”部分看到,BATT的方案让小程序真正能够作到像应用同样的体验。因为iOS应用没法开启新进程让小程序自己在独立进程中运行,因此iOS中的小程序只能与宿主应用共享同一进程。对于Android来讲就不同了,小程序占用独立进程,从安全角度来讲,二方后者三方的小程序应用与宿主应用进程隔离,小程序出现的问题不会影响宿主应用。并且,从性能角度来讲,小程序不会共享宿主应用的内存。从BATT的小程序实际应用操做来看,基本都会控制五个后台开启的进程保活,能够用五个容器Activity各自在本身的进程中渲染小程序,有的还会有后台的保活时间限制。再开启新的小程序,会关闭最先打开的小程序进程,这样达到了高效热启动的目标。
逻辑渲染隔离: 首先,在聊具体的多线程以前,先说一下小程序的逻辑和渲染的问题。小程序的逻辑和渲染是分离的,固然从功能层面,不分离同样能够实现。这里说的逻辑和渲染分离是指小程序的逻辑运行在单独JS环境的线程中,只须要JS引擎就能够,渲染运行在Webview线程中。逻辑和渲染分离到两个线程有几点好处:第一个是能够逻辑和渲染代码分离没有耦合,第二个是可让逻辑线程和渲染线程并行执行,JS执行不会阻塞UI。第三是补充了前面所说的UI管控的目的,逻辑线程里面JS在JS引擎中运行,而不是在Webview里面,这样就限制了经过注入JS代码来操做dom的可能,任何与UI相关的API都没有办法经过JS来改变,这样就与DSL一块儿达到了UI管控的目的。第四个好处是多个小程序页面共享同一个JS逻辑运行环境,能够方便高效的共享数据。
上图展现了逻辑层与视图层的通讯过程,通讯经过Bridge中转,利用发布/订阅模式。视图层经过触发UI事件,会把事件经过bridge传递到Native,Native再经过bridge把事件中转给逻辑层,逻辑层处理事件完成后,把数据再经过bridge传递到Native,以后再由bridge返回给视图层作渲染。
优化: 逻辑和渲染分离以后,逻辑线程须要把数据发送给渲染线程,渲染线程须要把事件发送给逻辑线程,这都须要序列化为字符串进行传输。这样会带来一个问题,频繁的数据传输,和单次大数据量传输都会带来性能问题。针对这个问题,支付宝小程序的设计思路比较值得借鉴,支付宝小程序从新设计了V8虚拟机,让逻辑和渲染都有本身的Local Runtime,存放私有的模块和数据。又提供了共享的Global Runtime的Shared Heap来共享数据,这样依然保证了逻辑和渲染的隔离,又减小了序列化和传输成本。
小程序的开发者在小程序应用方面,作了不少优化,好比:数据的预加载。从用户点击页面,到新页面onload(),会延迟100ms-300ms,这个延迟时间,能够作数据的预加载。这里所说的小程序启动预加载,是小程序渲染框架层面的。iOS的优化会预加载比实际渲染小程序数多一个wkwebview放在后台,打开新的小程序会直接把预先加载的wkwebview直接渲染,节省了初始化时间;Android上实现稍微复杂一点,不过依然是空间换时间的思路,从Android宿主应用启动开始,就会启动一个预留进程,当开启新的小程序,会占用这个进程,并再预加载新的进程,直到开启第五个进程的上限。
离线包机制的根本目的在于让小程序打开的时候,可让页面资源从网络IO替换为本地IO。其实就是在app打开以前从网络拉取或者推拉结合,预置等方式让离线包能够在打开小程序以前就已经在本地了。离线包模块的职责包括:更新,解压,存储,读取和校验等,固然也能够作二进制的差量包以。有了离线包机制,也要考虑把整个小程序总体做为一个离线包会影响效率的问题,因此这里须要增长分包的方案,能够把离线包分为一个主包和多个子包的形式,主包里面主要包括:首屏资源,公共代码,相关子包的信息等;子包能够包含二级页面的页面资源。这样就能够提升首屏打开速度,能够作到按需加载的目的,以下图:
小程序平台都有本身的IDE,对于多系统平台的现状,选取跨平台桌面技术开发小程序IDE,确定是最好的选择,这里选择了Electron和NW.JS作了一下对比:
对比结果简单的说,二者开源协议都比较友好,若是重视代码安全性或者兼容XP需求,就选择NW.JS,也是国内厂商的选择;若是从开发支持角度来比较,就选择Electron。
前面已经说了,逻辑层具备单独的JS环境,也从管控角度说明了这样作也能够防止js修改UI的风险。就技术选型角度来讲,iOS可使用自带的JScore,虽然iOS上wkwebview的JS引擎比JScore多了JIT优化,执行速度快不少,可是比起额外引入js引擎来讲,使用自带js引擎具备稳定且不增长包大小的先天优点。这块可能有人会提到Flutter在iOS里面引入了skia渲染引擎的问题,Flutter在iOS引入skia的好处是与Android自带的skia引擎使用相同渲染引擎,这样会在UI兼容性上有更好的提高。而js引擎兼容性问题就小的多。 Android方面,可选择性多一下,如下是一个主流JS引擎对比:
微信小程序旧版本用的JScore,新版本用的V8;支付宝小程序用的从新设计的V8;头条小程序也是使用的V8;能够看到V8的中标率仍是很高的,并且开源协议也比较友好。
本文算是介绍了一种小程序渲染架构的一种实现方式,就小程序平台自己来讲,还有一些其余的功能和优化点,好比:小程序路由,Debug包加载,埋点统计,虚拟Dom的优化等。文章只是介绍了一些主要流程和技术点,真正作一个完善的小程序平台仍是须要不少细节须要考虑的,就小程序开发者的角度来讲,也是有优化空间的,好比:骨架屏。作一个小程序平台须要多平台多种技术能力的综合应用才能不断完善,随着新技术的涌现,将来会有更多的技术应用到小程序中。
王利航,2018年9月加入团队,曾就任于阿里巴巴,搜狐畅游,目前负责民生科技公司移动开发平台建设。