前言
- 很多东西越到后面越复杂,越复杂的东西自己越懒得动手,或者懒得写,每次都是逼着自己写。
- 我这人比较无聊的专门下了个守望先锋玩了把,感觉有点无聊,dva就相当于王者荣耀的坦克,就是带技能的射击游戏,可能我周围没啥人玩这游戏所以一个人玩觉得很无聊吧。。。
dva
- 官网这么说的:Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:
把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面
增加了一个 Subscriptions, 用于收集其他来源的 action, eg: 键盘操作
model 写法很简约, 类似于 DSL 或者 RoR, coding 快得飞起✈️
约定优于配置, 总是好的😆 - dva的写法模式非常有意思,结合前面所学,每个组件需要有自己的状态,而按照dva的写法可以很好的把各个组件解耦。达到高可用的效益。
- dva = React-Router + Redux + Redux-saga
基本使用
- 首先还是用create-react-app创建项目。
- 然后需要安装这些库
cnpm install dva redux-logger -S
- 先做一个简单的加数字和减数字功能:
index.js
import React from 'react'
import dva, { connect } from 'dva'
let app = dva()
app.model({
namespace: 'counter1',
state: { number: 0 },
reducers: {
add(state) {
return { number: state.number + 1 }
},
minus(state) {
return { number: state.number - 1 }
}
}
})
app.model({
namespace: 'counter2',
state: { number: 0 },
reducers: {
add(state) {
return { number: state.number + 1 }
},
minus(state) {
return { number: state.number - 1 }
}
}
})
function Counter1(props) {
return (
<div>
<p>{props.number}</p>
<button onClick={() => { props.dispatch({ type: 'counter1/add' }) }}>+</button>
<button onClick={() => { props.dispatch({ type: 'counter1/minus' }) }}>-</button>
</div>
)
}
let ConnectedCounter1 = connect(state => state.counter1)(Counter1)
function Counter2(props) {
return (
<div>
<p>{props.number}</p>
<button onClick={() => { props.dispatch({ type: 'counter2/add' }) }}>+</button>
<button onClick={() => { props.dispatch({ type: 'counter2/minus' }) }}>-</button>
</div>
)
}
let ConnectedCounter2 = connect(state => state.counter2)(Counter2)
app.router(() => (
<>
<ConnectedCounter1></ConnectedCounter1>
<ConnectedCounter2></ConnectedCounter2>
</>
))
app.start('#root')
- 这里有些地方和以前用的有点区别,以前是需要引入react-redux里的connect,现在可以直接引dva里的。
- 每个model就是一个组件,里面有自己的reducer和state,不需要人工再进行合并。
- reducer里的方法名就是reducer的actionType,不需要去引入常量写,更快速了。
- 下面来看路由,将app.router进行改写:
import { Router, Route } from 'dva/router'
app.router(({ history }) => (
<Router history={history}>
<>
<Route path='/counter1' component={ConnectedCounter1}></Route>
<Route path='/counter2' component={ConnectedCounter2}></Route>
</>
</Router>
))
- 这个Router只能有一个子元素,所以需要拿个东西包一下。
- Router里需要传入history,这个history默认是哈希路由,需要可以用中间件的方式这么传:
import { createBrowserHistory } from 'history'
let app = dva({
history: createBrowserHistory()
})
-
这样使用的就是browser路由了。
-
下面看effects,做一个异步加一的操作:
const delay = ms => new Promise(function (resolve) {
setTimeout(() => {
resolve()
}, ms)
})
app.model({
namespace: 'counter1',
state: { number: 0 },
reducers: {
add(state) {
return { number: state.number + 1 }
},
minus(state) {
return { number: state.number - 1 }
}
},
effects: {
*asyncAdd(action, effects) {
yield effects.call(delay, 1000)
yield effects.put({ type: 'add' })
}
}
})
function Counter1(props) {
return (
<div>
<p>{props.number}</p>
<button onClick={() => { props.dispatch({ type: 'counter1/add' }) }}>+</button>
<button onClick={() => { props.dispatch({ type: 'counter1/asyncAdd' }) }}>asyncAdd+</button>
<button onClick={() => { props.dispatch({ type: 'counter1/minus' }) }}>-</button>
</div>
)
}
- 一般来说saga会有watcher和worker,但是这里就相当于不用watcher了,直接写worker就可以了。对应的action会自动派发到对应的组件上,如果effects跟其同名,就会执行其worker。
- 如果要派发别的namespace,把前缀加上即可。派发给自己namespace下的前缀不需要加。
- 如果effects的worker和reducer的函数同名,那么就会先派发给reducer再派发给effects。在原生的saga里,是通过中间件拦截action,由rootsaga进行派发,所以通常应该是saga的worker先执行。但可以通过worker派发reducer后再进行相应操作。实现效果都是一样的。dva在这里把这些简化,起了非常好的效果。
- 所以如果reducer先改了状态,同名effects里使用
state= yield select(state=>state.counter1)
,打印出的state则是reducer修改的状态。 - 还有个subscriptions,就是订阅:
app.model({
namespace: 'counter1',
state: { number: 0 },
reducers: {
add(state) {
return { number: state.number + 1 }
},
minus(state) {
return { number: state.number - 1 }
}
},
effects: {
*asyncAdd(action, effects) {
yield effects.call(delay, 1000)
yield effects.put({ type: 'add' })
}
},
subscriptions: {
subscribe({ dispatch, history }) {
//这里可以输入一些监听事件,满足监听条件进行dispatch或者别的操作。
//就相当于不需要手动派发,而是监听派发
history.listen((location) => {
document.title = location.pathname
//这里操作dom会有顺序问题,这里比react渲染dom先调用,所以react的渲染会覆盖这里操作。
})
}
}
})
- 订阅让功能更加灵活丰富,需要注意的点已经写在注释上了。
日志中间件
- 使用方法很简单:引入之后即可:
import { createLogger } from 'redux-logger'
let app = dva({
onAction: createLogger()
})
- 这样在每次派发时会在控制台输出。方便调试和写日志。
实现dva
- 观察上面dva的使用,发现dva传入个配置项,生成app,这个app上有model,router,start方法。
- 跟redux那些一样,就是个常见的闭包写法,可以先把大致结构理出来:
export default function (opts = {}) {
let app = {
_models: [],
model,
router,
_router: null,
start
}
function model(m) {
app._models.push(m)
}
function router(router) {
app._router = router
}
function start(container) {
ReactDOM.render(app._router,document.querySelector(container))
}
return app
}
- 首先要把store给加上去,加入store需要使用上下文,也就是react-redux的Provider,需要redux的createstore函数,这样就能把store给全局了。
- 但是store需要传入reducer,所以我们需要整合所有model的reducer,需要用到redux的combineReducers函数。每个组件的reducer配置都被push在_model里,我们可以从_model里拿出配置项:
import React from 'react'
import ReactDOM from 'react-dom'
import { createHashHistory } from 'history'
import { createStore, combineReducers } from 'redux'
import { Provider } from 'react-redux'
export default function (opts = {}) {
let history = opts.history || createHashHistory()
let app = {
_models: [],
model,
router,
_router: null,
start
}
function model(m) {
app._models.push(m)
}
function router(router) {
app._router = router
}
function start(container) {
let reducer = getReducer(app)
let store = createStore(reducer)
ReactDOM.render(
<Provider store={store}>
{app._router('history')}
</Provider>
, document.querySelector(container))
}
return app
}
function getReducer(app) {
let reducers = {}
for (let m of app._models) {//m是每个model的配置
reducer[m.namespace] = function (state=m.state, action) {//组织每个模块的reducer
let everyreducers = m.reducers//reducers的配置对象,里面是函数
let reducer = everyreducers[action.type]//相当于以前写的switch
if (reducer) {
return reducer(state, action)
}
return state
}
}
return combineReducers(reducers)//reducer结构{reducer1:fn,reducer2:fn}
}
-
这里如果自己手写一遍会发现可能会进入一个误区,就是在自己组织reducer结构然后传给combineReducers的时候,这里并不需要把配置项reducer方法一个个弄来做成switch找action.type。虽然这种方法也可以。这个everyreducers实际已经包含了每个reducer的所有逻辑,只不过是用函数做的type。当action过来时,就直接进行属性判断看存在不存在即可。不一定非要改写成switch形式。
-
这个action派发过来形式是
{type:counter1/add}
,所以按照上面的写法,reducer里面的方法名是counter1/add
,实际dva的reducer里并不需要加namespace就可以,所以还需要个方法来加前缀。 -
如果让我写我可能就直接循环个everyreducers然后每个方法名前面加个m.namespace。但是源码不是这么搞的,因为我这么写可能就reducer正常了,但是考虑到别的effects之类的必须继续改,所以得从源头修改。源码就是修改传入的_models。
function model(m) {
let prefixmodel = prefix(m)
app._models.push(prefixmodel)
}
function prefix(model) {
let everyreducers = model.reducers
let reducers = Object.keys(everyreducers).reduce((prev, next) => {//prev收集,next是每个函数名
let newkey = model.namespace + '/' + next
prev[newkey] = everyreducers[next]
return prev
}, {})
model = { ...model, reducers }
return model
}
- 可以试验下,能渲染出加一就成功了。
- 下面实现effects,effects肯定得用saga了,所以我们导入saga,以及applymiddleware函数把其作为中间件加入。
- 然后还需要把effects收集起来,用takeevery去监听做成watchersaga,用中间件run它。
import React from 'react'
import ReactDOM from 'react-dom'
import { createHashHistory } from 'history'
import { createStore, combineReducers, applyMiddleware } from 'redux'
import { Provider, connect } from 'react-redux'
import createSagaMiddleware from 'redux-saga'
import * as sagaEffects from 'redux-saga/effects'
export default function (opts = {}) {
let history = opts.history || createHashHistory()
let app = {
_models: [],
model,
router,
_router: null,
start
}
function model(m) {
let prefixmodel = prefixResolve(m)
app._models.push(prefixmodel)
}
function router(router) {
app._router = router
}
function start(container) {
let reducer = getReducer(app)
let sagas = getSagas(app)
//let store = createStore(reducer)
let sagaMiddleware = createSagaMiddleware()
app._store = applyMiddleware(sagaMiddleware)(createStore)(reducer)
sagas.forEach(sagaMiddleware.run)
ReactDOM.render(
<Provider store={app._store}>
{app._router('history')}
</Provider>
, document.querySelector(container))
}
return app
}
function getReducer(app) {
let reducers = {}
for (let m of app._models) {//m是每个model的配置
reducers[m.namespace] = function (state = m.state, action) {//组织每个模块的reducer
let everyreducers = m.reducers//reducers的配置对象,里面是函数
let reducer = everyreducers[action.type]//相当于以前写的switch
if (reducer) {
return reducer(state, action)
}
return state
}
}
return combineReducers(reducers)//reducer结构{reducer1:fn,reducer2:fn}
}
function prefix(obj, namespace) {
return Object.keys(obj).reduce((prev, next) => {//prev收集,next是每个函数名
let newkey = namespace + '/' + next
prev[newkey] = obj[next]
return prev
}, {})
}
function prefixResolve(model) {
if (model.reducers) {
model.reducers = prefix(model.reducers, model.namespace)
}
if (model.effects) {
model.effects = prefix(model.effects, model.namespace)
}
return model
}
function getSagas(app) {
let sagas = []
for (let m of app._models) {
sagas.push(function* () {
for (const key in m.effects) {//key就是每个函数名
const watcher = getWatcher(key, m.effects[key])
yield sagaEffects.fork(watcher) //用fork不会阻塞
}
})
}
return sagas
}
function getWatcher(key, effect) {
return function* () {
yield sagaEffects.takeEvery(key, function* (action) {//对action进行监控,调用下面这个saga
yield effect(action, sagaEffects)
})
}
}
export { connect }
- 这个effects还有点小问题,就是在put action时,必须put带前缀的,否则不认。解决方法就是重写put:
function getSagas(app) {
let sagas = []
for (let m of app._models) {
sagas.push(function* () {
for (const key in m.effects) {//key就是每个函数名
const watcher = getWatcher(key, m.effects[key], m)
yield sagaEffects.fork(watcher) //用fork不会阻塞
}
})
}
return sagas
}
function prefixType(type, model) {
if (type.indexOf('/') == -1) {//这个判断有点不严谨,可以自己再捣鼓下
return model.namespace + '/' + type
}
return type//如果有前缀就不加,因为可能派发给别的model下的
}
function getWatcher(key, effect, model) {
function put(action) {
return sagaEffects.put({ ...action, type: prefixType(action.type, model) })
}
return function* () {
yield sagaEffects.takeEvery(key, function* (action) {//对action进行监控,调用下面这个saga
yield effect(action, { ...sagaEffects, put })
})
}
}
- 这样就实现加前缀和不加前缀的问题了。
- 下面实现路由,路由其实就是原生的react-router-redux和react-router-dom
router.js
import * as routerRedux from 'react-router-redux';
export * from 'react-router-dom';
export { routerRedux }
- 然后把
import { Router, Route } from 'dva/router'
改成自己的就行了。 - 在dva里记得要把自己的闭包对象和history传过去就行了。
let history = opts.history || createHashHistory()
ReactDOM.render(
<Provider store={app._store}>
{app._router({ app, history })}
</Provider>
, document.querySelector(container))
-
但是,这个只能实现基本路由功能,派发action实现路由跳转是不行的。所以需要做些配置。
-
首先引入connected-react-router的这些东西:
import { routerMiddleware, connectRouter, ConnectedRouter } from 'connected-react-router'
- 用这个分3步,routerMiddleware做中间件,ConnectedRouter包裹并传入history,reducer里加入router的属性。实际这个就是dva/router里解构RouterRedux里解构出的方法。
app._store = applyMiddleware(routerMiddleware(history), sagaMiddleware)(createStore)(reducer)
ReactDOM.render(
<Provider store={app._store}>
<ConnectedRouter history={history}>
{app._router({ app, history })}
</ConnectedRouter>
</Provider>
, document.querySelector(container))
function getReducer(app) {
let reducers = {
router: connectRouter(app._history)
}
for (let m of app._models) {//m是每个model的配置
reducers[m.namespace] = function (state = m.state, action) {//组织每个模块的reducer
let everyreducers = m.reducers//reducers的配置对象,里面是函数
let reducer = everyreducers[action.type]//相当于以前写的switch
if (reducer) {
return reducer(state, action)
}
return state
}
}
return combineReducers(reducers)//reducer结构{reducer1:fn,reducer2:fn}
}
- 最后组件中进行派发action:
import { Router, Route, routerRedux } from './dva/router'
<button onClick={() => { props.dispatch(routerRedux.push('/counter2')) }}>跳转counter2</button>
- 最后还差个subscriptions,这个实现就是遍历它,然后执行,往函数里传history和dispatch即可。
function start(container) {
let reducer = getReducer(app)
let sagas = getSagas(app)
//let store = createStore(reducer)
let sagaMiddleware = createSagaMiddleware()
app._store = applyMiddleware(routerMiddleware(history), sagaMiddleware)(createStore)(reducer)
for (let m of app._models) {
if (m.subscriptions) {
for (let key in m.subscriptions) {
let subscription = m.subscriptions[key]
subscription({ history, dispatch: app._store.dispatch })
}
}
}
sagas.forEach(sagaMiddleware.run)
ReactDOM.render(
<Provider store={app._store}>
<ConnectedRouter history={history}>
{app._router({ app, history })}
</ConnectedRouter>
</Provider>
, document.querySelector(container))
}
- 先就写这么多,剩下的下次写。