背景
在前端開發中,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 = (request) => {
const token = uni.getStorageSync(TOKEN_KEY)
if (token) {
request.header.Authorization = token;
}
}
uni.addInterceptor("request", {
invoke: function (config) {
handleToken(config);
}
});
另外,系統通常會有多個環境。在這種情況下,可以根據不同的環境設置不同的 BASE_URL,這也可以通過攔截器來實現。
const BASE_URL = "";
const handleURL = (request) => {
const { url } = request;
if (!/https|http/.test(url)) {
request.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 = (request) => {
const { url } = request;
if (!/https|http/.test(url)) {
request.url = url.startsWith('/')
? `${BASE_URL}${url}`
: `${BASE_URL}/${url}`;
}
};
const handleToken = (request) => {
const token = uni.getStorageSync(TOKEN_KEY);
if (token) {
request.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": []
}
總結
我們完成了一個通用請求庫的封裝,這基本上可以滿足大多數業務需求。在具體請求中,狀態碼處理可以根據自身業務需求進行調整。
為了掌握上面的內容,需要清晰瞭解整個請求的完整流程。以下是整理的不同情況下的流程圖,可以參考學習。
如果大家覺得有幫助,請點贊、收藏、分享,謝謝!