前言
- 第一篇是基本應用與初步實現,第二篇是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 }