多端研发体系:可渐进迁移的提效之路

618 阅读7分钟

【多端】指 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 接口的入参呢?

  1. ref 在浏览器环境下,返回元素实例,可直接操作。
  2. ref 在小程序环境下,返回 VNode 节点信息,再根据 VNode 里的 id 属性去执行对应的 xx.createSelectorQuery

由于小程序双进程渲染机制,此类 API 都设计为 Promise 的形式。

统一平台样式

无线开发适配方案已经非常成熟了,这里没什么特别的,选择一个适合团队的就可以。目前最佳的实践是 web 模式下将 html.fontSize 设置为 100px (可根据分辨率动态变更)。

统一样式单位 rem 将开发习惯保持与 web 一致。

  1. web 开发模式下无需转换。
  2. 小程序下将 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 端就没那么复杂了,微前端方案有:

  1. icestark 面向大型应用的微前端解决方案;
  2. qiankun 乾坤;

他们都是可以支持跨框架运行的,而我当下的场景就更简单了。由于是统一技术栈的,不需要关心跨框架的问题,只要明确路由与 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 构建跨平台小程序