【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 }
發佈了153 篇原創文章 · 獲贊 8 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章