爲什麼咱們要用 React 來寫小程序 - Taro 誕生記

在互聯網不斷髮展的今天,前端程序員們也不斷面臨着新的挑戰,在這個變化無窮、不斷革新本身的領域,每年都有新的美好事物在發生。從去年微信小程序的誕生,到今年的逐漸火熱,以及異軍突起的輕應用、百度小程序等的出現,前端能夠延伸的領域已經愈來愈廣,固然也意味着業務在不斷擴大。這時候,如何經過技術手段來提高開發效率,應對不斷增加的業務,就是一個值得探索的話題。本文將對 Taro 誕生的故事,進行深刻淺出地介紹,記錄下這個忙碌的春夏之交發生的故事。javascript

讓人又愛又恨的微信小程序

2017-1-9 微信小程序(如下簡稱小程序)誕生以來,就伴隨着讚譽與爭議不斷。從發佈上線時的不被大多數人看好,到現在的逐漸火熱,甚至說是如日中天也不爲過,小程序用時間與實踐證實了本身的價值。同時於開發者來講,小程序的生態不斷在完善,許多的坑已被踩平,雖然仍是存在一些使人詬病的問題,但已經足見微信的誠意了。這個時候要是尚未上手把玩太小程序,就顯得很是OUT了。html

小程序對於前端程序員來講應該算得上是福音了,用前端相關的技術,得到絲般順滑的 Native 體驗,前端們又能夠在產品小姐姐面前硬氣一把了。能夠說小程序給前端程序員打開了一扇新的大門,你們都應該感謝微信,可是從開發的角度來講,小程序的開發體驗就很是值得商榷了,不只語法上顯得有些不三不四,並且有些莫名其妙的坑也常常讓人不經意間感嘆一下和諧社會,從市面上層出不窮的小程序開發框架就可見一斑。如下就盤點部分小程序開發的痛點。前端

代碼組織與語法

在小程序中,一個頁面 page 可能擁有 page.jspage.wxsspage.wxmlpage.json 四個文件java

這樣在開發的時候就須要來回進行文件切換,尤爲是在同時開發模板和邏輯的時候,切來切去會顯得尤爲麻煩,影響開發效率,但小程序原生只支持這麼寫,就顯得比較尷尬了。react

而在語法上,小程序的語法能夠說既像 React ,又像 Vue,不能說顯得有點不三不四吧,但在使用上老是感受有些彆扭,對於開發者來講,等於又要學習一套新的語法,提高了學習成本。並且,小程序的模板因爲沒有編輯器插件的支持,書寫的時候也沒有智能提示與 lint 檢查,書寫起來顯得有些麻煩。webpack

命名規範

在小程序中處處可見規範不統一的狀況git

例如組件的屬性,以最簡單的 <button /> 組件爲例,在小程序官方文檔中,該組件的屬性部分截圖以下,你們能夠感覺下程序員

<button /> 組件屬性名既有以中劃線分割多個單詞的狀況 session-form,也有多個單詞連寫的狀況 bindgetphonenumber。固然這也不是最嚴重的,你能夠說事件綁定的規範就是 bind + 事件名 ,而其餘屬性的規範就是中劃線分割單詞,我一度覺得小程序就是這個做爲標準,直到我看到了 <progress /> 組件github

這和說好的不同啊喂!web

一樣的狀況也出如今 頁面組件 的生命週期方法中,頁面 的生命週期方法有 onLoadonReadyonUnload 等,但到了 組件 中則是 createdattachedready 等,這樣規範又不統一了,爲啥 頁面 的生命週期方法是 on+Xxx 的格式,但到了 組件 裏缺不同了呢,有點費解。

開發方式

小程序官方提供了 微信開發工具 做爲開發編譯工具,而對於代碼自己沒有提供一個相似 webpack 的工程化開發工具,來解決開發中的一些問題,因此小程序原生的開發方式顯得不那麼現代化,這也是不少小程序開發框架致力於解決的問題。例如,在小程序開發中

  • 不能使用 npm 管理依賴,在小程序中須要手動把第三方代碼文件下載到本地,而後再 reuqire 進行使用,顯得不那麼優雅
  • 不能使用 Sass 等 CSS 預處理器,因爲沒有預編譯的概念,小程序開發中沒法使用市面上流行的 CSS 預處理器,這樣會使得樣式代碼難以管理
  • 不完整的 ES Next 語法支持,小程序默認只能支持極少一部分 ES6 規範的語法,而 ES 是不斷往前發展的,一些很是優秀的新語法特性就不能使用了
  • 手動的文件處理,像圖片壓縮、代碼壓縮等等的一些文件操做,必須手工來處理,顯得有些繁瑣

以上就是從開發者的角度看到的一些小程序的開發問題,不過縱然有千般困難,咱們總要面對,做爲新時代的前端開發工程師,咱們不能一味忍受問題,要保持技術的頭腦,以技術做爲武器,用技術手段去提高的咱們開發體驗。

突發奇想:我能不能用React來寫小程序

目前前端界言及前端框架,必離不開依然保持着統治地位的 ReactVue,這兩個都是很是優秀的前端 UI 框架,並且在網上也常常能看到兩個框架的粉絲之間熱情交流,碰撞出一些思想火花,顯得社區異常活躍。

而咱們團隊也在去年勇敢地拋棄了歷史包袱,很是榮幸地引入了 React 開發方式,讓咱們團隊丟掉了煤油燈,開始通上了電。並且也研發出了一款優秀的類 React 框架 Nerv ,讓咱們和 React 開發思想結合得更深。

與小程序的開發方式相比,React 明顯顯得更加現代化、規範化,並且 React 天生組件化更適合咱們的業務開發,JSX 也比字符串模板有更強的表現力。那麼這時候咱們就在思考,咱們能不能用 React 來寫小程序?

理性地探索

類比

經過對比體驗 小程序和 React ,咱們仍是能發現二者之間類似的地方

生命週期

小程序的生命週期和 React 的生命週期,在很大程度上是相似的,咱們甚至能找到他們之間的對應關係

app 及頁面的生命週期

小程序 React
onLaunch componentWillMount
onLoad componentWillMount
onReady componentDidMount
onShow 不支持,須要特殊處理
onHide 不支持,須要特殊處理
onUnload componentWillUnmount

能夠看出,對於 app頁面 來講,除了 onShowonHide 兩個方法,其餘方法都能在 React 中找到對應。

數據更新方式

React 中,組件的內部數據是用 state 來進行管理的,而在小程序中組件的內部數據都是用 data 來進行管理,二者具備必定類似性。而同時在 React 中,咱們更新數據使用的是 setState 方法,傳入新的數據或者生成新數據的函數,從而更新相應視圖。在小程序中,則對應的有 setData 方法,傳入新的數據,從而更新視圖。

二者都是以數據驅動視圖的方式進行更新,並且 api 神似。

事件綁定

小程序中綁定事件使用的是 bind + 事件名 的方式,例如點擊事件,小程序中是 bindtap

<view bindtap="handlClick">1</view> 複製代碼

而在 React 裏,則是 on + 事件名 的方式,例如點擊事件, React web 中是 onClick

<View onClick={this.handlClick}>1</View>
複製代碼

雖然看上去不同,但實際上是能夠類比的,咱們只須要在編譯時將 on + 事件名 的形式編譯成 bind + 事件名 的形式就能夠了。

如此看來,二者之間有些類似,用 React 來寫小程序貌似是可行的,但接下來咱們就發現了巨大的差別。

巨大的差別

React 與小程序之間最大的差別就是他們的模板了,在 React 中,是使用 JSX 來做爲組件的模板的,而小程序則與 Vue 同樣,是使用字符串模板的。這樣二者之間就有着巨大的差別了。

JSX

render () {
  return (
    <View className='index'> {this.state.list.map((item, idx) => ( <View key={idx}>{item}</View> ))} <Button onClick={this.goto}>走你</Button> </View>
  )
}
複製代碼

小程序模板

 <view class="index"> <view wx:key={idx} wx:for="{{list}}" wx:for-item="item" wx:for-index="idx">{{item}}</view> <view bindtap="goto">走你</view> </view> 複製代碼

衆所周知,JSX 其實本質上就是 JS,咱們能夠在裏面寫任意的邏輯代碼,這樣一來就比字符串模板的表現力與操做性要強多了,何況,小程序的字符串模板功能比較羸弱,只有一些比較基本的功能。那這樣的話,要如何來實現用 JSX 來寫小程序模板呢。

編譯原理的力量

咱們能夠仔細來分析咱們的需求,咱們指望使用 JSX 來書寫小程序模板,但小程序顯然是不支持執行 JSX 代碼的(要是支持的話,Taro 應該也就不存在了吧),咱們也不能指望微信能給咱們開個後門來跑 JSX。那麼這個時候咱們就想,咱們要是可以將 JSX 編譯成小程序模板就行了。

事實上在咱們平時的開發中,這種編譯的操做處處可見,babel 就是咱們最經常使用的 JS 代碼編譯器,通常瀏覽器是不能支持一些很是新的語法特性的,但咱們又想使用它們,這個時候就能夠藉助 babel 來將咱們的高版本的 ES 代碼,編譯成瀏覽器能夠運行的 ES 代碼。而咱們像要將 JSX編譯成小程序模板,也是一樣的道理。咱們首先來了解一下 Babel 的運行機制。

Babel 做爲一個 代碼編譯器 ,可以將 ES6/7/8 的代碼編譯成 ES5 的代碼,其核心利用的就是計算中很是基礎的編譯原理知識,將輸入語言代碼,經過編譯器執行,輸出目標語言的代碼。編譯原理的通常過程就是,輸入源程序,通過詞法分析、語法分析,構造出語法樹,再通過語義分析,理解程序正確與否,再對語法樹作出須要的操做與優化,最終生成目標代碼。

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

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

爲了更好地理解這些過程,你們能夠利用 Ast Explorer 這個網站接一下本身的代碼,感覺一下每一部分代碼所對應的 AST 結構。

能夠看到,一份源碼通過編譯器解析後,會變成相似以下的結構

{
  type: "Program",
  start: 0,
  end: 78,
  loc: { start, end }
  sourceType: "module",
  body: [
    { type: "VariableDeclaration", ... },
    { type: "VariableDeclaration", ... },
    { type: "FunctionDeclaration", ... },
    { type: "ExpressionStatement", ... }
  ]
  ...
}
複製代碼

其中,body 裏包含的就是咱們示例代碼的語法樹結構,第一個 VariableDeclaration 對應的是 const a = 1,第三個 FunctionDeclaration 對應的則是 function sum (a, b) { },分別就是 JS 中的變量定義與函數定義,每個樹節點裏都會包含許多子節點,這樣就造成了一個樹形結構,更多的節點類型,請參考 babel types

固然咱們在這兒只是簡單介紹下編譯原理與 babel,編譯原理是一門很是深奧的課程, babel 也是一個很是優秀的工具,但願在後續的文章中能和你們再詳細探討這一部份內容。

再次回到咱們的需求,將 JSX 編譯成小程序模板,很是幸運的是 babel 的核心編譯器 babylon 是支持對 JSX 語法的解析的,咱們能夠直接利用它來幫咱們構造 AST,而咱們須要專一的核心就是如何對 AST 進行轉換操做,得出咱們須要的新 AST,再將新 AST 進行遞歸遍歷,生成小程序的模板。

JSX 代碼

<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 則是真正的 JS 代碼擴展,其語法之豐富,顯然不是字符串模板所能比,在這一步中,咱們要作的操做,包括但不只限於以下

  • 理解三目運算符與邏輯表達式,例如三目運算符 abc ? : <View>1</View> : <View>2</View> 須要編譯成 <view wx:if="{{abc}}">1</view><view wx:else>2</view>
  • 理解數組 map 語法,例如 map 的使用 abc.map(item => <View>item</View>) 須要編譯成 <view wx:for="{{abc}}" wx:for-item="item">item</view>
  • 等等

以上僅僅是咱們轉換規則的冰山一角,JSX 的寫法極其靈活多變,咱們只能經過窮舉的方式,將經常使用的、React 官方推薦的寫法做爲轉換規則加以支持,而一些比較生僻的,或者是不那麼推薦的寫的寫法則不作支持,轉而以 eslint 插件的方式,提示用戶進行修改。目前咱們支持的 JSX 轉換規則,大體能覆蓋到 JSX 80% 的寫法操做。

關於 JSX 轉小程序模板這一部分,咱們將在後續的技術原理分析系列文章中,詳細爲你們介紹。

還能不能幹點別的

通過咱們一次次的探索,以及一波波猛如虎的操做,咱們已經能夠將類 React 代碼轉成小程序能夠跑的代碼了,也就是說咱們已經能夠正式以 React 的方式來寫小程序的代碼了。喜大普奔!可是咱們激動之餘,冷靜下來繼續思考,咱們還能不能幹點別的有意思的事情呢。

分析一下需求

咱們發現,在日常的工做中,咱們業務一般有一些多端的需求,就是要求小程序要有,H5 要有,甚至 RN 也能有就最好了,我猜產品經理還看不上快應用,否則確定要求咱們快應用也上一套吧,反正大家不是常常號稱代碼優秀、高度可複用麼。這個時候,你就會發現,差很少的界面和邏輯,你可能須要重複寫上好幾輪,這時候要是有個多端代碼生成工具就行了,只寫一份代碼,能夠多端運行。Write once, run anywhere,相信是全部工程師的夢想。

依然編譯原理的力量

這時候咱們回憶一下前文的內容,將一份代碼編譯成多端代碼,這不正是編譯原理乾的事麼,咱們能夠輸入一份源代碼,針對不一樣的端設定好對應的轉換規則,再一鍵轉換出對應端的代碼。並且因爲咱們已經遵循 React 語法了,那咱們再轉成 H5 端(使用 Nerv)與 RN 端(使用 React)也就有了自然的優點。

設計思路補完

可是仔細思考咱們又會發現,僅僅將代碼按照對應語法規則轉換過去後,還遠遠不夠,由於不一樣端會有本身的原生組件,端能力 API 等等,代碼直接轉換過去後,可能不能直接執行。例如,小程序中普通的容器組件用的是 <view />,而在 H5 中則是 <div />;小程序中提供了豐富的端能力 API,例如網絡請求、文件下載、數據緩存等,而在 H5 中對應功能的 API 則不一致。

因此,爲了彌補不一樣端的差別,咱們須要訂製好一個統一的組件庫標準,以及統一的 API 標準,在不一樣的端依靠它們的語法與能力去實現這個組件庫與 API,同時還要爲不一樣的端編寫相應的運行時框架,負責初始化等等操做。經過以上這些操做,咱們就能實現一份一鍵生成多端的需求了。在 Taro 最初的設計中,咱們組件庫與 API 的標準就是源自小程序的,由於咱們以爲既然已經有定義好的組件庫與 API 標準,那爲啥不直接拿來使用呢,這樣不只省去了定製標準的左思右想,同時也省去了爲小程序開發組件庫與 API 的麻煩,只須要讓其餘端來向小程序靠齊就好。

可能有些人會有疑問,既然是爲不一樣的端實現了對應的組件庫與端能力 API (小程序除外,由於組件庫和 API 的標準都是源自小程序),那麼是怎麼可以只寫一份代碼就夠了呢?由於咱們有編譯的操做,在書寫代碼的時候,只須要引入標準組件庫 @tarojs/components 與運行時框架 @tarojs/taro ,代碼通過編譯以後,會變成對應端所須要的庫。

既然組件庫以及端能力都是依靠不一樣的端作不一樣實現來抹平差別,那麼一樣的,若是咱們想爲 Taro 引入更多的功能支持的話,有時候也須要按照這個套路來。例如,爲了提高開發便利性,咱們爲 Taro 加入了 Redux 支持,咱們的作法就是,在小程序端,咱們實現了 @tarojs/redux 這個庫來做爲小程序的 Redux 輔助庫,而且以他做爲基準庫,它具備和 react-redux 一致的 API,在書寫代碼的時候,引用的都是 @tarojs/redux ,通過編譯後,在 H5 端會替換成 nerv-reduxNervRedux 輔助庫),在 RN 端會替換成 react-redux。這樣就實現了 Redux 在 Taro 中的多端支持。

以上就是 Taro 的總體設計思路,裏面還有不少細節沒有展開去闡述,可能你們會以爲有些意猶未盡,後續咱們將會產出一系列的文章來闡述 Taro 的技術細節,例如 《Taro 開發工具原理分析》、《Taro 代碼編譯的背後》、《深刻淺出 JSX 轉小程序模板》等等。

最後的最後

Taro 從立項之初到如今已經差很少有了三個月左右的時間,從最初的激烈討論方案,各類思想的碰撞,到方案逐漸成型,進入火熱的開發迭代,再到如今的小程序端和 H5 端順利支持,從而決定走向開源。這一路走來,收穫頗豐,既有跟團隊小夥伴一塊兒創造的激動,也有無數個日夜加班的苦思。Taro 是凹凸實驗室的誠意之做,咱們也將會一直維護下去,但願 Taro 能愈來愈好,幫助更多人創造更多價值。

項目官網:taro.aotu.io/

項目 GitHub:github.com/NervJS/taro

相關文章
相關標籤/搜索