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

React社區一直在探尋使用React語法開發小程序的方式,其中比較著名的項目有Taronanachi。而使用React語法開發小程序的難點主要就是在JSX語法上,JSX本質上是JS,相比於小程序靜態模版來講太靈活。本文所說的新思路就是在處理JSX語法上的新思路,這是一種更加動態的處理思路,相比於現有方案,基本上不會限制任何JSX的寫法,讓你以真正的React方式處理小程序,但願這個新思路能夠給任何有志於用React開發小程序的人帶來啓發。javascript

現有思路的侷限

在介紹新的思路以前,咱們先來看下Taro(最新版1.3)nanachi是怎麼在小程序端處理JSX語法的。簡單來講,主要是經過在編譯階段JSX轉化爲等效的小程序wxml來把React代碼運行在小程序端的。html

舉個例子,好比React邏輯表達式:java

xx && <Text>Hello</Text>

將會被轉化爲等效的小程序wx:if指令:git

<Text wx:if="{{xx}}">Hello</Text>

這種方式把對JSX的處理,主要放在了編譯階段,他依賴於編譯階段信息收集,以上面爲例,它必須識別出邏輯表達式,而後作對應的wx:if轉換處理。github

編譯階段有什麼問題和侷限呢?咱們如下面的例子說明:小程序

class App extends React.Component {
    render () {
        const a = <Text>Hello</Text>
        const b = a

        return (
            <View>
                {b}
            </View>
        )
    }
}

首先咱們聲明 const a = <Text>Hello</Text>,而後把a賦值給了b,咱們看下最新版本Taro 1.3的轉換,以下圖:babel

圖片描述

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

要想理解上面的代碼爲何報錯,咱們首先要理解編譯階段。本質上來講在編譯階段,代碼其實就是‘字符串’,而編譯階段處理方案,就須要從這個‘字符串’中分析出必要的信息(經過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'
   ...
}

因此上面那一段代碼在JS環境真正運行的時候,大概等效以下:

class App extends React.Component {
    render () {
        const a = {
            tag: Text,
            props: null,
            children: 'Hello'
            ...
        }
        const b = a

        return {
            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,假定咱們有以下代碼:

const a = <Text uuid="000001">Hello</Text>

const y = <View uuid="000002">
    <Image/>
    <Text/>
</View>

咱們給a片斷,y片斷 添加了uuid屬性

第二步:把React代碼經過babel轉義爲小程序能夠識別的代碼,例如JSX片斷用等效的React.createElement替換等

const a = React.createElement(Text, {
  uuid: "000001"
}, "Hello");

第三步:提取每一個獨立的JSX片斷,用小程序template包裹,生成wxml文件

<template name="000001">
    <Text>Hello</Text>
</template>

<template name="000002">
    <View uuid="000002">
        <Image/>
        <Text/>
    </View>
</template>


<!--佔位template-->
<template is="{{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代碼會被轉義爲:

class App extends React.Component {
    render () {
        const a = React.createElement(Text, {uuid: "000001"}, "Hello");
        const b = a
        
        return (
          React.createElement(View, {uuid: "000002"} , b);
        )
      }
}

同時生成wxml文件:

<template name="000001">
    <Text>Hello</Text>
</template>

<template name="000002">
    <View>
        <template is="{{child0001.name}}" data="{{...child0001}}"/>
    </View>
</template>

<!--佔位template-->
<template is="{{uiDes.name}}" data="{{...uiDes}}"/>

使用咱們定製以後render執行ReactDOM.render(<App/>, parent)。在render的遞歸過程當中,除了會執行常規的建立組件實例,執行生命週期以外,還會額外的收集執行過程當中組件的uuid標識,最終生成 uiDes 對象

const uiDes = {
    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/alita

固然,你也能夠基於這個思路,構造出本身的React小程序開發方案

相關文章
相關標籤/搜索