0. 引子
一码多端,是一个时常活跃于前端领域的名词。一码,顾名思义指一份代码,而多端,可以指安卓和 iOS,或 PC 和移动端,亦或多种小程序。而笔者这里要跨的端包括安卓App、iOS App、微信公众号、微信小程序、支付宝小程序等,多多益善。从前端系统建设的角度思考,前端事务不外乎标准、引擎、容器、框架、工程、平台6个层次。于小程序而言,厂家制定了其小程序标准及引擎实现,而本文将从容器、框架、工程等层面分享一码多端的实践探索。
1. 背景
当前公司已经有很多H5应用,以在线H5或离线包的形式内嵌在 App 环境中,提供丰富的业务功能。而随着超级 App 的出现,其把持着大量的移动互联网入口,拥有庞大的优质用户,各大厂的小程序平台已经成为重要的引流渠道和业务载体。因此如何将公司现有业务快速迁移到小程序平台成为迫在眉睫的事情。
技术侧需解决,一份代码是否可以同时适用于微信小程序和支付宝小程序,一份代码是否可以既适用于 App 环境又适用于小程序环境,以及当前各H5应用需要怎样的适配工作以能够在小程序环境工作。
2. 系统设计
现有架构
公司移动端架构如下图所示。公司现有的主要业务载体是App,另有少量业务投放在微信公众号环境。App 中包括传统容器、离线包容器、原生模块。传统容器通过 WebView 加载在线H5应用,其适用于简单页面或对性能不太敏感的页面。离线包容器则将 Web 资源存储在本地,及当线上版本更新后自动拉取并缓存新版本,适用于性能要求高的场景。而对于性能有极致要求的场景则使用原生技术开发。
建设目标
如上述架构图所示,当前 App(安卓 / iOS)端、公众号已经有成熟完整的跨端体系,而本次建设的目标则是将端能力延伸至小程序端。
容器层面:需要一个跨端的小程序框架,使得一份代码,可编译为不同厂家的小程序代码。
框架层面:基于当前框架适配层框架,继续拓展使其适配小程序 WebView 环境。以及一个通用路由框架,使得同一路由地址在 App 环境和小程序环境都能被正确路由。
工程层面:各H5应用适配小程序环境,及 App 原生模块需用小程序原生实现。
3. 跨端设计
3.1 跨端框架
小程序从技术架构、到开发语言、运行环境都区别于传统Web,不同厂商的语法和 API 存在差异,因此需要一个跨端的解决方案,使得一份代码可以编译为不同小程序的原生代码。
业内主流小程序多端方案对比如下表所示:
| 静态编译型(uniapp/Taro2.x) | 动态渲染型(mpvue/Remax/Taro3.x) | 原生增强型 Mpx | |
|---|---|---|---|
| DSL | Vue/React | Vue/React | 原生 |
| 技术方案 | 静态转译Vue/React -> 原生 | 递归动态模板模拟 Dom 环境上层完整Vue/React | 编译+运行时 |
| 优势 | 性能表现良好具备 Web 迁移能力 | Web 框架使用不受限具备完整的 Web 迁移能力 | 性能表现优秀原生使用不受限 |
| 缺点 | Web 框架使用受限 | 性能表现略差,可以接受 | Web 迁移能力较弱 |
目前业界主要有3种方式来做小程序的跨端:
1.静态编译型,通过用熟悉的Web语言编写程序,由框架通过静态AST解析转换为特定平台的小程序代码。如基于VUE的 uniapp 和基于 React 的 Taro2.x。
2.动态渲染型,基于虚拟DOM原理,复用 Web 框架的虚拟DOM Diff 算法动态计算更新节点,框架提供针对特定小程序的运行时库完成渲染。
3.原生增强型,并非基于Web 技术转化,而是直接专注于对原生编程体验的增强,以及按特定规则转化为其他小程序的标签模板。
原生增强的方案性能最好,但其专注于小程序环境,无 Web迁移能力。动态渲染虽比静态编译方案性能略差,但当前硬件性能过剩,性能差异不大,而动态渲染架构更合理,适配性更好。
考虑到公司当前已有大量基于 React 技术栈的H5应用,因此跨端框架选择使用 Taro3.x 最为合适。
3.2 跨端方案
那么全部使用 Taro 来实现小程序原生是否可行呢?
- 首先,小程序代码包最大不超过2M,即使使用分包技术,依然有包总大小限制。
- 其次,即使使用分包技术,其分包体积通常会比单独的H5页面大,小程序异步加载分包时速度更慢。
- 再次,小程序原生开发,任何代码改动都需要重新提审,影响对客时效性。
因此,复用现在的H5页面,从性能和时效看都是合理的选择。但现有H5应用依赖客户端提供的能力,而跨端到小程序环境则必须进行小程序环境的适配。此时有两种方案:
方案1:接入特定小程序 JSSDK。这样的好处是能使用其 JSSDK 提供的优秀接口,不足是接入其他小程序,则需再接入对应厂商的 JSSDK。
方案2:功能上做一些取舍,仅依赖浏览器或原生 Webview 提供的能力。这样的好处是便于移植,而不足自然是牺牲用户体验,甚至是牺牲部分功能。
这两种方案的抉择,跟实际的业务特点有很大的关系。
3.3 跨端架构
事实上上述的两种方案,我们都进行了采纳。
方案1:采用 Taro 做原生小程序开发,提供首页、路由等核心功能,并通过 Webview 加载H5,H5应用中引入 JSSDK。称之为混合方案。
混合方案需搭建基于 Taro 的应用,其职责包括:
- 承载小程序首页、我的等功能
- 负责框架功能,如路由、请求拦截、公参处理、埋点等
- 调度H5应用,及为H5应用间互跳提供中转
各H5应用需引入JSSDK,适配特定小程序提供的能力,如人脸识别、拍照等。
只需将 Taro 应用构建为对应小程序平台代码,并上传提审,而H5随时发版即可。
方案2:采用 Taro 开发首页等并编译为H5,Taro 应用中提供调度各H5应用的能力, 称之为纯H5方案。
纯H5方案中,小程序只是一个Webview容器,所有功能都由H5实现。但现有各H5应用都是独立的HTML,需要一个单页面框架将其组织起来。
因此此时 Taro 应用的主要职责就是单页应用框架,各应用通过 iframe 组织为 Taro 的单页面路由。
4. 路由设计
4.1 路由初衷
移动端开发时,通常会为不同的模块定义 URL scheme路由,如 scheme://module1。Web 应用中通常一个路由地址对应一个URI,如 https://domain/module1。
对于业务使用方来说,必然是希望无论是哪个环境都能打开同一个路由地址,即路由协议跨端。如 scheme://module1,无论是 App 环境还是小程序环境,都应该能跳转到 module1 模块。因此路由模块的设计,首先应建立 scheme 和 Web 路由的映射。
事实上,从 scheme 到 Web 路由的映射有很多情形需考虑:
- 要跳转的模块是否需要登录态:如需登录态且未登录,则应该先跳登录页,登录成功后再跳预设的模块
- 要跳转的模块不存在应怎样处理:不应该404报错,而应进入友好的兜底页面
- 如何根据不同的参数动态跳转不同的模块:目的地址除了地址还应该支持执行函数动态计算
- 跳转链接是遵循SPA的模块还是H5应用:SPA模块可直接跳转,而H5应用需使用 Webview 或 iframe 来加载
- 外部链接如何打开:外部链接也应统一到私有 scheme 来管理以便于跟踪打开情况
路由模块包括3个子模块:路由映射配置、路由初始化、路由跳转。路由模块的设计应为通用的设计,无论是混合方案或纯H5方案,都应适用。
4.2 路由映射配置
假如路由分散在各个应用中则不便于管理,甚至不同应用中需要定义重复的路由,当路由配置需要修改时,需同时修改多处。鉴于此,路由映射配置设计为在 Taro 应用中集中管理。每个应用中,如路由地址是 http 等可直接路由的地址时,使用 Webview 打开,否则需中转到 Taro 应用的路由中转模块,统一进行路由查询。
路由配置定义如下:
const urlMap = [ ['scheme://module1', '{domain}/path2module1'],
['scheme://logincheck/module2', '{domain}/path2module2'], // 登录态检查
['scheme://module3', '/pages/modules3'], // 从小程序中跳转到module3
['scheme://module4', 'mini://pages/modules4'], // 从H5跳转到小程序module4
['scheme://module5ormodule6', jumpByPrameter], // 函数处理后决定跳转
['', ] // 其他映射规则定义
];
4.3 路由初始化
为便于路由集中管理,将路由映射配置集中在 Taro 应用中,但每个应用都应进行路由初始化,以使其具备路由查询功能。路由,就是 Router类,其初始化依赖于:
- 路由映射配置:其作用是根据路由映射配置生成内存中的路由映射关系,H5应用路由通过 Taro 应用中转转发,因此无路由映射配置。
- 当前是否处于登录态的检查方法:用于处理 scheme://logincheck 情形
- 模块未注册路由时的处理方法:不应报404错,而应路由到兜底页
以混合方案为例,路由初始化如下图所示,Taro 应用中包含路由映射配置,并在应用初始化逻辑中初始化路由,各H5应用中也需在初始化逻辑中初始化路由。
4.4 路由跳转
路由跳转指当在某个页面中,用户点击按钮或发请求进而触发跳往某个自定义 scheme 地址的情形,此处需由路由查询模块进行处理,计算并匹配到真实跳转的地址。
5. SPA框架
如上所述混合方案,各应用中注入了JSSDK,页面跳转通过调用 Webview 环境可调用的 JSSDK 来实现页面跳转。而纯H5方案的设计初衷就是不引入任何小程序 JSSDK。
纯H5方案中,我们使用 Taro 应用来作为主应用,Taro 应用自身为单页面应用,Taro 应用负责框架层面工作,而各应用由 Taro 应用通过 iframe 来调度加载。
因此,SPA框架工作包括:环境判断、SDK注入、纯H5页面导航、iframe标题同步、页面可视处理及其他必要的技术工作等。
5.1 环境判断
环境适配的第一步就是环境识别。这里涉及到的环境至少包括:App环境、混合方案之小程序原生、混合方案之小程序 Webview、纯H5方案之 Taro 应用、纯H5方案之 iframe 环境。
- App环境:根据 App 中 Webview 的 userAgent 关键字段判断即可
- 混合方案之小程序原生:以微信小程序为例,可这样判断:typeof __wxConfig === 'object' && typeof wx === 'object' && typeof wx.getSystemInfo === 'function'
- 混合方案之小程序 Webview:以微信小程序 Webview 为例,可这样判断:userAgent 中包含 MicroMessenger 及 miniProgram 等关键特征
- 纯H5方案之 Taro 应用、纯H5方案之 iframe 环境:由于纯H5方案是通用方案,因此无法通过 userAgent 来直接判断,而需根据下一节中”SDK注入“相关技术来判断
5.2 SDK注入
混合方案各应用注入了第三方 JSSDK,纯H5方案虽不引入第三方 JSSDK,但也需要导航SDK的。纯H5方案中,利用 Taro 单页面架构的特点,通过 Taro 应用来做单页路由框架,H5应用通过 iframe 来加载。
试想,H5应用中需要触发路由跳转,需要跳转到另外一个H5应用,该如何实现呢?路由跳转需要借助 Taro 提供的方法,即页面跳转能力需由父应用(Taro应用)提供,iframe 的特点决定了同域的情况下iframe页面可直接访问父页面的方法。
因此,纯H5方案的路由思路就是,Taro 应用暴露导航SDK,H5应用中直接调用。Taro 应用暴露导航SDK的过程,称为SDK注入。Taro应用暴露的导航SDK形如:
- navigateTo
- navigateBack
- relaunch
- redirectTo
5.3 纯H5页面导航
前面反复提到不同环境的适配,这样同一个应用因为适配不同环境而会编写大量 if/else 代码,这将大大降低代码可读性和可维护性。
事实上,对于某个具体的应用来说,它只关心要达成的目标,并不关心在不同环境下是如何来达成的。如当需要页面跳转时,应用只需要知道调用 Util.openUrl("") 就可以打开一个 scheme 地址即可,至于 App环境、小程序或纯H5环境都是如何来实现的,应在底层库中屏蔽此细节。
5.4 iframe标题同步
小程序的导航条显示的是文档的 title,但是当应用以 iframe 的形式被加载时,title只是 iframe 的 title,并非外层文档的标题,此情形当 iframe 的 title 更新时,需同步更新外部文档的标题。
对于文档标题的更新代码分布于各个应用的各个模块,更新标题同步更新外层文档标题,此方案改动太多,更合理的方式是在 Taro 应用的 iframe 容器中,通过使用 MutationObserver 监听文档标题变化,进而自动更新标题。
通过监听 iframe.contentWindow.document 的变化,变化的 DOM 包括 TITLE 和 HTML 都可能导致标题被更新。代码形如:
new MutationObserver(function (mutations) {
if (mutations[0].target.nodeName.toLocaleUpperCase() === 'TITLE') {
}
else if (mutations[0].target.nodeName.toLocaleUpperCase() === 'HTML') {
}
}).observe(iframe.contentWindow.document);
5.5 页面可视处理
页面可视时执行一些逻辑是H5应用中常见的需求,如页面可视时,调用刷新数据接口、更新页面状态等。Webview 环境中(App Webview或小程序 Webview)可通过 visibilitychange 监听页面的可视变化,回调中执行可视逻辑。然而在纯 H5 iframe 环境下,页面可视本质上是路由的切换,并不是页面的显示与隐藏,因此监听 visibilitychange 的方案不可行。
由于页面可视本质是路由切换,因此Taro应用中, iframe 容器的 useDidShow(()=>{}) 函数回调正是 iframe 页面可视回调。
6.总结
本文探索了在企业真实而复杂的技术与业务场景的小程序探索实践,为了尽可能跨多端而选择了 Taro 框架,为了易于扩展更丰富的功能而采用原生与 Webview 相结合的技术,考虑到实际投放业务场景而设计了混合方案和纯H5方案,以及各种方案所面临的主要技术挑战与解决方案。此设计可解决企业面临的实际小程序快速投产问题,并带来良好的业务效果,有一定的借鉴意义。