前言

对React温习差不多了,是时候尝试制作个小项目。目前使用UmiJS + DvaJS来构建项目。

UmiJS是一个可插拔的企业级 react 应用框架,具体可查看官网文档

DvaJS是dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。官网地址:https://dvajs.com

创建项目

创建UmiJS项目很简单,根据文档的通过脚手架创建项目步骤就行了,这里不多描述。

提示安装类型:
Select the boilerplate type 选择 app
Do you want to use typescript? 选择 No
What functionality do you want to enable? 空格键选择 antddva回车确定

antd是蚂蚁金服的React UI框架,它的设计非常精美,其官网地址:https://ant.design/index-cn。这里加个UI框架只是为了项目数据展示美观些。

配置代理

下面插点数据,既然是用户管理系统就需要一些用户信息,利用mock制造一些json数据:http://jsonplaceholder.typicode.com/users,由于跨域等问题,还是将这个地址反代自己地址吧。根据UmiJS问题的Proxy配置,需要在项目目录下的.umirc.js或者config/config.js添加编辑:

"proxy": {
  "/api": {
    "target": "http://jsonplaceholder.typicode.com/",
    "changeOrigin": true,
    "pathRewrite": { "^/api" : "" }
  }
}

然后启动项目yarn start访问地址:http://localhost:8000/api/users是否成功。

生成路由

编写路由前有必要先了解下UmiJS路由运行方式:UmiJS 路由,路由就是应用不同的页面组成的,如果地址栏是/user/则页面应该是src/pages/user.js。当然你可以手动新建个js/css文件,不过UmiJS提供npx新建路由的方式:

yarn global add npx # 安装npx模块
 
npx umi g page user # 新建/user路由

然后可以在http://localhost:8000/user访问了。

构造 model 和 services

如果在src/modelsrc/services目录下新建文件名字和src/pages相同,Umi会自动注入同名业务代码。

可以利用DvaJS中的fetch方式访问接口获取数据,然后在src/utils/request.js编写一层fetch封装方法:

import fetch from 'dva/fetch';
 
function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }
 
  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}
 
/**
* Requests a URL, returning a promise.
*
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 * @return {object}           An object containing either "data" or "err"
*/
export default async function request(url, options) {
  const response = await fetch(url, options);
 
  checkStatus(response);
 
  const data = await response.json();
 
  const ret = {
    data,
    headers: {},
  };
 
  if (response.headers.get('x-total-count')) {
    ret.headers['x-total-count'] = response.headers.get('x-total-count');
  }
 
  return ret;
}

关于具体的Fetch资料可以到网上查阅。

然后建立请求接口的文件,在新建src/services/user.js

import { PAGE_SIZE } from '../constants'
import request from '@/utils/request'
 
export function queryUserList({ page = 1 }) {
  return request(`/api/users?_page=${page}&_limit=${PAGE_SIZE}`);
}

这里的请求是_page是当前页码,_limit是当前页码数据数量。

src/constants.js编写:

export const PAGE_SIZE = 5;

当然可以在请求参数里写死。

请求服务业务写完了,下面写models层业务,该业务主要是处理数据和逻辑,DvaJS通过model的概念把一个领域的模型管理起来,包含同步更新statereducers,处理异步逻辑的effects,订阅数据源的 subscriptions

新建src/models/users.js

import { queryUserList } from '@/services/users';
 
export default {
  namespace: 'users',
  state: {
    list: [],
    total: null,
    page: null,
  },
  reducers: {
    // 保存数据
    save(state, { payload: { data: list, total, page } }) {
      // 复制数组,将list, total, page内容复制到state
      return { ...state, list, total, page };
    },
  },
  effects: {
    // 访问接口获取数据 并且保存数据
    *fetchUserList({ payload: { page = 1 } }, { call, put }) {
      const { data, headers } = yield call(queryUserList, { page });
      yield put({
        type: 'save',
        payload: {
          data,
          total: parseInt(headers['x-total-count'], 10) ,
          page: parseInt(page, 10)
        }
      });
    },
  },
  subscriptions: {
    // https://github.com/dvajs/dva/issues/174
    setup({ dispatch, history }) {
      return history.listen(({ pathname, query }) => {
        // 进入'/users'地址触发fetchUserList
        if (pathname === '/users') {
          dispatch({ type: 'fetchUserList', payload: query });
        }
      });
    },
  },
};

看着有点多,但里面内容基本都是固定规范,详细的说明可以去DvaJS官网文档 Model查阅:

  • namespace:表示在全局state上的唯一名称
  • state:表示初始值
  • reducers:用于处理同步操作,唯一可以修改state的地方
  • effects:以key/value格式定义effect。用于处理异步操作和业务逻辑,不直接修改state
  • subscriptions:用于订阅一个数据源,然后根据需要 dispatch 相应的 action

构建 UI

修改src/pages/users.js

import { connect } from 'dva';
import { Table, Pagination, Popconfirm } from 'antd';
import styles from './users.css';
import { PAGE_SIZE } from '../constants';
 
/**
* 用户列表管理
* @param dispatch DvaJS dispatch函数 用于触发action 的函数 详解:https://dvajs.com/guide/concepts.html#dispatch-%E5%87%BD%E6%95%B0
* @param dataSource 用户列表信息
* @param total 总数据数量
* @param loading 加载状态
* @param current 当前页码
* @returns {*}
* @constructor
*/
function Users({ dispatch, list: dataSource, loading, total, page: current }) {
  function deleteHandler(id) {
    console.warn(`TODO: ${id}`);
  }
 
  /**
   * 页码改变的回调
   * @param page 改变后的页码
   * @param pageSize 每页条数
   */
  function onChangeUserPagination(page, pageSize) {
    dispatch({
      type: 'users/fetchUserList',
      payload: {
        page: page
      }, // 需要传递的信息
    });
  }
  const columns = [
    {
      title: '用户名',
      dataIndex: 'name',
      key: 'name',
    },
    {
      title: '电子邮件',
      dataIndex: 'email',
      key: 'email',
    },
    {
      title: '个人网站',
      dataIndex: 'website',
      key: 'website',
    },
    {
      title: '操作',
      key: 'operation',
      render: (text, { id }) => (
        <span className={styles.operation}>
           <Button>编辑</Button>
          <Popconfirm title="Confirm to delete?" onConfirm={deleteHandler.bind(null, id)}>
             <Button>删除</Button>
          </Popconfirm>
        </span>
      ),
    },
  ];
  return (
    <div className={styles.normal}>
      <div>
        <Table
          loading={loading}
          columns={columns}
          dataSource={dataSource}
          rowKey={record => record.id}
          pagination={false}
        />
        <Pagination
          className="ant-table-pagination"
          total={total} // 数据总数
          current={current} // 当前页数
          pageSize={PAGE_SIZE} // 每页条数
          onChange={onChangeUserPagination}
        />
      </div>
    </div>
  );
}
 
function mapStateToProps(state) {
  const { list, total, page } = state.users;
  return {
    list,
    total,
    page,
    loading: state.loading.models.users,
  };
}
 
export default connect(mapStateToProps)(Users);

这个页面用到了antd的表格和分页俩个组件,渲染组件是标准的React函数组件,接受带数据的props对象。

上面是进入页面获取数据,如果是主动性获取数据也很简单:先注释src/moudel/users.js中的subscriptions订阅模式模块,然后在src/pages/users.js添加按钮:

  function getUserListData() {
    dispatch({
      type: 'users/fetchUserList',
      payload: {}, // 需要传递的信息
    });
  }
...
  return (
    <div className={styles.normal}>
      <div>
        <Button type="primary" onClick={getUserListData}>获取用户数据</Button>
        <Table
          loading={loading}
          columns={columns}
          dataSource={dataSource}
          rowKey={record => record.id}
          pagination={false}
        />
...

就这样一个简单的页面构建完成。所以完整的功能页面就不在这篇文章详细的编写了,整个业务逻辑和上述过程差不多。

虽然之前做过不少Vue.js单应用页面开发,对比React应用开发,虽然是吃力了些,也麻烦了些,但很对地方写法比Vue.js开发优雅了许多。

参考资料

dva.js 知识导图
Umi入门