Taro實踐 - TOPLIFE小程式 開發體驗

凹凸實驗室發表於2018-07-03

來自團隊支援 TOPLIFE小程式 業務的小夥伴,關於 Taro 的一篇使用感受,希望對大家有所幫助。

前言

前陣子,來自我們凹凸實驗室的遵循 React 語法規範的多端開發方案 - Taro 終於對外開源了,歡迎圍觀star(先打波廣告)。作為第一批使用了 Taro 開發的TOPLIFE小程式的開發人員之一,自然是走了不少彎路,躺了不少坑,也幫忙找過不少bug。現在專案總算是上線了,那麼,也是時候給大家總結分享下了。

與WePY比較

當初開發TOPLIFE第一期的時候,用的其實是WePY(那時Taro還沒有開發完成),然後在第二期才全面轉換為用 Taro 開發。作為兩個小程式開發框架都使用過,並應用在生產環境裡的人,自然是要比較一下兩者的異同點。

相同點

  • 元件化開發
  • npm包支援
  • ES6+特性支援,PromiseAsync Functions
  • CSS預編譯器支援,Sass/Stylus/PostCSS等
  • 支援使用Redux進行狀態管理
  • …..

相同的地方也不用多說什麼,都2018年了,這些特性的支援都是為了讓小程式開發變得更現代,更工程化,重點是區別之處。

不同點

  • 開發風格
  • 實現原理
  • WePY支援slot,Taro暫不支援直接渲染children

開發風格

最大的不同之處,自然就是開發風格上的差異,WePY使用的是類Vue開發風格, Taro 使用的是類 React 開發風格,可以說開發體驗上還是會有較大的區別。貼一下官方的demo簡單闡述下。

WePY demo

<style lang="less">
    @color: #4D926F;
    .userinfo {
        color: @color;
    }
</style>
<template lang="pug">
    view(class='container')
        view(class='userinfo' @tap='tap')
            mycom(:prop.sync='myprop' @fn.user='myevent')
            text {{now}}
</template>

<script>
    import wepy from 'wepy';
    import mycom from '../components/mycom';

    export default class Index extends wepy.page {
        
        components = { mycom };
        data = {
            myprop: {}
        };
        computed = {
            now () { return new Date().getTime(); }
        };
        async onLoad() {
            await sleep(3);
            console.log('Hello World');
        }
        sleep(time) {
            return new Promise((resolve, reject) => setTimeout(resolve, time * 1000));
        }
    }
</script>
複製程式碼

Taro demo

import Taro, { Component } from '@tarojs/taro'
import { View, Button } from '@tarojs/components'

export default class Index extends Component {
  constructor () {
    super(...arguments)
    this.state = {
      title: '首頁',
      list: [1, 2, 3]
    }
  }

  componentWillMount () {}

  componentDidMount () {}

  componentWillUpdate (nextProps, nextState) {}

  componentDidUpdate (prevProps, prevState) {}

  shouldComponentUpdate (nextProps, nextState) {
    return true
  }

  add = (e) => {
    // dosth
  }

  render () {
    return (
      <View className='index'>
        <View className='title'>{this.state.title}</View>
        <View className='content'>
          {this.state.list.map(item => {
            return (
              <View className='item'>{item}</View>
            )
          })}
          <Button className='add' onClick={this.add}>新增</Button>
        </View>
      </View>
    )
  }
}
複製程式碼

可以見到在 WePY 裡,csstemplatescript都放在一個wpy檔案裡,template還支援多種模板引擎語法,然後支援computedwatcher等屬性,這些都是典型的Vue風格。

而在 Taro 裡,就是徹頭徹尾的 React 風格,包括constructorcomponentWillMountcomponentDidMount等各種 React 的生命週期函式,還有return裡返回的jsx,熟悉 React 的人上手起來可以說是非常快了。

除此之外還有一些細微的差異之處:

  • WePY 裡的模板,或者說是wxml,用的都是小程式裡原生的元件,就是小程式文件裡的各種元件;而Taro裡使用的每個元件,都需要從@tarojs/components裡引入,包括ViewText等基礎元件(這種做其實是為了轉換多端做準備)
  • 事件處理上
    • Taro 中,是用click事件代替tap事件
    • WePY使用的是簡寫的寫法@+事件;而Taro則是on+事件名稱
    • 阻止冒泡上WePY用的是@+事件.stop;而Taro則是要顯式地使用e.stopPropagation()來阻止冒泡
    • 事件傳參WePY可以直接在函式後面傳參,如@tap="click({{index}})";而Taro則是使用bind傳參,如onClick={this.handleClick.bind(null, params)}
  • WePY使用的是小程式原生的生命週期,並且元件有pagecomponent的區分;Taro 則是自己實現了類似React 的生命週期,而且沒有pagecomponent的區分,都是component

總的來說,畢竟是兩種不同的開發風格,自然還是會有許多大大小小的差異。在這裡與當前很流行的小程式開發框架之一WePY進行簡單對比,主要還是為了方便大家更快速地瞭解 Taro ,從而選擇更適合自己的開發方式。

實踐體驗

Taro 官方提供的 demo 是很簡單的,主要是為了讓大家快速上手,入門。那麼,當我們要開發偏大型的專案時,應該如何使用 Taro 使得開發體驗更好,開發效率更高?作為深度參與TOPLIFE小程式開發的人員之一,談一談我的一些實踐體驗及心得

如何組織程式碼

使用taro-cli生成模板是這樣的

├── dist                   編譯結果目錄
├── config                 配置目錄
|   ├── dev.js             開發時配置
|   ├── index.js           預設配置
|   └── prod.js            打包時配置
├── src                    原始碼目錄
|   ├── pages              頁面檔案目錄
|   |   ├── index          index頁面目錄
|   |   |   ├── index.js   index頁面邏輯
|   |   |   └── index.css  index頁面樣式
|   ├── app.css            專案總通用樣式
|   └── app.js             專案入口檔案
└── package.json
複製程式碼

假如引入了redux,例如我們的專案,目錄是這樣的

├── 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
複製程式碼

TOPLIFE小程式整個專案大概3萬行程式碼,數十個頁面,就是按上述目錄的方式組織程式碼的。比較重要的資料夾主要是pagescomponentsactions

  • pages裡面是各個頁面的入口檔案,簡單的頁面就直接一個入口檔案可以了,倘若頁面比較複雜那麼入口檔案就會作為元件的聚合檔案,redux的繫結一般也是在這裡進行。

  • 元件都放在components裡面。裡面的目錄是這樣的,假如有個coupon優惠券頁面,在pages自然先有個coupon,作為頁面入口,然後它的元件就會存放在components/coupon裡面,就是components裡面也會按照頁面分模組,公共的元件可以建一個components/public資料夾,進行復用。

    這樣的好處是頁面之間互相獨立互不影響。所以我們幾個開發人員,也是按照頁面的維度來進行分工,互不干擾,大大提高了我們的開發效率。

  • actions這個資料夾也是比較重要,這裡處理的是拉取資料,資料再處理的邏輯。可以說,資料處理得好,流動清晰,整個專案就成功了一半,具體可以看下面***更好地使用redux***的部分。如上,假如是coupon頁面的actions,那麼就會放在actions/coupon裡面,可以再一次見到,所有的模組都是以頁面的維度來區分的。

除此之外,asset檔案用來存放的靜態資源,如一些icon類的圖片,但建議不要存放太多,畢竟程式包有限制。而constants則是一些存放常量的地方,例如api域名,配置等等。

只要按照上述或類似的程式碼組織方式,遵循規範和約定,開發大型專案時不說能提高多少效率,至少順手了很多。

更好地使用redux

redux大家應該都不陌生,一種狀態管理的庫,通常會搭配一些中介軟體使用。我們的專案主要是用了redux-thunkredux-logger中介軟體,一個用於處理非同步請求,一個用於除錯,追蹤actions

資料預處理

相信大家都遇到過這種時候,介面返回的資料和頁面顯示的資料並不是完全對應的,往往需要再做一層預處理。那麼這個業務邏輯應該在哪裡管理,是元件內部,還是redux的流程裡?

舉個例子:

mage-20180612151609

例如上圖的購物車模組,介面返回的資料是

{
	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可以做更多的事情

connect大家都知道是用來連線storeactions和元件的,很多時候就只是根據樣板程式碼複製一下,改改元件各自的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,還可以實現其它的功能,具體就由各位看官自由發揮了。

專案感受

要說最大的感受,就是在開發的過程中,有時會忘記了自己在寫小程式,還以為是在寫React頁面。是的,有次我想給頁面繫結一個滾動事件,才醒悟根本就沒有doucment.body.addEventListener這種東西。在使用WePY過程中,那些奇奇怪怪的語法還是時常提醒著我這是小程式,不是h5頁面,而在用Taro的時候,這個差異化已經被消磨得很少了。儘管還是有一定的限制,但我基本上就是用開發React的習慣來使用Taro,可以說極大地提高了我的開發體驗。

一些需要注意的地方

Taro,或者是小程式開發,有沒有什麼要注意的地方?當然有,走過的彎路可以說是非常多了。

頁面棧只有10層
  • 估計是每個頁面的資料在小程式內部都有快取,所以做了10層的限制。帶來的問題就是假如頁面存在迴圈跳轉,即A頁面可以跳到B頁面,B頁面也可以跳到A頁面,然後使用者從A進入了B,想返回A的時候,往往是直接在B頁面裡點選跳轉到A,而不是點返回回到A,如此一來,10層很快就突破了。所以我們自己對navigateTo函式做了一層封裝,防止溢位。
頁面內容有快取
  • 上面說到,頁面內容有快取。所以假如某個頁面是根據不同的資料渲染檢視,新渲染時會有上一次渲染的快取,導致頁面看起來有個閃爍的變化,使用者體驗非常不好。其實解決的辦法也很簡單,每次在componentWillUnmount生命週期中清理一下當前頁面的資料就好了。小程式說到底不是h5,不會說每次進入頁面就會重新整理,也不會離開就銷燬,重新整理,清理資料的動作都需要自己再生命週期函式裡主動觸發。
不能隨時地監聽頁面滾動事件
  • 頁面的滾動事件只能通過onPageScroll來監聽,所以當我想在元件裡進監聽操作時,要將該部分的邏輯提前到onPageScroll函式,提高了抽象成本。例如我需要開發一個滾動到某個位置就吸頂的tab,本來可以在tab內部處理的邏輯被提前了,減少了其可複用性。
Taro開發需要注意的地方
  • 本來也想詳細描述下的,不過在我們幾位大佬的努力,加班加點下,已經開發出eslint外掛,及補充完整了 Taro 文件。大家只要遵循eslint外掛規範,檢視文件,應該不會有太大問題,有問題歡迎提issue

總結

總的來說,用 Taro 來開發小程式體驗還是很不錯的,最重要的是,可以使用jsx寫小程式了!!!作為React粉的一員,可以說是相當的興奮了~

最後,歡迎關注 github.com/nervjs/taro

相關文章