options请求_前端数据请求的终极方案
數(shù)據(jù)請求是我們開發(fā)中非常重要的一環(huán),如何優(yōu)雅地進行抽象處理,不是一件很容易的事情,也是經(jīng)常被忽略的事情,處理不好的話,重復的代碼散落在各處,維護成本極高。
所以我們需要好好梳理下數(shù)據(jù)請求涉及到哪些方面,對它有整體的管控,從而設計出擴展性高的方案。
案例分析
下面我們以 axios 這個請求庫進行講解。
假如我們在頁面中發(fā)出一個 POST 請求,類似這樣:
axios.post('/user/create', { name: 'beyondxgb' }).then((result) => {// do something });后來發(fā)現(xiàn)需要防止 CSRF,那我們需要在請求中的 headers 加上 X-XSRF-TOKEN,所以變成這樣:
axios.post('/user/create', { name: 'beyondxgb' }, {headers: {'X-XSRF-TOKEN': 'xxxxxxxx',}, }).then((result) => {// do something });這時可以發(fā)現(xiàn),難道每次發(fā)起 post 請求都需要這樣配置嗎?所以會想到把這部分配置抽離出來,抽象出類似這樣一個方法:
function post(url, data, config) {return axios.post(url, data, {headers: {'X-XSRF-TOKEN': 'xxxxxxxx',},...config,}); }所以我們需要對參數(shù)配置進行抽象。
到了測試流程的時候,發(fā)現(xiàn)服務端的請求不是總返回成功的,那怎么辦?那就 catch 處理一下:
post('/user/create', { name: 'beyondxgb' }).then((result) => {// do something }).catch((error) => {// deal with error// 200// 503// SESSION EXPIRED// ... });寫下來總感覺哪里不對啊,原來請求錯誤有這么多情況,我整個項目有很多請求數(shù)據(jù)的地方呢,這部分代碼肯定是通用的,抽象出來!
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); }所以我們需要對異常處理進行抽象。
項目上線前業(yè)務方可能提出穩(wěn)定性的需求,這時我們需要對請求進行監(jiān)控,把接口請求成功和失敗的情況都記錄下來。同樣,我們把這部分代碼也要寫到公用的地方,類似這樣:
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);); }所以我們需要對請求監(jiān)控進行抽象。
方案設計
從上面對一個簡單的 post 請求的案例分析中,我們可以看到,數(shù)據(jù)請求主要涉及三方面 參數(shù)配置、異常處理 和 請求監(jiān)控。上面例子的處理還是比較粗糙,整體上還是需要進行代碼組織和分層。
參數(shù)配置
首先,我們處理下參數(shù)的配置,上面的例子只是對 post 請求作了分析,其實對于其他比如 get,put 都一樣的,我們可以對這些請求作統(tǒng)一的處理。
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 作為示例,入?yún)⒎謩e是 API地址,數(shù)據(jù) 和 擴展配置。
異常處理
其實異常處理場景會比較復雜,不是簡單地 catch 一下,往往伴隨著業(yè)務邏輯和UI的交互,異常主要有兩方面,全局異常和業(yè)務異常。
全局異常,也可以說是通用的異常,比如服務端返回503,網(wǎng)絡異常,登錄失效,無權限等,這些異常是可以預料并可控的,只要和服務端約定好格式,捕獲下異常再展示出來即可。
業(yè)務異常,指的是和業(yè)務邏輯緊密相關的,比如提交失敗,數(shù)據(jù)校驗失敗等,這些異常往往每個接口有不一樣的情況,而且需要個性化展示錯誤,所以這部分可能不能進行統(tǒng)一處理,有時候需要把展示錯誤交到 View 層去實現(xiàn)。
在實現(xiàn)上,我們不會直接在上面的請求方法中直接 catch,而是利用 axios 提供的 interceptors 功能,這樣可以將異常的處理和核心的請求方法隔離出來,畢竟這部分是要和 UI 進行交互的。我們來看看如何實現(xiàn):
error.js
import axios from 'axios';// Add a response interceptor axios.interceptors.response.use((response) => {const { config, data } = response;// 和服務端約定的 Codeconst { 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 功能,其實就是一個鏈式調(diào)用,可以在請求前和請求后做事情,這里我們在請求后進行攔截處理,對返回的數(shù)據(jù)進行校驗和捕獲異常,對于通用的錯誤我們直接通過 UI 交互將錯誤展示出來,對于業(yè)務上的錯誤我們檢查下接口有沒有配置說要個性化展示錯誤,如果有的話,將錯誤處理交給頁面,如果沒有的話,進行錯誤兜底處理。
請求監(jiān)控
請求監(jiān)控這塊和異常處理類似,只不過這里只是記錄情況,不涉及到 UI 上的交互或者和業(yè)務代碼的交互,所以可以把這部分邏輯直接寫在異常處理那里,或者在請求后再添加一個攔截器,單獨處理。
monitor.js
axios.interceptors.response.use((response) => {const { status, data, config } = response;// 根據(jù)返回的數(shù)據(jù)和接口參數(shù)配置,對請求進行埋點 }, (error) => {// 根據(jù)返回的數(shù)據(jù)和接口參數(shù)配置,對請求進行埋點 });比較建議這樣做,保持每個模塊獨立,符合單一功能原則(SRP)。
好了,到現(xiàn)在為止,參數(shù)配置、異常處理 和 請求監(jiān)控 都設計完了,有三個文件:
- request.js:請求庫配置,對外暴露出 get,post 方法。
- error.js:請求的一些異常處理,涉及到和外面對接的是該接口是否需要個性化展示錯誤。
- monitor.js:請求的情況記錄,比較獨立的一塊。
那在頁面上調(diào)用的時候可以這樣子:
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 在多個頁面被調(diào)用,那維護成本就高了。我們有兩種方法,第一種就是將所有 API 獨立配置在一個文件中,給頁面去讀取,第二種辦法就是我們在請求庫和頁面之前再加一層,叫 service,也就是所謂的服務層,對外暴露接口方法給頁面,這樣頁面完全不需要關注接口是什么或者接口是如何取數(shù)據(jù)的,而且以后接口的任何修改,只要在服務層進行修改即可,對頁面沒有任何影響。
當然我是采取第二種方法,類似這樣子:
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 這個請求庫為例,其實思想是互通的,換一個請求庫也是一樣的處理的方法。不知大家有沒有注意到,把請求庫參數(shù)配置和異常處理兩個模塊獨立出來,完全是利用了 interceptors 的特性,這也是我喜歡 axios 的原因之一,我覺得這個設計得很好,類似中間件的做法,在請求數(shù)據(jù)到達頁面之前,我們可以通過寫攔截器對數(shù)據(jù)進行過濾、加工、校驗、異常監(jiān)控等。
我覺得任何一個請求庫都可以實現(xiàn)這個功能,就算請求庫是有歷史包袱,也可以自己在外面包一層。比如說有請求庫 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 interceptorsconst chain = [dispatchRequest, undefined];let promise = Promise.resolve(options);// Add request interceptorsthis.interceptors.request.forEach((interceptor) => {chain.unshift(interceptor.fulfilled, interceptor.rejected);});// Add response interceptorsthis.interceptors.response.forEach((interceptor) => {chain.push(interceptor.fulfilled, interceptor.rejected);});while (chain.length) {promise = promise.then(chain.shift(), chain.shift());}return promise; };更多
前面我們很好地解決了數(shù)據(jù)請求的問題,還有另一方面,也是和數(shù)據(jù)請求緊密相關的,就是數(shù)據(jù)模擬(Mock) 了,在項目開發(fā)前期服務端沒有準備好數(shù)據(jù)之前,我們只有自己在本地進行 Mock 數(shù)據(jù)了,或者很多公司已經(jīng)有比較好的平臺實現(xiàn)這個功能了,我這里介紹下不借助平臺,只是在本地啟動一個小工具即可實現(xiàn) Mock 數(shù)據(jù)。
這里我自己寫了一個小工具 @ris/mock,只要把它作為中間件注入到 webpack-dev-server 中就好了。
webpack.config.js
const mock = require('@ris/mock');module.exports = {//...devServer: {compress: true,port: 9000,after: (app) => {// Start mock datamock(app);},} };這時候在項目根目錄建立 mock 文件夾,文件夾里建一個 rules.js 文件,rules.js 里面配置的是接口的映射規(guī)則,類似這樣子:
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', };配置規(guī)則后,請求接口的時候,就會被轉(zhuǎn)發(fā),轉(zhuǎn)發(fā)的時候可以是一個 對象,函數(shù),文件,詳細使用可以參考文檔。
結(jié)語
在數(shù)據(jù)請求方案的設計中,也證實了我們的“寫代碼”是“程序設計”,而不是“程序編寫”,我們要對自己的代碼負責,如何讓自己的代碼可維護性高,易擴展,是優(yōu)秀工程師的基本素養(yǎng)。
以上的方案已沉淀在 RIS 中,包含代碼組織結(jié)構(gòu)和技術實現(xiàn),可以初始化一個 Standard 應用看看,之前的文章《RIS,創(chuàng)建 React 應用的新選擇》 有簡單提過,歡迎大家體驗。
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結(jié)
以上是生活随笔為你收集整理的options请求_前端数据请求的终极方案的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: visual studio 怎么生成co
- 下一篇: activex for chrome扩展