Taro3執行時機制剖析

語言: CN / TW / HK

閒來無事,抱著學習態度,研究一下Taro3執行時部分的原始碼,探究Taro內部的執行機制,積累一點跨端開發知識。 Taro可以讓開發者編寫一套程式碼,編譯適配多個小程式平臺,並且Taro目前也支援轉換為鴻蒙應用,逐步豐富在跨端領域的應用面。

專案結構

Taro是典型的MonoRepo的專案結構,各個npm包的作用如下表:

RN相關包未列入

| NPM包 | 作用 | | ------------------------------ | -------------------------------------------------------------------------------------------- | | 執行時 | 模擬BOM/DOM API,框架與小程式環境的對接處理 | | @tarojs/taro | 暴露給應用開發者的Taro核心api | | @tarojs/shared | Taro內部使用的共用utils | | @tarojs/api | 暴露給 @tarojs/taro 的所有端的公有 API | | @tarojs/components | Taro元件庫,H5端則使用stencil實現的Web Components | | @tarojs/components-react | React版元件庫,為了相容低版本瀏覽器 | | @tarojs/taro-h5 | 暴露給@tarojs/taro的H5端api | | @tarojs/react | 小程式端React渲染器,實現HostConfig介面,小程式版react-dom | | @tarojs/router | H5端路由系統,為了相容小程式規範的路由系統 | | @tarojs/runtime | Taro執行時,完整實現了DOM、BOM API,以及createReactApp\createVueApp\createPageConfig方法,將框架DSL與小程式環境橋接在一起。 | | | | | 編譯時 | 通過webpack+babel編譯工具組合,轉換為小程式環境的程式碼 | | @tarojs/cli | Taro命令列CLI | | @tarojs/taro-loader | 暴露給mini-runner 與 webpack-runner 使用的 webpack loader | | @tarojs/helper | 暴露給內部CLI或編譯時的輔助方法,如常量、fs等 | | @tarojs/mini-runner | 小程式端的webpack啟動器,編譯主流程 | | @tarojs/webpack-runner | H5端的webpack啟動器,編譯主流程 | | @tarojs/runner-utils | 暴露給mini-runner 與 webpack-runner 使用的公用工具函式 | | @tarojs/plugin-platform-react | Taro外掛,用於支援編譯React/Preact/Nerv | | @tarojs/plugin-platform-vue3 | Taro外掛,用於支援編譯Vue3 | | @tarojs/plugin-html | Taro外掛,用於支援編譯到H5端 | | @tarojs/service | Taro服務層,封裝了Kernel(擴充套件編譯時)以及TaroPlatformBase(擴充套件端平臺外掛) | | | | | 端平臺外掛(5橫1縱) | | | @tarojs/plugin-platform-weapp | 橫向擴充套件微信小程式 | | @tarojs/plugin-platform-qq | 縱向擴充套件QQ小程式 | | @tarojs/plugin-platform-alipay | 橫向擴充套件支付寶小程式 | | @tarojs/plugin-platform-jd | 橫向擴充套件京東小程式 | | @tarojs/plugin-platform-swan | 橫向擴充套件百度小程式 | | @tarojs/plugin-platform-tt | 橫向擴充套件頭條小程式 |

如果你對執行時感興趣,則要重點關注 @tarojs/react@tarojs/plugin-platform-react@tarojs/runtime 三個包,前兩者基於react-reconciler實現了自定義HostConfig介面,後者則模擬了BOM/DOM API實現了在小程式環境執行前端框架。吃透它們對Taro基於執行時的架構理解有很大幫助。

如果你對編譯時感興趣,則要重點關注 @tarojs/mini-runner@tarojs/taro-loader 兩個包,理解它們可以幫助你精進webpack配置能力。

以後再單獨研究一下編譯時,不在本文範圍內

執行時

Taro3整體架構

萬變不離其宗”:無論前端框架如何演進,其在底層呼叫都是W3C規範中的原生API,如window、document等。如果在小程式環境中實現相容W3C規範的DOM、BOM API,就可以讓不同的前端框架執行在小程式環境了。而不同小程式端平臺的差異性,如生命週期、元件庫、API等差異,則通過指定一套端平臺外掛標準由各端實現差異點,在編譯時注入到打包結果中。

小程式

開發者使用React或Vue框架撰寫業務程式碼,通過taro build進行編譯打包(觸發編譯時),而Taro在運⾏時中提供了 React 和 Vue 對應的介面卡進⾏適配(createReactApp,createVueApp),然後調⽤Taro提供的 DOM、BOM API,呼叫不同端平臺外掛,最後把整個程式渲染到所有的⼩程式端上⾯。

image.png

下文以React前端框架為例

React

React框架可以簡單分為三部分:react-core + react-reconciler + Renderer

react-core提供了供開發者呼叫的核心API,以jsx為DSL描述語言;

react-reconciler 內部基於“雙快取”的調和機制維護了Fiber元件樹,並完整實現了Diff演算法,決定何時更新、更新什麼;

Renderer 則具體實現客戶端的渲染,以及DOM事件處理;

react-dom是瀏覽器端的Renderer,呼叫DOM、BOM API來渲染介面。但在小程式環境中,則行不通。好在react-reconciler提供了HostConfig介面,理論上只要按照HostConfig介面實現對應方法,就可以在任意端完成介面渲染。

Taro提供了 @tarojs/taro-react 包,用來連線 react-reconcilertaro-runtime 的 BOM/DOM API。它就是基於 react-reconciler 的小程式專用 React 渲染器,連線 @tarojs/runtime 的 DOM 例項,相當於小程式版的react-dom,暴露的 API 也和 react-dom 保持一致。

H5環境下,taro-react直接使用 react-dom

image.png

1、實現宿主配置

完整實現HostConfig介面,在方法中呼叫對應的Taro BOM/DOM API

ts // taro-react/src/reconciler.ts import { document } from '@tarojs/runtime' // 模擬了window、document全域性物件 const hostConfig = { createInstance (type) { return document.createElement(type) }, // ... } const TaroReconciler = Reconciler(hostConfig) export { TaroReconciler }

2、實現渲染函式

核心邏輯是暴露 render 方法:

ts // taro-react/src/render.ts import { TaroReconciler } from './reconciler' class Root { constructor(renderer, domContainer){ this.renderer = renderer // 值得注意的是,傳入ConcurrentMode,則會啟用了React18中併發新特性 this.internalRoot = renderer.createContainer(domContainer, 0/** LegacyRoot: react-reconciler/src/ReactRootTags.js */, false, null) } render(children, cb){ const { renderer, internalRoot } = this renderer.updateContainer(children, internalRoot, null, cb) return renderer.getPublicRootInstance(internalRoot) } unmount (cb: Callback) { this.renderer.updateContainer(null, this.internalRoot, null, cb) } } export function render(element, domContainer, cb){ const root = new Root(TaroReconciler, domContainer) ContainerMap.set(domContainer, root) return root.render(element, cb) }

3、封裝為小程式版‘react-dom’

ts import {render} from './render' export default { render, // 其他方法 }

render何時呼叫?

如果前端框架為React時,在編譯時,會引入外掛 taro-plugin-react, 外掛內會呼叫 modifyMiniWebpackChain —> setAlias

ts if(framework === 'react'){ // 別名,在小程式內呼叫ReactDOM就是剛才封裝的小程式版react-dom alias.set('react-dom$', '@tarojs/react') }

使用者編寫的app.js會被 createReactApp 方法包裹,在createReactApp方法中會呼叫 ReactDOM.render 方法

ts // taro-plugin-react/src/runtime/connect.ts if (process.env.TARO_ENV !== 'h5') { appWrapper = ReactDOM.render?.(h(AppWrapper), document.getElementById('app')) }

image.png

至此搞定了在小程式環境中的邏輯層改造。

檢視

小程式邏輯層描述業務邏輯,而檢視層則負責展示內容,兩者通過基於Data/Event的jsbridge進行通訊。在小程式環境需要提前將檢視描述出來,不可以在邏輯層去動態生成檢視(所以React.createPortal用不了)。不過小程式提供了template的模板能力,支援在檢視層通過mustache語法來動態渲染檢視。

Taro使⽤了模板拼接的⽅式,根據運⾏時提供的 DOM 樹資料結構,各 templates 遞迴地 相互引⽤,最終可以渲染出對應的動態 DOM 樹。

模板化處理

首先,將小程式的所有元件挨個進行模版化處理,從而得到小程式元件對應的模版。如下圖就是小程式的 view 元件模版經過模版化處理後的樣子。⾸先需要在 template ⾥⾯寫⼀個 view,把它所有的屬性全部列出來(把所有的屬性都列出來是因為⼩程式⾥⾯不能去動態地新增屬性)。以下是view的模板化呈現:

html <template name="tmpl_0_view"> <view hover-class="{{xs.b(i.hoverClass,'none')}}" hover-stop-propagation="{{xs.b(i.hoverStopPropagation,false)}}" hover-start-time="{{xs.b(i.hoverStartTime,50)}}" hover-stay-time="{{xs.b(i.hoverStayTime,400)}}" bindtouchstart="eh" bindtouchmove="eh" bindtouchend="eh" bindtouchcancel="eh" bindlongpress="eh" animation="{{i.animation}}" bindanimationstart="eh" bindanimationiteration="eh" bindanimationend="eh" bindtransitionend="eh" style="{{i.st}}" class="{{i.cl}}" bindtap="eh" id="{{i.uid||i.sid}}" data-sid="{{i.sid}}" > <block wx:for="{{i.cn}}" wx:key="sid"> <template is="{{xs.e(cid+1)}}" data="{{i:item,l:l}}" /> </block> </view> </template> <template name="tmpl_1_view">...</template>

元件模板化的核心程式碼在 packages/shared/src/template.ts 檔案中.

可以在打包出dist目錄中的base.wxml中看到所有元件模板化的結果,當然Taro只會將專案中使用到的元件進行輸出。

同時,Taro會建立一個根模板,每個頁面都會從 taro_tmpl 開始遞迴渲染:

```html

「其他文章」