對於dva這個開發框架,國內從事react的前端工程師多半不會感到陌生,dva完善的開發體系和簡單的api,讓其被普遍運用到實際工做中。我所在的公司也是長期使用dva做爲基礎的開發框架,雖然好用,可是隨着前端技術的飛速發展,dva彷佛陷入停滯了,從npm官網上看其發版狀況看,正式版本2.4.1是三年前發佈的,最近一次是2.6.0-beta.22版本,也是半年前發佈的,所以 附錄【2】文章中指出dva將來不肯定性高的隱患。除此以外,關於dva的effect是否能支持async/await的討論(見附錄【1】連接),也暴露出dva在擴展性的短板。前端
上面簡單說了一下dva目前的狀況,本文的出發點也就是在dva的effect不支持async/await的問題上,用過dva的都清楚,dva的model層採用generator進行流程控制,雖然功能強大,但開發體驗跟async/await比起來仍是差了些,所以我就想實現一版支持async/await的mini-dva,其餘研發流程儘可能和dva保持一致。react
從這裏開始,咱們就造一個支持async/await的mini-dva吧,取個正式的名字就叫 mini-async-dva ,廢話不說了,先看一下mini-saync-dva和dva的一個具體對比吧:git
## dva const Foo = dynamic({ app, models: () => [import('./models/foo')], component: () => import('./pages/Foo'), }); ...... <Route path="/foo" component={Foo} /> ......
## mini-async-dva import Bar from './pages/Bar'; ...... <Route path="/bar"> <Bar /> </Route> ......
## dva export default { namespace: 'foo', state: { list: [] }, effects: { * fetchList({ payload }, { call }) { yield call(delay, 1000); } } };
## mini-async-dva export default { namespace: 'foo', state: { list: [] }, effects: { async fetchList(payload, updateStore) { await delay(); } } };
## dva import React from 'react'; import { connect } from 'dva'; @connect((state) => { return state.bar; }) class Bar extends React.Component { ...... } export default Bar;
## mini-async-dva import React from 'react'; import model from '@/model'; @model('bar') class Bar extends React.Component { ...... } export default Bar;
經過上面代碼的對比,發現mini-async-dva最大的特色就是model的effect支持async/await語法,路由組件默認就是異步導入,沒必要再使用dynamic進行包裹了,固然還有視圖層與model的綁定,也作了一點小優化,代碼事後,就開始分析一下輪子咋實現的吧。github
咱們這個輪子仍是沿用redux做爲狀態管理,可是因爲須要動態註冊model對象,所以須要手動接管reducer裏面的邏輯,好比當/foo
路由第一次激活時,Foo組件的model對象須要掛載到全局store裏面去,那麼經過發送一個type爲@@redux/register
的action,在reducer裏面手動掛載model對應的state對象,同時要將effects裏面的方法都緩存起來,便於後續執行,咱們代碼裏是保存在effectsMap中。npm
const effectsMap = {}; const store = createStore((state, action) =>; { const { type, payload = {} } = action; const { namespace, effects, initalState, updateState } = payload; if (type === '@@redux/register') { // 註冊 effectsMap[namespace] = effects; return Object.assign({}, state, { [namespace]: initalState }); } if (type === '@@redux/update') { // 反作用執行完畢,須要更新namespace對應的狀態值 return Object.assign({}, state, { [namespace]: Object.assign({}, state[namespace], updateState) }); } if (type.includes('/') && !type.includes('@@redux/INIT')) { // 視圖層發起的dispatch方法進入到這裏,須要分離出namespace和具體的effect方法名 const [ sliceNameSpace, effect ] = type.split('/'); if (effectsMap[sliceNameSpace] && effectsMap[sliceNameSpace][effect]) { executeAsyncTask(state, sliceNameSpace, effectsMap[sliceNameSpace][effect], payload); // 執行異步任務 } } return state; }, {});
結合註釋應該不難理解,接下來就看一下executeAsyncTask的實現吧,其實很簡單:redux
function updateStore(namespace) { return function(state) { Promise.resolve().then(() => { store.dispatch({ type: '@@redux/update', payload: { namespace, updateState: state, } }); }); } } async function executeAsyncTask(state, namespace, fn, payload) { const response = await fn.call(state[namespace], payload, updateStore(namespace)); store.dispatch({ type: '@@redux/update', // 發起更新state的意圖 payload: { namespace, updateState: response, } }); }
至此store就完成了動態註冊和狀態更新的基本需求,下面要實現組件的異步加載了。api
在mini-async-dva中,視圖是異步加載的,這裏的異步主要是控制視圖依賴的models實現異步加載和註冊,視圖須要等到models完成註冊後才能渲染,保證組件內部邏輯與store的狀態保持同步。緩存
import { useStore } from 'react-redux'; function AsyncComponent({ deps, children, ...rest }) { const store = useStore(); const [modelLoaded, setModelLoaded] = useState(!Array.isArray(deps) && deps.length === 0); useEffect(() => { if(!modelLoaded) { Promise.all(deps.map((dep) => runImportTask(dep))).then(() => { setModelLoaded(true); }); } }, []); function runImportTask(dep) { if (!store.getState().hasOwnProperty(dep)) { // model沒有註冊過 return new Promise((resolve, reject) => { import(`models/${dep}.js`).then((module) => { const { namespace, state: initalState = {}, effects } = module.default; store.dispatch({ type: '@@redux/register', payload: { effects, initalState, namespace: namespace || dep, } }); resolve(); }).catch(reject); }); } } if (modelLoaded) { return ( <> {React.createElement(children, rest)} </> ); } return null; }
AsyncComponent組件主要的功能包含兩點,其一是異步加載全部依賴的models,而後發起一個動態註冊model對象的意圖,其二是當models都加載完畢,渲染咱們的視圖。前端工程師
function model(...deps) { return function wrapComponent(target) { const cacheRender = connect(function mapStateToProps(state) { return deps.reduce((mapState, dep) => { mapState[dep] = state[dep]; return mapState; }, {}); }, null)(target); return (props) => { return ( <AsyncComponent deps={deps} {...props}> {cacheRender} </AsyncComponent> ) }; } }
model函數蒐集咱們的視圖組件依賴的model名稱,而後將視圖組件包裹在AsyncComponent內,從而實現動態控制和connect的綁定,至此就基本完成了mini-async-dva的核心功能了。app
到這裏本文也就結尾了,mini-async-dva的項目代碼已經放到github上了,具體地址可查看附錄【3】,若是看官以爲能夠,順手點個小星星唄。
附錄:
【1】https://github.com/dvajs/dva/issues/1919 (async支持討論)
【2】https://mp.weixin.qq.com/s/frSXO79aq_BHg09rS-xHXA (一文完全搞懂 DvaJS 原理)
【3】https://github.com/lanpangzi-zkg/mini-async-dva (mini-async-dva)