專案簡介
本專案是線上借書平臺小程式使用 Taro 重構後的版本,僅包含三個示例頁面,非常簡單。面向人群主要是 Taro/React/Redux 的初學者,目的是提供一個簡單的實踐專案,幫助理解 Taro 與 Redux 的配合方式與 Taro 的基本使用。本專案還提供了一個快速搭建本地 mock 服務的解決方案。
因為我也是剛接觸 Taro/React,所以只是分享一些開發經驗,繞開一些小坑。如果覺得不錯的話,請點右上角“⭐️Star”支援一下我,謝謝!如果有問題,歡迎提 issue;如果有任何改進,也歡迎 PR。
掃碼體驗:
技術棧
Taro + Taro UI + Redux + Webpack + ES6 + Mock
專案截圖
執行專案
本專案在以下環境中編譯通過: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-1
、at-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-panel
、my-search-bar
等。
元件可以使用externalClasses
定義若干個外部樣式類,或者開啟options.addGlobalClass
以使用全域性樣式。見Taro 文件 - 元件的外部樣式和全域性樣式。
如果希望能夠在元件的props
中直接傳遞className
或者style
,比如這樣:
// index.jsx
<MyComponent className='custom-class' style={/* ... */}>
複製程式碼
Taro 預設並不支援這一寫法。我們可以將className
和customStyle
作為元件的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 的尺寸單位是px
,預設的尺寸稿是 iPhone 6 750px。Taro 會 1:1 地將px
轉為小程式的rpx
。而在小程式中,px
與rpx
是 1:2 的關係。如果希望字型採用瀏覽器的預設大小14px
,那麼應該這麼寫:
- Taro:
28px
- Taro:
14PX
- Taro JSX 行內樣式:
Taro.pxTransform(14)
- 小程式原生:
28rpx
Taro 會將有大寫字母的Px
或PX
忽略,但是 VS Code 在使用 Prettier 外掛時會自動將Px
或PX
轉為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 的組成部分來劃分的,/constants
是action-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
接收四個引數,分別是mapStateToProps
、mapDispatchToProps
、mergeProps
和options
。本專案只用到了前兩個引數。
mapStateToProps
mapStateToProps
是一個函式,它將store
中的資料對映到元件的props
上。mapStateToProps
接收兩個引數:state
、ownProps
。第一個引數就是 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
也是一個函式,它接收兩個引數:dispatch
、ownProps
。第一個引數就是 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 展示錯誤資訊...
可以看到,如果將這些流程的程式碼都寫到一起,那麼程式碼將又長又亂,十分複雜。
我們可以使用攔截器來解決這個問題。攔截器就是中介軟體,可以幫助我們優雅地分離業務邏輯。我們將每一個業務邏輯寫成一個攔截器,在每個攔截器中,只需要關注當前階段的程式碼實現。
中介軟體的處理流程又稱為洋蔥模型,其執行過程是:先從最外層中介軟體從外到內依次執行到核心程式,再從核心程式從內到外依次執行到最外層中介軟體,每一箇中介軟體的執行引數均是前一箇中介軟體的返回值。如下圖所示:
下面是一個簡單的中介軟體/攔截器示例程式碼:
/**
* @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.js。json-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
複製程式碼
之後就享受愉快的開發過程吧!
補充說明
- 關閉
gulp mock
終端程式,模擬網路中斷場景;修改 /mock/server.js 中的延遲時長,模擬 timeout 場景。 - mock 伺服器只能在電腦訪問,如果想在真機上測試,可以使用 EasyMock:
- 啟動 mock 伺服器,訪問
localhost:3000
,可以看到所有 mock 介面 - 在 EasyMock 專案中新建介面,將 mock 介面的模擬資料複製過去
- 將 /src/service/config.js 中的開發環境
BASE_URL
改為 EasyMock 專案的BASE_URL
- 啟動 mock 伺服器,訪問
- 參考資料: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 檔案,有兩個解決方法:
- 在 js 中用
import '~taro-ui/dist/style/index.scss'
引入 - 增加 sass 的 importer 配置,可參考 github.com/js-newbee/t…
本專案採用的是第二種方法。
引入 iconfont 圖示
參考 Taro UI 文件
- 步驟一:修改 /config/dev.js 與 /config/prod.js
- 步驟二:下載 iconfont 圖示,儲存在 /src/assets/fonts;編寫字型圖示庫 css,程式碼見 /src/styles/common/icon.scss
- 步驟三:在
app.js
中全域性引入icon.scss