小程式能用react嗎

2022-12-29 14:00:54

小程式能用react,其使用方法:1、基於「react-reconciler」實現一個渲染器,生成一個DSL;2、建立一個小程式元件,去解析和渲染DSL;3、安裝npm,並執行開發者工具中的構建npm;4、在自己的頁面中引入包,再利用api即可完成開發。

本教學操作環境:Windows10系統、react18.0.0版、Dell G3電腦。

小程式能用react嗎?

能。

在微信小程式中直接執行React元件

在研究跨端開發時,我的一個重要目標,是可以讓react元件跑在微信小程式中。在這個過程中,我探索了微信小程式的架構,並且引發了很多思考。而作為跨端開發,實際上很難做到 write once,run anywhere,因為每個平臺所提供的能力是不一樣的,例如微信小程式提供了原生的能力,例如調起攝像頭或其他需要原生環境支援的能力,在微信小程式中開發雖然也是在webview中開展,但是,卻需要一些原生的思維。所以,要做到 write once 就必須有一些限制,這些限制註定了我們無法完全利用小程式的能力,僅僅只用到一些佈局的能力而已。所以,奉勸各位,在做跨端開發時,要有個心理準備。但如果跳出跨端開發,我現在只開發小程式,那我能否用我熟悉的react來開發呢?甚至,能否用我開發的nautil框架來開發呢?答案是可以的,本文將帶你一步一步實現自己的react小程式開發之路,幫助你在某些特定的場景下,完成react專案往小程式遷移的目標。

小程式執行React的方案對比

目前業界能夠比較好支援小程式(沒有特別註明的情況下,小程式特指微信小程式)執行React元件的,有3套方案,分別是京東凹凸實驗室的taro,螞蟻金服某團隊(未找到具體團隊名)的remax,微信某團隊的kbone。

Taro

  • 編譯,新版本也基於執行時

  • 解析為wxml+js

  • 老牌,不斷髮展,全平臺支援,持續迭代

Remax

  • 執行時,帶編譯宏

  • 基於reconciler

  • 最優雅,增量更新

  • 不夠成熟,後續發展未知

Kbone

  • 執行時,依賴webpack

  • 自己實現一套DOM API

  • 可相容vue,甚至任意基於DOM渲染的框架

  • 效能問題(全量檢查),幾乎停更

3套方案各有不同,而且在各自的思路上都是獨樹一幟。就我個人而言,如果不考慮跨端開發,自己實現一套DOM API這種方案是非常有價值的,因為DOM介面是HTML標準,你不需要自己去發明一套標準出來,而一旦實現了DOM API,那麼所以其他基於DOM實現的應用理論上都支援在這上面跑。但是,它的不足就是你每換一個平臺,就要針對這個平臺去實現一套DOM API,這個成本是非常大的,因為DOM介面標準極其龐大,實現的時候也很容易出bug。在我看來,最優雅的實現還是Remax的那種思路,基於react-reconciler做一個渲染器,這個渲染器將react元件範例抽象為一個統一的DSL,在不同的平臺上,去解析渲染這個DSL。

但是remax迭代更新之後,它開始強依賴自己的編譯工具,這直接導致我放棄在專案中使用它。因為對於我們自己的專案而言,我們其實有可能不需要它的全部,我們只是使用react來完成我們整個小程式中的某些部分(比如有些已經用react寫好的h5我們想要渲染到小程式,其他部分我們還是在原來的專案中跑)。如果對它的編譯工具有依賴,我們就不得不把整個專案遷移到它的編譯工具,那我還不如直接使用taro這個老牌比較穩定的工具。

整體實現思路

經過一番研究之後,我決定採用remax的思路,也就是基於react-reconciler實現一個渲染器,生成一個DSL,再建立一個小程式元件,去解析和渲染這個DSL。在完成實現之後,我把所有這些邏輯構建為最終產物,並以npm的形式釋出產物,對於小程式開發者而言,只需要npm安裝之後,執行開發者工具中的構建npm即可,之後在自己的頁面中引入這個包,利用api即可完成開發,而不在需要使用另外的編譯工具。

這一方案的最大好處是,對編譯工具的弱(無)依賴,這樣就可以讓我們的這套方案可以在任意的專案中去跑,而不需要額外引入編譯工具切換工具棧。另外,因為reconciler的部分已經打包進npm包了,所以它是一個可以獨立執行的模組,所以,你甚至可以在mpvue等vue風格或小程式原生風格專案中使用這個npm包來渲染react的元件。

39f2e22977c6e4d11cf1851fb953cb0.jpg

微信小程式中執行react元件的思路

如上圖所示,我們將一個react元件通過基於react-reconciler的渲染器,建立了一個DSL的純物件(包含回撥函數),我們在page的js檔案中,通過this.setData把這個物件傳送給渲染執行緒,在wxml中使用了我們提供的一個自參照巢狀的元件對DSL進行渲染。這裡需要注意一個點,react-reconciler會在元件更新的時候,觸發對應的勾點,此時,會再次生成新的DSL,並再次通過this.setData傳送渲染。所以,這個渲染器和單純使用createElement的結果是不同的,渲染器支援hooks等react內建的功能。

接下來,我將對其中的具體細節進行講解,以讓你儘可能自己可以手寫出本文所闡述的程式碼,以讓你在自己的專案中可以實現本文一致的效果。你可以克隆這個倉庫到本地,執行效果看看,研究它的整個實現過程。

將react元件渲染為純JS物件

react的渲染器本質上是一個基於react排程系統的副作用執行器,副作用的結果在web環境下就是DOM的操作,在native環境下就是呼叫渲染引擎光柵化圖形,在art環境下就是呼叫音效卡播放聲音,而在我們這次的計劃中,我們需要渲染器生成一個純js物件,以方便交給小程式在小程式的兩個執行緒之間作為訊息體進行傳遞,並基於這個物件在小程式中渲染介面。

有同學對我發出疑問:jsx編譯之後React.createElement的執行結果不就是純JS的物件麼?這裡需要了解react的本質。react的元件,實際上為react提供了一套描述系統,它描述了react所表達的具體物件的結構。但是,這個描述是抽象的,只有當你把它範例化,執行起來時,它才有意義。我們在元件中所做的描述,可不單單隻有jsx的部分,它還包括業務和程式層面的邏輯。比如很多場景下,我們需要根據元件狀態來決定返回那一部分jsx,從而渲染不同的介面。而這部分內容,需要依賴一個環境來執行,也就是react渲染器。

在以前,我們只能模擬react-dom,按照它的執行邏輯,自己手寫一個渲染器。而現在,react把它的排程器專門做了一個庫,react-reconciler,幫助開發者快速接入react的排程系統,從而可以構建自己的渲染器。這裡有一個視訊(自備梯子),介紹了react-reconciler的基本用法和使用效果。

import Reconciler from 'react-reconciler'
const container = {}
const HostConfig = {
  // ... 極其複雜的一個設定
}
const reconcilerInstance = Reconciler(HostConfig)
let rootContainerInstance = null
export function render(element, { mounted, updated, created }) {
  if (!rootContainerInstance) {
    rootContainerInstance = reconcilerInstance.createContainer(container, false, false)
  }
  return reconcilerInstance.updateContainer(element, rootContainerInstance, null, () => {
    notify = { mounted, updated, created }
    created && created(container)
    mounted(container.data)
  })
}
登入後複製

上面程式碼中,沒有給出的HostConfig的具體內容是關鍵,它用於配製一個Reconciler,從程式碼的角度,它就是一個勾點函數的集合,我們需要在每個勾點函數內部寫一些副作用來操作container,你可以看到,在不同的時刻,我們傳入的created, mounted, updated會被呼叫,而它們接收被操作過的container,從而讓我們獲得這個js物件(container上還有一些函數,但我們可以不用理會,因為this.setData會自動清除這些函數)。

由於這一設定內容太過複雜,要講解清楚需要花費比較大的篇幅,所以我直接把原始碼地址貼在這裡,你可以通過閱讀原始碼來了解它都有哪些設定項,並且你可以把這部分程式碼拆分出來後,執行一個自己的元件,通過console.log來觀察它們被呼叫的時機以及順序。

總而言之,這些介面都是知識層面的,不是什麼複雜的邏輯,瞭解每一個設定項的作用和執行時機之後,你就能寫出自己的渲染器。理論上,它沒有什麼難度。

基於react-reconciler,我在react執行時的每一個環節都做了一些副作用操作,這些副作用的本質,就是修改一個純js物件,當react被執行起來時,它會經歷一個生命週期,這在我的一個視訊中有講到react的生命週期的具體過程。你也可以關注我的個人微信公眾號 wwwtangshuangnet 和我討論相關的問題。在每一個生命週期節點上,排程器就會執行一個副作用,即修改我提供的那個純js物件。

我提供了兩個方法,用於在小程式的渲染器中,獲得生成好的js物件。得到這個js物件之後,就可以呼叫小程式的this.setData,把這個物件傳送到渲染執行緒進行渲染。

利用react渲染器得到的純物件上存在一些函數,呼叫這些函數會觸發它們對應的邏輯(比如呼叫setState觸發hooks狀態更新),從而觸發排程器中的勾點函數執行,container物件再次被修改,updated被再次呼叫,this.setData被再次執行,這樣,就實現了真正的react執行時在小程式中的植入。

巢狀遞迴自參照元件

渲染執行緒接收到this.setData傳送過來的js物件後,如何將這個物件作為佈局的資訊,渲染到介面上呢?由於小程式的特殊架構,它為了安全起見,渲染執行緒中無法執行可操作介面的指令碼,所有的渲染,都得依靠模板語法和少量的wxs指令碼。所以,要怎麼做呢?

小程式提供了自定義元件的功能,在app.json或對應的page.json中,通過usingComponents來指定一個路徑,從而可以在wxml中使用這個元件。而有趣的地方在於,元件本身也可以在元件自己的component.json中使用usingComponents這個設定,而這個設定的內容,可以直接指向自己,例如,我在自己的元件中,這樣自參照:

// dynamic.json
{
  "usingComponents": {
    "dynamic": "./dynamic"
  }
}
登入後複製

自己參照自己作為元件之後,在其wxml中,我們就可以使用元件自己去渲染子級資料,即一種巢狀遞迴的形式進行渲染。

我規定了一種特別的資料結構,大致如下:

{
  type: 'view',
  props: {
    class: 'shadow-component',
    bindtap: (e) => { ... },
  },
  children: [
    {
      type: 'view',
      props: {},
      children: [
        ...
      ],
    },
  ],
}
登入後複製

模板中,通過對type的判斷,選擇不同的模板程式碼進行渲染。

<block wx:if="{{ type === 'view' }}">
  <view class="{{ props.class }}" bindtap="bindtap">
    <block wx:if="{{ children.length }}" wx:for="{{ children }}">
      <dynamic data="{{ item }}" /> <!-- 巢狀遞迴 -->
    </block>
  </view>
</block>
登入後複製

在wxml中把所有元件通過這種形式列舉出來之後,這個元件就能按照上述的資料結構遞迴渲染出整個結構。

當然,這裡還需要處理一些細節,例如響應data的變化,事件響應函數等,你可以通過原始碼瞭解具體要怎麼處理。另外,微信小程式this.setData限制在1M以內,我雖然還沒有嘗試過很大的資料,但是,這個限制肯定在將來是一個風險點,我現在還沒有解決,還在思考應該怎麼最小化更新粒度。

不支援直接JSX的變通方法

小程式的編譯,沒有辦法自己設定支援新語法,所以如果我們在小程式程式碼中使用jsx,就必須先走一遍自己的編譯邏輯。有兩種解決辦法,一種是不使用jsx語法,而是使用hyperscript標記語法,比如:

import { createElement as h } from 'react'
function Some() {
  return h(
    'view',
    { class: 'some-component' },
    h(
      'view',
      { class: 'sub-view' },
      '一段文字',
    ),
    '一段文字',
  )
}
登入後複製

這樣的寫法顯然沒有直接寫jsx來的方便,但是閱讀上沒有什麼障礙,且不需要將jsx編譯的過程。

另一種辦法是走一遍編譯,在小程式的頁面目錄下,建立一個頁面同名的.jsx檔案,再利用bebel將它編譯為.js檔案。但是這樣的話,你需要在釋出小程式的時候,忽略掉所有的.jsx檔案。另外,還有一個坑是,小程式的編譯不提供process.env,所以編譯react的結果用的時候會報錯。解決辦法是把react的cjs/react.production.min.js作為react的入口檔案,通過小程式的構建npm的相關設定邏輯,指定react構建的檔案。

結語

本文詳細講解了如何在微信小程式中直接執行react元件的思路,同時,你可以參考這個倉庫,執行效果看看,研究它的整個實現過程。總結而言,這個方法分為3個部分:1. 基於react-reconciler實現一個把react元件渲染為純js物件的渲染器,之所以需要純js物件,是因為小程式傳送到渲染執行緒的資料必須是純物件。2. 利用小程式的自定義元件,實現自參照巢狀遞迴的元件,用於利用上一步得到的js物件渲染成真正的介面。3. 解決jsx問題,將前兩步的結果,在page中進行實施,以真正完成在小程式中渲染react元件的效果。當然,本文闡述過程,僅僅提供了這套思路,在真正用到專案中時,使用過程中肯定還會遇到一些坑,僅能作為原有小程式開發專案的補充手段,比如之前寫好的react元件不想重新寫成小程式版本,那麼就可以使用這個方法,同時在渲染元件的地方,把DOM的標籤,對映為小程式的標籤,就可以在一定程度上解決原有react程式碼複用的問題。如果你在實操過程中遇到什麼問題,歡迎在本文下方留言討論~

文中連結:
Nautil框架:https://github.com/tangshuang/nautil
演示倉庫:https://gitee.com/frustigor/wechat-dynamic-component
Building a Custom React Rendere: https://www.youtube.com/watch?v=CGpMlWVcHok
登入後複製

推薦學習:《》

以上就是小程式能用react嗎的詳細內容,更多請關注TW511.COM其它相關文章!