ReactNative是Facebook推出的跨端框架,可以用一套代码开发Android、iOS应用,借助社区的react-native-web框架,ReactNative应用还可以运行在浏览器中。那么有没有办法把ReactNative应用运行在国内流行的小程序平台呢?
这就是今天要介绍的Alita工具引擎,近期Alita发布了2.0版本,其构建方式基于webpack进行了重构,借助webpack的灵活性,2.0版本带来了诸多新特性,包括完善npm支持,包大小分析,一键小程序分包等等。Alita侵入性很低,选用与否,并不会对你的原有ReactNative开发方式造成太大影响。看下实际效果(web由react-native-web运行提供)。
下面我们从基本原理、2.0版本新特性、性能优化这三个方面介绍一下Alita。
与现有很多编译时方案不同,Alita对React语法有全新的处理方式,支持在运行时处理React语法,JSX语法在运行期仍然是React.createElement调用。Alita的整体架构借鉴了ReactNative,其上层存在一个为小程序定制的mini-react,底层是负责实际渲染的小程序原生代码。而中间存在一个两层互相联系的bridge。
mini-react负责运行所有React代码逻辑,包括递归的构建组件树结构,创建组件实例,执行组件对应生命周期,context计算等等。其最终将生成一份描述小程序视图的数据。这份数据通过bridge模块传递到底层小程序。底层小程序实例调用setData方法把数据刷给自身,完成渲染。
另外这种架构及渲染方式,我们在flutter平台也做了些许的尝试。
自2.x版本以后,Alita改为基于webpack打包构建,借助于webpack的特性,2.x版本的Alita更加简单易用。
微信小程序有一套自己的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 参数之后的大小分布结构。
有了这份大小分布图,优化变得有的放矢。这里lodash库占据了很多空间,有500多KB。实际上,通过分析发现只有redux-promise引用了lodash ,使用了个别的lodash库方法,我们可以发布一个简单的包 redux-promise-nolodash 替换一下即可。再次执行 alita --analyzer ,结果如下,bundle的大小由700多KB,降到了200多KB:
上面的方式,需要手动把代码中对redux-promise的引用改为redux-promise-nolodash,当文件很大项目已经成熟的时候,显然不太方便。更简单的,由于Alita基于webpack构建打包,所以webpack的配置字段基本上可以在Alita的配置文件中复用,只需要配置resove alias 字段即可:
module.exports = {
resolve: {
alias: {
"redux-promise": "redux-promise-nolodash",
}
}
}
有时当小程序越发复杂的时候,体积的确是需要超过2M的,此外为了减少首屏加载时长,需要减少初次加载的内容。这就需要使用到分包的能力,使用分包以后小程序体积可以支持到12M。小程序的分包如下:
小程序要求你在分包的时候,主从包的依赖必须手动管理好,比如分包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计算时长和小程序渲染时长。
下面分别说明。
首先,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计算之后,底层小程序只需要交互一次就可以完成所有数据的刷新。这在页面复杂,组件众多,尤其是大量组件同时频繁更新的业务场景下,会有非常显著的性能提升。
state中的所有数据都会影响视图吗?不一定。有的时候我们会偷懒直接把后端返回的数据放在state,或者放置一个复杂的对象却只用到个别属性渲染,比如一个 Date对象。所以一般来说state里面都会存在不涉及视图渲染的冗余数据,这些数据在mini-react的计算过程中会自动移除掉,传递给bridge的渲染数据只涉及视图相关数据。
更近一步,这份数据还会被diff 处理,最终小程序setData设置的只是极简的增量数据。
比如类似以上的新旧数据,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严康、刘艳
领取专属 10元无门槛券
私享最新 技术干货