dva的进一步学习创建UCRD项目(十大正规网站版)


title: dva的进一步学习创建UCRD项目(十大正规网站版)
date: 2018-05-31 01:07:50
tags: dva


目录

准备工作

划分结构

设计model

在了解了项目基本的结构划分以后,大家将要开始设计 model,在设计 model 之前,大家来回顾一下大家需要做的项目是什么样的:

pic1

Model的抽象

从设计稿中大家可以看出,这部分功能基本是围绕 以用户数据为基础 的操作,其中包含:

  1. 用户信息的展示(查询)
  2. 用户信息的操作(增加,删除,修改

大家在项目的model 下新增一个users.js

大家可以按照两个纬度的东西来看:1. 按照数据纬度 2. 按照业务纬度

数据纬度

按照数据维度的 model 设计原则就是抽离数据本身以及相关操作的方法,比如在本例的 users :

/**
 * model users.js的设计、
 * 按照数据维度的 model 设计原则就是抽离数据本身以及相关操作的方法,
 */
 export default {
     namespace:'users',
     state:{
         list:[],
         total:null,
     },
     effects:{
        *query(){},
        *create(){},
        // 因为delete是关键字
        *'delete'(){},
        *update(){},
     },
     reducers: {
        querySuccess(){},
        createSuccess(){},
        deleteSuccess(){},
        updateSuccess(){},
    }
 }

如果你写过后台代码,你会发现这跟大家常常写的后台接口是很类似的,只关心数据本身,至于在使用 users model 的组件中所遇到的状态管理等信息跟 model 无关,而是作为组件自身的state维护。

这种设计方式使得 model 很纯粹,在设计通用数据信息 model 的时候比较适用,比如当前用户登陆信息等数据 model。但是在数据跟业务状态交互比较紧密,数据不是那么独立的时候会有些不那么方便,因为在数据跟业务状态紧密相连的场景下,将状态放到 model 里面维护会使得大家的代码更加清晰可控,而这种方式就是下面将要先容的业务维度方式的设计。

业务纬度

按照业务维度的 model 设计,则是将数据以及使用强关联数据的组件中的状态统一抽象成 model 的方法,(更多会考虑交互上的方法,比如loading 的展示或者消失,modal的展示或者消失,在这个例子中,大家的组件会使用ant-design中的设计)在本例中,users model设计如下:

/**
 * model users.js的设计、
 * 按照数据维度的 model 设计原则就是抽离数据本身以及相关操作的方法,
 */

 export default {
     namespace:'users',
     state:{
         list:[],
         total:null,
         loading: false, // 控制加载状态
         current: null, // 当前分页信息
         currentItem: {}, // 当前操作的用户对象
         modalVisible: false, // 弹出窗的显示状态
         modalType: 'create', // 弹出窗的类型(添加用户,编辑用户)
     },
     effects:{
        *query(){},
        *create(){},
        // 因为delete是关键字
        *'delete'(){},
        *update(){},
     },
     reducers: {
        //业务纬度的设计(更多会考虑交互上的问题)
        showLoading(){}, // 控制加载状态的 reducer
        showModal(){}, // 控制 Modal 显示状态的 reducer
        hideModal(){},
        //数据纬度的设计
        querySuccess(){},
        createSuccess(){},
        deleteSuccess(){},
        updateSuccess(){},
    }
 }

组件设计方法

在初步确定了 model 的设计方法以后,让大家来看看如何设计 dva 中的 React 组件。

React 应用是由一个个独立的 Component 组成的,大家在拆分 Component 的过程中要尽量让每个 Component 专注做自己的事。(这也是react的核心思想,组件化的思想,在设计和使用的时候,大家拆分组件,让组件做自己的事情。)

一般来说,大家的组件有两种设计:

  • Container Component
  • Presentational Component

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

通常的写法为:

import React, { Component, PropTypes } from 'react';

// dva 的 connect 方法可以将组件和数据关联在一起
import { connect } from 'dva';

// 组件本身
const MyComponent = (props)=>{};
MyComponent.propTypes = {};

// 监听属性,建立组件和数据的映射关系
function mapStateToProps(state) {
  return {...state.data};
}

// 关联 model
export default connect(mapStateToProps)(MyComponent);

Presentational Component 的名称已经说明了它的职责,展示形组件,一般也称作:Dumb Component,它不会关联订阅 model 上的数据,而所需数据的传递则是通过 props 传递到组件内部。

通常写法为:

import React, { Component, PropTypes } from 'react';

// 组件本身
// 所需要的数据通过 Container Component 通过 props 传递下来
const MyComponent = (props)=>{}
MyComponent.propTypes = {};

// 并不会监听数据
export default MyComponent;
那么拆分组件有什么好处呢?
  • 让项目的数据处理更加集中;
  • 让组件高内聚低耦合,更加聚焦;

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

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

组件设计实践

首先啊,咱们需要先创建路由,设置 Users Router Container 的访问路径,并且在 /routes/ 下创建大家的组件文件 Users.jsx。

// .src/router.js
import React, { PropTypes } from 'react';
import { Router, Route } from 'dva/router';
import Users from './routes/Users';

export default function({ history }) {
  return (
    <Router history={history}>
      <Route path="/users" component={Users} />
    </Router>
  );
};

之后创建路由容器组件users.jsx

// .src/routes/Users.jsx
import React, { PropTypes } from 'react';

function Users() {
  return (
    <div>User Router Component</div>
  );
}

Users.propTypes = {
};

export default Users;

Users Container Component 的设计

大家采用自顶向下的设计方法,修改 ./src/routes/Users.jsx

// ./src/routes/Users.jsx
import React, { Component, PropTypes } from 'react';

// Users 的 Presentational Component
// 暂时都没实现
import UserList from '../components/Users/UserList';
import UserSearch from '../components/Users/UserSearch';
import UserModal from '../components/Users/UserModal';

// 引入对应的样式
// 可以暂时新建一个空的
import styles from './Users.less';

function Users() {

  const userSearchProps = {};
  const userListProps = {};
  const userModalProps = {};

  return (
    <div className={styles.normal}>
      {/* 用户筛选搜索框 */}
      <UserSearch {...userSearchProps} />
      {/* 用户信息展示列表 */}
      <UserList {...userListProps} />
      {/* 添加用户 & 修改用户弹出的浮层 */}
      <UserModal {...userModalProps} />
    </div>
  );
}

export default Users;

创建UserList,UserSearch,UserModal这三个组件

// ./src/components/Users/UserSearch.jsx
import React, { PropTypes } from 'react';
export default ()=><div>user search</div>;
// ./src/components/Users/UserList.jsx
import React, { PropTypes } from 'react';
export default ()=><div>user list</div>;
// ./src/components/Users/UserModal.jsx
import React, { PropTypes } from 'react';
export default ()=><div>user modal</div>;

这样如果你到现在都是成功的话,那么访问本地的地址,如:http://localhost:8000/#/users 应该是展示

user search
user list
user modal

这样的数据的。

值得注意的地方,通常定义大家的组件一般有三种方式:

// 1. 传统写法
const App = React.createClass({});

// 2. es6 的写法
class App extends React.Component({});

// 3. stateless 的写法(大家推荐的写法)
const App = (props) => ({});

其中第1种是大家不推荐的写法,第2种是在你的组件涉及 react 的生命周期方法的时候采用这种写法,而第3种则是大家一般推荐的写法。详细内容可以参看Stateless Functions。

UserList 组件

暂时放下<UserSearch />和<UserModal />,先来看看<UserList />的实现,这是一个用户的展示列表,大家希望只需要把数据传入进去,修改 ./src/components/Users/UserList.jsx:

这里大家采用antd UI组件

import { Table, message, Popconfirm } from 'antd';

这里大家不难发现。大家使用Table组件,因此需要传入一些参数。这里大家使用了stateless的写法

// ./src/components/Users/UserList.jsx
import React, { Component, PropTypes } from 'react';

// 采用antd的UI组件
import { Table, message, Popconfirm } from 'antd';

// 采用 stateless 的写法
// export default ()=><div>user list</div>;


const UserList = ({
    total,
    current,
    loading,
    dataSource,
}) => {
    const columns = [{
        title: '姓名',
        dataIndex: 'name',
        key: 'name',
        render: (text) => <a href="#">{text}</a>,
      }, {
        title: '年龄',
        dataIndex: 'age',
        key: 'age',
      }, {
        title: '住址',
        dataIndex: 'address',
        key: 'address',
      }, {
        title: '操作',
        key: 'operation',
        render: (text, record) => (
          <p>
            <a onClick={()=>{}}>编辑</a>
            &nbsp;
            <Popconfirm title="确定要删除吗?" onConfirm={()=>{}}>
              <a>删除</a>
            </Popconfirm>
          </p>
        ),
      }];


      //定义分页对象

      const pagination = {
        total,
        current,
        pageSize: 10,
        onChange: ()=>{},
      };

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



}
export default UserList;

需要注意的是,由于大家采用了 antd,所以大家需要在大家的代码中添加样式,可以在 ./src/index.js 中添加一行:

import 'antd/dist/antd.css'

接下来。由于大家传递了 columns ,dataSource ,loading ,rowKey,pagination等props,因此,大家需要制造一些假数据传入,使得页面更真实。

在routes/Users.jsx中,新增假数据

  const userListProps = {
    //默认先传一些假数据
    total: 3,
    current: 1,
    loading: false,
    dataSource: [
      {
        name: '张三',
        age: 23,
        address: '成都',
      },
      {
        name: '李四',
        age: 24,
        address: '杭州',
      },
      {
        name: '王五',
        age: 25,
        address: '上海',
      },
    ],

  };

最终展现的效果图如下所示:

效果图

组件设计小结

虽然大家上面实现的代码很简单,但是已经包含了组件设计的主要思路,可以看到 UserList 组件是一个很纯粹的 Presentation Component,所需要的数据以及状态是通过 Users Router Component 传递的,大家现在还是用的静态数据,接下来大家来看看如何在 model 创建 reducer 来将大家的数据抽象出来。

添加Reducer

如果对redux有所了解的话,那么就能很好的理解 Reducer,在这里我做个不恰当的比方,好比李云龙负责带兵打仗,那么李云龙在整个react中其实是负责展示这一块,大家知道军队还有后勤这一块,那么赵政委就是负责后勤这一块,告诉李云龙,这里不需要你负责,你只要展示,他们如果展示,如果操控我来负责。其实赵政委就是redux 。而Reducer 是主要控制状态改变 也就是大家所说的 state,而发出改变的命令是action来操控的。这也就有了dispatch的概念,dispatch其实就是命令。当然之后react-redux优化了这一块,使得大家在改变数据源的时候更加的方便。

给UserModal添加Reducers

回到大家之前的 /models/users.js,大家在之前已经定义好了它的 state,接下来大家看看如何根据新的数据来修改本身的 state,这就是 reducers 要做的事情。

export default {
  namespace: 'users',

  state: {
    list: [],
    total: null,
    loading: false, // 控制加载状态
    current: null, // 当前分页信息
    currentItem: {}, // 当前操作的用户对象
    modalVisible: false, // 弹出窗的显示状态
    modalType: 'create', // 弹出窗的类型(添加用户,编辑用户)
  },
  effects: {
    *query(){},
    *create(){},
    *'delete'(){},
    *update(){},
  },
  reducers: {
    showLoading(){}, // 控制加载状态的 reducer
    showModal(){}, // 控制 Modal 显示状态的 reducer
    hideModal(){},
    // 使用静态数据返回
    querySuccess(state){
      const mock = {
        total: 3,
        current: 1,
        loading: false,
        list: [
          {
            id: 1,
            name: '张三',
            age: 23,
            address: '成都',
          },
          {
            id: 2,
            name: '李四',
            age: 24,
            address: '杭州',
          },
          {
            id: 3,
            name: '王五',
            age: 25,
            address: '上海',
          },
        ],

      };
      return {...state, ...mock, loading: false};
    },
    createSuccess(){},
    deleteSuccess(){},
    updateSuccess(){},
  }
}

大家把之前 UserList 组件中模拟的静态数据,移动到了 reducers 中,通过调用 users/query/success 这个 reducer,大家就可以将 Users Model 的数据变成静态数据,那么大家如何调用这个 reducer,能够让这个数据传入 UserList 组件呢,接下来需要做的是:关联Model。

关联Model

// ./src/routes/Users.jsx
import React, { Component, PropTypes } from 'react';

// 引入 connect 工具函数
import { connect } from 'dva';

// Users 的 Presentational Component
// 暂时都没实现
import UserList from '../components/Users/UserList';
import UserSearch from '../components/Users/UserSearch';
import UserModal from '../components/Users/UserModal';

// 引入对应的样式
// 可以暂时新建一个空的
import styles from './Users.less';

function Users({ location, dispatch, users }) {

  const {
    loading, list, total, current,
    currentItem, modalVisible, modalType
    } = users;

  const userSearchProps={};
  const userListProps={
        dataSource: list,
        total,
        loading,
        current,
    };
  const userModalProps={};

  return (
    <div className={styles.normal}>
      {/* 用户筛选搜索框 */}
      <UserSearch {...userSearchProps} />
      {/* 用户信息展示列表 */}
      <UserList {...userListProps} />
      {/* 添加用户 & 修改用户弹出的浮层 */}
      <UserModal {...userModalProps} />
    </div>
  );
}

Users.propTypes = {
  users: PropTypes.object,
};

// 指定订阅数据,这里关联了 users
function mapStateToProps({ users }) {
  return {users};
}

// 建立数据关联关系
export default connect(mapStateToProps)(Users);

在之前的 组件设计 中讲到了 Presentational Component 的设计概念,在订阅了数据以后,就可以通过 props 访问到 model 的数据了,而 UserList 展示组件的数据,也是 Container Component 通过 props 传递的过来的。

组件和 model 建立了关联关系以后,如何在组件中获取 reduers 的数据呢,或者如何调用 reducers呢,就是需要发起一个 action。

Action

actions 的概念跟 reducers 一样,也是来自于 dva 封装的 redux,表达的概念是发起一个修改数据的行为,主要的作用是传递信息:

dispatch({
    type: '', // action 的名称,与 reducers(effects)对应
    ... // 调用时传递的参数,在 reducers(effects)可以获取
});

需要注意的是:action的名称(type)如果是在 model 以外调用需要添加 namespace。

回到例子中,目前传入 UserList 组件的只是默认空数据,那么如何调用 reducers 获取刚才定义的静态数据呢?发起一个 actions:

dispatch({
    type: 'users/querySuccess', // 调用一个actions
    payload: {}, // 调用时传递的参数
});

知道了如何发起一个 action,那么剩下的就是发起的时机了,通常大家建议在组件内部的生命周期发起,如:

componentDidMount() {
    this.props.dispatch({
        type: 'model/action',
    });
}

不过在本例中采用另一种发起 action 的场景,在本例中获取用户数据信息的时机就是访问 /users/ 这个页面,所以大家可以监听路由信息,只要路径是 /users/ 那么大家就会发起 action,获取用户数据:

// ./src/models/users.js
import { hashHistory } from 'dva/router';

export default {
  namespace: 'users',

  state: {
    list: [],
    total: null,
    loading: false, // 控制加载状态
    current: null, // 当前分页信息
    currentItem: {}, // 当前操作的用户对象
    modalVisible: false, // 弹出窗的显示状态
    modalType: 'create', // 弹出窗的类型(添加用户,编辑用户)
  },

  // Quick Start 已经先容过 subscriptions 的概念,这里不在多说
  subscriptions: {
    setup({ dispatch, history }) {
      history.listen(location => {
        if (location.pathname === '/users') {
          dispatch({
            type: 'querySuccess',
            payload: {}
          });
        }
      });
    },
  },

  effects: {
    *query(){},
    *create(){},
    *'delete'(){},
    *update(){},
  },
  reducers: {
    showLoading(){}, // 控制加载状态的 reducer
    showModal(){}, // 控制 Modal 显示状态的 reducer
    hideModal(){},
    // 使用静态数据返回
    querySuccess(state){
      const mock = {
        total: 3,
        current: 1,
        loading: false,
        list: [
          {
            name: '张三',
            age: 23,
            address: '成都',
          },
          {
            name: '李四',
            age: 24,
            address: '杭州',
          },
          {
            name: '王五',
            age: 25,
            address: '上海',
          },
        ],

      };
      return {...state, ...mock, loading: false};
    },
    createSuccess(){},
    deleteSuccess(){},
    updateSuccess(){},
  }
}

以上代码在浏览器访问 /users 路径的时候就会发起一个 action,数据准备完毕,别忘了回到 index.js 中,添加大家的 models:

// ./src/index.js
import './index.html';
import './index.less';
import dva, { connect } from 'dva';

import 'antd/dist/antd.css';

// 1. Initialize
const app = dva();

// 2. Model
app.model(require('./models/users.js'));

// 3. Router
app.router(require('./router'));

// 4. Start
app.start(document.getElementById('root'));

这样大家依旧可以看到如下的效果:

效果图

小结

在这个例子中,大家在 合适的时机(进入 /users/ )发起(dispatch)了一个 action,修改了 model 的数据,并且通过 Container Components 关联了 model,通过 props 传递到 Presentation Components,组件成功显示。如果你想了解更多关于 reducers & actions 的信息,可以参看 redux。

Effects

在之前的教程中,大家已经完成了静态数据的操作,但是在真实场景,数据都是从服务器来的,大家需要发起异步请求,在请求回来以后设置数据,更新 state,那么在 dva 中,这一切是怎么操作的呢,首先大家先来简单了解一下 Effects。

Effects 来源于 dva 封装的底层库 redux-sagas 的概念,主要指的是处理 Side Effects ,指的是副作用(源于函数式编程),在这里可以简单理解成异步操作,所以大家是不是可以理解成 Reducers 处理同步,Effects 处理异步?这么理解也没有问题,但是要认清 Reducers 的本质是修改 model 的 state,而 Effects 主要是 控制数据流程 ,所以最终往往大家在 Effects 中会调用 Reducers

给Users Model添加Effects

// ./src/models/users.js
import { hashHistory } from 'dva/router';
//import { create, remove, update, query } from '../services/users';

// 处理异步请求
import request from '../utils/request';
import qs from 'qs';
async function query(params) {
  return request(`/api/users?${qs.stringify(params)}`);
}

export default {
  namespace: 'users',

  state: {
    list: [],
    total: null,
    loading: false, // 控制加载状态
    current: null, // 当前分页信息
    currentItem: {}, // 当前操作的用户对象
    modalVisible: false, // 弹出窗的显示状态
    modalType: 'create', // 弹出窗的类型(添加用户,编辑用户)
  },

  subscriptions: {
    setup({ dispatch, history }) {
      history.listen(location => {
        if (location.pathname === '/users') {
          dispatch({
            type: 'query',
            payload: {}
          });
        }
      });
    },
  },

  effects: {
    *query({ payload }, { select, call, put }) {
      yield put({ type: 'showLoading' });
      const { data } = yield call(query);
      if (data) {
        yield put({
          type: 'querySuccess',
          payload: {
            list: data.data,
            total: data.page.total,
            current: data.page.current
          }
        });
      }
    },
    *create(){},
    *'delete'(){},
    *update(){},
  },
  reducers: {
    showLoading(state, action){
      return { ...state, loading: true };
    }, // 控制加载状态的 reducer
    showModal(){}, // 控制 Modal 显示状态的 reducer
    hideModal(){},
    // 使用服务器数据返回
    querySuccess(state, action){
      return {...state, ...action.payload, loading: false};
    },
    createSuccess(){},
    deleteSuccess(){},
    updateSuccess(){},
  }
}

首先大家需要增加 *query 第二个参数 *query({ payload }, { select, call, put }) ,其中 call 和 put 是 dva 提供的方便操作 effects 的函数,简单理解 call 是调用实行一个函数而 put 则是相当于 dispatch 实行一个 action,而 select 则可以用来访问其它 model,更多可以参看 redux-saga-in-chinese。

而在 query 函数里面,可以看到大家处理异步的方式跟同步一样,所以能够很好的控制异步流程,这也是大家使用 Effects 的原因,关于相关的更多内容可以参看 Generator 函数的含义与用法。

这里大家把请求的处理直接写在了代码里面,接下来大家需要把它拆分到 /services/ 里面统一处理

定义Services

之前大家已经:

  1. 设计好了 model state -> 抽象数据
  2. 完善了组件 -> 完善展示
  3. 添加了 Reducers -> 数据同步处理
  4. 添加了 Effects -> 数据异步处理

接下来就是将请求相关(与后台系统的交互)抽离出来,单独放到 /services/ 中,进行统一维护管理,所以大家只需要将之前定义在 Effects 的以下代码,移动到 /services/users.js 中即可:

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

import qs from 'qs';

export async function query(params) {
    return request(`/api/users?${qs.stringify(params)}`);
}

然后在 users model 中引入:

import { query } from '../services/users';

之后无论是更新,删除、添加等操作,跟用户相关的都可以统一放置在 /services/users.js 中。

没错,大家虽然有了接口,但是大家还没有数据,在 dva 中,大家配套的工具能够很方便的模拟数据,这样就可以脱离服务器复杂的环境进行模拟的本地调试开发。下面一节就会一起来看下,如何 mock 数据。

mock数据

大家采用了dora-plugin-proxy工具来完成了大家的数据 mock 功能。

roadhog

如果不用dora-plugin-proxy ,大家可以采用roadhog。Roadhog 是一个包含 dev、build 和 test 的命令行工具,他基于 react-dev-utils,和 create-react-app 的体验保持一致。你可以想象他为可配置版的 create-react-app。

Features

  • ? 开箱即用的 react 应用开发工具,内置 css-modules、babel、postcss、HMR 等
  • ? create-react-app 的体验
  • ? JSON 格式的 webpack 配置
  • ?? mock
  • ? 基于 jest 的 test,包括 UI 测试(基于 enzyme)

遗留问题

目前不知是不是proxy 的问题,导致无法获取到请求到数据。先上传代码。该问题之后有空来解决。

参考的文档上,给出的源码已经更改,我的github上的代码是根据文档来的,但是无法获取到数据。

github地址在这里。

问题报错

app.model: namespace should be defined

遇到的问题查询github的issue解决

参考文献

redux-sagas中文文档

参考文档

推荐阅读更多精彩内容