前端API層架構,也許你做得還不夠
上午好,今天為大家分享下個人對於前端API
層架構的一點經驗和看法。架構設計是一條永遠走不完的路,沒有最好,只有更好。這個道理適用於軟體設計的各個場景,前端API
層的設計也不例外,如果您覺得在呼叫介面時還存在諸多槽點,那就說明您的介面層架構還待優化。今天我以vue + axios
為例,為大家梳理下我的一些經歷和設想。
石器時代,痛苦
直接呼叫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 { // 結果失敗後的業務程式碼 } })
看起來確實很難受,每呼叫一次介面,就有這麼多重複的工作!
青銅器時代,中規中矩
為了解決直接呼叫axios
的痛點,我們一般會利用Promise
對axios
二次封裝,對介面響應狀態進行集中判斷,對外暴露get
, post
, put
, delete
等http
方法。
axios二次封裝
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中 }) } }
async/await改造
使用語義化的非同步函式
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
介面。如果每個呼叫者都要判斷是不是輸入了關鍵詞,再決定呼叫哪個介面,你覺得出錯機率有多大,用起來煩不煩? - 產品說,這些場景需要優化,預設按建立時間降序排序。我擦,又一個個改一遍?
- ......
那麼怎麼解決這些問題呢?請耐心接著看......
鐵器時代,it's cool
我想到的方案是在底層封裝和呼叫者之間再增加一層API
適配層(適配層,取量身定製之意),在適配層做統一處理,包括引數處理,請求頭處理,特殊化處理等,提煉出更語義化的方法,讓呼叫者“傻瓜式”呼叫,不再為了查詢介面url
和處理資料結構這些重複的工作而煩惱,把ViewModel
層繫結的資料模型直接丟給適配層統一處理。
對齊微服務架構
首先,為了對齊後端微服務架構,在前端將API
呼叫分為三個模組。
├─api
index.js axios底層封裝
├─base 負責呼叫基礎服務,basecenter
├─iot 負責呼叫物聯網服務,iotcenter
└─user 負責呼叫使用者相關服務,usercenter
每個模組下都定義了統一的微服務名稱空間,例如/src/api/user/index.js
:
export const namespace = 'usercenter';
特性模組
每個功能特性都有獨立的js
模組,以角色管理相關介面為例,模組是/src/api/user/role.js
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
- 根據ID查詢記錄:
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
放在前面,是為了滿足如果出現其他排序需求,呼叫者傳入的排序欄位能覆蓋掉預設引數。- 針對上節提到的關鍵字查詢場景,我們在適配層通過在入參中判斷是否有
mock先行
一個完善的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
檔案程式設計的能力,自動化生成前端介面層程式碼,解放雙手。
結語
當然,以上只是我的一點點經驗和設想,是在我能力範圍內能想到的東西,希望能幫助到一些有需要的同學。如果大佬們有更好的經驗,可以指點一二。
首發連結
往期精彩:
- 用初中數學知識擼一個canvas環形進度條