使用 Dva 开发复杂 SPA

到了真实的业务开发过程中,会遇到许许多多不能用那些基本操作覆盖的场景,本文尝试列举一些常见的需求在dva中的实现方式。

动态加载model

有不少业务场景下,我们可能会定义出很多个model,但并不需要在应用启动的时候就全部加载,比较典型的是各类管理控制台。如果每个功能页面是通过路由切换,互相之间没有关系的话,通常会使用webpack的require.ensure来做代码模块的懒加载。

我们也可以利用这个特性来做model的动态加载。

function RouterConfig({ history, app }) {const routes = [{path: '/',name: 'IndexPage',getComponent(nextState, cb) {require.ensure([], (require) => {registerModel(app, require('./models/dashboard'));cb(null, require('./routes/IndexPage'));});},},{path: '/users',name: 'UsersPage',getComponent(nextState, cb) {require.ensure([], (require) => {registerModel(app, require('./models/users'));cb(null, require('./routes/Users'));});},},];return <Router history={history} routes={routes} />;
}

这样,在视图切换到这个路由的时候,对应的model就会被加载。同理,也可以做model的动态移除,不过,一般情况下是不需要移除的。

使用model共享全局信息

在上一节我们提到,可以动态加载model,也可以移除。从这个角度看,model是可以有不同生命周期的,有些可以与功能视图伴随,而有些可以贯穿整个应用的生命周期。

在上一节我们提到,可以动态加载model,也可以移除。从这个角度看,model是可以有不同生命周期的,有些可以与功能视图伴随,而有些可以贯穿整个应用的生命周期。

从业务场景来说,有不少场景是可以做全局model的,比如说,我们在路由之间前进后退,model可以用于在路由间共享数据,比较典型的,像列表页和详情页的互相跳转,就可以用同一份model去共享它们的数据。

注意,如果当前应用中加载了不止一个model,在其中一个的effect里面做select操作,是可以获取另外一个中的state的:

*foo(action, { select }) {const { a, b } = yield select();
}

这里,a,b可以分别是两个不同model的state。所以,借助这个特点,我们就不必非要把model按照视图的结构进行组织,可以适当按照业务分类,把一些数据存在对应业务的model中,分别通过不同的effect去更新,在获取的地方再去组合,这样可以使得model拥有更好的复用性。

model的复用

有时候,业务上可能遇到期望把一些与外部关联较少的model拆出来的需求,我们可能会拆出这样的一个model,然后用不同的视图容器去connect它。

export default {namespace: 'reusable',state: {},reducers: {},effects: {}
}

所以,在业务上,可能出现的使用情况就是:

  ContainerA <-- ModelA|------------------------------|                            |
ContainerB <-- reusable     ContainerC <-- reusable

这里面,ContainerB和ContainerC是ContainerA的下属,它们的逻辑结构一致,只是展现不同。我们可以让它们分别connect同一个model,注意,这个时候,model的修改会同时影响到两个视图,因为model在state中是直接以namespace作key存放的,实际上只有一份实例。

动态扩展model

在上一节中,我们提到可以把model进行分类,以实现在若干视图中的共享,但业务需求是比较多变的,很可能我们又会遇到这种情况:

几个业务视图长得差不多,model也存在少量差别

这个情况下,如果我们让它们复用同一个model也可以,但这么做,对维护是一种挑战,很可能改其中一个,对另外一些造成了影响,所以这种情况下,可能会期望能够对model进行扩展。

所谓扩展,通常是要做几个事情:

  • 新增一些东西
  • 覆盖一些原有的东西
  • 根据条件动态创建一些东西

注意到dva中的每个model,实际上都是普通的JavaScript对象,包含
namespace
state
reducers
effects
subscriptions

从这个角度看,我们要新增或者覆盖一些东西,都会是比较容易的,比如说,使用Object.assign来进行对象属性复制,就可以把新的内容添加或者覆盖到原有对象上。

注意这里有两级,model结构中的state,reducers,effects,subscriptions都是对象结构,需要分别在这一级去做assign。

可以借助dva社区的dva-model-extend库来做这件事。

换个角度,也可以通过工厂函数来生成model,比如:

function createModel(options) {const { namespace, param } = options;return {namespace: `demo${namespace}`,states: {},reducers: {},effects: {*foo() {// 这里可以根据param来确定下面这个call的参数yield call()}}};
}const modelA = createModel({ namespace: 'A', param: { type: 'A' } });
const modelB = createModel({ namespace: 'A', param: { type: 'B' } });

这样,也能够实现对model的扩展。

长流程的业务逻辑

在业务中,有时候会出现较长的流程,比如说,我们的一个复杂表单的提交,中间会需要去发起多种对视图状态的操作:
这是一个真实业务

*submit(action, { put, call, select }) {const formData = yield select(state => {const buyModel = state.buy;const context = state.context;const { stock } = buyModel;return {uuid: context.uuid,market: stock && stock.market,stockCode: stock && stock.code,stockName: stock && stock.name,price: String(buyModel.price),// 委托数量entrustAmount: String(buyModel.count),totalBalance: buyModel.totalBalance,availableTzbBalance: buyModel.availableTzbBalance,availableDepositBalance: buyModel.availableDepositBalance,};});const result = yield call(post, '/h5/ajax/trade/entrust_buy', formData, { loading: true });if (result.success) {toast({type: 'success',content: '委托已受理',});// 成功之后再获取一次现价,并填入// yield put({type: 'fetchQuotation', payload: stock});yield put({ type: 'entrustNoChange', payload: result.result && result.result.entrustNo });// 清空输入框内容yield put({ type: 'searchQueryChange', value: '' });}// 403时,需要验证密码再重新提交if (!result.success && result.resultCode === 403) {yield put({ type: 'checkPassword', payload: {} });return;}// 失败之后也需要更新投资宝和保证金金额if (result.result) {yield put({ type: 'balanceChange', payload: result.result });}// 重新获取最新可撤单列表yield put({ type: 'fetchRevockList' });// 返回的结果里面如果有uuid, 用新的uuid替换if (result.uuid) {yield put({ type: 'context/updateUuid', payload: result.uuid });}
},

在一个effect中,可以使用多个put来分别调用reducer来更新状态。

存在另外一些流程,在effect中可能会存在多个异步的服务调用,比如说,要调用一次服务端的验证,成功之后再去提交数据,这时候,在一个effect中就会存在多个call操作了。

使用take操作进行事件监听
与上一节提到的情况相比,我们还可能遇到另外一些场景,比如:

一个流程的变动,需要扩散到若干个其他model中

这个需求其实也覆盖了上一节这种,但在这一节中,我们侧重讨论比较通用的这类需求的处理方式。

在redux-saga中,提供了take和takeLatest这两个操作,dva是redux-saga的封装,也是可以使用这种操作的。

要理解take操作的语义,可以参见这两种示例的对比:

假设我们有一个事件处理的代码:

someSource.on('click', event => doSomething(event))

这段代码转成用generator来表达,就是下面这个形式:

function* saga() {while(true) {const event = yield take('click');doSomething(event);}
}

所以,我们也可以在dva中使用take操作来监听action。

任务的并行执行
如果想要让任务并行执行,可以通过下面这种方式:

const [result1, result2]  = yield all([call(service1, param1),call(service2, param2)
])

把多个要并行执行的东西放在一个数组里,就可以并行执行,等所有的都结束之后,进入下个环节,类似promise.all的操作。一般有一些集成界面,比如dashboard,其中各组件之间业务关联较小,就可以用这种方式去分别加载数据,此时,整体加载时间只取决于时间最长的那个。

注意:上面代码中的那个:

yield [];

不要写成:

yield* [];

这两者含义是不同的,后者会顺序执行。

任务的竞争
如果多个任务之间存在竞争关系,可以通过下面这种方式:

const { data, timeout } = yield race({data: call(service, 'some data'),timeout: call(delay, 1000)
});if (data)put({type: 'DATA_RECEIVED', data});
elseput({type: 'TIMEOUT_ERROR'});

这个例子比较巧妙地用一个延时一秒的空操作来跟一个网络请求竞争,如果到了一秒,请求还没结束,就让它超时。

这个类似于Promise.race的作用。

跨model的通信

当业务复杂的情况下,我们可能会对model进行拆分,但在这种情况下,往往又会遇到一些比较复杂的事情,比如:

一个流程贯穿多个model

对这个事情,我们可能有若干中不同的解决办法。假设有如下场景:

  • 父容器A,子容器B,二者各自connect了不同的model A和B

  • 父容器中有一个操作,分三个步骤:

    model A中某个effect处理第一步
    call model B中的某个effect去处理第二步
    第二步结束后,再返回model A中做第三步

在dva中,可以用namespace去指定接受action的model,所以可以通过类似这样的方式去组合:

yield call({ type: 'a/foo' });
yield call({ type: 'b/foo' });
yield call({ type: 'a/bar' });

甚至,还可以利用take命令,在另外一个model的某个effect中插入逻辑:

*effectA() {yield call(service1);yield put({ type: 'service1Success' });// 如果我们复用这个effect,但要在这里加一件事,怎么办?yield call(service2);yield put({ type: 'service2Success' });
}

可以利用之前我们说的take命令:

yield take('a/service1Success');

这样,可以在外部往里面添加一个并行操作,通过这样的组合可以处理一些组合流程。但实际情况下,我们可能要处理的不仅仅是effect,很可能视图组件中还存在后续逻辑,在某个action执行之后,还需要再做某些事情。
比如:

yield call({ type: 'a/foo' });
yield call({ type: 'b/foo' });
// 如果这里是要在组件里面做某些事情,怎么办?

可以利用一些特殊手段把流程延伸出来到组件里。比如说,我们通常在组件中dispatch一个action的时候,不会处理后续事情,但可以修改这个过程:

new Promise((resolve, reject) => {dispatch({ type: 'reusable/addLog', payload: { data: 9527, resolve, reject } });
})
.then((data) => {console.log(`after a long time, ${data} returns`);
});

注意这里,我们是把resolve和reject传到action里面了,所以,只需在effect里面这样处理:

try {const result = yield call(service1);yield put({ type: 'service1Success', payload: result });resolve(result);
}
catch (error) {yield put({ type: 'service1Fail', error });reject(ex);
}

这样,就实现了跨越组件、模型的复杂的长流程的调用。

使用 Dva 开发复杂 SPA相关推荐

  1. React+DVA开发实践

    原文链接 文档概述 本文档在前面章节简单的介绍了React和其相关的一系列技术,最后章节介绍了React+Dva开发的整套过程和基本原理,也就是将一系列框架整合的结果. 文档结构 本文档划分为以下章节 ...

  2. umi 如何配置webpack_umi+dva开发环境+经常使用配置和webpack配置

    安装(官方文档:https://umijs.org/zh):javascript yarn global add umi 使用:css 使用umi -v可查看版本,确保全局安装没问题java umi ...

  3. React组件设计实践总结05 - 状态管理

    今天是 520,这是本系列最后一篇文章,主要涵盖 React 状态管理的相关方案. 前几篇文章在掘金首发基本石沉大海, 没什么阅读量. 可能是文章篇幅太长了?掘金值太低了? 还是错别字太多了? 后面静 ...

  4. Web 开发在 2015 年及未来的发展趋势

     Web 开发在 2015 年及未来的发展趋势 本文中,我们将一同看看当今 Web 开发的发展趋势,给大家分享我对 2015 年及未来的一些看法.观察和预测.我从 2000 年就开始做 Web 技 ...

  5. 温故知新,.Net Core遇见Blazor(FluentUI),属于未来的SPA框架

    什么是Blazor Blazor是一个使用.NET生成交互式客户端WebUI的框架: 使用C#代替JavaScript来创建信息丰富的交互式UI. 共享使用.NET编写的服务器端和客户端应用逻辑. 将 ...

  6. BeetleX.WebFamily针对Web SPA应用的改进

    BeetleX.WebFamily1.0在集成vue+element+axios的基础上添加应用页.窗体布局和登陆验证等功能.通过以上功能开发Web SPA应用时只需要编写vue控件和配置菜单即可实现 ...

  7. 构建现代Web应用时究竟是选择传统web应用还是SPA

    在大前端盛行的今天,似乎前后端分离的开发模式才是大势所趋,而SPA的概念更是应运而生.现在随便构建一个web应用程序如果你不是使用SPA的话,就会感觉有点low,但是真的是这样吗?今天这篇文章我们就来 ...

  8. cs架构用什么语言开发_我为什么建议Python开发者将ES6作为第二语言

    ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了.它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应 ...

  9. SPA (单页应用程序)

    单页Web应用 编辑 单页Web应用(single page web application,SPA),就是只有一张Web页面的应用.单页应用程序 (SPA) 是加载单个HTML 页面并在用户与应用程 ...

最新文章

  1. 实用 | 从Apache Kafka到Apache Spark安全读取数据
  2. java中获取当前服务器的Ip地址
  3. MOBA项目定点数的一个想法
  4. YTU 2887: D--机器人Bill
  5. 用数学方法分析哪类游戏中的AI难度最大
  6. Java ExecutorService 线程池
  7. JavaScript如何简单而准确地判断复杂数据类型
  8. 【渝粤教育】国家开放大学2018年秋季 0161-22T教师职业道德 参考试题
  9. 详细记录丨Realtek RTL8188FU WiFi 驱动移植
  10. Oracle数据库实现主键自增(利用sequence)和分页查询(利用rownum)
  11. if和else同时执行_为什么大量的if else这么不受待见?怎么“干掉”它?
  12. 令牌环(Token Ring)
  13. 使用axure9绘制三级导航
  14. winpe下安装linux工具箱,(U盘中安装WinPE、Ubuntu、BT3、CDLinux系统和DOS工具箱等工具的方法.doc...
  15. 关于iOS 报Command failed with exit 128: git错误额解决方案
  16. smarty-wap端
  17. 常用温度控制方法原理
  18. Directory 与 DirectoryInfo 的区别
  19. python黄金走势预测_python实时获取和讯网纸黄金价格信息
  20. jQuery前端开发学习指南(11)——jQuery属性过滤选择器

热门文章

  1. Linux配置SSH免密码登录(非root账号)
  2. 如何使用爬虫采集搜狐汽车新车资讯
  3. 7月编程排行榜新鲜出炉,再次上演神仙打架!
  4. 计算机类ei和sci期刊,请教大家计算机领域数据挖掘方面有哪些比较好中的EI期刊和SCI期刊 - 论文投稿 - 小木虫 - 学术 科研 互动社区...
  5. 应用电路笔记(1)-三极管8550和8050应用
  6. 计算机科学给稿费多少,科学网—千字千元的稿费标准高吗? - 籍利平的博文
  7. setImageBitmap 图片太大部分机型不显示
  8. Mac版word空格变成小点,多了很多“分节符(下一页)”和“窗体顶端”和“窗体底端”等字样,怎么解决?
  9. 微信小程序使用腾讯地图进行路线规划,坐标转地址,逆地理编码,计算目的地跟自身定位的距离
  10. 高次同余式的解数和解法