12

Ant Design Pro 实战

概述

Ant Design Pro 是蚂蚁金服出口的一个企业级中后台前端/设计解决方案,基于Ant Design React组件和umi框架。用于快速开发后台管理的前端。

为了不增加阅读难度,本文不讲umi、dva的原理,只讲简单的东西,基本操作,先做什么,再做什么,然后做什么,最后做什么。脚手架已经为我们生成了整项目的架构,我们只关心我们业务相关的内容就行了,先按步骤快速上手,等上手后,如果想知道更多原理,再去查看相关的文档。

此次实战环境:

  1. node v10.16.0
  2. npm 6.14.4
  3. Google Chrome 81.0.4044.122(正式版本) (64 位)

关键组件版本

  1. umi 3.1.1
  2. antd ^4.0.0
  3. ant design pro 4.x

代码已经提交到github,https://github.com/garrett12138/todo-list,参考说明档使用代码。

本文通过一个todo list的小项目进行演示,只用三个功能:

  1. 列表功能,列出所有todo项
  2. 添加新项,添加新的todo项
  3. 更新项,更新现有的todo项状态,修改成待办、已完成或者取消

数据结构:

{
    id:1, //唯一标识
    title:"完成Ant Design Pro 实战",//待办事项标题
    status:0  //状态:0=待办,1=已完成,2=已取消
}

创建新项目

本地环境需要安装 yarnnodegit 环境,如果不懂得安装网上很多文章有介绍,自行百度。

新建项目目录

mkdir  todo-list
cd todo-list

执行

yarn create umi

npm create umi
? Select the boilerplate type (Use arrow keys)
> ant-design-pro  - Create project with a layout-only ant-design-pro boilerplate, use together with umi block.
  app             - Create project with a simple boilerplate, support typescript.
  block           - Create a umi block.
  library         - Create a library with umi.
  plugin          - Create a umi plugin.

选择 ant-design-pro 回车

? Which language do you want to use? (Use arrow keys)
  TypeScript
 >JavaScript

选择javascript回车

? Do you need all the blocks or a simple scaffold? (Use arrow keys)
> simple
  complete

选择simple回车

? Time to use better, faster and latest antd@4! (Y/n)

我们要使用最新版的antd组件,直接回车, 脚手架将会自动安装需要的文件。

目录结构

├── config                   # umi 配置,包含路由,构建等配置
├── mock                     # 本地模拟数据
├── public                   # 一些公共静态资源
├── src
│   ├── assets               # 本地静态资源
│   ├── components           # 业务通用组件
│   ├── e2e                  # 集成测试用例
│   ├── layouts              # 通用布局
│   ├── models               # 全局 dva model
│   ├── pages                # 业务页面入口和常用模板
│   ├── services             # 后台接口服务
│   ├── utils                # 工具库
│   ├── locales              # 国际化资源
│   ├── global.less          # 全局样式
│   └── global.jsx            # 全局 JS
├── tests                    # 测试工具
├── README.md
└── package.json

期中,mock、models、pages、services将是我们的主要工作目录。

本地开发

安装依赖

npm install

安装完后运行

npm start
App running at:
- Local:   http://localhost:8000 (copied to clipboard)
- Network: http://192.168.56.101:8000

浏览器打开http://localhost:8000看到如下界面
andpro-1.png

这就是脚手架为我们建好的框架,一个欢迎页面,一个空白的二级页面和一个查询表格,实际开发中我们要删除掉这些页面,然后建自己的业务界面,这里我们就不删了,在上面添加我们需要的页面就行。

添加新功能

添加新功能我们主要的工作就是上面提到的mock、models、pages、services四个目录,其它虽然也有涉及,但频率相对低很多,这里就不讲了,看官方文档即可。

本项目只有三个功能:

  1. 列表功能,列出所有todo项
  2. 添加新项,添加新的todo项
  3. 更新项,更新现有的todo项状态,修改成待办、已完成或者取消

只要一查询表格页面就行,添加和修改都在同一页面完成。

步骤如下(实际上步骤可以不按这个顺序):

配置路由

... 
routes: [
        {
          path: '/',
          component: '../layouts/BasicLayout',
          authority: ['admin', 'user'],
          routes: [
            {
              path: '/',
              redirect: '/welcome',
            },
            {
              path: '/todo',//url中path部分,http://localhost:8000/todo状态跌幅到这个页面
              name: 'todo', //名称,国际化菜单配置根据这个名称来配置,如果不配置将直菜单将直接显示这个名称
              icon: 'unordered-list',//菜单图标名称
              component:'./todo'// 组件(页面)相对于src/pages的路径
            },
           ...
  ],
...

保存,这时会报错,因为src/todo相关的页面,我之所以习惯按这个顺序来操作是因为,不管我做了什么都可以立即得到反馈,出错了马上报错,不出错可以马上看到结果,我喜欢这种即时反馈。

添加空白页面

在src/pages下新建目录todo,在todo目录下新建空白index.jsx文件,代码如下:

import React, { Component } from 'react';
import { PageHeaderWrapper } from '@ant-design/pro-layout';


class TodoPage extends Component {

    componentWillMount() { }

    render() {
        return (
            <PageHeaderWrapper>
                <div>空白面</div>
            </PageHeaderWrapper>);
    }
}

export default TodoPage

我习惯也建议使用组件开发的方式,这样代码结构比较清晰。

项目中文件的修改、增删都会触发重新编译,浏览器上就可以马上看到效果,如果因为出错冲断了自动重新编译,请重新npm start.

此时,左边菜单多出了一个todo的菜单,点击可以打开我们刚建好的空白页面,如下图

andpro-2.png

修改国际化资源

打开src/locales/zh-CN/menu.js,添加 'menu.'+路由对象中的name为键,值为'待办事项'的键值对,如下

export default {
  ...
  'menu.todo': '待办事项',
};

保存,重新编译后,菜单名称,面包屑、页面标题都更新了,如下图:

andpro-3.png

本项目中只用到中文,如果使用到其它语言,按照这个方法分别修改src/locales下的其它文件夹下的menu.js。

修改空白页面

修改刚才建好的空白页面,在本页面添加模拟数据,和查询表格,事件处理等。组件的使用参考官司例子和组件文档。

import React, { Component } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import ProTable from '@ant-design/pro-table';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import { Button, Divider, Alert, Modal } from 'antd';

const status = [
  <Alert message="待办" type="info" showIcon={false} />,
  <Alert message="已完成" type="success" showIcon />,
  <Alert message="已取消" type="error" showIcon />];

class TodoPage extends Component {

  state = {
    modalVisible: false,
    todoList: [
      {
        "id": 8,
        "title": "完成Antd-Pro-Generator手动添加接口并生成代码",
        "status": 0
      },
      {
        "id": 7,
        "title": "修改Antd-Pro-Generator UI",
        "status": 0
      },
      {
        "id": 6,
        "title": "完善Antd-Pro-Generator数据类型定义",
        "status": 0
      },
      {
        "id": 5,
        "title": "文章使用Antd-Pro-Generator生成代码",
        "status": 1
      },
      {
        "id": 4,
        "title": "Antd-Pro-Generator支持TypeScript",
        "status": 2
      },
      {
        "id": 3,
        "title": "发布Antd-Pro-Generator vscode 插件",
        "status": 1
      },
      {
        "id": 2,
        "title": "文章Ant Design Pro 快速入门",
        "status": 1
      },
      {
        "id": 1,
        "title": "文章React快速入门",
        "status": 1
      }
    ]
  }

  componentDidMount() {
  }

  handelSubmit = async (values) => {
    const { todoList } = this.state;
    const item = { id: todoList.length + 1, title: values.title, status: 0 };
    this.setState({ todoList: [item, ...todoList] })
  }

  handleModalVisible(visible) {
    this.setState({ modalVisible: visible });
  }

  updateStatus(item, _status) {
    const { todoList } = this.state;
    for (let i = 0; i < todoList.length; i += 1) {
      if (todoList[i].id === item.id) {
        todoList[i].status = _status;
        break;
      }
    }
    this.setState({ todoList: [...todoList] });
  }

  render() {
    const { todoList, modalVisible } = this.state;
    const columns = [
      {
        title: 'id',
        dataIndex: 'id',
        hideInForm: true,
      },
      {
        title: '标题',
        dataIndex: 'title',
        rules: [
          {
            required: true,
            message: '待办事项标题不能为空',
          },
        ]
      },
      {
        title: '状态',
        dataIndex: 'status',
        hideInForm: true,
        render: val => status[val]
      },
      {
        title: '修改状态',
        hideInForm: true,
        render: (_, record) => {
          const operations = [];
          if (record.status !== 0) {
            operations.push(<a key='normal'
              onClick={() => this.updateStatus(record, 0)} >  待办</a>);
          }
          if (record.status !== 1) {
            if (operations.length > 0) {
              operations.push(<Divider key='done-divider' type="vertical" />);
            }
            operations.push(<a key='done'
              onClick={() => this.updateStatus(record, 1)} >  完成</a>);
          }
          if (record.status !== 2) {
            if (operations.length > 0) {
              operations.push(<Divider key='canceled-divider' type="vertical" />);
            }
            operations.push(<a key='canceled'
              onClick={() => this.updateStatus(record, 2)} > 取消</a>);
          }
          return (<>{operations}</>)
        },
      },
    ]
    return (
      <PageHeaderWrapper>
        <ProTable
          headerTitle="待办事项列表"
          rowKey="id"
          toolBarRender={() => [
            <Button type="primary" onClick={() => this.handleModalVisible(true)}>
              <PlusOutlined /> 新建
          </Button>,
          ]}
          search={false}
          dataSource={todoList}
          columns={columns}
          pagination={false}
          rowSelection={false}
          expandable={false}
        />
        <Modal
          destroyOnClose
          title="新建待办事项"
          visible={modalVisible}
          onCancel={() => this.handleModalVisible(false)}
          footer={null}
        >
          <ProTable
            onSubmit={async values => {
              await this.handelSubmit(values);
              this.handleModalVisible(false);
            }}
            rowKey="key"
            type="form"
            columns={columns}
          />
        </Modal>
      </PageHeaderWrapper>);
  }
}

export default TodoPage

antdpro-4.gif

添加后端接口访问函数

在src/services目录下新建todo.js文件,添加更新、添加、获取列表函数代码:

/**
 * api client for todo}
 */
import request from '@/utils/request';

/**
 * @summary 更新项
 * @description 更新item项,如果找不到相应id的项则返回错误
 * @param {*} item 参数 {id,title,status} 
 */
export async function updateItem(item) {
    const options = {
        method: 'PUT'
    };
    const url = '/item';
    options.data = item;
    return request(url, options);
}
/**
 * @summary 添加新项
 * @description 添加新的项目到todo列表中
 * @param {*} item 参数 {id,title,status} 
 */
export async function addItem(item) {
    const options = {
        method: 'POST'
    };
    const url = '/item';
    options.data = item;
    return request(url, options);
}
/**
 * @summary 获取所有todo项
 * @description 获取所有todo项
 */
export async function getAll() {
    const options = {
        method: 'GET'
    };
    const url = '/items';
    return request(url, options);
}

添加mock代码

使用mock模拟后端服务器,前后端分离同步开发时在没有后端服务器的情况下,可以本地开发和调试。

在mock文件下新建todo.js,添加代码

export default {
  'PUT /item': (req, res) => {
  // PUT 是http的method,GET,POST,PUT,DELETE等
  //  /item是接口相对于服务器地址的url
  // req 是request对象,http请求的信息包括头部,参数什么的,req.body可以取到传到服务端json,
  // 同样通过req.query,req.params获取其它参数
  // res 是response对象,http请求返回信息包括头部cookie,json数据什么的
  // res.send()是返回客户端的数据,这里直接返回 {code: 0, message: '操作成功', body: true, }
  // res.send(200) 返回200的状态码,不返回其它数据
    const result = {
      code: 0,
      message: '操作成功',
      body: true,
    };
    res.send(result);
  },
  'POST /item': (req, res) => {
    const result = {
      code: 0,
      message: '操作成功',
      body: true,
    };
    res.send(result);
  },
  'GET /items': (req, res) => {
    const result = {
      code: 0,
      message: '操作成功',
      body: [
        {
          id: 8,
          title: '完成Antd-Pro-Generator手动添加接口并生成代码',
          status: 0,
        },
        {
          id: 7,
          title: '修改Antd-Pro-Generator UI',
          status: 0,
        },
        {
          id: 6,
          title: '完善Antd-Pro-Generator数据类型定义',
          status: 0,
        },
        {
          id: 5,
          title: '文章使用Antd-Pro-Generator生成代码',
          status: 1,
        },
        {
          id: 4,
          title: 'Antd-Pro-Generator支持TypeScript',
          status: 2,
        },
        {
          id: 3,
          title: '发布Antd-Pro-Generator vscode 插件',
          status: 1,
        },
        {
          id: 2,
          title: '文章Ant Design Pro 快速入门',
          status: 1,
        },
        {
          id: 1,
          title: '文章React快速入门',
          status: 1,
        },
      ],
    };
    res.send(result);
  },
};

直接返回数据,没有逻辑,如果想模拟得逼真一点,适当加点逻辑。

添加model

react组件、页面中的state它只能在本组件、页面的范围内使用,其它页面是用不到的,虽然可以使用props的方式传子组件,但是有些组件,页面并不存在父子关系,所以数据在各组件、页面中共享很麻烦。model就是为这个而生的。本示例中,todoList 在todo页面用到了,全局顶部的头像下拉菜单也用到了,这种情况适合用model。

在src/models目录下新建todo.js文件

添加代码

import { getAll } from '@/services/todo';
import { message } from 'antd';

export default {
  namespace: 'todo',
  state: {
    todoList: [],
  },
  effects: {
    *fetchTodoList({ payload }, { call, put }) {
      const response = yield call(getAll, payload);
      if (response.code === 0) {
        yield put({
          type: 'setTodoList',
          payload: response.body,
        });
      } else {
        message.error(response.message);
        yield put({
          type: 'setTodoList',
          payload: [],
        });
      }
    },
  },
  reducers: {
    setTodoList(state, action) {
      return {
        ...state,
        todoList: action.payload,
      };
    },
  },
};

model的结构是这样的

export default {
    namespace: 'todo',//model中的effect和reducer要通过命名空间调用,不要和其他model同名
    state: { }, //state,跟react组件中的state差不多一个意思
    effects: { },//effect通常是调用服务端接口,后调用reducer更新state
    reducers: { } //state通过reducer来更新
}

修改页面

去掉页面中state的模拟数据todoList,使用 todo model 中的todoList,修改各事件处理调用api刷新列表。

import React, { Component } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import ProTable from '@ant-design/pro-table';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import { Button, Divider, Alert, Modal, message } from 'antd';
import { connect } from 'umi';
import { addItem, updateItem } from '@/services/todo'

const status = [
  <Alert message="待办" type="info" showIcon={false} />,
  <Alert message="已完成" type="success" showIcon />,
  <Alert message="已取消" type="error" showIcon />];

class TodoPage extends Component {

  state = {
    modalVisible: false,
  }

  componentDidMount() {
    this.loadData();
  }

  handelSubmit = async (values) => {
    const item = { title: values.title, status: 0 };
    //调用todo service的addItem添加待办事项
    const rsp = await addItem(item);
    if (rsp.code === 0) {
      message.success('添加成功!');
      this.loadData();
    } else {
      message.error(rsp.message);
    }
  }

  
  updateStatus = async (item, _status) => {
    //调用todo service的updateItem更新待办事项
    const rsp = await updateItem({ ...item, status: _status });
    if (rsp.code === 0) {
      message.success('修改成功!');
      this.loadData();
    } else {
      message.error(rsp.message);
    }
  }
  //获取待办事项列表
  loadData() {
    //使用connect后,dispatch通过props传给了组件
    const { dispatch } = this.props;
    dispatch({ type: 'todo/fetchTodoList', payload: null });
  }

  handleModalVisible(visible) {
    this.setState({ modalVisible: visible });
  }

  render() {
    const { todo } = this.props;
    const { todoList } = todo;
    const { modalVisible } = this.state;
    const columns = [
      {
        title: 'id',
        dataIndex: 'id',
        hideInForm: true,
      },
      {
        title: '标题',
        dataIndex: 'title',
        rules: [
          {
            required: true,
            message: '待办事项标题不能为空',
          },
        ]
      },
      {
        title: '状态',
        dataIndex: 'status',
        hideInForm: true,
        render: val => status[val]
      },
      {
        title: '修改状态',
        hideInForm: true,
        render: (_, record) => {
          const operations = [];
          if (record.status !== 0) {
            operations.push(<a key='normal'
              onClick={() => this.updateStatus(record, 0)} >  待办</a>);
          }
          if (record.status !== 1) {
            if (operations.length > 0) {
              operations.push(<Divider key='done-divider' type="vertical" />);
            }
            operations.push(<a key='done'
              onClick={() => this.updateStatus(record, 1)} >  完成</a>);
          }
          if (record.status !== 2) {
            if (operations.length > 0) {
              operations.push(<Divider key='canceled-divider' type="vertical" />);
            }
            operations.push(<a key='canceled'
              onClick={() => this.updateStatus(record, 2)} > 取消</a>);
          }
          return (
            <>
              {operations}
            </>
          )

        },
      },
    ]
    return (
      <PageHeaderWrapper>
        <ProTable
          headerTitle="待办事项列表"
          rowKey="id"
          toolBarRender={() => [
            <Button type="primary" onClick={() => this.handleModalVisible(true)}>
              <PlusOutlined /> 新建
          </Button>,
          ]}
          search={false}
          dataSource={todoList}
          columns={columns}
          pagination={false}
          rowSelection={false}
          expandable={false}
        />
        <Modal
          destroyOnClose
          title="新建待办事项"
          visible={modalVisible}
          onCancel={() => this.handleModalVisible(false)}
          footer={null}
        >
          <ProTable
            onSubmit={async values => {
              await this.handelSubmit(values);
              this.handleModalVisible(false);
            }}
            rowKey="key"
            type="form"
            columns={columns}
          />
        </Modal>
      </PageHeaderWrapper>);
  }
}
//使用umi的connect方法把命名空间为todo的model的数据通过props传给页面
export default connect(({ todo }) => ({ todo }))(TodoPage);

这时,虽然修改状态提示成功和添加成功,但列表数据不会有任何变化,这是因为我们的mock没有逻辑,获取列表什么时候返回的都是同样的数据,所以不所怎么操作,返回的数据都是一样不变的。其实,开发过程中客户端中没有什么逻辑,只要获取、提交数据的参数对了,对结果的判断处理正确了就行了,逻辑都是后端完成的。如果想模拟得真的一点,给mock加上逻辑。

修改mock逻辑

let list = [
  {
    id: 8,
    title: '完成Antd-Pro-Generator手动添加接口并生成代码',
    status: 0,
  },
  {
    id: 7,
    title: '修改Antd-Pro-Generator UI',
    status: 0,
  },
  {
    id: 6,
    title: '完善Antd-Pro-Generator数据类型定义',
    status: 0,
  },
  {
    id: 5,
    title: '文章使用Antd-Pro-Generator生成代码',
    status: 1,
  },
  {
    id: 4,
    title: 'Antd-Pro-Generator支持TypeScript',
    status: 2,
  },
  {
    id: 3,
    title: '发布Antd-Pro-Generator vscode 插件',
    status: 1,
  },
  {
    id: 2,
    title: '文章Ant Design Pro 快速入门',
    status: 1,
  },
  {
    id: 1,
    title: '文章React快速入门',
    status: 1,
  },
];
export default {
  'PUT /item': (req, res) => {
    let result;
    const item = req.body;
    for (let i = 0; i < list.length; i += 1) {
      if (item.id === list[i].id) {
        list[i] = item;
        result = {
          code: 0,
          message: '操作成功',
          body: true,
        };
        res.send(result);
        return;
      }
    }
    result = {
      code: 404,
      message: '待办事项不存在',
      body: true,
    };
    res.send(result);
  },
  'POST /item': (req, res) => {
    const item = { ...req.body, id: list.length + 1 };
    list = [item, ...item];
    const result = {
      code: 0,
      message: '操作成功',
      body: true,
    };
    res.send(result);
  },
  'GET /items': (req, res) => {
    const result = {
      code: 0,
      message: '操作成功',
      body: list,
    };
    res.send(result);
  },
};

修改顶部头像下拉菜单

修改头像下拉菜单

打开src/components/GlobalHeader/AvatarDropdown.jsx,修改代码,头像下拉菜单增加一个待办事项菜单,并加上徽标Badge当有待办事项时显示数量,同时用记名称也加上徽标,当有待办事项上显示红点,代码如下:

import { LogoutOutlined, SettingOutlined, UserOutlined, UnorderedListOutlined } from '@ant-design/icons';
import { Avatar, Menu, Spin, Badge } from 'antd';
import React from 'react';
import { history, connect } from 'umi';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';

class AvatarDropdown extends React.Component {
  onMenuClick = (event) => {
    const { key } = event;
    if (key === 'logout') {
      const { dispatch } = this.props;

      if (dispatch) {
        dispatch({
          type: 'login/logout',
        });
      }
      return;
    }
    //--------------------------------------
    //点击待办事项菜单时跳转到todo页面
    if (key === 'todo') {
      history.push(`/todo`);
      return;
    }
    //--------------------------------------  
    history.push(`/account/${key}`);
  };

  render() {
    const {
      currentUser = {
        avatar: '',
        name: '',
      },
      menu,
      todo: { todoList }
    } = this.props;
    //----------------------------------------------------------------
    //计算待办事项数量
    const todoNum = todoList.filter(item => item.status === 0).length;
    //----------------------------------------------------------------
    const menuHeaderDropdown = (
      <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
        {menu && (
          <Menu.Item key="center">
            <UserOutlined />
            个人中心
          </Menu.Item>
        )}
        {menu && (
          <Menu.Item key="settings">
            <SettingOutlined />
            个人设置
          </Menu.Item>
        )}
        {menu && <Menu.Divider />}
        <!---增加待办事项菜单------------------------------------------>
        <Menu.Item key="todo">
          <UnorderedListOutlined />
          <Badge offset={[13, 8]} count={todoNum}>待办事项</Badge>
        </Menu.Item>
         <!---------------------------------------------------------->
        <Menu.Item key="logout">
          <LogoutOutlined />
          退出登录
        </Menu.Item>
      </Menu>
    );
    return currentUser && currentUser.name ? (
      <HeaderDropdown overlay={menuHeaderDropdown}>
        <span className={`${styles.action} ${styles.account}`}>
          <Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
          <span className={styles.name}>
            <!---用户名增加徽标---------------------------------------->
            <Badge dot={true} count={todoNum}>{currentUser.name}</Badge>
          </span>
        </span>
      </HeaderDropdown>
    ) : (
        <span className={`${styles.action} ${styles.account}`}>
          <Spin
            size="small"
            style={{
              marginLeft: 8,
              marginRight: 8,
            }}
          />
        </span>
      );
  }
}
//----------------------------------------
//将命名空间为todo的model 通过props传给组件
export default connect(({ user, todo }) => ({
  currentUser: user.currentUser,
  todo,
}))(AvatarDropdown);

这个时,如果刚加载整个页面,显示欢迎页面,待办事项都是0,因为我们还没有打开todo页面,还没有拉取待办事项列表,要在页面加载时就拉取待办事项列表,要在基本布局组件加载时拉取数据。

修改布局组件,加载时获取待办列表

打开srclayoutsBasicLayout.jsx文件,增加拉取待办事项代码:

...

useEffect(() => {
    if (dispatch) {
      dispatch({
        type: 'user/fetchCurrent',
      });
      //---------------------------------
      //调用todo model 的fetchTodoList effect
      dispatch({
        type: 'todo/fetchTodoList',
      });
      //---------------------------------
    }
  }, []);

...

数据流程

此时todo model的数据connect两个地方,一个是AvatarDropdown,一个是TodoPage,打开todo页面,更新状态或者添加新的待办事项时,代码中都重新加载待办事项列表,数据流程如下:

  1. 调用加载列表函数 -->this.loadData
  2. loadData 调用todo model的effect fetchTodoList--> dispatch(type:'todo/fetchTodoList',payload:null)
  3. todo model的effect fetchTodoList 调用todo service的getAll函数 得到返回数据 response ---> const response = yield call(getAll, payload),再调用reducer setTotoList 更新model的state--> yield put({ type: 'setTodoList', payload: response.body})
  4. AvatarDropdown和TodoPage同时得知model的state有更新,各自调用render刷新自身,这样待力事项列表和头像下拉菜单同时显示变化。

结束

todo list这个项目到此就完成了,ant design pro 使用了umi,dva(model),还有一些es6的特性(effect 中的星 * ,yield),这些刚开始如果看不懂,可以先照着做,上手后再慢慢去查看相关的文档吧。


羽天
27 声望4 粉丝