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

前言

  • 第一篇是基本应用与初步实现,第二篇是dva-loading实现,顺便实现了2个钩子。
  • 这篇讲dynamic。

dynamic

  • dynamic可以解决组件动态加载问题
  • 先看使用:
import dynamic from 'dva/dynamic'
const DynamicPage = dynamic({
    app,
    models: () => [import('./models/mymodel')],
    component: () => import('./routes/mypage')
})
app.router(({ history, app }) => (
    <Router history={history}>
        <><ul>
            <li><Link to='/dynamic'>dynamic</Link></li>
            <li><Link to='/counter1'>counter1</Link></li>
            <li><Link to='/counter2'>counter2</Link></li>
        </ul>
            <Route path='/counter1' component={ConnectedCounter1}></Route>
            <Route path='/counter2' component={ConnectedCounter2}></Route>
            <Route path='/dynamic' component={DynamicPage}></Route>
        </>
    </Router>
)
app.start('#root')
  • 我们需要在dynamic里导入model的配置项以及组件:

models/mymodel

const delay = ms => new Promise(function (resolve) {
    setTimeout(() => {
        resolve()
    }, ms)
})
export default {
    namespace: 'mymodel',
    state: { number1: 0, number2: 0 },
    reducers: {
        add(state, action) {
            return { number1: state.number1 + 1, number2: state.number2 + action.payload }
        }
    },
    effects: {
        *asyncAdd(action, { put, call }) {
            yield call(delay, 1000)
            yield put({ type: 'add', payload: 5 })
        }
    }
}

routes/mypages

import React from 'react'
import { connect } from 'dva'

function Mypage(props) {
    return (
        <div>
            <div>{props.number1},{props.number2}</div>
            <button onClick={() => props.dispatch({ type: 'mymodel/asyncAdd' })}>派发</button>
        </div>

    )
}
export default connect(state => state.mymodel)(Mypage)
  • 简单写个组件,就是异步执行saga会派发add。
  • 这个组件在切换时候就是动态加载的了。
  • 可以打开network 点击link进行切换,发现webpack帮我们做好了懒加载。点跳转的时候出来2个chunk.js。
  • 特别注意这里的connect,使用自己写的dva一定要把它调成自己的connect或者2.60版以上的dva,否则会报“Could not find “store” in either the context or props of …”错误,这个bug让我找了半天,我当时是2.41的dva,后来把版本换来换去才发现原来是这个地方的问题。

dynamic实现

  • 这东西实现很巧妙,第一次看绝对大受启发。
  • 我们在调用dynamic时候,只是去把model和component传给他,同时也是借用了webpack的懒加载。
  • 数据问题先放一边, 先将其渲染出来。
import React from 'react'
const DefaultLoadingComponent = props => <div>加载中</div>
export default function dynamic(config) {
    let { app, models, component } = config
    return class extends React.Component {
        constructor(props) {
            super(props)
            this.LoadingComponent = config.LoadingComponent || DefaultLoadingComponent
            this.state = { AsyncComponent: null }
            this.load()
        }
        async load() {
            let [resolvedmodule, AsyncComponent] = await Promise.all([Promise.all(models()), component()])
            resolvedmodule = resolvedmodule.map((m) => m['default'] || m)
            AsyncComponent = AsyncComponent['default'] || AsyncComponent
            resolvedmodule.forEach((m) => app.model(m))
            this.setState({ AsyncComponent })
        }
        render() {
            let { AsyncComponent } = this.state
            let { LoadingComponent } = this
            return (
                AsyncComponent ? <AsyncComponent {...this.props}></AsyncComponent> : <LoadingComponent></LoadingComponent>
            )
        }
    }
}
  • 这个其实就是个高阶组件,拿到配置项后去执行配置项的Model和component,然后拿到的model要拿去app里把namespace之类的注册上,虽然现在暂时还不能注入。最后选择性渲染component。
  • model和component未在app上注册完时,AsyncComponent是拿不到值的,所以会渲染Loading组件,promise.all完成后则会拿到组件来渲染它。
  • 下面需要实现model注册,这个难点就在于动态加载时,dva实例已经运行了,而要注入新的model,需要改写reducer和saga,同时还需要保存已经加载的reducer和saga。
  • 首先需要先把getReducer函数进行拆分:
    let reducers = {
        router: connectRouter(app._history)
    }
 //改为initialreducers并提到外面
  for (let m of app._models) {//m是每个model的配置
            initialReducers[m.namespace] = getReducer(m)
   }
  function getReducer(m) {
    return 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
    }
}
 // 提到start执行时再进行装载
     function createReducer() {
        let extraReducers = plugin.get('extraReducers')
        return combineReducers({
            ...initialReducers,
            ...extraReducers//这里是传来的中间件对象
        })//reducer结构{reducer1:fn,reducer2:fn}
    }
    //最后的合并单独提出来做个函数。
  • 因为我们需要装载时得到一遍,懒加载再拿到一遍。到时候传来装载的model,然后getReducer就可以得到这个model的reducer了。
  • 同理,saga和subscription也要拆一下。使得我们传入model可以直接得到effects和subscriptions。
let sagas = getSagas(app)
for (let m of app._models) {
            if (m.subscriptions) {
                runSubscription(m)
            }
        }
  function runSubscription(m) {
        for (let key in m.subscriptions) {
            let subscription = m.subscriptions[key]
            subscription({ history, dispatch: app._store.dispatch })
        }
    }
    function getSagas(app) {
        let sagas = []
        for (let m of app._models) {
            sagas.push(getSaga(m, plugin.get('onEffect')))
        }
        return sagas
    }
  • 下面需要改写model方法,在一开始装载的时候,model方法是加前缀,然后把model存到闭包里,最后start时候才进行装载。但是我们插件再去调model的话只能存进闭包,没人去调它,所以需要改写model。有人说用其他方法不也可以实现吗?确实可以实现,不过需要其他插件在获取model后调用使用别的方法,而不是去执行app.model。既然约定用model方法那就使用这个方法。
       function model(m) {
        let prefixmodel = prefixResolve(m)
        app._models.push(prefixmodel)
        return prefixmodel
    }

   app.model = injectModel.bind(app)//都执行完把model方法改了,以后会走inject
        function injectModel(m) {
            m = model(m)//加前缀
            initialReducers[m.namespace] = getReducer(m)//此时的initialReducers是一开始装载后的,只要再添加新的替换调即可。
            store.replaceReducer(createReducer())
            if (m.effects) {
                sagaMiddleware.run(getSaga(m, plugin.get('onEffect')))
            }
            if (m.subscriptions) {
                runSubscription(m)
            }
        }
  • 首先这个app.model=injectModel.bind(app)表示装载后调app.model实际执行的时injectModel这个方法。
  • 这个model方法为了给后面懒加载使用,需要有个返回值,把带前缀的返回回来,确保reducer和effects能带上前缀。
  • 由于此时拿到的initialReducers就是已经装载过的,也就是包括前面一开始加载时用户配置的model,这时只要往上面添加键就是在原有基础上添加reducer了。
  • 最后使用store提供的replace方法进行替换。
  • 而saga需要中间件去run一下,subscription直接调方法就行了。
  • 另外需要改chunk名字就靠魔法字符串就行了,这是webpack的内容。/* webpackChunkName: "xxxxx" */
  • 目前整个手写dva index.js代码如下,下篇继续。
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'
import { routerMiddleware, connectRouter, ConnectedRouter } from 'connected-react-router'
import Plugin, { filterHooks } from './plugin'

export default function (opts = {}) {
    let history = opts.history || createHashHistory()

    let app = {
        _models: [],
        model,
        router,
        _router: null,
        start,
        _history: history
    }
    function model(m) {
        let prefixmodel = prefixResolve(m)
        app._models.push(prefixmodel)
        return prefixmodel
    }
    function router(router) {
        app._router = router
    }
    let plugin = new Plugin()
    plugin.use(filterHooks(opts))
    app.use = plugin.use.bind(plugin)
    function getSaga(m, onEffect) {
        return function* () {
            for (const key in m.effects) {//key就是每个函数名
                const watcher = getWatcher(key, m.effects[key], m, onEffect)
                yield sagaEffects.fork(watcher) //用fork不会阻塞
            }
        }
    }
    function getSagas(app) {
        let sagas = []
        for (let m of app._models) {
            sagas.push(getSaga(m, plugin.get('onEffect')))
        }
        return sagas
    }
    let initialReducers = {
        router: connectRouter(app._history)
    }
    function createReducer() {
        let extraReducers = plugin.get('extraReducers')
        return combineReducers({
            ...initialReducers,
            ...extraReducers//这里是传来的中间件对象
        })//reducer结构{reducer1:fn,reducer2:fn}
    }
    function getReducer(m) {
        return 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
        }
    }
    function runSubscription(m) {
        for (let key in m.subscriptions) {
            let subscription = m.subscriptions[key]
            subscription({ history, dispatch: app._store.dispatch })
        }
    }
    function start(container) {
        for (let m of app._models) {//m是每个model的配置
            initialReducers[m.namespace] = getReducer(m)
        }
        let reducer = createReducer()
        let sagas = getSagas(app)
        //let store = createStore(reducer)
        let sagaMiddleware = createSagaMiddleware()
        let store = applyMiddleware(routerMiddleware(history), sagaMiddleware)(createStore)(reducer)
        app._store = store
        for (let m of app._models) {
            if (m.subscriptions) {
                runSubscription(m)
            }
        }
        window.store = app._store//调试用
        sagas.forEach(sagaMiddleware.run)


        ReactDOM.render(
            <Provider store={app._store}>
                <ConnectedRouter history={history}>
                    {app._router({ app, history })}
                </ConnectedRouter>
            </Provider>
            , document.querySelector(container)
        )

        app.model = injectModel.bind(app)//都执行完把model方法改了,以后会走inject
        function injectModel(m) {
            m = model(m)//加前缀
            initialReducers[m.namespace] = getReducer(m)//此时的initialReducers是一开始装载后的,只要再添加新的替换调即可。
            store.replaceReducer(createReducer())
            if (m.effects) {
                sagaMiddleware.run(getSaga(m, plugin.get('onEffect')))
            }
            if (m.subscriptions) {
                runSubscription(m)
            }
        }

    }
    return app
}


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 prefixType(type, model) {
    if (type.indexOf('/') == -1) {//这个判断有点不严谨,可以自己再捣鼓下
        return model.namespace + '/' + type
    }
    return type//如果有前缀就不加,因为可能派发给别的model下的
}

function getWatcher(key, effect, model, onEffect) {
    function put(action) {
        return sagaEffects.put({ ...action, type: prefixType(action.type, model) })
    }
    return function* () {
        yield sagaEffects.takeEvery(key, function* (action) {//对action进行监控,调用下面这个saga
            if (onEffect) {
                for (const fn of onEffect) {//oneffect是数组
                    effect = fn(effect, { ...sagaEffects, put }, model, key)
                }
            }
            yield effect(action, { ...sagaEffects, put })
        })
    }
}
export { connect }
发布了163 篇原创文章 · 获赞 9 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/yehuozhili/article/details/104058175
dva