阅读 8169

DvaJS的学习之路1 - dva+umi官网例子学习

前置知识

umi + dva,完成用户管理的 CURD 应用

本文主要是在 dva 作者 @sorrycc 的例子 umi + dva,完成用户管理的 CURD 应用 基础上进行的一些自己的学习记录。 关于评论区的小伙伴说的照抄 @sorrycc 大神的github文章的问题,这里回应下,没错,本文 90% 以上的文字都出自 umi + dva,完成用户管理的 CURD 应用 这篇文章,而且作者和文章出处一开始就交代清楚了,写这个文章的目的是记录一下自己跟着教程 step by step 完成这个应用的过程,并不是教程,期望教程的小伙伴, @sorrycc 大大在 dva 官网上提供了很多丰富的例子,可以参考。

2018年9月10日更新,umi 已经升级到了 2.0 版本,希望使用 umi@2 开发的同学请移步至 DvaJS的学习之路2 - umi@2 + dva,完成用户管理的 CURD 应用

开始之前:

  • 确保 node 版本是 8.4 或以上
  • 用 cnpm 或 yarn 能节约你安装依赖的时间

Step 1. 安装 dva-cli@next 并创建应用

先安装 dva-cli,并确保版本是 1.0.0-beta.2 或以上。

$ cnpm i dva-cli@next -g
$ dva -v
dva-cli version 1.0.0-beta.4
    dva version 2.3.1
复制代码

这里需要注意的是安装dva-cli@next版本的原因是目前 umi 还未提供官方的脚手架工具,需要 dva + umi 结合使用可以使用 dva-cli@next 方式来初始化项目。详见:例子和脚手架

然后创建应用:

$ dva new user-dashboard
$ cd user-dashboard
复制代码

Step 2. 配置代理,能通过 RESTFul 的方式访问 http://localhost:8000/api/users

修改 .umirc.js ,加上 "proxy" 配置:

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

然后启动应用:(这个命令一直开着,后面不需要重启)

$ npm start
复制代码

浏览器会自动开启,并打开 http://localhost:8000

image

访问 http://localhost:8000/api/users ,就能访问到 jsonplaceholder.typicode.com/users 的数据。(由于 typicode.com 服务的稳定性,偶尔可能会失败。不过没关系,正好便于我们之后对于出错的处理)

image

Step 3. 生成 users 路由

umi 中文件即路由,所以我们要新增路由,新建文件即可,详见 umijs.org/guide/route…

新建 src/pages/users/page.js,内容如下:

export default () => {
  return (
    <div>
      Users Page
    </div>
  )
}
复制代码

然后访问 http://localhost:8000/users ,你会看到 Users Page 的输出。

注意:使用 umi 约定 src/pages 目录下的文件即路由,而文件则导出 react 组件。可以看到 umi 的特点:以页面维度,将 models 、 services 组织到一起。

Step 4. 构造 users model 和 service

新增 service: src/pages/users/services/users.js

import request from '../../../utils/request';

export function fetch({ page = 1 }) {
  return request(`/api/users?_page=${page}&_limit=5`);
}
复制代码

注意这里的 page 参数默认为 1limit 参数设置为 5

新增 model: src/pages/users/models/users.js,内容如下:

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

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

这里面有一些写法之前没有用过:比如{ payload: { data: list, total } }, 这个是析构时配 alias 的写法;return { ...state, list, total } 的写法用了Spread Operator ... 来组合新对象, 详见 dva知识地图#ES6对象和数组

由于我们需要从 response headers 中获取 total users 数量,所以需要改造下 src/utils/request.js

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;
}
复制代码

注意:这里使用了 ES7 的 async/await 特性,开始对这块不是很熟悉,看了一些关于 async/await 的文章,发现确实比 Promise 的写法语义化更加明显,下面这段是我改写的 Promise 写法:

/**
 * 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 function request(url, options) {
  let headers = {}
  return fetch(url, options)
    .then(checkStatus)
    .then(response => {
      const data = parseJSON(response)
      if (response.headers.get('x-total-count')) {
        headers['x-total-count'] = response.headers.get('x-total-count');
      }
      return data;
    }).then((data) => {
      return {data, headers}
    }).catch(err => ({ err }));
}
复制代码

切换到浏览器(会自动刷新),应该没任何变化,因为数据虽然好了,但并没有视图与之关联。但是打开 Redux 开发者工具,应该可以看到 users/fetchusers/save 的 action 以及相关的 state 。

image

Step 5. 添加界面,让用户列表展现出来

我们把组件存在 src/pages/users/components 里,所以在这里新建 Users.jsUsers.css。具体参考这个 Commit

需留意两件事:

  1. 对 model 进行了微调,加入了 page 表示当前页
  2. 由于 components 和 services 中都用到了 pageSize,所以提取到 src/constants.js

改完后,切换到浏览器,应该能看到带分页的用户列表。

image

有几点需要注意:

  • Users.js 里面使用了 antd 的组件,但是项目并没有手动安装 antd, 原来是 umi 帮我们引入了 antd 。
  • Users.js 里面将model和组件连接了起来,注意 const { list, total, page } = state.users; 里面的 usersmodel 里面的 namespace 名称。
  • 我们没有手动注册 model,umi 帮我们进行了这一步操作, 详见 src/pages/.umi/DvaContainer.js 文件,该文件会自动更新。相关规则详见 umi官网#model注册 一节。
  • 可以直接使用 css module

Step 6. 添加 layout

添加 layout 布局,使得我们可以在首页和用户列表页之间来回切换。umi 里约定 layouts/index.js 为全局路由,所以我们新增 src/layouts/index.js 和 CSS 文件即可。

参考这个 Commit

注意:

页头的菜单会随着页面切换变化,高亮显示当前页所在的菜单项

image

Step 7. 处理 loading 状态

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

umi-plugin-dva 默认内置了 dva-loading 插件。

然后在 src/components/Users/Users.js 里绑定 loading 数据:

+ loading: state.loading.models.users,
复制代码

具体参考这个 Commit

刷新浏览器,你的用户列表有 loading 了没?

Step 8. 处理分页

只改一个文件 src/pages/users/components/Users.js 就好。

处理分页有两个思路:

  1. 发 action,请求新的分页数据,保存到 model,然后自动更新页面
  2. 切换路由 (由于之前监听了路由变化,所以后续的事情会自动处理)

我们用的是思路 2 的方式,好处是用户可以直接访问到 page 2 或其他页面。

参考这个 Commit

Step 9. 处理用户删除

经过前面的 8 步,应用的整体脉络已经清晰,相信大家已经对整体流程也有了一定了解。

后面的功能调整基本都可以按照以下三步进行:

  1. service

  2. model

  3. component 我们现在开始增加用户删除功能。

  4. service, 修改 src/pages/users/services/users.js

export function remove(id) {
  return request(`/api/users/${id}`, {
    method: 'DELETE',
  });
}
复制代码
  1. model, 修改 src/pages/users/models/users.js
*remove({ payload: id }, { call, put, select }) {
  yield call(usersService.remove, id);
  const page = yield select(state => state.users.page);
  yield put({ type: 'fetch', payload: { page } });
},
复制代码
  1. component, 修改 src/pages/users/components/Users.js,替换 deleteHandler 内容:
dispatch({
  type: 'users/remove',
  payload: id,
});
复制代码

切换到浏览器,删除功能应该已经生效。

Step 10. 处理用户编辑

处理用户编辑和前面的一样,遵循三步走:

  1. service
  2. model
  3. component 先是 service,修改 src/pages/users/services/users.js
export function patch(id, values) {
  return request(`/api/users/${id}`, {
    method: 'PATCH',
    body: JSON.stringify(values),
  });
}
复制代码

再是 model,修改 src/pages/users/models/users.js

*patch({ payload: { id, values } }, { call, put, select }) {
  yield call(usersService.patch, id, values);
  const page = yield select(state => state.users.page);
  yield put({ type: 'fetch', payload: { page } });
},
复制代码

最后是 component,详见 Commit

需要注意的一点是,我们在这里如何处理 Modal 的 visible 状态,有几种选择:

  1. 存 dva 的 model state 里
  2. 存 component state 里

另外,怎么存也是个问题,可以:

  1. 只有一个 visible,然后根据用户点选的 user 填不同的表单数据
  2. 几个 user 几个 visible 此教程选的方案是 2-2,即存 component state,并且 visible 按 user 存。另外为了使用的简便,封装了一个 UserModal 的组件。

完成后,切换到浏览器,应该就能对用户进行编辑了。

Step 11. 处理用户创建

相比用户编辑,用户创建更简单些,因为可以共用 UserModal 组件。和 Step 10 比较类似,就不累述了,详见 Commit


到这里,我们已经完成了一个完整的 CURD 应用。如果感兴趣,可以进一步看下 dva 和 umi 的资料:

(完)

总结

做这个练习主要了解了 dva 和 umi 搭配使用的方式,使用 umi 的一些写法和 umi 的特点。感觉学习了Redux之后,dva上手会很快,dva 的网站资源很丰富,希望和大家一起学习,后续还会有 dva 学习的文章。

关注下面的标签,发现更多相似文章
评论