探索小程序原理

602 阅读10分钟

前言

       小程序最开始是由DCloud团队提出来的,但是当时并没有在业内流传开,17年张小龙在公开课上展示了小程序的开放能力,“跳一跳”火爆微信朋友圈,微信小程序凭借优秀的用户体验,丰富的生态以及强大的裂变能力迅速掀起了小程序的浪潮。如今淘宝、字节、京东...都有自己的小程序,企业微信、钉钉App中为向使用者提供拓展、定制化的功能,可以添加第三方小程序。支付宝团队还推出了mPaaS移动开发平台,使用小程序技术开发App,实现应用的热更新,并且小程序具备离线缓存和分包懒加载这些优势,这也就是为什么支付宝启动速度是所有app中最快的原因。小程序的热门无疑对前端开发者是非常利好的,最近在实现H5唤起小程序播放视频这个功能,勾起了对小程序的兴趣,了解小程序的运行原理对今后的开发也是很有帮助的,so,开始进入正题吧。

宿主环境

      小程序可以运行在各类移动应用中,在启动小程序时,所需要的运行环境与能力需要由客户端应用向小程序提供。我们将这套环境称为“宿主环境(宿主应用) ”,而通过宿主环境,小程序就可以实现很多原生应用于H5应用无法实现的功能。微信小程序可以运行在企业微信里,支付宝小程序可以运行在淘宝里,这里的微信、企业微信就是不同的宿主环境,有的api存在兼容性问题,大部分都是宿主环境导致的。

基本原理

以下内容摘抄至[微信官方文档]

a.png

小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。

小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型下图所示。

小程序采用了双线程的架构,渲染层与逻辑层通过Nactive进行转发,这与传统网页开发还有些不同。

小程序开发过程中需要面对的是两大操作系统 iOS 和 Android 的微信客户端,以及用于辅助开发的小程序开发者工具,小程序中在这三大运行环境如下:

运行环境逻辑层渲染层
iosJavascriptCoreWKWebView
安卓V8chromium定制内核
小程序开发者工具NWJSChrome WebView

可能大家比较陌生的是NWJS,从其官网nwjs.io/可以了解到,NWJS具备ChromiumNode.js 的特性. NWJS能够通过页面技术开发桌面应用。也就是说小程序开发者工具也和vscode一样,是前端技术开发的一款桌面编辑工具,那我们就可以从这里探究一下小程序的具体实现原理了。

原理探索

上面也说道,微信小程序的页面是由webview线程渲染的,也就是说大部分是网页渲染的,为什么说是大部分呢,因为微信小程序有一些组件是由客户端创建的原生组件,其中就包括video、map、canvas组件。 接下来使用微信开发者工具,打开一个小程序项目:

b.png 上面图片展示的是小程序的主页,页面中有一个英语入口按钮,点击后会进入英语播放列表,点击列表item会进入英语播放页面,一共三个页面。从app.json中pages属性中可以看到已经注册了几个页面。

然后我们在小程序开发者工具中进行一些操作,探究其原理。

逻辑层

c.gif

我们先在控制台输入getCurrentPage API获取当前小程序的页面栈,在首页时控制台中输入getCurrentPages打印出页面栈,长度为1。进入音乐播放详情页时,getCurrentPages打印出长度为3的数组,数组中的元素是每个页面的Page对象,可以从上面动图中看到,这个对象包含了页面的生命周期钩子函数、绑定事件的function、data、data(小程序globalData)、以及内置的一些对象router和全局方法(animation、setData)等。

接着我们在控制台中输入document:

d.gif

可以看到,当鼠标移动到document输出的HTMLDocument dom对象上是时,小程序编辑器左侧的视图区域变高亮了,这和我们平时开发调试网页很相似,然后展开整个html文档发现其实和普通的html没什么区别。

先展开head标签,里面引入了很多script标签,却没有一个style标签,从上往下依次为:

  • beforebaselibready.js    
  • apihooks.js
  • WASubContext.js
  • app-server.js
  • wx.config.js
  • ....
  • wxml.js
  • beforemaincoderready.js
  • /common/main.js           -------公共js代码
  • /components/aaaa.js     -------组件代码
  • /components/bbbb.js
  • /pages/index/index.js    -------页面代码
  • /pages/english/english.js
  • /pages/user/user.js
  • .....

从js的命名可以大致的知道,这个document首先执行了before-base-lib-ready这个钩子,然后为当前页面预置了运行环境apihooks,WASubContext,app-server等,wxml.js是与页面渲染相关的代码,最后加载的是所有业务ja代码,

再展开body标签,body内部比较简单,值嵌入了script标签,执行了window.parent中的方法,

window.parent.__subcontext_ready_to_evaluate__(location.search.match(/id=(.*?)(&|#|$)/)[1])

这个html中加载了几乎小程序所有页面&组件的js代码,并且没有style标签,body也是空空的,感觉就像是执行js的容器,其实在早些版本的时候,这个document.head中有一个title标签,里面写的是页面标题“小程序逻辑层”,只不过现在展开这个document标签发现已经被移除了。这里实际上就是小程序模拟器的逻辑层,所有与业务相关的网络请求、业务逻辑都在这执行。

渲染层

接着往下深入,我们点击电脑左上角 微信开发者工具 →  调试 → 调试微信开发者工具

e.png

f.png

接着会弹出一个DevTools程序,这个控制面板就和chrome的调试控制面板一模一样,我们先查看element 面板,发现里面的dom元素分别 对应的是微信开发者工具不同的开发模块。既然微信官方文档说了小程序中视图层是webview渲染的,那我们到console面板中输入document.getElementByTagName('webview'),查看一下:

g.png

发现一共打印出了7个webview标签,有部分是渲染小程序页面的,有部分是渲染开发者工具的。点击小程序视图高亮的webview标签,跳转到Element面板,发现在视图层区域div标签存在一些规律: 11.png 现在小程序所在的页面是音乐播放详情页,页面深度为3(首页->播放列表页->播放详情页),每一个页面由一个div.navigator标签和一个webview组成。上面截图中一共有3个webview组件。小程序中有时会定制页面header部分,class为navigator的这个div标签应该是实现导航栏功能的。下面的webview是一个web-component,有两个属性分别为src="http://127.0.0.1:63759/__pageframe__/pages/index/index"和route="pages/index/index",对应着不同的页面。展开此标签有个iframe子元素,由于shadow-root影子dom为close状态,我们展示无法看到内部实现细节。微信开发者工具模拟器将每个page页面渲染到不同的webview标签中,每个webview内部则是通过iframe实现的,当页面被激活时,通过控制dom的z-index和visibility等css属性达到视图的显示,页面栈(getCurrentpages)中有几个页面,则会生成多少个webview标签,每一个页面都是一个webview单独渲染的,这也就是为什么[小程序页面栈深度为10](developers.weixin.qq.com/miniprogram…

值得注意的是,微信开发者工具里的webview是自定义的web-component,是一个网页通用组件,在ios和安卓中则是使用的原生的webview渲染每个page页面,小程序的页面由微信页面栈统一管理。虽然小程序开发者工具和原生App环境有些不同,但是不影响我们学习了解小程序原理这个过程。

上面我们分析了小程序页面是如何渲染的,接下来我们看一下小程序的“wxml”标签是如何渲染成页面的。

标签渲染

我们将小程序返回到首页,使得页面栈中只有首页,然后在小程序开发者工具的DevTools中输入document.getElementByTagName('webview')[0].showDevtools(true); 虽然webview的shadow-root为false,但是可以开启调试工具呀......于是我们又又又打开了一个调试工具,这个工具是当前激活的页面(pages/index/index)的调试工具。

13.png 从上面动图可以看到,新打开的devtool页面标题为“小程序渲染层” ,body中的dom元素则是常规的div、span标签,而wx-view、wx-image、wx-text、wx-button这种非标准的标签在html5中也不会报错,通过css样式对这些标签进行美化后也跟div没什么区别。如果你自己观察dom元素会发现它们都有一个特征,

14.png

每一个<wx-**>开头的标签都有一个属性exparser:info-attr-id=1、2、3.....,嗯,这跟Virtual DOM中的key很相似呀,我们到page/index/index页面的调试工具console面板中找一下他们的关联,

15.png

可以看到这里输出的就是virtual dom呀,最外层是一个wx-page标签,内部包裹着其他的children,这个info-attr-id就是vdom深度优先遍历生成的id。看到这里不免联想到现在流行的uni-app、taro等框架也是基于vdom重写编译器实现跨端的。

原理总结

最后我们再屡屡,小程序的渲染层和逻辑层分别由2个线程管理:\

  • 渲染层:界面渲染相关的任务全都在WebView里执行。一个小程序存在多个界面,所以渲染层存在多个WebView线程。
  • 逻辑层:采用JsCore线程运行JS脚本

视图层和逻辑层通过系统层的WeixinJsBridge进行通信:逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。

页面渲染的具体流程是:在渲染层,宿主环境会把WXML转化成对应的JS对象(也就是vdom),在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异diff,把差异应用在原来的DOM树上,渲染出正确的UI界面。

16.png

小程序为什么使用双线程

我想很多人在看到官方文档给出的小程序原理描述后都会产生这个疑问,至于这个原因小程序官网也有解释,一方面是为了解决h5泛滥问题,另外一方面为了性能和交互上的提升,这里就不再多唠叨了。