dva的effect那麼難用,自己造一個輪子吧

福祿網路研發團隊發表於2021-07-29

背景

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

福祿·研發中心 福袋

相關文章