2020 年 3 月,在暨支援編譯時方案之後,Rax 小程式發布了支援執行時間方案的版本。截至目前,Rax 仍是業界唯一同時支援編譯時和執行時間方案的小程式開發框架。本文將向大家介紹 Rax 小程式運行時方案的原理以及我們的思考。
在介紹運行時方案之前,我們再回顧下什麼是編譯時方案。顧名思義,編譯時方案著重於編譯,這其中的代錶框架是 Taro v2.x。其透過靜態編譯的方式,將JSX 轉換為小程式的模板語言(即WXML/AXML 等),再輔以輕量級的執行時間JS 程式碼,抹平小程式生命週期和React 生命週期的差異,使使用者能夠以熟悉的React DSL 進行小程式開發。 Rax 的編譯時方案原理與 Taro v2.x 類似,關於實作細節,可以參考先前的文章Rax 轉小程式連結原理解析(一)及Rax 小程式編譯時方案原理解析。區別於編譯時方案,運行時方案側重於運行時實現渲染能力,不依賴靜態編譯,因此幾乎沒有語法限制,這也是其最大的特徵。下面就來看看運行時方案實現的原理。
小程式的底層實作其實也是基於 Web 技術,但是反映至開發者層面,與 Web 卻又大相逕庭。在小程式中,邏輯層和視圖層隔離,邏輯層透過唯一的setData
方法將資料傳遞至視圖層觸發渲染,視圖層則透過事件的方式觸發邏輯層程式碼,其架構如下圖所示。相較於 Web 開發時開發者可以透過 JS 呼叫瀏覽器提供的 DOM/BOM API 隨心所欲操作渲染內容,小程式的架構更封閉也更安全,但也意味著 Web 程式碼無法直接在小程式上運作。
對於現代的前端框架(React/Vue)來說,底層基本上就是透過呼叫 DOM API 來建立視圖。而小程式的視圖層範本是需要開發者事先寫好的,這意味著動態建立 DOM 的方式在小程式中不被允許。但是,小程式的自訂元件具有的『自引用』特性為動態建立 DOM 開啟了突破口。所謂自引用,就是自訂元件支援使用自己作為子節點,也意味著透過遞歸引用的方式,我們能夠建構任意層級和數量的 DOM 樹。
舉例來說,假設一個小程式自訂元件element 的WXML 範本如下所示:
<view> <block> <element></element> </block></view><text> {{r.content}}</text>复制代码
注意到,element 在範本中遞歸引用了自身,並透過條件判斷終止遞歸。那麼,當邏輯層透過setData
傳遞了以下一份資料過來時:
{ "nodeId": "1", "tagName": "view", "children": [ { "nodeId": "2", "tagName": "text", “content”: “我是?" }, { "nodeId": "3", “tagName": "text", "content": "rax" } ] }复制代码
最終呈現出來的視圖便成了:
<view> <text>我是</text> <text>rax</text></view>复制代码
透過這種方式,我們巧妙地實現了在WXML 範本固定的情況下,根據傳入的setData
資料來動態渲染視圖的能力。而這,也正是運行時方案能夠誕生的基礎。
Rax 的運行時方案脫胎自 kbone——微信官方推出的小程式與 web 端同構解決方案。 kbone 的設計原理可以參考其官網介紹,簡單總結就是透過在邏輯層模擬DOM/BOM API,將這些建立視圖的方法轉換為維護一棵VDOM 樹,再將其轉換成對應setData
#的數據,最後透過預置好的模板遞歸渲染出實際視圖。從 DOM API 到維護 VDOM 樹的過程基本原理並不複雜,createElement/appendChild/insertBefore/removeChild 等對應著基本的資料結構的操作。
熟悉 Rax 的同學應該知道,為了支援跨端,Rax 有 driver 的設計。實際上,我們完全可以針對小程式端再寫一個 driver,根據上述原理實作其介面 API 即可。但我們最後的選擇還是透過更底層的模擬 BOM/DOM API 來完成了整個渲染機制。這麼做的考量是,第一,基於kbone 開發,這是最快的一套方案,小程式端的driver 只需複用web 端的driver-dom 即可,畢竟底層的document
和window
變數都已經模擬好;第二,則是因為我們想提供開發者更貼近web 的開發體驗。這套方案意味著開發者除了使用 JSX 之外,也是支援直接使用 BOM/DOM API 建立視圖的,彈性度會更高一點。我們把目光拉長到整個市面上的小程式執行時間框架,remax 透過react-reconciler 直接從VDOM 層和小程式對接(類似上面說的Rax 小程式driver 設計),而kbone 和Taro 3.0 都選擇透過模擬Web 環境來實作渲染。這也與框架開發人員的設計意圖有關,見仁見智。 Rax 小程式執行時間方案的基本原理圖如下:
Rax 小程式執行階段中,模擬DOM/BOM API 的函式庫為解密與思考Rax 小程式運行時方案,其支援的API 如下:
除了處理渲染資料外,另一個比較重要的事情就是事件系統。其透過 EventTarget
基底類別實作了一套完整的事件派發機制。邏輯層 DOM 節點均繼承自 EventTarget
,透過唯一的 nodeId
來收集自身綁定事件。視圖層模板上的每個內建元件都會綁定 nodeId
,並監聽所有可觸發的事件,例如一個簡單的 view 標籤,會將 bindtap/bindtouchstart/bindtouchend 等事件都進行綁定。在事件觸發時,透過 event.currentTarget.dataset.nodeId
取得到目標節點 id,再觸發該節點上使用者綁定的對應函數。
Rax 小程式執行階段的工程主體流程 follow 了 Rax Web 的設計,Web 端 Webpack 打包出的 JS Bundle 可以在小程式執行時中重複使用。我們透過插件將 解密與思考Rax 小程式運行時方案 模擬出的 window 和 document 變數注入該 bundle,再產生一個固定的小程式專案骨架,在 app.js 中載入 JS Bundle 即可。其整體工程結構如下圖所示:
以上架構是逐步演進的結果。最初,我們使用了 webpack 的多 entry 模式打包執行階段小程式程式碼,也就是每個頁面都會作為一個 entry 獨立打包。這使得從行為上來說小程式更像一個 MPA。這帶來的問題就是頁間公共依賴的程式碼不在同一記憶體中執行,與原生小程式表現不符。這個差異導致我們最終決定變更工程打包模式。目前版本的 Rax 運行時小程式更符合 SPA 的形式,所有的業務代碼都打包到了一個 JS 檔案中。
我們將Rax 工程入口的rax-app 套件在小程式運行時上的連結做了一定改造,其在初始化時會根據路由返回各個頁面的render
函數,此render
函數建立root 節點(document.createElement
)將對應的Rax 元件掛載至其上,並將root 節點append 到body 節點(document.body. appendChild
)。小程式每個頁面在 onLoad 生命週期中,會建立一個獨立的 document
並設定為全域變量,然後呼叫其對應的 render
函數進行各個頁面的獨立渲染。
从上面的小程序运行时原理来看,其性能相比原生是存在一定差距的,这主要由以下几个方面造成:第一:逻辑层运行完整的 Rax + 通过模拟 DOM/BOM API 处理 VDOM 并生成 setData 数据,需要消耗更多的计算时间;第二,相比原生小程序需要传递更多 setData 数据,如果容器层数据序列化能力较弱,会大大增加数据传输耗时;第三,视图层通过自定义组件递归动态生成视图,而我们知道递归动作本身就是一个性能损耗点。此外,由于无法预先知晓用户需要绑定的属性和事件,自定义组件模板中只能将所有属性和事件预先绑好,这导致小程序运行过程中会触发很多无用的事件,进一步加重负担。经过我们的 benchmark 计算,在支付宝小程序平台上,运行时小程序框架(包括 Rax/Taro/Remax 等)与原生小程序存在约 40% 的性能差距。
Rax 小程序运行时发布后,经测试其性能相比其他运行时框架存在着较为明显的差距,于是我们启动了性能调优的专项计划。通过以下方面的重构,成功将 Rax 小程序运行时小程序的性能拉升至业界领先水平,与 Taro/Remax 基本处于同一水平线。
更新数据精确化。在旧版本中,setData 的数据是全量更新的,虽然有 dom 子树分割批量更新的设计,但是数据传输仍然存在大量冗余。重构版本中,Rax 增加了节点渲染判断,未挂载节点无须触发更新;将所有更新收拢至顶层 root 节点统一批量处理, 并且通过精确计算数据更新的 path,实现局部更新。比如某次更新节点的 class 属性时,setData 的数据可能是:
{ "root.children.[0].children.[1].class": "active"}复制代码
内置小程序组件无需维护其属性列表,而是根据用户传参直接赋值。旧版本中,我们维护了所有内置组件的属性,在获取属性值的时候均需要调用 domNode.getAttribute,具有一定性能开销。重构版本 Rax 直接根据用户传参给属性赋值,并将默认值设置的操作移至视图层 WXS/SJS 中处理。
更新 解密與思考Rax 小程式運行時方案 中的数据结构。经过梳理,Rax 移除了冗余的 tree 数据,重写了 getaElementById 等 API;重构了 attribute、classList 等类;使用了更符合场景需要的 Map/Set 等数据结构,提升了整体的数据处理性能。
渲染模板优化。在支付宝小程序中,Rax 使用 template 进行递归调用;在微信中,Rax 使用 template 调用 element 再调用 template 的形式以避免微信端递归调用 template 的层数限制。在模板中,我们尽量使用 template is 语法进行判断,减少 a:if/wx:if 条件判断,提升模板递归时的性能。
无论是出于旧有业务的迁移,或者是出于性能考虑,Rax 小程序运行时中都存在着混合使用的需求。目前,Rax 已经打通与小程序内置组件、小程序自定义组件、小程序页面、小程序插件混合使用的能力。这其中,使用小程序自定义组件是最为复杂的。
在 Rax 中使用小程序自定义组件,其引入路径需要与 usingComponents
保持一致(例如 import CustomComp from '../components/CustomComp/index'
)。 在编译阶段,Rax 工程使用 Babel 插件进行代码扫描,检测到 JSX 中使用的某个组件是小程序自定义组件(根据其引入路径是否存在同名 axml 文件)时,会将其使用到的属性和事件进行缓存,然后通过 webpack 插件动态生成至递归模板中。在运行时中的创建节点阶段,通过查询缓存判断节点是否为自定义组件。若是自定义组件,则其渲染数据中会插入缓存中的属性,并且绑定事件至该自定义组件实例。
通过 Rax 小程序编译时方案产出的组件,从使用形态上来说,可以直接视为小程序自定义组件。而 Rax 工程加强了运行时与编译时的联系,当在 Rax 小程序运行时中使用编译时组件 npm 包时,用户无需引入组件的具体路径,只需像使用普通组件时一样引入,Rax 工程将自动根据该组件 package.json
中是否存在 miniappConfig
字段来判断其是否为一个 Rax 多端组件,然后直接使用其编译时的组件实现。
Rax 作為業界唯一一個同時支援編譯時和執行時間引擎的小程式開發方案,其雙引擎混合使用的能力能夠較為完美地實現效能與開發效率的平衡。在未來,Rax 將實現更靈活的雙引擎混合使用方式,例如支援在單一專案中指定某個元件以編譯時引擎編譯,為業務提供更高的靈活度。
以上就是 Rax 小程式執行時間方案的原理解析。運行時方案解決了編譯時方案天生的語法限制問題,但是也存在著明顯的效能掣肘。可以說,在目前 2020 年這個節點,小程式開發仍然沒有所謂的銀彈,也許 Rax 小程式雙引擎的融合會是相對範圍內的最優解。逆標準而上的小程式究竟能走多遠,誰也無法下定論,未來相當一段時間內,開發者仍要面臨種種問題。站在小程式開發框架的角度,只希望所有開發者能夠選擇對自己最合適的框架,爽快又有效率地完成小程式的開發。
相關免費學習推薦:微信小程式開發教學
以上是解密與思考Rax 小程式運行時方案的詳細內容。更多資訊請關注PHP中文網其他相關文章!