前端-这个前端Api管理方案会更好?


[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函数封装了一层,可以统一管理接口提高拓展性和复用性,例如统一给actionget的套一个节流函数

// 暴露一个访问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里模块里的接口都被打包引入了 img

但api文件占的大小是多少呢? img 这个模块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()

类继承的方式是很棒的设计,非常适合crudrestful 规范的场景,可以较好的提高代码复用性,也方便对对象进行管理维护。

但是从方案的可读性上看,类依旧不如apiMap直观,因为接口实际上只需要关注接口名以及注释,而类则混入了逻辑代码,没有apiMap那么聚合。

并且当脱离crudrestful后,复用性的优势也没有了。

因此这个方案还是更推荐apiMap的方式。

6.应用场景

我个人认为小型到中大型无论是否crud都适用,很大规模的项目没接触过,但与其遵循什么低耦合高内聚,职责单一原则整一大长串文件没人看,还不如一把梭至少还能看的懂,设计原则的最终目的应该是面向开发者

7.模块里的对象太多怎么处理

答:拆成二级模块,模块文件夹下新增二级模块文件夹,保持apiMap的二级结构, 也能降低Tree shaking的影响

问:又太多了 答:继续拆

问:又又又又太多了,还要拆出去,你这方案也不怎么样嘛?还不如我左低耦合高内聚,右职责单一原则,单独export ! 答:…… 可以,重构! 问:看着5个二级模块,近100个对象陷入沉思……

前端小白记录思考点点滴滴,点赞收藏关注一下我呗(QwQ)~

作者:爱吃好果汁丶
链接:https://juejin.cn/post/7290558277666930744


文章作者: 千羽
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 千羽 !
评论
  目录