浅谈微信小程序

1,071 阅读17分钟

由衷的欢迎各位老师/同学批评与指正~ 。◕‿◕。~ !!!

1、前言

加入团队以来,相当一部分的时间在做微信小程序的开发,需求都跟的特别紧、倒排期,没时间总结小程序的知识。本文主要浅显的总结了小程序的一些知识点。小程序迁移uni-app推荐 微信小程序转uni-app经验分享~

2、简介

微信小程序就是以微信为宿主的应用程序,是一种全新连接用户与服务的方式。得益于微信这个巨大的流量入口,小程序自发布而来飞速发展。凭借着轻量无需安装、体验优秀、方便快捷等各种优势,迅速的让用户接受了这种新的服务方式。小程序基于现有成熟的web技术,开发成本低、可快速迭代,依靠小程序的底层基础库,web开发者可以使用微信的原生功能提供强大的能力。小程序从架构层面有了很好的创新,同时也是前端的一个工程化范式。

2.1、发展史

小程序出现之前微信成为移动端web的重要入口,微信团队对内推出了JS API(WeixinJSBridge),用于解决web应用原生能力不足的问题,被外部人员发现并使用,逐渐成为微信网页中的标准。后续微信对外前后推出了JS-SDK、JS-SDK 的增强版本用于微信API的调用,JS-SDK 增强版本主要在前者的基础上加入”微信 Web 资源离线存储“。JS-SDK 并没有解决体验不良的问题,对应大型的web应用受限于移动设备性能和网络速度,移动端白屏会更加明显。为了解决白屏、页面切换的生硬和点击的迟滞感等问题,微信官方推出了微信小程序,并希望其具有如下能力:

  • 快速的加载
  • 更强大的能力
  • 原生的体验
  • 易用且安全的微信数据开放
  • 高效和简单的开发

2.2、技术选型

用什么技术来渲染小程序,涉及到开发者的学习成本,对小程序的推广和社区的发展都有很大的影响。当前界面的渲染一般有如下三种方式

  • 客户端原生渲染
  • web渲染
  • Hybrid渲染

通过以下几个方面分析,小程序采用哪种技术方案

  • 版本更新:Web 支持在线更新,Native 则需要打包到微信一起审核发布
  • 开发门槛:Web 门槛低,Native 也有像 RN 这样的框架支持
  • 体验:Native 体验比 Web 要好太多,Hybrid 在一定程度上比 Web 接近原生体验
  • 管控和安全:Web 可跳转或是改变页面内容,存在一些不可控因素和安全风险

使用客户端原生技术的缺点之一是需要和微信客户端一起发版。大量的小程序会导致微信程序包过大,管理成本、三方接入成本增高。小程序出现的原因之一就是解决以微信中的web应用的性能问题,小程序也应和web应用一样能够快速迭代,并且将资源放在云端,下载资源后动态执行渲染出小程序页面。

小程序采用了Hybrid 技术,基于当前成熟的web技术和宿主提供的丰富的客户端原生能力,用一种近似web的方式来开发,实现在线更新代码,降低了小程序的学习成本。同时引入由宿主渲染的原生组件,原生组件具有如下一些优点:

  • 扩展Web的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力。
  • 体验更好,同时也减轻WebView的渲染工作。比如像地图组件(map)这类较复杂的组件,其渲染工作不占用WebView线程,而交给更高效的客户端原生处理,同时也具有更好的性能。
  • 绕过setData、数据通信和重渲染流程,使渲染性能更好。比如像画布组件(canvas)可直接用一套丰富的绘图接口进行绘制。

2.3、管控与安全

采用web技术具有不可控因素和安全风险,Web技术非常开放灵活,开发者可以为所欲为。这种情况在小程序中是不允许的,不允许使用<iframe>、不允许<a>直接外跳到其他在线网页、不允许开发者触碰DOM、不允许改变界面上的任意内容、不允许使用某些未知的危险API等。基于一些列不可控因素,小程序采用双线程架构,将开发者的代码放入纯javascript解析执行环境,避免浏览器相关接口的调用。

2.4、VS 普通网页

微信小程序基于现有的web技术,主要开发语言是JS,对比普通网页具有很多区别

  • 网页的渲染线程和脚本线程是互斥,脚本运行可能导致页面失去响应,脚本线程具有完整的DOM和BOM对象。小程序的渲染线程和脚本线程分开的,缺少完整的浏览器对象,和NodeJS 环境也不相同。
  • 网页开发时主要使用浏览器,和辅助工具或者编辑器。小程序需要申请小程序帐号、安装小程序开发者工具、配置项目等。
  • 网页的宿主环境是各式的浏览器、webview。小程序的宿主环境是iOS 和 Android 的微信客户端,开发者工具。
  • 小程序是客户端原生技术与 Web 技术结合的混合技术(简称 Hybrid 技术)来渲染,可以使用微信提供的特定能力。

3、运行机制

3.1、启动方式

小程序启动有两种方式

App启动方式分三种:冷启动(cold start)、热启动(hot start)、温启动(warm start)

  • 冷启动: 首次打开,或小程序销毁后再次打开;
  • **热启动:**已经打开过小程序,并且并未被销毁;

3.2、销毁时机

通常,只有当小程序进入后台一定时间,或者系统资源占用过高,才会被销毁。

  • 当小程序进入后台,可以维持一小段时间的运行状态,如果这段时间内都未进入前台,小程序会被销毁。
  • 当小程序占用系统资源过高,可能会被系统销毁或被微信客户端主动回收。

3.3、启动流程

在小程序启动前,微信会为小程序准备好通用的运行环境,包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。不仅如此,微信还会提前准备好一个"页面层级"用于展示小程序的首页,每当一个页面层级被用于渲染页面,微信都会提前开始准备一个新的页面层级。页面层级准备过程如下:

页面层级.png

在小程序启动过程中,会尽可能的并行进行各启动流程,并且会尽可能的使用缓存。微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。此时,微信会在背后完成几项工作:下载小程序代码包、加载小程序代码包、初始化小程序首页。小程序启动流程如下:

启动流程.png

3.4、更新方式

未启动时更新

开发者在管理后台发布新版本的小程序之后,如果某个用户本地有小程序的历史版本,此时打开的可能还是旧版本。微信客户端会有若干个时机去检查本地缓存的小程序有没有更新版本,如果有则会静默更新到新版本。总的来说,开发者在后台发布新版本之后,无法立刻影响到所有现网用户,但最差情况下,也在发布之后 24 小时之内下发新版本信息到用户。用户下次打开时会先更新最新版本再打开。

启动时更新

如果需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理。

小程序每次冷启动时,都会检查是否有更新版本,如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。

4、底层原理

4.1、双线程模型

小程序相对传统web应用的单线程在架构上做了创新,采用了双线程模型。小程序的渲染层和逻辑层分别由两个线程管理:渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本。

双线程模型.jpeg

  • 逻辑层:一个单独的线程,执行小程序业务逻辑的代码,负责逻辑处理、数据请求、接口调用等。
  • 视图层:界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。
  • JSBridge: 小程序与宿主环境的桥梁,承担逻辑层与视图层的通信中转,逻辑层的网络请求转发,宿主环境元素能力调用等职责。

小程序在不同的微信客户端的运行环境如下:

运行环境逻辑层渲染层
iOSJavaScriptCoreWKWebView
安卓V8(X5 JSCore)chromium定制内核(X5浏览器)
小程序开发者工具NWJSChrome WebView

各个运行环境十分类似,但是任然有有差异,主要表现在JavaScript 语法和 API 支持不一致,WXSS 渲染表现不一致。

注意: 开发者工具只提供预览、调试使用,真实表现需要以实际客户端为主。

4.2、通信模型

一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端做中转,逻辑层发送网络请求也经由微信客户端转发,小程序的通信模型下图所示。

通信模型.png

视图层组件

内置组件中有部分组件是利用到客户端原生提供的能力,会涉及到视图层与客户端的交互通信,该通信机制在 iOS 和安卓系统的实现方式并不一样。

  • iOS 是利用了WKWebView 的提供 messageHandlers 特性;
  • 安卓则是往 WebView 的 window 对象注入一个原生方法;

将两个实现方式最终封装成 WeiXinJSBridge 这样一个兼容层,主要提供了调用(invoke)和监听(on)两种方法。

通常在视图层与客户端的通信中,开发者只是间接调用的,真正调用是在组件的内部实现中。开发者插入一个原生组件,一般而言,组件运行的时候被插入到 DOM 树中,会调用客户端接口,通知客户端在哪个位置渲染一块原生界面。

逻辑层接口

逻辑层与客户端原生通信机制与渲染层类似,不同在于,iOS平台可以往JavaScripCore框架注入一个全局的原生方法,而安卓方面则是跟渲染层一致的。

同样地,开发者也是间接地调用到与客户端原生通信的底层接口。一般我们会对逻辑层接口做层封装后才暴露给开发者,封装的细节可能是统一入参、做些参数校验、兼容各平台或版本问题等等。

4.3、数据驱动

WXML结构实际上等价于一棵Dom树,通过一个JS对象也可以来表达Dom树的结构,如下图

domtree.png

在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面

数据驱动.png

4.4、组件系统

Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。

Exparser的组件模型与WebComponents标准中的ShadowDOM高度相似。Exparser会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的Shadow DOM实现。Exparser的主要特点包括以下几点:

  1. 基于Shadow DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
  2. 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
  3. 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。

在Exparser的组件模型中,组件的节点树称为“ShadowTree”,即组件内部的实现;最终拼接成的页面节点树被称为“Composed Tree”,即将页面所有组件节点树合成之后的树。在进行了这样的组件分离之后,整个页面节点树实质上被拆分成了若干个ShadowTree(页面的body实质上也是一个组件,因而也是一个ShadowTree)。各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、setData调用,createSelectorQuery也将运行在Shadow Tree的层面上。

4.5、原生组件

并不完全在Exparser的渲染体系下,而是由客户端原生参与组件的渲染

渲染机制

原生组件在webview中只渲染一个占位元素,可以理解成div。小程序渲染元素组件步骤如下:

  1. 组件被创建,包括组件属性会依次赋值。
  2. 组件被插入到DOM树里,浏览器内核会立即计算布局,此时我们可以读取出组件相对页面的位置(x, y坐标)、宽高。
  3. 组件通知客户端,客户端在相同的位置上,根据宽高插入一块原生区域,之后客户端就在这块区域渲染界面。
  4. 当位置或宽高发生变化时,组件会通知客户端做相应的调整。

渲染限制

通过上述渲染机制可知,css样式无法应用于元素组件之上,且原生组件的层级比非原生组件都高,且不能被 z-index 改变。可使用cover-viewcover-image组件覆盖原生组件,这两个组件同样是原生组件。

5、基础库

小程序基础库版本号使用 semver 规范,格式为 Major.Minor.Patch

小程序的基础库可以被注入到视图层和逻辑层运行,主要用于以下几个方面:

  • 在视图层,提供各类组建界面的组件
  • 在逻辑层,提供各类 API 来处理各种逻辑
  • 处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑

由于小程序的渲染层和逻辑层是两个线程管理,分为WebView基础库和AppService基础库,两个线程各自注入了基础库。小程序的基础库不会被打包在某个小程序的代码包里边,它会被提前内置在微信客户端,作用如下:

  • 降低业务小程序的代码包大小
  • 可以单独修复基础库中的 Bug,无需修改到业务小程序的代码包

6、性能优化

6.1、启动加载性能

控制包大小

  • 精简代码,去掉不必要的WXML结构和未使用的WXSS定义。
  • 减少在代码包中直接嵌入的资源文件。
  • 压缩图片,使用适当的图片格式。
  • 将静态文件放在cdn,如iconfont 文件。

如果小程序比较复杂,优化后的代码总量可能仍然比较大,此时可以采用分包加载的方式进行优化。

采用分包加载机制

在小程序启动时,子包不会马上被下载下来,只有主包的内容才会被下载。利用这个特性就可以显著降低初始启动时的下载时间。 使用分包时需要注意代码和资源文件目录的划分。启动时需要访问的页面及其依赖的资源文件应放在主包中。

采用分包预加载技术

在“分包加载机制”的基础上,用户点击分包所在的页面,会有一个分包下载的过程,此时可采取两种方法:

  • 减小分包大小
  • 根据用户可能进入的页面,预先加载分包

采用独立分包技术

小程序的部分页面不强依赖于主包,但不独立于项目。此时可以采用独立分包,在访问相关页面时只需要下载独立分包资源的大小。

首屏加载的优化

  • 提前请求
  • 利用缓存
  • 避免白屏
  • 及时反馈

6.2、数据通信性能

页面初始数据通信

初始渲染时间大致由页面初始数据通信时间初始渲染时间两部分构成,因而减少初始数据传输量也是有效的优化方式。

更新数据通信

每次调用setData逻辑层会执行一次JSON.stringify来去除掉setData数据中不可传输的部分,然后传输至渲染层。使用setData调用时,最好遵循以下原则:

  • 不要频繁调用setData,应考虑将多次setData合并成一次setData调用。
  • 数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用setData来设置这些数据。
  • 与界面渲染无关的数据考虑设置在page对象的其他字段下。

用户事件通信

  • 去掉不必要的事件绑定(WXML中的bindcatch),从而减少通信的数据量和次数。
  • 事件绑定时需要传输targetcurrentTargetdataset,因而不要在节点的data前缀属性中放置过大的数据。
  • 避免不当的使用onPageScroll。

6.3、渲染性能

优化视图节点

首次渲染的时间开销大体上与节点树中节点的总量成正比例关系,减少WXML中的节点,可以有效降低初始渲染和重渲染的时间开销,提升渲染性能。

使用自定义组件

自定义组件的更新只在组件内部进行,不受页面其他不能分内容的影响,同时组件的更新并不会影响页面上其他元素的更新;各个组件具有独立的逻辑空间和setData调用。在频繁更新的场景下,可使用自定义组件来优化渲染性能。

7、参考