dva

文章目录

dva

dva 是 react 的最佳实践,本身其实没有什么内容,只是把各个常用的包引入后封装了一层。

  • react-dom
  • redux
  • react-redux (router 与 redux 结合的中间件)
  • redux-saga
  • react-router-redux(history 与 redux 结合的中间件,源码这个库已废弃,推荐改为 connected-react-router)

用法

一个 react 项目对应一个 dva 对象。

调用 dva 会返回一个对象 app。app 上有几个常用函数

  • model:将传入的对象转为 redux/saga 形式,并在键名前添加命名空间,之后保存在内部的 _models 中。
  • router:传入一个函数,会给这个函数传入一个带有 history 属性的对象,这个传入函数的返回结果会被渲染在页面中
  • start:传入一个 selector,表示挂载根节点的 DOM 元素。
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
import React from 'react';
import dva, { connect, ConnectedRouter } from './dva'
import { Router, Route, Link } from './dva/router'
import { push } from 'connected-react-router'
const app = dva()

app.model({
namespace: 'counter',
state: { number: 0 },
reducers: {
add(state, action) {
return { number: state.number + 1 }
},
minus(state, action) {
return { number: state.number - 1 }
}
},
effects: {
*asyncAdd(action, { call, put }) {
yield call(delay, 1000)
yield put({ type: 'add' })
},
*goto({payload}, { call, put }) {
// 派发一个跳转到首页的动作
yield put(push(payload))
}
}
})

function delay(ms) {
return new Promise(resolve => {
setTimeout(() => resolve(), ms)
})
}

const Counter = props => {
return (
<div>
<p>{props.number}</p>
<button onClick={() => props.dispatch({type:'counter/add'})}>+</button>
<button onClick={() => props.dispatch({type:'counter/asyncAdd'})}>asyncAdd</button>
<button onClick={() => props.dispatch({type:'counter/goto', payload: '/'})}>goto</button>
</div>
)
}

const ConnectedCounter = connect(state => state.counter)(Counter)
// app.router(() => <ConnectedCounter />)
const Home = () => <div>Home</div>
app.router((api) => (
<ConnectedRouter history={api.history}>
<>
<Link to="/">Home</Link>
<Link to="/counter">Couter</Link>
<Route path="/" component={Home} exact={true}></Route>
<Route path="/counter" component={ConnectedCounter}></Route>
</>
</ConnectedRouter>
))

app.start('#root')

实现

mini 版 dva

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider, connect } from 'react-redux'
import { combineReducers, createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import * as sagaEffects from 'redux-saga/effects'
import { NAMESPACE_SEP } from './constants'
import prefixNamespace from './prefixNamespace'
import { createHashHistory } from 'history'
import { ConnectedRouter, connectRouter, routerMiddleware } from 'connected-react-router'
export { connect, ConnectedRouter }
let history = createHashHistory()
function dva() {
const app = {
_models: [],
model,
router,
_router: null,
start
}

// 为 model 的 effects 和 reducers 添加命名空间,并 push 到 _models
function model(modelObject) {
const prefixedModel = prefixNamespace(modelObject)
app._models.push(prefixedModel)
return prefixedModel
}

function router(routerConfig) {
app._router = routerConfig
}

let initialReducers = {router: connectRouter(history)}
function start(selector) {
for (const model of app._models) {
// 把添加命名空间后的 reducer 加入到 initialReducers 上
initialReducers[model.namespace] = getReducer(model)
}
// 把 initialReducers 作为根 reducer
let rootReducer = createReducer()
// 根据 effects 生成 saga
const sagas = getSagas(app)
let sagaMiddleware = createSagaMiddleware()
// 添加 saga 中间件,路由中间件
let store = applyMiddleware(sagaMiddleware, routerMiddleware(history))(createStore)(rootReducer)
// sagaMiddleware.run(saga)
// saga.run
sagas.forEach(sagaMiddleware.run)
// 用 app._router 渲染
ReactDOM.render(
<Provider store={store}>
{/* <ConnectedRouter history={history}> */}
{app._router({history})}
{/* </ConnectedRouter> */}
</Provider>,
document.querySelector(selector)
)
}
// 把加了命名空间后的 saga push 到 sagas
function getSagas(app) {
let sagas = []
for(let model of app._models) {
sagas.push(getSaga(model.effects, model))
}
return sagas
}
function getSaga(effects, model) {
return function* () {
for (const key in effects) {
const watcherSaga = getWatcher(key, model.effects[key], model)
yield sagaEffects.fork(watcherSaga)
}
}
}
function getWatcher(key, effect, model) {
return function* () {
// 监听所有 saga,由于源码调用时有 action 和 sagaEffect,所以这里自执行一次
yield sagaEffects.takeEvery(key, function* (action) {
yield effect(action, {
...sagaEffects,
put: action => sagaEffects.put({...action, type: prefixType(action.type, model)})
})
})
}
}

function createReducer() {
return combineReducers(initialReducers)
}

return app
}
// 添加 type 的命名空间前缀
function prefixType(type, model) {
if (!~type.indexOf('/')) {
return `${model.namespace}${NAMESPACE_SEP}${type}`
}
console.warn(`Warning: [sagaEffects.put] ${type} should not be prefix`)
return type
}
function getReducer(model) {
let { state: initialState, reducers } = model
// 返回 reducer 函数
return (state = initialState, action) => {
let reducer = reducers[action.type]
if (reducer) return reducer(state, action)
return state
}
}

export default dva

其余文件

prefixNamespace.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { NAMESPACE_SEP } from './constants'
/**
* 把一个对象的 key 从一个老的值变成 namespce/老的值
* @param {*} obj
* @param {*} namespace
*/
function prefix(obj, namespace) {
return Object.keys(obj).reduce((memo, key) => {
const newKey = `${namespace}${NAMESPACE_SEP}${key}`
memo[newKey] = obj[key]
return memo
}, {})
}

export default function prefixNamespace(model) {
if (model.reducers) {
model.reducers = prefix(model.reducers, model.namespace)
}
if (model.effects) {
model.effects = prefix(model.effects, model.namespace)
}
return model
}

router.js

1
export * from 'react-router-dom'

constants.js

1
export const NAMESPACE_SEP = '/'
分享到:

评论完整模式加载中...如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理