渲染线程与 JS 引擎线程是互斥的,存在阻塞问题,长时间的 JS 脚本执行直接会影响到 UI 视图的渲染,导致页面卡顿
危险的 HTML 标签(a、script)和 JS API(Function、eval、DOM API),攻击者很容易对页面内容进行篡改,非法获取到用户信息等
模型中对 HTML 标签进行了封装,开发者无法直接调用原生标签,JS 中原有的危险逻辑操作 API 能力也被屏蔽,涉及到的通信动作只能通过中间层提供的 API 去触发虚拟 DOM 更新(数据驱动视图)
其中视图层及逻辑层的能力都是通过微信客户端内置的小程序基础库 JS 文件来提供的
WAService.js提供逻辑层基础的 API 能力,向开发者暴露各类操作的 API
不同运行环境下,脚本执行环境以及用于组件渲染的环境是不同的,性能表现也存在差异:
在 Android 上,小程序逻辑层的 JavaScript 代码运行在V8中,视图层是由基于 Mobile Chromium 内核的微信自研 XWeb 引擎来渲染的(从过去的 X5 换成了自研的 XWeb 引擎,视图层是基于 Mobile Chrome 内核来渲染的,而且根据官方披露,将来逻辑层也会脱离 V8,使用定制化的 XWeb Worker 线程,即自定义一个 Web Worker 线程,作为小程序的逻辑层。参考链接)
Web Component是浏览器中创建封装功能和定制元素的能力,由三项主要技术组成
Shadow DOM(影子DOM)一组 JavaScript API,用于将封装的“影子” DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突
template 和 slot 元素可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用
Exparser 是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持 小程序内的所有组件,包括内置组件和自定义组件,都由 Exparser 组织管理
小程序中,所有节点树相关的操作都依赖于 Exparser,包括 WXML 到页面最终节点树的构建、 createSelectorQuery 调用和自定义组件特性等
Exparser 会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的 Shadow DOM 实现
基于 Shadow DOM 模型:模型上与 Web Component 的 Shadow DOM 高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他 API 以支持小程序组件编程
可在纯 JS 环境中运行:这意味着逻辑层也具有一定的组件树组织能力(在组件的渲染中就会用到)
高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小
小程序基础库对外提供有 Page 和 Component 两个构造器,小程序启动将 properties、data、methods 等定义字段,写入 Exparser 的组件注册表,在初始化页面时, Exparser 会创建出页面根组件的一个实例,用到的其他组件也会相应创建组件实例(这是一个递归的过程)
从上图中我们能够比较明显的看到小程序在 Page 渲染和 Component 渲染时通信方式上存在差别。其中,Page 渲染中 VDOM 的生成和 diff 都是在视图层完成的,逻辑层只负责对 data 数据的发送,来触发渲染层的更新逻辑。而在 Component 的渲染中,逻辑层和视图层需要共同维护一套 VDOM,方式则是在组件初始化时在逻辑层构建组件的 VDOM,然后将其同步到视图层。后续的更新操作则会先在逻辑层进行新旧 VDOM 的 diff,然后仅将 diff 之后的结果进行通信,传递到视图层之后直接进行 VDOM 的更新和渲染。这样做最大的好处就是将视图更新通信的粒度控制成 DOM 级别,只有最终发生改变的 DOM 才会被更新过去(因为有时候 data 的改变并不一定会带来视图的更新),相对于之前 data 级别的更新会更加精准,避免非必要的通信成本,性能更好。
扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力。
体验更好,同时也减轻 WebView 的渲染工作。比如像地图组件(map)这类较复杂的组件,其渲染工作不占用 WebView 线程,而交给更高效的客户端原生处理。
渲染性能更好,绕过 setData、数据通信和重渲染流程,比如像画布组件(canvas)可直接用一套丰富的绘图接口进行绘制。
组件被创建后插入到 DOM 树里,渲染一个占位元素,浏览器内核会立即计算布局,此时我们可以读取出组件相对页面的位置(x, y 坐标)、宽高
组件通知客户端,客户端在相同的位置上,根据宽高插入一块原生区域,之后客户端就在这块区域渲染界面
同层渲染(通过一定的技术手段把原生组件直接渲染到 WebView 同一层级上)
WKWebView 在内部采用的是分层的方式进行渲染,WKWebView 会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView 组件,但合成层与 DOM 节点之间不存在一对一的映射关系(内核一般会将多个 DOM 节点合并在一个合成层中)。
通过上述流程,小程序的原生组件就入到 WKChildScrollView 了
embed 标签是 HTML5 新增的标签,是使用来定义嵌入的内容(如插件、媒体等),格式可以是 PDF、Midi、Wav、MP3 等等,比如 Chrome 浏览器上的 PDF 预览,就是基于 embed 标签实现的。
微信开发者工具内置了二进制的 WXML 和 WXSS 代码文件编译器,编译器接受 WXML 和 WXSS 代码文件列表,处理完成之后输出 JavaScript 代码
JS 文件包含添加尺寸单位 rpx 转换方法,可根据设备屏幕宽度自适应计算成 px 单位
提供 wx 全局对象下面的 api 方法,网络、媒体、文件、数据缓存、位置、设备、界面、界面节点信息还有一些特殊的开放接口
它通过实例化一个 VM 环境来执行 JS 代码,如果你有多个 JS 需要执行,就需要实例化多个 VM。并且需要注意这几个 VM 之间是不能相互交互的,因为容易出现 GC 问题
这其实就是典型的模块加载思路,与 Webpack 打包模块的处理方式十分相似
WAWebview.js 和 WAService.js 文件中除了提供视图层、逻辑层基础的 API 能力之外,还提供了这两个线程之间进行通信的能力
小程序逻辑层和渲染层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发
几个端的不同实现最终会封装成 WeiXinJSBridge 这样一个兼容层供给开发者调用
WeixinJSBridge提供了视图层 JS 与 Native、视图层与逻辑层之间消息通信的机制,提供了如下几个方法:
部分和原生组件相关的视图有使用限制,如微信的 scrollView 内不能有 textarea
双线程带来的性能瓶颈,也是微信自身一直致力于解决的关键问题。前面提到小程序将会在新的 XWeb 内核里,将逻辑层的实现替换为通过修改 Chromium 内核,实现自定义的 XWeb Worker 线程,这样就不再需要额外的 V8 了,使得内存占用方面有显著减少。另外,由于是基于 Chromium 内核的 Worker 线程,所以关于数据通信这块,很自然的也会拥有 PostMessage 的能力,来替代原有的 setData 底层通信方式,获得更高性能的通信能力
另外,我还了解研究了支付宝小程序底层的相关实现。支付宝小程序也使用了类似的双线程架构模型,通过使用 UC 提供的浏览器内核,渲染层是在 Webview 线程中运行,逻辑层则是另起一个线程运行 Service Worker。但是 Service Worker 需要通过 MessageChannel API 来与 渲染线程通信,当数据量较大、对象较复杂时同样存在性能瓶颈。
因此支付宝小程序重新设计了现有的 JS 虚拟机 V8,提出了一种优化的隔离模型(Optimized isolation model, OIM)。OIM 的主要思路是共享 JS 虚拟机实例中与线程执行环境无关的数据和基础设施,以及不可变或不易变的 JS 对象,使得在保持 JS 层逻辑隔离的前提下,节省多实例场景下在内存和功耗上的开销。尽管有些实例间共享的数据会带来同步的开销,但是在隔离模型下,本方案所共享的数据、对象、代码和虚拟机基础设施都是不可变或者不易变的,所以很少发生竞争。
在新的隔离模型下,Webview 里面的 V8 实例就是一个 Local Runtime,Worker 线 实例也是一个 Local Runtime,在逻辑层和渲染层交互时,setData 对象的会直接创建在 Shared Heap 里面,因此渲染层的 Local Runtime 可以直接读到该对象,并且用于渲染层的渲染,减少了对象的序列化和网络传输,极大的提升了启动性能和渲染性能。
除此之外,支付宝小程序实现了首页离线缓存优化,首先渲染上次保存下面的首页 UI 页面,把首页展现给用户,然后在后台继续加载前端框架和业务的代码,加载完成后再和离线缓存的首页 UI 进行合并,给用户展现动态的首页,这一点和微信小程序的初始渲染缓存方案十分类似且更加激进。还使用了 WebAssembly 技术重新实现了虚拟 DOM 这块的核心代码,提升了小程序的页面渲染。
技术架构为解决问题而生,一个好的技术方案不仅需要设计者在开发效率、技术成本、性能体验、系统安全之间做出平衡和取舍,还需要与业务走向、产品形态、用户诉求紧密结合
在小程序启动(冷启动)时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标
此时,微信会在背后完成几项工作:下载小程序代码包、加载小程序代码包、初始化小程序首页
从开发者的角度看,控制代码包大小有助于减少小程序的启动时间。对低于1MB的代码包,其下载时间可以控制在929ms(iOS)、1500ms(Android)内
在小程序启动前,微信会提前准备好一个页面层级用于展示小程序的首页。每当一个页面层级被用于渲染页面,微信都会提前开始准备一个新的页面层级,使得每次调用 wx.navigateTo 都能够尽快展示一个新的页面。视图层页面内容都是通过 pageframe.html 模板来生成
第二阶段是在 WebView 中初始化基础库,此时还会进行一些基础库内部优化,以提升页面渲染性能
第三阶段是注入小程序 WXML 结构和 WXSS 样式,使小程序能在接收到页面初始数据之后马上开始渲染页面(这一阶段无法在小程序启动前执行)
__wxConfig:是根据全局配置和页面配置生成的配置对象,包含 page、分包 page、tabbar 等信息
WXML:$gwx 函数的执行结果,是一个可执行函数,执行后将会返回 VDOM 对象
WXSS:eval 执行样式函数的结果,是一个可执行函数,执行后插入 style 标签
通常情况下,在小程序启动时,启动页面所在分包和主包(独立分包除外)的所有 JS 代码会全部合并注入。自基础库版本 2.11.1 起,配置了按需注入和用时注入,小程序仅注入当前页面需要的自定义组件和页面代码,在页面中必然不会用到的自定义组件不会被加载和初始化
在小程序启动或一个新的页面被打开时,页面的初始数据(data)和路径等相关信息会从逻辑层发送给视图层,用于视图层的初始渲染。
初始渲染发生在页面刚刚创建时。初始渲染时,将初始数据套用在对应的 WXML 片段上生成节点树。节点树也就是在开发者工具 WXML 面板中看到的页面树结构,它包含页面内所有组件节点的名称、属性值和事件回调函数等信息。最后根据节点树包含的各个节点,在界面上依次创建出各个组件。初始渲染中得到的 data 和当前节点树会保留下来用于重渲染。
重渲染时,逻辑层将 setData 数据合并到 data 中,渲染层将 data 和 setData 数据套用在 WXML 片段上,得到一个新节点树。然后将新节点树与当前节点树进行比较,这样可以得到哪些节点的哪些属性需要更新、哪些节点需要添加或移除。最后,用新节点树替换旧节点树,用于下一次重渲染。
用户事件的通信比较简单,当一个用户事件被触发且有相关的事件需要被触发时,视图层会将信息反馈给逻辑层。
Tab页(系统要求)、业务必要页面(错误兜底页、登陆授权页等),其余文件都以业务模块或者页面的维度,拆分成各自的分包
据用户的实际使用场景,预测下一跳的页面,将可能性最高的场景设置为预加载分包(可以参照业务埋点数据),例如:进入电商首页后,需要对会场和商详页的分包进行预加载
小程序不支持主包引用分包代码,只能在分包中引用主包代码,所以把公共使用的组件代码放在主包目录中,但这些公共组件未必在主包所属的页面中会被引用,可能只是在分包页面中被多次引用,这样使得主包中代码体积越来越大,用户首次加载速度变慢。
将所有需要分发的组件放置主包指定目录中,并添加配置文件,说明组建文件分发信息。在开发时用 gulp 任务监听单个文件变化、在构建时递归遍历所有组件,将其复制到配置文件中指定的子包路径目录中。
小程序基础库版本 2.17.3 及以上开始支持分包异步化,即可以在分包之间互相引用,这个能力可以快速取代我们自己的组件分发方案,而且优化效果更佳,可以将分包中公共依赖的代码部分打成新的分包,在使用时异步按需引入,能力与 Web 端的异步组件类似,但这个方案在生产环境的稳定性有待验证。
合并 setData 调用,将其放在下个时间片(事件循环)统一传输,降低通信频率
需要先将逻辑层 this.data 进行更新,避免前后数据不一致造成逻辑出错。将需要传送至视图层的 data 进行整合,在 nextTick 中调用原生的 setData 统一进行传送,可以有效降低通信频率,并且在传送前手动做一次与 this.data 的 diff 操作,降低通信体积
参考京东 Taro 中 diff 的实现,对基本、数组、对象等不同类型进行处理,最终转换为 arr[1]、x.y 这样的属性路径语法,减少传输信息量
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 并将结果缓存在内存中,之后的调用则直接返回缓存信息。
从 A 页面跳转到 B 页面之前,在 A 页面 emit 消息,触发 B 页面数据接口请求,并缓存结果数据,当 B 页面打开时,先取缓存,取不到再重新调用接口
B 页面虽然没有实例化,但是页面已经被注册,Page 外层代码已被执行,因此可以事先与 A 页面完成消息的发布订阅
当需要预加载的页面没有在主包或者是异步注册时,无法通过发布订阅的模式进行预请求,我们可以通过全局注册预加载方法的方式来实现
在前一页面路由跳转时发起预请求,将此请求保存到一个全局的 Promise 变量里,在需要预加载的页面首次渲染时优先去取全局的 Promise 变量 then 回调里的数据结果
BeautyWe是一个三方小程序业务开发框架,通过对生命底层公共业务逻辑的封装
公共业务逻辑,例如每个页面对用户登录状态的校验、获取和处理 url 参数、PV 自动埋点、性能监控等
生命周期函数插件化,将原生的 onLoad、onShow、onReady 等函数使用 Object.defineProperty 的方式进行重写,使其内部形成一个 Promise 任务链,通过引入插件,向任务链中前后位置自由插入方法,在系统生命周期函数执行时触发并依次调用。
基于插件化框架的方案,对生命周期钩子函数做重写,即可在页面程序执行过程中自动记录和上报性能数据
官方文档中对框架底层技术的不断披露,说明小程序技术建设日益成熟完善。基建生态的不断丰富,同层渲染、网络环境监测、初始渲染缓存、启动性能优化,官方陆续提供了这些能力的支持,让小程序与 Web 生态逐步靠拢。
我们自身在业务迭代中,为了解决各种问题,自己造了很多轮子,例如:分包异步化、CI 打包、性能 Performance API,这些能力后面微信都原生给予实现。看上去似乎是做了无用功,但事实上恰恰说明我们过去做的事情是正确的,方向也是和整个生态的发展是相吻合的。
除了技术能力的完善之外,微信小程序生态还在不断丰富更多业务场景下的能力支撑,例如支持小程序分享朋友圈、支持生成短链接、微信聊天素材打开小程序,为我们业务提供更多可能性与想象力。
微信还推出了小程序硬件框架,能让硬件设备(非通用型计算设备)在缺乏条件运行微信客户端的情况下运行微信小程序,在各行各业的安卓系统平板电脑、大屏设备等硬件,提供低成本屏幕互动解决方案,为物联网设备用户提供更标准化、功能丰富的使用体验。
自从 2017 年微信小程序诞生以来,超级 App + 小程序/轻应用的这种模式在各种业务中屡试不爽,例如微信和微信小程序、支付宝和支付宝小程序、抖音和抖音小程序等等,小程序这种成本低、迭代快、易推广的产品模式,在超级 App 带来的巨大流量加持下,在各个领域都获得了巨大的成功,并且已经逐渐成为一种趋势,越来越多的公司在自己的产品中加入了这种模式。小程序本身也已经在不知不觉中融入到了生活点滴中,疫情期间各个地区的健康码应用,小区里的电商团购、饭店点餐的小程序码,想喝奶茶咖啡在小程序上提前下单,人们的生活已经与小程序紧不可分,小程序这一产品技术方案确确实实创造了极大的社会价值。
我曾经与朋友聊天时戏称小程序是具有中国特色的 PWA 应用,面向未来小程序仍然大有可为,除了现有占比较高的的网络购物和生活服务类应用,在更多的场景下也都能看到小程序技术的用武之地,例如在健康医疗、线下零售、娱乐游戏、AI 智能等行业领域内远远没有达到饱和的状态,市场空白让小程序的开发潜力变得更大,小程序正向着当初设立的目标
*请认真填写需求信息,我们会在24小时内与您取得联系。