背景
在前端開發中,HTTP 請求是與伺服器進行資料互動的核心手段。無論是獲取資料還是提交資料,前端應用幾乎都離不開 HTTP 請求。在 uniapp 中,uni.request
是官方提供的用於發起 HTTP 請求的基礎 API。然而,直接使用 uni.request
存在一些問題和不足,比如:
- 程式碼冗餘:每次發起請求時都需要編寫類似的配置程式碼,導致程式碼重複。
- 缺乏統一管理:沒有統一的地方管理請求引數、頭資訊、錯誤處理等,使得程式碼不易維護
意義
- 簡化請求配置:在每次發起請求時,通常需要配置很多引數,比如 URL、請求頭、請求體等。透過封裝請求庫,可以設定預設的請求引數,簡化每次請求的配置操作,減少開發人員的工作量,提高開發效率。
- 管理請求憑證:透過封裝請求庫,可以集中管理憑證,確保每次請求都自動攜帶正確的憑證。
- 便於維護和擴充套件:封裝請求庫後,如果需要對請求邏輯進行修改或擴充套件,只需要在封裝庫中進行調整,而不需要在專案的各個地方逐一修改。此外,如果需要將請求庫更換為其他庫(例如 Axios),只需修改封裝的請求庫部分,而無需改動業務程式碼。
- 提高使用者體驗:透過統一處理全域性請求
Loading
狀態,可以在請求進行中顯示載入提示,提升使用者體驗。
實現思路
1. 把 uni.request 改為支援 Promise 呼叫方式
將 uni.request
改為支援 Promise
呼叫方式的好處是可以避免回撥巢狀問題,並且可以藉助 async/await
實現同步呼叫。
實現方式大概有如下兩種:
1.1 透過 uni 自身提供的方法
呼叫 uni.request
時,如果不傳入 success
、fail
、complete
回撥函式,uni.request
的返回值將是一個 Promise
物件。
uni.request({
url: "",
// ... 其他配置
}).then(()=> {
}).catch(()=> {
}).finally(()=> {
});
1.2 透過 Promise 包裝
new Promise((resolve, reject)=> {
uni.request({
url: "",
success(res) {
resolve(res);
},
fail(error) {
reject(error);
},
complete() {
}
});
});
具體採用哪種方式都可以,這裡選擇第一種。
2. 定義預設請求引數
在請求時,通常需要設定 content-type
、timeout
等資訊。這些引數通常不會改變,因此可以設計為預設引數,同時保留外部覆蓋預設引數值的能力。
2.1 定義預設引數
// 定義預設引數
const defaultOptions = {
timeout: 15000,
dataType: "json",
header: {
"content-type": "application/json",
}
};
2.2 合併外部引數與預設引數
提供外部覆蓋預設引數值的能力
const defaultConfig = {
timeout: 15000,
dataType: 'json',
header: {
'content-type': 'application/json',
},
};
const wrapRequest = ({
url = '',
data = {},
method = "GET",
header = {}
} = {}) => {
return uni.request({
...defaultConfig,
url,
data,
method,
header: {
...defaultOptions.header,
...header
}
});
}
3. 統一處理請求憑證
在大多數系統中,介面請求通常需要傳遞使用者憑證。通常的做法是在請求的 Header
中新增 Authorization
屬性。為了簡化這個過程,可以透過攔截器來實現。
const TOKEN_KEY = 'token';
// 處理 token
const handleToken = (config) => {
const token = uni.getStorageSync(TOKEN_KEY)
if (token) {
config.header.Authorization = token;
}
}
uni.addInterceptor("request", {
invoke: function (config) {
handleToken(config);
}
});
另外,系統通常會有多個環境。在這種情況下,可以根據不同的環境設定不同的 BASE_URL
,這也可以透過攔截器來實現。
const BASE_URL = "";
const handleURL = (config) => {
const { url } = config;
if (!/https|http/.test(url)) {
config.url = url.startsWith("/")
? `${BASE_URL}${url}`
: `${BASE_URL}/${url}`;
}
}
uni.addInterceptor("request", {
invoke: function (config) {
handleURL(config);
}
});
如果有其他處理需求,可以直接在這裡新增。
4. 統一處理公共響應狀態碼
為了避免在多個地方處理公共的錯誤邏輯,例如憑證無效時跳轉到登入頁、移除本地 token 等,我們可以在全域性請求響應攔截器中集中處理這些問題。
const LOGIN_INVALID_CODE_LIST = ["INVALID_TOKEN", "EXPIRED_TOKEN"];
const SUCCESS = "SUCCESS";
uni.addInterceptor("request", {
success(res){
const { data: resData } = res;
const { code, message } = resData;
if (code !== SUCCESS) {
// 如果響應程式碼在登入無效程式碼列表中
if (LOGIN_INVALID_CODE_LIST.includes(code)) {
uni.showToast({
title: message,
icon: "none",
});
uni.navigateTo({
url: "/pages/login/login"
});
return;
} else {
// 處理其他錯誤程式碼
return Promise.reject(resData)
}
}
return Promise.resolve(resData)
},
});
5. 封裝公共方法 GET、POST、DEL、PUT
為了進一步簡化請求引數,可以提供一系列方法,例如 GET
、POST
、DELETE
、PUT
。
export const get = (params) => wrapRequest({ ...params, method: 'GET' });
export const post = (params) => wrapRequest({ ...params, method: 'POST' });
export const put = (params) => wrapRequest({ ...params, method: 'PUT' });
export const del = (params) => wrapRequest({ ...params, method: 'DELETE' });
這樣做的好處,它消除了每次呼叫時顯式傳入 HTTP
方法的需要,使程式碼更簡潔、更易讀。這樣做的好處是你在呼叫這些方法時只需關注請求引數,而不需要重複指定 HTTP
方法。
6. 定義全域性請求 Loading
在正常情況下,我們的介面通常會很快完成。然而,考慮到不同網路狀況下,介面響應速度可能會變慢,從而增加使用者的等待時間。為了最佳化使用者體驗,我們可以在全域性請求中新增 Loading
提示,這將大大提升使用者體驗。
const showLoading = () => {
uni.showLoading({
title: '載入中',
});
};
const hideLoading = () => {
uni.hideLoading();
};
uni.addInterceptor("request", {
invoke: function (request) {
showLoading();
return request;
},
complete() {
hideLoading();
}
});
這樣每個介面請求時都會觸發顯示 Loading
。考慮到某些介面可能不需要顯示 Loading
,我們可以允許使用者在定義介面時明確控制是否展示 Loading
。
const showLoading = (loading) => {
uni.showLoading({
title: '載入中',
});
};
const hideLoading = (loading) => {
uni.hideLoading();
};
uni.addInterceptor("request", {
invoke: function (config) {
if (config.loading) {
showLoading();
}
return request;
},
complete() {
hideLoading();
}
});
const wrapRequest = ({
url = '',
data = {},
method = 'GET',
header = {},
loading = true // 預設是展示 loading
} = {}) => {
return uni.request({
...defaultConfig,
url,
data,
method,
loading,
header: {
...defaultOptions.header,
...header,
},
});
};
為了解決介面請求很快時 Loading
閃爍的問題,我們可以新增一個延遲引數。如果請求時間超過 50ms
(具體閥值可以自己去定義) 才顯示 Loading
,否則就不展示:
const LOADING_DELAY = 50; // 50ms 延遲
let loadingTimer;
const showLoading = () => {
uni.showLoading({
title: '載入中',
});
};
const hideLoading = () => {
uni.hideLoading();
};
uni.addInterceptor("request", {
invoke: function (config) {
if (config.loading) {
loadingTimer = setTimeout(showLoading, LOADING_DELAY);
}
return config;
},
complete() {
clearTimeout(loadingTimer);
hideLoading();
}
});
7. 完整程式碼如下
const defaultOptions = {
timeout: 15000,
dataType: 'json',
header: {
'content-type': 'application/json',
},
};
const TOKEN_KEY = 'token';
const BASE_URL = '';
const LOGIN_INVALID_CODE_LIST = ['INVALID_TOKEN', 'EXPIRED_TOKEN'];
const SUCCESS = 'SUCCESS';
const LOADING_DELAY = 50; // 50ms 延遲
let loadingTimer;
const handleURL = (config) => {
const { url } = config;
if (!/https|http/.test(url)) {
config.url = url.startsWith('/')
? `${BASE_URL}${url}`
: `${BASE_URL}/${url}`;
}
};
const handleToken = (config) => {
const token = uni.getStorageSync(TOKEN_KEY);
if (token) {
config.header.Authorization = token;
}
};
const showLoading = () => {
uni.showLoading({
title: '載入中',
});
};
const hideLoading = () => {
uni.hideLoading();
};
uni.addInterceptor('request', {
invoke: function (config) {
if (config.loading) {
loadingTimer = setTimeout(showLoading, LOADING_DELAY);
}
handleURL(config);
handleToken(config);
},
success(res) {
const { data: resData } = res;
const { code, message } = resData;
if (code !== SUCCESS) {
// 如果響應程式碼在登入無效程式碼列表中
if (LOGIN_INVALID_CODE_LIST.includes(code)) {
uni.showToast({
title: message,
icon: 'none',
});
uni.navigateTo({
url: '/pages/login/login',
});
return;
} else {
// 處理其他錯誤程式碼
return Promise.reject(resData);
}
}
return Promise.resolve(resData);
},
complete() {
clearTimeout(loadingTimer);
hideLoading();
},
});
const wrapRequest = ({
url = '',
data = {},
method = 'GET',
header = {},
loading = true,
} = {}) => {
return uni.request({
...defaultOptions,
url,
data,
method,
loading,
header: {
...defaultOptions.header,
...header,
},
});
};
export const get = (params) => wrapRequest({ ...params, method: 'GET' });
export const post = (params) => wrapRequest({ ...params, method: 'POST' });
export const put = (params) => wrapRequest({ ...params, method: 'PUT' });
export const del = (params) => wrapRequest({ ...params, method: 'DELETE' });
8. 測試
import { get } from '@/utils/request';
get({
url: "https://api.aigcway.com/aigc/chat-category/list"
}).then((res)=> {
console.log(res);
})
輸出如下:
{
"code": "SUCCESS",
"message": "操作成功",
"data": []
}
總結
我們完成了一個通用請求庫的封裝,這基本上可以滿足大多數業務需求。在具體請求中,狀態碼處理可以根據自身業務需求進行調整。
為了掌握上面的內容,需要掌握 uni.addInterceptor
、uni.request
執行的完整流程。以下是整理的不同情況下的流程圖,可以參考學習。
上面流程圖對應示例程式碼:
uni.addInterceptor('request', {
invoke: function (config) {
console.log("interceptor invoke");
},
success(res) {
console.log("interceptor success");
},
complete() {
console.log("interceptor complete");
},
});
uni.request({
url: ""
}).then(()=> {
console.log("then");
}).catch(()=> {
console.log("catch");
}).finally(()=> {
console.log("finally");
})
上面流程圖對應示例程式碼:
uni.addInterceptor('request', {
invoke: function (config) {
console.log("interceptor invoke");
},
success(res) {
console.log("interceptor success");
},
complete() {
console.log("interceptor complete");
},
});
uni.request({
success(){
console.log("success");
},
fail(){
console.log("fail");
},
complete(){
console.log("complete");
}
});
如果大家覺得有幫助,請點贊、收藏、分享,謝謝!