跨端小程式框架 --Taro演進

Afterward發表於2021-12-17

hybrid 應用

Hybrid App(混合模式移動應用)是指介於web-app、native-app這兩者之間的app,兼具“Native App良好使用者互動體驗的優勢”和“Web App跨平臺開發的優勢”。

客戶端提供webview控制元件,能在App內使用H5技術開發頁面,如果需要呼叫到系統能力的功能需要客戶端實現,兩者建立橋樑通訊實現互動

優點

  • 具備完整webview功能
  • 可間接使用系統原生功能
  • 隨時更新無需發版

缺點

  • 效能差距明顯
  • 白屏時間長,使用者體驗差
  • 系統操作需要客戶端原生支援提供方法
  • 有時候受限客戶端原因,需要特殊相容

微信小程式

簡介

「觸手可及,用完即走」

小程式提供了自己的檢視層描述語言 WXMLWXSS,以及基於 JavaScript 的邏輯層框架,並在檢視層與邏輯層間提供了資料傳輸和事件系統,讓開發者能夠專注於資料與邏輯。

框架的核心是一個響應的資料繫結系統,可以讓資料與檢視非常簡單地保持同步。當做資料修改的時候,只需要在邏輯層修改資料,檢視層就會做相應的更新。

技術發展史

小程式並非憑空冒出來的一個概念。當微信中的 WebView 逐漸成為移動 Web 的一個重要入口時,微信就有相關的 JS API 了。

2015年初,微信釋出了一整套網頁開發工具包,稱之為 JS-SDK,開放了拍攝、錄音、語音識別、二維碼、地圖、支付、分享、卡券等幾十個API。

JS-SDK是對之前的 WeixinJSBridge 的一個包裝,以及新能力的釋放,並且由對內開放轉為了對所有開發者開放,在很短的時間內獲得了極大的關注。從資料監控來看,絕大部分在微信內傳播的移動網頁都使用到了相關的介面。

JS-SDK 解決了移動網頁能力不足的問題,通過暴露微信的介面使得 Web 開發者能夠擁有更多的能力,然而在更多的能力之外,JS-SDK 的模式並沒有解決使用移動網頁遇到的體驗不良的問題

使用者在訪問網頁的時候,在瀏覽器開始顯示之前都會有一個白屏的過程,在移動端,受限於裝置效能和網路速度,白屏會更加明顯。因此設計了一個 JS-SDK 的增強版本,其中有一個重要的功能,稱之為“微信 Web 資源離線儲存”。

微信 Web 資源離線儲存是面向 Web 開發者提供的基於微信內的 Web 加速方案。

通過使用微信離線儲存,Web 開發者可藉助微信提供的資源儲存能力,直接從微信本地載入 Web 資源而不需要再從服務端拉取,從而減少網頁載入時間,為微信使用者提供更優質的網頁瀏覽體驗。每個公眾號下所有 Web App 累計最多可快取 5M 的資源。

在內部測試中,我們發現離線儲存能夠解決一些問題,但對於一些複雜的頁面依然會有白屏問題,例如頁面載入了大量的 CSS 或者是 JavaScript 檔案。除了白屏,影響 Web 體驗的問題還有缺少操作的反饋,主要表現在兩個方面:頁面切換的生硬和點選的遲滯感

微信面臨的問題是如何設計一個比較好的系統,使得所有開發者在微信中都能獲得比較好的體驗。這個問題是之前的 JS-SDK 所處理不了的,需要一個全新的系統來完成,它需要使得所有的開發者都能做到:

  • 快速的載入
  • 更強大的能力
  • 原生的體驗
  • 易用且安全的微信資料開放
  • 高效和簡單的開發
  • 無需安裝

而缺點也很多:

  • 元件較少
  • webview限制很多,普通頁面開發區別很大
  • 行為限制很多,包體積大小,資源使用,頁面開啟層級等等
  • 不同機型的相容問題也是比較多

這就是小程式的由來。

小程式與普通網頁開發的區別

網頁開發渲染執行緒和指令碼執行緒是互斥的,這也是為什麼長時間的指令碼執行可能會導致頁面失去響應,而在小程式中,二者是分開的,分別執行在不同的執行緒中。

網頁開發者可以使用到各種瀏覽器暴露出來的 DOM API,進行 DOM 選中和操作。而如上文所述,小程式的邏輯層和渲染層是分開的,邏輯層執行在 JSCore 中,並沒有一個完整瀏覽器物件,因而缺少相關的DOM API和BOM API。這一區別導致了前端開發非常熟悉的一些庫,例如 jQueryZepto 等,在小程式中是無法執行的。

同時 JSCore 的環境同 NodeJS 環境也是不盡相同,所以一些 NPM 的包在小程式中也是無法執行的。

開發端開發場景
網頁開發者各式各樣的瀏覽器
PC 端IE、Chrome、QQ瀏覽器等
移動端Safari、Chrome以及 iOS、Android 系統中的各式 WebView
小程式兩大作業系統 iOS 和 Android 的微信客戶端,以及用於輔助開發的小程式開發者工具

小程式中三大執行環境也是有所區別的

執行環境邏輯層渲染層
iOSJavaScriptCoreWKWebView
安卓V8chromium定製核心
小程式開發者工具NWJSChrome WebView

網頁開發者在開發網頁的時候,只需要使用到瀏覽器,並且搭配上一些輔助工具或者編輯器即可。小程式的開發則有所不同,需要經過申請小程式帳號、安裝小程式開發者工具、配置專案等等過程方可完成。

渲染層和邏輯層

小程式的執行環境分成渲染層邏輯層,其中 WXML 模板和 WXSS 樣式工作在渲染層,JS 指令碼工作在邏輯層。

小程式的渲染層和邏輯層分別由2個執行緒管理:渲染層的介面使用了WebView 進行渲染;邏輯層採用JsCore執行緒執行JS指令碼。一個小程式存在多個介面,所以渲染層存在多個WebView執行緒,這兩個執行緒的通訊會經由微信客戶端(下文中也會採用Native來代指微信客戶端)做中轉,邏輯層傳送網路請求也經由Native轉發,小程式的通訊模型下圖所示。

特別需要注意的是setData呼叫方式和時機

  • setData介面的呼叫涉及邏輯層與渲染層間的執行緒通訊,通訊過於頻繁可能導致處理佇列阻塞,介面渲染不及時而導致卡頓,應避免無用的頻繁呼叫。
  • 由於小程式執行邏輯執行緒與渲染執行緒之上,setData的呼叫會把資料從邏輯層傳到渲染層,資料太大會增加通訊時間
  • setData操作會引起框架處理一些渲染介面相關的工作,一個未繫結的變數意味著與介面渲染無關,傳入setData會造成不必要的效能消耗。

開發環境

小程式提供了一個基於nwjs實現的IDE開發工具,可以模擬程式碼預覽效果,但是實際上並不是真機的webview環境,所以很多時候會發現開發環境跟真機預覽的差別存在很多細節問題.並且因為小程式本身的包體大小限制,很多時候無法直接喚起真機除錯,需要特殊處理將體積壓到2M以下才行

載入過程

微信客戶端在開啟小程式之前,會把整個小程式的程式碼包下載到本地。

緊接著通過 app.jsonpages 欄位就可以知道你當前小程式的所有頁面路徑:

{
  "pages":[
    "pages/logs/logs"
  ]
}

於是微信客戶端就把首頁的程式碼裝載進來,通過小程式底層的一些機制,就可以渲染出這個首頁。

小程式啟動之後,在 app.js 定義的 App 例項的 onLaunch 回撥會被執行:

App({
  onLaunch: function () {
    // 小程式啟動之後 觸發
  }
})

整個小程式只有一個 App 例項,是全部頁面共享的

你可以觀察到 pages/logs/logs 下其實是包括了4種檔案的,微信客戶端會先根據 logs.json 配置生成一個介面,頂部的顏色和文字你都可以在這個 json 檔案裡邊定義好。緊接著客戶端就會裝載這個頁面的 WXML 結構和 WXSS 樣式。最後客戶端會裝載 logs.js,你可以看到 logs.js 的大體內容就是:

Page({
  data: { // 參與頁面渲染的資料
    logs: []
  },
  onLoad: function () {
    // 頁面渲染後 執行
  }
})

Page 是一個頁面構造器,這個構造器就生成了一個頁面。在生成頁面的時候,小程式框架會把 data 資料和 index.wxml 一起渲染出最終的結構,於是就得到了你看到的小程式的樣子。

在渲染完介面之後,頁面例項就會收到一個 onLoad 的回撥,你可以在這個回撥處理你的邏輯。

開發痛點

依賴管理混亂、工程化流程落後、ES Next 支援不完善、命名規範不統一等。這些問題在現在看來都已經有了各種官方或非官方的解決辦法.

最常見的開發模式就是使用某一套完善的開發框架,他們最主要的區別就是DSL,類Vue或者類React語法為主.

在微信小程式之後,各大廠商紛紛釋出了自己的小程式平臺,多端適配型框架的需求也就應運而生了,(Taro, uni-app等)

所以開發技術選型的主要考慮因素就是: DSL 以及 多端適配

Taro1.x

市面上第一款遵循React語法的多端小程式框架,自研出Nervjs.同時也支援使用 React/Vue/Nerv 等框架來開發

Write once Run anywhere

目前(Taro3.x)官方支援轉換的平臺如下:

  • H5
  • ReactNative
  • 微信小程式
  • 京東小程式
  • 百度小程式
  • 支付寶小程式
  • 位元組跳動小程式
  • QQ 小程式
  • 釘釘小程式
  • 企業微信小程式
  • 支付寶 IOT 小程式
  • 飛書小程式

設計思路

必須滿足下述要求:

  • 程式碼多端複用,不僅能執行在時下最熱門的 H5、微信小程式、React Native,對其他可能會流行的端也留有餘地和可能性。
  • 完善和強大的元件化機制,這是開發複雜應用的基石。
  • 與目前團隊技術棧有機結合,有效提高效率。
  • 學習成本足夠低
  • 背後的生態強大
在一個優秀且嚴格的規範限制下,從更高抽象的視角(語法樹)來看,每個人寫的程式碼都差不多。

也就是說,對於微信小程式這樣不開放不開源的端,我們可以先把 React 程式碼分析成一顆抽象語法樹,根據這顆樹生成小程式支援的模板程式碼,再做一個小程式執行時框架處理事件和生命週期與小程式框架相容,然後把業務程式碼跑在執行時框架就完成了小程式端的適配。

對於 React 已經支援的端,例如 Web、React Native 甚至未來的 React VR,我們只要包一層元件庫再做些許樣式支援即可。鑑於時下小程式的熱度和我們團隊本身的業務側重程度,元件庫的 API 是以小程式為標準,其他端的元件庫的 API 都會和小程式端的元件保持一致。

架構

Taro 當前的架構主要分為:編譯時執行時

Taro 編譯時

使用 babel-parser 將 Taro 程式碼解析成抽象語法樹,然後通過 babel-types 對抽象語法樹進行一系列修改、轉換操作,最後再通過 babel-generate 生成對應的目的碼。

Babel 的編譯過程亦是如此,主要包含三個階段

  1. 解析過程,在這個過程中進行詞法、語法分析,以及語義分析,生成符合 ESTree 標準 虛擬語法樹(AST)
  2. 轉換過程,針對 AST 做出已定義好的操作,babel 的配置檔案 .babelrc 中定義的 preset 、 plugin 就是在這一步中執行並改變 AST 的
  3. 生成過程,將前一步轉換好的 AST 生成目的碼的字串

如果對AST是什麼不瞭解的話可以使用這個網站嘗試一下https://astexplorer.net/


省去整個編譯過程得到的結果程式碼如下

<View className='index'>
  <Button className='add_btn' onClick={this.props.add}>+</Button>
  <Button className='dec_btn' onClick={this.props.dec}>-</Button>
  <Button className='dec_btn' onClick={this.props.asyncAdd}>async</Button>
  <View>{this.props.counter.num}</View>
  <A />
  <Button onClick={this.goto}>走你</Button>
  <Image src={sd} />
</View>
<import src="../../components/A/A.wxml" />
<block>
  <view class="index">
    <button class="add_btn" bindtap="add">+</button>
    <button class="dec_btn" bindtap="dec">-</button>
    <button class="dec_btn" bindtap="asyncAdd">async</button>
    <view>{{counter.num}}</view>
    <template is="A" data="{{...$$A}}"></template>
    <button bindtap="goto">走你</button>
    <image src="{{sd}}" />
  </view>
</block>

因為JSX的豐富自由度不是字串模板可以比擬的,所以當時只能支援大概80%的寫法轉換,剩餘不支援的寫法轉由eslint外掛提醒使用者修改。

在開源的過程中,Taro 支援的 JSX 寫法一直在不斷完善,力求讓開發體驗更加接近於 React,主要包括以下語法支援:

  • 支援 Ref,提供了更加方便的元件和元素定位方式
  • 支援 this.props.children 寫法,方便進行自定義元件傳入子元素
  • 在迴圈體內執行函式和表示式
  • 定義 JSX 作為變數使用
  • 支援複雜的 if-else 語句
  • 在 JSX 屬性中使用複雜表示式
  • 在 style 屬性中使用物件
  • 只有使用到的變數才會作為 state 加入到小程式 data,從而精簡小程式資料

Taro 執行時

我們可以對比一下編譯後的程式碼,可以發現,編譯後的程式碼中,React 的核心 render 方法沒有了。同時程式碼裡增加了 BaseComponentcreateComponent ,它們是 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 的一些核心方法:setStateforceUpdate 等進行了替換和重寫,結合前面編譯後 render 方法被替換,大家不難猜出:Taro 當前架構只是在開發時遵循了 React 的語法,在程式碼編譯之後實際執行時,和 React 並沒有關係

createComponent 主要作用是呼叫 Component() 構建頁面;對接事件、生命週期等;進行 Diff Data 並呼叫 setData 方法更新資料。

跨端相容

僅僅將程式碼按照語法規範轉換之後還遠遠不夠,因為不同端的特有原生能力或元件等限制,所以決定了跨端開發必然有部分程式碼需要開發中實現相容,而為了最大程度彌補不同端的差異,Taro制定了一個統一標準,在不同端依靠它們的語法與能力去實現元件庫與API,同時還要為不同端編寫相應的執行時框架,初始化等等。因為最初的設計來源就是小程式,所以決定直接採用小程式的標準,讓其他端向小程式靠齊。

因為我們有編譯的操作,在書寫程式碼的時候,只需要引入標準元件庫 @tarojs/components 與執行時框架 @tarojs/taro ,程式碼經過編譯之後,會變成對應端所需要的庫。

例如,為了提升開發便利性,我們為 Taro 加入了 Redux 支援,我們的做法就是,在小程式端,我們實現了 @tarojs/redux 這個庫來作為小程式的 Redux 輔助庫,並且以他作為基準庫,它具有和 react-redux 一致的 API,在書寫程式碼的時候,引用的都是 @tarojs/redux ,經過編譯後,在 H5 端會替換成 nerv-reduxNervRedux 輔助庫),在 RN 端會替換成 react-redux。這樣就實現了 Redux 在 Taro 中的多端支援。

小程式元件化

開源之初,由於種種原因,Taro 的微信小程式端元件化採用的是小程式 <template /> 標籤來實現的,利用小程式 <template /> 標籤的特性,將元件 JS 檔案編譯成 JS + WXML 模板,在父元件(頁面)的模板中通過 <template /> 標籤引用子元件的 WXML 模板來進行拼接,從而達到元件化的目的。

實踐證明,Template 模板方案是一個失敗的元件化方案,Taro 開源初期的 Bug 主要來源於此。因為這一方案將 JS 邏輯與模板拆分開了,需要手工來保證 JS 與模板中資料一致,這樣在迴圈元件渲染、元件多重巢狀的情況下,要保證元件正確渲染與 props 正確傳遞的難度非常大,實現的成本也非常高。而且,囿於小程式 <template /> 標籤的缺陷,一些功能(例如自定義元件包含子元素等)無法實現。

所以,在經過艱辛的探索與實踐之後,我們採用了小程式原生元件化來作為 Taro 的小程式端元件化方案,並且通過一些處理,繞開了小程式元件化的諸多限制,為 Taro 的穩定性打下了堅實基礎,並帶來了以下好處:

  • 小程式端元件化更加健壯
  • 儘可能減少由於框架帶來的效能問題
  • 依託官方元件化,方便以後解鎖更多可能

功能更新

1.1

  • 加入了對 百度智慧小程式支付寶小程式 的支援
  • 為每個平臺提供了平臺標識

1.2

  • 位元組跳動(頭條)小程式支援
  • 微信小程式轉 Taro
  • CSS Modules 支援
  • MobX 支援

1.3

  • 支援快應用和 QQ 小程式的開發
  • 全面支援 JSX 語法和 React Hooks
  • 大幅提高 H5 效能和可用性
  • Taro Doctor

技術選型與權衡

總結

當前Taro的特點是:

  • 重編譯,輕執行
  • 編譯程式碼與React無關
  • 直接使用Babel編譯,導致在工程化和外掛方面羸弱

缺點也比較多:

  • 維護困難,每次需要新增一個功能,例如支援解析 Markdown 檔案,就需要直接改動 CLI,不夠靈活
  • 難以共建,CLI 的程式碼非常複雜,而且邏輯分支眾多,讓很多想要一起共建的人難以入手
  • 可擴充套件性偏低,自研的構建系統,設計之初沒有考慮到後續的擴充套件性,導致開發者想要新增自定義的功能無從下手

mpvue2.0(截至2018.8.10已停止維護)

mpvue 是一個使用 Vue.js 開發小程式的前端框架,目前支援 微信小程式、百度智慧小程式,頭條小程式 和 支付寶小程式。 框架基於 Vue.js,修改了的執行時框架 runtime 和程式碼編譯器 compiler 實現,使其可執行在小程式環境中,從而為小程式開發引入了 Vue.js 開發體驗。

Vue.js 小程式版, fork 自 vuejs/vue@2.4.1,保留了 vue runtime 能力,新增了小程式平臺的支援。

所以本章節Vue相關知識都是指Vue2

  • 徹底的元件化開發能力:提高程式碼複用性
  • 完整的 Vue.js 開發體驗
  • 方便的 Vuex 資料管理方案:方便構建複雜應用
  • 快捷的 webpack 構建機制:自定義構建策略、開發階段 hotReload
  • 支援使用 npm 外部依賴
  • 使用 Vue.js 命令列工具 vue-cli 快速初始化專案
  • H5 程式碼轉換編譯成小程式目的碼的能力

框架原理

  • mpvue 保留了 vue.runtime 核心方法,無縫繼承了 Vue.js 的基礎能力
  • mpvue-template-compiler 提供了將 vue 的模板語法轉換到小程式的 wxml 語法的能力
  • 修改了 vue 的建構配置,使之構建出符合小程式專案結構的程式碼格式: json/wxml/wxss/js 檔案

Vue程式碼

  • 將小程式頁面編寫為 Vue.js 實現
  • 以 Vue.js 開發規範實現父子元件關聯

小程式程式碼

  • 以小程式開發規範編寫檢視層模板
  • 配置生命週期函式,關聯資料更新呼叫
  • 將 Vue.js 資料對映為小程式資料模型

並在此基礎上,附加如下機制

  • Vue.js 例項與小程式 Page 例項建立關聯
  • 小程式和 Vue.js 生命週期建立對映關係,能在小程式生命週期中觸發 Vue.js 生命週期
  • 小程式事件建立代理機制,在事件代理函式中觸發與之對應的 Vue.js 元件事件響應

這套機制總結起來非常簡單,但實現卻相當複雜。在揭祕具體實現之前,讀者可能會有這樣一些疑問:

要同時維護 Vue.js 和小程式,是否需要寫兩個版本的程式碼實現?

首先,mpvue 為提高效率而生,本身提供了自動生成小程式程式碼的能力,小程式程式碼根據 Vue.js 程式碼構建得到,並不需要同時開發兩套程式碼。

小程式負責檢視層展現,Vue.js的檢視層是否還需要,如果不需要應該如何處理?

Vue.js 檢視層渲染由 render 方法完成,同時在記憶體中維護著一份虛擬 DOM,mpvue 無需使用 Vue.js 完成檢視層渲染,因此我們改造了 render 方法,禁止檢視層渲染。

生命週期如何打通,資料同步更新如何實現?

生命週期關聯:生命週期和資料同步是 mpvue 框架的靈魂,Vue.js 和小程式的資料彼此隔離,各自有不同的更新機制。mpvue 從生命週期和事件回撥函式切入,在 Vue.js 觸發資料更新時實現資料同步。小程式通過檢視層呈現給使用者、通過事件響應使用者互動,Vue.js 在後臺維護著資料變更和邏輯。可以看到,資料更新發端於小程式,處理自 Vue.js,Vue.js 資料變更後再同步到小程式。為實現資料同步,mpvue 修改了 Vue.js runtime 實現,在 Vue.js 的生命週期中增加了更新小程式資料的邏輯。

事件代理機制:使用者互動觸發的資料更新通過事件代理機制完成。在 Vue.js 程式碼中,事件響應函式對應到元件的 method, Vue.js 自動維護了上下文環境。然而在小程式中並沒有類似的機制,又因為 Vue.js 執行環境中維護著一份實時的虛擬 DOM,這與小程式的檢視層完全對應,我們思考,在小程式元件節點上觸發事件後,只要找到虛擬 DOM 上對應的節點,觸發對應的事件不就完成了麼;另一方面,Vue.js 事件響應如果觸發了資料更新,其生命週期函式更新將自動觸發,在此函式上同步更新小程式資料,資料同步也就實現了。

建構流程

建構流程是整個專案最核心的地方之一,通過我們所熟知的 webpack,完成了 template 轉換為 wxml 和 樣式轉換優化以及其他的若干程式碼的拼接壓縮混淆等操作,最終使之可以執行在微信小程式的環境中。

生命週期

除了 Vue2 本身的生命週期外,mpvue 還相容了小程式生命週期

app 部分:

  • onLaunch,初始化
  • onShow,當小程式啟動,或從後臺進入前臺顯示
  • onHide,當小程式從前臺進入後臺

page 部分:

  • onLoad,監聽頁面載入
  • onShow,監聽頁面顯示
  • onReady,監聽頁面初次渲染完成
  • onHide,監聽頁面隱藏
  • onUnload,監聽頁面解除安裝
  • onPullDownRefresh,監聽使用者下拉動作
  • onReachBottom,頁面上拉觸底事件的處理函式
  • onShareAppMessage,使用者點選右上角分享
  • onPageScroll,頁面滾動
  • onTabItemTap, 當前是 tab 頁時,點選 tab 時觸發 (mpvue 0.0.16 支援)

架構

mpvue 的實現同樣分為:編譯時執行時

在 Vue 原始碼的 platforms 資料夾下面增加了 mp 目錄,在裡面實現了 complier(編譯時)runtime (執行時)支援。

編譯時

相比於Taro將JSX轉成小程式模板,mpvue是將vue模板進行轉換,兩者的相似性可以簡化很多工作程式

<div v-if="condition" :id="`item-${id}`" v-model="value"  @click="tapName"> {{ message }} </div>
-------------------------------------------編譯--------------------------------------------------------
<view wx:if="{{condition}}" id="item-{{id}}" model:value="{{value}}"  bindtap="tapName"> {{ message }} </view>

執行時

Vue2原理圖

vue檔案主要分三部分: template, script, style

模板部分會經由vue-loader進行ast分析,生成render渲染函式

指令碼部分會進行Vue例項化,對data資料進行響應式處理,每次修改都會觸發render函式

render函式會生成虛擬Dom,每次新舊虛擬Dom之間會進行patch對比,得到最終修改結果才去操作真實Dom更新

mpvue 整體原理圖

其中mpvue-template-compiler作用是將Vue2預編譯成render函式以避免執行時編譯開銷和CSP限制, 只有在編寫具有特定需求的構建工具時才需要它。

因為小程式跟Vue原理不同,所以直接移除Vue的dom操作階段,取而代之的是直接交給小程式原生實現

在Vue例項化的時候也會呼叫小程式例項化,每次Vue進行patch之後,會直接呼叫$updateDataToMP()獲取掛載在page例項上資料,然後通過setData方法更新檢視

總結

mpvue屬於半編譯半執行,主要體現在

  • JS執行本質上還是用的是Vue那一套程式碼,除了部分特性因為小程式的限制無法相容(如 :filterslotv-html
  • 模板程式碼需要預先進行編譯成WXML模板

所以兩邊屬於一個割裂狀態,vue負責資料處理,小程式負責渲染檢視,實際上是方便開發但是沒有優化小程式的體驗

Wepy2(已廢棄)

設計思想

  1. 非侵入式設計 WePY 2 執行於小程式之上,是對小程式原有能力的封裝和優化,並不會對原有小程式框架有任何改動或者影響。
  2. 相容原生程式碼 能夠相容原生程式碼,即部分頁面為原生,部分頁面為 WePY。同時做到無需任何改動即可引用現有原生開發的小程式元件。
  3. 基於小程式原生元件實現元件化開發 小程式原生元件在效能上相較之前有了很大的提升。因此 WePY 2 完全基於小程式原生元件實現,不支援小程式基礎庫 < 1.6.3 的版本。
  4. 基於 Vue Observer 實現資料繫結 資料繫結基於 Vue Observer 實現,同時為其定製小程式特有的優化。
  5. 更優的可擴充套件性 對於 core 庫提供 mixin、hooks、use 等方式進行開發擴充套件,對於 cli 編譯工具提供更強大的外掛機制方便開發者可以侵入到編譯的任何一個階段進行個性化定製的編譯。

轉換流程

WePY 繼承了 WXML 的基本模板語法,並支援大部分 Vue 模板語法。

HTML 模板標籤對映表

標籤轉換後
selectpicker
datalistpicker
imgimage
sourceaudio
videovideo
trackvideo
anavigator
spanlabel
其它view

事件處理

小程式原生的事件系統 使用bind,catch 等關鍵字進行事件監聽。 而在 WePY 中,事件沿用了 Vue 的風格,使用 v-on 或者是 @ 操作符進行事件監聽。同時 WePY 中會有一個統一的事件分發器接管原生事件。大致如下圖:

WePY 在編譯過程中,會找到所有監聽事件,併為其分配事件 ID。同時將事件程式碼(可以是一個方法,也可以是一個簡單的程式碼片段)注入至 JS 程式碼中。 然後當事件分發器接收到原生事件時,會通過事件 ID,分發到相應的事件邏輯當中。

這樣做的好處主要是:

  1. 可控性更強。使用者可利用相關鉤子函式從而處理全域性的使用者事件。(典型場景:為頁面全部按鈕統一增加一個點選上報功能)
  2. 靈活度更高。相對於原生只能使用函式名的方式來說,還可使用簡單程式碼片段。(典型場景:@tap="num++")

Taro2.x

更多是對底層架構的革新,提高擴充性,穩定性,可維護性,降低開發和學習成本

CLI

Taro 2.0 的 CLI 將會變得非常輕量,只會做區分編譯平臺、處理不同平臺編譯入參等操作,隨後再呼叫對應平臺的 runner 編譯器 做程式碼編譯操作,而原來大量的 AST 語法操作將會改造成 Webpack Plugin 以及 Loader,交給 Webpack 來處理。

  • 利於維護,大量的邏輯交由 Webpack 來處理,我們只需要維護一些外掛
  • 更加穩定,相較於自研的構建系統,新的構建會更加穩定,降低一些奇怪錯誤的出現概率
  • 可擴充套件性強,可以通過自行加入 Webpack Loader 與 Plugin 的方式做自己想要的擴充套件
  • 各端編譯統一,接入 Webpack 後,Taro 各端的編譯配置可以實現非常大程度的統一

其他

編譯配置調整

非同步程式設計調整

主編譯流程鉤子

編譯新增 Loader

編譯新增 Plugin

Taro RN 依賴升級

Taro3.x

跨框架:React、Nerv、Vue 2、Vue 3、jQuery

更快的執行速度

執行時效能主要分為兩個部分,一是更新效能,二是初始化效能。

對於更新效能而言,舊版本的 Taro 會把開發者 setState 的資料進行一次全量的 diff,最終返回給小程式是按路徑更新的 data。而在 Taro Next 中 diff 的工作交給了開發者使用的框架(React/Nerv/Vue),而框架 diff 之後的資料也會通過 Taro 按路徑去最小化更新。因此開發者可以根據使用框架的特性進行更多更細微的效能優化。

初始化效能則是 Taro Next 的痛點。原生小程式或編譯型框架的初始資料可以直接用於渲染,但 Taro Next 在初始化時會把框架的渲染資料轉化為小程式的渲染資料,多了一次 setData 開銷。

為了解決這個問題,Taro 從服務端渲染受到啟發,在 Taro CLI 將頁面初始化的狀態直接渲染為無狀態的 wxml,在框架和業務邏輯執行之前執行渲染流程。我們將這一技術稱之為預渲染(Prerender),經過 Prerender 的頁面初始渲染速度通常會和原生小程式一致甚至更快。

更快的構建速度和 source-map 支援

作為一個編譯型框架,舊版本的 Taro 會進行大量的 AST 操作,這類操作顯著地拖慢了 Taro CLI 的編譯速度。而在 Taro Next 中不會操作任何開發者程式碼的 AST,因此編譯速度得到了大幅的提高。

正因為 AST 操作的取消,Taro Next 也輕鬆地實現了 source-map 的支援。這對於開發體驗是一個巨大的提升:

其他

跨端:H5、微信、支付寶、百度、位元組跳動...小程式

微信小程式轉 React/Vue

渲染 HTML 字串

CSS-in-JS

虛擬列表(VirtualList)

開放式外掛系統

預渲染

開放式架構

不同於 Taro 1、2 時代的架構,新的架構主要基於執行時,我們都知道使用 React 開發 web,渲染頁面主要依靠的是 react-dom 去操作 DOM 樹,而 React Native 依靠的是 Yoga 佈局引擎,但是我們卻能通過 React 將他們聯絡在一起,這主要是通過抽象的 Virtual DOM 技術來實現的,通過 Virtual DOM 達到跨平臺統一的目的。而小程式中雖然沒有直接暴露 DOM 和 BOM API,但是我們卻可以類比 React 和 React Native 的思想,在小程式中模擬實現 DOM 以及 BOM 的 API,從而實現直接將 React 執行到小程式環境中的目的,這就是 Taro 新架構的由來。

目標是可以通過外掛的形式擴充套件 Taro 的端平臺支援能力:

  • 外掛開發者無需修改 Taro 核心庫程式碼,即可編寫出一個端平臺外掛。
  • 外掛使用者只需安裝、配置端平臺外掛,即可把程式碼編譯到指定平臺。
  • 開發者可以繼承現有的端平臺外掛,然後對平臺的適配邏輯進行自定義。

初 Taro 選擇重編譯時的主要原因是處於效能考慮,畢竟同等條件下,編譯時做的工作越多,也就意味著執行時做的工作越少,效能會更好;另外,重編譯時也保證了 Taro 的程式碼在編譯之後的可讀性。但是從長遠來看,計算機硬體的效能越來越冗餘,如果在犧牲一點可以容忍的效能的情況下換來整個框架更大的靈活性和更好的適配性,我們認為是值得的

Taro實現了一套高效、精簡版的 DOM/BOM API執行時渲染程式碼 @tarojs/runtime

Taro 執行時。在小程式端連線框架(DSL)渲染機制到小程式渲染機制,連線小程式路由和生命週期到框架對應的生命週期。在 H5/RN 端連線小程式生命週期規範到框架生命週期。

不管什麼框架,最終都是轉成瀏覽器可執行的程式碼,用的都是通用的DOM/BOM API,例如createElementappendChildremoveChild

DOM/BOM 注入之後,理論上來說,Nerv/Preact 就可以直接執行了。但是 React 有點特殊,因為 React-DOM 包含大量瀏覽器相容類的程式碼,導致包太大,而這部分程式碼我們是不需要的,因此我們需要做一些定製和優化。

React16+實現

可以看到React大體分三層

  • React核心實現原始碼
  • Diff/Fiber演算法實現原始碼, 負責維護虛擬樹
  • 具體平臺渲染實現原始碼,負責元件,事件,節點渲染等

所以Taro實現了@tarojs/react

基於 react-reconciler 的小程式專用 React 渲染器,連線 @tarojs/runtime 的 DOM 例項,相當於小程式版的 react-dom,暴露的 API 也和 react-dom 保持一致。

其中最重要的的兩個實現功能

  • hostConfig: 關聯對應的BOM/DOM實現API,完成Dom操作功能

  • render: 實際的渲染方法

    function render(element, domContainer, cb) {
        const oldRoot = ContainerMap.get(domContainer);
        if (oldRoot != null) {
            return oldRoot.render(element, cb);
        }
        const root = new Root(TaroReconciler, domContainer);
        ContainerMap.set(domContainer, root);
        return root.render(element, cb);
    }

目前為止已經完成了程式碼執行邏輯,剩下的就是基於元件的template,動態渲染頁面

Vue實現

拋開已實現的BOM/DOM API之後,React和Vue的區別很小,只需要在執行時的CreateVuePage進行一些處理,例如生命週期對齊等,其他部分基本都一致

事件

首先的 Taro Next 事件,具體的實現方式如下:

  1. 在 小程式元件的模版化過程中,將所有事件方法全部指定為 呼叫 ev 函式,如:bindtapbindchangebindsubmit 等。
  2. 在 執行時實現 eventHandler 函式,和 eh 方法繫結,收集所有的小程式事件
  3. 通過 document.getElementById() 方法獲取觸發事件對應的 TaroNode
  4. 通過 createEvent() 建立符合規範的 TaroEvent
  5. 呼叫 TaroNode.dispatchEvent 重新觸發事件

Taro Next 事件本質上是基於 Taro DOM 實現了一套自己的事件機制,這樣做的好處之一是,無論小程式是否支援事件的冒泡與捕獲,Taro 都能支援。

更新

無論是 React 還是 Vue ,最終都會呼叫 Taro DOM 方法,如:appendChildinsertChild 等。

這些方法在修改 Taro DOM Tree 的同時,還會呼叫 enqueueUpdate 方法,這個方法能獲取到每一個 DOM 方法最終修改的節點路徑和值,如:{root.cn.[0].cn.[4].value: "1"},並通過 setData 方法更新到檢視層。

這裡更新的粒度是 DOM 級別,只有最終發生改變的 DOM 才會被更新過去,相對於之前 data 級別的更新會更加精準,效能更好。

新架構特點

新的架構基本解決了之前的遺留問題:

  • 無 DSL 限制:無論是你們團隊是 React 還是 Vue 技術棧,都能夠使用 Taro 開發
  • 模版動態構建:和之前模版通過編譯生成的不同,Taro Next 的模版是固定的,然後基於元件的 template,動態 “遞迴” 渲染整棵 Taro DOM 樹。
  • 新特性無縫支援:由於 Taro Next 本質上是將 React/Vue 執行在小程式上,因此,各種新特性也就無縫支援了。
  • 社群貢獻更簡單:錯誤棧將和 React/Vue 一致,團隊只需要維護核心的 taro-runtime。
  • 基於 Webpack:Taro Next 基於 Webpack 實現了多端的工程化,提供了外掛功能。

效能優化

Taro Next 的新架構變成近乎全執行之後,花了很多精力在效能優化上面。

相比原生小程式,Taro Next 多了紅色部分的帶來的效能隱患,如:引入 React/Vue 帶來的 包的 Size 增加,執行時的損耗、Taro DOM Tree 的構建和更新、DOM data 初始化和更新。

而我們真正能做的,只有綠色部分,也就是:Taro DOM Tree 的構建和更新DOM data 初始化和更新

Update Date

Taro Next 的更新是 DOM 級別的,比 Data 級別的更新更加高效,因為 Data 粒度更新實際上是有冗餘的,並不是所有的 Data 的改變最後都會引起 DOM 的更新

其次,Taro 在更新的時候將 Taro DOM Tree 的 path 進行壓縮,這點也極大的提升了效能。

最終的結果是:在某些業務場景寫,addselect 資料,Taro Next 的效能比原生的還要好。

功能更新

3.1

開放式架構

新增小程式效能優化元件 CustomWrapper

原生小程式漸進式混合使用 Taro 開發

擁抱 React 17、TypeScript 4

3.2

更快編譯速度

source-map 支援

多 React Native 版本支援,擁抱最新版 0.64

更豐富API與元件

API與元件按需引入

3.3

支援使用 H5 標籤

小程式支援使用框架的 DevTools

修復百度小程式 flex 佈局的問題

3.4 beta

支援使用 Preact 進行開發(一款體積超小的類 React 框架,提供和 React 幾乎一致的 API,而體積只有 5k 左右)

Vue 3 支援 Composition API 版本的小程式生命週期鉤子

執行時體積優化

3.5 canary

支援適配鴻蒙

參考引用文章

小程式跨框架開發的探索與實踐

小程式框架全面測評

多端統一開發框架 - Taro

為何我們要用 React 來寫小程式 - Taro 誕生記

微信官方文件

相關文章