@umijs/plugin-dva 的相关概念和使用笔记
dva 是一个基于 redux 和 redux-saga 的数据流方案,为了简化开发体验,dva 额外内置了 react-router 和 fetch。@umijs/plugin-dva
目的是能在umi中快速集成dva。
虽然
dva
或redux
目前(2021年)依然是业内React
应用的常用数据流解决方案,但并不代表是最好的解决方案。拿dva
来说,在我看来可能存在以下问题:
Reducer
和Effect
都是修改State
的,区别在于一个同步一个异步且Effect
提供更多语法糖,并且Reducer
的实现基本都是模板化的(即多个Model
基本就是复制粘贴),而这样的做法仅仅只是为了适应React
单一方向数据流思想而已。- 在调用
Reducer
或Effect
时,只能通过字符串来表明需要调用的方法以及该方法所在的命名空间,而使用字符串的缺点显而易见。- 由于触发调用
Reducer
或Effect
时只能通过构造Action
来完成,但是Action
的结构是非常奇怪的,原因是由于Action
必须具有type
属性,导致我们需要将载荷数据放在Action
的某个字段内(通常是payload
),否则无法在载荷数据中携带type
属性。这样对于老手来说可能不是问题,但是对于新手来说很容奇产生问题,因为这必须依赖经验(如载荷放到payload
内)来避免问题。- 实际应用中,
Subscription
几乎不会用到,原因是由于Subscription
方法会在服务端和客户端都会执行,且它产生的数据是通过Props
传递的,这就必须要求它产生的数据只能是“固定”的,否则在开启SSR
时会导致服务端渲染得到结果和客户端渲染的不同从而导致React报错。退一步讲,这里假设是React的bug,那Subscription
在客户端和服务端都执行也是不合理的。除了
dva
和redux
,数据流框架还有很多,在这里推荐使用RxJs。
数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch
发起一个 action,如果是同步行为会直接通过 Reducers
改变 State
,如果是异步行为(副作用)会先触发 Effects
然后流向 Reducers
最终改变 State
,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。
Model
namespace: model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持多层命名空间
state: 存储数据,类似于React的State
reducers: 以 key/value 格式定义 reducer。用于处理同步操作,唯一可以修改(由 action
触发) state
的地方。
effects: 以 key/value 格式定义 effect。用于处理异步操作和业务逻辑,不直接修改 state
。由 action
触发,可以触发 reducer
,可以和服务器交互,可以获取全局 state
的数据等等
subscriptions: 以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。在 app.start()
时被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等
// 示例 models/products.js
export default {
// 命名空间
namespace: 'index',
// 保存数据
state: { name: '' } as State,
// 唯一修改 state 的地方
reducers: {
// 没启用 immer 之前
// save(state, action) {
// return { ...state, ...action.payload };
// },
// 启用 immer 之后
save(state: State, action: AsyncAction) {
state.name = action.name;
},
} as { save: ImmerReducer<IndexModelState>; }, // 启用 immer 之前为 Reducer<IndexModelState>
effects: {
* query(action: AsyncAction, effectsCommandMap: EffectsCommandMap) { /* ... */ },
* addRemote({ name }, { put, call }) {
yield call(addTodo, name);
yield put({ type: 'name', name });
},
} as { query: Effect; addRemote: Effect; },
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname }: any) => {
if (pathname === '/') {
// 在 model 内调用,不需要添加 namespace
dispatch({ type: 'query', });
}
});
},
} as { setup: Subscription }
};
State
type State = any
State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。
Action
type AnyAction = {
// type 的值为 reducers 或者 effects 中的方法名,格式为: namespace/name
type: string;
// 允许在 AnyAction 中定义任何额外的属性。
[extraProps: string]: any;
}
Action 是一个普通 javascript 对象,用于指明需要进行的操作(通过type
属性)以及放该操作中需要用到的数据(可以自定义其他字段)。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过dispatch
函数发起一个 action,从而改变对应的数据。需要注意的是 dispatch
是在组件 connect Models以后,通过 props 传入的。
// 示例
dispatch({ type: 'index/save', name: 'Laeni' });
dispatch 函数
type dispatch = (a: Action) => Action
dispatch 函数用于发射 action,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。
在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如:
dispatch({
type: 'user/add', // 如果在 model 外调用,需要添加 namespace
...{ /* 需要传递的信息 */ }
});
Reducer
type Reducer<S = any, A extends Action = AnyAction> =
(state: S | undefined, action: A) => S;
// 这是采用了 immer 框架,即使在此处直接修改 state 也不会违反 Reducer 的规则
type ImmerReducer<S = any, A extends Action = AnyAction> =
(state: S, action: A) => void;
Reducer(也称为 reducing function)函数接受两个参数:state 为该model改变前的数据,action 为前面所讲的一个行为描述,返回的是一个新的累积结果。即该函数把 state 和 action 合并成一个新 state。
需要注意的是 Reducer 必须是纯函数,所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。并且,每一次的计算都应该使用不可变数据(immutable data),这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。
Effect
type Effect = (action: AnyAction, effects: EffectsCommandMap) => void;
Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。
dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数。至于为什么我们这么纠结于纯函数,如果你想了解更多可以阅读Mostly adequate guide to FP,或者它的中文译本JS函数式编程指南。
EffectsCommandMap
-
actionChannel: ƒ actionChannel(pattern, buffer)
-
all: ƒ all(effects)
-
apply: ƒ apply(context, fn)
-
call: ƒ call(fn) - 用于调用异步逻辑,支持Promise
const result = yield call(fetch, '/todos');
这个call与JS的call用法大概一致,第一个参数是要调用的函数,第二个参数开始是要传递给被调函数的参数,可传递多个。
-
cancel: ƒ cancel()
-
cancelled: ƒ cancelled()
-
cps: ƒ cps(fn)
-
flush: ƒ flush(channel)
-
fork: ƒ fork(fn)
-
getContext: ƒ getContext(prop)
-
join: ƒ join()
-
put: ƒ put(action) - 用于触发action
yield put({ type: 'todos/add', payload: 'Learn Dva'});
-
race: ƒ race(effects)
-
select: ƒ select(selector) - 用于从state里获取数据
const todos = yield select(state => state.todos);
-
setContext: ƒ setContext(props)
-
spawn: ƒ spawn(fn)
-
take: ƒ take(type)
-
takeEvery: ƒ takeEvery(patternOrChannel, worker)
-
takeLatest: ƒ takeLatest(patternOrChannel, worker)
-
takem: ƒ ()
-
throttle: ƒ throttle(ms, pattern, worker)
Subscription
Subscriptions 是一种从 源 获取数据的方法,它来自于 elm。
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'}) });
},
}
});
umi 接口
常用方法可从 umi 直接 import。
比如:import { connect } from 'umi';
接口包含:
connect
绑定数据到组件。
getDvaApp
获取 dva 实例,即之前的 window.g_app
。
useDispatch
hooks 的方式获取 dispatch,dva 为 2.6.x 时有效。
useSelector
hooks 的方式获取部分数据,dva 为 2.6.x 时有效。
useStore
hooks 的方式获取 store,dva 为 2.6.x 时有效。
类型
通过 umi 导出类型:ConnectRC
,ConnectProps
,Dispatch
,Action
,Reducer
,ImmerReducer
,Effect
,Subscription
,和所有 model
文件中导出的类型。
其他
immer.js
Immer 是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对JS不可变数据结构的需求。
简单使用和介绍见: https://segmentfault.com/a/1190000017270785
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
在组件设计方法中,我们提到过 Container Components,在 dva 中我们通常将其约束为 Route Components,因为在 dva 中我们通常以页面维度来设计 Container Components。
所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在/routes/
目录下,而/components/
目录下则是纯组件(Presentational Components)。