上午好,今天爲你們分享下我的對於前端API
層架構的一點經驗和見解。架構設計是一條永遠走不完的路,沒有最好,只有更好。這個道理適用於軟件設計的各個場景,前端API
層的設計也不例外,若是您以爲在調用接口時還存在諸多槽點,那就說明您的接口層架構還待優化。今天我以vue + axios
爲例,爲你們梳理下個人一些經歷和設想。javascript
直接調用axios
,真的痛苦,每一個調用的地方都要進行響應狀態的判斷,冗餘代碼超級多。前端
import axios from "axios" axios.get('/usercenter/user/page?pageNo=1&pageSize=10').then(res => { const data = res.data // 判斷請求狀態,success字段爲true表明成功,視先後端約束而定 if (data.success) { // 結果成功後的業務代碼 } else { // 結果失敗後的業務代碼 } }) 複製代碼
看起來確實很難受,每調用一次接口,就有這麼多重複的工做!vue
爲了解決直接調用axios
的痛點,咱們通常會利用Promise
對axios
二次封裝,對接口響應狀態進行集中判斷,對外暴露get
, post
, put
, delete
等http
方法。java
import axios from "axios" import router from "@/router" import { BASE_URL } from "@/router/base-url" import { errorMsg } from "@/utils/msg"; import { stringify } from "@/utils/helper"; // 建立axios實例 const v3api = axios.create({ baseURL: process.env.BASE_API, timeout: 10000 }); // axios實例默認配置 v3api.defaults.headers.common['Content-Type'] = 'application/x-www-form-urlencoded'; v3api.defaults.transformRequest = data => { return stringify(data) } // 返回狀態攔截,進行狀態的集中判斷 v3api.interceptors.response.use( response => { const res = response.data; if (res.success) { return Promise.resolve(res) } else { // 內部錯誤碼處理 if (res.code === 1401) { errorMsg(res.message || '登陸已過時,請從新登陸!') router.replace({ path: `${BASE_URL}/login` }) } else { // 默認的錯誤提示 errorMsg(res.message || '網絡異常,請稍後重試!') } return Promise.reject(res); } }, error => { if (/timeout\sof\s\d+ms\sexceeded/.test(error.message)) { // 超時 errorMsg('網絡出了點問題,請稍後重試!') } if (error.response) { // http狀態碼判斷 switch (error.response.status) { // http status handler case 404: errorMsg('請求的資源不存在!') break case 500: errorMsg('內部錯誤,請稍後重試!') break case 503: errorMsg('服務器正在維護,請稍等!') break } } return Promise.reject(error.response) } ) // 處理get請求 const get = (url, params, config = {}) => v3api.get(url, { ...config, params }) // 處理delete請求,爲了防止和關鍵詞delete衝突,方法名定義爲deletes const deletes = (url, params, config = {}) => v3api.delete(url, { ...config, params }) // 處理post請求 const post = (url, params, config = {}) => v3api.post(url, params, config) // 處理put請求 const put = (url, params, config = {}) => v3api.put(url, params, config) export default { get, deletes, post, put } 複製代碼
import api from "@/api"; methods: { getUserPageData() { api.get('/usercenter/user/page?pageNo=1&pageSize=10').then(res => { // 狀態已經集中判斷了,這裏直接寫成功的邏輯 // 業務代碼...... const result = res.result; }).catch(res => { // 失敗的狀況寫在catch中 }) } } 複製代碼
使用語義化的異步函數node
methods: { async getUserPageData() { try { const res = await api.get('/usercenter/user/page?pageNo=1&pageSize=10') // 業務代碼...... const { result } = res; } catch(error) { // 失敗的狀況寫在catch中 } } } 複製代碼
url
api
層難以維護,如後端接口發生改動,前端每處都須要大改。UI
組件的數據模型與後端接口要求的數據結構存在差別,每處調用接口前都須要進行數據處理,抹平差別,好比[1,2,3]
轉1,2,3
這種(固然,這只是最簡單的一個例子)。這樣若是數據處理不慎,調用者出錯概率過高!keyword
,必須調用/user/search
接口,若是沒有輸入關鍵詞,只能調用/user/page
接口。若是每一個調用者都要判斷是否是輸入了關鍵詞,再決定調用哪一個接口,你以爲出錯概率有多大,用起來煩不煩?那麼怎麼解決這些問題呢?請耐心接着看......webpack
我想到的方案是在底層封裝和調用者之間再增長一層API
適配層(適配層,取量身定製之意),在適配層作統一處理,包括參數處理,請求頭處理,特殊化處理等,提煉出更語義化的方法,讓調用者「傻瓜式」調用,再也不爲了查找接口url
和處理數據結構這些重複的工做而煩惱,把ViewModel
層綁定的數據模型直接丟給適配層統一處理。ios
首先,爲了對齊後端微服務架構,在前端將API
調用分爲三個模塊。web
├─api
index.js axios底層封裝
├─base 負責調用基礎服務,basecenter
├─iot 負責調用物聯網服務,iotcenter
└─user 負責調用用戶相關服務,usercenter
複製代碼
每一個模塊下都定義了統一的微服務命名空間,例如/src/api/user/index.js
:vue-cli
export const namespace = 'usercenter'; 複製代碼
每一個功能特性都有獨立的js
模塊,以角色管理相關接口爲例,模塊是/src/api/user/role.js
typescript
import api from '../index' import { paramsFilter } from "@/utils/helper"; import { namespace } from "./index" const feature = 'role' // 添加角色 export const addRole = params => api.post(`/${namespace}/${feature}/add`, paramsFilter(params)); // 刪除角色 export const deleteRole = id => api.deletes(`/${namespace}/${feature}/delete`, { id }); // 更新角色 export const updateRole = params => api.put(`/${namespace}/${feature}/update`, paramsFilter(params)); // 條件查詢角色 export const findRoles = params => api.get(`/${namespace}/${feature}/find`, paramsFilter(params)); // 查詢全部角色,不傳參調用find接口表明查詢全部角色 export const getAllRoles = () => findRoles(); // 獲取角色詳情 export const getRoleDetail = id => api.get(`/${namespace}/${feature}/detail`, { id }); // 分頁查詢角色 export const getRolePage = params => api.get(`/${namespace}/${feature}/page`, paramsFilter(params)); // 搜索角色 export const searchRole = params => params.keyword ? api.get(`/${namespace}/${feature}/search`, paramsFilter(params)) : getRolePage(params); 複製代碼
每一條接口都根據RESTful
風格,調用增(api.post
)刪(api.deletes
)改(api.put
)查(api.get
)的底層方法,對外輸出語義化方法。
調用的url
由三部分組成,格式:/微服務命名空間/特性命名空間/方法
接口適配層函數命名規範:
addXXX
deleteXXX
updateXXX
getXXXDetail
findOneXXX
findXXXs
getAllXXXs
getXXXPage
searchXXX
語義化程度更高,配合vscode
的代碼提示功能,用起來不要太爽!
迅速響應接口改動,適配層統一處理
集中進行數據處理(對於公用的數據處理,咱們用paramsFilter
解決,對於特殊的狀況,再另行處理),調用者安心作業務便可
知足特殊場景,佛系應對後端和產品朋友
keyword
字段,決定調用search
仍是page
接口。對外咱們只需暴露searchRole
方法,調用者只須要調用searchRole
方法便可,無需作其餘考慮。export const searchRole = params => params.keyword ? api.get(`/${namespace}/${feature}/search`, paramsFilter(params)) : getRolePage(params); 複製代碼
首先,咱們新建一個專門管理默認參數的js
,如src/api/default-options.js
// 默認按建立時間降序的參數對象 export const SORT_BY_CREATETIME_OPTIONS = { sortField: 'createTime', // desc表明降序,asc是升序 sortType: 'desc' } 複製代碼
接着,咱們在接口適配層作集中化處理
import api from '../index' import { SORT_BY_CREATETIME_OPTIONS } from "../default-options" import { paramsFilter } from "@/utils/helper"; import { namespace } from "./index" const feature = 'role' export const getRolePage = params => api.get(`/${namespace}/${feature}/page`, paramsFilter({ ...SORT_BY_CREATETIME_OPTIONS, ...params })); 複製代碼
SORT_BY_CREATETIME_OPTIONS
放在前面,是爲了知足若是出現其餘排序需求,調用者傳入的排序字段能覆蓋掉默認參數。
一個完善的API
層設計,確定是離不開mock
的。在後端提供接口以前,前端必須經過模擬數據並行開發,不然進度沒法保證。那麼如何設計一個跟真實接口契合度高的mock
系統呢?我這裏簡單作下分享。
mock
專用的axios
實例咱們在src
目錄下新建mock
目錄,並在src/mock/index.js
簡單封裝一個axios
實例
// 僅限模擬數據使用 import axios from "axios" const mock = axios.create({ baseURL: '' }); // 返回狀態攔截 mock.interceptors.response.use( response => { return Promise.resolve(response.data) }, error => { return Promise.reject(error.response) } ) export default mock 複製代碼
mock
一樣也要分模塊,以usercenter
微服務下的角色管理mock
接口爲例├─mock
index.js mock底層axios封裝
├─user 負責調用基礎服務,usercenter
├─role
├─index.js
複製代碼
咱們在src/mock/user/role/index.js
中簡單模擬一個獲取全部角色的接口getAllRoles
import mock from "@/mock"; export const getAllRoles = () => mock.get('/static/mock/user/role/getAllRoles.json') 複製代碼
能夠看到,咱們是在mock
接口中獲取了static/mock
目錄下的json
數據。所以咱們須要根據接口文檔或者約定好的數據結構準備好getAllRoles.json
數據
{ "success": true, "result": { "pageNo": 1, "pageSize": 10, "total": 2, "list": [ { "id": 1, "createTime": "2019-11-19 12:53:05", "updateTime": "2019-12-03 09:53:41", "name": "管理員", "code": "管理員", "description": "一個擁有部分權限的管理員角色", "sort": 1, "menuIds": "789,2,55,983,54", "menuNames": "數據字典, 後臺, 帳戶信息, 修改密碼, 帳戶中心" }, { "id": 2, "createTime": "2019-11-27 17:18:54", "updateTime": "2019-12-01 19:14:30", "name": "前臺測試", "code": "前臺測試", "description": "一個擁有部分權限的前臺測試角色", "sort": 2, "menuIds": "15,4,1", "menuNames": "油耗統計, 車聯網, 物聯網監管系統" } ] }, "message": "請求成功", "code": 0 } 複製代碼
mock
是怎麼作的先看下真實接口的調用方式
import { getAllRoles } from "@/api/user/role"; created() { this.getAllRolesData() }, methods: { async getAllRolesData() { const res = await getAllRoles() console.log(res) } } 複製代碼
那麼mock
時怎麼作呢?很是簡單,只要將mock
中提供的方法替代掉api
提供的方法便可。
// import { getAllRoles } from "@/api/user/role"; import { getAllRoles } from "@/mock/user/role"; 複製代碼
能夠看到,這種mock
方式與調用真實接口的契合度仍是挺高的,正式調試接口時,只需將註釋的代碼調整便可,過渡很是平滑!
static/mock
目錄下的內容copy
到dist
目錄下,咱們須要配置下CopyWebpackPlugin
,以vue-cli@2
爲例,咱們修改webpack.base.conf.js
便可。const devMode = process.env.NODE_ENV === 'development'; new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: devMode ? config.dev.assetsSubDirectory : config.build.assetsSubDirectory, ignore: devMode ? '' : 'mock/**/*' } ]) 複製代碼
下一步的設想,使用類型安全的typescript
,讓前端API
層真正作到面向接口文檔編程,規範入參,出參,可選參數,等等,提升可維護性,在編碼階段就大大下降出錯概率。雖然還在重構階段,可是我想說,重拾typescript
是真香,忽然懷念使用Angular
的那兩年了,期待vue3.0
能將typescript
結合得更加完美......
將來還有無限可能,面對日漸複雜和多樣化的業務場景,咱們會提煉出更好的架構和設計模式。目前有一個不成熟的設想,是否能在接口設計上作到更規範化,後端輸出接口文檔的同時,提煉出API json
之類的數據結構?前端拿到API json
,經過nodejs
文件編程的能力,自動化生成前端接口層代碼,解放雙手。
固然,以上只是個人一點點經驗和設想,是在我能力範圍內能想到的東西,但願能幫助到一些有須要的同窗。若是大佬們有更好的經驗,能夠指點一二。
往期精彩:
我是Tusi,一個創業公司前端小leader,天天依然爲寫不完的業務代碼煩惱,在打磨產品道路上沉澱技術,探索成長路線。若是你與我同樣,正在思考本身的技術成長與價值,歡迎加我微信交流探討,微信號ice_lloly。我會在公衆號大前端技術沙龍和小程序Tusi博客同步博客內容,快來撩我!