前端數據請求的終極方案

數據請求是咱們開發中很是重要的一環,如何優雅地進行抽象處理,不是一件很容易的事情,也是常常被忽略的事情,處理很差的話,重複的代碼散落在各處,維護成本極高。webpack

因此咱們須要好好梳理下數據請求涉及到哪些方面,對它有總體的管控,從而設計出擴展性高的方案。ios

案例分析

下面咱們以 axios 這個請求庫進行講解。git

假如咱們在頁面中發出一個 POST 請求,相似這樣:github

axios.post('/user/create', { name: 'beyondxgb' }).then((result) => {
  // do something
});
複製代碼

後來發現須要防止 CSRF,那咱們須要在請求中的 headers 加上 X-XSRF-TOKEN,因此變成這樣:web

axios.post('/user/create', { name: 'beyondxgb' }, {
  headers: {
    'X-XSRF-TOKEN': 'xxxxxxxx',
  },
}).then((result) => {
  // do something
});
複製代碼

這時能夠發現,難道每次發起 post 請求都須要這樣配置嗎?因此會想到把這部分配置抽離出來,抽象出相似這樣一個方法:json

function post(url, data, config) {
  return axios.post(url, data, {
    headers: {
      'X-XSRF-TOKEN': 'xxxxxxxx',
    },
    ...config,
  });
}
複製代碼

因此咱們須要對參數配置進行抽象。axios

到了測試流程的時候,發現服務端的請求不是總返回成功的,那怎麼辦?那就 catch 處理一下:api

post('/user/create', { name: 'beyondxgb' }).then((result) => {
  // do something
}).catch((error) => {
  // deal with error
  // 200
  // 503
  // SESSION EXPIRED
  // ...
});
複製代碼

寫下來總感受哪裏不對啊,原來請求錯誤有這麼多狀況,我整個項目有不少請求數據的地方呢,這部分代碼確定是通用的,抽象出來!promise

function dealWithRequestError(error) {
  // deal with error
  // 200
  // 503
  // SESSION EXPIRED
  // ...
}
function post(url, data, config) {
  return axios.post(url, data, {
    headers: {
      'X-XSRF-TOKEN': 'xxxxxxxx',
    },
    ...config,
  }).catch(dealWithRequestError);
}
複製代碼

因此咱們須要對異常處理進行抽象。markdown

項目上線前業務方可能提出穩定性的需求,這時咱們須要對請求進行監控,把接口請求成功和失敗的狀況都記錄下來。一樣,咱們把這部分代碼也要寫到公用的地方,相似這樣:

function post(url, data, config) {
  return axios.post(url, data, {
    headers: {
      'X-XSRF-TOKEN': 'xxxxxxxx',
    },
    ...config,
  }).then((result) => {
    // 記錄成功狀況
    ...
    return result;
  })
  .catch((error) => {
    // 記錄失敗狀況
    ...
    return dealWithRequestError(error);
  );
}
複製代碼

因此咱們須要對請求監控進行抽象。

方案設計

從上面對一個簡單的 post 請求的案例分析中,咱們能夠看到,數據請求主要涉及三方面 參數配置異常處理請求監控。上面例子的處理仍是比較粗糙,總體上仍是須要進行代碼組織和分層。

參數配置

首先,咱們處理下參數的配置,上面的例子只是對 post 請求做了分析,其實對於其餘好比 getput 都同樣的,咱們能夠對這些請求做統一的處理。

request.js

import axios from 'axios';

// The http header that carries the xsrf token value { X-XSRF-TOKEN: '' }
const csrfConfig = {
  'X-XSRF-TOKEN': '',
};
// Build uniform request
async function buildRequest(method, url, params, options) {
  let param = {};
  let config = {};
  if (method === 'get') {
    param = { params, ...options };
  } else {
    param = JSON.stringify(params);
    config = {
      headers: {
        ...csrfConfig,
      },
    };
    config = Object.assign({}, config, options);
  }
  return axios[method](url, param, config);
}

export const get = (url, params = {}, options) => buildRequest('get', url, params, options);
export const post = (url, params = {}, options) => buildRequest('post', url, params, options);
複製代碼

這樣的話,咱們對外就暴露出 getpost 的方法,其餘請求相似,在此只用 getpost 做爲示例,入參分別是 API地址數據擴展配置

異常處理

其實異常處理場景會比較複雜,不是簡單地 catch 一下,每每伴隨着業務邏輯UI的交互,異常主要有兩方面,全局異常業務異常

全局異常,也能夠說是通用的異常,好比服務端返回503,網絡異常,登陸失效,無權限等,這些異常是能夠預料並可控的,只要和服務端約定好格式,捕獲下異常再展現出來便可。

業務異常,指的是和業務邏輯緊密相關的,好比提交失敗,數據校驗失敗等,這些異常每每每一個接口有不同的狀況,並且須要個性化展現錯誤,因此這部分可能不能進行統一處理,有時候須要把展現錯誤交到 View 層去實現。

在實現上,咱們不會直接在上面的請求方法中直接 catch,而是利用 axios 提供的 interceptors 功能,這樣能夠將異常的處理和核心的請求方法隔離出來,畢竟這部分是要和 UI 進行交互的。咱們來看看如何實現:

error.js

import axios from 'axios';

// Add a response interceptor
axios.interceptors.response.use((response) => {
  const { config, data } = response;
  // 和服務端約定的 Code
  const { code } = data;
  switch (code) {
    case 200:
      return data;
    case 401:
      // 登陸失效
      break;
    case 403:
      // 無權限
      break;
    default:
      break;
  }
  if (config.showError) {
    // 接口配置指定須要個性化展現錯誤
    return Promise.reject(data);
  }
  // 默認展現錯誤
  // ... Toast error
}, (error) => {
  // 通用錯誤
  if (axios.isCancel(error)) {
    // Request cancel
  } else if (navigator && !navigator.onLine) {
    // Network is disconnect
  } else {
    // Other error
  }
  return Promise.reject(error);
});
複製代碼

axiosinterceptors 功能,其實就是一個鏈式調用,能夠在請求前和請求後作事情,這裏咱們在請求後進行攔截處理,對返回的數據進行校驗和捕獲異常,對於通用的錯誤咱們直接經過 UI 交互將錯誤展現出來,對於業務上的錯誤咱們檢查下接口有沒有配置說要個性化展現錯誤,若是有的話,將錯誤處理交給頁面,若是沒有的話,進行錯誤兜底處理。

請求監控

請求監控這塊和異常處理相似,只不過這裏只是記錄狀況,不涉及到 UI 上的交互或者和業務代碼的交互,因此能夠把這部分邏輯直接寫在異常處理那裏,或者在請求後再添加一個攔截器,單獨處理。

monitor.js

axios.interceptors.response.use((response) => {
  const { status, data, config } = response;
  // 根據返回的數據和接口參數配置,對請求進行埋點
}, (error) => {
  // 根據返回的數據和接口參數配置,對請求進行埋點
});
複製代碼

比較建議這樣作,保持每一個模塊獨立,符合單一功能原則(SRP)。

好了,到如今爲止,參數配置異常處理請求監控 都設計完了,有三個文件:

  • request.js:請求庫配置,對外暴露出 getpost 方法。
  • error.js:請求的一些異常處理,涉及到和外面對接的是該接口是否須要個性化展現錯誤。
  • monitor.js:請求的狀況記錄,比較獨立的一塊。

那在頁面上調用的時候能夠這樣子:

import { get, post } from 'request.js';

get('/user/info').then((data) => {});
post('/user/update', { name: 'beyondxgb' }, { showError: true }).then((data) => {
  if (data.code !== 200) {
    // 展現錯誤
  } else {
    // do something
  }
});
複製代碼

再仔細思考下,以爲還不是最完美的,API 名稱直接在頁面上引用,這樣會給本身埋坑,若是後面 API 名稱改了,並且這個 API 在多個頁面被調用,那維護成本就高了。咱們有兩種方法,第一種就是將全部 API 獨立配置在一個文件中,給頁面去讀取,第二種辦法就是咱們在請求庫和頁面以前再加一層,叫 service,也就是所謂的服務層,對外暴露接口方法給頁面,這樣頁面徹底不須要關注接口是什麼或者接口是如何取數據的,並且之後接口的任何修改,只要在服務層進行修改便可,對頁面沒有任何影響。

固然我是採起第二種方法,相似這樣子:

services.js

import { get, post } from 'request.js';

// fetch random data
export async function fetchRandomData(params) {
  return get('https://randomuser.me/api', params);
}

// update user info
export async function updateUserInfo(params, options) {
  return post('/user/info', params, { showError: true, ...options });
}
複製代碼

這樣子的話,頁面就不會直接和請求庫進行交互,而是跟服務層獲取對應的方法。

import { fetchRandomData, updateUserInfo } from 'services.js';

fetchRandomData().then((data) => {});
updateUserInfo({ name: 'beyondxgb' }).then((data) => {
  if (data.code !== 200) {
    // 展現錯誤
  } else {
    // do something
  }
});
複製代碼

咱們來看看最終的方案是這樣子的:

延伸擴展

上面講的都是以 axios 這個請求庫爲例,其實思想是互通的,換一個請求庫也是同樣的處理的方法。不知你們有沒有注意到,把請求庫參數配置和異常處理兩個模塊獨立出來,徹底是利用了 interceptors 的特性,這也是我喜歡 axios 的緣由之一,我以爲這個設計得很好,相似中間件的作法,在請求數據到達頁面以前,咱們能夠經過寫攔截器對數據進行過濾加工校驗異常監控等。

我以爲任何一個請求庫均可以實現這個功能,就算請求庫是有歷史包袱,也能夠本身在外面包一層。好比說有請求庫 abc,它有一個 request 方法,能夠這樣複寫它:

import abc from 'abc';

function dispatchRequest(options) {
  const reqConfig = Object.assign({}, options);
  return abc.request(reqConfig).then(response => ({
    response,
    options,
  })).catch(error => (
    Promise.reject({
      error,
      options,
    })
  ));
}

class Request {
  constructor(config) {
    this.default = config;
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager(),
    };
  }
}

Request.prototype.request = function request(config = {}) {
  // Add interceptors
  const chain = [dispatchRequest, undefined];
  let promise = Promise.resolve(options);

  // Add request interceptors
  this.interceptors.request.forEach((interceptor) => {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // Add response interceptors
  this.interceptors.response.forEach((interceptor) => {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};
複製代碼

更多

前面咱們很好地解決了數據請求的問題,還有另外一方面,也是和數據請求緊密相關的,就是數據模擬(Mock) 了,在項目開發前期服務端沒有準備好數據以前,咱們只有本身在本地進行 Mock 數據了,或者不少公司已經有比較好的平臺實現這個功能了,我這裏介紹下不借助平臺,只是在本地啓動一個小工具便可實現 Mock 數據。

這裏我本身寫了一個小工具 @ris/mock,只要把它做爲中間件注入到 webpack-dev-server 中就行了。

webpack.config.js

const mock = require('@ris/mock');

module.exports = {
  //...
  devServer: {
    compress: true,
    port: 9000,
    after: (app) => {
      // Start mock data
      mock(app);
    },
  }
};
複製代碼

這時候在項目根目錄創建 mock 文件夾,文件夾裏建一個 rules.js 文件,rules.js 裏面配置的是接口的映射規則,相似這樣子:

module.exports = {
  'GET /api/user': { name: 'beyondxgb' },
  'POST /api/form/create': { success: true },
  'GET /api/cases/list': (req, res) => { res.end(JSON.stringify([{ id: 1, name: 'demo' }])); },
  'GET /api/user/list': 'user/list.json',
  'GET /api/user/create': 'user/create.js',
};
複製代碼

配置規則後,請求接口的時候,就會被轉發,轉發的時候能夠是一個 對象函數文件,詳細使用能夠參考文檔

結語

在數據請求方案的設計中,也證明了咱們的「寫代碼」是「程序設計」,而不是「程序編寫」,咱們要對本身的代碼負責,如何讓本身的代碼可維護性高,易擴展,是優秀工程師的基本素養。

以上的方案已沉澱在 RIS 中,包含代碼組織結構和技術實現,能夠初始化一個 Standard 應用看看,以前的文章《RIS,建立 React 應用的新選擇》 有簡單提過,歡迎你們體驗。

相關文章
相關標籤/搜索