• dva的effect那么难用,自己造一个轮子吧


    背景

    对于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)

    福禄·研发中心 福袋
  • 相关阅读:
    禁止用户打开HTML页面调试
    Git 忽略提交 .gitignore
    PHP日期时间处理
    HTML页面中JavaScript能获取到的各种屏幕大小信息
    移动浏览器H5页面通过scheme打开本地应用
    Python爬虫利器:Beautiful Soup
    MySQL分页查询优化
    JavaScrpit中异步请求Ajax实现
    PHP反射机制
    CentOS下nginx php mysql 环境搭建
  • 原文地址:https://www.cnblogs.com/fulu/p/15065669.html
Copyright © 2020-2023  润新知