如何構建通用 api 中間層

零、問題的由來

開門見山地說,這篇文章是一篇安利軟文~,安利的對象就是最近搞的 tua-apihtml

顧名思義,這就是一款輔助獲取接口數據的工具。前端

發請求相關的工具辣麼多,那我爲啥要用你呢?jquery

理想狀態下,項目中應該有一個 api 中間層。各類接口在這裏定義,業務側不該該手動編寫接口地址,而應該調用接口層導出的函數。ios

import { fooApi } from '@/apis/'

fooApi
    .bar({ a: '1', b: '2' }) // 發起請求,a、b 是請求參數
    .then(console.log)       // 收到響應
    .catch(console.error)    // 處理錯誤
複製代碼

那麼如何組織實現這個 api 中間層呢?這裏涉及兩方面:git

  • 如何發請求,即「武器」部分
  • 如何組織管理 api 地址

讓咱們先回顧一下有關發請求的歷史。github

1、如何發請求

1.1.原生 XHR (XMLHttpRequest)

說到發請求,最經典的方式莫過於調用瀏覽器原生的 XHR。在此不贅述,有興趣能夠看看MDN 上的文檔ajax

var xhr = window.XMLHttpRequest
    ? new XMLHttpRequest()
    // 在萬惡的 IE 上可能尚未 XMLHttpRequest 這對象
    : new ActiveXObject('Microsoft.XMLHTTP')

xhr.open('GET', 'some url')
xhr.responseType = 'json'

// 傳統使用 onreadystatechange
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText)
    }
}

// 或者直接使用 onload 事件
xhr.onload = function () {
    console.log(xhr.response)
}

// 處理出錯
xhr.onerror = console.error

xhr.send()
複製代碼

這代碼都不用看,想一想就頭皮發麻...json

1.2.jQuery 封裝的 ajax

因爲原生 XHR 寫起來太繁瑣,再加上當時 jQuery 如日中天。平常開發中用的比較多的仍是 jQuery 提供的 ajax 方法。jQuery ajax 文檔點這裏redux

var params = {
    url: 'some url',
    data: { name: 'Steve', location: 'Beijing' },
}

$.ajax(params)
    .done(console.log)
    .fail(console.error)
複製代碼

jQuery 不只封裝了 XHR,還十分貼心地提供跨域的 jsonp 功能。axios

$.ajax({
    url: 'some url',
    data: { name: 'Steve', location: 'Beijing' },
    dataType: 'jsonp',
    success: console.log,
    error: console.error,
})
複製代碼

講道理,jQuery 的 ajax 已經很好用了。然而隨着 Vue、React、Angular 的興起,連 jQuery 自己都被革命了。新項目爲了發個請求還引入巨大的 jQuery 確定不合理,固然後面這些替代方案也功不可沒...

1.3.現代瀏覽器的原生 fetch

XHR 是一個設計粗糙的 API。記得當年筆試某部門的實習生的時候就有手寫 XHR 的題目,我反正記不住 api,並無寫出來...

fetch api 基於 Promise 設計,調用起來比 XHR 方便多了。

fetch(url)
    .then(res => res.json())
    .then(console.log)
    .catch(console.error)
複製代碼

async/await 天然也能使用

try {
    const data = await fetch(url).then(res => res.json())
    console.log(data)
} catch (e) {
    console.error(e)
}
複製代碼

固然 fetch 也有很多的問題

  • 兼容性問題
  • 使用繁瑣,詳見參考文獻之 fetch 沒有你想象的那麼美
  • 不支持 jsonp(雖然理論上不該該支持,但實際上平常仍是須要使用的)
  • 只對網絡請求報錯,對400,500都當作成功的請求,須要二次封裝
  • 默認不會帶 cookie,須要添加配置項
  • 不支持 abort,不支持超時控制,使用 setTimeout 及 Promise.race 的實現的超時控制並不能阻止請求過程繼續在後臺運行,形成了流量的浪費
  • 沒有辦法原生監測請求的進度,而 XHR 能夠

1.4.基於 Promise 的 axios

axios 算是請求框架中的明星項目了。目前 github 5w+ 的 star...

先來看看有什麼特性吧~

  • 同時支持瀏覽器端和服務端的請求。(XMLHttpRequests、http)
  • 支持 Promise
  • 支持請求和和數據返回的攔截
  • 轉換請求返回數據,自動轉換JSON數據
  • 支持取消請求
  • 客戶端防止 xsrf 攻擊

嗯,看起來確實是居家旅行全棧開發必備好庫,可是 axios 並不支持 jsonp...

1.5.不得不用的 jsonp

在服務器端不方便配置跨域頭的狀況下,採用 jsonp 的方式發起跨域請求是一種常規操做。

在此不探究具體的實現,原理上來講就是

  • 因爲 script 標籤能夠設置跨域的來源,因此首先動態插入一個 script,將 src 設置爲目標地址
  • 服務端收到請求後,根據回調函數名(可本身約定,或做爲參數傳遞)將 json 數據填入(即 json padding,因此叫 jsonp...)。例如 callback({ "foo": "bar" })
  • 瀏覽器端收到響應後天然會執行該 script 即調用該函數,那麼回調函數就收到了服務端填入的 json 數據了。

上面講到新項目通常都棄用 jQuery 了,那麼跨域請求仍是得發呀。因此可能你還須要一個發送 jsonp 的庫。(實踐中選了 fetch-jsonp,固然其餘庫也能夠)

綜上,平常開發在框架的使用上以 axios 爲主,實在不得不發 jsonp 請求時,就用 fetch-jsonp。這就是咱們中間層的基礎,即「武器」部分。

1.6.小程序場景

在小程序場景沒得選,只能使用官方的 wx.request 函數...

2、構建接口層基礎功能

對於簡單的頁面,直接裸寫請求地址也沒毛病。可是一旦項目變大,頁面數量也上去了,直接在頁面,或是組件中裸寫接口的話,會帶來如下問題

  • 代碼冗餘:不少接口請求都是相似的代碼,有許多相同的邏輯
  • 不一樣的庫和場景下的接口寫法不一樣(ajax、jsonp、小程序...)
  • 不方便切換測試域名
  • 不方便編寫接口註釋
  • 無法實現統一攔截器、甚至中間件功能

如何封裝這些接口呢?

2.1.接口地址劃分

首先咱們來分析一下接口地址的組成

  • https://example-base.com/foo/create
  • https://example-base.com/foo/modify
  • https://example-base.com/foo/delete

對於以上地址,在 tua-api 中通常將其分爲3部分

  • host: 'https://example-base.com/'
  • prefix: 'foo'
  • pathList: [ 'create', 'modify', 'delete' ]

2.2.文件結構

apis/ 通常是這樣的文件結構:

.
└── apis
    ├── prefix-1.js
    ├── prefix-2.js
    ├── foo.js      // <-- 以上的 api 地址會放在這裏
    └── index.js
複製代碼

index.js 做爲接口層的入口,會導入並生成各個 api 而後再導出。

2.3.基礎配置內容

因此以上的示例接口地址能夠這麼寫

// src/apis/foo.js

export default {
    // 請求的公用服務器地址。
    host: 'http://example-base.com/',

    // 請求的中間路徑,建議與文件同名,以便後期維護。
    prefix: 'foo',

    // 接口地址數組
    pathList: [
        { path: 'create' },
        { path: 'modify' },
        { path: 'delete' },
    ],
}
複製代碼

這時若是想修改服務器地址,只須要修改 host 便可。甚至還能這麼玩

// src/apis/foo.js

// 某個獲取頁面地址參數的函數
const getUrlParams = () => {...}

export default {
    // 根據 NODE_ENV 採用不一樣的服務器
    host: process.env.NODE_ENV === 'test'
        ? 'http://example-test.com/'
        : 'http://example-base.com/',

    // 根據頁面參數採用不一樣的服務器,即頁面地址帶 ?test=1 則走測試地址
    host: getUrlParams().test
        ? 'http://example-test.com/'
        : 'http://example-base.com/',

    // ...
}
複製代碼

2.4.配置導出

下面來看一下 apis/index.js 該怎麼寫:

import TuaApi from 'tua-api'

// 初始化
const tuaApi = new TuaApi({ ... })

// 導出
export const fooApi = tuaApi.getApi(require('./foo').default)
複製代碼

這樣咱們就把接口地址封裝了起來,業務側不須要關心接口的邏輯,然後期接口的修改和升級時只須要修改這裏的配置便可。

2.5.接口參數與接口類型

示例的接口地址太理想化了,若是有參數如何傳遞?

假設以上接口添加 id、from 和 foo 參數。而且增長如下邏輯:

  • foo 參數默認填 bar
  • from 參數默認填 index-page
  • delete 接口使用 jsonp 的方式,from 參數默認填 delete-page
  • modify 接口使用 post 的方式,from 參數不須要填

i-choose-death

哎~,別急着死,暫且看看怎麼用 tua-api 來抽象這些邏輯?

// src/apis/foo.js

export default {
    // ...

    // 公共參數,將會合併到後面的各個接口參數中
    commonParams: {
        foo: 'bar',
        from: 'index-page',
    },

    pathList: [
        {
            path: 'create',
            params: {
                // 相似 Vue 中 props 的類型檢查
                id: { required: true },
            },
        },
        {
            path: 'modify',
            // 使用 post 的方式
            type: 'post',
            params: {
                // 寫成 isRequired 也行
                id: { isRequired: true },
                // 接口不合並公共參數,即不傳 from 參數
                commonParams: null,
            },
        },
        {
            path: 'delete',
            // 使用 jsonp 的方式(不填則默認使用 axios)
            reqType: 'jsonp',
            params: {
                id: { required: true },
                // 這裏填寫的 from 會覆蓋 commonParams 中的同名屬性
                from: 'delete-page',
            },
        },
    ],
}
複製代碼

如今來看看業務側代碼有什麼變化。

import { fooApi } from '@/apis/'

// 直接調用將會報錯,由於沒有傳遞 id 參數
await fooApi.create()

// 請求參數使用傳入的 from:id=1&foo=bar&from=foo-page
await fooApi.create({ id: 1, from: 'foo-page' })

// 請求參數將只有 id:id=1
await fooApi.modify({ id: 1 })

// 請求參數將使用自身的 from:id=1&foo=bar&from=delete-page
await fooApi.delete({ id: 1 })
複製代碼

2.6.接口重命名

假設如今後臺又添加了如下兩個新接口,我們該怎麼寫配置呢?

  • remove/all
  • add-array

首先,把後臺同窗砍死...2333

欲言又止

這什麼鬼接口地址,直接填的話會業務側就會寫成這樣。

fooApi['remove/all']
fooApi['add-array']
複製代碼

這代碼簡直沒法直視...讓咱們用 name 屬性,將接口重命名一下。

// src/apis/foo.js

export default {
    // ...

    pathList: [
        // ...

        { path: 'remove/all', name: 'removeAll' },
        { path: 'add-array', name: 'addArray' },
    ],
}
複製代碼

更多配置請點擊這裏查看

3、高級功能

一個接口層僅僅只能發 api 請求是遠遠不夠的,在平常使用中每每還有如下需求

  • 發起請求時展現 loading,收到響應後隱藏
  • 出錯時展現錯誤信息,例如彈一個 toast
  • 接口上報:包括性能和錯誤
  • 添加特技:如接口參數加密、校驗

3.1.小程序端的 loading 展現

小程序端因爲原生自帶 UI 組件,因此框架內置了該功能。主要包括如下參數

  • isShowLoading
  • showLoadingFn
  • hideLoadingFn

顧名思義,就是開關和具體的顯示、隱藏的方法,詳情參閱這裏

3.2.基礎鉤子函數

最簡單的鉤子函數就是 beforeFn/afterFn 這倆函數了。

beforeFn 是在請求發起前執行的函數(例如小程序能夠經過返回 header 傳遞 cookie),由於是經過 beforeFn().then(...) 調用,因此注意要返回 Promise。

afterFn 是在收到響應後執行的函數,能夠不用返回 Promise。

注意接收的參數是一個【數組】 [ res.data, ctx ]

因此默認值是 const afterFn = ([x]) => x,即返回接口數據到業務側

  • 第一個參數是接口返回的數據對象 { code, data, msg }
  • 第二個參數是請求相關參數的對象,例若有請求的 host、type、params、fullPath、reqTime、startTime、endTime 等等

3.3.middleware 中間件

鉤子函數有時不太夠用,而且代碼一長不太好維護。因此 tua-api 還引入了中間件功能,用法上和 koa 的中間件很像(其實底層直接用了 koa-compose)。

export default {
    middleware: [ fn1, fn2, fn3 ],
}
複製代碼

首先說下中間件執行順序,koa 中間件的執行順序和 redux 的正好相反,例如以上寫法會以如下順序執行:

請求參數 -> fn1 -> fn2 -> fn3 -> 響應數據 -> fn3 -> fn2 -> fn1

簡單說下中間件的寫法,分爲兩種

  • 普通函數:注意必定要 return next() 不然 Promise 鏈就斷了!
  • async 函數:注意必定要 await next()
// 普通函數,注意必定要 return next()
function (ctx, next) {
    ctx.req       // 請求的各類配置
    ctx.res       // 響應,但這時還未發起請求,因此是 undefined!
    ctx.startTime // 發起請求的時間

    // 傳遞控制權給下一個中間件
    return next().then(() => {
        // 注意這裏纔有響應!
        ctx.res       // 響應對象
        ctx.res.data  // 響應的數據
        ctx.reqTime   // 請求花費的時間
        ctx.endTime   // 收到響應的時間
    })
}

// async/await
async function (ctx, next) {
    ctx.req // 請求的各類配置

    // 傳遞控制權給下一個中間件
    await next()

    // 注意這裏纔有響應響應!
    ctx.res // 響應對象
}
複製代碼

其餘參數參閱這裏

4、小結

這篇安利文,先是從前端發請求的歷史出發。一步步介紹瞭如何構建以及使用 api 中間層,來統一管理接口地址,最後還介紹了下中間件等高級功能。話說回來,這麼好用的 tua-api 各位開發者老爺們不來了解一下麼?

笑容逐漸放肆

參考文獻

相關文章
相關標籤/搜索