GMTC | 《小程序跨框架開發的探索與實踐》 演講全文

image

前言:隨着小程序開發的熱度上升,小程序開發框架也層出不窮。但目前每一個框架都會綁定一個專屬 DSL,如類 React 或者類 Vue,在一個框架內,開發者沒法根據團隊技術棧自由選擇 DSL,同時也沒法共享框架自己的生態與工具。

本次分享將爲你們介紹 Taro 如何將各類語法的前端框架(React/Vue 等)運行在小程序上,討論一個框架支持多 DSL 的實現探索,使得開發者可使用任意熱門框架/語法/DSL 來編寫小程序應用,同時複用相關生態。css

小程序開發的歷程

2017 年 1 月 9 日凌晨,萬衆期待的微信小程序正式上線。html

在此以前,京東投入一個前端小團隊,通過一個月的封閉式開發,以一週一個版本的速度進行迭代,終於在第一時間發佈了本身的 「京東購物」 小程序,儘管功能和界面如今看起來有些簡陋,但在當時是徹底符合微信小程序「觸手可及,用完即走」的理念。前端

image

固然,隨着整個項目的不斷迭代,如今的 「京東購物」小程序在設計、交互以及功能複雜度已經全面向 APP 端看齊,這裏面的工程化實踐已經由 劉慧敏 老師在 GMTC 全球大前端技術大會(北京站)2019 進行過度享,有興趣的能夠下載 PPT:京東購物小程序工程化之路vue

當時的微信小程序的開發存在一些缺點,好比依賴管理混亂、工程化流程落後、ES Next 支持不完善、命名規範不統一等。這些問題在如今看來都已經有了各類官方或非官方的解決辦法,可是在當時小程序開發的探索階段,這些問題都是一些痛點問題。node

有句話我我的特別喜歡,那就是「當一門語言的能力不足,而用戶的運行環境又不支持其它選擇的時候,這門語言就會淪爲 「編譯目標」 語言」。react

縱觀整個前端的歷史,不管是 CSS 預處理器的大行其道、各類模版的流行,仍是 CoffeeScript 乃至 TypeScript 的誕生,都印證了這個說法,微信小程序這裏也不例外。所以,各類小程序開發框架如百花齊放,層出不窮。webpack

image

這些小程序開發框架最主要的區別是 DSL,這點從 logo 顏色上就能夠看出來,除了滴滴的 Chameleon 是自定義 DSL 外,其他的綠色的 logo 是遵循了 Vue 語法(如 mpvue ),藍色的 logo 是遵循了 React 的語法(如 Taro)。git

在微信小程序以後,各大廠商紛紛發佈了本身的小程序平臺,好比:支付寶、百度、頭條、QQ等,再加上快應用、網易、360、京東等,小程序的賽道愈來愈擁擠,開發人員須要適配的小程序平臺愈來愈多,所以,各大小程序開發框架也紛紛進行了多端適配。github

image

所以,站在這個時間節點反過來回顧整個小程序開發框架的進程,你會發現整個 2018 年乃至 2019 年初,小程序的開發框架主要的區別和重心在於:DSL 以及 多端適配web

Taro 的起源與初心

正所謂「業務孵化技術,技術服務業務」,Taro 的誕生源自於業務需求的增長,當時咱們的團隊須要同時負責:京東購物,TOPLIFE 等業。團隊人力資源捉襟見肘,與此同時,以上的業務都或多或少存在多端的需求,好比 微信小程序、H五、React Native(京東的主流 APP基本都內置了 React Native 渲染引擎),並且能夠預見的是,之後頗有可能須要適配更多的小程序平臺,而每一個端開發一套代碼又不現實,會致使:研發成本上升,代碼維護困難

當時咱們團隊自研了一款 類React 框架:Nervjs, 整個團隊的技術棧所以所有轉向了 React ,而當時市面上又沒有一款遵循 React 語法的小程序框架,所以,咱們開發了 Taro,但願可以使用 React 語法寫小程序的同時,經過「Write once Run anywhere」來實現跨端的。

image

整個 Taro 框架從 2018 年 6 月 7 日開源至今,一致保持着高速迭代,這些迭代主要集中在三個方面:

image

通過團隊 一年多的努力,Taro 獲得了社區的普遍承認,截止 2019年 12 月 18日,Taro 已擁有 22254 Stars 和 250 名 Contributors,社區主動提交的開發案例 150+:taro-user-cases,其中不乏多端案例。

可是儘管如此,Taro 仍是存在一些問題沒法解決,或者說:沒那麼好解決。好比:和 React DSL 強綁定、JSX 適配工做量大、社區貢獻複雜等。這些問題歸根到底,很大一部分是 Taro 的架構問題。

image

所以咱們團隊也一直在等待一次合適的機會,對整個架構進行一次提高,同時修復一些項目快速迭代欠下的技術債。

最主要的是,單純的項目維護迭代已經知足不了咱們團隊躁動的心,咱們渴望藉此機會進行一次技術突破。

小程序跨框架開發的探索

在講 Taro 架構以前,咱們先來回顧一下小程序的架構。

微信小程序主要分爲 邏輯層視圖層,以及在他們之下的原生部分。邏輯層主要負責 JS 運行,視圖層主要負責頁面的渲染,它們之間主要經過 EventData 進行通訊,同時經過 JSBridge 調用原生的 API。這也是以微信小程序爲首的大多數小程序的架構。

image

因爲原生部分對於前端開發者來講就像是一個黑盒,所以,整個架構圖的原生部分能夠省略。同時,咱們咱們對 邏輯層 和 視圖層 也作一下簡化,最後能夠獲得小程序架構圖的極簡版:

image

也就是說,只須要在邏輯層調用對應的 App()/Page() 方法,且在方法裏面處理 data、提供生命週期/事件函數等,同時在視圖層提供對應的模版及樣式供渲染就能運行小程序了。這也是大多數小程序開發框架重點考慮和處理的部分。

Taro 當前架構

Taro 當前的架構主要分爲:編譯時運行時

其中編譯時主要是將 Taro 代碼經過 Babel 轉換成 小程序的代碼,如:JSWXMLWXSSJSON

運行時主要是進行一些:生命週期、事件、data 等部分的處理和對接。

image

Taro 編譯時

有過 Babel 插件開發經驗的應該對一下流程十分熟悉,Taro 的編譯時也是遵循了此流程,使用 babel-parser 將 Taro 代碼解析成抽象語法樹,而後經過 babel-types 對抽象語法樹進行一系列修改、轉換操做,最後再經過 babel-generate 生成對應的目標代碼。

詳情能夠參考:babel-handbook

image

整個編譯時最複雜的部分在於 JSX 編譯。

咱們都知道 JSX 是一個 JavaScript 的語法擴展,它的寫法變幻無窮,十分靈活。這裏咱們是採用 窮舉 的方式對 JSX 可能的寫法進行了一一適配,這一部分工做量很大,實際上 Taro 有大量的 Commit 都是爲了更完善的支持 JSX 的各類寫法

但儘管如此,咱們也不可能徹底覆蓋全部的狀況,所以仍是推薦你們按照官方規範書寫 React 代碼,同時,咱們也提供了豐富的 ESlint 插件來輔助你們書寫規範的代碼。

image

這一塊咱們團隊內部一直有個梗:若是你使用 Taro 開發感受 Bug 少,那說明你的 React 代碼寫得很規範

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 並無關係

image

createComponent 主要做用是調用 Component() 構建頁面;對接事件、生命週期等;進行 Diff Data 並調用 setData 方法更新數據。

總結

所以,整個 Taro 當前架構的特色是:

  • 重編譯時,輕運行時:這從兩邊代碼行數的對比就可見一斑。
  • 編譯後代碼與 React 無關:Taro 只是在開發時遵循了 React 的語法。
  • 直接使用 Babel 進行編譯:這也致使當前 Taro 在工程化和插件方面的羸弱。

image

其它解決方案的架構

小程序開發框架百花齊放,咱們也從社區裏獲得了很多啓發。

接下來咱們來看看 遵循 vue 語法的小程序開發框架的表明:mpvue 是怎樣實現的。

image

看過 Vue 源碼的同窗對上面的文件夾和架構確定熟悉,本質上,mpvue 就是 fork 了一份 vuejs/vue@2.4.1 的代碼,保留了 Vue runtime 能力,同時添加了小程序平臺的支持。

具體在源碼中的表現就是:在 Vue 源碼的 platforms 文件夾下面增長了 mp 目錄,在裏面實現了 complier(編譯時)runtime (運行時)支持。

mpvue 的實現一樣分爲:編譯時運行時

mpvue 編譯時

其中編譯時作的事情和 Taro 很相似:將 Vue SFC 寫法的代碼編譯成 小程序代碼文件(JS、WXML、WXSS、JSON)。

最大的區別是 Taro 將 JSX 編譯成 小程序模版,而 mpvue 是將 Vue 模版編譯成 小程序模版。可是因爲 Vue 模版和 小程序模版的類似性,mpvue 在這一塊的工做量比 Taro 少得多。

image

mpvue 運行時

而 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 節點進行修改。

image

而 mpvue 的運行時,會首先將 patch 階段的 DOM 操做相關方法置空,也就是什麼都不作。其次,在建立 Vue 實例的同時,還會偷偷的調用 Page() 用於生成了小程序的 page 實例。而後 運行時的 patch 階段會直接調用 $updateDataToMp() 方法,這個方法會獲取掛在在 page 實例上維護的數據 ,而後經過 setData 方法更新到視圖層。

mpvue 總體原理圖也就以下:

image

一些總結與思考

所以,和 Taro 重編譯時輕運行時不一樣,mpvue 算是:半編譯時,半運行時。這點從代碼量的對比也能大體反映出來。

mpvue 的 WXML 模版和 Taro 同樣,也是經過代碼編譯獲得的;不一樣於 Taro 運行時和 React 無關,mpvue 本質上仍是將 Vue 運行在了小程序,且實現了 Vue@2.4.1 絕大部分特性(只有極少數特性因爲小程序模版的限制未能實現,如 :filterslotv-html);且整個框架基於 Webpack 實現了較爲完善的工程化。

mage

其餘小程序框架的實現原理和效果上的差別性,也帶來了咱們的一些思考:

  • 編譯時 OR 運行時:當初 Taro 選擇重編譯時的主要緣由是處於性能考慮,畢竟同等條件下,編譯時作的工做越多,也就意味着運行時作的工做越少,性能會更好;另外,重編譯時也保證了 Taro 的代碼在編譯以後的可讀性。可是從長遠來看,計算機硬件的性能愈來愈冗餘,若是在犧牲一點能夠容忍的性能的狀況下換來整個框架更大的靈活性和更好的適配性,咱們認爲是值得的
  • 模版靜態編譯 OR 動態構建:儘管 Taro 和 mpvue 的模版都是經過靜態編譯生成的,可是社區也不乏動態構建的例子,好比:Remax
  • DSL 限制:咱們可否實現一個小程序開發框架,擺脫 DSL 的限制?

新架構 Taro Next 的適配與實現

這一次,咱們站在瀏覽器的角度來思考前端的本質:不管開發這是用的是什麼框架,React 也好,Vue 也罷,最終代碼通過運行以後都是調用了瀏覽器的那幾個 BOM/DOM 的 API ,如:createElementappendChildremoveChild 等。

image

所以,咱們建立了 taro-runtime 的包,而後在這個包中實現了 一套 高效、精簡版的 DOM/BOM API(下面的 UML 圖只是反映了幾個主要的類的結構和關係):

image

而後,咱們經過 Webpack 的 ProvidePlugin 插件,注入到小程序的邏輯層。

image

這樣,在小程序的運行時,就有了 一套高效、精簡版的 DOM/BOM API

React 實現

DOM/BOM 注入以後,理論上來講,Nerv/Preact 就能夠直接運行了。可是 React 有點特殊,由於 React-DOM 包含大量瀏覽器兼容類的代碼,致使包太大,而這部分代碼咱們是不須要的,所以咱們須要作一些定製和優化。

在 React 16+ ,React 的架構以下:

image

最上層是 React 的核心部分 react-core ,中間是 react-reconciler,其的職責是維護 VirtualDOM 樹,內部實現了 Diff/Fiber 算法,決定何時更新、以及要更新什麼。

Renderer 負責具體平臺的渲染工做,它會提供宿主組件、處理事件等等。例如 React-DOM 就是一個渲染器,負責 DOM 節點的渲染和 DOM 事件處理。

所以,咱們實現了 taro-react 包,用來鏈接 react-reconcilertaro-runtime 的 BOM/DOM API:

image

具體的實現主要分爲兩步:

  1. 實現 react-reconcilerhostConfig 配置,即在 hostConfig 的方法中調用對應的 Taro BOM/DOM 的 API。
  2. 實現 render 函數(相似於 ReactDOM.render)方法,能夠當作是建立 Taro DOM Tree 的容器。

image

通過上面的步驟,React 代碼實際上就能夠在小程序的運行時正常運行了,而且會生成 Taro DOM Tree,那麼偌大的 Taro DOM Tree 怎樣更新到頁面呢?

首先,咱們將小程序的全部組件挨個進行模版化處理,從而獲得小程序組件對應的模版,以下圖就是小程序的 view 組件通過模版化處理後的樣子:

image

而後,咱們會:基於組件的 template,動態 「遞歸」 渲染整棵樹

具體流程爲先去遍歷 Taro DOM Tree 根節點的子元素,再根據每一個子元素的類型選擇對應的模板來渲染子元素,而後在每一個模板中咱們又會去遍歷當前元素的子元素,以此把整個節點樹遞歸遍歷出來。

image

整個 Taro Next 的 React 實現流程圖以下:

image

Vue 實現

別看 React 和 Vue 在開發時區別那麼大,其實在實現了 BOM/DOM API 以後,它們之間的區別就很小了。

Vue 和 React 最大的區別就在於運行時的 CreateVuePage 方法,這個方法裏進行了一些運行時的處理,好比:生命週期的對齊。

image

其餘的部分,如經過 BOM/DOM 方法構建、修改 DOM Tree 及渲染原理,都是和 React 一致的。

Flutter 實現

提到 Flutter ,就不得不提 Flutter WebFlutter Web 是在標準瀏覽器 API 之上實現 Flutter 的核心繪圖層,本質上也是最終調用了 BOM/DOM API。所以,理論來講,也是能夠進行適配的,但這一塊咱們並不會投入太多的精力,最終會像快應用同樣交給社區來實現和維護。

image

更多細節

接下來和你們展開聊一下 Taro Next 更多的細節實現,好比:事件、更新、生命週期。

事件

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

  1. 在 小程序組件的模版化過程當中,將全部事件方法所有指定爲 調用 ev 函數,如:bindtapbindchangebindsubmit 等。
  2. 在 運行時實現 eventHandler 函數,和 eh 方法綁定,收集全部的小程序事件
  3. 經過 document.getElementById() 方法獲取觸發事件對應的 TaroNode
  4. 經過 createEvent() 建立符合規範的 TaroEvent
  5. 調用 TaroNode.dispatchEvent 從新觸發事件

image

能夠看到,Taro Next 事件本質上是基於 Taro DOM 實現了一套本身的事件機制,這樣作的好處之一是,不管小程序是否支持事件的冒泡與捕獲,Taro 都能支持。

更新

不管是 React 仍是 Vue ,最終都會調用 Taro DOM 方法,如:appendChildinsertChild 等。

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

image

能夠看到,這裏更新的粒度是 DOM 級別,只有最終發生改變的 DOM 纔會被更新過去,相對於以前 data 級別的更新會更加精準,性能更好。

生命週期

相對與其餘部分大刀闊斧的升級改造,生命週期多是變更最小的部分之一。和以前相似,生命週期的實現是在運行時維護的 App 實例 / Page 實例進行了生命週期方法的一一對應。

const config: PageInstance = {
  onLoad (this: MpInstance, options) {
    //...
  },
  onUnload () {
    //...
  },
  onShow () {
    safeExecute('onShow')
  },
  onHide () {
    safeExecute('onHide')
  },
  onPullDownRefresh () {
    safeExecute('onPullDownRefresh')
  }
  //...
}

新架構特色

和以前的架構不一樣,Taro Next 是 近乎全運行

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

  • 無 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 的流程和原生小程序的流程對比。

image

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

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

Size

首先咱們來看包 Size,下面的表格是 TodoMVC 的例子,在原生、Taro Old、Taro Next 等狀況下的包大小對比,能夠看到,引入 React/Vue 後,包大小在 Gzip 狀況下大概增長了 30k 左右。

image

不過咱們在前面一再強調:和以前模版經過編譯生成的不一樣,Taro Next 的模版是固定的,而後基於組件的 template,動態 「遞歸」 渲染整棵 Taro DOM 樹。也就是說,Taro Next 的 WXML 大小是有上限的

隨着項目的增長,頁面愈來愈多,原生的項目 WXML 體積會不斷增長,而 Taro Next 不會。也就是說,當頁面的數量超過一個臨界點時,Taro Next 的包體積可能會更小。所以,包 Size 的問題不足爲慮。

image

DOM Tree

在 Taro DOM Tree 的構建和更新階段,咱們實現了一套僅實現了高效的、精簡版 DOM/BOM API,並且僅僅實現了必要的。

Github上有一個倉庫 jsdom,基本上是在 Node.js 上實現了一套 Web 標準的 DOM/BOM ,這個倉庫的代碼在壓縮前大概有 2.1M,而 Taro Next 的核心的 DOM/BOM API 代碼才 1000 行不到。

所以,咱們最大限度的保證了 Taro DOM Tree 構建和更新階段的性能。

image

Update Date

在數據更新階段,首先前面有提到過,Taro Next 的更新是 DOM 級別的,比 Data 級別的更新更加高效,由於 Data 粒度更新其實是有冗餘的,並非全部的 Data 的改變最後都會引發 DOM 的更新

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

image

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

taro-benchmark

固然,實驗的數據總歸會有缺陷,最終具體的性能表現,還要靠各類複雜業務場景的檢驗。你們若是對 Taro Next 的性能感興趣的,能夠自行跑一下 taro-benchmark 包,對比一下結果。

咱們也在一直持續的全方位優化 Taro Next 的性能,具體能夠關注 Taro Next 的最新的 Commit 。

總結及展望

Taro 將來規劃

Taro Next 將會在不久以後的 3.0 版本正式發佈,支持使用 React/Vue 開發跨端小程序,而後在會在後續的迭代中拓展至其餘端,並完善對應的生態。

image

Taro 團隊仍是會將支持的重點放在 React/Vue,Flutter 和 Angular 會像快應用同樣,交給社區來適配和維護,快應用就是華爲的 Qiyu8Issacpeng 在幫咱們進行適配,很是感謝他們。

同時,咱們還打造了 「Taro 移動端一站式研發平臺」,將先前積累的多端開發工做流和工程化的方案進行了統一,並內置了數據監控、組件市場以及可視化搭建,當前正處於內測階段。

image

一點思考

  • 業務孵化技術,技術服務業務:這也是整個 Taro 項目從建立到迭代至今最重要的、感覺最深的一點。
  • 自上而下 OR 自下而上:從開發者的角度自上而下看,React/Vue 的代碼書寫方式差別挺大的;然而站在瀏覽器的角度自下而上的看,它們的差異其實沒那麼大,都是調用了 BOM/DOM 那幾個經常使用的 API。若是咱們再往底層一點,站在渲染層的角度,不一樣平臺之間的差別會不會也沒那麼大?好比:Flutter。
  • Learn Once Write AnyWhere & Write Once Run AnyWhere:不少開發者更喜歡 React 提出的Learn Once Write AnyWhere,而咱們 Taro 的口號是 Write Once Run AnyWhere,這一點也常常致使咱們常常被人噴,這裏說一點我本身的想法:Learn Once Write AnyWhere其實本質上對開發者更友好,好比開發者只須要學習 React 技術棧,就能夠開發 Web/移動端 應用,可是對項目就沒那麼友好了,每一個項目都得維護一份代碼;而 Write Once Run AnyWhere 是對開發者沒那麼友好(適配的端越多,適配的成本必然也會水漲船高,對開發者要求也很變高),可是根據咱們的實踐,對項目會更友好,「一套代碼,多端適配」。固然,這裏適配的粒度,並不必定是項目級別的,其實在咱們的具體實踐中,有至關一部分是:業務級甚至是頁面級的

image

寫在最後

正所謂「單絲不成線,獨木不成林」,Taro 發展至今早已不在屬於單一團隊的項目了,而是整個 Taro 開發社區共同的項目。

最後,仍是藉此機會感謝一些社區全部幫助過 Taro 的成長的人,特別是 Taro 的貢獻者們,很是感謝!

image

同時也感謝受邀成爲 TaroUI 核心維護人員的 Garfield550 (小姐姐)、梁音ShaoQian Liu,他們將支撐起 TaroUI 的後續迭代與維護。

固然還有在社區中樂於助人、積極貢獻的 zacksleoJay Fongloveonelonglolipop99波仔糕原罪lentoo白領夏公子YuanQuantourzelingxiaoZhu 等等。

此外,還要感謝一直默默爲 Taro 發展提供寶貴建議的研發團隊:騰訊雲、數字廣東、騰訊CDC、網易嚴選、華爲開源團隊、招聯消費金融等等。

長風破浪會有時,直掛雲帆濟滄海。

歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章:

image

相關文章
相關標籤/搜索