Ant Design Pro,React/dva/antd

Ant Design Pro 是一个企业级中后台前端/设计解决方案。本地环境需要安装 node 和 git,技术栈基于 ES2015+、React、dva、g2 和 antd。

参考:https://dvajs.com/

https://github.com/ant-design/ant-design-pro/blob/master/README.zh-CN.md

https://pro.ant.design/docs/getting-started-cn

1、预备知识

1)Redux 是 JavaScript 状态容器,提供可预测化的状态管理;Redux 除了和 React 一起用外,还支持其它界面库。

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]):连接 React 组件与 Redux store。

[mapStateToProps(state, [ownProps]): stateProps] (Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。

  • 函数将被调用两次。第一次是设置参数,第二次是组件与 Redux store 连接:connect(mapStateToProps, mapDispatchToProps, mergeProps)(MyComponent)

  • connect 函数不会修改传入的 React 组件,返回的是一个新的已与 Redux store 连接的组件,而且你应该使用这个新组件。

  • mapStateToProps 函数接收整个 Redux store 的 state 作为 props,然后返回一个传入到组件 props 的对象。

注入 dispatchtodos

function mapStateToProps(state) {
  return { todos: state.todos }
}
export default connect(mapStateToProps)(TodoApp)

// 注入 dispatch 和全局 state
export default connect(state => state)(TodoApp)
// 不要这样做!这会导致每次 action 都触发整个 TodoApp 重新渲染
// 最好在多个组件上使用 connect(),每个组件只监听它所关联的部分 state。

Action 是把数据从应用(这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。

Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。

2)redux-saga 是一个 redux 中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。

redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。通过这样的方式,这些异步的流程看起来就像是标准同步的 Javascript 代码。

effects: {
  *create({ payload: values }, { call, put }) {
    yield call(usersService.create, values);
    yield put({ type: 'reload' });
  },
  *reload(action, { put, select }) {
    const page = yield select(state => state.users.page);
    yield put({ type: 'fetch', payload: { page } });
  },
}

call(fn, ...args)

创建一个 Effect 描述信息,用来命令 middleware 以参数 args 调用函数 fn

  • fn: Function - 一个 Generator 函数, 也可以是一个返回 Promise 或任意其它值的普通函数。
  • args: Array<any> - 传递给 fn 的参数数组。

put(action)

创建一个 Effect 描述信息,用来命令 middleware 向 Store 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。

select(selector, ...args)

创建一个 Effect,用来命令 middleware 在当前 Store 的 state 上调用指定的选择器。

  • selector: Function - 一个 (state, ...args) => args 的函数。它接受当前 state 和一些可选参数,并返回当前 Store state 上的一部分数据。

2、dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。

dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念。dva 帮你自动化了Redux 架构一些繁琐的步骤,比如redux store 的创建,中间件的配置,路由的初始化等等,只需写几行代码就可以实现上述步骤。

1)使用 antd

通过 npm 安装 antdbabel-plugin-importbabel-plugin-import 是用来按需加载 antd 的脚本和样式的;编辑 .webpackrc,使 babel-plugin-import 插件生效。

// .webpackrc.js
extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]]

2)dva应用

// src/index.js 入口js
import dva from 'dva';
import browserHistory from 'history/createBrowserHistory';
import createLoading from 'dva-loading';

// 1. Initialize
const app = dva({
    history: browserHistory(),
});
// 2. Plugins
app.use(createLoading());
// 3. Model
app.model(require('./models/global').default);
app.model(require('./models/menu').default);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root'); // 启动应用

app = dva(opts)-》创建应用,返回 dva 实例。(注:dva 支持多实例)

opts 包含:

  • history:指定给路由用的 history,默认是 hashHistory

2)定义路由

app.router(({ history, app }) => RouterConfig)

注册路由表,推荐把路由信息抽成一个单独的文件,这样结合 babel-plugin-dva-hmr 可实现路由和组件的热加载(只更新页面修改的部分,不会刷新整个页面)。

// .webpackrc.js
env: {
  development: {
    extraBabelPlugins: ['dva-hmr'],
  },
},

3)定义 Model(处理数据和逻辑)

dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。

import * as usersService from '../services/users';

export default {
  namespace: 'users',
  state: {
    list: [],
    total: null,
    page: null,
  },
  reducers: {
    save(state, { payload: { data: list, total, page } }) {
      return { ...state, list, total, page };
    },
  },
  effects: {
    *fetch({ payload: { page = 1 } }, { call, put }) {
      const { data, headers } = yield call(usersService.fetch, { page });
      yield put({
        type: 'save',
        payload: {
          data,
          total: parseInt(headers['x-total-count'], 10),
          page: parseInt(page, 10),
        },
      });
    },
    *remove({ payload: id }, { call, put }) {
      yield call(usersService.remove, id);
      yield put({ type: 'reload' });
    },*reload(action, { put, select }) {
      const page = yield select(state => state.users.page);
      yield put({ type: 'fetch', payload: { page } });
    },
  },
  subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname, query }) => {
        if (pathname === '/users') {
          dispatch({ type: 'fetch', payload: query });
        }
      });
    },
  },
};

namespace:model 的命名空间,同时也是他在全局 state 上的属性

state:初始值

reducers:以 key/value 格式定义 reducer。用于处理同步操作,唯一可以修改 state 的地方。由 action 触发

effects:以 key/value 格式定义 effect。用于处理异步操作和业务逻辑,不直接修改 state。由 action 触发,可以触发 action,可以和服务器交互,可以获取全局 state 的数据等等。

subscriptions:以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。在 app.start() 时被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

app.model(model)-》注册 model

4)编写UI Component并connect起来

import React from 'react';
import { connect } from 'dva';
import { Table, Pagination, Popconfirm, Button } from 'antd';
import { routerRedux } from 'dva/router';
import styles from './Users.css';
import { PAGE_SIZE } from '../../../../constants';
import UserModal from './UserModal';

function Users({ dispatch, list: dataSource, loading, total, page: current }) {
  function deleteHandler(id) {
    dispatch({
      type: 'users/remove',
      payload: id,
    });
  }

  function pageChangeHandler(page) {
    dispatch(
      routerRedux.push({
        pathname: '/users',
        query: { page },
      })
    );
  }

  const columns = [
    {
      title: 'Username',
      dataIndex: 'username',
      key: 'username',
      render: text => <a href="">{text}</a>,
    },
    {
      title: 'Street',
      dataIndex: 'address.street',
      key: 'street',
    },
    {
      title: 'Website',
      dataIndex: 'website',
      key: 'website',
    },
    {
      title: 'Operation',
      key: 'operation',
      render: (text, record) => (
        <span className={styles.operation}>
          <Popconfirm title="Confirm to delete?" onConfirm={deleteHandler.bind(null, record.id)}>
            <a href="">Delete</a>
          </Popconfirm>
        </span>
      ),
    },
  ];

  return (
    <div className={styles.normal}>
      <div>
        <Table
          columns={columns}
          dataSource={dataSource}
          loading={loading}
          rowKey={record => record.id}
          pagination={false}
        />
        <Pagination
          className="ant-table-pagination"
          total={total}
          current={current}
          pageSize={PAGE_SIZE}
          onChange={pageChangeHandler}
        />
      </div>
    </div>
  );
}

function mapStateToProps(state) {
  const { list, total, page } = state.users;
  return {
    loading: state.loading.models.users,
    list,
    total,
    page,
  };
}
export default connect(mapStateToProps)(Users);

5)相关概念

dva 提供了 connect 方法,这个 connect 就是 react-redux 的 connect 。 connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。connect 方法传入的第一个参数是 mapStateToProps 函数,mapStateToProps 函数会返回一个对象,用于建立 State 到 Props 的映射关系。

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State。

Model 对象的属性

  • namespace: 当前 Model 的名称。整个应用的 State,由多个小的 Model 的 State 以 namespace 为 key 合成
  • state: 该 Model 当前的状态。数据保存在这里,直接决定了视图层的输出
  • reducers: Action 处理器,处理同步动作,用来算出最新的 State
  • effects:Action 处理器,处理异步动作

Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 type 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch 函数;需要注意的是 dispatch 是在组件 connect Models以后,通过 props 传入的。在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects

dispatch({
  type: 'user/add', // 如果在 model 外调用,需要添加 namespace
  payload: {}, // 需要传递的信息
});

Reducer函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。

state: {
  list: [],
  total: null,
  page: null,
},
reducers: {
  save(state, { payload: { data: list, total, page } }) {
    return { ...state, list, total, page };
  },
}

Effect:Action 处理器,处理异步动作,基于 Redux-saga 实现。Effect 指的是副作用。根据函数式编程,计算以外的操作都属于 Effect,典型的就是 I/O 操作、数据库读写。

dva 提供多个 effect 函数内部的处理函数,比较常用的是 callput

  • call:执行异步函数
  • put:发出一个 Action,类似于 dispatch
effects: {
  *create({ payload: values }, { call, put }) {
    yield call(usersService.create, values);
    yield put({ type: 'reload' });
  },
  *reload(action, { put, select }) {
    const page = yield select(state => state.users.page);
    yield put({ type: 'fetch', payload: { page } });
  },
}

Router:这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API 可以监听浏览器url的变化,从而控制路由相关操作。

dva 实例提供了 router 方法来控制路由,使用的是react-router。

在组件设计方法中,我们提到过 Container Components,在 dva 中我们通常将其约束为 Route Components,因为在 dva 中我们通常以页面维度来设计 Container Components。

所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在/routes/目录下,而/components/目录下则是纯组件。

组件设计

React 应用是由一个个独立的 Component 组成的,我们在拆分 Component 的过程中要尽量让每个 Component 专注做自己的事。

一般来说,我们的组件有两种设计:Container Component、Presentational Component

  • Container Component

Container Component 一般指的是具有监听数据行为的组件,一般来说它们的职责是绑定相关联的 model 数据,以数据容器的角色包含其它子组件。

  • Presentational Component

它不会关联订阅 model 上的数据,而所需数据的传递则是通过 props 传递到组件内部。

对组件分类,主要有两个好处:让项目的数据处理更加集中;让组件高内聚低耦合,更加聚焦;

试想如果每个组件都去订阅数据 model,那么一方面组件本身跟 model 耦合太多,另一方面代码过于零散,到处都在操作数据,会带来后期维护的烦恼。

除了写法上订阅数据的区别以外,在设计思路上两个组件也有很大不同。 Presentational Component是独立的纯粹的,可以参考 ant.design UI组件的React实现 ,每个组件跟业务数据并没有耦合关系,只是完成自己独立的任务,需要的数据通过 props 传递进来,需要操作的行为通过接口暴露出去。 而 Container Component 更像是状态管理器,它表现为一个容器,订阅子组件需要的数据,组织子组件的交互逻辑和展示。

3、其它

1)roadhog-》和 webpack 相似的库,起的是 webpack 自动打包和热更替的作用

roadhog 是一个 cli 工具,提供 dev、 buildtest 三个命令,分别用于本地调试、构建和测试,并且提供了特别易用的 mock 功能。在体验上,保持了和 create-react-app一致(如 redbox 显示出错信息、HMR、ESLint 出错提示等等),并且提供了 JSON 格式的配置方式。如果 create-react-app 的默认配置不能满足需求,而他又不提供定制的功能,于是基于他实现了一个可配置版。所以如果既要 create-react-app 的优雅体验,又想定制配置,那么可以试试 roadhog 。

## Install globally or locally 
$ npm i roadhog -g

## Local development 
$ roadhog dev

## Build 
$ roadhog build
 
## Test 
$ roadhog test

roadhog dev支持mock, 在.roadhogrc.mock.js里配置

export default {
  // Support type as Object and Array
  'GET /api/users': { users: [1,2] },
  // Method like GET or POST can be omitted(省略)
  '/api/users/1': { id: 1 },
  // Support for custom functions, the API is the same as express@4
  'POST /api/users/create': (req, res) => { res.end('OK'); },
};

roadhog的webpack部分是基于af-webpack的实现。在项目根目录创建 .webpackrc进行配置,格式是JSON。

2)react-router-redux和dva

redux 是状态管理的库,router 是(唯一)控制页面跳转的库。两者都很美好,但是不美好的是两者无法协同工作。换句话说,当路由变化以后,store 无法感知到。于是便有了 react-router-redux

react-router-redux 是 redux 的一个中间件,主要作用是:加强了React Router库中history这个实例,以允许将history中接受到的变化反应到state中去。

从代码上讲,主要是监听了 history 的变化。dva 在此基础上又进行了一层代理,把代理后的对象当作初始值传递给了 dva-core,方便其在 model 的 subscriptions 中监听 router 变化。

3)dva/fetch-》异步请求库,输出 isomorphic-fetch 的接口。

4)dva-loading

dva 有一个管理 effects 执行的 hook,并基于此封装了 dva-loading 插件。通过这个插件,我们可以不必一遍遍地写 showLoading 和 hideLoading,当发起请求时,插件会自动设置数据里的 loading 状态为 true 或 false 。然后我们在渲染 components 时绑定并根据这个数据进行渲染。

// 1、注册 dva-loading 插件
import dva from 'dva';
import createLoading from 'dva-loading';
const app = dva();
app.use(createLoading());

// 2、从store中获取loading状态
import React from 'react';
import { connect } from 'dva';
import { Table } from 'antd';

function Users({ dispatch, list: dataSource, loading }) {
  const columns = [
    {
      title: 'Username',
      dataIndex: 'username',
      key: 'username',
      render: text => <a href="">{text}</a>,
    },
    {
      title: 'Street',
      dataIndex: 'address.street',
      key: 'street',
    },
    {
      title: 'Website',
      dataIndex: 'website',
      key: 'website',
    }
  ];

  return (
    <div className={styles.normal}>
      <Table
        columns={columns}
        dataSource={dataSource}
        loading={loading}
        rowKey={record => record.id}
        pagination={false}
      />
    </div>
  );
}

function mapStateToProps(state) {
  const { list } = state.users;
  return {
    loading: state.loading.models.users,
    list,
  };
}
export default connect(mapStateToProps)(Users);

2、项目积累

1)React 中常见模式是为一个组件返回多个元素。为了包裹多个元素我们写过很多的 div 和 span,进行不必要的嵌套,无形中增加了浏览器的渲染压力。

react15版以前,render 函数的返回必须有一个根节点,否则报错,为满足这一原则我会使用一个没有任何样式的 div 包裹一下。

import React from 'react';
export default function () {
    return (
        <div>
            <div>一步 01</div>
            <div>一步 02</div>
            <div>一步 03</div>
        </div>
    );
}

react 16版开始, render支持返回数组,这一特性已经可以减少不必要节点嵌套。

import React from 'react';
export default function () {
    return [
        <div>一步 01</div>,
        <div>一步 02</div>,
        <div>一步 03</div>
    ];
}

而且,React 16为我们提供了Fragment。Fragment与Vue.js的<template>功能类似,可做不可见的包裹元素。

import React from 'react';
export default function () {
    return (
        <React.Fragment>
            <div>一步 01</div>
            <div>一步 02</div>
            <div>一步 03</div>
        </React.Fragment>
    );
}

参考:https://segmentfault.com/a/1190000013220508

附录:es6

1)Generator 函数

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();

上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(helloworld),即该函数有三个状态:hello,world 和 return 语句(结束执行)。

然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象—遍历器对象。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }

遍历器对象的next方法的运行逻辑如下。

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined

总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

2)Generator 函数的异步应用

ES6 诞生以前,异步编程的方法,大概有四种:回调函数、事件监听、发布/订阅、Promise 对象。Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。