虽然 Umi 中的 dva 已经不是官方推荐的最好的数据流管理方案了,但是学习 dva 的时候,其实更有利于我们后面熟悉纯 hooks 的数据流管理方案。
在 Umi 中我们对请求方法做了高效的封装,对开发中遇到的请求相关的服务都做了内置功能。
比如 mock 数据、请求代理、统一请求地址配置、接口文件组织等都有鲜明的 Umi 风格。
在接下来的几个课程中,我们会详细的说明,Umi 在数据获取方面提供的能力和服务。
Umi 将如何帮助你高效的完成数据获取和绑定的工作。
学完这节课您将会掌握 dva 的基本入门。
dva
什么是 dva?
dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
通常在 dva 的项目中,你需要掌握 (6 个 API](dvajs.com/guide/conce… redux 的概念已经很少了,但是在 Umi 项目中,你需要掌握的 API 数是 0。 即一个也不需要掌握。因为在 Umi 中通过约定的方式组织代码,框架自动完成了相应 API 的执行。
比如,在 src/models
中新建文件,就会被自动使用 app.model
绑定到 dva 中。
为什么要用 dva
经过一段时间的自学或培训,大家应该都能理解 redux 的概念,并认可这种数据流的控制可以让应用更可控,以及让逻辑更清晰。
但随之而来通常会有这样的疑问:概念太多,并且 reducer, saga, action 都是分离的(分文件)。
这带来的问题是:
- 编辑成本高,需要在 reducer, saga, action 之间来回切换
- 不便于组织业务模型 (或者叫 domain model) 。比如我们写了一个 userlist 之后,要写一个 productlist,需要复制很多文件。
还有一些其他的:
- saga 书写太复杂,每监听一个 action 都需要走 fork -> watcher -> worker 的流程
- entry 书写麻烦
- ...
而 dva 正是用于解决这些问题。
什么时候需要使用 dva
在 react hooks 上线之后。在 Umi 项目中,我们建议轻量的使用 dva。仅仅在以下几种场景下推荐使用 dva:
- 父子组件之间的数据互通
- 多页面之间的数据传递(即,公共数据)
- 非 react 组件的场景
dva 入门课
::: tip 内容来自之前为阿里内部同学准备的入门课。根据 Umi 中的使用情况,对无需关注的概念做了删减。 :::
React 没有解决的问题
React 本身只是一个 DOM 的抽象层,使用组件构建虚拟 DOM。
如果开发大应用,还需要解决一个问题。
- 通信:组件之间如何通信?
- 数据流:数据如何和视图串联起来?路由和数据如何绑定?如何编写异步逻辑?等等
通信问题
组件会发生三种通信。
- 向子组件发消息
- 向父组件发消息
- 向其他组件发消息
React 只提供了一种通信手段:传参。对于大应用,很不方便。
组件通信的例子
步骤 1
class Son extends React.Component { render() { return <input />; } } class Father extends React.Component { render() { return ( <div> <Son /> <p>这里显示 Son 组件的内容</p> </div> ); } } ReactDOM.render(<Father />, mountNode); 复制代码
看这个例子,思考一下父组件如何拿到子组件的值。
步骤 2
class Son extends React.Component { render() { return <input onChange={this.props.onChange} />; } } class Father extends React.Component { constructor() { super(); this.state = { son: '', }; } changeHandler(e) { this.setState({ son: e.target.value, }); } render() { return ( <div> <Son onChange={this.changeHandler.bind(this)} /> <p>这里显示 Son 组件的内容:{this.state.son}</p> </div> ); } } ReactDOM.render(<Father />, mountNode); 复制代码
看下这个例子,看懂源码,理解子组件如何通过父组件传入的函数,将自己的值再传回父组件。
数据流图
核心概念
- State:一个对象,保存整个应用状态
- View:React 组件构成的视图层
- Action:一个对象,描述事件
- connect 方法:一个函数,绑定 State 到 View
- dispatch 方法:一个函数,发送 Action 到 State
State 和 View
State 是储存数据的地方,收到 Action 以后,会更新数据。
View 就是 React 组件构成的 UI 层,从 State 取数据后,渲染成 HTML 代码。只要 State 有变化,View 就会自动更新。
Action
Action 是用来描述 UI 层事件的一个对象。
{ type: 'click-submit-button', payload: this.form.data } 复制代码
connect 方法
connect 是一个函数,绑定 State 到 View。也支持高阶函数的用法。
import { connect } from 'dva'; function mapStateToProps(state) { return { todos: state.todos }; } connect(mapStateToProps)(App); 复制代码
connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。
connect 方法传入的第一个参数是 mapStateToProps 函数,mapStateToProps 函数会返回一个对象,用于建立 State 到 Props 的映射关系。
dispatch 方法
dispatch 是一个函数方法,用来将 Action 发送给 State。
dispatch({ type: 'click-submit-button', payload: {}, }); 复制代码
dispatch 方法从哪里来?被 connect 的 Component 会自动在 props 中拥有 dispatch 方法。
connect 的数据从哪里来? connect 方法传入的第一个参数是 mapStateToProps 函数,该函数默认传入一个参数 state 对应了整个应用的 state,你可以通过设置映射关系,将 state 中的某个值,绑定到页面组件的 props 里面。
数据流图
model 最简结构
export default { namespace: 'count', state: 0, reducers: { add(state) { return state + 1; }, }, effects: { *addAfter1Second(action, { call, put }) { yield call(delay, 1000); yield put({ type: 'add' }); }, }, }; 复制代码
Model 对象的属性
- namespace: 当前 Model 的名称。整个应用的 State,由多个小的 Model 的 State 以 namespace 为 key 合成
- state: 该 Model 当前的状态。数据保存在这里,直接决定了视图层的输出
- reducers: Action 处理器,处理同步动作,用来算出最新的 State
- effects:Action 处理器,处理异步动作
Reducer
Reducer 是 Action 处理器,用来处理同步操作,可以看做是 state 的计算器。它的作用是根据 Action,从上一个 State 算出当前 State。
一些例子:
// 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, }; } 复制代码
Effect
Action 处理器,处理异步动作,基于 Redux-saga 实现。Effect 指的是副作用。根据函数式编程,计算以外的操作都属于 Effect,典型的就是输入输出操作,全局 dom 变化,访问服务端数据等。
function* addAfter1Second(action, { put, call }) { yield call(delay, 1000); yield put({ type: 'add' }); } 复制代码
Generator 函数
Effect 是一个 Generator 函数,内部使用 yield 关键字,标识每一步的操作(不管是异步或同步)。
call 和 put
dva 提供多个 effect 函数内部的处理函数,比较常用的是 call
和 put
。
- call:执行异步函数
- put:发出一个 Action,相当于 View 里面的 dispatch
到这里,我们就将 dva 的基本概念讲解清楚了,如果你不是很理解,建议你多看几遍。如果你稍微有了一点概念,那就可以在后续的课程中慢慢掌握以上所有概念。这样你会更加清楚的了解到如何在项目中使用 dva。
感谢阅读,今天不需要写任何的代码,只需要简单的搞懂 dva 的概念即可。我们会在后续手动创建 dva 插件来完成上述提到的,为什么 dva 中的 6 个概念,正在的业务开发中可以一个都不用掌握。