前端未来的主流技术方向有哪些?腾讯、京东、同城旅行等大厂都是怎么布局的?戳此了解 了解详情
写点什么

深入剖析 Alita 引擎:解密如何实现 RN 转微信小程序

2019 年 6 月 27 日

深入剖析Alita引擎:解密如何实现RN转微信小程序

5 月底我们正式对外开源了业内首个 React Native 转微信小程序引擎Alita项目


这个项目的发起是因为团队内部有大量使用 React Native 开发的业务模块,而大部分业务都有移植到 H5 和微信小程序的需求。所以我们开始思考如何通过技术的方式来实现把React Native代码转换微信小程序的。经过内部孵化和大量业务落地验证,最终我们对社区贡献了 Alita 引擎。她的定位非常明确,我们不造新框架,必须低侵入性并且只做好一件事情,就是把你的React Native代码转换并运行在微信小程序端(未来可能覆盖更多端)。


开源社区其实也一直在致力于打通 React 和微信小程序,涌现出了很多优秀的框架(同是京东凹凸实验室出品的Taro就是非常出色的框架),但是我们发现大部分还是基于React,并且提供了新的框架和新语法规则,而对React Native的处理比较少。更重要的是大部分框架对React语法采用的是编译时处理方案,对JSX语法限制比较大(后面文章会详细分析)。我们对Alita的期望是不能对JSX语法有太多限制,不能有侵入性,不给React Native的开发者带来太多的负担。所以最终Alita`引擎没有基于任何现有的编译时方案,而是另辟溪路,走了一条颇具开创性的运行期处理方案


抛开技术细节,使用 Alita 转换工具有 2 点约定必须了解:


  1. 如果你打算转换复杂的RN应用,需要特别注意,微信小程序包有大小限制,不能超过 4M。

  2. Alita不能直接把原生组件/第三方组件转换成小程序代码。不过,Alita提供了扩展这些组件的方式,这点很像在 RN 上提供原生平台组件。另外,我们近期会推出alita-ui,这个UI库包含了社区常用的RN组件,可以直接被Alita转换引擎支持。


Talk is cheap. Show me the code


直接上干货!接下来我们从纯技术的角度剖析一下Alita引擎的核心部分:如何实现运行期处理 JSX


现有社区方案 — 编译时

我们说社区现有方案无法满足我们的需求,尤其是在JSX语法的支持上,那么现有社区方案是怎么做的?他们主要是通过在编译阶段React代码转换为等效的微信小程序代码,来把React代码运行在微信小程序端。


举个例子,比如React逻辑表达式:


xx && <Text>Hello</Text>
复制代码


将会被转换为等效的小程序wx:if指令:


<Text wx:if="{{xx}}">Hello</Text>
复制代码


那么这种编译阶段处理的方式有什么问题呢,通过下面的React代码看下。


class App extends React.Component {    render () {        const a = <Text>Hello</Text>        const b = a
return ( <View> {b} </View> ) }}
复制代码


这里声明了变量aconst a = <Text>Hello</Text>,变量bconst b = a。 我们看下编译方案(这里以Taro1.3为例)对上面代码的转换过程。



这个例子不是特别复杂,却报错了。


要想理解上面的代码为什么报错,我们首先要理解编译阶段。本质上来说在编译阶段,代码其实就是‘字符串’,而编译阶段处理方案,就需要从这个‘字符串’中分析出必要的信息(通过AST,正则等方式)然后做对应的等效转换处理。


而对于上面的例子,需要做什么等效处理呢?需要我们在编译阶段分析出bJSX片段:b = a = <Text>Hello</Text>,然后把<View>{b}</View>中的{b}等效替换为<Text>Hello</Text>。然而在编译阶段要想确定b的值是很困难的,有人说可以往前追溯来确定 b 的值,也不是不可以,但是考虑一下 由于b = a,那么就先要确定a的值,这个a的值怎么确定呢?需要在b可以访问到的作用域链中确定a,然而a可能又是由其他变量赋值而来,循环往复,期间一旦出现不是简单赋值的情况,比如函数调用,三元判断等运行时信息,追溯就宣告失败,要是a本身就是挂在全局对象上的变量,追溯就更加无从谈起。


所以在编译阶段 是无法简单确定b的值的。


我们再仔细看下上图的报错信息:a is not defined



为什么说a未定义呢?这是涉及到另外一个问题,我们知道<Text>Hello</Text>,其实等效于React.createElement(Text, null, 'Hello'),而React.createElement方法的返回值就是一个普通JS对象,形如


// ReactElement对象{   tag: Text,   props: null,   children: 'Hello'   ...}
复制代码


但是,我们刚说了编译阶段需要对JSX做等效处理,需要把JSX转换为wxml,所以<Text>Hello</Text>这个JSX片段被特殊处理了,a不再是一个普通js对象,这里我们看到a变量甚至丢失了,这里暴露了一个很严重的问题:代码语义被破坏了,也就是说由于编译时方案对JSX的特殊处理,真正运行在小程序上的代码语义并不是你的预期。这个是比较头疼。


另外不得不提编译时方案的 React 语法限制往往是和常识相违背的,因为大家是‘运行时’的思维方式,这就导致即使我们转换失败报错了,也很难定位到具体原因,你很难理解b = a为啥报错。


Alita 处理方案 — 运行时

我们说Alita处理 React 语法采用的是运行时方案。那接下来,我们就一步步揭秘运行时方案,沿着下面原理图。我们从两个方面说明:Alita 编译阶段,Alita 运行时。



编译阶段

概括的来说,静态编译阶段主要做两个事情:


  1. 转译React代码,使之可以被小程序识别,具体的比如用React.createElement替换JSX,比如async/await语法处理等等。

  2. 枚举并标识独立JSX片段,生成小程序wxml文件。


为了直观的表明Alita与社区其他编译时方案的不同,假定有一下JSX片段,我们看下Alita静态编译做的事情。


const x = <Text>x</Text>
const y = ( <View> <Button/> <Image/> <View> <Text>HI</Text> </View> </View>)
复制代码


经过 Alita 编译阶段之后:


const x = React.createElement(Text, {uuid: "000001"}, "x");const y = React.createElement(    View,    {uuid: "000002"},    React.createElement(Button, null),    React.createElement(Image, null),    React.createElement(        View,        null,        React.createElement(Text, null, "HI")    ));
复制代码


每一个独立 JSX 片段,都会被uuid唯一标识。同时生成 wxml 文件


<template name="000001">  <Text>x</Text></template>
<template name="000002"> <View> <Button/> <Image/> <View> <Text>HI</Text> </View> </View></template>
<!--占位template--><template is="{{uiDes.name}}" data="{{...uiDes}}"/>
复制代码


用小程序template包裹独立 JSX 片段,其name属性就是上文的uuid。最后,需要在结尾生成一个占位模版:


<template is="{{uiDes.name}}" data="{{...uiDes}}"/>。

复制代码


[template is]的动态性配合uuid标识将是运行时处理 JSX 的关键,下文会继续提及。


编译阶段到这里就结束了。


Alita 运行时

关于Alita运行时,核心是内嵌的mini-react,这是一个适用微信小程序并且五脏俱全的React。让我们先简单回顾一下React的渲染过程:


递归(React16.x引入Fiber之后,不再使用递归的方式了)的构建组件树结构,创建组件实例,执行组件对应生命周期,context计算,ref等等。当state有更新时,再次调用相应生命周期,判断节点是否复用(virtual-dom)等。此外,在执行过程中会调用浏览器DOM APIappendChildremoveChild, insertBefore等等方法)不断操作原生节点,最终生成 UI 视图。


Alita mini-react的执行过程基本和这一致,也会递归构建组件树,调用生命周期等等,区别在于Alita无法调用DOM API,熟悉微信小程序开发的同学都知道,微信小程序屏蔽了DOM API。那么没有了DOM API,只剩小程序的wxml静态模版,怎么实现动态化处理React语法呢?


还记得编译阶段生成的uuid吗?每一个uuid代表了一个独立的JSX片段,在ReactDOM.render递归执行阶段,Alita 会收集聚合JSX片段的uuid属性,生成并返回uiDes数据结构,这个uiDes数据包含了所有要渲染的片段信息,这份数据会传递给小程序,然后小程序把uiDes 设置给占位模版(如下所示),递归渲染出最终的视图。


`<template is="{{uiDes.name}}" data="{{...uiDes}}"/>`,

复制代码


下面我们看一段相对复杂的React代码,我们将以这段代码,完整的说明Alita的运行过程:


class App extends React.Component {
getHeader() { return ( <View> <Image/> <Text>Header</Text> </View> ) }
f(a) { if (!this.props.xx) { return a }
return null }
render() { const a = <Text>Hello World</Text> const b = this.f(a)
return <View> {this.getHeader()} {b} </View> }}
复制代码


首先用uuid标识独立JSX片段,并用babel转义以上代码,如下:


class App extends React.Component {    getHeader() {        return React.createElement(            View,             {uuid: "000001"},            React.createElement(Image, null),            React.createElement(Text, null, "Header")        );    }
f(a) { if (!this.props.xx) { return a; }
return null; }
render() { const a = React.createElement(Text, {uuid: "000002"}, "Hello World"); const b = this.f(a); return React.createElement(View, {uuid: "000003"}, this.getHeader(), b); }
}
复制代码


同时提取独立JSX片段生成wxml文件:


<template name="000001">    <View>        <Image/>        <Text>Header</Text>    </View></template>
<template name="000002"> <Text>Hello World</Text></template>
<template name="000003"> <View> <template is="{{child001.name}}" data="{{...child001}}"/> <template is="{{child003.name}}" data="{{...child002}}"/> </View></template>
<!--占位template--><template is="{{uiDes.name}}" data="{{...uiDes}}"/>
复制代码


以上过程都是在编译阶段就处理完毕的,现在让我们考虑一下ReactDOM.render(<App/>, parent)执行过程(这里使用ReactDOM.render只是方便理解):


  1. ReactDOM.render 判断出App是自定义组件,创建其实例,执行componentWillMount等生命周期。递归处理render方法返回的ReactElement对象,即:React.createElement(View, {uuid: "000003"}, this.getHeader(), b);

  2. 处理最外层 View,收集uuid,生成UI描述:uiDes = {name: "000003"}

  3. 遍历children

  4. 处理第一个孩子节点:this.getHeader(),它的值是React.createElement(Text,{name: "000001"}, "Header"),递归处理这个值,由于Text是基本元素,递归终止,第一个孩子处理结束。此时uiDes的值如下:


   uiDes = {     name: "000003",          child001: {         name: "000001"     }   }
复制代码


  1. 处理第二个孩子节点,b。当this.props.xxtrue的时候b就是null,直接忽略。 这里并没有传递xx属性,所以b = a = React.createElement(Text, {name: "000002"}, "Hello World")Text是基本元素,递归终止,第二个孩子处理结束,此时uiDes的值如下:


   uiDes = {     name: "000003",          child001: {         name: "000001"     },          child002: {         name: "000002"     }   }
复制代码


  1. children遍历结束。

  2. 微信小程序获取到uiDes,设置到下面的占位模版,渲染对应视图,首先是外层000003模版,然后是其两个孩子节点,分别是000001模版,000002模版,最终渲染出完整视图。


   <template is="{{uiDes.name}}" data="{{...uiDes}}"/>
复制代码


在这整个过程中,你的所有JS代码都是运行在React过程中的,语义完全一致JSX片段也不会被任何特殊处理,只是简单的React.createElement调用。最终会输出一个uiDes数据到小程序,小程序通过这个uiDes渲染出视图。另外由于这里的React过程只是纯js运算,不涉及DOM操作,执行是非常迅速的,通常只有几 ms,也就是说mini-react的开销是很小的。



以上可见AlitaJSX片段的处理是动态的,你可以在任何地方,任何函数出现任何JSX片段, 最终代码执行结果会确定渲染哪一个片段,只有执行结果的片段的uuid会被写入uiDes。这和编译时方案的静态识别有着本质的区别。


另外由于上层运行还是React,所以Alita在支持redux等库上有天然的优势。


总结

我们需要一种更加React的方式处理小程序。Alita在这个方向上更进了一些,Alita的源码是完全公开的,我们也会不断提供剖析Alita原理的文章,希望给社区带来一个新的思路,也给开发者提供一个新的选择,另外让更多的的开发者理解Alita的原理,也是希望更多的人能够参与进来, "The world needs more heroes!!”。


Alita可以转换React应用吗?基于我们内部需求,我们只处理了React Native代码。但是React语法处理是相通的,把Alita扩展到转换React应用并不是很困难,不过暂时我们还没有扩展的排期,我们下一步的计划是优化/重构内部在使用的 RN 转 H5 工具,最终的形态应该是基于 RN 开发生态,通过Alita转换引擎支持实现全端的覆盖。


额外提一句,Flutter也是可以运行在Web端的,而微信小程序的运行环境就是web,那么基于Alita运行时方案,是不是可以幻想一下Flutter与微信小程序的融合呢。


Github: https://github.com/areslabs/alita


2019 年 6 月 27 日 19:454473

评论

发布
暂无评论
发现更多内容

用 Sublime Text 编辑 Markdown

U+2647

sublime-text markdown 4月日更

架构训练营模块1作业-江哲

江哲

作业

从小白程序员到大厂高级技术专家我看过哪些书籍?

冰河

程序人生 冰河 程序员进阶 推荐书单

架构实战营-模块1-作业

泄矢的呼啦圈

架构实战营

配置化开发是否可行?

顿晓

重构 配置化开发 4月日更

“圈粉”行业龙头 数字人民币搅动投资江湖

CECBC区块链专委会

数字人民币

机器学习 | 数据缩放与转换方法(1)

披头

智慧公安重点人员管控系统搭建,助推公安智慧化发展

13828808769

区块链+ #区块链#

【死磕JVM】给同事讲了一遍GC后,他要去面试,年轻人,就是容易冲动!

牧小农

JVM 垃圾回收 垃圾收集 垃圾回收算法

区块链BaaS平台+BI大数据系统

电微13828808271

区块链+

重点人员预警防控平台开发,智慧公安警务系统开发解决方案

WX13823153201

智慧党建系统搭建,干部管理平台开发

13823153121

打完新冠疫苗后要注意的两件事

石云升

28天写作 新冠疫苗 4月日更

Hive相关的总结

大数据技术指南

hive 4月日更

深度分析区块链是如何改变世界的

CECBC区块链专委会

区块链

大数据计算生态之数据计算(一)

小舰

4月日更

重构: 自己挖的坑自己填

夏兮。

Java 重构 测试 单元测试

区块链BaaS平台,创造不一样的服务

电微13828808271

区块链+

聪明人的训练(三)

Changing Lin

4月日更

重点人员可视化管理平台搭建,公安指挥调度平台

13823153121

基于角色访问控制RBAC权限模型的动态资源访问权限管理实现

crudapi

spring security 权限 rbac crudapi 角色

WordPress统计文章浏览次数

Sakura

4月日更

当云计算飞向深空

脑极体

区块链赋能文化旅游,推动旅游行业转型升级

13828808769

区块链 #区块链#

【译】JavaScript: 带你彻底搞懂 this

清秋

JavaScript 翻译 4月日更 this

Android面试你必须要知道的那些知识,重难点整理

欢喜学安卓

android 程序员 面试 移动开发

雄安区块链实验室副主任李军:把区块链植入数字雄安

CECBC区块链专委会

区块链

互联网广告成315曝光重灾区,广告怎样才能让消费者放心?

󠀛Ferry

七日更 4月日更

制作颜色选择器(全)

空城机

JavaScript Vue 前端 4月日更 颜色选择器

Kafka的再平衡机制

五分钟学大数据

kafka 4月日更

区块链技术推动自然资源领域信息化发展

13828808769

区块链+ #区块链#

技术为帆,纵横四海- Lazada技术东南亚探索和成长之旅

技术为帆,纵横四海- Lazada技术东南亚探索和成长之旅

深入剖析Alita引擎:解密如何实现RN转微信小程序-InfoQ