如何解决异步接口请求快慢不均导致的数据错误问题? - DevUI
DevUI 是一款面向企业中后台产品的开源前端解决方案,它倡导沉浸
、灵活
、至简
的设计价值观,提倡设计者为真实的需求服务,为多数人的设计,拒绝哗众取宠、取悦眼球的设计。如果你正在开发 ToB
的工具类产品
,DevUI 将是一个很不错的选择!
引言
搜索功能,我想很多业务都会涉及,这个功能的特点是:
- 用户可以在输入框中输入一个关键字,然后在一个列表中显示该关键字对应的数据;
- 输入框是可以随时修改/删除全部或部分关键字的;
- 如果是实时搜索(即输入完关键字马上出结果,不需要额外的操作或过多的等待),接口调用将会非常频繁。
实时搜索都会面临一个通用的问题,就是:
浏览器请求后台接口都是异步的,如果先发起请求的接口后返回数据,列表/表格中显示的数据就很可能会是错乱的。
问题重现
最近测试提了一个搜索(PS:此处的搜索就是用 DevUI 新推出的 CategorySearch 组件实现的)相关的缺陷单,就涉及到了上述问题。
这个bug单大致意思是:
搜索的时候,连续快速输入或者删除关键字,搜索结果和搜索关键字不匹配。
从缺陷单的截图来看,本意是要搜索关键字8.4.7迭代】
,表格中的实际搜索结果是8.4.7迭代】过
关键字的数据。
缺陷单的截图还非常贴心地贴了两次请求的信息:
作为一名“有经验的”前端开发,一看就是一个通用的技术问题:
- 浏览器从服务器发起的请求都是异步的;
- 由于前一次请求服务器返回比较慢,还没等第一次请求返回结果,后一次请求就发起了,并且迅速返回了结果,这时表格肯定显示后一次的结果;
- 过了2秒,第一次请求的结果才慢吞吞地返回了,这时表格错误地又显示了第一次请求的结果;
- 最终导致了这个bug。
怎么解决呢?
在想解决方案之前,得想办法必现这个问题,靠后台接口是不现实的,大部分情况下后台接口都会很快返回结果。
所以要必现这个问题,得先模拟慢接口。
模拟慢接口
为了快速搭建一个后台服务,并模拟慢接口,我们选择 Koa 这个轻量的 Node 框架。
快速开始
Koa 使用起来非常方便,只需要:
- 新建项目文件夹:
mkdir koa-server
- 创建 package.json:
npm init -y
- 安装 Koa:
npm i koa
- 编写服务代码:
vi app.js
- 启动:
node app.js
- 访问:
http://localhost:3000/
编写服务代码
使用以下命令创建 app.js 启动文件:
vi app.js
在文件中输入以下 3 行代码,即可启动一个 Koa 服务:
const Koa = require('koa'); // 引入 Koa
const app = new Koa(); // 创建 Koa 实例
app.listen(3000); // 监听 3000 端口
访问
如果没有在3000端口启动任务服务,在浏览器访问:
会显示以下页面:
启动了我们的 Koa Server 之后,访问:
会显示:
get 请求
刚才搭建的只是一个空服务,什么路由都没有,所以显示了Not Found
。
我们可以通过中间件的方式,让我们的 Koa Server 显示点儿东西。
由于要增加一个根路由,我们先安装路由依赖
npm i koa-router
然后引入 Koa Router
const router = require('koa-router')();
接着是编写get接口
app.get('/', async (ctx, next) => {
ctx.response.body = '<p>Hello Koa Server!</p>';
});
最后别忘了使用路由中间件
app.use(router.routes());
改完代码需要重启 Koa 服务,为了方便重启,我们使用 pm2 这个 Node 进程管理工具来启动/重启 Koa 服务,使用起来也非常简单:
- 全局安装 pm2:npm i -g pm2
- 启动 Koa Server:pm2 start app.js
- 重启 Koa Server:pm2 restart app.js
重启完 Koa Server,再次访问
会显示以下内容:
post 请求
有了以上基础,就可以写一个 post 接口,模拟慢接口啦!
编写 post 接口和 get 接口很类似:
router.post('/getList', async (ctx, next) => {
ctx.response.body = {
status: 200,
msg: '这是post接口返回的测试数据',
data: [1, 2, 3]
};
});
这时我们可以使用 Postman 调用下这个 post 接口,如期返回:
允许跨域
我们尝试在 NG CLI 项目里调用这个 post 接口:
this.http.post('http://localhost:3000/getList', {
id: 1,
}).subscribe(result => {
console.log('result:', result);
});
但是在浏览器里直接调用,却得不到想要的结果:
- result 没有打印出来
- 控制台报错
- Network请求也是红色的
由于本地启动的项目端口号(4200)和 Koa Server 的(3000)不同,浏览器认为这个接口跨域,因此拦截了。
NG CLI 项目本地链接:
Koa Server 链接:
Koa 有一个中间件可以允许跨域:koa2-cors
这个中间件的使用方式,和路由中间件很类似。
先安装依赖:
npm i koa2-cors
然后引入:
const cors = require('koa2-cors');
再使用中间件:
app.use(cors());
这时我们再去访问:
就能得到想要的结果啦!
慢接口
post 接口已经有了,怎么模拟慢接口呢?
其实就是希望服务器延迟返回结果。
在 post 接口之前增加延迟的逻辑:
async function delay(time) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve();
}, time);
});
}
await delay(5000); // 延迟 5s 返回结果
ctx.response.body = { ... };
再次访问 getList 接口,发现前面接口会一直pending
,5s 多才真正返回结果。
取消慢接口请求
能模拟慢接口,就能轻易地必现测试提的问题啦!
先必现这个问题,然后尝试修复这个问题,最后看下这个问题还出不出现,不出现说明我们的方案能解决这个bug,问题还有说明我们得想别的办法。
这是修复bug正确的打开方式。
最直观的方案就是再发起第二次请求之后,如果第一次请求未返回,那就直接取消这次请求,使用第二次请求的返回结果。
怎么取消一次http请求呢?
Angular 的异步事件机制是基于 RxJS 的,取消一个正在执行的 http 请求非常方便。
前面已经看到 Angular 使用 HttpClient 服务来发起 http 请求,并调用subscribe 方法来订阅后台的返回结果:
this.http.post('http://localhost:3000/getList', {
id: 1,
}).subscribe(result => {
console.log('result:', result);
});
要取消 http 请求,我们需要先把这个订阅存到组件一个变量里:
private getListSubscription: Subscription;
this.getListSubscription = this.http.post('http://localhost:3000/getList', {
id: 1,
}).subscribe(result => {
console.log('result:', result);
});
然后在重新发起 http 请求之前,取消上一次请求的订阅即可。
this.getListSubscription?.unsubscribe(); // 重新发起 http 请求之前,取消上一次请求的订阅
this.getListSubscription = this.http.post(...);
其他 http 库如何取消请求
至此这个缺陷算是解决了,其实这是一个通用的问题,不管是在什么业务,使用什么框架,都会遇到异步接口慢导致的数据错乱问题。
那么,如果使用 fetch 这种浏览器原生的 http 请求接口或者 axios 这种业界广泛使用的 http 库,怎么取消正在进行的 http 请求呢?
fetch
先来看下 fetch,fetch 是浏览器原生提供的 AJAX 接口,使用起来也非常方便。
使用 fetch 发起一个 post 请求:
fetch('http://localhost:3000/getList', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({
id: 1
})
}).then(result => {
console.log('result', result);
});
可以使用 AbortController
来实现请求取消:
this.controller?.abort(); // 重新发起 http 请求之前,取消上一次请求
const controller = new AbortController(); // 创建 AbortController 实例
const signal = controller.signal;
this.controller = controller;
fetch('http://localhost:3000/getList', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({
id: 1
}),
signal, // 信号参数,用来控制 http 请求的执行
}).then(result => {
console.log('result', result);
});
axios
再来看看 axios,先看下如何使用 axios 发起 post 请求。
先安装:
npm i axios
再引入:
import axios from 'axios';
发起 post 请求:
axios.post('http://localhost:3000/getList', {
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
data: {
id: 1,
},
})
.then(result => {
console.log('result:', result);
});
axios 发起的请求可以通过 cancelToken 来取消。
this.source?.cancel('The request is canceled!');
this.source = axios.CancelToken.source(); // 初始化 source 对象
axios.post('http://localhost:3000/getList', {
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
data: {
id: 1,
},
}, { // 注意是第三个参数
cancelToken: this.source.token, // 这里声明的 cancelToken 其实相当于是一个标记或者信号
})
.then(result => {
console.log('result:', result);
});
小结
本文通过实际项目中遇到的问题,总结缺陷分析和解决的通用方法,并对异步接口请求导致的数据错误问题进行了深入的解析。
加入我们
我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。
文/DevUI Kagol
往期文章推荐
如何解决异步接口请求快慢不均导致的数据错误问题? - DevUI的更多相关文章
- vue解决sass-loader的版本过高导致的编译错误
Module build failed: TypeError: this.getResolve is not a function at Object.loader (E:\appEx\PreRese ...
- LoadRunner11脚本小技能之同步/异步接口分离+批量替换请求头
最近在公司又进行了一次LoadRunner11性能测试,技能又get了一点,继续Mark起来!!! 一.异步/同步接口分离 之前在另一篇博文中有提到"事务拆分"的小节,即一个htm ...
- iOS 多个异步网络请求全部返回后再执行具体逻辑的方法
对于dispatch多个异步操作后的同步方法,以前只看过dispatch_group_async,看看这个方法的说明: * @discussion * Submits a block to a dis ...
- 接口请求失败处理,重新请求并限制请求次数.自己封装搞定retry函数
最近开发一款小程序的时候想到一个问题,如果接口突然挂掉怎么办呢,于是乎想到一个解决办法.接口请求重试功能.并限制请求次数 用最新的async函数语法实现.代码简洁明了. 测试代码如下: functio ...
- nodejs服务实现反向代理,解决本地开发接口请求跨域问题
前后端分离项目需要解决第一个问题就是,前端本地开发时如何解决通过ajax请求产生的跨域的问题.一般的做法是通过本地配置nginx反向代理进行处理的,除此之外,还可以通过nodejs来进行代理接口.当然 ...
- Android Asynchronous Http Client-Android异步网络请求客户端接口
1.简介 Android中网络请求一般使用Apache HTTP Client或者采用HttpURLConnect,但是直接使用这两个类库需要写大量的代码才能完成网络post和get请求,而使用and ...
- 【性能测试】:LR中解决接口请求中包含中文字符,服务器不识别的问题
在LR中,直接写的接口请求,如果请求字段包含中文字段,服务器会不识别,这个时候就要用到lr_convert_string_encoding这个函数: 具体用法: lr_convert_string_e ...
- LR中解决接口请求中包含中文字符,服务器不识别的问题
在LR中,直接写的接口请求,如果请求字段包含中文字段,服务器会不识别,这个时候就要用到lr_convert_string_encoding这个函数: 具体用法: lr_convert_string_e ...
- jquery.Deferred promise解决异步回调
我们先来看一下编写AJAX编码经常遇到的几个问题: 1.由于AJAX是异步的,所有依赖AJAX返回结果的代码必需写在AJAX回调函数中.这就不可避免地形成了嵌套,ajax等异步操作越多,嵌套层次就会越 ...
随机推荐
- FreeBSD 日常应用
freebsd日常应用 办公libreoffice或者apache openoffice 设计 图像编辑:gimp 矢量图设计:lnkscape 视频剪辑:openshot 视频特效:natron 编 ...
- [MongoDB知识体系] 一文全面总结MongoDB知识体系
MongoDB教程 - Mongo知识体系详解 本系列将给大家构建MongoDB全局知识体系.@pdai MongoDB教程 - Mongo知识体系详解 知识体系 学习要点 学习资料 官网资料 入门系 ...
- 三分钟教你提升应用推送的ROI
推送是App应用性价比最高也是最直接的营销运营手段,其细节颇多,非常考验运营人员的功力,本文将从ROI角度来分析怎么提升营销类推送的收益.(非IM类.系统类等功能服务型推送) 以一个日活100万的应用 ...
- Java 语言基础 (初识Java语言, 变量和数据类型, 运算符, 流程控制语句, 数组)
初始 Java 语言 Java SE -- Java Platform, Standard Edition 是 Java 平台的基础 Java SE 以前称为 J2SE, 可以编写桌面应用和基于 we ...
- 一招教你写博客,Typora+PicGo+阿里云oss,最好用的Markdown+最好用的图床工具!
博客 写博客的好处 1.使自己变得更善于观察.一旦你养成了记博客的习惯,与此同时你也赋予了一个更好的机会给自己,让自己去更细致地观察生活.一个人的生活经历本就是价值连城的,从中学习到的知识,教训更是异 ...
- P1601_A+B Problem(高精)(JAVA语言)
思路:BigInteger first blood! //四行搞定 题目背景 无 题目描述 高精度加法,x相当于a+b problem,[b][color=red]不用考虑负数[/color][/b] ...
- java中的String,StringBuffer与StringBuilder
String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁. StringBuffer对象则代表一个字符序列可变的字符串,当一个Stri ...
- 【Django笔记1】-视图(views)与模板(templates)
视图(views)与模板(templates) 1,视图(views) 将接收到的数据赋值给模板(渲染),再传递给浏览器.HTML代码可以直接放在views.py(文件名可任意更换),也可以放在t ...
- DAOS 分布式异步对象存储|数据平面
DAOS 通过两个紧密集成的平面进行运转.数据平面处理繁重的运输操作,而控制平面负责进程编排和存储管理,简化数据平面的操作. 模块接口 I/O 引擎支持一个模块接口,该接口允许按需加载服务器端代码.每 ...
- [枚举]P1089 津津的储蓄计划
津津的储蓄计划 题目描述 津津的零花钱一直都是自己管理.每个月的月初妈妈给津津300元钱,津津会预算这个月的花销,并且总能做到实际花销和预算的相同. 为了让津津学习如何储蓄,妈妈提出,津津可以随时把整 ...