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

摘要:
此外,关于dva的效果是否支持异步/等待的讨论也暴露了dva在可扩展性方面的弱点。我们之所以需要建造轮子,是为了简要描述dva的现状。本文的出发点是dva的效果不支持异步/等待。使用过dva的人很清楚。dva的模型层使用生成器进行过程控制。尽管功能强大,但开发体验仍然不如async/await。因此,我想实现一个支持异步/等待的mini-dva版本。其他研发流程应尽可能与dva保持一致。

背景

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

福禄·研发中心福袋

免责声明:文章转载自《dva的effect那么难用,自己造一个轮子吧》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Nginx基础06:location语法Android 之采用execSQL和rawQuery方法完成数据的添删改查操作下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

【转】在 ASP.NET Core 中使用 IHttpClientFactory 发出 HTTP 请求

https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0#consumption-patterns 在 ASP.NET Core 中使用 IHttpClientFactory 发出 HTTP 请求 2021/01/21...

多页应用 Webpack4 配置优化与踩坑记录

前言 最近新起了一个多页项目,之前都未使用 webpack4 ,于是准备上手实践一下。这篇文章主要就是一些配置介绍,对于正准备使用 webpack4 的同学,可以做一些参考。 webpack4 相比之前的 2 与 3,改变很大。最主要的一点是很多配置已经内置,使得 webpack 能“开箱即用”。当然这个开箱即用不可能满足所有情况,但是很多以往的配置,其实...

10分钟学会React Context API

Create-react-app来学习这个功能: 注意下面代码红色的即可,非常简单. 在小项目里Context API完全可以替换掉react-redux. 修改app.js import React, { lazy, useState } from "react"; import { Button } from 'antd'; import { Hash...

react 监听页面滚动

1. onScrollCapture html: // 如果使用typescript, 定义dom类型 private dom: HTMLDivElement | null // ReactJS中,对Div监听只需要绑定 onScrollCapture事件 <div style={{ flex: 1,...

vue 查询分页

后端用的flask,前端vue,查询结果展示以及分页 如图: 代码如下: 前端: 1 <template> 2 <div> 3 <el-row> 4 <el-col :span="18"> 5 <el...

vue导出excel文件损坏

export function downloadFile(obj, name, suffix = "xlsx") { const url = window.URL.createObjectURL(new Blob([obj], {type: "application/vnd.ms-excel"})) const link = document.cr...