php入门到就业线上直播课:进入学习
一、探讨一下双线程架构
对微信小程序底层设计的疑问
- 双线程模型到底是什么?
- 如何将视图和逻辑分开?
- 为什么要用 WXML、WXSS 这种奇怪的语法?
- 如何屏蔽掉危险操作 API?
- 如何进行通信,数据驱动视图更新如何实现?
- 双线程模型有何缺点?
1、模型组成
1)、传统浏览器进程、线程模型存在的问题
性能问题
渲染线程与 JS 引擎线程是互斥的,存在阻塞问题,长时间的 JS 脚本执行直接会影响到 UI 视图的渲染,导致页面卡顿
安全问题
危险的 HTML 标签(a、script)和 JS API(Function、eval、DOM API),攻击者很容易对页面内容进行篡改,非法获取到用户信息等
解决方案
小程序采用了双线程模型,将视图展示和逻辑处理分开在两个线程中执行
模型中对 HTML 标签进行了封装,开发者无法直接调用原生标签,JS 中原有的危险逻辑操作 API 能力也被屏蔽,涉及到的通信动作只能通过中间层提供的 API 去触发虚拟 DOM 更新(数据驱动视图)
2)、小程序双线程模型
其中视图层及逻辑层的能力都是通过微信客户端内置的小程序基础库 JS 文件来提供的
WAWebview.js 提供视图层基础的 API 能力,包括一整套组件系统及 Virtual Dom(虚拟DOM)的相关实现
WAService.js 提供逻辑层基础的 API 能力,向开发者暴露各类操作的 API
3)、客户端引擎底层实现
不同运行环境下,脚本执行环境以及用于组件渲染的环境是不同的,性能表现也存在差异:
在 iOS、iPadOS 和 Mac OS 上,小程序逻辑层的 JavaScript 代码运行在 JavaScriptCore 中,视图层是由 WKWebView 来渲染的,环境有 iOS 14、iPad OS 14、Mac OS 11.4 等
在 Android 上,小程序逻辑层的 JavaScript 代码运行在 V8 中,视图层是由基于 Mobile Chromium 内核的微信自研 XWeb 引擎来渲染的(从过去的 X5 换成了自研的 XWeb 引擎,视图层是基于 Mobile Chrome 内核来渲染的,而且根据官方披露,将来逻辑层也会脱离 V8,使用定制化的 XWeb Worker 线程,即自定义一个 Web Worker 线程,作为小程序的逻辑层。参考链接)
在 Windows 上,小程序逻辑层 JavaScript 和视图层都是用 Chromium 内核
在 开发工具上,小程序逻辑层的 JavaScript 代码是运行在 NW.js 中,视图层是由 Chromium WebView 来渲染的
2、视图层
1)、小程序组件系统
小程序中会涉及到的组件共有三种类型:内置组件、自定义组件、原生组件
2)、小程序组件组织框架
1、Web Component
Web Component 是浏览器中创建封装功能和定制元素的能力,由三项主要技术组成
Custom elements(自定义元素)一组 JavaScript API,允许定义 custom elements 及其行为,然后可以在用户界面中按照需要使用它们
Shadow DOM(影子DOM)一组 JavaScript API,用于将封装的“影子” DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突
HTML templates(HTML模板)
template 和 slot 元素可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用
2、Exparser 框架
Exparser 是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持 小程序内的所有组件,包括内置组件和自定义组件,都由 Exparser 组织管理
小程序中,所有节点树相关的操作都依赖于 Exparser,包括 WXML 到页面最终节点树的构建、 createSelectorQuery 调用和自定义组件特性等
Exparser 会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的 Shadow DOM 实现
3、Exparser 特点
基于 Shadow DOM 模型:模型上与 Web Component 的 Shadow DOM 高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他 API 以支持小程序组件编程
可在纯 JS 环境中运行:这意味着逻辑层也具有一定的组件树组织能力(在组件的渲染中就会用到)
高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小
4、Exparser 核心方法
registerBehavior 注册组件的一些基础行为,供组件继承
registerElement 注册组件,跟我们交互接口主要是属性和事件
组件创建
小程序基础库对外提供有 Page 和 Component 两个构造器,小程序启动将 properties、data、methods 等定义字段,写入 Exparser 的组件注册表,在初始化页面时, Exparser 会创建出页面根组件的一个实例,用到的其他组件也会相应创建组件实例(这是一个递归的过程)
5、Page 和 Component 的创建过程
从上图中我们能够比较明显的看到小程序在 Page 渲染和 Component 渲染时通信方式上存在差别。其中,Page 渲染中 VDOM 的生成和 diff 都是在视图层完成的,逻辑层只负责对 data 数据的发送,来触发渲染层的更新逻辑。而在 Component 的渲染中,逻辑层和视图层需要共同维护一套 VDOM,方式则是在组件初始化时在逻辑层构建组件的 VDOM,然后将其同步到视图层。后续的更新操作则会先在逻辑层进行新旧 VDOM 的 diff,然后仅将 diff 之后的结果进行通信,传递到视图层之后直接进行 VDOM 的更新和渲染。这样做最大的好处就是将视图更新通信的粒度控制成 DOM 级别,只有最终发生改变的 DOM 才会被更新过去(因为有时候 data 的改变并不一定会带来视图的更新),相对于之前 data 级别的更新会更加精准,避免非必要的通信成本,性能更好。
3)、小程序原生组件
1、原生组件的优点
扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力。
体验更好,同时也减轻 WebView 的渲染工作。比如像地图组件(map)这类较复杂的组件,其渲染工作不占用 WebView 线程,而交给更高效的客户端原生处理。
渲染性能更好,绕过 setData、数据通信和重渲染流程,比如像画布组件(canvas)可直接用一套丰富的绘图接口进行绘制。
2、原生组件创建过程
组件被创建后插入到 DOM 树里,渲染一个占位元素,浏览器内核会立即计算布局,此时我们可以读取出组件相对页面的位置(x, y 坐标)、宽高
组件通知客户端,客户端在相同的位置上,根据宽高插入一块原生区域,之后客户端就在这块区域渲染界面
当位置或宽高发生变化时,组件会通知客户端做相应的调整
3、原生组件渲染限制
一些 CSS 样式无法应用于原生组件
层级问题(原生组件层级最高,会覆盖 WebView 层的组件)
解决方式
使用 cover-view 和 cover-image 组件覆盖原生组件(能力有限,不灵活)
同层渲染(通过一定的技术手段把原生组件直接渲染到 WebView 同一层级上)
4)、原生组件同层渲染
1、iOS 系统实现原理
小程序 iOS 端的同层渲染是基于 WKChildScrollView 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 WKChildScrollView 容器下
原理分析
WKWebView 在内部采用的是分层的方式进行渲染,WKWebView 会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView 组件,但合成层与 DOM 节点之间不存在一对一的映射关系(内核一般会将多个 DOM 节点合并在一个合成层中)。
WKChildScrollView:当把一个 DOM 节点的 CSS 属性设置为 overflow: scroll (低版本需同时设置 -webkit-overflow-scrolling: touch)之后,WKWebView 会为其生成一个 WKChildScrollView(为了可以让 iOS 上的 WebView 滚动有更流畅的体验,WebView 里的滚动实际上是由真正的原生滚动组件来承载的,WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系),并且 WKChildScrollView 与 DOM 节点存在一对一映射的关系。
创建流程
创建一个 DOM 节点并设置其 CSS 属性为 overflow: scroll 且 -webkit-overflow-scrolling: touch
通知客户端查找到该 DOM 节点对应的原生 WKChildScrollView 组件
将原生组件挂载到该 WKChildScrollView 节点上作为其子 View
通过上述流程,小程序的原生组件就被插入到 WKChildScrollView 了
2、Android 系统实现原理
WebPlugin 是浏览器内核的一个插件机制,小程序 Android 端的同层渲染是基于 embed 标签和 WebPlugin 实现的
原理分析
embed 标签是 HTML5 新增的标签,是使用来定义嵌入的内容(如插件、媒体等),格式可以是 PDF、Midi、Wav、MP3 等等,比如 Chrome 浏览器上的 PDF 预览,就是基于 embed 标签实现的。
基于 embed 标签结合 Chromium 内核扩展来实现,Chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述 embed 标签,将原生组件渲染到内核提供的 Texture 纹理上
创建流程
WebView 侧创建一个 embed DOM 节点并指定组件类型
Chromium 内核会创建一个 WebPlugin 实例,并生成一个 RenderLayer
Android 客户端初始化一个对应的原生组件
Android 客户端将原生组件的画面绘制到步骤 2 创建的 RenderLayer 所绑定的 SurfaceTexture 上
通知 Chromium 内核渲染该 RenderLayer
Chromium 渲染该 embed 节点并上屏
5)、视图层代码编译
微信开发者工具内置了二进制的 WXML 和 WXSS 代码文件编译器,编译器接受 WXML 和 WXSS 代码文件列表,处理完成之后输出 JavaScript 代码
1、WXSS 代码文件
wcsc 命令工具执行过程
wcsc 编译 WXSS 生成一个 JS 文件产物
JS 文件包含添加尺寸单位 rpx 转换方法,可根据设备屏幕宽度自适应计算成 px 单位
提供 setCssToHead 方法将转换后的 css 内容添加到 html 模版的 head 中
eval 方法执行这个 JS 文件字符串完成样式注入
2、WXML 代码文件
WCC 命令工具执行过程
wcc 编译 WXML 生成一个 JS 文件产物
JS 文件包含 $gwx 方法,接收 WXML 文件路径,执行后生成 generateFunc 方法,并触发 generateFuncReady 事件
generateFunc 方法接受动态数据 data,类似于一个 render 函数,用于生成 virtual dom
3、逻辑层
1)、逻辑层包含内容
提供 JS 代码执行环境
为 window 对象添加模块接口 require define
提供 Page,App,getApp 等接口
提供 wx 全局对象下面的 api 方法,网络、媒体、文件、数据缓存、位置、设备、界面、界面节点信息还有一些特殊的开放接口
2)、JS 代码执行环境
1、JSVirtualMachine
它通过实例化一个 VM 环境来执行 JS 代码,如果你有多个 JS 需要执行,就需要实例化多个 VM。并且需要注意这几个 VM 之间是不能相互交互的,因为容易出现 GC 问题
2、JSContext
JSContext 是 JS 代码执行的上下文对象,相当于一个 Webview 中的 window 对象。在同一个 VM 中,你可以传递不同的 Context
3、JSValue
和 WASM 类似,JsValue 主要就是为了解决 JS 数据类型和 Swift 或 Java 数据类型之间的相互映射。也就是说任何挂载在 JSContext 的内容都是 JSValue 类型,Swift 和 Java 在内部自动实现了和 JS 之间的类型转换
4、JSExport
是 JSCore 里面,用来暴露 Native 接口的一个 protocol。简单来说,它会直接将 Native 的相关属性和方法,直接转换成 prototype object 上的方法和属性
3、模块规范的实现
通过 define 定义一个模块,限制了模块可使用的其他模块,如 window,document 等,define 方法则是通过将所有的 JS 代码逻辑都以“路径-模块”的键值对的方式存在了全局变量里实现的
使用 require 来应用一个模块,使用模块时只会传入 require 和 module、exports,代码中读取到的其他变量就会都是 undefined,这也是不能在小程序中获取一些浏览器环境对象的原因
这其实就是典型的模块加载思路,与 Webpack 打包模块的处理方式十分相似
4、通信原理
WAWebview.js 和 WAService.js 文件中除了提供视图层、逻辑层基础的 API 能力之外,还提供了这两个线程之间进行通信的能力
小程序逻辑层和渲染层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发
1)、底层实现
几个端的不同实现最终会封装成 WeiXinJSBridge 这样一个兼容层供给开发者调用
1、iOS 端
视图层是通过 WKWebView 的 window.webkit.messageHandlers.NAME.postMessage 和 eval 实现。逻辑层是往 JavaScripCore 框架注入一个全局的原生方法
2、Android 端
视图层和逻辑层的实现原理一致,都是往 WebView 的 window 对象注入一个原生方法 WeixinJSCore 实现,这个 WeixinJSCore 是微信提供给 JS 调用的接口(native实现)
3、微信开发者工具
使用 websocket 进行通信
2)、WeixinJSBridge 模块对象
WeixinJSBridge提供了视图层 JS 与 Native、视图层与逻辑层之间消息通信的机制,提供了如下几个方法:
invoke:JS 调用 Native API
on:JS 监听 Native 消息
publish:视图层发布消息
subscribe:订阅逻辑层的消息
5、双线程架构的缺陷和优化
1)、双线程架构的缺陷
不能灵活操作 DOM,无法实现较为复杂的效果
部分和原生组件相关的视图有使用限制,如微信的 scrollView 内不能有 textarea
页面大小、打开页面数量都受到限制
需要单独开发适配,不能复用现有代码资源
在 JSCore 中 JS 体积比较大的情况下,其初始化时间会产生影响
传输数据中,序列化和反序列化耗时需要考虑
2)、双线程架构的优化
双线程带来的性能瓶颈,也是微信自身一直致力于解决的关键问题。前面提到小程序将会在新的 XWeb 内核里,将逻辑层的实现替换为通过修改 Chromium 内核,实现自定义的 XWeb Worker 线程,这样就不再需要额外的 V8 了,使得内存占用方面有显著减少。另外,由于是基于 Chromium 内核的 Worker 线程,所以关于数据通信这块,很自然的也会拥有 PostMessage 的能力,来替代原有的 setData 底层通信方式,获得更高性能的通信能力
3)、支付宝小程序的双线程架构
另外,我还了解研究了支付宝小程序底层的相关实现。支付宝小程序也使用了类似的双线程架构模型,通过使用 UC 提供的浏览器内核,渲染层是在 Webview 线程中运行,逻辑层则是另起一个线程运行 Service Worker。但是 Service Worker 需要通过 MessageChannel API 来与 渲染线程通信,当数据量较大、对象较复杂时同样存在性能瓶颈。
因此支付宝小程序重新设计了现有的 JS 虚拟机 V8,提出了一种优化的隔离模型(Optimized isolation model, OIM)。OIM 的主要思路是共享 JS 虚拟机实例中与线程执行环境无关的数据和基础设施,以及不可变或不易变的 JS 对象,使得在保持 JS 层逻辑隔离的前提下,节省多实例场景下在内存和功耗上的开销。尽管有些实例间共享的数据会带来同步的开销,但是在隔离模型下,本方案所共享的数据、对象、代码和虚拟机基础设施都是不可变或者不易变的,所以很少发生竞争。
在新的隔离模型下,Webview 里面的 V8 实例就是一个 Local Runtime,Worker 线程里面的 V8 实例也是一个 Local Runtime,在逻辑层和渲染层交互时,setData 对象的会直接创建在 Shared Heap 里面,因此渲染层的 Local Runtime 可以直接读到该对象,并且用于渲染层的渲染,减少了对象的序列化和网络传输,极大的提升了启动性能和渲染性能。
除此之外,支付宝小程序实现了首页离线缓存优化,首先渲染上次保存下面的首页 UI 页面,把首页展现给用户,然后在后台继续加载前端框架和业务的代码,加载完成后再和离线缓存的首页 UI 进行合并,给用户展现动态的首页,这一点和微信小程序的初始渲染缓存方案十分类似且更加激进。还使用了 WebAssembly 技术重新实现了虚拟 DOM 这块的核心代码,提升了小程序的页面渲染。【相关视频教程推荐:web前端】
6、对双线程模型架构设计的思考
技术架构为解决问题而生,一个好的技术方案不仅需要设计者在开发效率、技术成本、性能体验、系统安全之间做出平衡和取舍,还需要与业务走向、产品形态、用户诉求紧密结合
二、运行时机制一探究竟
1、代码包下载
在小程序启动(冷启动)时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标
此时,微信会在背后完成几项工作:下载小程序代码包、加载小程序代码包、初始化小程序首页
从开发者的角度看,控制代码包大小有助于减少小程序的启动时间。对低于 1MB 的代码包,其下载时间可以控制在 929ms(iOS)、1500ms(Android) 内
优化方案
减小主包、分包大小
合理分配,分包预加载规则
精细拆分,异步化分包
2、页面层级准备
在小程序启动前,微信会提前准备好一个页面层级用于展示小程序的首页。每当一个页面层级被用于渲染页面,微信都会提前开始准备一个新的页面层级,使得每次调用 wx.navigateTo 都能够尽快展示一个新的页面。视图层页面内容都是通过 pageframe.html 模板来生成
页面层级的准备步骤
第一阶段是启动一个 WebView,在 iOS 和 Android 系统上,操作系统启动 WebView 都需要一小段时间
第二阶段是在 WebView 中初始化基础库,此时还会进行一些基础库内部优化,以提升页面渲染性能
第三阶段是注入小程序 WXML 结构和 WXSS 样式,使小程序能在接收到页面初始数据之后马上开始渲染页面(这一阶段无法在小程序启动前执行)
3、页面代码注入和执行
1)、初始化全局变量
视图层全局变量
- __wxConfig:是根据全局配置和页面配置生成的配置对象,包含 page、分包 page、tabbar 等信息
- __wxAppCode__:非JS类型的开发者代码
- JSON:JSON 配置文件内容
- WXML:$gwx 函数的执行结果,是一个可执行函数,执行后将会返回 VDOM 对象
- WXSS:eval 执行样式函数的结果,是一个可执行函数,执行后插入 style 标签
逻辑层全局变量
- __wxConfig:与视图层类似,不包含分包 page、tabbar 等信息
- __wxAppCode__:与视图层类似,不包含 WXSS 类型数据
- __wxRoute: 用于指向当前正在加载的页面路径
- __wxRouteBegin: 用于标志 Page 的正确注册
- __wxAppCurrentFile__: 用于指向当前正在加载的 JS 文件
- __wxAppData: 小程序每个页面的 data 域对象
- Component: 自定义组件构造器
- Behavior: 自定义组件 behavior 构造器
- definePlugin: 自定义插件的构造器
- global: 全局对象
- WeixinWorker: 多线程构造器
2)、基础库和业务代码JS文件注入
视图层代码注入
- 注入并执行 wxml.js 文件(定义 $gwx 方法)
- 注入并执行 wxss.js 文件(eval 方法,添加 app.wxss 样式文件)
- 添加 __wxAppCode__ 变量
- 执行 eval 方法,插入当前页面及引用组件样式文件
- 执行 $gwx(当前页面路径),返回对应的生成虚拟 DOM 的方法
逻辑层代码注入
- 注入并执行 JS 文件(顺序:其他、babel、Component、Page、App)
- 注入并执行wxml.js文件(定义$gwx方法)
按需注入和用时注入
通常情况下,在小程序启动时,启动页面所在分包和主包(独立分包除外)的所有 JS 代码会全部合并注入。自基础库版本 2.11.1 起,配置了 按需注入和用时注入 ,小程序仅注入当前页面需要的自定义组件和页面代码,在页面中必然不会用到的自定义组件不会被加载和初始化
3)、逻辑层业务代码执行
4、数据通信和视图渲染
1)、页面初始渲染
数据通信
在小程序启动或一个新的页面被打开时,页面的初始数据(data)和路径等相关信息会从逻辑层发送给视图层,用于视图层的初始渲染。
Native 层会将这些数据直接传递给视图层,同时向用户展示一个新的页面层级,视图层在这个页面层级上进行界面绘制。
视图层接收到相关数据后,根据页面路径来选择合适的 WXML 结构,WXML 结构与初始数据相结合,得到页面的第一次渲染结果
在完成视图层代码注入,并收到逻辑层发送的初始数据后,结合从初始数据和视图层得到的页面结构和样式信息,小程序框架会进行小程序首页的渲染,展示小程序首屏,并触发首页的 onReady 事件。
如果开启了 初始渲染缓存,首次渲染可以直接使用渲染层的缓存数据完成,不依赖逻辑层的初始数据,降低启动耗时(onReady 事件提前执行)。
页面初始化的时间大致由页面初始数据通信时间和初始渲染时间两部分构成。 其中,数据通信的时间指数据从逻辑层开始组织数据到视图层完全接收完毕的时间,数据量小于 64KB 时总时长可以控制在 30ms 内。
传输时间与数据量大体上呈现正相关关系,传输过大的数据将使这一时间显著增加。 因而减少传输数据量是降低数据传输时间的有效方式。
初始渲染
初始渲染发生在页面刚刚创建时。初始渲染时,将初始数据套用在对应的 WXML 片段上生成节点树。节点树也就是在开发者工具 WXML 面板中看到的页面树结构,它包含页面内所有组件节点的名称、属性值和事件回调函数等信息。最后根据节点树包含的各个节点,在界面上依次创建出各个组件。初始渲染中得到的 data 和当前节点树会保留下来用于重渲染。
在这整个流程中,时间开销大体上与节点树中节点的总量成正比例关系。因而 减少 WXML 中节点的数量 可以有效降低初始渲染和重渲染的时间开销,提升渲染性能。
2)、更新数据渲染
初始渲染完毕后,视图层可以在开发者调用 setData 后执行界面更新。
setData 原理
1、改变对应的 this.data 的值(同步操作)
解析属性名(包含 . 和 [] 等数据路径符号),返回相应的层级结构,修改对应的局部 data 的值
{abc: 1} 中 abc 属性名 => [abc] {a.b.c: 1} 中 'a.b.c' 属性 => [a,b,c] {"array[0].text": 1} => [array, 0, text]
2、将数据从逻辑层发送到视图层(异步操作)
evaluateJavascript:用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境
重渲染时,逻辑层将 setData 数据合并到 data 中,渲染层将 data 和 setData 数据套用在 WXML 片段上,得到一个新节点树。然后将新节点树与当前节点树进行比较,这样可以得到哪些节点的哪些属性需要更新、哪些节点需要添加或移除。最后,用新节点树替换旧节点树,用于下一次重渲染。
在进行当前节点树与新节点树的比较时,会着重比较 setData 数据影响到的节点属性。因而,去掉不必要设置的数据、减少 setData 的数据量也有助于提升这一个步骤的性能。
3)、用户事件通信
视图层会接受用户事件,如 点击事件、触摸事件 等。
用户事件的通信比较简单,当一个用户事件被触发且有相关的事件监听器需要被触发时,视图层会将信息反馈给逻辑层。
因为这个通信过程是异步的,会产生一定的延迟,延迟时间同样与传输的数据量正相关,数据量小于 64KB 时在 30ms 内。
降低延迟时间的方法主要有两个
去掉不必要的事件绑定(WXML 中的 bind 和 catch),从而减少通信的数据量和次数
事件绑定时需要传输 target 和 currentTarget 的 dataset,因而不要在节点的data前缀属性中放置过大的数据
三、小程序基建和优化方案实践
1、小程序性能优化
1)、代码分包
1、合理的分包和分包预加载策略
主包内容
Tab页(系统要求)、业务必要页面(错误兜底页、登陆授权页等),其余文件都以业务模块或者页面的维度,拆分成各自的分包
分包预加载
据用户的实际使用场景,预测下一跳的页面,将可能性最高的场景设置为预加载分包(可以参照业务埋点数据),例如:进入电商首页后,需要对会场和商详页的分包进行预加载
2、组件分包分发
实现思路
小程序不支持主包引用分包代码,只能在分包中引用主包代码,所以把公共使用的组件代码放在主包目录中,但这些公共组件未必在主包所属的页面中会被引用,可能只是在分包页面中被多次引用,这样使得主包中代码体积越来越大,用户首次加载速度变慢。
将主包页面不依赖的公共组件分别分发到依赖它们的分包目录中,虽然分包各自的体积会有所增大,但主包体积会有显著下降
实现原理
将所有需要分发的组件放置主包指定目录中,并添加配置文件,说明组建文件分发信息。在开发时用 gulp 任务监听单个文件变化、在构建时递归遍历所有组件,将其复制到配置文件中指定的子包路径目录中。
目标文件在复制之前,都先要将文件内的依赖路径进行更新,使其在子包中运行时也能引用成功。针对不同类型的文件,采取不同的依赖分析手段。
JS 文件:使用 babel.transformFile 修改依赖引用地址
WXSS 文件:使用 postcss.plugin('transform-wxss') 处理依赖的 @import 导入样式文件地址
WXML 文件:使用 require('htmlparser2').Parser 来转换内部 wxs、template(import 和 include 导入)依赖的引用地址
异步分包
小程序基础库版本 2.17.3 及以上开始支持分包异步化,即可以在分包之间互相引用,这个能力可以快速取代我们自己的组件分发方案,而且优化效果更佳,可以将分包中公共依赖的代码部分打成新的分包,在使用时异步按需引入,能力与 Web 端的异步组件类似,但这个方案在生产环境的稳定性有待验证。
2)、setData 优化
实现思路
合并 setData 调用,将其放在下个时间片(事件循环)统一传输,降低通信频率
实现原理
需要先将逻辑层 this.data 进行更新,避免前后数据不一致造成逻辑出错。将需要传送至视图层的 data 进行整合,在 nextTick 中调用原生的 setData 统一进行传送,可以有效降低通信频率,并且在传送前手动做一次与 this.data 的 diff 操作,降低通信体积
1. 降低频率
const nextTick = wx.nextTick ? wx.nextTick : setTimeout; // 小程序里的时间片 API
2. 减少体积
参考京东 Taro 中 diff 的实现,对基本、数组、对象等不同类型进行处理,最终转换为 arr[1]、x.y 这样的属性路径语法,减少传输信息量
3)、Storage API 重封装
实现思路
onLaunch、onLoad 等生命周期函数中存在大量对微信 Storage 的同步调用(使用 Sync 结尾的API),这些操作涉及JS与原生通信,同步等待耗时过久,推迟页面 onReady 触发即用户可交互时间,影响用户体验。直接改为异步操作又存在业务代码改动量较大的问题,存在一定风险,大量的异步回调代码语义不优雅、可读性较差。因此需要对原生 Storage 操作进行重封装,改为对内存中对象的实时存取,提高响应速度,并定期调用原生 API 向真实 Storage 中同步。
实现原理
封装调用方式一致的 API,以 getStorage 和 getStorageSync 为基础API,首次调用时触发原生 API 获取原始数据,获取到之后存储至内存对象,后面的各种操作(删改查)便都基于这个对象做操作。其中的 set、remove 操作需要对对应的数据做脏标志并存储到一张脏表里,以便在后面将变动同步到原生端。调用方需要定时调用变动同步方法来持久化数据(遍历同步脏表中的数据),以防内存运行时数据意外丢失,一般需要定期执行(app 的 onShow 执行 setInterval)和在 app 的 onHide 生命周期执行。
我们不仅对 Storage API 进行了重封装,对于其他耗时较久的同步 API(例如用来获取设备和系统信息的 getSystemInfo/getSystemInfoSync API,底层都是同步实现),我们也采用了类似的方式,仅在第一次获取时调用系统 API 并将结果缓存在内存中,之后的调用则直接返回缓存信息。
4)、Data Prefetch
1、发布订阅方式
实现思路
从 A 页面跳转到 B 页面之前,在 A 页面 emit 消息,触发 B 页面数据接口请求,并缓存结果数据,当 B 页面打开时,先取缓存,取不到再重新调用接口
实现原理
B 页面虽然没有实例化,但是页面已经被注册,Page 外层代码已被执行,因此可以事先与 A 页面完成消息的发布订阅
2、全局注册变量方式
实现思路
当需要预加载的页面没有在主包或者是异步注册时,无法通过发布订阅的模式进行预请求,我们可以通过全局注册预加载方法的方式来实现
实现原理
在前一页面路由跳转时发起预请求,将此请求保存到一个全局的 Promise 变量里,在需要预加载的页面首次渲染时优先去取全局的 Promise 变量 then 回调里的数据结果
5)、内存优化
1、图片优化
对整体的图片资源做优化(阿里云函数)
- 尺寸:实际宽高的 1-2 倍宽度
- Webp 格式:Android 端全量使用
- Png 格式:透明底强诉求场景才使用,其余默认使用 Jpg/Jpeg
- 图片懒加载:开启 Image 组件的 lazy-load 属性
2、低端机降级
实现思路
根据用户机型硬件性能层级,前端展示设置不同的呈现策略
实现原理
低端机场景下对以下功能进行降级
- gif 动图进行降级成静态图片
- JS、CSS 动画降级成静态样式
- 不展示 swiper 组件
- 质量使用 60% 的 quality
3、长列表优化
- RecycleView
- IntersectionObserver
2、小程序工程化
1)、插件化框架
框架工具
BeautyWe 是一个三方小程序业务开发框架,通过对生命底层公共业务逻辑的封装
应用场景
公共业务逻辑,例如每个页面对用户登录状态的校验、获取和处理 url 参数、PV 自动埋点、性能监控等
实现原理
生命周期函数插件化,将原生的 onLoad、onShow、onReady 等函数使用 Object.defineProperty 的方式进行重写,使其内部形成一个 Promise 任务链,通过引入插件,向任务链中前后位置自由插入方法,在系统生命周期函数执行时触发并依次调用。
实现生命周期钩子的插件化之后,我们便可以将各类需要处理的底层公共逻辑进行封装,将其插入到宿主(原生生命周期)的 Promise 任务链中
2)、自动化部署
基于小程序 CI + Gitlab CI + Puppeteer 实现了一套半自动的小程序构建部署工具,详见我的另一篇文章《微信小程序自动化部署方案》,后面微信小程序提供了可以在 Linux 环境单独运行的 miniprogram-ci,更加简单易用。
3)、埋点监控
监控能力
业务埋点(页面 PV、模块曝光等业务埋点)
性能上报(小程序启动、页面渲染、FMP、内存告警)
异常监控(JS错误、接口错误、业务错误)
性能监控上报类型
memory_warning:内存告警数据上报。收集方法:wx.onMemoryWarning 回调中收集告警等级
app_launch:app 运行时数据上报。收集方法:记录 app 生命周期执行时间(onLaunch、onShow),在 page 的 onLoad 时上报
page_render:page 运行时数据上报。收集方法:记录 page 生命周期执行时间(onLoad、onShow、onReady)、FMP 等,在 page 的 onHide 或 onUnload 时上报
实现原理
基于插件化框架的方案,对生命周期钩子函数做重写,即可在页面程序执行过程中自动记录和上报性能数据
3、性能优化落地和日常迭代规范
我们使用了 FMP 作为衡量秒开率的关键指标,详细内容参考我的另一篇文章以用户体验为中心的前端性能优化。另外,在日常开发中,我们也沉淀出一些规范,可以提升日常业务迭代中对性能、稳定性的基础保障,内容详见微信小程序开发军规。
四、总结思考与发展前景
基建能力不断完善
官方文档中对框架底层技术的不断披露,说明小程序技术建设日益成熟完善。基建生态的不断丰富,同层渲染、网络环境监测、初始渲染缓存、启动性能优化,官方陆续提供了这些能力的支持,让小程序与 Web 生态逐步靠拢。
我们自身在业务迭代中,为了解决各种问题,自己造了很多轮子,例如:分包异步化、CI 打包、性能 Performance API,这些能力后面微信都原生给予实现。看上去似乎是做了无用功,但事实上恰恰说明我们过去做的事情是正确的,方向也是和整个生态的发展是相吻合的。
更多场景赋能业务
除了技术能力的完善之外,微信小程序生态还在不断丰富更多业务场景下的能力支撑,例如支持小程序分享朋友圈、支持生成短链接、微信聊天素材打开小程序,为我们业务提供更多可能性与想象力。
微信还推出了小程序硬件框架,能让硬件设备(非通用型计算设备)在缺乏条件运行微信客户端的情况下运行微信小程序,在各行各业的安卓系统平板电脑、大屏设备等硬件,提供低成本屏幕互动解决方案,为物联网设备用户提供更标准化、功能丰富的使用体验。
这不禁让我想起那句流传了数十年的计算机技术领域 Atwood 定律,任何可以用 JavaScript 来编写的应用,最终都将用 JavaScript 来编写。那么同样的,任何可以用小程序来实现的产品,最终都将用小程序来实现。
小程序发展前景展望
自从 2017 年微信小程序诞生以来,超级 App + 小程序/轻应用的这种模式在各种业务中屡试不爽,例如微信和微信小程序、支付宝和支付宝小程序、抖音和抖音小程序等等,小程序这种成本低、迭代快、易推广的产品模式,在超级 App 带来的巨大流量加持下,在各个领域都获得了巨大的成功,并且已经逐渐成为一种趋势,越来越多的公司在自己的产品中加入了这种模式。小程序本身也已经在不知不觉中融入到了生活点滴中,疫情期间各个地区的健康码应用,小区里的电商团购、饭店点餐的小程序码,想喝奶茶咖啡在小程序上提前下单,人们的生活已经与小程序紧不可分,小程序这一产品技术方案确确实实创造了极大的社会价值。
我曾经与朋友聊天时戏称小程序是具有中国特色的 PWA 应用,面向未来小程序仍然大有可为,除了现有占比较高的的网络购物和生活服务类应用,在更多的场景下也都能看到小程序技术的用武之地,例如在健康医疗、线下零售、娱乐游戏、AI 智能等行业领域内远远没有达到饱和的状态,市场空白让小程序的开发潜力变得更大,小程序正向着当初设立的目标 让小程序触手可及,无处不在 不断迈进。
原文地址:https://juejin.cn/post/7100752247381819399
(学习视频分享:web前端入门)
前端(VUE)零基础到就业课程:点击学习
自己动手写 PHP MVC 框架:点击学习
![](http://m.sbmmt.com/img/upload/article/000/000/020/633501a29d313483.png)