背景
對於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)