一種讓小程序支持JSX語法的新思路
React
社區一直在探尋使用React
語法開發小程序的方式,其中比較著名的項目有Taro
, nanachi
。而使用React
語法開發小程序的難點主要就是在JSX
語法上, JSX
本質上是JS
,相比於小程序靜態模版來說太靈活。本文所說的新思路就是在處理JSX
語法上的新思路,這是一種更加動態的處理思路,相比於現有方案,基本上不會限制任何JSX
的寫法,讓你以真正的React方式處理小程序,希望這個新思路可以給任何有志於用React
開發小程序的人帶來啟發。
現有思路的局限
在介紹新的思路之前,我們先來看下Taro(最新版1.3)
, nanachi
是怎麼在小程序端處理JSX
語法的。簡單來說,主要是通過在編譯階段把JSX
轉化為等效的小程序wxml
來把React
代碼運行在小程序端的。
舉個例子,比如React
邏輯表達式:
xx && <Text>Hello</Text>
將會被轉化為等效的小程序wx:if指令:
<Text wx:if="{{xx}}">Hello</Text>
這種方式把對JSX
的處理,主要放在了編譯階段,他依賴於編譯階段的訊息收集,以上面為例,它必須識別出邏輯表達式,然後做對應的wx:if
轉換處理。
那編譯階段有什麼問題和局限呢?我們以下面的例子說明:
classAppextendsReact.Component{render(){consta=<Text>Hello</Text>constb=areturn(<View>{b}</View>)}}
首先我們聲明const a = <Text>Hello</Text>
,然後把a
賦值給了b
,我們看下最新版本Taro 1.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'...}
所以上面那一段代碼在JS
環境真正運行的時候,大概等效如下:
classAppextendsReact.Component{render(){consta={tag:Text,props:null,children:'Hello'...}constb=areturn{tag:View,props:null,children:b...}}}
但是,我們剛說了編譯階段需要對JSX
做等效處理,需要把JSX
轉換為wxml
,所以<Text>Hello</Text>
這個JSX
片段被特殊處理了, a
不再是一個普通js
對象,這裡我們看到a
變量甚至丟失了,這裡暴露了一個很嚴重的問題:代碼語義被破壞了,也就是說由於編譯時方案對JSX
的特殊處理,真正運行在小程序上的代碼語義並不是你的預期。這個是比較頭疼。
新的思路
正因為編譯時方案,有如上的限制,在使用的時候常常讓你有“我還是在寫React
嗎?”這種感覺。
下面我們介紹一種全新的處理思路,這種思路在小程序運行期間和真正的React
幾無區別,不會改變任何代碼語義, JSX
表達式只會被處理為React.createElement
方法調用,實際運行的時候就是普通js
對象,最終通過其他方式渲染出小程序視圖。下面我們仔細說明一下這個思路的具體內容。
第一步:給每個獨立的JSX
片段打上唯一標識uuid
,假定我們有如下代碼:
consta=<Textuuid="000001">Hello</Text>consty=<Viewuuid="000002"><Image/><Text/></View>
我們給a
片段, y
片段添加了uuid
屬性
第二步:把React
代碼通過babel
轉義為小程序可以識別的代碼,例如JSX
片段用等效的React.createElement
替換等
consta=React.createElement(Text,{uuid:"000001"},"Hello");
第三步:提取每個獨立的JSX
片段,用小程序template
包裹,生成wxml
文件
<templatename="000001"><Text>Hello</Text></template><templatename="000002"><Viewuuid="000002"><Image/><Text/></View></template><!--占位template--><templateis="{{uiDes.name}}"data="{{...uiDes}}"/>
注意這裡每一個template
的name
標識和JSX
片段的唯一標識uuid
是一樣的。最後,需要在結尾生成一個佔位模版: <template is="{{uiDes.name}}" data="{{...uiDes}}"/>
。
第四步:修改ReactDOM.render
的遞歸( React 16.x
之後,不在是遞歸的方式)過程,遞歸執行階段,聚合JSX
片段的uuid
屬性,生成並返回uiDes
數據結構。
第五步:把第四步生成的uiDes
,傳遞給小程序環境,小程序把uiDes
設置給佔位模版<template is="{{uiDes.name}}" data="{{...uiDes}}"/>
,渲染出最終的視圖。
我們以上面的App
組件的例子來說明整個過程,首先js
代碼會被轉義為:
classAppextendsReact.Component{render(){consta=React.createElement(Text,{uuid:"000001"},"Hello");constb=areturn(React.createElement(View,{uuid:"000002"},b);)}}
同時生成wxml
文件:
<templatename="000001"><Text>Hello</Text></template><templatename="000002"><View><templateis="{{child0001.name}}"data="{{...child0001}}"/></View></template><!--占位template--><templateis="{{uiDes.name}}"data="{{...uiDes}}"/>
使用我們定制之後render
執行ReactDOM.render(<App/>, parent)
。在render
的遞歸過程中,除了會執行常規的創建組件實例,執行生命週期之外,還會額外的收集執行過程中組件的uuid
標識,最終生成uiDes
對象
constuiDes={name:"000002",child0001:{name:000001,...}...}
小程序獲取到這個uiDes
,設置給佔位模版<template is="{{uiDes.name}}" data="{{...uiDes}}"/>
。最終渲染出小程序視圖。
在這整個過程中,你的所有JS
代碼都是運行在React过程
中的,語義完全一致, JSX
片段也不會被任何特殊處理,只是簡單的React.createElement
調用,另外由於這裡的React过程
只是純js
運算,執行是非常迅速的,通常只有幾ms。最終會輸出一個uiDes
數據到小程序,小程序通過這個uiDes
渲染出視圖。
現在我們在看之前的賦值const b = a
,就不會有任何問題了,因為a
不過是普通對象。另外對於常見的編譯時方案的限制,比如任意函數返回JSX
片段,動態生成JSX
片段, for
循環使用JSX
片段等等,都可以完全解除了,因為JSX
片段只是js
對象,你可以做任何操作,最終ReactDOM.render
會蒐集所有執行結果的片段的uuid
標識,生成uiDes
,而小程序會根據這個uiDes
數據結構渲染出最終視圖。
可以看出這種新的思路和以前編譯時方案還是有很大的區別的,對JSX
片段的處理是動態的,你可以在任何地方,任何函數出現任何JSX
片段,最終執行結果會確定渲染哪一個片段,只有執行結果的片段的uuid
會被寫入uiDes
。這和編譯時方案的靜態識別有著本質的區別。
結語
"Talk is cheap. Show me your code!" 這僅僅是一個思路?還是已經有落地完整的實現呢?
是有完整的實現的, alita項目在處理JSX
語法的時候,採用的就是這個思路,這也是alita基本不限制寫法卻可以轉化整個React Native項目的原因,另外alita在這個思路上做了很多優化。如果對這個思路的具體實現有興趣,可以去研讀一下alita源碼,它完全是開源的https:// github.com/areslabs/ali ta 。
當然,你也可以基於這個思路,構造出自己的React小程序開發方案。