項目地址:imageslr/taro-libraryphp
本項目是在線借書平臺小程序使用 Taro 重構後的版本,僅包含三個示例頁面,很是簡單。面向人羣主要是 Taro/React/Redux 的初學者,目的是提供一個簡單的實踐項目,幫助理解 Taro 與 Redux 的配合方式與 Taro 的基本使用。本項目還提供了一個快速搭建本地 mock 服務的解決方案。css
由於我也是剛接觸 Taro/React,因此只是分享一些開發經驗,繞開一些小坑。若是以爲不錯的話,請點右上角「⭐️Star」支持一下我,謝謝!若是有問題,歡迎提 issue;若是有任何改進,也歡迎 PR。html
掃碼體驗:
前端
Taro + Taro UI + Redux + Webpack + ES6 + Mocknode
本項目在如下環境中編譯經過:taro v1.2.20、nodejs v8.11.二、gulp v3.9.一、微信開發者工具最新版react
$ git clone https://github.com/imageslr/taro-library.git
$ cd taro-library
$ npm install 或者 yarn
$ npm run dev:weapp
// 新建一個終端,在項目根目錄下執行
$ gulp mock
複製代碼
Taro 是一個遵循 React 語法規範的多端開發解決方案。最近想學習 React,因而就想到使用 Taro 重構很早以前開發的在線借書平臺小程序。雖然 Taro 上手有必定難度,可是其 React 框架比小程序原生更爲靈活與規範,給我帶來了非凡的開發體驗。git
在正式開始以前,您必須對 Taro 框架、 React 語法與小程序框架有必定的瞭解。此外,我建議您閱讀如下文檔,會更容易上手:github
開發工具:VS Code
代碼規範:Prettier 插件 + ES Lint 插件npm
VS Code 對 JSX 與 TypeScript 有自然的支持,使用 VS Code 開發 Taro,不須要配置任何插件就能實現 Taro 組件的自動 import 與 props 提示,很是方便。json
代碼格式化插件我選擇 Prettier,它屏蔽了不少配置項,強制遵循約定的規範。與之相似的格式化插件還有 Beautify,不過我更喜歡 Prettier 對 JSX 屬性強制自動換行的風格。
ES Lint 是 JavaScript 與 JSX 的靜態檢測工具,安裝 ES Lint 插件後在代碼編寫階段就能夠檢測到不易發現的錯誤(如爲常量賦值、變量未使用、變量未定義等等)。Taro 已經定義了一套 ES Lint 規則集,使用 taro-cli 生成的 Taro 項目基本不須要再做額外配置。
Taro UI 定義了不少變量與可複用的 mixins。爲了與 Taro UI 樣式風格保持一致,本項目採用 Taro UI 所使用的 Sass 做爲 CSS 預處理器。
優先使用 Flex 佈局。學習 Flex 佈局能夠參考這兩篇文章:
Taro UI 封裝了一些經常使用的 Flex 樣式類,包括:
at-col-1
、at-col-2
等at-col__offset-1
等flex
屬性:超出換行at-row--wrap
,寬度根據內容撐開at-col--auto
不過 Taro UI 並無爲flex: none;
提供樣式類。
關於 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
,那麼應該這麼寫:
28px
14PX
Taro.pxTransform(14)
28rpx
Taro 會將有大寫字母的Px
或PX
忽略,可是 VS Code 在使用 Prettier 插件時會自動將Px
或PX
轉爲px
。對於這個問題,有兩種解決方案:
/* 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 模板。打開文件夾,能夠看到 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 分支。
推薦先閱讀 Redux 文檔。
使用 Redux 以後,咱們能夠將數據存儲在store
中,經過action
操做數據。那麼怎麼在組件中訪問與操做數據呢?react-redux
提供了connect
方法,容許咱們將store
中的數據與action
做爲props
綁定到組件上。
從原理上來說,connect
方法返回的是一個高階組件。這個高階組件會對原組件進行包裝,而後返回新的組件。不過咱們這裏不講connect
的細節,只講它的使用方法。有關connect
方法與 Redux 的原理,推薦閱讀 React.js 小書。
connect
接收四個參數,分別是mapStateToProps
、mapDispatchToProps
、mergeProps
和options
。本項目只用到了前兩個參數。
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
也是一個函數,它接收兩個參數: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
方法將組件與 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 返回的是一個參數爲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 在使用上並無任何區別。
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。
最後,當咱們發起網絡請求時,可使用 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 平臺有 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
就行了。
└── 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 場景。localhost:3000
,能夠看到全部 mock 接口BASE_URL
改成 EasyMock 項目的BASE_URL
不能在render()
之外的函數中返回 JSX,也就是說下面這種寫法是不容許的:
renderA() {
return <View>A</View>
}
renderB() {
return <View>B</View>
}
render () {
return (
<View> {someCondition1 && this.renderA()} {someCondition2 && this.renderB()} </View>
)
}
複製代碼
Taro 編譯到小程序端後,每一個組件的constructor
首先會被調用一次(即便沒有實例化),見Taro 文檔。
在constructor
中初始化state
,在componentDidMount
中發起網絡請求,componentWillMount
不知道有什麼用。更多有關生命週期的知識,請查看 Taro 文檔與 React 組件生命週期。
在 sass 中經過別名(@ 或 ~)引用其餘 sass 文件,有兩個解決方法:
import '~taro-ui/dist/style/index.scss'
引入本項目採用的是第二種方法。
參考 Taro UI 文檔
app.js
中全局引入icon.scss