前言
- 接上篇,这次是extraEnhancers
extraEnhancers
- extraEnhancers其实相当于外部中间件,我们通过学习一个持久化插件来学习它。
- 前面我们持久化是利用onStateChange钩子当状态发生变更就写入localStorage,而取出数据使用initialState传入。
- 有个插件叫redux-persist可以实现持久化。
- 先安装,然后看如何使用:
cnpm i redux-persist -S
- 然后index.js里加入这些:
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'
- 这个persisitStore是需要把store传给它。
- persisReducer则是需要把persistConfig和reducer传给它。类似于reducer中间件,增加reducer功能。
- storage是配置在persistConfig里的。指定存储引擎,默认localStorage。
- 最后gate大门,大写字母开头,不用说就是包裹组件的。
const persistConfig = {
key: 'root',
storage
}
let app = dva({
onReducer: reducer => {
let undoReducer = undoable(reducer)
let rootReducer = persistReducer(persistConfig, undoReducer)
return function (state, action) {
let newState = rootReducer(state, action)
let router = newState.present.router ? newState.present.router : newState.present.routing
return { ...newState, router: router }
}
},
history: createBrowserHistory()
})
app.router(({ history, app }) => {
let persistor = persistStore(app._store)
return (
<PersistGate persistor={persistor}>
<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>
</PersistGate>
)
})
- 然后看浏览器里的localStorage的key是不是persist:root,再进行变更state,刷新浏览器,如果状态没变,就ok。
- 下面先实现这个插件,首先先仿照导入的文件把目录和文件建出来。
lib/storage.js
export default {
setItem(key, value) {
localStorage.setItem(key, value)
},
getItem(key) {
return localStorage.getItem(key)
}
}
- storage也可以改成数据库什么的,主要就是一个存,一个取。
persistReducer.js
const PERSIST_INIT = 'PERSIST_INIT'
export default function (persistConfig, reducer) {
let initialized = false
let persistKey = `persist:${persistConfig.key}`
return function (state, action) {
switch (action.type) {
case PERSIST_INIT:
initialized = true
let value = persistConfig.storage.getItem(persistKey)
state = value ? JSON.parse(value) : undefined
return reducer(state, action)//有初始值就读出来给reducer
default:
if (initialized) {//已经初始化过的就写入即可
state = reducer(state, action)
persistConfig.storage.setItem(persistKey, JSON.stringify(state))
return state
}
return reducer(state, action)
}
}
}
- 这个增强reducer的函数需要设定读storage数据和写storage数据的时机。
- 先给个是否初始化,存储在storage的键名也是这里指定。
- 返回的reducer就能把是否初始化作为闭包,更改其状态。
- 当刷新时,派发初始化的action,便会读storage状态,然后传给reducer。
- 当初始化完成后,每次有action来都会写入state。其实这里可以扩展一下,不然状态没变也写感觉有点浪费。
integration/react.js
import React, { Component } from 'react'
class PersistGate extends Component {
componentDidMount() {
this.props.persistor.initState()
}
render() {
return this.props.children
}
}
export { PersistGate }
- 这个大门就是个高阶组件,只要在其加载时派发初始化的action就行了。
persistStore.js
export default function (store) {
let persistor = {
...store,
initState() {
persistor.dispatch({
type: 'PERSIST_INIT'
})
}
}
return persistor
}
- 这就把store上再挂个方法,让前面大门能拿到就可以。
- 总结下就是包装store加入派发初始化方法,用高阶组件加载后派发它,在reducer里对action处理变更reducer内部状态,从而产生写入storage和读取storage的逻辑,storage就一个读一个写2方法。
- 但是这个取persistor的方式不优雅,利用extraEnhancer可以优雅的把persistor挂到store上:
extraEnhancers: [
createStore => (...args) => {
const store = createStore(...args)
let persistor = persistStore(store)
store.persistor = persistor
return store
}
],
- 这个钩子有点特殊,是个数组,写的时候稍微注意下,这个写法有没有很熟悉?都是createStore=>reducer=>store,就是个applyMiddleWare写法,只不过中间件的第二个参数写的reducer,实际这个createStore除了reducer还有初始状态以及enhancer,而最后返回值我们是把加的已经放到store上了,中间件是把store里的dispatch进行改写。
- 这样自然我们可以使用createStore拿到store,再把persistor的派发方法加到store上。这样大门在取的时候,就变成这样:
app.router(({ history, app }) =>(
<PersistGate persistor={app._store.persistor}>
<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>
</PersistGate>
)
)
- 既然是钩子,我们实现dva也要加上,由于传入是数组,所以在use时,把不要push进去,直接覆盖。所以这样后面的配置会盖掉前面配置,这个钩子只能在一个地方配置。
plugin.js
use(plugin) {
const { hooks } = this
for (let key in plugin) {
if (key === 'extraEnhancers') {
hooks[key] = plugin[key]
} else {
hooks[key].push(plugin[key])
}
}//{hook:[fn|obj]}
}
- 然后去整合中间件传给createStore:
let sagaMiddleware = createSagaMiddleware()
let extraMiddleware = plugin.get('onAction')
let extraEnhancers = plugin.get('extraEnhancers')
let enhancers = [...extraEnhancers, applyMiddleware(routerMiddleware(history),
sagaMiddleware, ...extraMiddleware)]
let store = (createStore)(reducer, opts.initialState, compose(...enhancers))
- 这里可能有点绕,不过只要把握主线就行。前面说extraEnhancers写法跟applyMiddleWare一样,那么就相当于这2个函数是同级别的,需要compose把这2函数组合成一个复合函数再去使用。
- 所以,上面那种写法也相当于这样:
let sagaMiddleware = createSagaMiddleware()
let extraMiddleware = plugin.get('onAction')
let extraEnhancers = plugin.get('extraEnhancers')
let enhancers = [...extraEnhancers, applyMiddleware(routerMiddleware(history), sagaMiddleware, ...extraMiddleware)]
let composedMiddleWare = compose(...enhancers)
let store = composedMiddleWare(createStore)(reducer, opts.initialState)
- 这就跟以前那种写法差不多,实测也是ok的。
- 这篇先写到这。