dva实现原理及自定义实现
# 原生dva实现
create-react-app demo
npm start
1
2
2
基本计数器项目
index.js
import React from 'react';
import dva, { connect } from 'dva'
// 1. Initialize
const app = dva()
// 2. Plugins
// app.use({});
// 3. Model
// app.model(require('./models/example').default);
app.model({
namespace: 'counter',
state: { number: 0 },
reducers: {
//接收老状态,返回新状态
add(state) {
//dispatch({type:'add'});
return { number: state.number + 1 }
},
minus(state) {
//dispatch({type:'minus'})
return { number: state.number - 1 }
}
}
})
const Counter = connect(state => state.counter)(props => (
<div>
<p>{props.number}</p>
<button onClick={() => props.dispatch({ type: 'counter/add' })}>+</button>
<button onClick={() => props.dispatch({ type: 'counter/minus' })}>-</button>
</div>
))
// 4. Router
// app.router(require('./router').default);
app.router(() => <Counter />)
// 5. Start
app.start('#root')
// ReactROM.render(() => <Counter />, document.querySelector('#root'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 手写实现
# demo引入
import React from 'react';
// import dva, { connect } from 'dva'
import dva, { connect } from './dva'
// 1. Initialize
const app = dva()
console.log(app);
1
2
3
4
5
6
7
2
3
4
5
6
7
# 初始化项目
create-react-app dva
cd dva
npm i -S dva redux react-redux react-saga react-router-dom react-router-redux history
npm start
#注意history用4.x版本,不要用5.0版本;npm i -S history@4.10.1
1
2
3
4
5
2
3
4
5
# 实现reducers同步
要点:
- let reducer = combineReducers(reducers) //合并reducers
- let store = createStore(reducer) //创建仓库
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'
import { Provider, connect } from 'react-redux'
const NAMESPACE_SEPERATOR = '/'
export { connect }
//https://github.com/dvajs/dva/blob/master/packages/dva-core/src/prefixNamespace.js
function prefix(reducers, namespace) {
// return Object.keys(reducers).reduce((memo, key) => {
// const newKey = `${namespace}${NAMESPACE_SEPARATOR}${key}`
// memo[newKey] = reducers[key]
// return memo
// }, {})
let newReducers = {}
for (const key in reducers) {
const newKey = `${namespace}${NAMESPACE_SEPARATOR}${key}`
newReducers[newKey] = reducers[key]
}
return newReducers
}
export default function () {
let app = {
model,
_models: [], // 存储传入的model
router,
_router: null, // 存储传入的路由配置
start
}
function model(model) {
app._models.push(model)
}
function router(routeConfig) {
app._router = routeConfig
}
// 启动渲染
function start(root) {
let reducers = {}
// 实现reducer同步功能
for (const model of app._models) {
let { namespace, state: initState, reducers: modelReducers } = model
let reducerWithPrefix = prefix(modelReducers, namespace)
// 新方式
reducers[namespace] = function (state = initState, action) {
let reducer = reducerWithPrefix[action.type] //type = "counter/add"
if (reducer) {
return reducer(state, action) //如匹配到reducer就处理
}
return state //没有匹配到的话,返回老状态
}
// 老方式
// reducers[model.namespace] = function (state = model.state, action) {
// let actionType = action.type
// const [namespace, type] = actionType.split(NAMESPACE_SEPERATOR)
// if (namespace === model.namespace) {
// let reducer = model.reducers[type]
// if (reducer) {
// return reducer(state, action) //如匹配到reducer就处理
// }
// }
// return state
// }
}
let reducer = combineReducers(reducers) //合并reducers
let store = createStore(reducer) //创建仓库
let App = app._router //获取要渲染的组件
//执行渲染
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector(root)
)
}
return app
}
/**
combineReducers({
a:function(state, action){},
b:function(state, action){},
})
{
a:xxx,
b:yyy,
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# 实现effects异步
要点:
- yield effects.takeEvery
- createStore(reducer, applyMiddleware(sagaMiddleware)) //创建仓库
- sagaMiddleware.run(rootSaga)
demo/index.js
+ function delay(ms) {
+ return new Promise((resolve,reject) => {
+ setTimeout(function () {
+ resolve();
+ },ms);
+ });
+ }
+ effects:{
+ *asyncAdd(action,{call,put}){
+ yield call(delay,1000);
+ yield put({type:'counter/add'});
+ }
+ }
+ <button onClick={()=>props.dispatch({type:"counter/asyncAdd"})}>异步+</button>
+//index.js:1 Warning: [sagaEffects.put] counter/add should not be prefixed with namespace counter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
src/dva/index.js
+import {createStore,combineReducers,applyMiddleware} from 'redux';
import {connect,Provider} from 'react-redux';
+import createSagaMiddleware from 'redux-saga';
+import * as sagaEffects from 'redux-saga/effects';
let rootReducer = combineReducers(reducers);
+ let sagaMiddleware = createSagaMiddleware();
+ function* rootSaga(){
+ let {takeEvery} = sagaEffects;
+ for(const model of app._models){
+ for(const key in model.effects){
+ yield takeEvery(model.namespace+'/'+key,function*(action){
+ yield model.effects[key](action,sagaEffects);
+ });
+ }
+ }
+ }
+ let store = createStore(rootReducer,applyMiddleware(sagaMiddleware));
+ sagaMiddleware.run(rootSaga);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 实现路由功能
demo/index.js
import React from 'react';
import dva,{connect} from 'dva';
+import {Router,Route} from 'dva/router';
+const Home = ()=><div>Home</div>
+app.router(({history})=>(
+ <Router history={history}>
+ <>
+ <Route path="/" exact={true} component={Home}/>
+ <Route path="/counter" component={Counter}/>
+ </>
+ </Router>
+));
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
src/index.js
+import {createHashHistory} from 'history';
+ let history = createHashHistory();
+ let App = app._router({history,app});
+ ReactDOM.render(<Provider store={store}>{App}</Provider>,document.querySelector(root));
1
2
3
4
2
3
4
src\dva\router.js
export * from 'react-router-dom';
1
# 实现跳转功能
demo/index.js
+ import {Router,Route,routerRedux} from './dva/router';
+ *goto({to}, { put }) {
+ yield put(routerRedux.push(to));
+ }
<button onClick={()=>props.dispatch({type:"counter/goto",to:'/'})}>跳转到/</button>
1
2
3
4
5
2
3
4
5
src/index.js
react-router-redux改用【connected-react-route】
+ let history = createHashHistory();
+ const reducers = {
+ router: connectRouter(history)
+ };
+ let store = createStore(rootReducer,applyMiddleware(routerMiddleware(history),sagaMiddleware));
sagaMiddleware.run(rootSaga);
+ let App = app._router({history,app});
+ ReactDOM.render(
+ <Provider store={store}>
+ <ConnectedRouter history={history}>
+ {App}
+ </ConnectedRouter >
+ </Provider>,document.querySelector(root));
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
src\dva\router.js
+module.exports = require('react-router-dom');
+module.exports.routerRedux = require('connected-react-router');
1
2
2
# 实现自定义effects功能
demo/index.js
// 自定义effects示范; 加完三次后自动断开;
addWatchers: [
function* ({ take, put, call }) {
for (let i = 0; i < 3; i++) {
const { payload } = yield take('counter/addWatcher')
yield call(delay, 1000)
yield put({ type: 'counter/add', payload })
}
alert('不能再加了')
},
{ type: 'watcher' }
]
<br />
<button onClick={() => props.dispatch({ type: 'counter/addWatcher', payload: 2 })}>自定义异步+2</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
src/index.js
for (const key in model.effects) {
let effect = model.effects[key]
if (!Array.isArray(effect)) {
yield takeEvery(`${model.namespace}${NAMESPACE_SEPERATOR}${key}`, function* (action) {
yield model.effects[key](action, sagaEffects)
})
} else {
const [func, typeObj] = effect // 支持自定义effects
if (typeObj.type === 'watcher') {
yield sagaEffects.fork(func, sagaEffects)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 实现钩子初始化
demo/index.js
+import { message } from 'antd';
+import logger from 'redux-logger';
+import {createBrowserHistory} from 'history';
+import 'antd/dist/antd.css'
+const SHOW = 'SHOW';
+const HIDE = 'HIDE';
+const initialState = {
+ global: false,
+ models: {},
+ effects: {}
+};
+const app = dva({
+ history:createBrowserHistory(),//自定义history
+ initialState:{counter:{number:5}},//自定义初始状态
+ //effect 执行错误或 subscription 通过 done 主动抛错时触发,可用于管理全局出错状态
+ onError:((err, dispatch) => {message.error(err.message)}),
+ //在 action 被 dispatch 时触发,用于注册 redux 中间件。支持函数或函数数组格式
+ onAction:logger,
+ //state 改变时触发,可用于同步 state 到 localStorage,服务器端等
+ onStateChange:(state)=>{console.log(state)},
+ //封装 reducer 执行。比如借助 redux-undo 实现 redo/undo
+ onReducer:reducer=>(state,action)=>{//增加额外的reducer
+ localStorage.setItem('action',JSON.stringify(action));
+ return reducer(state,action);
+ },
+ //封装 effect 执行。比如 dva-loading 基于此实现了自动处理 loading 状态。
+ onEffect:(effect, { put }, model, actionType)=>{
+ const { namespace } = model;
+ return function*(...args) {
+ yield put({ type: SHOW, payload: { namespace, actionType } });
+ yield effect(...args);
+ yield put({ type: HIDE, payload: { namespace, actionType } });
+ };
+ },
+ //指定额外的 reducer,比如 redux-form 需要指定额外的 form reducer:
+ extraReducers:{
+ loading(state = initialState, { type, payload }) {
+ const { namespace, actionType } = payload || {};
+ switch(type){
+ case SHOW:
+ return {global:true,models: { ...state.models, [namespace]: true }, effects: { ...state.effects, [actionType]: true },};
+ case HIDE:
+ const effects = { ...state.effects, [actionType]: false };
+ const models = { ...state.models, [namespace]: false };
+ const global = Object.keys(models).some(namespace => {
+ return models[namespace];
+ });
+ return {global,models,effects};
+ default:
+ return state;
+ }
+ }
+ },
+ //指定额外的 StoreEnhancer ,比如结合 redux-persist 的使用:
+ //指定额外的 StoreEnhancer
+ //它的参数是创建store的函数(store creator),返回值是一个可以创建功能更加强大的store的函数(enhanced store creator)
+ extraEnhancers:(createStore)=>{
+ return (...args)=>{
+ let store = createStore(...args);
+ console.log('返回更强大的store')
+ return {...store,more(){console.log('更强大的强能')}};
+ }
+ }
+});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
src/index.js
react-router-redux改用【connected-react-route】
function start(root) {
+ let history = options.history||createHashHistory();
- let history = createHashHistory();
+ let reducers = {
+ router: connectRouter(history)
+ };
+ if(options.extraReducers){
+ reducers = {...reducers,...options.extraReducers};
+ }
for (let i = 0; i < app._models.length; i++) {
let model = app._models[i];
reducers[model.namespace] = function(state = model.state, action) {
let actionType = action.type;
let values = actionType.split(NAMESPACE_SEPARATOR);
if (values[0] === model.namespace) {
let reducer = model.reducers[values[1]];
if (reducer) {
return reducer(state, action);
}
}
return state;
};
}
+ let combinedReducer = combineReducers(reducers);
+ let rootReducer = function(state,action){
+ let newState = combinedReducer(state,action);
+ options.onStateChange&&options.onStateChange(newState);
+ return newState;
+ }
+ if(options.onReducer){
+ rootReducer = options.onReducer(rootReducer);
+ }
+ if(options.onAction){
+ if(typeof options.onAction == 'function'){
+ options.onAction = [options.onAction];
+ }
+ }else{
+ options.onAction=[];
+ }
+ let enhancedCreateStore;
+ if(options.extraEnhancers){
+ enhancedCreateStore = options.extraEnhancers(createStore);
+ }
+ let store = enhancedCreateStore(
+ rootReducer,
+ applyMiddleware(routerMiddleware(history), sagaMiddleware,...options.onAction)
+ );
sagaMiddleware.run(rootSaga);
let App = app._router({ history, app });
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>{App}</ConnectedRouter>
</Provider>,
document.querySelector(root)
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 最后方法抽取到各文件
简化主文件`index.js
上次更新: 2022/04/15, 05:41:29