Taro實踐 - 深度開發實踐體驗及總結

前言

凹凸實驗室的 Taro 是遵循 React 語法規範的多端開發方案Taro 目前已對外開源一段時間,受到了前端開發者的普遍歡迎和關注。截止目前 star 數已經突破11.7k,還在開啓的 Issues 有 200多個,已經關閉700多個,可見使用並參與討論的開發者是很是多的。Taro 目前已經支持微信小程序、H五、RN、支付寶小程序、百度小程序,持續迭代中的 Taro,也正在兼容更多的端以及增長一些新特性的支持。javascript

迴歸正題,本篇文章主要講的是 Taro 深度開發實踐,綜合咱們在實際項目中使用 Taro 的一些經驗和總結,首先會談談 Taro 爲何選擇使用React語法,而後再從Taro項目的代碼組織數據狀態管理性能優化以及多端兼容等幾個方面來闡述 Taro 的深度開發實踐體驗。css

爲何選擇使用React語法

這個要從兩個方面來講,一是小程序原生的開發方式不夠友好,或者說不夠工程化,在開發一些大型項目時就會顯得很吃力,主要體如今如下幾點:html

  • 一個小程序頁面或組件,須要同時包含 4 個文件,以致開發一個功能模塊時,須要多個文件間來回切換
  • 沒有自定義文件預處理,沒法直接使用 Sass、Less 以及較新的 ES Next 語法
  • 字符串模板太過孱弱,小程序的字符串模板仿的是 Vue,可是沒有提供 Vue 那麼多的語法糖,當實現一些比較複雜的處理時,寫起來就很是麻煩,雖然提供了 wxs 做爲補充,可是使用體驗仍是很是糟糕
  • 缺少測試套件,沒法編寫測試代碼來保證項目質量,也就不能進行持續集成,自動化打包

原生的開發方式不友好,天然就想要有更高效的替代方案。因此咱們將目光投向了市面上流行的三大前端框架ReactVueAngularAngular在國內的流行程度不高,咱們首先排除了這種語法規範。而類 Vue 的小程序開發框架市面上已經有一些優秀的開源項目,同時咱們部門內的技術棧主要是 React,那麼 React 語法規範 也天然成爲了咱們的第一選擇。除此以外,咱們還有如下幾點的考慮:前端

  • React 一門很是流行的框架,也有廣大的受衆,使用它也能下降小程序開發的學習成本
  • 小程序的數據驅動模板更新的思想與實現機制,與 React 相似
  • React 採用 JSX 做爲自身模板,JSX 相比字符串模板來講更加自由,更天然,更具表現力,不須要依賴字符串模板的各類語法糖,也能完成複雜的處理
  • React 自己有跨端的實現方案 ReactNative,而且很是成熟,社區活躍,對於 Taro 來講有更多的多端開發可能性

綜上所述,Taro 最終採用了 React 語法 來做爲本身的語法標準,配合前端工程化的思想,爲小程序開發打造了更加優雅的開發體驗。java

Taro項目的代碼組織

要進行 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萬行代碼,數十個頁面,就是按上述目錄的方式組織代碼的。比較重要的文件夾主要是pagescomponentsactionsjson

  • 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 中的用法大同小異,先創建 storereducers,再編寫 actions;而後經過@tarojs/redux,使用Providerconnect,將 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 自己的初衷,數據驅動視圖

用Connect實現計算屬性

計算屬性?這不是響應式視圖庫纔會有的麼,其實也不是真正的計算屬性,只是經過一些處理達到模擬的效果而已。由於不少時候咱們使用 redux 就只是根據樣板代碼複製一下,改改組件各自的storeactions。實際上,咱們可讓它能夠作更多的事情,例如:

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 的用法。接下咱們聊一下關於性能優化的。

setState的使用

其實在小程序的開發中,最大可能的會遇到的性能問題,大多數出如今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

使用process.env.TARO_ENV能夠幫助咱們判斷當前的編譯環境,從而作一些特殊處理,目前它的取值有 weappswanalipayh5rn 五個。能夠經過這個變量來書寫對應一些不一樣環境下的代碼,在編譯時會將不屬於當前編譯類型的代碼去掉,只保留當前編譯類型下的代碼,從而達到兼容的目的。例如想在微信小程序和 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端的滾動事件的綁定兼容(其它端的處理也是相似的)。

canvas兼容

假如要同時在小程序和 H5 中使用 canvas,一樣是須要進行一些兼容處理。canvas 在小程序和 H5 中的 API 基本都是一致的,但有幾點不一樣:

  • canvas 上下文的獲取方式不一樣,h5 中是直接從 dom 中獲取;而小程序裏要經過調用 Taro.createCanvasContext 來手動建立
  • 繪製時,小程序裏還需在手動調用 CanvasContext.draw 來進行繪製

因此作兼容處理時就圍繞這兩個點來進行兼容

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 的深度開發實踐體驗。總體而言,都是一些較爲深刻的,偏實踐類的內容,若有什麼觀點或異議,歡迎加入開發交流羣,一塊兒參與討論。

相關文章
相關標籤/搜索