背景
對於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保持一致。
輪子對比
從這里開始,我們就造一個支持async/await的mini-dva吧,取個正式的名字就叫 mini-async-dva ,廢話不說了,先看一下mini-saync-dva和dva的一個具體對比吧:
1.路由文件
## 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>
......
2.models
## 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();
}
}
};
3.view層
## 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的綁定,也做了一點小優化,代碼過后,就開始分析一下輪子咋實現的吧。
輪子實現
1.store管理
我們這個輪子還是沿用redux作為狀態管理,但是由於需要動態注冊model對象,因此需要手動接管reducer里面的邏輯,比如當/foo
路由第一次激活時,Foo組件的model對象需要掛載到全局store里面去,那么通過發送一個type為@@redux/register
的action,在reducer里面手動掛載model對應的state對象,同時要將effects里面的方法都緩存起來,便於后續執行,我們代碼里是保存在effectsMap中。
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的實現吧,其實很簡單:
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就完成了動態注冊和狀態更新的基本需求,下面要實現組件的異步加載了。
2.異步加載
在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都加載完畢,渲染我們的視圖。
3.狀態綁定
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的核心功能了。
最后
到這里本文也就結尾了,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)