封裝 fetch 與 Error,返回 Promise 物件
阿新 • • 發佈:2021-07-20
fetch在目前已經是很成熟的請求資源的方法,但為了方便在專案中呼叫,一般都會進行二次封裝
一、定義錯誤型別
對於封裝公共元件或方法,一定要多想,七分設計,三分開發
而對於一個網路請求來說,除了處理請求體、響應體之外,還有一個常常被忽略的環節,那就是定義 Error
專案中關於網路請求的錯誤有很多種,比如超時錯誤、伺服器錯誤、斷網錯誤,這些都可以加以封裝
// errors.js
export class ApiError extends Error {
constructor(message, url) {
super(message);
this.message = message;
this.name = 'ApiError';
this.url = url;
}
}
export class DisconnectError extends ApiError {
constructor(url) {
super('網路已斷開,請重新連線', url);
this.name = 'DisconnectError';
}
}
export class ApiServerError extends ApiError {
constructor(statusCode, url) {
super(`請求伺服器出錯:${statusCode}`, url);
this.name = 'ApiServerError';
this.statusCode = statusCode;
}
}
export class ApiJsonError extends ApiError {
constructor(url) {
super('請求伺服器出錯:無法轉換為JSON', url);
this.name = 'ApiJsonError';
}
}
export class ApiTimeoutError extends ApiError {
constructor(time, url) {
super('請求超時 ', url);
this.name = 'ApiTimeoutError';
this.time = time;
}
}
上面定義了幾種常見的請求錯誤,其實還可以定義一種業務錯誤
比如某個請求的狀態是 200,但不符合後端定義的業務邏輯,返回了特殊的 code
這時就可以根據後端返回的 code 進行業務錯誤的封裝
二、丟擲錯誤
在丟擲上面定義的 Error 的時候,需要做一些判斷,這部分邏輯可以抽出來
// 檢查網路狀態是否已連線
function checkonLine(url) {
return new Promise((resolve, reject) => {
if (!window.navigator.onLine) {
reject(new DisconnectError(url));
} else {
resolve(url);
}
});
}
// 校驗狀態碼
function checkStatus(response) {
const status = Number(response.status);
if (status >= 200 && status < 300) {
return response;
}
throw new ApiServerError(status, response.url);
}
// 解析 fetch 的響應結果
function parseJSON(response) {
return new Promise((resolve, reject) => {
response
.json()
.then((json) => {
// 記錄請求的地址
// eslint-disable-next-line
json._SERVER_URL = response.url;
resolve(json);
})
.catch((error) => {
if (error instanceof SyntaxError) {
reject(new ApiJsonError(response.url));
} else {
reject(error);
}
});
});
}
三、封裝 fetch
準備就緒,可以上主菜了
const FETCH_TIMEOUT = 1000 * 30;
function request(path, params, options = {}) {
const { body, method = 'GET', ...other } = params || {};
const newMethod = `${method}`.toUpperCase();
const newParams = { method: newMethod, credentials: 'include', ...other };
const timeout = newParams.timeout || FETCH_TIMEOUT;
let url = path;
if (newMethod !== 'GET') {
if (!(body instanceof FormData)) {
newParams.headers = {
Accept: 'application/json',
'Content-Type': 'application/json; charset=utf-8',
...newParams.headers,
};
newParams.body = JSON.stringify(body);
}
} else {
// 對GET請求增加時間戳 以避免IE快取
const timestamp = Date.now();
const queryURL = qs.stringify({ ...body, t: timestamp });
url = `${url}?${queryURL}`;
}
// 封裝請求頭
newParams.headers = {
...newParams.headers,
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
credentials: 'include',
};
return Promise.race([
// 校驗網路連線
checkonLine(url)
.then(() => {
return fetch(url, newParams, options);
})
.then(checkStatus)
.then(parseJSON)
.catch((err) => {
throw err;
}),
new Promise((resolve, reject) => {
setTimeout(reject, timeout, new ApiTimeoutError(timeout, url));
}),
]);
}
請求函式 request 已經搞定,這時可以簡單的粗暴的 export 這個函式
也可以匯出具體的 get、post 方法
export default {
get: async (path, params, options) =>
request(path, { method: 'GET', body: params }, options),
post: async (path, params, options) =>
request(path, { method: 'POST', body: params }, options),
delete: async (path, params, options) =>
request(path, { method: 'DELETE', body: params }, options),
put: async (path, params, options) =>
request(path, { method: 'PUT', body: params }, options),
};
四、更進一步
上面的程式碼匯出的是一個含有 get 等方法的物件,需要這麼使用:
import http from './request'
http.get('/api/get', { name: 'wise' });
http.post('/api/save', { name: 'wise' });
不過對於 get 請求,很多的庫做了進一步的封裝,可以直接呼叫
// 直接呼叫,預設使用 get 請求
http('/api/get', { name: 'wise' });
為了更好的體驗,我們的程式碼也可以更進一步:
const http = (url) => {
return http.get(url);
};
http.get = async (path, params, options) =>
request(path, { method: 'GET', body: params }, options);
http.post = async (path, params, options) =>
request(path, { method: 'POST', body: params }, options);
http.delete = async (path, params, options) =>
request(path, { method: 'DELETE', body: params }, options);
http.put = async (path, params, options) =>
request(path, { method: 'PUT', body: params }, options);
export default http;
搞定~