一種讓小程序支持JSX語法的新思路

blank

一種讓小程序支持JSX語法的新思路

React社區一直在探尋使用React語法開發小程序的方式,其中比較著名的項目有Taronanachi 。而使用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的轉換,如下圖:

blank

這個例子不是特別複雜,卻報錯了。

要想理解上面的代碼為什麼報錯,我們首先要理解編譯階段。本質上來說在編譯階段,代碼其實就是'字符串',而編譯階段處理方案,就需要從這個'字符串'中分析出必要的訊息(通過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

blank

為什麼說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}}"/>

注意這裡每一個templatename標識和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源碼,它完全是開源的 github.com/areslabs/ali

當然,你也可以基於這個思路,構造出自己的React小程序開發方案

What do you think?

Written by marketer

blank

WebGL進階——走進圖形噪聲

blank

Vue組件庫工程探索與實踐之按需加載篇