前言
- 从第一次接触前端参加比赛,到第一次在微信实习,到如今在蚂蚁金服体验技术部的实习业务开发,小程序似乎是陪伴我最久的业务形式了。在完成项目的过程中大部分时间都在埋头赶业务,这次决定好好梳理一下对于小程序的认知。
- 笔者不敢保证言论完全正确,希望有建议和不同意见的同学可以补充我的总结。
- 希望这篇文章能给对小程序没有一个整体认知的同学一些启发。
从一个奇妙比喻开始
摘自阿里巴巴玖五同学的github 他的博客让我对小程序的设计和架构有了一个非常直观和感性的认识。
- “采实现多UI线程并不复杂,如果宿主环境是浏览器,则可以在页面中使用多个iframe叠在一起,每当跳转页面时,创建一个新的iframe盖在最上面,当回退时,把最上层的iframe删除即可。如果宿主环境是手机上的超级APP,则把iframe改成WebView,套路都是一样的。”
这种比喻让我豁然开朗,同时也对这种研究思路和过程产生了不小的兴趣。
为什么需要小程序
根据历史经验来看,传统的 Web 开发有以下的优缺点:
| 优点 | 缺点 |
|---|---|
| 开发快速 | 开放环境内有一些安全问题 |
| 发布不敏感,随时更新随时发布 | 复杂交互场景渲染性能差(JS脚本与UI渲染在统一线程互斥) |
而使用native开发,也有以下的优缺点:
| 优点 | 缺点 |
|---|---|
| 性能很好 | 发布流程长;无法动态打包,动态下发 |
综上:小程序要解决的核心问题有以下几个
- 动态
- 安全
- 性能
抛出结论
绝大多数小程序都是通过将UI与逻辑解耦,建立逻辑层、UI层的多线程模型进行实现的。
下边依次解释小程序是如何解决核心问题的:
动态
解决动态下发的问题,最简单直接的方式就是采用web开发模式,这也是目前的主流方式。 即通过webView的方式,在端内渲染前端UI页面。
安全
-
html内的安全性如何保证?
html的安全性主要体现在<iframe /><a />标签等自带默认行为的标签上。小程序希望这些标签被禁用。所以在微信小程序和支付宝小程序中,都各自实现了一套类html语法。微信小程序:wxml;支付宝小程序:axml。类html语法在经过编译之后最终都会生成html。 -
如何防止操作dom,调用浏览器api等不安全操作?
由于原生web开发过于开放,无论是各种BOM,DOM接口,还是cookie等各种自带功能,都导致了环境的过于开发和自由。著名的xss攻击也就是由于浏览器的开放性导致的。在小程序中,考虑到安全性,需要提供一个干净、纯粹、低风险的js运行环境。于是小程序的架构中,将JS的执行从webview中抽离了出来,而选择使用客户端自带的JS引擎。
客户端的js引擎:- 在ios中:可以使用自带的JavaScriptCore引擎;
- 在安卓中:微信小程序选用腾讯x5内核提供的JsCore环境;支付宝小程序选用V8 / UC内核提供的serviceWorker
性能
从上文可以知道,微信和支付宝一致采取了将JS的脚本执行从webview中剥离出来的策略。其实在保证安全性的同时,也将复杂交互性能差的问题一并解决了。
在这种架构下,webview专门负责UI的渲染,也可以叫渲染层;而客户端为js提供执行环境,可以叫逻辑层。
- 逻辑层和渲染层分别工作在不同的线程中,这样避免了逻辑和渲染UI互相抢占线程、争夺资源的现象。
- 不仅减少了单线程的心智负担,同时使得性能更好了。
一个插曲
在学习的过程中也发现了一篇有意思的博客# Figma 团队分享其插件系统的架构设计
虽然不是做小程序的架构实现,但与小程序诞生过程中的思考有异曲同工之妙。
无论是Figma插件系统,还是小程序的架构,本质上其实都是为了解决安全和性能问题。二者最终的方案都敲定在给出一个纯粹、安全的JS执行环境,这大概是当下的最佳实践了~
至关重要的容器
JSBridge
关于JSBridge的定义以及实现方式可以参考我之前的博客 # JSBridge的重点解析
简而言之,JSB给予了H5页面、小程序等前端环境调用客户端的能力。
典型代表 :蚂蚁金服 支付宝H5开放文档。
文档摘录:JSAPI是支付宝客户端通过bridge机制,允许前端H5页面通过特定的JS方法,可以直接调用对应的支付、拍照、分享、弹出浮层之类的Native功能。
总结:
- JSB的特点:能够使得js环境获取native的丰富能力。
- JSB的限制:只有端内才可以调起。
UI与data通信
由于UI和数据在小程序的架构中不再由统一线程管控,而是被分到了不同的线程中,那么UI与data的通信也是需要特别设计的地方。
以支付宝小程序为例:支付宝小程序采用了message Channel做消息转发的方式,具体过程可以参考如下:
- 数据从逻辑层传递到客户端的messageChnnel
- 客户端将数据传递到webview。
- webview 上根据传过来的数据构造 Virtual-DOM,并与之前做diff并渲染。
由于逻辑层与webview通信时,数据需要序列化,然后到了 webview 需要执行 evaluateJavascript。
因此如果一次性传输数据太大,会影响首屏渲染性能,这也解释了为什么不鼓励setData传递数据量过大。
原生组件
在传统的 H5 开发中,输入框、地图等组件在移动端体验非常差,输入框无法自定义的控制键盘,地图组件渲染卡顿。为了保证小程序的用户体验,原生组件的概念出现了。
-
所谓原生组件就是:非 Web 渲染而由客户端渲染的组件。
-
以微信小程序为例:
WebView 的 window 对象注入一个原生方法,最终会封装成 WeiXinJSBridge 这样一个兼容层,主要提供了调用(invoke)和监听(on)这两种方法。开发者插入一个原生组件,一般而言,组件运行的时候被插入到 DOM 树中,会调用客户端接口,通知客户端在哪个位置渲染一块原生界面。在后续开发者更新组件属性时,同样地,也会调用客户端提供的更新接口来更新原生界面的某些部分。
支付宝小程序的实现方式不外如是。
原生组件的优点是:性能好且不阻塞webview渲染线程;但同时存在缺点:必在顶层显示,z-index是无效的。
为了解决这样的问题,各小程序厂商开发了cover-view cover-image等一系列组件。