简介
dva首先是一个基于redux和redux-saga的数据流方案,然后为了简化开发体验,dva还额外内置了react-router和fetch,所以也可以理解为一个轻量级的应用框架。[dva = React-Router + Redux + Redux-saga]
最简结构
dva应用的最简结构:
import dva from 'dva';
const App = () => <div>Hello dva</div>;
// 创建应用
const app = dva();
// 注册视图
app.router(() => <App />);
// 启动应用
app.start('#root');
🥷分层开发
主要分层结构主要有以下几点:
Page
负责与用户直接打交道:渲染页面,接受用户的操作输入,侧重于展示型交互性逻辑,这里需要了解无状态组件Model
负责处理业务逻辑,为Page做数据、状态的读写、变换、暂存等,Dva中model就是做了这一层的操作Service
负责与HTTP接口对接,进行纯粹的数据读写
🥷数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过dispatch发起一个action,如果是同步行为会直接通过Reducers改变State,如果是异步行为(副作用) 会先触发Effects然后流向Reducers最终改变State
核心概念:
-
State
:一个对象,保存整个应用状态 -
View
:React 组件构成的视图层 -
Action
:一个对象,描述事件 -
connect
方法:一个函数,绑定State到View
-
dispatch
方法:一个函数,发送Action到State
🥷基础概念
🍪namespace
model的命名空间,同时也是他在全局state上的属性
- 整个应用的State,由多个小的Model的State以namespace为key合成
- 只能用字符串,不支持通过“.”的方式创建多层命名空间,相当于这个model的key
在组件里面,通过connect这个key将想要引入的model加入
import { connect } from 'dva';
export default connect(({namespaceValue}) => ({...namespaceValue}))(DvaComponent);
🍪State
type State = any
表示Model的状态数据
(通常表现为一个javascript对象)- 数据保存在这里,直接决定了视图层的输出
- 操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系
🍪State 和 View
State是储存数据的地方,收到Action以后,会更新数据。
View就是React组件构成的UI层,从State取数据后,渲染成HTML代码。只要State有变化,View就会自动更新
。
🍪Reducer
type Reducer<S, A> = (state: S, action: A) => S
- 必须是纯函数,有固定输入输出,
主要目的是修改自身state
(Action处理器,处理同步动作,用来算出最新的State) - 可以看做是state的计算器。它的作用是根据Action,从上一个State算出当前State
- Reducer函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值
- 在dva中,reducers聚合积累的结果是当前model的state对象。通过actions中传入的值,与当前reducers中的值进行运算获得新的值(也就是新的state)
- 需要注意的是同样的输入必然得到同样的输出,它们不应该产生任何副作用effect。并且,每一次的计算都应该使用immutable data
// count +1
function add(state) { return state + 1; }
// 往 [] 里添加一个新 todo
function addTodo(state, action) { return [...state, action.payload]; }
// 往 { todos: [], loading: true } 里添加一个新 todo,并标记 loading 为 false
function addTodo(state, action) {
return {
...state,
todos: state.todos.concat(action.payload),
loading: false
};
}
...
reducers: {
/** 设置RPC标签列表 */
setRpcLabels(state, { payload: { data } }) {
const {
labelList,
} = data;
return {
...state,
labelList: [...labelList],
};
},
...
}
...
🍪Effect
主要用于异步请求,接口调用之类的(Action处理器,处理异步动作)
- effect被称为副作用,在我们的应用中,最常见的就是异步操作
- 它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出
- dva为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数
function *addAfter1Second(action, { put, call }) {
yield call(delay, 1000);
yield put({ type: 'add' });
}
...
effects: {
/** 获取RPC已有标签列表 */
* getRpcLabels({ payload }, { call, put }) {
const {
produceId, // 模块id
} = payload;
if (produceId) {
const rsp = yield call(service.getLabels, { produceId });
if (rsp) {
yield put({ type: 'setRpcLabels', payload: { data: rsp.data } });
}
return rsp;
}
},
...
},
...
🍪Subscription
- subscription语义是订阅,用于订阅一个数据源,然后
根据条件dispatch需要的action
- 数据源可以是当前的时间、服务器的websocket连接、keyboard输入、geolocation变化、history路由变化等等
- 内部定义的函数都会被被执行,执行之后作为监听来处理事务
import key from 'keymaster';
...
app.model({
namespace: 'count',
subscriptions: {
keyEvent({dispatch}) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
}
});
🍪Action
type AsyncAction = any
- Action是一个普通javascript对象,
它是改变State的唯一途径(是用来描述 UI 层事件的一个对象)
- 无论是从UI事件、网络回调,还是WebSocket等数据源所获得的数据,最终都会通过dispatch函数调用一个action,从而改变对应的数据
action必须带有type属性
指明具体的行为,其它字段可以自定义,如果要发起一个action需要使用 dispatch函数;需要注意的是dispatch是在组件connect Models以后,通过props传入的
dispatch({
type: 'add',
});
🍪dispatch函数
type dispatch = (a: Action) => Action
- dispatch函数是
一个用于触发action的函数(用来将Action发送给State)
,action是改变state的唯一途径,但是它只描述了一个行为,而dipatch可以看作是触发这个行为的方式,Reducer则是描述如何改变数据的 - 在Dva中,connect model的组件通过props可以访问到dispatch, (意思就是:
被connect的Component会自动在props中拥有dispatch方法
) 可以调用Model中的Reducer或者Effects
常见形式:
dispatch({
type: 'user/add', // 如果在model外调用,需要添加namespace
payload: {}, // 需要传递的信息
});
import { connect } from 'dva';
const testCom = props => {
const { dispatch } = props;
const changeValue = (id, val) => {
// 调用reducer,一般是同步修改state中的值
dispatch({
type: 'dva/save',
payload: {
param: val
},
});
// 调用effect,一般是发送后台请求
dispatch({
type: 'dva/queryValue',
payload: {
id: id
},
});
};
return(
<div>'hello world'</div>
)
}
export default connect(({ dva }) => ({ ...dva }))(testCom);
🍪Router
- 这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的
History API
可以监听浏览器url的变化,从而控制路由相关操作。 - dva 实例提供了router方法来控制路由,使用的是react-router
import { Router, Route } from 'dva/router';
app.router(({history}) =>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
);
🍪Route Components
- 在dva中,通常需要 connect Model的组件都是 Route Components,组织在/routes/目录下,而 /components/目录下则是纯组件
🍪connect方法
connect是一个函数,绑定State到View
- connect 方法返回的也是一个React组件,通常称为容器组件。因为它是原始UI组件的容器,即在外面包了一层State。
- connect 方法传入的第一个参数是mapStateToProps函数,mapStateToProps函数会返回一个对象,用于建立State到Props的映射关系。
import { connect } from 'dva';
function mapStateToProps(state) {
return { todos: state.todos };
}
connect(mapStateToProps)(App);
export default connect(({ rpc, global, login }) => ({ rpc, global, login }))(UpdateCaseForm);
最简结构(带model)
dva提供 app.model 这个对象,所有的应用逻辑都定义在它上面:
const app = dva();
// 新增这一行
app.model({ /**/ });
app.router(() => <App />);
app.start('#root');
Model对象的例子:
{
namespace: 'count',
state: 0,
reducers: {
add(state) { return state + 1 },
},
effects: {
*addAfter1Second(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'add' });
},
},
}
dva应用的最简结构(带model):
// 创建应用
const app = dva();
// 注册 Model
app.model({
namespace: 'count',
state: 0,
reducers: {
add(state) { return state + 1 },
},
effects: {
*addAfter1Second(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'add' });
},
},
});
// 注册视图
app.router(() => <ConnectedApp />);
// 启动应用
app.start('#root');
🥷Model中的Effects函数解析
需要注意的是:Effects里面的函数都是Generator函数
🍪Generator 函数
Effect是一个Generator函数,内部使用yield关键字,标识每一步的操作(不管是异步或同步)
* getRpcPageType({ payload }, { call, put }) {
const {
dictType,
} = payload;
if (dictType) {
const rsp = yield call(service.getOptions, { dictType });
if (rsp) {
yield put({ type: 'setRpcPageTypes', payload: { data: rsp.data } });
}
return rsp;
}
},
...
🍪yield
- 固定关键词,Generator函数自带的关键词,和*搭配使用,有点像async和await,使用*则表明它是Generator函数
然后每使用一个yield就是告诉程序这里是异步,需要等待这个后面的代码执行完成
,同步代码可不使用该关键词
🍪payload
- 页面上通过dispatch传过来的payload同名参数
🍪select
- Dva中Effects函数的固定传参
用于拿到model中state的数据
,需要注意的是,state后面跟命名空间namespace的值
const data = yield select((state) => state.namespaceName.valueName);
🍪call
- Dva中Effects函数的固定传参
- 第一个参数是一个异步函数,payload是参数,可以
通过call来执行一个完整的异步请求
,又因为yield的存在,就实现了异步转同步的方案
const { data } = yield call(queryInterface, payload);
🍪put
- Dva中Effects函数的固定传参
发出一个Action,类似于dispatch
- 可以使用同model中的Reducers或者Effects,通过Reducers来实现数据到页面的更新,也可以通过put实现Effects的嵌套使用
yield put({
type: 'save',
payload: {
...payload
},
});
🥷开发目录
由于常见的umi框架已经对Dva做了深度继承,会默认将src/models下的model定义自动挂载
,只需要在model文件夹中新建文件即可新增一个model用来管理组件状态,对于某个page文件夹下面的model也会默认挂载
├─assets `静态资源`
├─components `公共组件`
├─config `路由和环境配置`
├─constants `全局静态常量`
├─locale `国际化`
│ ├─en_US `英文配置`
│ └─zh_CN `中文配置`
├─models `全局数据状态` *Dva涉及的目录*
├─pages `页面目录,用我参与开发的其中一个目录来作为示例` *Dva涉及的目录*
│ ├─NodeConfig `NodeConfig示例目录`
│ │ ├─components
│ │ │ ├─Select `Select组件页面文件` *Dva涉及的目录*
│ │ │ │ └─components
│ │ │ │ ├─AudienceInfo
│ │ │ │ │ ├─index.js
│ │ │ │ │ └─index.less
│ │ │ │ ├─BlackList
│ │ │ │ │ ├─index.js
│ │ │ │ │ └─index.less
│ │ │ │ ├─ControlGroup
│ │ │ │ │ ├─index.js
│ │ │ │ │ └─index.less
│ │ │ │ ├─GroupSelect
│ │ │ │ │ ├─index.js
│ │ │ │ │ └─index.less
│ │ │ │ ├─index.js
│ │ │ │ └─index.less
│ │ ├─models
│ │ │ ├─select.js `Select组件数据状态管理` *Dva涉及的目录*
│ │ └─services
├─services `全局接口配置`
├─themes `全局样式主题`
└─utils `js通用工具`
PS: 该树形图通过 `windows shell` 自带的 `tree` 命令生成
Dva示例
步骤1:首先定义一个简易的model示例
export default {
namespace: 'dva',
state: {
id: '',
value: {},
},
effects: {
// 所有effect前必须要加 *
* queryValue({ payload }, { select, call, put }) {
const params = {
id: payload.id ? payload.id : yield select(state => state.select.id)
}
// queryInterface是定义好的后台请求接口,一般用axios或fetch来完成
const { data } = yield call(queryInterface, params);
yield put({ type: 'save', payload: data });
},
},
reducers: {
save(state, { payload }) {
return {
...state,
...payload,
};
},
},
subscriptions: {
keyboardWatcher({ dispatch }) {
key('⌘+up, ctrl+up', () => { dispatch( {type:'save'}) });
},
},
};
步骤2:然后把model和组件绑定在一起(React的Connect函数是一种柯里化写法)
import { connect } from 'dva';
const testCom = props => {
const { helloWorld = 'hello world'} = props;
return(
<div>{ helloWorld }</div>
)
}
// 绑定之后就可以在testCom组件中使用命名为dva的model了
export default connect(({ dva }) => ({ ...dva }))(testCom);
备注知识点:
1)柯里化
是把接受多个参数的函数转换成接受一个单一参数的函数
// 柯里化
var foo = function(x) {
return function(y) {
return x + y
}
}
foo(3)(4)
// 普通方法
var add = function(x, y) {
return x + y;
}
add(3, 4)
2)无状态组件
创建无状态组件是为了创建纯展示组件,这种组件只负责根据传入的props来展示,不涉及到要改变state状态的操作,在实际项目中页面组件被写成无状态的组件,通过简单组合可以构建成页面或复杂组件,通过多个简单组件来合并成一个复杂的大应用。
优点是:由于是无状态组件,所以无状态组件就不会在有组件实例化的过程,无实例化过程也就不需要分配多余的内存,从而性能得到一定的提升;代码整洁、可读性高,对于大型项目的开发维护非常有好处
const NoStateComponent = props => {
const { helloWorld = 'hello world'} = props;
return(
<div>{ helloWorld }</div>
)
}
export default NoStateComponent;
参考内容
Dva官网:dvajs.com/
DvaJS github地址:github.com/dvajs/dva
Dva.js快速上手指南:blog.csdn.net/PirateRacco…
图解DVA:www.yuque.com/flying.ni/t…
Generator函数详解:blog.csdn.net/weixin_4617…