【dva】dva使用与实现(一)

7 篇文章 2 订阅

前言

  • 很多东西越到后面越复杂,越复杂的东西自己越懒得动手,或者懒得写,每次都是逼着自己写。
  • 我这人比较无聊的专门下了个守望先锋玩了把,感觉有点无聊,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))
    }
  • 先就写这么多,剩下的下次写。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

业火之理

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值