Taro-library:Taro + Redux + 本地 Mock Server 示例專案

Segami發表於2019-03-28

專案簡介

專案地址:imageslr/taro-library

本專案是線上借書平臺小程式使用 Taro 重構後的版本,僅包含三個示例頁面,非常簡單。面向人群主要是 Taro/React/Redux 的初學者,目的是提供一個簡單的實踐專案,幫助理解 Taro 與 Redux 的配合方式與 Taro 的基本使用。本專案還提供了一個快速搭建本地 mock 服務的解決方案。

因為我也是剛接觸 Taro/React,所以只是分享一些開發經驗,繞開一些小坑。如果覺得不錯的話,請點右上角“⭐️Star”支援一下我,謝謝!如果有問題,歡迎提 issue;如果有任何改進,也歡迎 PR。

掃碼體驗:

code

技術棧

Taro + Taro UI + Redux + Webpack + ES6 + Mock

專案截圖

UI

執行專案

本專案在以下環境中編譯通過:taro v1.2.20、nodejs v8.11.2、gulp v3.9.1、微信開發者工具最新版

$ git clone https://github.com/imageslr/taro-library.git

$ cd taro-library

$ npm install 或者 yarn

$ npm run dev:weapp

// 新建一個終端,在專案根目錄下執行
$ gulp mock
複製程式碼

開始學習

Taro 簡介

Taro 是一個遵循 React 語法規範的多端開發解決方案。最近想學習 React,於是就想到使用 Taro 重構很早之前開發的線上借書平臺小程式。雖然 Taro 上手有一定難度,但是其 React 框架比小程式原生更為靈活與規範,給我帶來了非凡的開發體驗。

在正式開始之前,您必須對 Taro 框架、 React 語法與小程式框架有一定的瞭解。此外,我建議您閱讀以下文件,會更容易上手:

  • Taro 官方文件:必讀,開發時也會隨時查閱
  • Taro UI 官方文件:推薦,本專案使用 Taro UI 作為 UI 元件庫
  • React 官方文件:必讀,掌握 React 語法的必經之路,讀完 MAIN CONCEPTS 部分就差不多了。對應的中文文件在這裡,與英文版略有區別
  • Redux 文件:推薦,Redux 是最經常與 React 搭配使用的狀態管理庫。不過這個文件過於詳實,讀起來比較費勁,推薦你掌握 Redux 三大概念(Action、Reducer、Store)後直接在實踐中體會 Redux 的原理與作用
  • React.js 小書:推薦,一步步從零構建 React 與 Redux,非常好的入門教程
  • Mock.js 文件:推薦,速查模擬資料佔位符與模板

開發工具

開發工具:VS Code
程式碼規範:Prettier 外掛 + ES Lint 外掛

VS Code 對 JSX 與 TypeScript 有天然的支援,使用 VS Code 開發 Taro,不需要配置任何外掛就能實現 Taro 元件的自動 import 與 props 提示,非常方便。

程式碼格式化外掛我選擇 Prettier,它遮蔽了很多配置項,強制遵循約定的規範。與之類似的格式化外掛還有 Beautify,不過我更喜歡 Prettier 對 JSX 屬性強制自動換行的風格。

ES Lint 是 JavaScript 與 JSX 的靜態檢測工具,安裝 ES Lint 外掛後在程式碼編寫階段就可以檢測到不易發現的錯誤(如為常量賦值、變數未使用、變數未定義等等)。Taro 已經定義了一套 ES Lint 規則集,使用 taro-cli 生成的 Taro 專案基本不需要再作額外配置。

樣式規範

CSS 前處理器

Taro UI 定義了很多變數可複用的 mixins。為了與 Taro UI 樣式風格保持一致,本專案採用 Taro UI 所使用的 Sass 作為 CSS 前處理器。

佈局

優先使用 Flex 佈局。學習 Flex 佈局可以參考這兩篇文章:

Taro UI 封裝了一些常用的 Flex 樣式類,包括:

  • 1~12 的柵格化長度類at-col-1at-col-2
  • 柵格化偏移類at-col__offset-1
  • flex屬性:超出換行at-row--wrap,寬度根據內容撐開at-col--auto
  • 對齊方式、排列方式

不過 Taro UI 並沒有為flex: none;提供樣式類。

BEM 命名規範

關於 BEM,網上有很多的教程,就不再細說了。Block__Element--Modifier的命名方式在 Sass 中很容易描述:

.block {
  //...
  &__element {
    //...
    &--modifier {
      //...
    }
  }
}
複製程式碼

元件樣式

對於/components目錄下的可複用元件,使用my作為名稱空間,避免被全域性樣式汙染,比如my-panelmy-search-bar等。

元件可以使用externalClasses定義若干個外部樣式類,或者開啟options.addGlobalClass以使用全域性樣式。見Taro 文件 - 元件的外部樣式和全域性樣式

如果希望能夠在元件的props中直接傳遞className或者style,比如這樣:

// index.jsx
<MyComponent className='custom-class' style={/* ... */}>
複製程式碼

Taro 預設並不支援這一寫法。我們可以將classNamecustomStyle作為元件的props,然後在render()中手動將這兩個props新增到根元素上:

// my-component.jsx
export default MyComponent extends Component {
  static options = {
    addGlobalClass: true
  }

  static defaultProps = {
    className: '',
    customStyle: {}
  }

  render () {
    const { className, customStyle } = this.props
    return <View
      className={'my-class ' + className}
      style={customStyle}
    >
      元件內容
    </View>
  }
}
複製程式碼

尺寸單位

Taro 文件 - 設計稿及尺寸單位

Taro 的尺寸單位是px,預設的尺寸稿是 iPhone 6 750px。Taro 會 1:1 地將px轉為小程式的rpx。而在小程式中,pxrpx是 1:2 的關係。如果希望字型採用瀏覽器的預設大小14px,那麼應該這麼寫:

  • Taro:28px
  • Taro:14PX
  • Taro JSX 行內樣式:Taro.pxTransform(14)
  • 小程式原生:28rpx

Taro 會將有大寫字母的PxPX忽略,但是 VS Code 在使用 Prettier 外掛時會自動將PxPX轉為px。對於這個問題,有兩種解決方案:

  • 換用 Beautify 外掛
  • 在包含大寫字母的屬性的前一行新增/* prettier-ignore */
    /* prettier-ignore */
    $input-padding: 25PX;
    複製程式碼

專案初始化

$ taro init taro-library
> ...
> ? 請輸入專案介紹! Taro圖書小程式
> ? 是否需要使用 TypeScript ? No
> ? 請選擇 CSS 前處理器(Sass/Less/Stylus) Sass
> ? 請選擇模板 Redux 模板
>
> ✔ 建立專案: taro-library
複製程式碼

安裝專案依賴:

$ npm install taro-ui && npm install json-server mockjs gulp gulp-nodemon browser-sync --save-dev
複製程式碼

引入 Redux

Redux 檔案設定

在初始化的時候,我們選擇了 Redux 模板。開啟資料夾,可以看到 Taro 建立了一個示例頁面,redux 相關的資料夾為:

├── actions
│   └── counter.js
├── constants
│   └── counter.js
├── reducers
│   ├── counter.js
│   └── index.js
└── store
    └── index.js
複製程式碼

這種方式是按照 Redux 的組成部分來劃分的,/constantsaction-type字串的宣告檔案,不同資料夾中的同名檔案對應同一份資料。

另一種劃分方式是將同一份資料的所有檔案組合在同一個資料夾裡:

└── store
    ├── counter
    │   ├── action-type.js // 對應/constants/counter.js
    │   ├── action.js // 對應/actions/counter.js
    │   └── reducer.js // 對應/reducers/counter.js
    ├── home
    │   ├── action-type.js
    │   ├── action.js
    │   └── reducer.js
    ├── index.js // 對應/store/index.js
    └── rootReducer.js // 對應/reducer/index.js
複製程式碼

本專案採用第二種方式管理 Redux 資料。Taro 生成的 Redux 模板中已經新增了redux-logger中介軟體實現日誌列印功能。

程式碼見 dev-redux-init 分支

connect 方法

推薦先閱讀 Redux 文件

使用 Redux 之後,我們可以將資料儲存在store中,通過action運算元據。那麼怎麼在元件中訪問與運算元據呢?react-redux提供了connect方法,允許我們將store中的資料與action作為props繫結到元件上。

從原理上來講,connect方法返回的是一個高階元件。這個高階元件會對原元件進行包裝,然後返回新的元件。不過我們這裡不講connect的細節,只講它的使用方法。有關connect方法與 Redux 的原理,推薦閱讀 React.js 小書

引數

connect接收四個引數,分別是mapStateToPropsmapDispatchToPropsmergePropsoptions。本專案只用到了前兩個引數。

mapStateToProps

mapStateToProps是一個函式,它將store中的資料對映到元件的props上。mapStateToProps接收兩個引數:stateownProps。第一個引數就是 Redux 的store,第二個資料是元件自己的props

舉個例子:

const mapStateToProps = (state) => {
  return {
    count: state.count
  }
}
複製程式碼

這段程式碼的功能是將store中的count屬性的值,對映到元件的 this.props.count 上。當我們訪問this.props.count時,輸出的就是store.count的值。當store.count值變化時,元件也會同步更新。

我們還可以使用 ES6 的物件解構賦值、屬性簡寫和箭頭函式等語法,進一步簡化上面的程式碼:

const mapStateToProps = ({ count }) => ({
  count
});
複製程式碼

有時候我們需要根據元件自身的props作一些條件判斷,這時候就需要用到第二個引數。

mapDispatchToProps

mapDispatchToProps也是一個函式,它接收兩個引數:dispatchownProps。第一個引數就是 Redux 的dispatch方法,第二個資料是元件自己的props。它的功能是將action作為props繫結到元件上。

舉個例子:

import { add, minus, asyncAdd } from "@store/counter/action";

const mapDispatchToProps = (dispatch) => {
  return {
    add() {
      dispatch(add());
    },
    dec() {
      dispatch(minus());
    },
    asyncAdd() {
      dispatch(asyncAdd());
    }
  }
}
複製程式碼

當我們呼叫this.props.add時,實際上是在呼叫dispatch(add())

使用 connect 方法

使用connect方法將元件與 Redux 結合:

import { add, minus, asyncAdd } from "@store/counter/action";

// 首先定義元件
class MyComponent extends Component {
  render() {
    return;
    <View>
      <Button onClick={this.props.add}>點選 + 1</Button>
      <View>計數:{this.props.count}次</View>
    </View>;
  }
}

// 定義 mapStateToProps
const mapStateToProps = ({ count }) => ({
  count
});

// 定義 mapDispatchToProps
const mapDispatchToProps = dispatch => {
  return {
    add() {
      dispatch(add());
    }
  };
};

// 使用 connect 方法,export 包裝後的新元件
export connect(mapStateToProps, mapDispatchToProps)(MyComponent);
複製程式碼

這種分散的寫法不利於我們檢視元件從 Redux 中引入了多少props。我們可以使用 ES6 的裝飾器語法進一步改造它:

import { add, minus, asyncAdd } from "@store/counter/action";

@connect(
  ({ counter }) => ({
    counter
  }),
  dispatch => ({
    add() {
      dispatch(add());
    }
  })
)
class MyComponent extends Component {
  render() {
    return;
    <View>
      <Button onClick={this.props.add}>點選 + 1</Button>
      <View>計數:{this.props.count}次</View>
    </View>;
  }
}

export default MyComponent;
複製程式碼

我們甚至可以使用物件形式來傳遞mapDispatchToProps,獲得更簡化的寫法:

@connect(
  ({ counter }) => ({
    counter
  }),
  {
    // 呼叫 this.props.dispatchAdd() 相當於
    // 呼叫 dispatch(add())
    dispatchAdd: add,
    dispatchMinus: minus,
    // ...
  }
)
複製程式碼

這就是 Taro 元件與 Redux 結合的最終形式。

非同步 Action

非同步 Action 返回的是一個引數為dispatch的函式,這個函式本身也可以被dispatch。我們只需要在 Redux 中引入redux-thunk中介軟體,就可以使用非同步 Action。關於非同步 Action 的原理,可以檢視Redux 官方文件

Taro Redux 模板提供了一個非同步 Action 的簡單示例:

/* /store/counter/action.js */
export function asyncAdd() {
  return dispatch => {
    setTimeout(() => {
      dispatch(add());
    }, 2000);
  };
}

// 元件中
@connect(
  ({ counter }) => ({
    counter
  }),
  dispatch => ({
    asyncAdd() {
      dispatch(asyncAdd());
    }
  })
)
class MyComponent extends Component {
  render () {
    return <Button onClick={this.props.asyncAdd}>點選 + 1</Button>
  }
}
複製程式碼

可以看到,非同步 Action 和常規 Action 在使用上並沒有任何區別。

API 封裝

Taro 已經封裝了網路請求,支援 Promise 化使用。本專案對Taro.request()進一步封裝,以便統一管理介面、根據不同環境選擇不同域名、設定請求攔截器、響應攔截器等。完整程式碼見 /src/service 資料夾。

域名切換

生產環境使用線上介面,開發環境使用本地介面。新建/service/config.js檔案:

export default BASE_URL =
  process.env.NODE_ENV === "development"
    ? "http://localhost:3000" // 開發環境,需要開啟mock server(執行:gulp mock)
    : "TODO"; // 生產環境,線上伺服器
複製程式碼

封裝請求

程式碼見 /src/service/api.js,程式碼非常簡單。訪問後臺所需要的認證資訊(token)可以新增在option.header中。

新增攔截器

Taro 支援新增攔截器,可以使用攔截器在請求發出前後做一些額外操作。

為什麼要用攔截器呢?設想一下網路請求的場景。我們的目的是發出一個網路請求並接收響應,但是在發出請求之前,我們可能需要檢查資料、新增使用者的許可權資訊;如果專案大一些,我們可能還需要在發出請求之前先上報統計資料。這一系列流程之後才能真正執行我們的目標操作:網路請求。而獲取到伺服器響應後,我們還需要根據狀態碼執行不同的操作:401/403 跳轉到登入頁面,404 跳轉到空白頁面,500 展示錯誤資訊...

可以看到,如果將這些流程的程式碼都寫到一起,那麼程式碼將又長又亂,十分複雜。

我們可以使用攔截器來解決這個問題。攔截器就是中介軟體,可以幫助我們優雅地分離業務邏輯。我們將每一個業務邏輯寫成一個攔截器,在每個攔截器中,只需要關注當前階段的程式碼實現。

中介軟體的處理流程又稱為洋蔥模型,其執行過程是:先從最外層中介軟體從外到內依次執行到核心程式,再從核心程式從內到外依次執行到最外層中介軟體,每一箇中介軟體的執行引數均是前一箇中介軟體的返回值。如下圖所示:

Taro-library:Taro + Redux + 本地 Mock Server 示例專案

下面是一個簡單的中介軟體/攔截器示例程式碼:

/**
 * @param {object} req request物件
 * @param {function} next 呼叫下一個中介軟體的函式
 */
function interceptor(req, next) {
  // 在下一個中介軟體執行之前做一些操作...
  // 比如新增一個引數
  req.token = 'token'

  // 執行下一個中介軟體...
  // 儲存其返回值
  var res = next(req)

  // 在下一個中介軟體返回結果之後做一些操作...
  // 比如判斷伺服器返回的狀態碼
  if(res.status == 401){
    // ...
  }
  return res
}

複製程式碼

Taro.request的攔截器函式與上例略有不同,將攔截器的呼叫方法改為了非同步的形式:

/**
 * @param {object} chain.requestParmas request物件
 * @param {function} chain.proceed  呼叫下一個中介軟體的函式
 */
function interceptor(chain) {
  // 在下一個中介軟體執行之前做一些操作...
  // 比如新增一個引數
  var requestParmas = chain.requestParmas;
  requestParmas.token = "token";

  // 執行下一個中介軟體...
  return chain.proceed(requestParmas).then(res => {
    // 在下一層行動返回結果之後做一些操作...
    // 比如判斷伺服器返回的狀態碼
    if (res.status == 401) {
      // ...
    }
    return res;
  });
}
複製程式碼

採用攔截器有利於程式碼解耦,符合高內聚低耦合的原則。本專案將攔截器定義在一個單獨的檔案中,以陣列形式統一匯出。使用 Taro 內建攔截器Taro.interceptors.logInterceptor列印請求的相關資訊。程式碼見 /src/service/interceptors.js

async 和 await

最後,當我們發起網路請求時,可以使用 ES6 的async/await語法代替 Promise 物件,能大大提高程式碼的可讀性。關於 async 和 await 的原理,可以檢視理解 JavaScript 的 async/await

一個簡單示例:

// API.get() 返回一個 Promise 物件
// Promise 方法呼叫
function getBook(id) {
  API.get(`/books/${id}`).then(res => {
    this.setState({book: res});
  }).catch(e => {
    console.error(e);
  })
}

// async/await 語法呼叫
async function getBook(id) {
  try {
    const book = API.get(`/books/${id}`);
    this.setState({book: res});
  } catch(e) {
    console.error(e)
  }
}
複製程式碼

搭建本地 mock 服務

常見的 mock 平臺有 EasyMock、rap2 等,不過這些網站有時候響應較慢,除錯起來也不太方便,因此在本地搭建一個 mock 伺服器是更好的選擇。

搭建本地 mock 伺服器有幾種思路,如本地安裝 EasyMock,或者 php 簡單寫幾行返回資料的程式碼,但是這些都需要安裝額外的執行環境,工作量較大。所以我選擇 json-server 實現 mock 服務,搭建過程主要參考了純手工打造前端後端分離專案中的 mock-server

json-server 是一個開箱即用的 REST API 模擬工具,它的文件中有一些簡單示例。不過json-server還無法滿足我對 mock 伺服器的全部需求,所以後面還需要對它進行一些配置。

完整程式碼見 /mock

安裝依賴

這裡需要安裝幾個依賴包,之前安裝過就不用再裝了:

$ npm install json-server mockjs gulp gulp-nodemon browser-sync --save-dev
複製程式碼

要注意 gulp 需要是 3.9.* 版本。後續編譯小程式或者啟動 mock 伺服器時如果報錯,再執行一遍npm install就好了。

設定 json-server

└── mock
    ├── factory
    │   └── book.js
    ├── db.js
    ├── routes.js
    └── server.js
複製程式碼

首先使用 Mock.js 生成一些模擬資料。這部分程式碼見 /mock/factory/book.js,Mock.js 的使用方式請檢視文件

然後建立 mock 資料來源,程式碼見 /mock/db.jsjson-server會將資料來源中的鍵名作為介面路徑名,作為介面返回的資料。

json-server不支援在資料來源的鍵名中新增/,無法直接設定/books/new這樣的二級路徑,因此我們需要使用json-server提供的路由重寫功能:在資料來源中,使用books-new表示books/new;在路由表中,將/books/new指向/books-new。程式碼見 /mock/routes.js

最後在 /mock/server.js 中新增兩個中介軟體。第一個是將所有的POST請求轉為GET請求,防止資料被修改;第二個是為伺服器設定一個 750ms 的延遲,模擬更真實的載入過程:

// 將 POST 請求轉為 GET
server.use((request, res, next) => {
  request.method = "GET";
  next();
});

// 新增一個750ms的延遲
server.use((request, res, next) => {
  setTimeout(next, 750);
});
複製程式碼

啟動服務

在專案根目錄下執行gulp mock即可啟動 mock 伺服器,之後改動/mock資料夾的任何內容,均會實時重新整理 mock 伺服器。程式碼見 /gulpfile.js

開發時,首先執行如下命令,編譯小程式:

$ npm run dev:weapp
複製程式碼

然後新建一個終端,執行以下命令,啟動 mock 伺服器:

$ gulp mock
複製程式碼

之後就享受愉快的開發過程吧!

補充說明

  1. 關閉gulp mock終端程式,模擬網路中斷場景;修改 /mock/server.js 中的延遲時長,模擬 timeout 場景。
  2. mock 伺服器只能在電腦訪問,如果想在真機上測試,可以使用 EasyMock:
    1. 啟動 mock 伺服器,訪問localhost:3000,可以看到所有 mock 介面
    2. 在 EasyMock 專案中新建介面,將 mock 介面的模擬資料複製過去
    3. /src/service/config.js 中的開發環境BASE_URL改為 EasyMock 專案的BASE_URL
  3. 參考資料:json-server 文件純手工打造前端後端分離專案中的 mock-server

其他補充

Taro JSX

不能在render()以外的函式中返回 JSX,也就是說下面這種寫法是不允許的:

renderA() {
  return <View>A</View>
}

renderB() {
  return <View>B</View>
}

render () {
  return (
    <View>
      {someCondition1 && this.renderA()}
      {someCondition2 && this.renderB()}
    </View>
  )
}
複製程式碼

Taro 生命週期

Taro 編譯到小程式端後,每個元件的constructor首先會被呼叫一次(即使沒有例項化),見Taro 文件

constructor中初始化state,在componentDidMount中發起網路請求,componentWillMount不知道有什麼用。更多有關生命週期的知識,請檢視 Taro 文件 React 元件生命週期

執行配置相關

允許在 sass 中通過別名引入其他 sass 檔案

在 sass 中通過別名(@ 或 ~)引用其他 sass 檔案,有兩個解決方法

  1. 在 js 中用import '~taro-ui/dist/style/index.scss'引入
  2. 增加 sass 的 importer 配置,可參考 github.com/js-newbee/t…

本專案採用的是第二種方法。

引入 iconfont 圖示

參考 Taro UI 文件

相關文章