背景

对于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那么难用,自己造一个轮子吧的更多相关文章

  1. 造一个轮子然后安装到pypi上

    之前写了一个爬虫的包,主要是根据自己写爬虫的情况总结一下. 因为每次都要重复写一些代码,所以提炼出来,类似一个框架的样子吧. 开始是放在自己的项目里引用,但如果换了一个项目,就得重新拷一遍,很麻烦. ...

  2. 【记录】如何造一个vite插件(1)

    在看文章前,先做个定位,这不是一篇纯粹的技术性文章,可以把它理解成一个叙述文章,记录我开发插件的过程. 开始前简单的吹个牛 vue2 也写了很多年了,多人合作始终避不开用到别人的组件.关键是有些组件没 ...

  3. Asp.net Mvc 请求是如何到达 MvcHandler的——UrlRoutingModule、MvcRouteHandler分析,并造个轮子

    这个是转载自:http://www.cnblogs.com/keyindex/archive/2012/08/11/2634005.html(那个比较容易忘记,希望博主不要生气的) 前言 本文假定读者 ...

  4. 「造个轮子」——cicada 设计一个配置模块

    前言 在前两次的 cicada 版本中其实还不支持读取配置文件,比如对端口.路由的配置. 因此我按照自己的想法创建了一个 issue ,也收集到了一些很不错的建议. 最终其实还是按照我之前的想法来做了 ...

  5. 「造个轮子」——cicada 源码分析

    前言 两天前写了文章<「造个轮子」--cicada(轻量级 WEB 框架)> 向大家介绍了 cicada 之后收到很多反馈,也有许多不错的建议. 同时在 GitHub 也收获了 80 几颗 ...

  6. 造个轮子之基于 Netty 实现自己的 RPC 框架

    原文地址: haifeiWu和他朋友们的博客 博客地址:www.hchstudio.cn 欢迎转载,转载请注明作者及出处,谢谢! 服务端开发都会或多或少的涉及到 RPC 的使用,当然如果止步于会用,对 ...

  7. 我厌倦了 Redux,那就造个轮子 Rectx:第三集

    仓库:215566435/rectx 前言 麻烦快去我的仓库里面喷: 老子学不动了,求不要更新. 呵呵,你没想到吧,这玩意儿竟然有第三集!我靠,我自己都没想到,让我们悄悄的回顾一下前两集完全没想到,竟 ...

  8. 尝鲜刚发布的 SpringFox 3.0.0,以前造的轮子可以不用了...

    最近 SpringFox 3.0.0 发布了,距离上一次大版本2.9.2足足有2年多时间了.可能看到这个名字,很多读者会有点陌生.但是,只要给大家看一下这两个依赖,你就知道了! <depende ...

  9. RPC基础以及造一个RPC的轮子需要注意些什么

    RPC基础以及造一个RPC的轮子需要注意些什么 前言 rpc即远程过程调用,是分布式系统常用的通信方法.远程可以是在一台机器上的不同进程或在不同一个机器上的不同进程.rpc更看重速度,像调用本地方法一 ...

随机推荐

  1. Docker系列——Grafana+Prometheus+Node-exporter钉钉推送(四)

    近期搭建的服务器监控平台,来进行一个总结.主要分为监控平台的搭建.告警中心的配置以及消息的推送.推送的话,支持多种终端.具体详细可查看之前的博文,在这里罗列下,方便查看. Docker系列--Graf ...

  2. 【NX二次开发】Block UI 超级截面

    属性说明 属性   类型   描述   常规           BlockID    String    控件ID    Enable    Logical    是否可操作    Group    ...

  3. NX二次开发-矩阵乘矩阵的几何意义

    函数:UF_MTX3_multiply() 或者UF_MTX3_multiply_t().推荐使用UF_MTX3_multiply() 函数说明:矩阵相乘,得到新的矩阵,如下图WCS与ABS重合,在暗 ...

  4. Quill基本使用和配置 - DevUI

    DevUI 是一款面向企业中后台产品的开源前端解决方案,它倡导沉浸.灵活.至简的设计价值观,提倡设计者为真实的需求服务,为多数人的设计,拒绝哗众取宠.取悦眼球的设计.如果你正在开发 ToB 的工具类产 ...

  5. C#串口通信——DtrEnable 和RtsEnable 两个属性

    转自 http://www.cnblogs.com/hengbo/archive/2011/12/19/2293272.html 在开发中有些串口设备需要串口供电(本人在开发门禁系统时,对起落杆进行控 ...

  6. Electron-Vite2-MacUI桌面管理框架|electron13+vue3.x仿mac桌面UI

    基于vue3.0.11+electron13仿制macOS桌面UI管理系统ElectronVue3MacUI. 前段时间有分享一个vue3结合electron12开发后台管理系统项目.今天要分享的是最 ...

  7. vue 快速入门 系列 —— vue loader 上

    其他章节请看: vue 快速入门 系列 vue loader 上 通过前面"webpack 系列"的学习,我们知道如何用 webpack 实现一个不成熟的脚手架,比如提供开发环境和 ...

  8. Binding(四):数据校验

    ​    除了上一节讲的类型转换器,Binding还自带数据校验功能,这节主要来讲一下. 跟类型转换器一样,数据校验需要我们继承ValidationRule类,实现其中的Validate方法,并写入我 ...

  9. 图的存储与遍历C++实现

    1.图的存储 设点数为n,边数为m 1.1.二维数组 方法:使用一个二维数组 adj 来存边,其中 adj[u][v] 为 1 表示存在 u到 v的边,为 0 表示不存在.如果是带边权的图,可以在 a ...

  10. CRM系统用多种形式与客户无障碍沟通

    当您与潜在客户.联系人交流,或者团队内部沟通时,是不是经常要在CRM系统和第三方应用之间来回切换,仅仅是为了打电话.开会等,简直不要太麻烦.而CRM客户管理系统与Zoho Meeting的集成,就能够 ...