dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
简单来说 Dva 类似于 Redux 之于 React,二者可以更好的搭配,通过 Dva 管理状态,可以分离 JSX 组件中的大量状态控制,使代码更加清爽易读。
比起 redux-saga,减少了大量的文件分离,只用一个 model 文件就可以定义定义一个状态中心。
初始化
现在 Dva 已经集成到了 Umi(本文使用 Umi4) 内部,需要在 Umi 的配置文件中打开 Dva 的选项(直接添加配置是会报错的,需要通过微生成器添加,往下看)
umi: {}
使用 Umi 脚手架创建项目,以下三种方式任选其一,推荐使用 pnpm,选择标准项目即可。
$ pnpm dlx create-umi@latest
$ npx create-umi@latest
$ yarn create umi
创建项目之后使用 umi 的微生成器来初始化 dva
$ pnpm umi g dva
// 如果是本地全局安装的 umi
$ umi g dva
然后 .umirc.ts 配置文件中会自动生成 dva 的相关配置
export default defineConfig({
npmClient: "pnpm",
dva: {},
plugins: ["@umijs/plugins/dist/dva"],
});
并且生成 model 文件
现在 dva 已经初始化完成了,我们继续来看 dva 的使用
工作流
dva 的工作流如下:
数据统一在
src/models
中的 model 管理,组件内尽可能的不去维护数据,而是通过 connect 去关联 model 中的数据。页面有操作的时候则触发一个 action 去请求后端接口以及修改 model 中的数据,将业务逻辑分离到一个环形的闭环中,使得数据在其中单向流动。让应用更好维护。这样的思想最初来源于 Facebook 的 flux。接下来我们来具体看看如何在 Umi 中实现这样的逻辑。
models
Umi 会默认将 src/models
下的 model 定义自动挂载,你只需要在 model 文件夹中新建文件即可新增一个 model 用来管理组件状态。
需要注意的是 model 的 namespace 是全局的,你仍然需要保证你的 namesapce 唯一(默认是文件名)
一个 model 中可以定义如下几个部分:
- namespace : model 的命名空间,唯一标识一个 model,如果与文件名相同可以省略不写
- state : model 中的数据
- effects : 异步 action,用来发送异步请求
- reducers : 同步 action,用来修改 state
connect
connect 用于将model 和组件关联在一起,会将 state 和dispatch 添加到组件的 props 中。
如果你熟悉 redux,这个 connect 就是 react-redux 的 connect 。
我们使用初始化时生成的 model 来做示例,注意要将示例的 add action添加返回值,否则调用之后会报错。
import { connect } from "umi"
export function CountPage(props: any) {
const { loading, dispatch, count: {num} } = props
const addHandler = () => {
dispatch({
type: 'count/addAsync'
})
}
return <div>
{num}
<button onClick={addHandler}>+</button>
</div>
}
export default connect(
({ loading, count }: any) => ({ loading, count })
)(CountPage)
框架会默认添加一个命名空间为 loading 的 model,该 model 包含 effects 异步加载 loading 的相关信息,它的 state 格式如下:
{ global: Boolean, // 是否真正有异步请求发送中 models: { [modelnamespace]: Boolean, // 具体每个 model 的加载情况 }, effects: { [modelnamespace/effectname]: Boolean, // 具体每个 effect 的加载情况 }, }
函数组件的使用方式大致如上所述,类组件的使用方式更加简洁,使用装饰器语法可以将 dva 进行注入。
@connect(({ count }) => ({ count }))
class CountPage extends React.Component {
constructor(props: any) {
super(props);
}
addHandler() {
this.props.dispatch({
type: 'count/addAsync'
})
}
render() {
return (
<div>
{this.props.count.num}
<button onClick={() => this.props.dispatch({
type: 'count/addAsync'
})}>+</button>
</div>
)
}
}
export default CountPage
但是目前对 TS 的语法检查支持不是很友好,会有一些爆红,此外,model 中 action 的返回值一定要是一个新的值,也就是说如果 state 是一个对象类型不能直接返回,引用地址相同 dva 会认为没有改变,从而导致页面不会更新。
定义 model 核心
namespace
namespace是 model 的唯一标识,通过 namespace 来操作不同的 model,namespace 必须唯一,否则会导致数据混乱
state
State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。
定义的 state就是 model 的初始数据,可以在初始化时拿到。
state: {
num: 0,
time: 1000,
},
reducer
reducer 是一个函数,用来处理修改数据的逻辑(同步,不能请求后端)。接受 state 和 action,返回老的或新的 state 。即:
(state, action) => state
。
在 model 中,reducer 用于修改数据,在异步请求之后,或者任意 action 触发都可以调用 reducer 来更新 state
reducers: {
add(state, { payload: todo }) {
return state.concat(todo);
},
remove(state, { payload: id }) {
return state.filter(todo => todo.id !== id);
},
update(state, { payload: updatedTodo }) {
return state.map(todo => {
if (todo.id === updatedTodo.id) {
return { ...todo, ...updatedTodo };
} else {
return todo;
}
});
},
},
effect
effect 可以进行异步操作,基于生成器函数来完成一步流程,这一点也是继承自 redux-saga
effects: {
*addAsync(_action: any, { put, call, select }: any) {
const time = yield select(state => state.time)
yield call(delay, time);
yield put({ type: 'add' });
},
},
effect 中定义的异步 action接收两个参数,第一个参数是 Action,通常用于接收 payload;第二个参数是 dva 提供的函数 select(从 state 中获取数据)、put(触发 action)、call(执行异步函数)
dispatch
dispatch 用于model 外部触发 model 内定义的 action(effect和 reducer 都属于 action),在外部触发时需要添加 namespace,如 model 中定义了名为 getData 的 effect,那么在 model 外部触发时需要使用如下方式
dispatch({
type: 'count/getData',
payload: {
page: 1,
size: 10
}
})