在公司使用Antd时接触到了dva,它是由阿里架构师 sorrycc 带领 team 完成的一套前端框架,在作者的 github 里是这么描述它的:“dva 是 react 和 redux 的最佳实践”。dva 是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。简单的说 dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装。dva 是 react 和 redux 的最佳实践。最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起。Dva = React-Router + Redux + Redux-saga。Dva官网地址
Dva快速上手
dva的安装和启动只需四个步骤,整合antd地址
// 1.安装dva-cli
npm install dva-cli -g
// 2.创建项目:dva-test项目名
dva new dva-test
// 3.进入dva-test目录
cd dva-quickstart
// 4.启动项目
npm start
启动成功后访问 http://localhost:8000/, 会出现如下界面
react项目的推荐目录结构,如果使用dva脚手架创建,则自动生成如下
|── /mock/ # 数据mock的接口文件
|── /src/ # 项目源码目录(我们开发的主要工作区域)
| |── /components/ # 项目组件(用于路由组件内引用的可复用组件)
| |── /routes/ # 路由组件(页面维度)
| | |── route1.js
| | |── route2.js # 根据router.js中的映射,在不同的url下,挂载不同的路由组件
| | └── route3.js
| |── /models/ # 数据模型(可以理解为store,用于存储数据与方法)
| | |── model1.js
| | |── model2.js # 选择分离为多个model模型,是根据业务实体进行划分
| | └── model3.js
| |── /services/ # 数据接口(处理前台页面的ajax请求,转发到后台)
| |── /utils/ # 工具函数(工具库,存储通用函数与配置参数)
| |── router.js # 路由配置(定义路由与对应的路由组件)
| |── index.js # 入口文件
| |── index.less
| └── index.html
|── package.json # 项目信息
└── proxy.config.js # 数据mock配置
Dva 概念
数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch
发起一个 action,如果是同步行为会直接通过 Reducers
改变 State
,如果是异步行为(副作用)会先触发 Effects
然后流向 Reducers
最终改变 State
,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。
从图中可以看出dva是设计理念是把数据层和组件层剥离开来,实现完全解耦。简单的分析一下这个图:
首先我们根据 url
访问相关的 Route-Component
,在组件中我们通过 dispatch
发送 action
到 model
里面的 effect
或者直接 Reducer
当我们将action
发送给Effect
,基本上是取服务器上面请求数据的,服务器返回数据之后,effect
会发送相应的 action
给 reducer
,由唯一能改变 state
的 reducer
改变 state
,然后通过connect
重新渲染组件。当我们将action
发送给reducer
,那直接由 reducer
改变 state
,然后通过 connect
重新渲染组件。这样我们就能走完一个流程了。
Models
State
State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。在 dva 中你可以通过 dva 的实例属性 _store
看到顶部的 state 数据,但是通常你很少会用到:
const app = dva();
console.log(app._store); // 顶部的 state 数据
Action
action
的格式如下,它需要有一个 type
,表示这个 action
要触发什么操作;payload
则表示这个 action
将要传递的数。action 必须带有 type
属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch
函数;需要注意的是 dispatch
是在组件 connect Models以后,通过 props 传入的。
dispatch({
type: 'add',
});
具体可以查看文档:redux——action
Dispatch 函数
dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如:
dispatch({
type: 'user/add', // 如果在 model 外调用,需要添加 namespace
payload: {}, // 需要传递的信息
});
Reducer
Reducer(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。Reducer 的概念来自于是函数式编程,很多语言中都有 reduce API。如在 javascript 中:
[{x:1},{y:2},{z:3}].reduce(function(prev, next){
return Object.assign(prev, next);
})
//return {x:1, y:2, z:3}
在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。需要注意的是 Reducer 必须是纯函数,所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。并且,每一次的计算都应该使用immutable data,这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。
Effect
Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。
dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数。至于为什么我们这么纠结于 纯函数,如果你想了解更多可以阅读Mostly adequate guide to FP,或者它的中文译本JS函数式编程指南。其中它用到了redux-saga,里面有几个常用的函数。
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
put
yield put({ type: 'minus' });
call 用于异步调用逻辑,支持Prommise
const result = yield call(fetch, '/todos');
select 用于从 state 中获取数据
const todos = yield call(state, state.todos);
简单使用例子
构建service
新增文件 src/services/users.js
,内容如下,这里写了用户的api接口列表,即用户增删改查
import request from '../utils/request';
//查询
export function fetch({ page }) {
return request(`/api/users?_page=${page}&_limit=$5`);
}
//删除
export function remove(id) {
return request(`/api/users/${id}`, {
method: 'DELETE',
});
}
//修改
export function patch(id, values) {
return request(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(values),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
}
//新增
export function create(values) {
return request('/api/users', {
method: 'POST',
body: JSON.stringify(values),
});
}
构建model
可在终端通过命令 dva g model users
生成文件,也可手动创建,然后修改 src/models/users.js
import fetch as usersService from '../services/users';
export default {
namespace: 'example', //表示对于整个应用不同的命名空间,以便通过this.props.example访问,和当前model文件名相同就好之前的reducer名字相同,是全局state的属性,只能为字符串,不支持.的方式建立多重
state: {initText:"hello"}, //表示当前的example中的state状态,这里可以给初始值
subscriptions: {
setup({ dispatch, history }) { // 订阅,可以监听服务器连接,键盘输入,路由,状态等的变化
},
},
effects: {
*test1({ payload }, { call, put }) {
//payload是从组件router传递过来的参数,
//这里的call方法可以使用payload参数传递给后台程序进行处理这里可以调用service层的方法进行调用后端程序,
//这里的put表示存储在当前命名空间example中,通过save方法存在当前state中
const response = yield call(fetch , payload);
yield put({
type: 'saveTest',
payload: response
});
},
},
//用来保存更新state值 上面的put方法调用这里的方法
reducers: {
saveTest(state, payload) {
return { ...state, ...payload };
},
},
};
上面的model是需要注册到src/index.js中的app.router(require(‘./router’).default);否则后面和route组件无法用connect传递数据,如果定义了多个model,那么需要使用多个app.model()来传递model数据;
import dva from 'dva';
import './index.css';
// 1. Initialize
const app = dva();
// 2. Plugins
// app.use({});
// 3. Model
app.model(require('./models/example').default);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root');
构建components
前面的model的demo中定义的example.js中定义了state数据,那么在route的路由组件中怎么获取到这个数据呢?通过connect可以传递过来,然后通过this.props就可以访问了,同样也会把dispatch(可以发送请求到model去),history方法传递过来,这样就可以通过组件获取到model保存的值了。这里有三种写法
方式一
import React from "react"; import {Component} from 'react'; import { connect } from "dva"; //从dva中导入connect class Counter extends Component { constructor(props){ super(props) } render (){ return ( <div> <p> this.props.example.initText</p> //如这里就获取到了上面定义的initText数据了 </div> ) } } const mapStateToProps = (state) =>{ return { example:state.example, //这里的example表示后面用this.props.example获取state(根节点)中exmpale命名空间(model的example.js中的state所有数据)的数据 } } export default connect (mapStateToProps)(Counter) //通过这种方式来把model层的数据传递到当前组件了
方式二
import React from "react"; import {Component} from 'react'; import { connect } from "dva"; //从dva中导入connect class Counter extends Component { constructor(props){ super(props) } render (){ return ( <div> <p> this.props.example.initText</p> //如这里就获取到了上面定义的initText数据了 </div> ) } } export default connect ({example})(Counter) //通过这种方式来把model层的数据传递到当前组件了,默认这面的也是example属性,通过this.props.example可以获取到model(example.js)中state的数据了
方式三
采用ES6注解的形式传递值,效果也是一样的
import React from "react"; import {Component} from 'react'; import { connect } from "dva"; //从dva中导入connect @connect({example}) class Counter extends Component { constructor(props){ super(props) } render (){ return ( <div> <p> this.props.example.initText</p> //如这里就获取到了上面定义的initText数据了 </div> ) } }