umi-request 網絡請求之路

做者 慄頭 螞蟻金服·數據體驗技術團隊javascript

背景

在作中臺業務應用開發的過程當中,咱們發如今請求鏈路上存在如下問題:前端

  1. 請求庫各式各樣,沒有統一。 每次新起應用都須要重複實現一套請求層邏輯,切換應用時須要從新學習請求庫 API。
  2. 各應用接口設計不一致、混亂。 先後端同窗每次需從新設計接口格式,前端同窗在切換應用時需從新瞭解接口格式才能作業務開發。
  3. 接口文檔維護各式各樣。 有的在語雀(雲端知識庫)上,有的在 RAP (開源接口管理工具)上,有的靠閱讀源碼才能知道,不管是維護、mock 數據仍是溝通都很浪費人力。

針對以上問題,咱們提出了請求層治理,但願能經過統一請求庫、規範請求接口設計規範、統一接口文檔這三步,對請求鏈路的前中後三個階段進行提效和規範, 從而減小開發者在接口設計、文檔維護、請求層邏輯開發上花費的溝通和人力成本。其中,統一請求庫做爲底層技術支持,須要提早打好基地,爲上層提供穩定、完善的功能支持,基於此,umi-request 應運而生。java

umi-request

umi-request 是基於 fetch 封裝的開源 http 請求庫,旨在爲開發者提供一個統一的 API 調用方式,同時簡化使用方式,提供了請求層經常使用的功能:node

  • URL 參數自動序列化
  • POST 數據提交方式簡化
  • Response 返回處理簡化
  • 請求超時處理
  • 請求緩存支持
  • GBK 編碼處理
  • 統一的錯誤處理方式
  • 請求取消支持
  • Node 環境 http 請求
  • 攔截器機制
  • 洋蔥中間件機制

與 fetch、axios 的異同?

特性 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 環境、自定義內核請求等能力。

支持 node 環境發送 http 請求

基於  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 原文連接

相關文章
相關標籤/搜索