[TOC]

与普通网页的不同

在小程序未诞生前,我们在微信端打开网页都是什么体验:加载缓慢、跳转二次加载、内容粗制滥造、网页安全性未知、上下滑动体验差、风格各式各样熟悉成本高,无法保留使用痕迹,无统一入口

不只是微信打开网页出现这些问题,任何移动端浏览器打开网页,体验都是非常糟糕的,同时也苦了广大的前端开发者,为了各种机型、尺寸要做各种兼容和响应式,尽管做了很多努力,还是没办法让用户满意,小程序的出现就是为了解决了这些痛点,下面我们分析小程序相比网页的优势:

加载更快,可离线打开

小程序在不分包的情况下,最大限制是 2M,普通复杂度的小程序是难以达到 2MB 体积的,加上微信客户端在加载小程序包时给出的 loading 动效(不至于网页加载时直接白屏),给到用户一种加载快的体验,并且微信对加载过的小程序包做了离线缓存的处理,因此在网络状况差、离线的环境下,也能打开小程序。

应用质量、安全性

小程序的发布首先要申请类目,这个类目是微信官方提供的,也就是你的小程序必须属于该类目下,如果不属于那么无法发布上线,类目申请完毕后,开发完成提交代码还需要内容审核,这一步主要检查小程序的功能是否符合类目要求、功能是否正常、页面样式是否美观、是否涉黄赌毒邪教等,保证小程序的用户体验与安全性。

更多的原生能力

这里指的原生能力,即原生的组件、功能,比如说地图组件,如果需要用网页实现地图组件,首先微信客户端分配给 webview 的计算资源就不多容易导致性能问题,另外加载一整个地图组件也是非常耗时间的,加之在体验本不好的网页容器中使用,用户体验会非常糟糕,而小程序中提供非常多种原生组件,性能和用户体验相比网页要好许多。除原生组件外,微信客户端为小程序提供了许多原生的能力,如存储、微信生态能力、访问设备权限等,能够实现更丰富的业务功能。

规范化的项目结构

一千个 H5 网页就有一千种 style,有的是用 SPA 技术实现的,跳转页面极快,但手指向右一滑,整个网页就没了,WTF?有的是 MPA 实现的,每次跳转都得白屏一会儿,这都是 5G 时代了,还让我等?还有甚者 SPA 与 MPA 混合开发,这个跳转体验不用我多说了吧,相信移动端网页体验多的同学一定能明白这些痛点。而小程序将所有的页面都打入包中,规范化了所有的跳转逻辑,并且对于首屏更是加入了 App 规范化的 tabbar,让用户有种使用小程序就是在使用原生应用的错觉

渲染层实现方案

在我们不做任何调研的情况下,仅凭直觉去猜测其渲染层实现方案,我们可能会想出以下几种方案:

转译为原生移动端代码

将开发者提交的代码编译为移动端原生代码,绑定在微信客户端,使用纯客户端原生技术渲染,打开小程序就像使用微信客户端的一个功能,这种方法类似于市面上多端统一的转译器框架。这显然是不行的,小程序的发版还必须得依靠微信的法办,并且假设有几千几万个小程序,那么微信客户端安装包大小是不可估量的。

纯网页实现

这种方式就像小程序新创了一个小程序前端框架,用小程序的写法来写网页,就像 React、Vue 等前端主流框架,只不过是将微信客户端的一些独有的能力(如微信支付、获取用户信息等)封装在小程序前端框架中,那么这又与普通的移动端网页有什么区别呢,无非是不需要自己引入微信的 JS-SDK 安装包,通过自由的 js 环境,我们完全能打破小程序的各种规范,这与推出小程序方案初衷不符。

Hybrid 混合

这种技术已经流行很多年了,客户端 App 提供接口,能够让网页调用客户端的能力,互相能够通信,这种技术被成为 Hybrid 混合开发,最常见的是运行在微信浏览器环境下的网页,能够调用微信原生的支付能力。这又回到了上面我们说的那种方法。显然,要实现小程序各种特性,我们不得不将这种方法演化地更为激进,也就是提供原生移动端应用更多的能力、更多的权限给到网页,并且对 webview 做进一步的约束、改造,实现小程序预期该有的功能。

实现方案分析

猜测的底层模型

上面我们分析得到,小程序的实现方案是 Hybrid 混合技术,小程序页面是微信客户端的 webview,通过客户端与 webview 之间的通信赋予小程序更多的原生组件、功能,即与普通的 Hybrid 混合开发没有很大的区别,业务代码运行在单个 webview 中,html、css、js 都是在这个 webview 中执行。这是我最开始以为的模型:
image.png
首先从最底部的小程序业务代码开始说起,这部分是我们按照小程序开发规范写出的代码,上传到微信后端经过编译,生成可以在小程序 webview 中运行的代码,微信客户端为小程序 webview 定制化了许多能力与组件,小程序 webview 通过 JS Bridge 进行调用、通信,这是我最开始猜测的模型。

但是仔细分析后,为什么微信要多此一举出一个小程序框架,开发者必须要按照这个框架的规则去编写代码,最终还是转译成运行在 webview 中的 js 代码,为什么不直接让开发者用更成熟的 React 、Vue 去写运行在 webview 中的代码?

其实猜测的这套模型是错误的,上面这套模型有个致命的缺点:安全与管控

如果把开发者写的代码直接放在小程序唯一的 webview 中运行,那么开发者可以通过 js 去操控一切,直接操作 dom 去更改任何预设组件的事件、样式,或者通过 标签去控制页面的跳转到任何页面,又或者通过 eval 来去动态加载任何 js 代码…,开发者脑洞是无穷的,你不知道开发者会把小程序玩成咋样,并且微信客户端开放了这么多能力给小程序,那么必然要保证用户的安全性,对开发者的代码必须要做到严防。

按照上面这一套模型,开发者的代码直接运行在小程序 webview 中,可以通过各种未知的操作调用到微信客户端给的能力,那么必然导致安全问题,你可能会说,不是进行了上传编译这个过程吗?没错,这一步确实能够检查出有安全问题的代码,但是没办法做到 100% 的过滤,可能升级个 webview 版本,又出现了新的漏洞可以钻,这就成了一个微信官方与开发者的持久攻防战了,并且大概率微信官方会输,导致重大的安全事故。

解决安全管控问题:双线程模型

是否有一种方法,能够解决上述的 JS自由 的问题,阻止开发者修改 dom 、任意跳转等行为?下文将会讲解小程序真实的架构模型:视图层与逻辑层隔离,双线程模型:
image.png

View 视图层:视图层并不只有一个 webview,小程序每次跳转或者切换 tabbar 都会新建一个 webview,视图层提供渲染页面与监听用户操作能力。

Service 逻辑层:单独创建的一个 webview ,但是只使用其 JS 引擎,在这里执行的都是小程序的业务代码,主要是执行各种生命周期函数和监听 View 视图层传过来的 Event,触发函数改变 Data,将新的 Data 传入 View 层,完成页面刷新。

Native 原生层:这一层可以说是视图层与逻辑层的承上启下的一层,其中包含了视图层与逻辑层的通信工具 WeixinJSBridge,又包含了为小程序定制的各种微信原生能力。

整个运行流程就是:逻辑层生命周期函数执行得到初始 Data —-> Data 传入视图层渲染页面 —-> 用户触发点击、输入等行为将 Event 数据传入逻辑层 —-> 逻辑层接收到 Event 数据触发函数生成新的 Data —-> 视图层接到新的 Data 更新视图。

了解整个流程后,我们回到双线程这个模型中,为什么说要创出一个独立的 JS 引擎来专门运行业务代码呢?这里有两个原因:

阻止开发者操作 Dom:上文提到操作 Dom 会导致很多不可预期的问题出现,如控制页面的跳转,操作 dom 改变小程序预设的组件的事件、样式,将开发者的业务代码放在一个无 html 只有 js 引擎的环境中,那么只能运行 js 代码,因为该 webview 的视图层是不对外展示的,所以怎么操作 dom 也无所谓。

配合小程序多 Page 运行:小程序每个 Page 都是一个单独的 webview ,如果说将每个页面的逻辑层都放在自己的 webview 中执行,那么每次新建一个 Page 相当于新建一个 js 执行环境,并且页面相互通信起来也是非常繁琐,单独的逻辑执行环境能够对 Page 统一管理,业务逻辑更内聚。

将开发者的业务代码放入单独的 JS 引擎中执行,只负责处理函数的执行产生新的 Data、按照小程序的 API 调用原生的能力、组件,无法触碰到 dom ,只能调用预设的 API,开发者的能力被限制住了,不能像在浏览器上一样自由地执行各种操作,也就避免了绝大部分安全问题。

不可避免的延迟

这种模型难道就是完美的吗?当然不是,双线程最大的问题是因为逻辑层与视图层隔离的,需要 WeixinJSBridge 中间层来进行通信,这个过程是有延迟的,会导致一些渲染延迟的问题出现,所以我们看到小程序中大部分 api 都是异步的。这个问题似乎没办法解决,开发者能做的就是更高效地执行 setData,避免 Data 密集的改变。

webview 组件系统

上文讲到视图层使用的是成熟的 webview 解决方案,那么是否和普通的 html 一样,使用 <div>、<a>、<span> 等这一套标签来进行布局与事件派发吗?假如直接使用 html 的能力,那么又回到之前说的管控问题,用户可以通过 标签来进行跳转,通过