前言:隨着小程序開發的熱度上升,小程序開發框架也層出不窮。但目前每一個框架都會綁定一個專屬 DSL,如類 React 或者類 Vue,在一個框架內,開發者沒法根據團隊技術棧自由選擇 DSL,同時也沒法共享框架自己的生態與工具。本次分享將爲你們介紹 Taro 如何將各類語法的前端框架(React/Vue 等)運行在小程序上,討論一個框架支持多 DSL 的實現探索,使得開發者可使用任意熱門框架/語法/DSL 來編寫小程序應用,同時複用相關生態。css
2017 年 1 月 9 日凌晨,萬衆期待的微信小程序正式上線。html
在此以前,京東投入一個前端小團隊,通過一個月的封閉式開發,以一週一個版本的速度進行迭代,終於在第一時間發佈了本身的 「京東購物」 小程序,儘管功能和界面如今看起來有些簡陋,但在當時是徹底符合微信小程序「觸手可及,用完即走」的理念。前端
固然,隨着整個項目的不斷迭代,如今的 「京東購物」小程序在設計、交互以及功能複雜度已經全面向 APP 端看齊,這裏面的工程化實踐已經由 劉慧敏 老師在 GMTC 全球大前端技術大會(北京站)2019 進行過度享,有興趣的能夠下載 PPT:京東購物小程序工程化之路。vue
當時的微信小程序的開發存在一些缺點,好比依賴管理混亂、工程化流程落後、ES Next 支持不完善、命名規範不統一等。這些問題在如今看來都已經有了各類官方或非官方的解決辦法,可是在當時小程序開發的探索階段,這些問題都是一些痛點問題。node
有句話我我的特別喜歡,那就是「當一門語言的能力不足,而用戶的運行環境又不支持其它選擇的時候,這門語言就會淪爲 「編譯目標」 語言」。react
縱觀整個前端的歷史,不管是 CSS 預處理器的大行其道、各類模版的流行,仍是 CoffeeScript 乃至 TypeScript 的誕生,都印證了這個說法,微信小程序這裏也不例外。所以,各類小程序開發框架如百花齊放,層出不窮。webpack
這些小程序開發框架最主要的區別是 DSL,這點從 logo 顏色上就能夠看出來,除了滴滴的 Chameleon 是自定義 DSL 外,其他的綠色的 logo 是遵循了 Vue 語法(如 mpvue ),藍色的 logo 是遵循了 React 的語法(如 Taro)。git
在微信小程序以後,各大廠商紛紛發佈了本身的小程序平臺,好比:支付寶、百度、頭條、QQ等,再加上快應用、網易、360、京東等,小程序的賽道愈來愈擁擠,開發人員須要適配的小程序平臺愈來愈多,所以,各大小程序開發框架也紛紛進行了多端適配。github
所以,站在這個時間節點反過來回顧整個小程序開發框架的進程,你會發現整個 2018 年乃至 2019 年初,小程序的開發框架主要的區別和重心在於:DSL 以及 多端適配。web
正所謂「業務孵化技術,技術服務業務」,Taro 的誕生源自於業務需求的增長,當時咱們的團隊須要同時負責:京東購物,TOPLIFE 等業。團隊人力資源捉襟見肘,與此同時,以上的業務都或多或少存在多端的需求,好比 微信小程序、H五、React Native(京東的主流 APP基本都內置了 React Native 渲染引擎),並且能夠預見的是,之後頗有可能須要適配更多的小程序平臺,而每一個端開發一套代碼又不現實,會致使:研發成本上升,代碼維護困難。
當時咱們團隊自研了一款 類React 框架:Nervjs, 整個團隊的技術棧所以所有轉向了 React ,而當時市面上又沒有一款遵循 React 語法的小程序框架,所以,咱們開發了 Taro,但願可以使用 React 語法寫小程序的同時,經過「Write once Run anywhere」來實現跨端的。
整個 Taro 框架從 2018 年 6 月 7 日開源至今,一致保持着高速迭代,這些迭代主要集中在三個方面:
通過團隊 一年多的努力,Taro 獲得了社區的普遍承認,截止 2019年 12 月 18日,Taro 已擁有 22254 Stars 和 250 名 Contributors,社區主動提交的開發案例 150+:taro-user-cases,其中不乏多端案例。
可是儘管如此,Taro 仍是存在一些問題沒法解決,或者說:沒那麼好解決。好比:和 React DSL
強綁定、JSX 適配工做量大、社區貢獻複雜等。這些問題歸根到底,很大一部分是 Taro 的架構問題。
所以咱們團隊也一直在等待一次合適的機會,對整個架構進行一次提高,同時修復一些項目快速迭代欠下的技術債。
最主要的是,單純的項目維護迭代已經知足不了咱們團隊躁動的心,咱們渴望藉此機會進行一次技術突破。
在講 Taro 架構以前,咱們先來回顧一下小程序的架構。
微信小程序主要分爲 邏輯層 和 視圖層,以及在他們之下的原生部分。邏輯層主要負責 JS 運行,視圖層主要負責頁面的渲染,它們之間主要經過 Event
和 Data
進行通訊,同時經過 JSBridge
調用原生的 API。這也是以微信小程序爲首的大多數小程序的架構。
因爲原生部分對於前端開發者來講就像是一個黑盒,所以,整個架構圖的原生部分能夠省略。同時,咱們咱們對 邏輯層 和 視圖層 也作一下簡化,最後能夠獲得小程序架構圖的極簡版:
也就是說,只須要在邏輯層調用對應的 App()/Page()
方法,且在方法裏面處理 data、提供生命週期/事件函數等,同時在視圖層提供對應的模版及樣式供渲染就能運行小程序了。這也是大多數小程序開發框架重點考慮和處理的部分。
Taro 當前的架構主要分爲:編譯時 和 運行時。
其中編譯時主要是將 Taro 代碼經過 Babel 轉換成 小程序的代碼,如:JS
、WXML
、WXSS
、JSON
。
運行時主要是進行一些:生命週期、事件、data 等部分的處理和對接。
有過 Babel 插件開發經驗的應該對一下流程十分熟悉,Taro 的編譯時也是遵循了此流程,使用 babel-parser 將 Taro 代碼解析成抽象語法樹,而後經過 babel-types 對抽象語法樹進行一系列修改、轉換操做,最後再經過 babel-generate 生成對應的目標代碼。
詳情能夠參考:babel-handbook
整個編譯時最複雜的部分在於 JSX 編譯。
咱們都知道 JSX 是一個 JavaScript 的語法擴展,它的寫法變幻無窮,十分靈活。這裏咱們是採用 窮舉 的方式對 JSX 可能的寫法進行了一一適配,這一部分工做量很大,實際上 Taro 有大量的 Commit 都是爲了更完善的支持 JSX 的各類寫法。
但儘管如此,咱們也不可能徹底覆蓋全部的狀況,所以仍是推薦你們按照官方規範書寫 React 代碼,同時,咱們也提供了豐富的 ESlint 插件來輔助你們書寫規範的代碼。
這一塊咱們團隊內部一直有個梗:若是你使用 Taro 開發感受 Bug 少,那說明你的 React 代碼寫得很規範。
接下來,咱們能夠對比一下編譯後的代碼,能夠發現,編譯後的代碼中,React 的核心 render 方法 沒有了。同時代碼裏增長了 BaseComponent
和 createComponent
,它們是 Taro 運行時的核心。
// 編譯前 import Taro, { Component } from '@tarojs/taro' import { View, Text } from '@tarojs/components' import './index.scss' export default class Index extends Component { config = { navigationBarTitleText: '首頁' } componentDidMount () { } render () { return ( <View className=‘index' onClick={this.onClick}> <Text>Hello world!</Text> </View> ) } } // 編譯後 import {BaseComponent, createComponent} from '@tarojs/taro-weapp' class Index extends BaseComponent { // ... _createDate(){ //process state and props } } export default createComponent(Index)
BaseComponent
大概的 UML 圖以下,主要是對 React 的一些核心方法:setState
、forceUpdate
等進行了替換和重寫,結合前面編譯後 render 方法被替換,你們不難猜出:Taro 當前架構只是在開發時遵循了 React 的語法,在代碼編譯以後實際運行時,和 React 並無關係。
而 createComponent
主要做用是調用 Component()
構建頁面;對接事件、生命週期等;進行 Diff Data
並調用 setData
方法更新數據。
所以,整個 Taro 當前架構的特色是:
小程序開發框架百花齊放,咱們也從社區裏獲得了很多啓發。
接下來咱們來看看 遵循 vue 語法的小程序開發框架的表明:mpvue 是怎樣實現的。
看過 Vue 源碼的同窗對上面的文件夾和架構確定熟悉,本質上,mpvue 就是 fork 了一份 vuejs/vue@2.4.1
的代碼,保留了 Vue runtime 能力,同時添加了小程序平臺的支持。
具體在源碼中的表現就是:在 Vue 源碼的 platforms 文件夾下面增長了 mp 目錄,在裏面實現了 complier(編譯時)
和 runtime (運行時)
支持。
mpvue 的實現一樣分爲:編譯時和運行時。
其中編譯時作的事情和 Taro 很相似:將 Vue SFC 寫法的代碼編譯成 小程序代碼文件(JS、WXML、WXSS、JSON)。
最大的區別是 Taro 將 JSX 編譯成 小程序模版,而 mpvue 是將 Vue 模版編譯成 小程序模版。可是因爲 Vue 模版和 小程序模版的類似性,mpvue 在這一塊的工做量比 Taro 少得多。
而 mpvue 的運行時和 Vue 的運行時是強關聯的,首先咱們來看看 Vue 的運行時。
一個 .vue 的單文件由三部分構成: template
, script
, style
。
橙色路徑部分, template 會在編譯的過程當中,在 vue-loader
中經過 ast 進行分析,最終生成一段 render 函數,執行 render 函數會生成虛擬dom樹,虛擬 DOM 樹是對真實 DOM 樹的抽象,樹中的節點被稱做 vnode 。
Vue 拿到 虛擬 DOM 樹以後,就能夠去和上次老的 虛擬 DOM 樹 作 patch diff
對比。patch 階段以後,vue 就會使用真實的操做DOM 的方法(好比說 insertBefore
, appendChild
之類的),去操做DOM結點,更新視圖。
同時,綠色路徑的部分,在實例化 Vue 的時候,會對數據 data 作響應式的處理,在監測到 data 發生改變時,會調用 render 函數,生成最新的虛擬 DOM 樹, 接着對比老的虛擬 DOM 樹進行 patch, 找出最小修改代價的 vnode 節點進行修改。
而 mpvue 的運行時,會首先將 patch 階段的 DOM 操做相關方法置空,也就是什麼都不作。其次,在建立 Vue 實例的同時,還會偷偷的調用 Page()
用於生成了小程序的 page 實例。而後 運行時的 patch 階段會直接調用 $updateDataToMp()
方法,這個方法會獲取掛在在 page 實例上維護的數據 ,而後經過 setData
方法更新到視圖層。
mpvue 總體原理圖也就以下:
所以,和 Taro 重編譯時輕運行時不一樣,mpvue 算是:半編譯時,半運行時。這點從代碼量的對比也能大體反映出來。
mpvue 的 WXML 模版和 Taro 同樣,也是經過代碼編譯獲得的;不一樣於 Taro 運行時和 React 無關,mpvue 本質上仍是將 Vue 運行在了小程序,且實現了 Vue@2.4.1
絕大部分特性(只有極少數特性因爲小程序模版的限制未能實現,如 :filter
、slot
、v-html
);且整個框架基於 Webpack 實現了較爲完善的工程化。
其餘小程序框架的實現原理和效果上的差別性,也帶來了咱們的一些思考:
這一次,咱們站在瀏覽器的角度來思考前端的本質:不管開發這是用的是什麼框架,React 也好,Vue 也罷,最終代碼通過運行以後都是調用了瀏覽器的那幾個 BOM/DOM 的 API ,如:createElement
、appendChild
、removeChild
等。
所以,咱們建立了 taro-runtime 的包,而後在這個包中實現了 一套 高效、精簡版的 DOM/BOM API(下面的 UML 圖只是反映了幾個主要的類的結構和關係):
而後,咱們經過 Webpack 的 ProvidePlugin 插件,注入到小程序的邏輯層。
這樣,在小程序的運行時,就有了 一套高效、精簡版的 DOM/BOM API。
在 DOM/BOM
注入以後,理論上來講,Nerv/Preact 就能夠直接運行了。可是 React 有點特殊,由於 React-DOM
包含大量瀏覽器兼容類的代碼,致使包太大,而這部分代碼咱們是不須要的,所以咱們須要作一些定製和優化。
在 React 16+ ,React 的架構以下:
最上層是 React 的核心部分 react-core
,中間是 react-reconciler
,其的職責是維護 VirtualDOM
樹,內部實現了 Diff/Fiber
算法,決定何時更新、以及要更新什麼。
而 Renderer
負責具體平臺的渲染工做,它會提供宿主組件、處理事件等等。例如 React-DOM
就是一個渲染器,負責 DOM 節點的渲染和 DOM 事件處理。
所以,咱們實現了 taro-react 包,用來鏈接 react-reconciler
和 taro-runtime
的 BOM/DOM API:
具體的實現主要分爲兩步:
react-reconciler
的 hostConfig
配置,即在 hostConfig
的方法中調用對應的 Taro BOM/DOM 的 API。ReactDOM.render
)方法,能夠當作是建立 Taro DOM Tree
的容器。
通過上面的步驟,React 代碼實際上就能夠在小程序的運行時正常運行了,而且會生成 Taro DOM Tree
,那麼偌大的 Taro DOM Tree 怎樣更新到頁面呢?
首先,咱們將小程序的全部組件挨個進行模版化處理,從而獲得小程序組件對應的模版,以下圖就是小程序的 view 組件通過模版化處理後的樣子:
而後,咱們會:基於組件的 template,動態 「遞歸」 渲染整棵樹。
具體流程爲先去遍歷 Taro DOM Tree
根節點的子元素,再根據每一個子元素的類型選擇對應的模板來渲染子元素,而後在每一個模板中咱們又會去遍歷當前元素的子元素,以此把整個節點樹遞歸遍歷出來。
整個 Taro Next 的 React 實現流程圖以下:
別看 React 和 Vue 在開發時區別那麼大,其實在實現了 BOM/DOM API 以後,它們之間的區別就很小了。
Vue 和 React 最大的區別就在於運行時的 CreateVuePage
方法,這個方法裏進行了一些運行時的處理,好比:生命週期的對齊。
其餘的部分,如經過 BOM/DOM 方法構建、修改 DOM Tree 及渲染原理,都是和 React 一致的。
提到 Flutter ,就不得不提 Flutter Web ,Flutter Web 是在標準瀏覽器 API 之上實現 Flutter 的核心繪圖層,本質上也是最終調用了 BOM/DOM API。所以,理論來講,也是能夠進行適配的,但這一塊咱們並不會投入太多的精力,最終會像快應用同樣交給社區來實現和維護。
接下來和你們展開聊一下 Taro Next 更多的細節實現,好比:事件、更新、生命週期。
首先的 Taro Next 事件,具體的實現方式以下:
bindtap
、bindchange
、bindsubmit
等。eventHandler
函數,和 eh 方法綁定,收集全部的小程序事件document.getElementById()
方法獲取觸發事件對應的 TaroNode
createEvent()
建立符合規範的 TaroEvent
TaroNode.dispatchEvent
從新觸發事件
能夠看到,Taro Next 事件本質上是基於 Taro DOM 實現了一套本身的事件機制,這樣作的好處之一是,不管小程序是否支持事件的冒泡與捕獲,Taro 都能支持。
不管是 React 仍是 Vue ,最終都會調用 Taro DOM 方法,如:appendChild
、insertChild
等。
這些方法在修改 Taro DOM Tree 的同時,還會調用 enqueueUpdate
方法,這個方法能獲取到每個 DOM 方法最終修改的節點路徑和值,如:{root.cn.[0].cn.[4].value: "1"}
,並經過 setData
方法更新到視圖層。
能夠看到,這裏更新的粒度是 DOM 級別,只有最終發生改變的 DOM 纔會被更新過去,相對於以前 data 級別的更新會更加精準,性能更好。
相對與其餘部分大刀闊斧的升級改造,生命週期多是變更最小的部分之一。和以前相似,生命週期的實現是在運行時維護的 App 實例 / Page 實例進行了生命週期方法的一一對應。
const config: PageInstance = { onLoad (this: MpInstance, options) { //... }, onUnload () { //... }, onShow () { safeExecute('onShow') }, onHide () { safeExecute('onHide') }, onPullDownRefresh () { safeExecute('onPullDownRefresh') } //... }
和以前的架構不一樣,Taro Next 是 近乎全運行。
新的架構基本解決了以前的遺留問題:
前面提到,同等條件下,編譯時作的工做越多,也就意味着運行時作的工做越少,性能會更好。Taro Next 的新架構變成 近乎全運行 以後,花了不少精力在性能優化上面。
再這以前。能夠先看一下 Taro Next 的流程和原生小程序的流程對比。
能夠發現,相比原生小程序,Taro Next 多了紅色部分的帶來的性能隱患,如:引入React/Vue 帶來的 包的 Size 增長,運行時的損耗、Taro DOM Tree 的構建和更新、DOM data 初始化和更新。
而咱們真正能作的,只有綠色部分,也就是:Taro DOM Tree 的構建和更新、DOM data 初始化和更新。
首先咱們來看包 Size,下面的表格是 TodoMVC 的例子,在原生、Taro Old、Taro Next 等狀況下的包大小對比,能夠看到,引入 React/Vue 後,包大小在 Gzip 狀況下大概增長了 30k 左右。
不過咱們在前面一再強調:和以前模版經過編譯生成的不一樣,Taro Next 的模版是固定的,而後基於組件的 template,動態 「遞歸」 渲染整棵 Taro DOM 樹。也就是說,Taro Next 的 WXML 大小是有上限的。
隨着項目的增長,頁面愈來愈多,原生的項目 WXML 體積會不斷增長,而 Taro Next 不會。也就是說,當頁面的數量超過一個臨界點時,Taro Next 的包體積可能會更小。所以,包 Size 的問題不足爲慮。
在 Taro DOM Tree 的構建和更新階段,咱們實現了一套僅實現了高效的、精簡版 DOM/BOM API,並且僅僅實現了必要的。
Github上有一個倉庫 jsdom,基本上是在 Node.js 上實現了一套 Web 標準的 DOM/BOM ,這個倉庫的代碼在壓縮前大概有 2.1M,而 Taro Next 的核心的 DOM/BOM API 代碼才 1000 行不到。
所以,咱們最大限度的保證了 Taro DOM Tree 構建和更新階段的性能。
在數據更新階段,首先前面有提到過,Taro Next 的更新是 DOM 級別的,比 Data 級別的更新更加高效,由於 Data 粒度更新其實是有冗餘的,並非全部的 Data 的改變最後都會引發 DOM 的更新。
其次,Taro 在更新的時候將 Taro DOM Tree 的 path
進行壓縮,這點也極大的提高了性能。
最終的結果是:在某些業務場景寫,add
、select
數據,Taro Next 的性能比原生的還要好。
固然,實驗的數據總歸會有缺陷,最終具體的性能表現,還要靠各類複雜業務場景的檢驗。你們若是對 Taro Next 的性能感興趣的,能夠自行跑一下 taro-benchmark 包,對比一下結果。
咱們也在一直持續的全方位優化 Taro Next 的性能,具體能夠關注 Taro Next 的最新的 Commit 。
Taro Next 將會在不久以後的 3.0 版本正式發佈,支持使用 React/Vue 開發跨端小程序,而後在會在後續的迭代中拓展至其餘端,並完善對應的生態。
Taro 團隊仍是會將支持的重點放在 React/Vue,Flutter 和 Angular 會像快應用同樣,交給社區來適配和維護,快應用就是華爲的 Qiyu8 和 Issacpeng 在幫咱們進行適配,很是感謝他們。
同時,咱們還打造了 「Taro 移動端一站式研發平臺」,將先前積累的多端開發工做流和工程化的方案進行了統一,並內置了數據監控、組件市場以及可視化搭建,當前正處於內測階段。
Learn Once Write AnyWhere
,而咱們 Taro 的口號是 Write Once Run AnyWhere
,這一點也常常致使咱們常常被人噴,這裏說一點我本身的想法:Learn Once Write AnyWhere
其實本質上對開發者更友好,好比開發者只須要學習 React 技術棧,就能夠開發 Web/移動端 應用,可是對項目就沒那麼友好了,每一個項目都得維護一份代碼;而 Write Once Run AnyWhere
是對開發者沒那麼友好(適配的端越多,適配的成本必然也會水漲船高,對開發者要求也很變高),可是根據咱們的實踐,對項目會更友好,「一套代碼,多端適配」。固然,這裏適配的粒度,並不必定是項目級別的,其實在咱們的具體實踐中,有至關一部分是:業務級甚至是頁面級的。
正所謂「單絲不成線,獨木不成林」,Taro 發展至今早已不在屬於單一團隊的項目了,而是整個 Taro 開發社區共同的項目。
最後,仍是藉此機會感謝一些社區全部幫助過 Taro 的成長的人,特別是 Taro 的貢獻者們,很是感謝!
同時也感謝受邀成爲 TaroUI 核心維護人員的 Garfield550 (小姐姐)、梁音、ShaoQian Liu,他們將支撐起 TaroUI 的後續迭代與維護。
固然還有在社區中樂於助人、積極貢獻的 zacksleo 、Jay Fong、loveonelong、lolipop99、波仔糕、原罪、lentoo 、白領夏公子 、YuanQuan、 tourze、 lingxiaoZhu 等等。
此外,還要感謝一直默默爲 Taro 發展提供寶貴建議的研發團隊:騰訊雲、數字廣東、騰訊CDC、網易嚴選、華爲開源團隊、招聯消費金融等等。
長風破浪會有時,直掛雲帆濟滄海。
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章: