您好,登錄后才能下訂單哦!
本篇內容主要講解“小程序能不能用react”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“小程序能不能用react”吧!
小程序能用react,其使用方法:1、基于“react-reconciler”實現一個渲染器,生成一個DSL;2、創建一個小程序組件,去解析和渲染DSL;3、安裝npm,并執行開發者工具中的構建npm;4、在自己的頁面中引入包,再利用api即可完成開發。
小程序能用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寫好的h6我們想要渲染到小程序,其他部分我們還是在原來的項目中跑)。如果對它的編譯工具有依賴,我們就不得不把整個項目遷移到它的編譯工具,那我還不如直接使用taro這個老牌比較穩定的工具。
整體實現思路
經過一番研究之后,我決定采用remax的思路,也就是基于react-reconciler實現一個渲染器,生成一個DSL,再創建一個小程序組件,去解析和渲染這個DSL。在完成實現之后,我把所有這些邏輯構建為最終產物,并以npm的形式發布產物,對于小程序開發者而言,只需要npm安裝之后,執行開發者工具中的構建npm即可,之后在自己的頁面中引入這個包,利用api即可完成開發,而不在需要使用另外的編譯工具。
這一方案的最大好處是,對編譯工具的弱(無)依賴,這樣就可以讓我們的這套方案可以在任意的項目中去跑,而不需要額外引入編譯工具切換工具棧。另外,因為reconciler的部分已經打包進npm包了,所以它是一個可以獨立運行的模塊,所以,你甚至可以在mpvue等vue風格或小程序原生風格項目中使用這個npm包來渲染react的組件。
微信小程序中運行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”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。