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
往期文章推薦