數據請求是咱們開發中很是重要的一環,如何優雅地進行抽象處理,不是一件很容易的事情,也是常常被忽略的事情,處理很差的話,重複的代碼散落在各處,維護成本極高。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
請求做了分析,其實對於其餘好比 get
,put
都同樣的,咱們能夠對這些請求做統一的處理。
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); 複製代碼
這樣的話,咱們對外就暴露出 get
和 post
的方法,其餘請求相似,在此只用 get
和 post
做爲示例,入參分別是 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); }); 複製代碼
axios
的 interceptors
功能,其實就是一個鏈式調用,能夠在請求前和請求後作事情,這裏咱們在請求後進行攔截處理,對返回的數據進行校驗和捕獲異常,對於通用的錯誤咱們直接經過 UI
交互將錯誤展現出來,對於業務上的錯誤咱們檢查下接口有沒有配置說要個性化展現錯誤,若是有的話,將錯誤處理交給頁面,若是沒有的話,進行錯誤兜底處理。
請求監控這塊和異常處理相似,只不過這裏只是記錄狀況,不涉及到 UI
上的交互或者和業務代碼的交互,因此能夠把這部分邏輯直接寫在異常處理那裏,或者在請求後再添加一個攔截器,單獨處理。
monitor.js
axios.interceptors.response.use((response) => { const { status, data, config } = response; // 根據返回的數據和接口參數配置,對請求進行埋點 }, (error) => { // 根據返回的數據和接口參數配置,對請求進行埋點 }); 複製代碼
比較建議這樣作,保持每一個模塊獨立,符合單一功能原則(SRP)。
好了,到如今爲止,參數配置、異常處理 和 請求監控 都設計完了,有三個文件:
request.js
:請求庫配置,對外暴露出 get
,post
方法。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 應用的新選擇》 有簡單提過,歡迎你們體驗。