1. 程式人生 > 實用技巧 >小程式支援JSX語法的新思路

小程式支援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指令:

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

這種方式把對JSX的處理,主要放在了編譯階段,他依賴於編譯階段資訊收集,以上面為例,它必須識別出邏輯表示式,然後做對應的wx:if轉換處理。

編譯階段有什麼問題和侷限呢?我們以下面的例子說明:

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的轉換,如下圖:

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

要想理解上面的程式碼為什麼報錯,我們首先要理解編譯階段。本質上來說在編譯階段,程式碼其實就是‘字串’,而編譯階段

處理方案,就需要從這個‘字串’中分析出必要的資訊(通過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環境真正執行的時候,大概等效如下:

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}}"/>

注意這裡每一個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程式碼會被轉義為:

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。這和編譯時方案的靜態識別有著本質的區別。

廣州品牌設計公司https://www.houdianzi.com PPT模板下載大全https://redbox.wode007.com

結語

"Talk is cheap. Show me your code!" 這僅僅是一個思路?還是已經有落地完整的實現呢?

是有完整的實現的,alita專案在處理JSX語法的時候,採用的就是這個思路,這也是alita基本不限制寫法卻可以轉化整個React Native專案的原因,另外alita在這個思路上做了很多優化。如果對這個思路的具體實現有興趣,可以去研讀一下alita原始碼,它完全是開源的https://github.com/areslabs/alita。

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