项目地址: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