前言
- 第一篇是基本应用与初步实现,第二篇是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 }