小程式能用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 ################以recon #################################################################################################################。 #########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的元件。
微信小程式中運行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视频教程》
以上是小程式能用react嗎的詳細內容。更多資訊請關注PHP中文網其他相關文章!