[TOC]
这个前端Api管理方案会更好?一
简介:大家好,前端小白一枚,目前接触后台管理系统比较多,经常遇到不同对象的增删改查的接口,如何对Api进行一个有比较好的管理是个问题。
在学习偏函数的时候有了灵感,想到一个不错的API管理方案,并应用在项目一个模块当中,并且开发效率和维护性可读性都很不错,和大家分享一下~
1.背景一:当前项目的前端API管理方案
// 封装的接口
export function obj1Func1(){}
export function obj1Func2(){}
export function obj2Func3(){}
export function obj2Func4(){}
// 引入方式
import { obj1Func1, obj1Func2, obj2Func3, obj2Func4 } from 'xxx'
// 使用方式
const params = {...}
await obj1Func1(params)
当接口多了之后,我们管理接口(查找)是一件很麻烦且废眼睛的事,需要一直翻,注释看不过来,维护性和可读性差。
统一export方式
// 封装的接口
function obj1Func1(){}
function obj1Func2(){}
function obj2Func3(){}
function obj2Func4(){}
// 导出接口
export {
obj1Func1,//注释
obj1Func2,//注释
obj2Func3,//注释
obj2Func4,//注释
...
}
// 引入方式
import { obj1Func1, obj1Func2, obj2Func3, obj2Func4 } from 'xxx'
// 使用方式
const params = {...}
await obj1Func1(params)
这样加上函数功能注释,我们只需要在export内查找即可,比原来方便不少,但每次查找都需要拉到文件底下(export
必须在函数定义之后),再一行行函数注释去看,还是不太方便。
2.方案调研
面向对象的方式
// module/objApi.js
function objFunc1(){}
function objFunc2(){}
function objFunc3(){}
function objFunc4(){}
// 导出接口
export default {
get: objFunc1,//注释
upd: objFunc2,//注释
del: objFunc3,//注释
add: objFunc4,//注释
...
}
// 引入方式
import objApi from 'module/objApi.js'
// 使用方式
const params = {...}
await objApi.get(params)
await objApi.upd(params)
优点
- 面向对象,不需要每个接口函数都引入,开发时调用方便
- 简化操作类型命名,如
update -> upd
,开发和维护方便
缺点
- 当模块涉及的对象很多,则需要建立非常多的文件,当文件名复杂时,难以看懂维护难度up。
- 当页面需要引入多个对象,需要引入一个个文件,降低开发效率。
- 找对象需要拖拽到文件最底部
以面向模块(对象)的方式
假设当前模块下涉及到 n 个对象及对应的增删改查接口, 定义一个映射表
// api映射表
const apiMap = {
// 公共接口
common: {
commonFun1,
commonFun2,
},
//对象1
dog: {
//增删改查
add: obj1Func1
get: obj1Func2
},
//对象2
cat: {
//增删改查
upd: obj2Func3,
del: obj2Func4,
},
...
}
apiMap
对象
一级键名是模块涉及的对象
二级键名是对象相关的操作类型,值是对应的接口函数
3.导出方式1
直接导出各个对象
export default {
commonApi: apiMap['common'],
dogApi: apiMap['dog'],
catApi: apiMap['cat'],
}
import {commonApi,...} from "xxx"
面向模块(多个对象)
本质就是以对象的方式来进行管理,只不过这里面向的是模块。这里一个模块只对应一个文件,包含了涉及到的n个对象的接口。因为我觉得一个模块下建n个对象一长串的Api
文件,又没法对文件名注释(文件名总有不认识或拼接的单词吧)只会带来更大的维护困难
找了几个不常见的英语名词,英语烂仔直接带上痛苦面具
而有了映射表就相当于有了一个目录(文件最上方一目了然, Map下的对象十分清晰还有注释),
至少目前我是能都秒读懂接口含义了
也不会出现面对老项目里那种长得拖不完的不知名接口文件的懵逼,点进去还只有几行代码(雪花飘飘~)
优点:
- 同上
apiMap
变成接口目录,可读性和可维护性提高(下方介绍)- 涉及同模块多个对象只需要引入一个文件
缺点:
- apiMap(目录)和export的位置一个在文件最上方,一个在最下方,浏览时非常不方便,依旧需要经常拖拽
这种方式已经比较好用了,从可维护性和可读性,拓展性来看我更推荐第二种方式
4.导出方式2
导出一个访问映射表的函数,参数是对象及操作类型,如(dog, upd)
// 暴露一个访问api映射表的函数, 参数是对象和操作
// 这里没有错误处理,jym看懂就行
export default function api(obj, action) {
if (action) {
// 返回某对象某操作的接口函数,如dogUpdate
return apiMap[obj][action]
}
// 返回一个包含多个操作接口函数的对象或公共接口
return apiMap[obj]
}
// 封装的接口
function obj1Func1(){}
function obj1Func2(){}
function obj2Func3(){}
function obj2Func4(){}
使用时方式1:
import API from 'xxx'
// 没有错误处理,主打看懂
async function getData() {
const params = {...}
const data = await API(obj1, 'get')(params)
await API(obj1, 'upd')(params)
await API(obj1, 'del')(params)
}
使用时方式2:
import API from 'xxx'
const obj1API = API(obj1)
const data = await obj1API.get(params)
const data = await obj1API.upd(params)
const data = await obj1API.del(params)
api
函数
api`函数可以复制或者写在公共模块引入就行了,实际上工作量只在维护映射表`apiMap
现在查找接口原本一个模块里可能涉及10个对象共100个接口,顺序查找最差情况要看100条函数注释,而根据对象查找最差情况是10(对象)+10(操作类型)即20条函数注释。
const apiMap = {
...
// 注释:这是target对象
targetObj: {
add: objAddFunc, // 注释:增删改查的话可有可无
upd: objUpdateFunc,
del: objDeleteFunc,
get: objGetFunc,
}
...
}
只需要关注目标对象(其实是注释),清晰且一目了然,甚至不需要函数注释,不需要拖到文件底部
5.实现效果:
模块化
这种方式用对象结构拆分也算是模块化了,看着不太习惯,但一个文件里对象和接口能都读懂,维护性和可读性也更好,即便接口函数再多行数再多,其实也只需要看apiMap
5.封装
(接下来开始胡扯~)
api函数
封装了一层,可以统一管理接口提高拓展性和复用性,例如统一给action
为get
的套一个节流函数
// 暴露一个访问api映射表的函数, 参数是对象和操作
// 这里没有错误处理,jym看懂就行
export default function api(obj, action) {
if (action) {
if (action === "get") {
return throttle(apiMap[obj][action], 500); // 设置节流时间为 500 毫秒,期间返回空函数
}
return apiMap[obj][action]
}
// 返回一个包含多个操作接口函数的对象或公共接口
return apiMap[obj]
}
或者当模块下有多个对象需要增删改查,且只需要一个参数id,那么只要再加个定制场景的api函数
// 偏函数固定参数,这里constParams假设为 { id:007 }
export function paramsApi(constParams, obj) {
// otherParams是额外参数,在各自接口做个合并Object.assign()
return (action, otherParams) => apiMap[obj][action](otherParams)
}
// 固定参数
const constParams = { id: 007 }
const dogApi = paramsApi(constParams, 'dog')
const catApi = paramsApi(constParams, 'cat')
// 免参数直接调用即可
dogApi('get')
dogApi('update', { color: 6 })
dogApi('delete')
catApi('get')
catApi('update', { color: 6 })
catApi('delete')
这个场景有些理想化,但有一层封装也确实能够在需要时方便统一管理
然后这里 action: objActionFunc
这里也算是一层封装,方便进行命名简写和规范
// xxxModule.js
const apiMap = {
//对象1
obj: {
action: objActionFunc
add: dogAdd, // xxx/dog/add
mAdd: dogImport, // xxx/dog/import
upd: dogUpdate, // xxx/dog/update
},
}
7.来看看开发体验啦
和同事沟通后,我应用在项目的一个模块中,感觉很棒!
首先写接口引入公共模块的api
函数,定义apiMap
映射表,正常写接口,工作量多了一个apiMap
罢了
// 引入api,getType函数
import { api } from "common"
// api映射表
const apiMap = {
...
}
// 暴露api函数
export default api
// 封装的接口
...
开发时查找接口就全程对着文件最上方的映射表复制,基本不需要怎么拖拽,不需要切换文件去找其他对象,也不需要看一堆无关代码,全程看注释,大大降低心智负担,小白上手也能分分钟找到
// xxxModule.js
const apiMap = {
//对象1
dog: {
add: xxx
get: xxx
},
//对象2
cat: {
upd: xxx,
del: xxx,
},
}
引入接口时只要一个API函数,调用方式基本大差不差吧,反正都是copy
,然后改下操作类型
import API from "@/api/xxx/xxxModule"
// 调用时, 两种方式,复制个对象名,记住个操作类型,完事
const dogAPI = API('dog')
const catAPI = API('cat')
await dogAPI.get(params)
await dogAPI.upd(params)
await catAPI.get(params)
// 或
await API('dog', 'get')(params)
await API('dog', 'upd')(params)
await API('cat', 'get')(params)
开发效率我觉得和对象方式的API管理方案没啥区别,都是copy
然后改下操作类型,但其实最大的好处还是在可维护性和可读性上
8.写在最后
非常感谢看到最后的jym,这是我本0前端小白通过偏函数产生的一点小想法,感觉挺好用的分享一下(可能场景比较简单局限后台系统),欢迎jym多多指点发表建议(玻璃心)
下章:方案进一步的思考和示例说明(Tree shaking, 类继承, 函数封装, 应用场景),感兴趣的jym可以看看哦~
这个前端Api管理方案会更好?二
1.上篇回顾
总结几种API管理方案的优劣,提出Api文件面向模块的方案,解决冗长文件带来的难维护性,可读性差的问题,大大提升接口的可读性。并通过封装函数统一对接口管理,提高代码的拓展性,灵活性和复用性。
接下来对评论区的一些问题进行解答~~~~~~~
2.为什么导出的是一个封装函数api?
// api映射表
const apiMap = {
// 公共接口
common: {
commonFun1,
commonFun2,
},
//对象1
dog: {
//增删改查
add: obj1Func1
get: obj1Func2
},
//对象2
cat: {
//增删改查
upd: obj2Func3,
del: obj2Func4,
},
...
}
// 暴露一个访问api映射表的函数, 参数是对象和操作
// 这里没有错误处理,jym看懂就行
export default function api(obj, action) {
if (action) {
// 返回某对象某操作的接口函数,如dogUpdate
return apiMap[obj][action]
}
// 返回一个包含多个操作接口函数的对象或公共接口
return apiMap[obj]
}
// 封装的接口
function obj1Func1(){}
function obj1Func2(){}
function obj2Func3(){}
function obj2Func4(){}
首先,封装带来的开发效率和开发成本是一个值得思考的平衡问题,我看到了越来越多鼓励减少封装的文章,我本人也趋向尽量减少不必要的封装
3.但为什么这个方案里我倾向封装函数呢?
优点:方便统一管理多个对象的接口,接口了多了之后,加一层抽象可以提高代码的拓展性,灵活性和可复用性。因为面向同一模块,不同对象的接口一定程度上有共性,如果需要对这些接口作拓展又不影响原接口,那么封装无疑是必需的。
举几个理想的例子,例如:
1. 统一给接口加上固定参数
// 偏函数固定参数,这里constParams假设为 { id:007 }
export function paramsApi(constParams, obj) {
// otherParams是额外参数,在各自接口做个合并Object.assign()
return (action, otherParams) => apiMap[obj][action](otherParams)
}
// 固定参数
const constParams = { id: 007 }
const dogApi = paramsApi(constParams, 'dog')
const catApi = paramsApi(constParams, 'cat')
// 无参数直接调用即可
dogApi('get')
dogApi('update', { color: 6 })
dogApi('delete')
catApi('get')
catApi('update', { color: 6 })
catApi('delete')
芜湖,接口调用不需要参数啦?!
2. 统一给get列表接口加上本地缓存回显判断
希望给该模块所有列表页,统一新增本地缓存列表的筛选项功能
export function getApi(obj, storageKey, params) {
// 若带参数,则将参数缓存至本地
if(params) {
localStorage.setItem(storageKey, JSON.stringify(params))
return apiMap[obj]['get'](params)
}
// 若不带参数且本地有缓存的筛选项,那么直接取出并作为请求的params
let params = { page: 1, pageSize:20 }
const storageParams = localStorage.getItem(storageKey)
if(storageParams) params = JOSN.parse(storageParams)
return apiMap[obj]['get'](params)
}
// 使用
import { getApi } from "xxxmodule"
// 页面初始化列表
api('dog', this.storageKey)
// 页面获取列表
api('dog', this.storageKey, params)
芜湖,一个函数给n个列表页加上本地缓存功能?!
3. 统一给get列表接口加上节流
// 暴露一个访问api映射表的函数, 参数是对象和操作
// 这里没有错误处理,jym看懂就行
export function throttleApi(obj, action, time) {
const thrList = ['goodBoy', '爱吃好果汁丶']
if (action) {
if (action === "get" && thrList.includes(obj)) {
return throttle(apiMap[obj][action], time); // 设置节流时间为 time 毫秒,期间返回空函数
}
return apiMap[obj][action]
}
// 返回一个包含多个操作接口函数的对象或公共接口
return apiMap[obj]
}
芜湖,一个函数给n个get请求加上节流优化?!还能限制影响的对象!?
4. 统一让get请求同时拥有上面三种功能
function -.-Api() {
zhijixie
}
芜湖,一个函数……咦,怎么报错了?
因此我们可以根据场景选择导出封装函数,很好的解决了拓展性和复用性的问题
import { paramsApi, getApi, throttleApi } from 'xxxmodule'
如果换做面向对象,想在模块里统一添加上述功能可以试想一下多麻烦
4.这个方案不利于Tree shaking?
“
Tree shaking:当使用 ES6 模块语法时,模块的导入和导出关系是静态的,这意味着在编译时可以确定模块之间的依赖关系。Tree shaking 利用这个特性,通过静态分析代码,识别出未被使用的模块、函数、变量等,并将其从最终的打包结果中删除。
是的,Tree shaking
需要在编译时确定模块之间的依赖关系,而导出的api
函数通过参数访问apiMap
映射表,只有运行时才能确定依赖关系,即api
函数是动态函数因此不利于Tree shaking
。
但实际上客观来说是可以接受的。
首先接口基本都是会被引用的,不存在被摇树去除的情况,因此对打包总体积没有影响 影响的是动态路由加载的页面,首次加载页面会把不需要的接口函数一起请求加载
在这个chunk
里模块里的接口都被打包引入了
但api文件占的大小是多少呢? 这个模块31
个接口函数,原 7.0k
压缩后4.7kb
,在chunk
里1%都占不到,即便300
个接口,估计也就占个5%
因此带来的问题基本可以忽略不计,膈应的话(不建议)可以使用拆模块,对象导出或者webpack
优化降低影响。
5.拆模块
将当前模块的对象作模块化拆成更小的二级模块,但如果接口或对象不多不建议拆,避免大量文件->大量文件夹
对象导出
export default {
commonApi: apiMap['common'],
dogApi: apiMap['dog'],
catApi: apiMap['cat'],
}
import {commonApi,...} from "xxx"
严格说导入带有多个属性的对象也不利于Tree shaking
, 无引用的接口函数也会被打包,但影响基本是忽略不计中的忽略不计,另外也无法直接使用封装函数。
webpack优化
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
...
api: {
name:'chunk-apis'
test: /api(/|\).*/index.js/,
priority: 10,
minChunks: 2,
}
}
})
将api
文件作为独立的chunk
块,从而更好地利用加载缓存,其他页面引用api
文件时就不会再重复加载了。
使用类继承
class api {
constructor(name) {
this.name = name;
}
get() {
return axios.get(`/${this.name}/get`)
}
add() {
return axios.get(`/${this.name}/add`)
}
upd() {
return axios.get(`/${this.name}/upd`)
}
}
class dogApi extend api {
constructor(name) {
super(name);
}
bark() {
return axios.get(`/${this.name}/bark`)
}
}
const dog = new dogApi('dog')
dog.get()
dog.bark()
类继承的方式是很棒的设计,非常适合crud
和 restful
规范的场景,可以较好的提高代码复用性,也方便对对象进行管理维护。
但是从方案的可读性上看,类依旧不如apiMap
直观,因为接口实际上只需要关注接口名以及注释,而类则混入了逻辑代码,没有apiMap
那么聚合。
并且当脱离crud
和restful
后,复用性的优势也没有了。
因此这个方案还是更推荐apiMap
的方式。
6.应用场景
我个人认为小型到中大型无论是否crud
都适用,很大规模的项目没接触过,但与其遵循什么低耦合高内聚,职责单一原则整一大长串文件没人看,还不如一把梭至少还能看的懂,设计原则的最终目的应该是面向开发者
7.模块里的对象太多怎么处理
答:拆成二级模块,模块文件夹下新增二级模块文件夹,保持apiMap
的二级结构, 也能降低Tree shaking的影响
问:又太多了 答:继续拆
问:又又又又太多了,还要拆出去,你这方案也不怎么样嘛?还不如我左低耦合高内聚,右职责单一原则,单独export
! 答:…… 可以,重构! 问:看着5个二级模块,近100个对象陷入沉思……
前端小白记录思考点点滴滴,点赞收藏关注一下我呗(QwQ)~
作者:爱吃好果汁丶
链接:https://juejin.cn/post/7290558277666930744