一、Dva简单介绍
基于 redux 和 redux-saga 的数据流方案.
实际上通俗点的讲法,就是集成了react的一些库,包括react
、react-dom
、react-router-dom
、connected-react-router
、redux
、redux-saga
组件传值:
-
父传子
-
子传父
-
兄弟组件传值(约定最小公约父节点)
一些容易混淆的基本概念的区别
create-react-app
内置了webpack
配置的脚手架roadhog
相当于可配置的create-react-app
umi
=roadhog
+ 路由系统dva
管理数据流工具
二、Dva数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch
发起一个 action
,如果是同步行为会直接通过 Reducers
改变 State
,如果是异步行为(副作用)会先触发 Effects
然后流向 Reducers
最终改变 State
,所以在 dva 中,数据流向非常清晰简明
基本结构
import dva from 'dva'
const app = dva({
//指定给路由用的 history,默认是 hashHistory
history,
//初始化Model中state状态,比Model优先高, 默认 {}
initialState,
// 对应功能钩子函数
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
})
// 执行插件
app.use()
// 存储纯函数 修改数据的方法对象
app.model( required('..model/example.js').defaut ) //
// 路由配置文件,可对路由表以JavaScript对象的形式去配置, 如不要路由可直接返回组件,
app.router( requried('../router').default)
// 挂载启动
app.start('element');
-
model结构
app.model({ namespace: 'count', state: { current: 0, }, reducers: { // getReducer add(state) { const newCurrent = state.current + 1; return { ...state, current: newCurrent, }; }, }, effects: { // react-saga 实现 *add(action, { call, put }) { yield call(delay, 1000); yield put({ type: 'add' }); // yield 的作用:xxxxxxxx }, }, subscriptions: { keyboardWatcher({ dispatch }) { key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) }); }, }, });
*reducer:纯函数,有固定输入输出,主要目的是修改自身state
*effect:一些外部服务,请求
*subscriptions:内部定义的函数都会被被执行 ,执行之后作为监听来处理事务
三、图解
图解一: React表示法
- 多个
Component
之间发生交互,状态(数据)维护在Component
的最小公约父节点上,即<App/>
上
图解二: Redux 表示法
-
React
只负责页面渲染,而不负责页面逻辑 -
我们把页面逻辑单独抽取出来,就是我们需要的
reducer
,加上页面的state
数据,基本上就是store
的框架了 -
通过
dispatch
派发函数的过程是可以被拦截的,所以我们可以在中间加不同的Middleware
实现自定义功能.比如: -
耦合度更低, 复用度更高, 扩展性更好
图解三: 加入Saga
- 点击创建
Todo
的按钮, 发起一个type == addTodo
的action
saga
拦截这个action
, 发起http
请求, 如果请求成功, 则继续向reducer
发一个type == addTodoSucc
的action
, 提示创建成功, 反之则发送type == addTodoFail
的action
即可
图解四: Dva表示法
- 把
store
及saga
统一为一个model
的概念, 写在一个 js 文件里面 - 增加了一个
Subscriptions
, 用于收集其他来源的action
, eg: 键盘操作 model
写法很简约, 类似于DSL
或者RoR
, coding 快得飞起✈️- 约定大于配置 ,比如:
namespace
作为key
RoR
一切皆为对象- 设计的目的: 简化元素,降低难度,让你不用管他怎么实现的,我们按照默认的这个规则去写就可以的
- 约定大于配置 ,比如:
四、源码解读
看源码 – 不同步骤做了什么事情, 简化版
https://github.com/dvajs/dva/blob/45dfa782c4e6f2854fe6e5c6f34fee1b0d6ef151/packages/dva/src/index.js
-
model
function model(model) { let prefixModel = prefixNamespace(model); app._models.push(prefixModel); } /** * 把reducer 对象的属性名 加上 `namespace` * @param {*} m */ function prefixNamespace(m) { let reducers = m.reducers; m.reducer = Object.keys(reducers).reduce((memo, key) => { let newKey = `${m.namespace}/${key}`; memo[newKey] = reducers[key]; return memo; }, {}); return m; }
-
router
function router(routerConfig) { app._router = routerConfig; }
-
start
function start(container) { let reducers = getReducers(app); app._store = createStore(reducers); ReactDOM.render( <Provider store={app._store}>{app._router()}</Provider>, document.querySelector(container) ); } //getReducers
- getReducer
/** * 将所有的model 的reducer 以namespace 为key 整合成新的reducers * { * namespace1:function(state,action){}, * namespace2:function(state,action){} * } * @param {*} app */ function getReducers(app) { let reducers = {}; //用来合并,会传递给combineReducers for (const model of app._models) { // 这里的state 是这个 model 对应的分状态 reducers[model.namespace] = function (state = model.state || {}, action) { let model_reducers = model["reducers"] || {}; // 拿到一个model 的所有reducer let reducer = model_reducers[action.type]; // model_reducers['counter/add'] => model_reducers['add'] if (reducer) { return reducer(state, action); } return state; }; } return combineReducers(reducers); }
- effects
//rootSaga import createSagaMiddleware from "redux-saga"; import * as sagaEffects from "redux-saga/effects"; import { applyMiddleware } from "redux"; let sagaMiddleware = createSagaMiddleware(); // 新建middleware app._store = createStore(reducers, applyMiddleware(sagaMiddleware)); //插入middleware function* rootSaga() { const { takeEvery } = sagaEffects; for (const model of app._models) { const effects = model.effects; for (const key in effects) { //遍历effects yield takeEvery(`${model.namespace}/${key}`, function* (action) { console.log("执行了saga"); yield effects[key](action, sagaEffects); }); } } } sagaMiddleware.run(rootSaga);
- subscriptions
// subscriptions 监听函数全部跑一遍 for (const model of app._models) { if (model.subscriptions) { for (const key in model.subscriptions) { //遍历跑一遍 subscriptions let sub = model.subscriptions[key]; sub({ history, dispatch: app._store.dispatch }); } } }
-
provider 和 connect
参考 https://juejin.cn/post/6844903505191239694
provider 组件将数据与视图联系了起来,生成 React 元素呈现给使用者
//provider // 使用 querySelector 获得 dom if (isString(container)) { container = document.querySelector(container); invariant( container, `[app.start] container ${container} not found`, ); } // 其他代码 // 实例化 store oldAppStart.call(app); const store = app._store; // export _getProvider for HMR // ref: https://github.com/dvajs/dva/issues/469 app._getProvider = getProvider.bind(null, store, app); // If has container, render; else, return react component // 如果有真实的 dom 对象就把 react 拍进去 if (container) { render(container, store, app, app._router); // 热加载在这里 app._plugin.apply('onHmr')(render.bind(null, container, store, app)); } else { // 否则就生成一个 react ,供外界调用 return getProvider(store, this, this._router); } // 使用高阶组件包裹组件 function getProvider(store, app, router) { return extraProps => ( <Provider store={store}> { router({ app, history: app._history, ...extraProps }) } </Provider> ); } // 真正的 react 在这里 function render(container, store, app, router) { const ReactDOM = require('react-dom'); // eslint-disable-line ReactDOM.render(React.createElement(getProvider(store, app, router)), container); }
//connect export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { return function wrapWithConnect(WrappedComponent) { class Connect extends Component { constructor(props, context) { // 从祖先Component处获得store this.store = props.store || context.store this.stateProps = computeStateProps(this.store, props) this.dispatchProps = computeDispatchProps(this.store, props) this.state = { storeState: null } // 对stateProps、dispatchProps、parentProps进行合并 // 合并在一起得到nextState,作为props传给真正的Component this.updateState() } shouldComponentUpdate(nextProps, nextState) { // 进行判断,当数据发生改变时,Component重新渲染 if (propsChanged || mapStateProducedChange || dispatchPropsChanged) { this.updateState(nextProps) return true } } componentDidMount() { // 改变Component的state this.store.subscribe(() = { this.setState({ storeState: this.store.getState() }) }) } render() { // 生成包裹组件Connect return ( <WrappedComponent {...this.nextState} /> ) } } Connect.contextTypes = { store: storeShape } return Connect; } }
### dva-loading
```javascript
import createLoading from "dva-loading";
app.use(createLoading()); // 使用插件
五、hooks
https://dvajs.com/API.html#app-use-hooks
const app = dva({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
});
onError((err, dispatch) => {})
effect 执行错误或 subscription 通过 done 主动抛错时触发,可用于管理全局出错状态
注意:subscription 并没有加 try…catch,所以有错误时需通过第二个参数 done 主动抛错
例子:
app.model({
subscriptions: {
setup({ dispatch }, done) {
done(e)
},
},
})
onAction(fn | fn[])
在action被dispatch时触发,用于注册 redux 中间件。支持函数或函数数组格式
例如我们要通过 redux-logger 打印日志:
import createLogger from 'redux-logger';
const app = dva({
onAction: createLogger(opts),
})
onStateChange(fn)
state
改变时触发,可用于同步 state 到 localStorage,服务器端等
onReducer(fn)
封装 reducer 执行,比如借助 redux-undo 实现 redo/undo :
import undoable from 'redux-undo';
const app = dva({
onReducer: reducer => {
return (state, action) => {
const undoOpts = {};
const newState = undoable(reducer, undoOpts)(state, action);
// 由于 dva 同步了 routing 数据,所以需要把这部分还原
return { ...newState, routing: newState.present.routing };
},
},
})
onEffect(fn)
封装 effect 执行。比如 dva-loading
基于此实现了自动处理 loading 状态
6、 onHmr(fn)
热更新相关,目前用于 babel-plugin-dva-hmr
六、 补充
yeild 具体业务:如 提交报名/创建用户 --1.检查用户名、邮箱、手机号合法 2.存储如数据库
const [result1, result2] = yield all([
call(service1, param1),
call(service2, param2)
])
yeild 与yeild* ,前者是并行,后者是顺序执行。(适用在all的情况下)
yield*
操作符来组合多个 Sagas,使得它们保持顺序。 这让你可以一种简单的程序风格来排列你的 宏观任务(macro-tasks)。
function* playLevelOne() { ... }
function* playLevelTwo() { ... }
function* playLevelThree() { ... }
function* game() {
const score1 = yield* playLevelOne()
yield put(showScore(score1))
const score2 = yield* playLevelTwo()
yield put(showScore(score2))
const score3 = yield* playLevelThree()
yield put(showScore(score3))
}
注意,使用 yield*
将导致该 Javascript 运行环境 漫延 至整个序列。 由此产生的迭代器(来自 game()
)将 yield 所有来自于嵌套迭代器里的值。一个更强大的替代方案是使用更通用的中间件组合机制。