凹凸實驗室的 Taro 是遵循 React 語法規範的多端開發方案,Taro 目前已對外開源一段時間,受到了前端開發者的普遍歡迎和關注。截止目前 star 數已經突破11.7k,還在開啓的 Issues 有 200多個,已經關閉700多個,可見使用並參與討論的開發者是很是多的。Taro 目前已經支持微信小程序、H五、RN、支付寶小程序、百度小程序,持續迭代中的 Taro,也正在兼容更多的端以及增長一些新特性的支持。javascript
迴歸正題,本篇文章主要講的是 Taro 深度開發實踐,綜合咱們在實際項目中使用 Taro 的一些經驗和總結,首先會談談 Taro 爲何選擇使用React語法,而後再從Taro項目的代碼組織、數據狀態管理、性能優化以及多端兼容等幾個方面來闡述 Taro 的深度開發實踐體驗。css
這個要從兩個方面來講,一是小程序原生的開發方式不夠友好,或者說不夠工程化,在開發一些大型項目時就會顯得很吃力,主要體如今如下幾點:html
wxs
做爲補充,可是使用體驗仍是很是糟糕原生的開發方式不友好,天然就想要有更高效的替代方案。因此咱們將目光投向了市面上流行的三大前端框架React、Vue、Angular 。Angular在國內的流行程度不高,咱們首先排除了這種語法規範。而類 Vue 的小程序開發框架市面上已經有一些優秀的開源項目,同時咱們部門內的技術棧主要是 React,那麼 React 語法規範 也天然成爲了咱們的第一選擇。除此以外,咱們還有如下幾點的考慮:前端
綜上所述,Taro 最終採用了 React 語法 來做爲本身的語法標準,配合前端工程化的思想,爲小程序開發打造了更加優雅的開發體驗。java
要進行 Taro 的項目開發,首先天然要安裝 taro-cli,具體的安裝方法可參照文檔,這裏不作過多介紹了,默認你已經裝好了 taro-cli 並能運行命令。react
而後咱們用 cli 新建一個項目,獲得的項目模板以下:git
├── dist 編譯結果目錄
├── config 配置目錄
| ├── dev.js 開發時配置
| ├── index.js 默認配置
| └── prod.js 打包時配置
├── src 源碼目錄
| ├── pages 頁面文件目錄
| | ├── index index頁面目錄
| | | ├── index.js index頁面邏輯
| | | └── index.css index頁面樣式
| ├── app.css 項目總通用樣式
| └── app.js 項目入口文件
└── package.json
複製代碼
若是是十分簡單的項目,用這樣的模板即可以知足需求,在 index.js 文件中編寫頁面所須要的邏輯github
假如項目引入了 redux,例如咱們以前開發的項目,目錄則是這樣的:npm
├── dist 編譯結果目錄
├── config 配置目錄
| ├── dev.js 開發時配置
| ├── index.js 默認配置
| └── prod.js 打包時配置
├── src 源碼目錄
| ├── actions redux裏的actions
| ├── asset 圖片等靜態資源
| ├── components 組件文件目錄
| ├── constants 存放常量的地方,例如api、一些配置項
| ├── reducers redux裏的reducers
| ├── store redux裏的store
| ├── utils 存放工具類函數
| ├── pages 頁面文件目錄
| | ├── index index頁面目錄
| | | ├── index.js index頁面邏輯
| | | └── index.css index頁面樣式
| ├── app.css 項目總通用樣式
| └── app.js 項目入口文件
└── package.json
複製代碼
咱們以前開發的一個電商小程序,整個項目大概3萬行代碼,數十個頁面,就是按上述目錄的方式組織代碼的。比較重要的文件夾主要是pages
、components
和actions
。json
pages裏面是各個頁面的入口文件,簡單的頁面就直接一個入口文件能夠了,假若頁面比較複雜那麼入口文件就會做爲組件的聚合文件,redux
的綁定通常也是此頁面裏進行。
組件都放在components裏面。裏面的目錄是這樣的,假若有個coupon
優惠券頁面,在pages
天然先有個coupon
,做爲頁面入口,而後它的組件就會存放在components/coupon
裏面,就是components裏面也會按照頁面分模塊,公共的組件能夠建一個components/public
文件夾,進行復用。
這樣的好處是頁面之間互相獨立,互不影響。因此咱們幾個開發人員,也是按照頁面的維度來進行分工,互不干擾,大大提升了咱們的開發效率。
actions這個文件夾也是比較重要,這裏處理的是拉取數據,數據再處理的邏輯。能夠說,數據處理得好,流動清晰,整個項目就成功了一半,具體能夠看下面***數據狀態管理***的部分。如上,假如是coupon
頁面的actions
,那麼就會放在actions/coupon
裏面,能夠再一次見到,全部的模塊都是以頁面的維度來區分的。
除此以外,asset文件用來存放的靜態資源,如一些icon類的圖片,但建議不要存放太多,畢竟程序包有限制。而constants則是一些存放常量的地方,例如api
域名,配置等等。
項目搭建完畢後,在根目錄下運行命令行 npm run build:weapp
或者 taro build --type weapp --watch
編譯成小程序,而後就能夠打開小程序開發工具進行預覽開發了。編譯成其餘端的話,只需指定 type 便可(如編譯 H5 :taro build --type h5 --watch
)。
使用 Taro 開發項目時,代碼組織好,遵循規範和約定,便成功了一半,至少會讓開發變得更有效率。
上面說到,會用 redux 進行數據狀態管理。
說到 redux,相信你們早已耳熟能詳了。在 Taro 中,它的用法和平時在 React 中的用法大同小異,先創建 store
、reducers
,再編寫 actions
;而後經過@tarojs/redux
,使用Provider
和 connect
,將 store 和 actions 綁定到組件上。基礎的用法你們都懂,下面我給你們介紹下如何更好地使用 redux。
相信你們都遇到過這種時候,接口返回的數據和頁面顯示的數據並非徹底對應的,每每須要再作一層預處理。那麼這個業務邏輯應該在哪裏管理,是組件內部,仍是redux
的流程裏?
舉個例子:
例如上圖的購物車模塊,接口返回的數據是
{
code: 0,
data: {
shopMap: {...}, // 存放購物車裏商品的店鋪信息的map
goods: {...}, // 購物車裏的商品信息
...
}
...
}
複製代碼
對的,購車裏的商品店鋪和商品是放在兩個對象裏面的,但視圖要求它們要顯示在一塊兒。這時候,若是直接將返回的數據存到store
,而後在組件內部render
的時候東拼西湊,將二者信息匹配,再作顯示的話,會顯得組件內部的邏輯十分的混亂,不夠純粹。
因此,我我的比較推薦的作法是,在接口返回數據以後,直接將其處理爲與頁面顯示對應的數據,而後再dispatch
處理後的數據,至關於作了一層攔截,像下面這樣:
const data = result.data // result爲接口返回的數據
const cartData = handleCartData(data) // handleCartData爲處理數據的函數
dispatch({type: 'RECEIVE_CART', payload: cartData}) // dispatch處理事後的函數
...
// handleCartData處理後的數據
{
commoditys: [{
shop: {...}, // 商品店鋪的信息
goods: {...}, // 對應商品信息
}, ...]
}
複製代碼
能夠見到,處理數據的流程在render前被攔截處理了,將對應的商品店鋪和商品放在了一個對象了.
這樣作有以下幾個好處:
handleCartData
函數裏面的邏輯,不用改動組件內部的邏輯。實際上,不僅是後臺數據返回的時候,其它數據結構須要變更的時候均可以作一層數據攔截,攔截的時機也能夠根據業務邏輯調整,重點是要讓組件內部自己不關心數據與視圖是否對應,只專一於內部交互的邏輯,這也很符合 React
自己的初衷,數據驅動視圖。
計算屬性?這不是響應式視圖庫纔會有的麼,其實也不是真正的計算屬性,只是經過一些處理達到模擬的效果而已。由於不少時候咱們使用 redux 就只是根據樣板代碼複製一下,改改組件各自的store
、actions
。實際上,咱們可讓它能夠作更多的事情,例如:
export default connect(({
cart,
}) => ({
couponData: cart.couponData,
commoditys: cart.commoditys,
editSkuData: cart.editSkuData
}), (dispatch) => ({
// ...actions綁定
}))(Cart)
// 組件裏
render () {
const isShowCoupon = this.props.couponData.length !== 0
return isShowCoupon && <Coupon /> } 複製代碼
上面是很普通的一種connect
寫法,而後render
函數根據couponData
裏是否數據來渲染。這時候,咱們能夠把this.props.couponData.length !== 0
這個判斷丟到connect
裏,達成一種computed
的效果,以下:
export default connect(({
cart,
}) => {
const { couponData, commoditys, editSkuData } = cart
const isShowCoupon = couponData.length !== 0
return {
isShowCoupon,
couponData,
commoditys,
editSkuData
}}, (dispatch) => ({
// ...actions綁定
}))(Cart)
// 組件裏
render () {
return this.props.isShowCoupon && <Coupon /> } 複製代碼
能夠見到,在connect
裏定義了isShowCoupon
變量,實現了根據couponData
來進行computed
的效果。
實際上,這也是一種數據攔截處理。除了computed
,還能夠實現其它的功能,具體就由各位看官自由發揮了。
關於數據狀態處理,咱們提到了兩點,主要都是關於 redux 的用法。接下咱們聊一下關於性能優化的。
其實在小程序的開發中,最大可能的會遇到的性能問題,大多數出如今setData
(具體到 Taro 中就是調用 setState
函數)上。這是由小程序的設計機制所致使的,每調用一次 setData
,小程序內部都會將該部分數據在邏輯層(運行環境 JSCore
)進行相似序列化的操做,將數據轉換成字符串形式傳遞給視圖層(運行環境 WebView
),視圖層經過反序列化拿到數據後再進行頁面渲染,這個過程下來有必定性能開銷。
因此關於setState
的使用,有如下幾個原則:
setState
。實際上在 Taro 中 setState 是異步的,而且在編譯過程當中會幫你作了這層優化,例如一個函數裏調用了兩次 setState,最後 Taro 會在下一個事件循環中將二者合併,並剔除重複數據。setState
。這個更有多是由於在定時器等異步操做中使用了 setState,致使後臺態頁面進行了 setState 操做。要解決問題該就在頁面銷燬或是隱藏時進行銷燬定時器操做便可。在咱們開發的一個商品列表頁面中,是須要有無限下拉的功能。
所以會存在一個問題,當加載的商品數據愈來愈多時,就會報錯,invokeWebviewMethod 數據傳輸長度爲 1227297 已經超過最大長度 1048576
。緣由就是咱們上面所說的,小程序在 setData 的時候會將該部分數據在邏輯層與視圖層之間傳遞,當數據量過大時就會超出限制。
爲了解決這個問題,咱們採用了一個大分頁思想的方法。就是在下拉列表中記錄當前分頁,達到 10 頁的時候,就以 10 頁爲分割點,將當前 this.state
裏的 list
取分割點後面的數據,判斷滾動向前滾動就將前面數據 setState 進去,流程圖以下:
能夠見到,咱們先把商品全部的原始數據放在this.allList
中,而後判斷根據頁面的滾動高度,在頁面滾動事件中判斷當前的頁碼。頁碼小於10,取 this.allList.slice 的前十項,大於等於10,則取後十項,最後再調用 this.setState
進行列表渲染。這裏的核心思想就是,把看得見的數據才渲染出來,從而避免數據量過大而致使的報錯。
同時爲了提早渲染,咱們會預設一個500的閾值,使整個渲染切換的流程更加順暢。
儘管 Taro 編譯能夠適配多端,但有些狀況或者有些 API 在不一樣端的表現差別是十分巨大的,這時候 Taro 沒辦法幫咱們適配,須要咱們手動適配。
使用process.env.TARO_ENV
能夠幫助咱們判斷當前的編譯環境,從而作一些特殊處理,目前它的取值有 weapp
、swan
、 alipay
、 h5
、 rn
五個。能夠經過這個變量來書寫對應一些不一樣環境下的代碼,在編譯時會將不屬於當前編譯類型的代碼去掉,只保留當前編譯類型下的代碼,從而達到兼容的目的。例如想在微信小程序和 H5 端分別引用不一樣資源:
if (process.env.TARO_ENV === 'weapp') {
require('path/to/weapp/name')
} else if (process.env.TARO_ENV === 'h5') {
require('path/to/h5/name')
}
複製代碼
咱們知道了這個變量的用法後,就能夠進行一些多端兼容了,下面舉兩個例子來詳細闡述
在小程序中,監聽頁面滾動須要在頁面中的onPageScroll
事件裏進行,而在 H5 中則是須要手動調用window.addEventListener
來進行事件綁定,因此具體的兼容咱們能夠這樣處理:
class Demo extends Component {
constructor() {
super(...arguments)
this.state = {
}
this.pageScrollFn = throttle(this.scrollFn, 200, this)
}
scrollFn = (scrollTop) => {
// do something
}
// 在H5或者其它端中,這個函數會被忽略
onPageScroll (e) {
this.pageScrollFn(e.scrollTop)
}
componentDidMount () {
// 只有編譯爲h5時下面代碼纔會被編譯
if (process.env.TARO_ENV === 'h5') {
window.addEventListener('scroll', this.pageScrollFn)
}
}
}
複製代碼
能夠見到,咱們先定義了頁面滾動時所需執行的函數,同時外面作了一層節流的處理(不瞭解函數節流的能夠看這裏)。而後,在 onPageScroll
函數中,咱們將該函數執行。同時的,在 componentDidMount
中,進行環境判斷,若是是 h5
環境就將其綁定到 window
的滾動事件上。
經過這樣的處理,在小程序中,頁面滾動時就會執行 onPageScroll
函數(在其它端該函數會被忽略);在 h5 端,則直接將滾動事件綁定到window
上。所以咱們就達成小程序,h5端的滾動事件的綁定兼容(其它端的處理也是相似的)。
假如要同時在小程序和 H5 中使用 canvas
,一樣是須要進行一些兼容處理。canvas
在小程序和 H5 中的 API 基本都是一致的,但有幾點不一樣:
因此作兼容處理時就圍繞這兩個點來進行兼容
componentDidMount () {
// 只有編譯爲h5下面代碼纔會被編譯
if (process.env.TARO_ENV === 'h5') {
this.context = document.getElementById('canvas-id').getContext('2d')
// 只有編譯爲小程序下面代碼纔會被編譯
} else if (process.env.TARO_ENV === 'weapp') {
this.context = Taro.createCanvasContext('canvas-id', this.$scope)
}
}
// 繪製的函數
draw () {
// 進行一些繪製操做
// .....
// 兼容小程序端的繪製
typeof this.context.draw === 'function' && this.context.draw(true)
}
render () {
// 同時標記上id和canvas-id
return <Canvas id='canvas-id' canvas-id='canvas-id'/> } 複製代碼
能夠見到,先是在 componentDidMount 生命週期中,分別針對不一樣的端的方法而取得 CanvasContext 上下文,在小程序端是直接經過Taro.createCanvasContext
進行建立,同時須要在第二個參數傳入this.$scope
;在 H5 端則是經過 document.getElementById(id).getContext('2d')
來得到 CanvasContext 上下文。
得到上下文後,繪製的過程是一致的,由於兩端的 API 基本同樣,而只需在繪製到最後時判讀上下文是否有 draw 函數,有的話就執行一遍來兼容小程序端,將其繪製出來。
咱們內部用 Canvas 寫了一個彈幕掛件,正是用這種方法來進行兩端的兼容。
上述兩個具體例子總結起來,就是先根據 Taro 內置的 process.env.TARO_ENV
環境變量來判斷當前環境,而後再對某些端進行單獨適配。所以具體的代碼層級的兼容方式會多種多樣,徹底取決於你的需求,但願上面的例子能對你有所啓發。
本文先談了 Taro 爲何選擇使用React語法,而後再從Taro項目的代碼組織、數據狀態管理、性能優化以及多端兼容這幾個方面來闡述了 Taro 的深度開發實踐體驗。總體而言,都是一些較爲深刻的,偏實踐類的內容,若有什麼觀點或異議,歡迎加入開發交流羣,一塊兒參與討論。