fanfanxiaozu 2020-03-15
作者:阿里巴巴淘系前端工程师 弗申 逆葵
Rax Github Repo——https://github.com/alibaba/rax
Rax 小程序官网——https://rax.js.org/miniapp
经过持续的迭代,Rax 小程序迎来了一个大的升级,支持全新的运行时方案。站在 2020 年初这个时间点,我们想从 Rax 小程序的特点出发,进行一次全面的梳理与总结,并且在文末附上了 Rax 与当前主流的小程序开发框架的对比。本文将从 API 设计与性能、双引擎架构、优秀的多端组件协议设计和基于 webpack 的工程架构四个方向展开。
当决定一个产品的技术选型的时候,我们往往会从几个方面考虑,(1)可用生态,即周边相关的工具是否满足产品开发的条件;(2)风险率,即出现问题是否能够快速定位解决,所使用的技术是否会持续维护;(3)上手成本,即需不需要很大代价才能达到能够使用的阶段;(4)性能,即能够满足产品既定的性能标准以及用户体验。
本节主要会介绍 Rax 小程序在后面两点上的优势。
框架整体的上手成本是比较小的,Rax 小程序链路从框架上是继承自 Rax(构建多端应用的渐进式类 React 框架)。所以只要你会 Rax Web/Weex 开发或者 React,那么你就会用 Rax 开发小程序,并且可以同时投放到 Rax 所支持的其它端。
但是由于小程序端的特殊性,总会存在无法抹平以及需要单独处理的地方。得益于 Rax 已经做了比较久的多端方案,我们认为,每个端独立的属性不应该入侵基础框架本身,保证基础框架的纯净有利于做更多的扩展。
以下面的代码为例:
Taro:
import Taro, { Component } from '@tarojs/taro' import { View, Text } from '@tarojs/components' export default class Index extends Component { config = { navigationBarTitleText: '首页' } componentWillMount () { } componentDidMount () { } componentWillUnmount () { } componentDidShow () { } componentDidHide () { } render () { return ( <View> <Text>1</Text> </View> ) } }
Rax:
import { createElement, Component } from 'rax'; import View from 'rax-view'; import Text from 'rax-text'; import { isMiniApp } from 'universal-api'; import { registerNativeListeners, addNativeEventListener, removeNativeEventListener } from 'rax-app'; function handlePageShow() {} class Index extends Component { componentWillMount () { } componentDidMount () { if (isMiniApp) { addNativeEventListener('onShow', handlePageShow); } } componentWillUnmount () { if (isMiniApp) { removeNativeEventListener('onShow', handlePageShow); } } render () { return ( <View> <Text>1</Text> </View> ) } } if (isMiniApp) { registerNativeListeners(Index, ['onShow']); } export default Index;
在和 Taro 的对比中,可以看出主要是两点差异:(1)Rax 没有 componentDidShow
componentDidHide
的概念,新增了和 W3C 标准类似的 addNativeEventLisenter
removeEventListener
等 API;(2)组件实例上没有一个叫做 config
的静态属性用来设置页面的 title
等配置。
这就是前文所说的不入侵基础框架本身,React 本身其实是没有 componentDidShow
这些概念,因为这和组件本身的生命周期其实是无关的。我们更期望引导用户用标准的 API 来写业务代码。同时,这种写法的设计带来的还有性能相关的提升,后文会具体说明。
当然这种设计本身会导致代码量一定的膨胀,但是通过工程上的手段,是可以保证最后产物代码的体积几乎毫无差异。
小程序的性能问题是在业务开发中经常会遇到的,为此 Rax 小程序现有的编译时方案也做了很多的努力。通过阿里小程序真机云测的功能,我们对一个无限下拉的列表做了测试。
页面结构如下:
根据真机测试报告,原生小程序三次平均是 2008 ms,Taro 是 2003ms,Rax 是 1924ms,当然其实相差并不多,但是实际的业务场景其实远比上面的页面结构更加复杂。
与 Taro 类似的,Rax 小程序侧的基础框架没有在逻辑层弄一个 VDOM,而是通过数据合并、传统的数据 diff,来避免用户更新冗余的数据。更多的是,阿里小程序原生提供了私有方法 $spliceData
来进行性能优化,Rax 底层会去识别用户需要更新的值是否是数组,然后自动根据场景使用 $spliceData
来优化渲染。
另外需要提到的是,前面说的原生事件监听,小程序本身需要预先注册才能监听事件(这也是为了保障性能),即需要:
Page({ onShow() {} });
而不能动态注册:
const config = {} Page(config); setTimeout(() => { config.onShow = () => {}; }, 1000);
所以加入 componentDidShow
这类概念真的不是好的做法,这会导致页面由于不知道是否需要注册 onShow
属性而将所有的原生事件全部注册监听,这不仅会造成开发者不能灵活扩展,更会导致内存泄漏的风险。
于是 Rax 小程序引入了 registerNativeListeners
,只给开发者一种新的认知,就是需要先在页面上注册事件才能进行监听。这样不仅解决了扩展性的问题,还解决了潜在的性能问题。
当然,Rax 小程序能做的性能优化到此为止了么?在可计划的未来,Rax 小程序编译时方案已经有一些明确的 action,比如进一步减轻框架对 props
更新的管理,更多的利用小程序原生的能力来实现组件更新,从而避免和小程序基础框架做重复的事情导致性能损耗。
Rax (可能)是业界首个同时支持编译时和运行时方案的小程序解决方案。两种方案之间的切换无比简单,我们将高性能 or 完整语法的选择权真正地交给了用户。双引擎驱动的 Rax 小程序架构如下:
下面我们将分别介绍两种编译方案。
Rax 小程序编译时方案是基于 AST 转译的前提下,将 Rax 代码通过词法、语法分析转译成小程序原生代码的过程。由于 JavaScript 的动态能力以及 JSX 本身不是一个传统模板类型语法,所以在这个方案中引入一个较为轻量的运行时垫片。
Rax 小程序编译时架构的核心主要分为两个部分,AST 转译和运行时垫片。下文会针对这两个部分做简要的介绍。
AST 转译部分的架构相比同类产品 Taro 来说,更加清晰以及可维护性更强。这里不得不提到,它的分语法场景转译以及洋葱模型。我们可以粗略的看一下,分语法场景转译部分的代码结构:
可以比较清晰的看到,针对需要转译的每一个语法场景都有一个模块专门负责转译,这就让整个转译的过程轻松了起来,只要每一部分的转译结果符合预期,那么转译结果就是符合预期的。这样的设计可以让我们能够充分利用单元测试来对转译前后的代码进行比较。
而洋葱模型的设计则是AST 转译的另一个主要设计,整个转译过程实际上分为 4 个步骤:
洋葱模型主要进行的是后面三步,在 parser 层将原有的 AST 树修改为符合预期的新 AST 树,然后在 generate 层将新的 AST 树转译成小程序代码。
由于 JSX 的动态能力以及 Rax 原本提供的一些例如 hooks 之类的特性。所以,Rax 小程序编译时方案提供了一个运行时垫片,用来对齐模拟 Rax core API 。
既然引入了运行时,自然可以基于这套机制对数据流做更多的管理,以及提供 Rax 工程在其他端上的 API,比如路由相关的 history
location
等。
Rax 小程序的运行时方案没有自研,而是『站在了巨人的肩膀上』,复用了 kbone 的架构并对其作了一定程度的改造以接入 Rax 小程序的工程体系。关于运行时方案的实现原理可以点击这里查看,此处不再详细介绍。首先需要介绍的是 Rax 小程序同时也是 kbone 的优点:
而在 kbone 的基础上,Rax 小程序运行时方案还新增了不少特性,概括起来有以下几点:
最后,我们也不能回避的是,Rax 小程序运行时方案具有所有运行时方案都存在的问题:性能损耗。事实上,运行时方案就是以一定的性能损耗来换取更为全面的 Web 端特性支持。所以,如果你对小程序有一定的性能要求,建议使用编译时方案;如果对性能要求不高,那么运行时方案就是助你快速开发小程序的利器。双引擎驱动的 Rax 小程序,总有一处能够击中你的内心。
Rax 小程序编译时方案支持项目级开发和组件级开发。与 Taro 将组件统一在项目中进行编译产出为小程序代码不同,Rax 在组件工程中即可构建出小程序组件。结合一套优秀的多端组件协议设计,我们做到了在 Rax 小程序项目和原生小程序项目中都能正常使用 Rax 小程序组件,同时保持统一的多端开发体验。该协议定义在 package.json 中的 miniappConfig
字段中,其具体用法设计可以参见文档 Rax 小程序——多端组件开发。
对于那些已经使用原生语法开发了完整的小程序的开发者来说,一个很合理的需求就是渐进地切换到 Rax 开发链路上来,毕竟整个项目迁移可能成本高昂。而 Rax 依托多端组件协议,能够帮助开发者平滑过渡。
按照设计,Rax 小程序组件工程的构建产物为符合小程序语法的组件,因此其理所当然可以直接在原生小程序项目中使用。这意味着,如果你想渐进式地使用 Rax 来开发小程序,可以以组件或者页面为单位将之前使用原生语法开发的小程序逐渐地迁移到 Rax 上来。而这,也是 Taro 等其他框架不具备的能力。在 Rax 的使用方中,浙江省网上政务平台『浙里办』支付宝小程序即采用了渐进式接入 Rax 的方式。
当使用 Rax 组件工程发布的小程序组件在 Rax 项目中使用时,构建器会自动通过 miniappConfig
规定的路径去寻找该组件的小程序实现从而实现替换。用户在业务代码编写层面无需像传统引入原生小程序组件的方式一样写具体路径,而是与 Web/Weex 端保持一致即可,示例如下:
// Wrong import CustomComponent from 'custom-component/miniapp/index' // Correct import CustomComponent from 'custom-component'
除此之外,多端组件协议还可以扩展成多端组件库协议,支持更灵活的类似 import { Button } from 'fusion-mobile'
的写法。以 Rax 多端组件协议为基础,你可以快速为你的多端项目开发通用组件或者组件库。比如,Rax 基础组件就都是以该方式开发的。
Rax 工程以阿里巴巴集团前端统一的 CLI 工具 @alib/build-script 为基础,其依赖 webpack,通过插件体系支持各个场景,同时基于 webpack-chain 提供了灵活的 webpack 配置能力,用户可以通过组合各种插件实现工程需求。Rax 小程序的编译时方案通过 webpack loader 来处理自身逻辑。以 app/page/component 等文件角色分类的 webpack loader 会调用 jsx-compiler 进行代码的 AST 分析及处理,再将处理完的代码交由 loader 生成对应的小程序文件;运行时方案直接复用 Web 端的编译配置,再通过额外的 webpack 插件生成具体的小程序代码。
相比其他自研的工程体系,整套架构具有如下优点:
最后,附上 Rax 和现有主流小程序框架的对比。
以上是我们对 Rax 小程序的核心竞争力的阶段性总结与思考。小程序已经不是初生牛犊,小程序的解决方案也早已汗牛充栋,但我们相信,Rax 的入局,会让你的小程序开发有那么一些不一样。
更多关于 Rax 小程序的内容,欢迎访问 https://rax.js.org/miniapp 了解!