如何解決非同步介面請求快慢不均導致的資料錯誤問題? - DevUI

DevUI團隊發表於2021-05-08

DevUI 是一款面向企業中後臺產品的開源前端解決方案,它倡導沉浸靈活至簡的設計價值觀,提倡設計者為真實的需求服務,為多數人的設計,拒絕譁眾取寵、取悅眼球的設計。如果你正在開發 ToB工具類產品,DevUI 將是一個很不錯的選擇!

Kagol.png

引言

搜尋功能,我想很多業務都會涉及,這個功能的特點是:

  • 使用者可以在輸入框中輸入一個關鍵字,然後在一個列表中顯示該關鍵字對應的資料;
  • 輸入框是可以隨時修改/刪除全部或部分關鍵字的;
  • 如果是實時搜尋?(即輸入完關鍵字馬上出結果,不需要額外的操作或過多的等待),介面呼叫將會非常頻繁。

實時搜尋都會面臨一個通用的問題,就是:

瀏覽器請求後臺介面都是非同步的,如果先發起請求的介面後返回資料,列表/表格中顯示的資料就很可能會是錯亂的。

問題重現

最近測試提了一個搜尋(PS:此處的搜尋?就是用 DevUI 新推出的 CategorySearch 元件實現的)相關的缺陷單,就涉及到了上述問題。

1-1.png

這個bug單大致意思是:

搜尋的時候,連續快速輸入或者刪除關鍵字,搜尋結果和搜尋關鍵字不匹配。

從缺陷單的截圖來看,本意是要搜尋關鍵字8.4.7迭代】,表格中的實際搜尋結果是8.4.7迭代】過關鍵字的資料。

缺陷單的截圖還非常貼心地貼了兩次請求的資訊:

2.png

作為一名“有經驗的”前端開發,一看就是一個通用的技術問題:

  1. 瀏覽器從伺服器發起的請求都是非同步的;
  2. 由於前一次請求伺服器返回比較慢,還沒等第一次請求返回結果,後一次請求就發起了,並且迅速返回了結果,這時表格肯定顯示後一次的結果;
  3. 過了2秒,第一次請求的結果才慢吞吞地返回了,這時表格錯誤地又顯示了第一次請求的結果;
  4. 最終導致了這個bug。

怎麼解決呢?

在想解決方案之前,得想辦法必現這個問題,靠後臺介面是不現實的,大部分情況下後臺介面都會很快返回結果。

所以要必現這個問題,得先模擬慢介面。

模擬慢介面

為了快速搭建一個後臺服務,並模擬慢介面,我們選擇 Koa 這個輕量的 Node 框架。

快速開始

Koa 使用起來非常方便,只需要:

  1. 新建專案資料夾:mkdir koa-server
  2. 建立 package.json:npm init -y
  3. 安裝 Koa:npm i koa
  4. 編寫服務程式碼:vi app.js
  5. 啟動:node app.js
  6. 訪問: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埠啟動任務服務,在瀏覽器訪問:

http://localhost:3000/

會顯示以下頁面:

3.png

啟動了我們的 Koa Server 之後,訪問:

http://localhost:3000/

會顯示:

4.png

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,再次訪問

http://localhost:3000/

會顯示以下內容:

4-1.png

post 請求

有了以上基礎,就可以寫一個 post 介面,模擬慢介面啦!

編寫 post 介面和 get 介面很類似:

router.post('/getList', async (ctx, next) => {
  ctx.response.body = {
    status: 200,
    msg: '這是post介面返回的測試資料',
    data: [1, 2, 3]
  };
});

這時我們可以使用 Postman 呼叫下這個 post 介面,如期返回:

5.png

允許跨域

我們嘗試在 NG CLI 專案裡呼叫這個 post 介面:

this.http.post('http://localhost:3000/getList', {
  id: 1,
}).subscribe(result => {
  console.log('result:', result);
});

但是在瀏覽器裡直接呼叫,卻得不到想要的結果:

  • result 沒有列印出來
  • 控制檯報錯
  • Network請求也是紅色的

6.png

由於本地啟動的專案埠號(4200)和 Koa Server 的(3000)不同,瀏覽器認為這個介面跨域,因此攔截了。

NG CLI 專案本地連結:

http://localhost:4200/

Koa Server 連結:

http://localhost:3000/

Koa 有一箇中介軟體可以允許跨域:koa2-cors

這個中介軟體的使用方式,和路由中介軟體很類似。

先安裝依賴:

npm i koa2-cors

然後引入:

const cors = require('koa2-cors');

再使用中介軟體:

app.use(cors());

這時我們再去訪問:

http://localhost:4200/

就能得到想要的結果啦!

7.png

慢介面

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 多才真正返回結果。

8.png

9.png

取消慢介面請求

能模擬慢介面,就能輕易地必現測試提的問題啦!

先必現這個問題,然後嘗試修復這個問題,最後看下這個問題還出不出現,不出現說明我們的方案能解決這個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 Admin V1.0 釋出啦!》

《讓我們一起建設 Vue DevUI 專案吧!? 》

《2021年最值得推薦的7個Angular前端元件庫》

相關文章