5月底咱們正式對外開源了業內首個React Native轉微信小程序引擎Alita項目( github.com/areslabs/al…)。 這個項目的發起是由於團隊內部有大量使用React Native開發的業務模塊,大部分業務都有移植到H5和微信小程序的需求。因此咱們開始思考如何經過技術的方式來實現把React Native
代碼轉換微信小程序的。通過內部孵化和大量業務落地驗證,最終咱們對社區貢獻了Alita引擎。她的定位很是明確,咱們不造新框架,Alita必須低侵入性而且只作好一件事情,就是把你的React Native
代碼轉換並運行在微信小程序端(將來可能覆蓋更多端)。javascript
開源社區其實也一直在致力於打通React
和微信小程序,涌現出了不少優秀的框架(同是京東凹凸實驗室出品的Taro
就是很是出色的框架),咱們發現大部分雖然基於React
,可是提供了新的框架和新語法規則,對React Native
的處理比較少。更重要的是現有框架對React
語法採用的是編譯時處理方案,對JSX語法限制比較大(後面文章會詳細分析)。咱們對Alita
的指望是不能對JSX
語法有太多限制,不能有侵入性,不給React Native
的開發者帶來太多的負擔。因此最終Alita
引擎沒有基於任何現有的編譯時方案,而是另闢溪路,走了一條頗具開創性的運行期處理方案。html
拋開技術細節,針對Alita的使用者有2點必須瞭解:1)若是你打算轉換複雜的RN
應用,須要特別注意,微信小程序包有大小限制,不能超過4M。2)Alita
不能直接把原生組件/第三方組件轉換成小程序代碼。不過,Alita
提供了擴展這些組件的方式,這點很像在RN上提供原平生臺組件。另外,咱們近期會推出alita-ui
,這個UI
庫包含了社區經常使用的RN
組件,能夠直接被Alita
轉換引擎支持。java
Talk is cheap. Show me the code.react
直接上乾貨!接下來咱們從純技術的角度剖析一下Alita
引擎的核心部分:如何實現運行期處理JSXgit
在剖析Alita以前,咱們先來看一下現有的社區方案,咱們說現有方案對JSX如今比較大,那他們是怎麼作的呢?他們主要是經過在編譯階段把React
代碼轉換爲等效的微信小程序代碼,來把React
代碼運行在微信小程序端。 舉個例子,好比React
邏輯表達式:github
xx && <Text>Hello</Text>
複製代碼
將會被轉換爲等效的小程序wx:if
指令:web
<Text wx:if="{{xx}}">Hello</Text>
複製代碼
那麼這種編譯階段處理的方式有什麼問題呢,經過下面的React
代碼看下。redux
class App extends React.Component {
render () {
const a = <Text>Hello</Text>
const b = a
return (
<View> {b} </View>
)
}
}
複製代碼
這裏聲明瞭變量a
:const a = <Text>Hello</Text>
,變量b
:const b = a
。 咱們看下編譯方案(這裏以Taro1.3
爲例)對上面代碼的轉換過程。小程序
這個例子不是特別複雜,卻報錯了。微信小程序
要想理解上面的代碼爲何報錯,咱們首先要理解編譯階段。本質上來講在編譯階段,代碼其實就是‘字符串’,而編譯階段處理方案,就須要從這個‘字符串’中分析出必要的信息(經過AST
,正則等方式)而後作對應的等效轉換處理。
而對於上面的例子,須要作什麼等效處理呢?須要咱們在編譯階段分析出b
是JSX
片斷: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
變量甚至丟失了,這裏暴露了一個很嚴重的問題:代碼語義被破壞了。
編譯時解決方案,更加像一個加強的模版,只要你遵循相關約定和語法限制,從工程角度來講徹底是能夠構建出完整的應用的。可是站在更高的抽象上,React帶來的自己就是對UI的從新思考, 不管是UI as code
, 仍是React is "value UI"
。
Alita並不改變React語義,她採用的是一種動態處理JSX的方案,那接下來,咱們就一步步揭祕Alita的運行時方案,沿着下面原理圖。咱們從兩個方面說明:Alita編譯階段,Alita運行時。
歸納的來講,靜態編譯階段主要作兩個事情:
React
代碼,使之能夠被小程序識別,具體的好比用React.createElement
替換JSX
,好比async/await
語法處理等等。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
運行時,核心是內嵌的mini-react
,這是一個適用微信小程序而且五臟俱全的React
。讓咱們先簡單回顧一下React
的渲染過程:
遞歸(React16.x
引入Fiber
以後,再也不使用遞歸的方式了)的構建組件樹結構,建立組件實例,執行組件對應生命週期,context
計算,ref
等等。當state
有更新時,再次調用相應生命週期,判斷節點是否複用(virtual-dom
)等。此外,在執行過程當中會調用瀏覽器DOM API
(appendChild
, removeChild
, 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
只是方便理解):
ReactDOM.render
判斷出App
是自定義組件,建立其實例,執行componentWillMount
等生命週期。遞歸處理render
方法返回的ReactElement
對象,即:React.createElement(View, {uuid: "000003"}, this.getHeader(), b);
。
處理最外層 View
,收集uuid
,生成UI
描述:uiDes = {name: "000003"}
遍歷children
處理第一個孩子節點:this.getHeader()
,它的值是React.createElement(Text,{name: "000001"}, "Header")
,遞歸處理這個值,因爲Text
是基本元素,遞歸終止,第一個孩子處理結束。此時uiDes
的值以下:
uiDes = {
name: "000003",
child001: {
name: "000001"
}
}
複製代碼
處理第二個孩子節點,b
。當this.props.xx
爲true
的時候b
就是null
,直接忽略。 這裏並無傳遞xx
屬性,因此b = a = React.createElement(Text, {name: "000002"}, "Hello World")
。Text
是基本元素,遞歸終止,第二個孩子處理結束,此時uiDes
的值以下:
uiDes = {
name: "000003",
child001: {
name: "000001"
},
child002: {
name: "000002"
}
}
複製代碼
children
遍歷結束。
微信小程序獲取到uiDes
,設置到下面的佔位模版,渲染對應視圖,首先是外層000003
模版,而後是其兩個孩子節點,分別是000001
模版,000002
模版,最終渲染出完整視圖。
<template is="{{uiDes.name}}" data="{{...uiDes}}"/>
複製代碼
在這整個過程當中,你的全部JS
代碼都是運行在React過程
中的,語義徹底一致,JSX
片斷也不會被任何特殊處理,只是簡單的React.createElement
調用。最終會輸出一個uiDes
數據到小程序,小程序經過這個uiDes
渲染出視圖。另外因爲這裏的React過程
只是純js
運算,不涉及DOM
操做,執行是很是迅速的,一般只有幾ms,也就是說mini-react
的開銷是很小的。
以上可見Alita
對JSX
片斷的處理是動態的,你能夠在任何地方,任何函數出現任何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: github.com/areslabs/al…