Skip to content
Taro + Redux + 本地 Mock Server 示例项目
JavaScript CSS HTML
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
assets
config
mock 修改mock图片路径 May 8, 2019
src
.editorconfig redux init Mar 26, 2019
.eslintrc
.gitignore
README.md
gulpfile.js Mar 27, 2019
package-lock.json
package.json
project.config.json Update project.config.json Apr 17, 2019
yarn-error.log
yarn.lock Mar 27, 2019

README.md

项目简介

本项目是在线借书平台小程序使用 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(mapStateToPropsmapDispatchToProps)(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.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 配置,可参考 https://github.com/js-newbee/taro-yanxuan/blob/master/config/index.js

本项目采用的是第二种方法。

引入 iconfont 图标

参考 Taro UI 文档

You can’t perform that action at this time.