【多端】指 Web H5 & 小程序生态
个人介绍
农有宝。2011年末加入虾米网开始前端之旅,2013年加入阿里巴巴 (花名:宝码),2015年初转岗到淘宝前端团队,负责中台研发,参与建设飞冰(ICE)项目,早期 iceworks 开发负责人。2018年末离职加入初创团队,创业未果。2020年初加入微拍堂,目前负责多端体系的研发与架构。
成果
目前 Facejs 多端研发体系 facejs.org/ 已正式投入生产使用,微拍堂主站与小程序已逐步迁移上线。
会在适合的时机开源
前言
在2018年末第一次接触微信小程序,抛开平台的一些限制不谈,能很快的创建一个小程序出来,正因为相似的 css、html 等语法,几乎零门槛上手,我也很快的创建了一个自己的小程序并发布。基于职业的本能就会想是否有开发框架能提高小程序的开发效率,先后了解到的有:mpvue uniapp wepy taro 等开发框架。由于业务发展尚未达到规模,就暂时没有深入的了解。
直到后来加入了微拍堂团队,才开始真正的去关注多端的生态,微拍堂的前端业务涵盖了:PC、H5、微信小程序、头条小程序、百度小程序四大平台,不排除未来新的平台出现的可能。也让我真正的开始去深入了解小程序。
启蒙
在聊多端之前,我想先聊聊在原生微信小程序中如何渲染一个富文本的。举个例子,比较热门的是 github.com/icindy/wxPa… 其原理是转换为 JSON 数据后再通过模板的能力渲染出来,这就是一个通用的模式。只要有一份 JSON 数据约定,就可以通过平台的模板能力渲染出来。当时基于业务的简单需求,我也创建了一个 github.com/noyobo/html… 的库,用于渲染 html 富文本的。这些都是某种意义上的数据驱动视图的方式。
起点 - Remax
使用真正的 React 构建跨平台小程序
React 的虚拟 DOM 模式,就能产生一个页面级别 JSON 数据。恰好 Remax 也抓住了这一点,并在渲染性能上优化到了极致。这正是我理想中的多端运行基础,开始着手基于 Remax 思考如何打造适合团队需要的多端研发体系。
详细参阅:Reamx 的实现原理 remaxjs.org/guide/imple…
体系的目标
团队内部基于 React 生态已经有完善的开发流程,在这个前提下,如何保证原有模式不变,同时支持多端,是一个比较有意思的挑战。
目前体系主要包含:
- 业务框架 - 模式共存,现有业务平滑迁移;
- 组件生态 - 变现一致,渐进增强抹各端平台差异;
- 工程化 - 屏蔽工程复杂度,保障稳定性,降低使用门槛。
1. 业务框架 - 平滑迁移
微拍堂的无线主站是一个 Web App,最大的特点是:单页面渲染机制,多页面实例共存。不同于普通的 SPA、MPA,主站使用起来更像是一个 App 一般,有进出场效果,页面数据缓存,页面堆栈管理等,使用起来就如同一个小程序般。
页面共存
保障业务平滑升级,首先得了解现有的页面加载流程,梳理的加载流程大致如下:
每个页面构建后的产物是一个 bundle.js,不管是历史页面,还是新的多端页面,都是一个 bundle。
为了适配小程序的生命周期,页面中模拟了 onShow
onLoad
onHide
所以在【渲染实例】这一步需要对不同的路由使用不同的渲染器,以响应新页面内部的生命周期。
主要判断是新的路由地址,就使用 Racejs Renderer 进行渲染,此处引进一个路由声明:
// face.config.js
export defualt {
routes: ['/detail/:uri']
}
在【路由解析】阶段标记,迁移了一个页面就增加一条新的记录,实现逐步迁移。
路由映射
只有一套代码,那么代码中也就只存在一个路由调用,例如
import { Link } from '@facejs/router';
<Link to="/detail/10000">商品标题</Link>
这样的地址需要在小程序中跳转到对应的页面,这里引入一个 route.alias.js
配置文件。
export defualt {
'/detail/:uri': function (params) {
return `/pages/detail/index?uri=${params.uri}`;
},
}
Link
组件内调用的 router.to 方法。
router 提供一个绑定 alias 的接口,只在小程序的运行环境下执行,router 就具备路由映射的能力,可实现一套路由标准全平台跳转:
// app.js 小程序的启动文件
import router from '@facejs/router'; // 单例模式
import alias from './face.alias.js';
router.init({ alias });
router.to(url, options);
2. 跨平台组件 & API
各平台之间的差异是客观存在的,无法真正意义的一套代码在各端执行,但可以通过底层的渲染基础,能让上层应用、业务逻辑只写一份即可。
文件同构基础
基本架构如下:
基础 UI 与 API 通过文件同构的方式,实现各平台的 UI 表现,依靠 webpack extensions 的能力,基础 UI 组件的文件会存在多个文件:
├── View.toutiao.tsx
├── View.tsx
├── View.wechat.tsx
├── index.less
├── index.ts
└── props.ts # 统一组件 props
在构建时通过构建参数,会有不同的 webpack config 配置:
// 微信小程序
module.exports = {
resolve: {
extensions: ['.wechat.js', '.js']
}
};
// 头条小程序
module.exports = {
resolve: {
extensions: ['.toutiao.js', '.js']
}
};
从而屏蔽掉业务中编写的差异。
DOM / Event 抹平
为什么要做 DOM / Event 的抹平?在浏览器中与小程序的事件响应是两种不同的内容。
例如 Input 的输入回调,web 通过 event.target.value
、小程序通过 event.detail.value
获取。此类需求通过定义统一的 Interface 类型返回即可:
interface Event {
value?: string;
vnode?: Record<string, any>
}
一些动画计算难免使用到后去 DOM 元素的位置 getBoundingClientRect
,则通过统一接收 ref 参数,内部去实现,为什么选择使用 ref 作为 DOM 接口的入参呢?
- ref 在浏览器环境下,返回元素实例,可直接操作。
- ref 在小程序环境下,返回 VNode 节点信息,再根据 VNode 里的 id 属性去执行对应的
xx.createSelectorQuery
由于小程序双进程渲染机制,此类 API 都设计为 Promise 的形式。
统一平台样式
无线开发适配方案已经非常成熟了,这里没什么特别的,选择一个适合团队的就可以。目前最佳的实践是 web 模式下将 html.fontSize 设置为 100px (可根据分辨率动态变更)。
统一样式单位 rem
将开发习惯保持与 web 一致。
- web 开发模式下无需转换。
- 小程序下将
rem
转换为${n * 100}rpx
。1rem
=>100rpx
需要额外处理的是内敛样式,需要将 style={{width: 500}}
=> style={{width: '5rem'}}
,针对内敛样式可通过底层基础 UI 组件来完成。
px
单位不转换,保留绝对单位能力。
小结
通过基础差异抹平,搭配扩展封装,Api 实现,在业务使用上已经不存在差异。
web | 微信 | 头条 |
---|---|---|
web 演示: face-js.github.io/web/extensi…
扫码查看 Facejs 组件示例
3. 工程化
业务框架 + 组件生态 已准备就绪,如何将多端模式与现有业务结合,是一个真正决定这套方案能否落地的事情,业务的马车已经开动起来了,如何保证业务稳定运行的同时,逐步的升级零件变成一辆跑车?
着重聊下应用的混合,针对这个问题我引入一个【多端微应用】的概念。
每个页面构建产物分两种,1. 小程序的页面文件;2. 浏览器的 bundle.js & 路由关系表。
小程序通过混合操作将构建产物与原声小程序进行合并,得到一个可运行的混合小程序。混合操作没 remax 文档中的那么简单,针对原生小程序,或多或少存在一些过去的逻辑,还需要将 app.js 文件进行合并,app.config.js 文件合并等操作,remax 也提供了对应的解决方案途径,具体可查看使用插件 章节。
这块进一步扩展编写了一套混合操作,执行结果示例如下:
info [hybrid] 开始合并混合项目
info [hybrid] 多端输入目录 /Users/noyobo/home/wpt/wpt-miniapp/face-dist/wechat
info [hybrid] 原生输入目录 /Users/noyobo/home/wpt/wpt-miniapp/pre_dist
info [hybrid] 混合输出目录 /Users/noyobo/home/wpt/wpt-miniapp/dist
info [hybrid] 正在清除目录 /Users/noyobo/home/wpt/wpt-miniapp/dist
WARN [plugin-hybrid] 使用原生配置 app.json
WARN [plugin-hybrid] 使用原生配置 project.config.json
WARN [plugin-hybrid] 使用原生配置 sitemap.json
WARN [plugin-hybrid] 多端应用覆盖文件,请回归测试!!!
- pages/bbs/index/index.js
- pages/bbs/index/index.json
- pages/bbs/index/index.wxml
- pages/bbs/index/index.wxss
info [hybrid] 混合项目合并完成!
[22:04:29] Finished '<anonymous>' after 1.63 s
web 端就没那么复杂了,微前端方案有:
他们都是可以支持跨框架运行的,而我当下的场景就更简单了。由于是统一技术栈的,不需要关心跨框架的问题,只要明确路由与 bundle.js 的关联关系,加载时获取对应的 bundle.js 执行即可。主应用负责加载,以及页面堆栈缓存的操作。
主应用这个可以简单讲讲,我称之为 MSPA (MultiInstance SinglePage Web Application) 多实例单页面应用
w.weipaitang.com 可点击体验
看起来是一个单页面应用,它内部还管理着多个页面的实例,可实现 bundle.js 预加载,转场动画,数据预加载等,有效的提高页面的交互体验。
关于 MSPA 可以在后续再进行详细的介绍。
未来
Write once run anywhere 是一个追求的方向,期间会遇到种种让你抓狂的问题,或由于平台特定不得不做出妥协、降级、并存的解决方案,都在不断的朝着我们的目标前行;也不仅仅局限于 Web & 小程序。
这篇文章粗略的聊聊大致的实现过程和落地到实际业务中去,由于篇幅关系不能深入详细的讲解。 也只是 Facejs 多端研发体系中的一小部分,项目创建、开发调试、性能优化、组件生态、自动化测试、发布部署、性能监控等等,都在稳步进行中,微拍堂前端团队也在寻找有一同志向的人员加入,如果你也感兴趣欢迎与我联系。
- 联系方式:nongyb#weipaitang.com
感谢
- Remax 使用真正的 React 构建跨平台小程序