前言
hello~ ,上周发了我的处女文《这个前端Api管理方案会更好?》,收获了"海量"支持和鼓励,也有一部分建议和质疑,一个个仔细看后也作了认真思考,希望这篇文章能够回应疑问和建议,感谢指点~
上篇回顾
还没看《这个前端Api管理方案会更好?》的可以先去看~~
总结几种API管理方案的优劣,提出Api文件面向模块 的方案,解决冗长文件带来的难维护性,可读性差 的问题,大大提升接口的可读性 。并通过封装函数统一对接口管理,提高代码的拓展性,灵活性和复用性。
接下来对评论区的一些问题进行解答~~~~~~~
为什么导出的是一个封装函数api?
js
// 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(){}
首先,封装带来的开发效率和开发成本是一个值得思考的平衡问题,我看到了越来越多鼓励减少封装的文章,我本人也趋向尽量减少不必要的封装
但为什么这个方案里我倾向封装函数呢?
优点:方便统一管理多个对象的接口,接口了多了之后,加一层抽象可以提高代码的拓展性,灵活性和可复用性。因为面向同一模块,不同对象的接口一定程度上有共性,如果需要对这些接口作拓展又不影响原接口,那么封装无疑是必需的。
举几个理想的例子,例如:
1. 统一给接口加上固定参数
js
// 偏函数固定参数,这里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列表接口加上本地缓存回显判断
希望给该模块所有列表页,统一新增本地缓存列表的筛选项功能
js
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列表接口加上节流
js
// 暴露一个访问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请求同时拥有上面三种功能
js
function -.-Api() {
zhijixie
}
芜湖,一个函数......咦,怎么报错了?
因此我们可以根据场景选择导出封装函数,很好的解决了拓展性和复用性的问题
js
import { paramsApi, getApi, throttleApi } from 'xxxmodule'
如果换做面向对象,想在模块里统一添加上述功能可以试想一下多麻烦
这个方案不利于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
优化降低影响。
拆模块
将当前模块的对象作模块化拆成更小的二级模块,但如果接口或对象不多不建议拆,避免大量文件->大量文件夹
对象导出
js
export default {
commonApi: apiMap['common'],
dogApi: apiMap['dog'],
catApi: apiMap['cat'],
}
import {commonApi,...} from "xxx"
严格说导入带有多个属性的对象也不利于Tree shaking
, 无引用的接口函数也会被打包,但影响基本是忽略不计中的忽略不计,另外也无法直接使用封装函数。
webpack优化
js
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
...
api: {
name:'chunk-apis'
test: /api(/|\).*/index.js/,
priority: 10,
minChunks: 2,
}
}
})
将api
文件作为独立的chunk
块,从而更好地利用加载缓存,其他页面引用api
文件时就不会再重复加载了。
使用类继承
js
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
的方式。
应用场景
我个人认为小型到中大型无论是否crud
都适用,很大规模的项目没接触过,但与其遵循什么低耦合高内聚,职责单一原则整一大长串文件没人看,还不如一把梭至少还能看的懂,设计原则的最终目的应该是面向开发者
模块里的对象太多怎么处理
答:拆成二级模块,模块文件夹下新增二级模块文件夹,保持apiMap
的二级结构, 也能降低Tree shaking的影响
问:又太多了
答:继续拆
问:又又又又太多了,还要拆出去,你这方案也不怎么样嘛?还不如我左低耦合高内聚,右职责单一原则,单独export
!
答:...... 可以,重构!
问:看着5个二级模块,近100个对象陷入沉思......
前端小白记录思考点点滴滴,点赞收藏关注一下我呗(QwQ)~