大家好,我是来自阿里巴巴淘系技术部前端架构团队的弗申,今天我给大家分享的主题是如何实现跨端框架的标准化研发模式。
前端发展到现在,工程架构日趋复杂,业务需要投放到的容器也不尽相同,那么大家在不同的业务领域可能会有自己不同的业务诉求,在大背景下怎么去打造一个标准化的研发模式,以及一个同基础通用的跨端研发框架,这件事情变得更加重要。
因为技术体系的发展决定了业务架构的鲁棒性,所以接下来我会围绕阿里巴巴集团使用面最广的渐进式 React 的框架 Rax,来给大家分享一下我们在框架设计方面的一些思考,以及在这次分享过程中会从一些比较小的点,着手分析业务中是真实会遇到的问题,然后讲解我们是怎么去解决以及思考的。
我们来先简单看一下这次分享的提纲,主要是四个部分,重点是前面两个部分。
在设计框架的时候,我们需要用到三段式思维方式来完成设计,就是 Why、What、How。
Rax 的跨端方案诞生的时间大概是在2015年,容器在不断的发生变化。因为当时有 WebView,然后有 ReactNative,还有 Weex。对我们当时社区有了 React Native 的情况下,为什么我们要有 Rax 呢?
首先第一个是当时我们阿里巴巴集团的前端大部分的技术栈,由于我们当时是 Allin 无线,所以大家的技术栈还是在 React 那边,因此在集团有 Weex 的情况下,我们需要让开发者能够使用 React 来开发 Weex,并且同时我们的业务需要投放到 Web,那么我们跨端方向的一个大背景就是容器在不断发生变化。
第二个就是一次编写,需要多端投放的业务诉求。
第三个就是前端技术生态比较丰富,我们希望能够尽可能的运用前端生态技术来解决一些跨端的问题。
Rax 诞生的背景就是 React 加上 Weex,即 Rax 是一个渐进式的 React 框架。在社区有了 React Native 的情况下,为什么需要 Rax ?Rax 解决了哪些问题?React Native 首先在那个时代背景下是没法支持 Web 的业务场景的。发展到现在 React 也有自己的 React Reconcile 的概念,可以基于它去做一些多端的事情。但是在当时那个时候是没有的,然而我们的业务又有需要投放到 Web 的场景,这是一个强诉求。
第二点是 React Native 类似方案 Weex 的诞生,但是 Weex 当时是 DSL 是 hack 了一份 Vue 对与我们的 PC React 为主的技术栈是不匹配的。
第三个是我们尝试将 React 和 Weex 直接相结合的时候,发现有很多问题,包括一些性能上的问题。实际上大家对 React 是比较了解的,其实可以看到 react-dom 这个包,它在 gzip 之后,其实体积还是比较大的。
Rax 解决了什么问题, Rax 主要核心解决了下面三个问题。
因为这一切的原因,所以诞生了 Rax。所以在目前为止,因为 Rax 发展了到今年已经大概是第5年了。Rax 已经广泛的应用在阿里巴巴集团的各个场景,包括历年的双11,覆盖的 BU 有淘系、飞猪、优酷、阿里巴巴等等。
接下来我给大家分享一下我们跨端框架的设计思路,希望能够让大家在做业务技术选型的时候,或者说是在架构设计的时候,给大家带来一些帮助和启发。
我们先来看一下框架体系的概览。首先底层我们的 DSL 的标准是遵循 React 的,Rax DSL 再加上一个 JSX Plus 的规范,大家对 JSX Plus 可能不够了解,给大家简单介绍一下:JSX Plus 实际上就是能够在 JSX 上面去用一些简单的指令,比如说像 Vue 里面的 v-if,或者说 v-for 通过指令来做一些更加简便的操作。
往上面就是我们的工程体系,工程体系的最底层是 Build Scripts,这个是基于“阿里巴巴前端委员会”共建的一套工程体系底层能力,可以让开发者通过插件的方式扩展 Webpack 配置。再往上是 Rax-App 研发框架,这套研发框架是基于Build Scripts 去设计的,其中包含了丰富的运行时功能,以及开箱即用的工程配置能力,最终能够让大家的代码实现一码多端,也就是一次编写就可以跑在多个容器上面。更上层是我们的基础生态体系,包括跨端基础组件,以及跨端 API。往上最后一层就是生态体系,包括组件库等等。
我们希望这一整套体系给到开发者的是:开发者不用去关心底层容器是什么,只需要使用 Rax 提供的 DSL ,包括我们提供的工程,以及多端体验一致的一些生态体系、组件、 API 等等,就可以运行到 Rax 支持的所有容器当中。比如说 Web 、小程序,这里小程序包括微信小程序、阿里小程序、字节跳动小程序等等。还有 Flutter ,基于 Kraken 方案的一个 Flutter 的方案。然后还有 Weex 等等。
接下来我给大家介绍一下我们的 DSL 设计。
Rax DSL 本身是遵循 React 的标准的,也就是说开发者完全可以用熟悉的 JSX 语法来开发业务,然后没有任何的上手成本。当然和 React 类似,Rax 也是同样是基于 VDOM 的,为什么选择 VDOM ?有些同学可能会说 VDOM 树可以减少操作真实 VDOM 的频率,从而提升性能。其实事实并非完全是这个样子的,因为 VDOM 带来的性能优势从来不是最大的,在现代浏览器中直接操作真实 DOM 效率或者说是性能,可能比 VDOM 的效果更好,性能更高。
那么我们为什么选择为 VDOM 呢?VDOM 给我们带来了主要是有两方面的收益:
接下来给大家介绍一下我们基于 VDOM 怎么去做一个跟容器解耦的事情。我们是基于 Driver 的概念,站在今天这个角度来讲的话,大家可能比较熟悉的一个概念是 React 中的 reconciler。其实这种方案和理念, Rax 比 React 提出的要更早一些。也就是说我们通过 Driver 实现的这些让大家乍一看很像 DOM API 的函数,能够抹平对应容器的渲染能力。比如说 getElementById 这个方法,在 Rax DSL 里面,因为我们在 Rax 核心库里面相当于是直接去调用的 Driver.getElementById 方法,所以 Rax DSL 本身是不关心 getElementById 是怎么实现的,我们可以针对特定的容器来实现这个方法,从而达到跨端的一个目的。
我们的研发框架主要分为三个部分:第一部分,框架运行时:它提供了整个 APP 级别的就是应用级别的生命周期,包括
第二部分,工程构建能力
第三部分,也是比较重要的一部分,就是我们的高度插件化的能力。
这样的一种扩展运行时能力带来的一种收益是什么?假如说你的业务需要一个跨端的能力,那么你可以选择 Rax APP 研发框架。当你的业务到了一个深水区,比如说是互动领域,你有自己的一些垂直的 API 需要去注入或者是有自己的品牌,你可以基于我们的 Rax APP,再去做一个更上层的框架,然后在上层框架里面去注入。以互动领域为例,可以注入互动的一些运行时的 API ,例如:import xxx from 框架名,同时你还享有 Rax APP 提供的一些基础的运行时API。
下面给大家来介绍一下我们框架运行时,在此之前我们先看一下整个研发框架的概览。我们的整个设计思路:
首先是下面的基础能力:
再往上层是我们的框架核心:也就一个是工程构建中台,我大概讲一下里面包含了各种比如说 CLI 注册一些参数,你可以通过 --https 去开启 https 的一些调试能力,比如说是状态管理这些能力都是通过构建中台去包装的。
再往上层是渲染器:其中包括小程序的渲染器,例如更新小程序的视图。第二个是 Rax 应用的渲染器。第三个是 React应用的渲染器。可以在底层通过 Rax 或者是 React 的 setState 操作来更新视图。
更上层的是容器:我们支持 Web、小程序、Weex 、Kraken ,其中 Kraken 是 Flutter 的方案。
再往上层是核心框架:我们这套整个下层的体系支持了 icejs(中后台),还有一个是 Rax APP 无线跨端。
更往上层的业务框架:已经有很多团队开始基于我们的框架去做他们自己的研发框架。比如说阿里云团队,政务钉钉或者是阿里体育团队。
继续讲我们的框架运行时。
我会从后面几个比较小的点来着手分析,通过框架运行时的一些能力,我们解决了怎样的问题。
首先第一部分是生命周期。如果大家接触 React 多的话,可能对 componentWillMount、componentDidMount 这些组件的实例的生命周期比较熟悉。完整的生命周期,更多的指的是什么?更多指的是一个应用的生命周期。如果大家开发过小程序可能比较清楚,就是小程序的一些生命周期。
完整的生命周期可以做哪些事情?
所以基于上面这些需求,以及一些业务上的真实痛点,我们做了一个多端体验一致的生命周期,主要分为两部分:
这里同学会问说为什么 usePageShow 、usePageHide 这里称为 Hooks,而 onShow,onHide 称为事件。这是因为在社区里面我们看到的常见的 useXXX,其实就是 API 的调用,并不是真正的 Hooks,也就是说这种 API 的调用方式跟 Hooks 本身并没有关系。我们这里的 usePageShow 其实是真正的 Hooks,它跟 onShow 不同之处在于,ousePageShow 的触发时机是在组件渲染完成之后,而 onShow 是在组件示例初始化的时候,也就是页面刚进来的时候,就会触发这个 onShow 事件。
我们还提供了基于小程序的若干小程序独有的声明周期。如果有接触过小程序的同学应该知道,诸如页面触底的一些事件。无论是在 Weex,还是在 Kraken ,还是在小程序,或是在 Web 等等业务形态上,我们的方案表现都是完全一致的,包括触发时机和触发顺序,这样能够让你的业务逻辑在开发时保持完全一致。usePageShow 是在组件渲染完成之后,onShow 是在组件实例化的时候,也就是页面刚进入时。
接着我想介绍的这个是我们的生命周期的一个演示代码。左边是应用入口的演示,在应用入口中注册自己的生命周期,然后右边是页面的演示 usePageShow 和 usePageHide,你可以尝试这样去做。大家可以简单看一下。
接下来主要是给大家介绍的是我们的状态管理,其实飞冰这个产品也是属于我们团队的,我们核心能力也是基于飞冰 ICE Store 提供的状态管理的能力。我们希望能够把状态管理这件事情做得更加简单,更加符合业务使用的直觉,而不需要自己去包裹 Provider 或者是去理解更多的概念,你只需要用一些最基础的能力就可以完成状态管理。
首先是创建 store,你可以在 src 里面新建一个 store.ts 的文件,你可以去创建 state 、 reducers 、effects。分别对应 redux 里面的一些概念,即 state、 reducers 、还有会产生副作用的一些方法。在页面里面可以去消费这些全局的状态,可以直接去 import store 就是 APP 的 store ,从根路由引入。
然后通过 useModel 的方法拿到 state 的状态,通过 APP 上的 dispatchers 去触发一些视图上的更新,就可以很轻松的做到状态管理的 0 成本上手。只需要简单的看一下示例代码,就可以完全涵盖到无线端场景的大部分状态管理的诉求。当然由于我们提供了内置的状态管理的能力,包括一些注入的方法。这样可能导致我们的包体积变大,但我们希望在无线端给到大家的方案是足够轻量的,所以在一些无线端比较简单的页面中,可能业务没有必要去用到状态管理,也不建议大家在无线端去大量的使用状态管理。
因为无线端我们尽可能的把事情做简单,做的更轻量,所以我们提供了一种方式,就是在我们的配置文件里面,就是 build.json 里手动关闭状态管理。可以用很简单的一个开关,设置 store 为 false ,就可以关掉状态管理。根据我们关闭 store 前后的打包文件的体积对比,在打开 gzip 压缩的之后,关掉状态管理,我们的代码体积可以缩小大概 20KB ,在一个无线端场景 20KB 其实可以算是优化中的一个比较重要的指标。
关于路由方案,我们采用的是标准协议约定式路由,在 app.json 里面配置路由。以 home 页面为例,设置 path 、source(页面来源),同时给 home 页面配置一个 title,可以用这种方式创建页面。
还可以对 tabBar 进行配置,利用刚才提到的协议配置,可以很容易的在应用中配置 tabBar, 由于我们有一套相对完美的降级到 Web 应用的方案,所以这个 tabBar 的配置能力不仅仅作用于小程序,也可以在 Web 应用中实现对应的 tabBar 功能。
路由操作及数据获取,通过 Rax App 中引入 history 、getSearchParams 就可以去很轻松的拿到我们需要的当前页面的history 相关数据。在 react-router 中,也提供了类似的 useHistory,useLocation 这样的API,其底层是利用了 React 的 useContext 的能力,使用了像 hooks 类似的命名,但其实它们与命名并没有直接关系,所以在 Rax App 中我们把它设计成为直接可以获取,并进行操作。类似的 getSearchParams 也是替换了原来的 hooks 写法。
从架构设计的角度来说,history 的设计思路是什么呢?由于我们要适配多端,所以我们需要支持 Web、Weex 以及 Kraken。我们使用的是社区中的一个 history 包提供的能力。但是由于小程序本身没有 history 的概念,所以我们模拟了 mini-app 自己去封装实现了一套 history 的 API 暴露给用户使用。这样可以帮助用户向用其他端一样在小程序中调用诸如 history.push、history.replace 等等方法。对开发者而言,只需要理解一个 history 概念就好。
在实际开发过程中,常常遇到以下的问题:
这些问题在 Rax App 中都变得很容易。比如通过 npm start -- --https 就可以开启本地的 https 调试,通过 npm start -- --analyzer 就可以一键开启依赖分析,并不需要额外的引入社区的插件。添加 babel 插件,只需要在配置中添加 babelPlugins 对应的插件即可,不再需要去社区里找一堆需要引入的插件。
其他的一些配置,比如可以通过简单的配置 minify 关闭压缩,这在开发环境中常常用到。通过配置 proxy 即可将 API 请求代理到本地,以此来解决跨域问题。同样的环境变量也可以不再依赖 process.env 这种方式,只需要在 define 中配置对应的变量即可注入业务中需要的环境变量。
一码多端能力,通过如下图所示在配置文件中写入对应的 targets 端,就可以一次命令同时开启多个端的业务构建,如 Web、Weex、Kraken、小程序等等。同时也支持 Web 中的 SSR,MPA 的特殊需求。
针对小程序,也有小程序独有的配置,包括针对微信小程序或者阿里小程序的比较独特的一些配置。同时后面还会跟大家说到一个小程序的特有 runtimeDependencies 的配置项。
代码体积,在一码多端的场景下,代码体积的大小是一个很重要的性能指标。Rax App 中是通过特定的 Babel 插件 rax-platform-loader 来判断不同的端场景,并只保留对应的端场景下的代码,从而控制代码体积。
现在我们介绍刚才卖的关子——小程序的实现部分。我们小程序方案主要是基于双引擎来驱动的一个完整的小程序开发体系。以此来满足开发者既要高性能,同时也要代码的灵活度的需求。
编译时引擎,比如像 Taro (应该是 Taro 1.x、Taro 2.x 的版本)更多的是基于编译时的引擎,将 AST 语法树在编译打包时就进行解析,以保证能够 1:1 的尽可能还原到小程序的 DSL ,并能正常运行。
运行时引擎,实现起来并不复杂,本质上就是将 VDOM 树通过小程序的操作 setData ,然后去遍历递归,最终渲染到模板上。当然这其中涉及到很多渲染时的优化,比如如何避免频繁的渲染操作,如何能够支持不同的小程序,因为不同平台的小程序底层实现的方式也是有所差异的,因此优化方式也会有所区别。
如图中所示,Rax App 其实是同时支持了编译时引擎和运行时引擎两种模式。
小程序编译时的引擎,是通过对 AST 树的解析,解析用户的一些条件的代码如 map 的 list 循环,在编译时会把它和小程序做吻合解析,识别出 map 循环,并将它转化成小程序的 wx-for 等语法。整个编译时的引擎,主要是一个洋葱式的模型。如果了解过 koa 框架,应该比较容易理解洋葱式的模型是什么概念。每一个文件进来之后,它会被解析成 AST 树,再出去的时候,我们会做一些 AST 的修改,再转成代码文件,最后产出小程序的代码。
运行时垫片,完整的模拟了 React / Rax 的核心 API。即在编译时里提到的 useEffect 其实和 React / Rax 的 useEffect 完全不同,是另一套实现。从下图中可以看出,运行时垫片其实是将小程序实例与 Rax 组件实例进行了相互绑定的操作。即当 Rax 组件实例更新,会反馈到小程序实例进行改变;而小程序实例的创建、销毁、更新都会通知到 Rax 组件实例。
在此之上,还实现了 Rax 的一些核心 API,如 Hooks、Component、Event、Lifecycle 等等。
运行时方案的背景,弥补编译时方案不足
运行时方案的特点,用性能换取完整语法支持的诉求
基于 Rax 体系,如何实现小程序运行时这套方案?
实现小程序运行时的这套方案,需要了解之前提到的 dirver 的概念,在 Web 和 Weex 的应用端会模拟操作 DOM 的 API。在小程序端,其实是通过 worker 线程调用并计算需要渲染的 DOM ,传递给 render 线程,然后通过小程序的 setData (微信小程序)/ $spliceData (阿里小程序)进行视图渲染。
在开发中,往往我们有各种各样的要求:比如开发者希望能够拥有运行时的能力,同时又想拥有使用小程序原生组件的能力;想使用小程序编译时的组件,想节省渲染时的节点数,又想在 worker 和 render 之间传递更大的 JSON 数据;想提高渲染效率,又想让工程师能够使用我们已有的方案体系,组件体系等等。
针对上述的这些要求,在社区里,比较而言,Rax App 其实是更符合这类需求的综合类的解决方案。
下图示例中,向大家说明了如何做到小程序的双引擎混用。其中左边为双引擎混用的工程目录示例,右边为使用方法示例。
插件化包括:运行时插件和工程插件。工程插件是基于 build scripts 实现。框架运行时插件可以通过自己开发一个插件,然后在 runtime.tsx 的文件,利用框架暴露的一些 API 在业务项目中使用的 Rax App 核心包注入运行时的能力。当开发者使用时,可以直接从 Rax App 中导出已经注入的运行时能力。
下图示例了如何开发和使用插件的示例。
Rax 的框架架构图,主要包括 2 个部分,框架中台和框架品牌。例如 rax-app 和 ice.js 其实都是基于框架中台实现了,这也意味着开发者可以根据自己的需求去自定义的封装出适用于自身业务的框架,甚至也可以将自己定制化开发的框架开源,回馈社区。
简单列举了框架提供的常见的一些 API ,详细的可以去官方文档中进行查阅。
下图示例了框架为用户封装的业务组件和基础组件的示例,详细的可以去官方文档中进行查阅。
Rax 的使命,是让多端开发简单而美好。Rax 的愿景,基于前端生态打通多端体系。
我们希望能够融合丰富的前端生态,包括 npm 以及前端其他很美好的东西,我们希望能够把他们引入到多端体系中。而不希望用户由于今天有了 Flutter,需要去学习 Dart,有了另外一个框架,就需要学习另外一门语言,或者是另外一个生态体系。
我们就希望前端能够用自己的生态体系来做各个平台的渲染。关于渲染这件事情,我们希望能够让它变得更简单,入门更加容易。
为什么选择 Rax APP?Rax主要是有6个点:
高度可扩展性,即用户可以无限的扩展自己的需求。
经过充分的业务验证,是因为我们有一个可靠的团队去维护它,保证它的稳定性。大家如果用过飞冰应该知道,飞冰已经运行维护好几年了,同样的 Rax 也是运行维护好几年了。同时阿里巴巴集团内也有大量的业务去验证这套技术方案。
团队做技术选型,可以分为 6 个部分:
代码可维护性,一个更小的团队,比如说 3~5 个人 5~10 个人的团队,要去开发一个业务代码,可维护性的迫切程度是相当高的。Rax App 提供了跟 icejs 一样的一个 ESLint 标准,或者说是一个基于 iceworks 的代码健康检查插件,包括智能化辅助工具,可以让你很好的去管理你的项目,能够让你的项目的代码健壮度更高,能够有更大更高的稳定性。
多场景的支撑,在业务的场景很多的情况下,在做技术选型的时候,不能只考虑当下,需要考虑未来 1 年或者是 3~5 年之后你所选择的框架如何适应业务,而不是成为一个历史包袱。我们希望在业务中做技术选型的时候,无论是选择了 Rax 还是飞冰这样的技术方案,都能够对你的多业务场景提供可持续的维护性,编写高质量的代码。
业务可扩展性,当你今天在完成一个 A 需求时,能够为明天的 B 需求做好铺垫,提前做好对应的考虑。
可用生态,Rax 选择 React 作为标准的原因,以及为什么是一个 React 的渐进式框架?是因为 React 生态是足够的丰富的。我们希望能够有更多的生态可以直接在 Rax 中使用。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8