開門見山地說,這篇文章是一篇安利軟文~,安利的對象就是最近搞的 tua-api。html
顧名思義,這就是一款輔助獲取接口數據的工具。前端
發請求相關的工具辣麼多,那我爲啥要用你呢?
理想狀態下,項目中應該有一個 api 中間層。各類接口在這裏定義,業務側不該該手動編寫接口地址,而應該調用接口層導出的函數。jquery
import { fooApi } from '@/apis/' fooApi .bar({ a: '1', b: '2' }) // 發起請求,a、b 是請求參數 .then(console.log) // 收到響應 .catch(console.error) // 處理錯誤
那麼如何組織實現這個 api 中間層呢?這裏涉及兩方面:ios
讓咱們先回顧一下有關發請求的歷史。git
說到發請求,最經典的方式莫過於調用瀏覽器原生的 XHR。在此不贅述,有興趣能夠看看MDN 上的文檔。github
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()
這代碼都不用看,想一想就頭皮發麻...ajax
因爲原生 XHR 寫起來太繁瑣,再加上當時 jQuery 如日中天。平常開發中用的比較多的仍是 jQuery 提供的 ajax 方法。jQuery ajax 文檔點這裏json
var params = { url: 'some url', data: { name: 'Steve', location: 'Beijing' }, } $.ajax(params) .done(console.log) .fail(console.error)
jQuery 不只封裝了 XHR,還十分貼心地提供跨域的 jsonp 功能。redux
$.ajax({ url: 'some url', data: { name: 'Steve', location: 'Beijing' }, dataType: 'jsonp', success: console.log, error: console.error, })
講道理,jQuery 的 ajax 已經很好用了。然而隨着 Vue、React、Angular 的興起,連 jQuery 自己都被革命了。新項目爲了發個請求還引入巨大的 jQuery 確定不合理,固然後面這些替代方案也功不可沒...axios
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 也有很多的問題
axios 算是請求框架中的明星項目了。目前 github 5w+ 的 star...
先來看看有什麼特性吧~
嗯,看起來確實是居家旅行全棧開發必備好庫,可是 axios 並不支持 jsonp...
在服務器端不方便配置跨域頭的狀況下,採用 jsonp 的方式發起跨域請求是一種常規操做。
在此不探究具體的實現,原理上來講就是
callback({ "foo": "bar" })
。上面講到新項目通常都棄用 jQuery 了,那麼跨域請求仍是得發呀。因此可能你還須要一個發送 jsonp 的庫。(實踐中選了 fetch-jsonp
,固然其餘庫也能夠)
綜上,平常開發在框架的使用上以 axios
爲主,實在不得不發 jsonp 請求時,就用 fetch-jsonp
。這就是咱們中間層的基礎,即「武器」部分。
在小程序場景沒得選,只能使用官方的 wx.request
函數...
對於簡單的頁面,直接裸寫請求地址也沒毛病。可是一旦項目變大,頁面數量也上去了,直接在頁面,或是組件中裸寫接口的話,會帶來如下問題
如何封裝這些接口呢?
首先咱們來分析一下接口地址的組成
https://example-base.com/foo/create
https://example-base.com/foo/modify
https://example-base.com/foo/delete
對於以上地址,在 tua-api
中通常將其分爲3部分
'https://example-base.com/'
'foo'
[ 'create', 'modify', 'delete' ]
apis/
通常是這樣的文件結構:
. └── apis ├── prefix-1.js ├── prefix-2.js ├── foo.js // <-- 以上的 api 地址會放在這裏 └── index.js
index.js
做爲接口層的入口,會導入並生成各個 api 而後再導出。
因此以上的示例接口地址能夠這麼寫
// 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/', // ... }
下面來看一下 apis/index.js
該怎麼寫:
import TuaApi from 'tua-api' // 初始化 const tuaApi = new TuaApi({ ... }) // 導出 export const fooApi = tuaApi.getApi(require('./foo').default)
這樣咱們就把接口地址封裝了起來,業務側不須要關心接口的邏輯,然後期接口的修改和升級時只須要修改這裏的配置便可。
示例的接口地址太理想化了,若是有參數如何傳遞?
假設以上接口添加 id、from 和 foo 參數。而且增長如下邏輯:
bar
index-page
delete-page
哎~,別急着死,暫且看看怎麼用 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 })
假設如今後臺又添加了如下兩個新接口,我們該怎麼寫配置呢?
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' }, ], }
一個接口層僅僅只能發 api 請求是遠遠不夠的,在平常使用中每每還有如下需求
小程序端因爲原生自帶 UI 組件,因此框架內置了該功能。主要包括如下參數
顧名思義,就是開關和具體的顯示、隱藏的方法,詳情參閱這裏
最簡單的鉤子函數就是 beforeFn/afterFn
這倆函數了。
beforeFn 是在請求發起前執行的函數(例如小程序能夠經過返回 header 傳遞 cookie),由於是經過 beforeFn().then(...)
調用,因此注意要返回 Promise。
afterFn 是在收到響應後執行的函數,能夠不用返回 Promise。
注意接收的參數是一個【數組】[ res.data, ctx ]
因此默認值是
const afterFn = ([x]) => x
,即返回接口數據到業務側
{ code, data, msg }
鉤子函數有時不太夠用,而且代碼一長不太好維護。因此 tua-api 還引入了中間件功能,用法上和 koa 的中間件很像(其實底層直接用了 koa-compose
)。
export default { middleware: [ fn1, fn2, fn3 ], }
首先說下中間件執行順序,koa 中間件的執行順序和 redux 的正好相反,例如以上寫法會以如下順序執行:
請求參數 -> fn1 -> fn2 -> fn3 -> 響應數據 -> fn3 -> fn2 -> fn1
簡單說下中間件的寫法,分爲兩種
return next()
不然 Promise
鏈就斷了!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 // 響應對象 }
這篇安利文,先是從前端發請求的歷史出發。一步步介紹瞭如何構建以及使用 api 中間層,來統一管理接口地址,最後還介紹了下中間件等高級功能。話說回來,這麼好用的 tua-api 各位開發者老爺們不來了解一下麼?