做者 慄頭 螞蟻金服·數據體驗技術團隊javascript
在作中臺業務應用開發的過程當中,咱們發如今請求鏈路上存在如下問題:前端
針對以上問題,咱們提出了請求層治理,但願能經過統一請求庫、規範請求接口設計規範、統一接口文檔這三步,對請求鏈路的前中後三個階段進行提效和規範, 從而減小開發者在接口設計、文檔維護、請求層邏輯開發上花費的溝通和人力成本。其中,統一請求庫做爲底層技術支持,須要提早打好基地,爲上層提供穩定、完善的功能支持,基於此,umi-request 應運而生。java
umi-request 是基於 fetch 封裝的開源 http 請求庫,旨在爲開發者提供一個統一的 API 調用方式,同時簡化使用方式,提供了請求層經常使用的功能:node
特性 | umi-request | fetch | axios |
---|---|---|---|
實現 | fetch | 瀏覽器原生支持 | XMLHttpRequest |
query 簡化 | ✅ | ❌ | ✅ |
post 簡化 | ✅ | ❌ | ❌ |
超時 | ✅ | ❌ | ✅ |
緩存 | ✅ | ❌ | ❌ |
錯誤檢查 | ✅ | ❌ | ❌ |
錯誤處理 | ✅ | ❌ | ✅ |
攔截器 | ✅ | ❌ | ✅ |
前綴 | ✅ | ❌ | ❌ |
後綴 | ✅ | ❌ | ❌ |
處理 gbk | ✅ | ❌ | ❌ |
中間件 | ✅ | ❌ | ❌ |
取消請求 | ✅ | ❌ | ✅ |
umi-request 底層拋棄了設計粗糙、不符合關注分離的 XMLHttpRequest,選擇了更加語義化、基於標準 Promise 實現的 fetch(更多細節詳見);同時同構更方便,使用 isomorphic-fetch(目前已內置);而基於各業務應用場景提取常見的請求能力並支持快速配置如 post 簡化、先後綴、錯誤檢查等。ios
安裝git
npm install --save umi-request
複製代碼
執行 GET 請求github
import request from "umi-request";
request
.get("/api/v1/xxx?id=1")
.then(function(response) {
console.log(response);
})
.catch(function(error) {
console.log(error);
});
// 也可將 URL 的參數放到 options.params 裏
request
.get("/api/v1/xxx", {
params: {
id: 1
}
})
.then(function(response) {
console.log(response);
})
.catch(function(error) {
console.log(error);
});
複製代碼
執行 POST 請求npm
import request from "umi-request";
request
.post("/api/v1/user", {
data: {
name: "Mike"
}
})
.then(function(response) {
console.log(response);
})
.catch(function(error) {
console.log(error);
});
複製代碼
請求通常都有一些通用的配置,咱們不想在每一個請求裏去逐個添加,例如通用的前綴、後綴、頭部信息、異常處理等等,那麼能夠經過 extend 來新建一個 umi-request 實例,從而減小重複的代碼量:json
import { extend } from "umi-request";
const request = extend({
prefix: "/api/v1",
suffix: ".json",
timeout: 1000,
headers: {
"Content-Type": "multipart/form-data"
},
params: {
token: "xxx" // 全部請求默認帶上 token 參數
},
errorHandler: function(error) {
/* 異常處理 */
}
});
request
.get("/user")
.then(function(response) {
console.log(response);
})
.catch(function(error) {
console.log(error);
});
複製代碼
fetch 自己並不提供請求超時、緩存、取消等能力,而在業務開發中卻經常須要,所以 umi-request 對常見的請求能力進行封裝內置,減小重複開發:axios
{
// 'params' 是即將於請求一塊兒發送的 URL 參數,參數會自動 encode 後添加到 URL 中
// 類型需爲 Object 對象或者 URLSearchParams 對象
params: { id: 1 },
// 'paramsSerializer' 開發者可經過該函數對 params 作序列化(注意:此時傳入的 params 爲合併了 extends 中 params 參數的對象,若是傳入的是 URLSearchParams 對象會轉化爲 Object 對象
paramsSerializer: function (params) {
return Qs.stringify(params, { arrayFormat: 'brackets' })
},
// 'data' 做爲請求主體被髮送的數據
// 適用於這些請求方法 'PUT', 'POST', 和 'PATCH'
// 必須是如下類型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 瀏覽器專屬:FormData, File, Blob
// - Node 專屬: Stream
data: { name: 'Mike' },
// 'headers' 請求頭
headers: { 'Content-Type': 'multipart/form-data' },
// 'timeout' 指定請求超時的毫秒數(0 表示無超時時間)
// 若是請求超過了 'timeout' 時間,請求將被中斷並拋出請求異常
timeout: 1000,
// 'prefix' 前綴,統一設置 url 前綴
// ( e.g. request('/user/save', { prefix: '/api/v1' }) => request('/api/v1/user/save') )
prefix: '',
// 'suffix' 後綴,統一設置 url 後綴
// ( e.g. request('/api/v1/user/save', { suffix: '.json'}) => request('/api/v1/user/save.json') )
suffix: '',
// 'credentials' 發送帶憑據的請求
// 爲了讓瀏覽器發送包含憑據的請求(即便是跨域源),須要設置 credentials: 'include'
// 若是隻想在請求URL與調用腳本位於同一塊兒源處時發送憑據,請添加credentials: 'same-origin'
// 要改成確保瀏覽器不在請求中包含憑據,請使用credentials: 'omit'
credentials: 'same-origin', // default
// 'useCache' 是否使用緩存,當值爲 true 時,GET 請求在 ttl 毫秒內將被緩存,緩存策略惟一 key 爲 url + params 組合
useCache: false, // default
// 'ttl' 緩存時長(毫秒), 0 爲不過時
ttl: 60000,
// 'maxCache' 最大緩存數, 0 爲無限制
maxCache: 0,
// 'charset' 當服務端返回的數據編碼類型爲 gbk 時可以使用該參數,umi-request 會按 gbk 編碼作解析,避省得到亂碼, 默認爲 utf8
// 當 parseResponse 值爲 false 時該參數無效
charset: 'gbk',
// 'responseType': 如何解析返回的數據,當 parseResponse 值爲 false 時該參數無效
// 默認爲 'json', 對返回結果進行 Response.text().then( d => JSON.parse(d) ) 解析
// 其餘(text, blob, arrayBuffer, formData), 作 Response[responseType]() 解析
responseType: 'json', // default
// 'errorHandler' 統一的異常處理,供開發者對請求發生的異常作統一處理,詳細使用請參考下方的錯誤處理文檔
errorHandler: function(error) { /* 異常處理 */ },
}
複製代碼
複雜場景應用對請求先後有定製化的處理需求,請求庫除了提供基礎的內置能力外,也須要提高自身拓展性,基於此 umi-request 引入**中間件機制,**選擇了類 KOA 的洋蔥圈模型:
由上圖可看出,每一層洋蔥圈爲一箇中間件,請求通過一箇中間件都會執行兩次,開發者能夠根據業務需求很方便地實現請求先後加強處理:
import request from "umi-request";
request.use(function(ctx, next) {
console.log("a1");
return next().then(function() {
console.log("a2");
});
});
request.use(function(ctx, next) {
console.log("b1");
return next().then(function() {
console.log("b2");
});
});
// 執行順序以下:
// a1 -> b1 -> b2 -> a2
// 使用 async/await 能讓結構、順序更清晰明瞭:
request.use(async (ctx, next) => {
console.log("a1");
await next();
console.log("a2");
});
request.use(async (ctx, next) => {
console.log("b1");
await next();
console.log("b2");
});
const data = await request("/api/v1/a");
// 執行順序以下:
// a1 -> b1 -> b2 -> a2
複製代碼
那麼洋蔥圈的中間件機制是如何實現的呢?它主要由中間件數組和中間件組合兩部分組成,前者負責存儲掛載的中間件,後者負責將中間件按照洋蔥的結構進行組合並返回真實可執行函數:
class Onion {
constructor() {
this.middlewares = [];
}
// 存儲中間件
use(newMiddleware) {
this.middlewares.push(newMiddleware);
}
// 執行中間件
execute(params = null) {
const fn = compose(this.middlewares);
return fn(params);
}
}
複製代碼
上述代碼中的 compose 即爲組合中間件的函數實現,精簡後邏輯以下(詳見):
export default function compose(middlewares) {
return function wrapMiddlewares(params) {
let index = -1;
function dispatch(i) {
index = i;
const fn = middlewares[i];
if (!fn) return Promise.resolve();
return Promise.resolve(fn(params, () => dispatch(i + 1)));
}
return dispatch(0);
};
}
複製代碼
compose 函數經過 dispatch(0) 先執行了第一個中間件 fn(params, () => dispatch(i +1))
,並提供 () => dispatch(i +1)
做爲入參供每一箇中間件能在下一個中間件執行完畢後回來繼續處理本身的事務,直到全部中間件完成後執行 Promise.resolve()
,造成洋蔥圈中間件機制。
面對多端、多設備應用,umi-request 不只支持 browser http 請求,也同時知足 node 環境、自定義內核請求等能力。
基於 isomorphic-fetch 實現對 node 環境的請求支持:
const umi = require("umi-request");
const extendRequest = umi.extend({ timeout: 10000 });
extendRequest("/api/user")
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});
複製代碼
移動端應用通常都會有本身的請求協議如 RPC 請求,前端會經過 SDK 去調用客戶端請求 API,umi-request 支持開發者本身封裝請求能力,例子:
// service/some.js
import request from "umi-request";
// 自定義請求內核中間件
function SDKRequest(ctx, next) {
const { req } = ctx;
const { url, options } = req;
const { __umiRequestCoreType__ = "normal" } = options;
if (__umiRequestCoreType__.toLowerCase() !== "SDKRequest") {
return next();
}
return Promise.resolve()
.then(() => {
return SDK.request(url, options); // 假設已經引入了 SDK 而且能經過 SDK 發起對應請求
})
.then(result => {
ctx.res = result; // 將結果注入到 ctx 的 res 裏
return next();
});
}
request.use(SDKRequest, { core: true }); // 引入內核中間件
export async function queryUser() {
return request("/api/sdk/request", {
__umiRequestCoreType__: "SDKRequest", // 聲明使用 SDKRequest 來發起請求
data: []
});
}
複製代碼
隨着 umi-request 能力的完善,已經可以支持各個場景、端應用的請求,前端開發只須要掌握一套 API 調用就能實現多端開發,不再用關注底層協議實現,把更多的精力放在前端開發上。而基於此,umi-request 能在底層作更多的事情,如 mock 數據、自動識別請求類型、接口異常監控上報、接口規範校驗等等,最終實現請求治理的目標。umi-request 還有不少能力沒有在文中說起,若有興趣歡迎查看詳細文檔,若是你有好的建議和需求,也歡迎提 issue。
giuthub blog 原文連接