RN 转小程序引擎 Alita 2.0 正式发布:基于 webpack 进行重构

阅读数:3 2020 年 1 月 2 日 16:41

RN转小程序引擎Alita 2.0正式发布:基于webpack进行重构

ReactNative 是 Facebook 推出的跨端框架,可以用一套代码开发 Android、iOS 应用,借助社区的 react-native-web 框架,ReactNative 应用还可以运行在浏览器中。那么有没有办法把 ReactNative 应用运行在国内流行的小程序平台呢?

这就是今天要介绍的 Alita 工具引擎,近期 Alita 发布了 2.0 版本,其构建方式基于 webpack 进行了重构,借助 webpack 的灵活性,2.0 版本带来了诸多新特性,包括完善 npm 支持,包大小分析,一键小程序分包等等。Alita 侵入性很低,选用与否,并不会对你的原有 ReactNative 开发方式造成太大影响。看下实际效果(web 由 react-native-web 运行提供)。

RN转小程序引擎Alita 2.0正式发布:基于webpack进行重构

下面我们从基本原理、2.0 版本新特性、性能优化这三个方面介绍一下 Alita。

基本原理

与现有很多编译时方案不同,Alita 对 React 语法有全新的处理方式,支持在运行时处理 React 语法,JSX 语法在运行期仍然是 React.createElement 调用。Alita 的整体架构借鉴了 ReactNative,其上层存在一个为小程序定制的 mini-react,底层是负责实际渲染的小程序原生代码。而中间存在一个两层互相联系的 bridge。

mini-react 负责运行所有 React 代码逻辑,包括递归的构建组件树结构,创建组件实例,执行组件对应生命周期,context 计算等等。其最终将生成一份描述小程序视图的数据。这份数据通过 bridge 模块传递到底层小程序。底层小程序实例调用 setData 方法把数据刷给自身,完成渲染。

另外这种架构及渲染方式,我们在 flutter 平台也做了些许的尝试。

RN转小程序引擎Alita 2.0正式发布:基于webpack进行重构

2.0 版本新特性

自 2.x 版本以后,Alita 改为基于 webpack 打包构建,借助于 webpack 的特性,2.x 版本的 Alita 更加简单易用。

完善 npm 包支持

微信小程序有一套自己的 npm 包使用方式,这种方式并不完全和官方 npm 兼容,这会导致很多常见的包不能直接在小程序平台使用。比如很多包都是以如下的方式提供

复制代码
if (process.env.NODE_ENV === 'production') {
module.exports = require('./index.production.min.js');
} else {
module.exports = require('./index.development.js');
}

但是微信小程序平台并不支持 process.env.NODE_ENV,导致以上的包无法使用,会报错Uncaught ReferenceError: process is not defined

另外小程序怪异的 npm 包使用方式,要求其在安装之后手动执行npm 构建过程,这给本地开发调试包带了很多麻烦。

Alita 基于 webpack 的打包等于是移除了对小程序 npm 能力的依赖,整个打包过程更加可控。另外借助于 webpack 灵活的 loader/plugin 机制,可以实现诸多常见特性。比如借助 DefinePlugin 插件就可以处理 process.env.NODE_ENV,实现在开发模式 / 生产模式下加载不同包的需求。

复制代码
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
});

小程序体积分析及优化

我们知道微信小程序的体积是有限制的,不能超过 2M。通过 Alita 运行的小程序上当然也有这样的限制,好在 webpack 内置了 tree-shaking,但是仍然会有体积超过 2M 的情况,此时如何减少小程序大小呢?首先需要知道每一个文件、每一个包占据了多少空间。借助 webpack 的 BundleAnalyzerPlugin 插件(Alita 内置了这个插件),事情变得非常简单,只需要在执行 Alita 打包命令的时候添加 --analyzer 参数即可。下面是 Alita 提供的案例代码 Todo,添加–analyzer 参数之后的大小分布结构。

RN转小程序引擎Alita 2.0正式发布:基于webpack进行重构

有了这份大小分布图,优化变得有的放矢。这里 lodash 库占据了很多空间,有 500 多 KB。实际上,通过分析发现只有 redux-promise 引用了 lodash , 使用了个别的 lodash 库方法,我们可以发布一个简单的包 redux-promise-nolodash 替换一下即可。再次执行 alita --analyzer ,结果如下,bundle 的大小由 700 多 KB,降到了 200 多 KB:

RN转小程序引擎Alita 2.0正式发布:基于webpack进行重构

上面的方式,需要手动把代码中对 redux-promise 的引用改为 redux-promise-nolodash,当文件很大项目已经成熟的时候,显然不太方便。更简单的,由于 Alita 基于 webpack 构建打包,所以 webpack 的配置字段基本上可以在 Alita 的配置文件中复用,只需要配置 resove alias 字段即可:

复制代码
module.exports = {
resolve: {
alias: {
"redux-promise": "redux-promise-nolodash",
}
}
}

一键自动小程序分包

有时当小程序越发复杂的时候,体积的确是需要超过 2M 的,此外为了减少首屏加载时长,需要减少初次加载的内容。这就需要使用到分包的能力,使用分包以后小程序体积可以支持到 12M。小程序的分包如下:

RN转小程序引擎Alita 2.0正式发布:基于webpack进行重构

小程序要求你在分包的时候,主从包的依赖必须手动管理好,比如分包 A 不能依赖分包 B 的代码,当业务越发复杂,每一个模块之间的依赖关系将变的特别复杂,这给小程序原生的手动分包带来了很多麻烦,比如我们必须手动把共用依赖提取到主包中维护等诸如此类。

但是,管理模块依赖是 webpack 的强项!webpack 很早就有提取公共模块拆分包的插件 CommonsChunkPlugin,webpack4.0 以后更是提供了更加强大的 SplitChunkPlugin,基于 SplitChunkPlugin 插件 Alita 实现了非常易用的自动分包。比如:

复制代码
<Router>
<Route key={"A"} component={A}/>
<Route subpage={'sub1'} key={"B"} component={B}/>
<Route subpage={'sub1'} key={"C"} component={C}/>
<Route subpage={'sub2'} key={"D"} component={D}/>
<Route subpage={'sub2'} key={"E"} component={E}/>
</Router>

只需要在路由配置中添加 subpage 属性即可!如上的配置,Alita 将把你的目标小程序分成 3 个包:主包包含 A 页, sub1 分包保护 B,C 页,sub2 包含 D,E 页。共用依赖模块将会被提取到主包,而 B,C 单独依赖模块项将只会在 sub1 分包存在,整个过程不需要任何的手动干预,也不需要开发者自己去管理模块直接的依赖关系。

借助于 webpack 灵活的 plugin/loader 机制,整个 Alita 构建打包的过程更加可控,扩展功能更加方便。官方及社区丰富的插件,也帮助我们实现了自动分包,包体积分析优化等诸多功能。

性能相关

通过 Alita 运行的小程序性能如何呢?后续文章我们会带来更加详细的性能评测数据,本文先从整体架构上分析一下性能相关的问题。正如前文所说,Alita 运行阶段存在两个过程,首先是上层的 React 运行阶段,包括递归遍历组件树,产生渲染数据等。然后通过 bridge 把这份数据刷给小程序,小程序接收到数据之后通过调用自身 setData 渲染。由此可见组件的渲染耗时主要由两个部分组成:React 计算时长和小程序渲染时长。

下面分别说明。

setState 合并

首先,Alita 会尽量减少整个渲染过程的频率,移除无效渲染。具体的,Alita 并不会对每次 setState 都执行计算过程,熟悉 React 的同学都知道,React 会尝试合并每次的 setState。 比如

复制代码
class A extends Component {
componentDidMount() {
this.setState({})
this.setState({})
this.setState({})
}
...
}

上面的 A 组件 componentDidMount 调用了 3 次 setState。更加复杂的情形

复制代码
class A extends Component {
componentDidMount() {
xxx.setState({})
yyy.setState({})
zzz.setState({})
}
...
}

componentDidMount 分别调用了 xxx,yyy,zzz 组件的 setState。类似 React,Alita 的 mini-react 合并策略会保证上面的所有这些 setState 只发生一次实际的计算过程。

小程序批量更新

每一个上层 React 组件,Alita 都会在小程序端生成一个对应的小程序自定义组件来负责这个组件的渲染,这保证了局部刷新只会影响局部组件,但是也带来了问题。当计算过程结束,渲染数据传递过来的时候,会出现很多小程序组件同时需要调用 setData 更新自身的数据的情况,这在列表数组渲染多个组件的时候,尤其明显。而 setData 调用是小程序最耗时的操作,频繁调用会产生卡顿。有没有办法一次 setData 交互,就把所有小程序组件数据更新完成呢?答案是可以的,Alita 内部利用了小程序 groupSetData,实现了小程序批量更新,本质上每次上层 React 计算之后,底层小程序只需要交互一次就可以完成所有数据的刷新。这在页面复杂,组件众多,尤其是大量组件同时频繁更新的业务场景下,会有非常显著的性能提升。

RN转小程序引擎Alita 2.0正式发布:基于webpack进行重构

冗余数据与增量修改

state 中的所有数据都会影响视图吗?不一定。有的时候我们会偷懒直接把后端返回的数据放在 state,或者放置一个复杂的对象却只用到个别属性渲染,比如一个 Date 对象。所以一般来说 state 里面都会存在不涉及视图渲染的冗余数据,这些数据在 mini-react 的计算过程中会自动移除掉,传递给 bridge 的渲染数据只涉及视图相关数据。

更近一步,这份数据还会被 diff 处理,最终小程序 setData 设置的只是极简的增量数据。

RN转小程序引擎Alita 2.0正式发布:基于webpack进行重构

比如类似以上的新旧数据,diff 之后只有 age 字段发生了改变,所以小程序实际更新只会执行:setData({“person.age”: 19})

除了上文提及的优化之外,Alita 还会对 ReactNative 官方组件节点做特别的优化,View,Image 等组件会直接用小程序 view,image 组件替代,另外 Alita 还会尝试扁平过深的节点结构。

结束语

“Talk is cheap. Show me the code!!”,Alita 的所有代码都托管在了 github,包括上文提到的 flutter 渲染在小程序的方案(flutter 方案由于时间关系,更新非常有限)。地址如下:

Alita: https://github.com/areslabs/alita
flutter_mp: https://github.com/areslabs/flutter_mp
欢迎使用 & 交流 & issue & star !!

作者介绍:
京东 ARES 严康、刘艳

评论

发布