前端项目中如何优雅地封装接口请求?一篇讲清 JS 请求管理思路
在前端开发中,接口请求几乎贯穿了所有业务页面。列表查询、详情回显、表单提交、删除操作,本质上都离不开请求后端接口。
很多项目在初期为了赶进度,往往会把请求直接写在页面里。页面一多,问题就慢慢暴露出来了:请求逻辑重复、接口地址分散、异常处理混乱、loading状态难维护,最后整个页面会变得越来越难改。
这篇文章只聊一件事:前端项目中如何更规范地处理接口请求。
一、为什么不建议在页面里直接写请求
很多人一开始会这样写:
axios.post('/xxx/list',params).then(res=>{this.tableData=res.data})这种写法短期看没问题,但一旦项目变大,很容易出现以下问题:
- 接口地址分散在各个页面中,不方便统一维护
- 页面里既有业务逻辑,又有请求细节,可读性差
- 相同接口调用方式重复书写,复用性差
- 后续如果要统一加 token、错误提示、请求拦截,改动范围会很大
- 页面代码越来越臃肿,不利于排查问题
所以,更推荐的方式是:把接口请求统一封装,再由页面按需调用。
二、接口请求为什么要单独封装
在实际项目中,比较常见的做法是把接口统一放到api目录下,按模块拆分文件。页面只负责调用接口方法,不直接关心底层请求实现。
例如,我们可以这样封装一个接口方法:
importrequestfrom'@/utils/request'exportfunctiongetListApi(params,data){returnrequest({url:'/module/list',method:'post',params,data})}这种方式的优势非常明显:
- 请求地址集中管理,方便维护
- 页面中调用语义更清晰
- 相同接口可以多处复用
- 方便统一处理请求头、鉴权、拦截器
- 页面代码更专注于业务逻辑本身
简单来说,接口封装的核心价值就是解耦。
页面负责“用”,接口文件负责“管”。
三、一个项目里的接口请求通常分为哪几层
如果想让请求逻辑更清晰,通常可以分为三层:
1. 请求工具层
这一层一般是对axios的再次封装,主要负责:
- 基础地址配置
- 超时时间配置
- 请求头统一处理
- token 注入
- 请求拦截和响应拦截
- 通用错误提示
例如:
importaxiosfrom'axios'constservice=axios.create({baseURL:'/api',timeout:10000})exportdefaultservice这一层通常不会写具体业务,只负责“底层能力”。
2. 接口模块层
这一层是把每个业务模块的接口单独封装成方法。
importrequestfrom'@/utils/request'exportfunctiongetListApi(params,data){returnrequest({url:'/module/list',method:'post',params,data})}exportfunctiondeleteApi(id){returnrequest({url:`/module/delete?id=${id}`,method:'get'})}这一层的作用是:把具体业务接口抽象成可调用的方法。
3. 页面调用层
这一层就是我们日常写页面逻辑的地方。页面只关心:
- 什么时候发请求
- 请求成功后如何赋值
- 请求失败后如何提示
- 是否展示
loading
例如:
getList(){this.loading=truereturngetListApi(this.page,this.formData).then((res)=>{if(res.code===200){this.tableData=res.rows}else{this.$message.error(res.msg)}}).finally(()=>{this.loading=false})}这种写法结构很清晰,职责也很明确。
四、为什么loading最好放在finally中处理
很多人在写接口请求时,习惯在then里关闭loading:
getList(){this.loading=truegetListApi().then(res=>{this.loading=falsethis.tableData=res.rows})}这并不是完全错误,但它有明显风险。
因为只有在请求成功进入then时,这段代码才会执行。如果出现下面这些情况:
- 网络异常
- 接口超时
- 服务端报错
- 请求被拦截器拦截
- 业务处理过程中抛出异常
那么this.loading = false就可能执行不到,页面会一直处于加载状态。
因此,更稳妥的写法是把关闭loading放进finally:
getList(){this.loading=truereturngetListApi().then((res)=>{if(res.code===200){this.tableData=res.rows}}).finally(()=>{this.loading=false})}为什么推荐这样写?
因为finally的作用就是:
无论请求成功还是失败,只要这次 Promise 结束了,都会执行这里的代码。
这就非常适合放一些“收尾动作”,比如:
- 关闭
loading - 关闭弹层加载态
- 清理临时状态
- 还原按钮禁用状态
所以,页面中凡是涉及异步请求的状态收尾,优先考虑finally,会更稳。
五、一个更推荐的接口请求思路
在实际开发中,我更推荐采用这样的思路:
1. 所有请求都走统一请求工具
这样便于统一处理:
- token
- baseURL
- 超时
- 通用错误
- 拦截器逻辑
2. 所有业务接口都单独封装方法
不要把接口地址散落在页面里。
页面应该调用方法,而不是直接拼请求配置。
3. 页面只负责业务数据处理
页面中最重要的是“业务表达”,而不是请求细节。
4. 所有异步状态收尾统一放到finally
尤其是:
loading- 按钮禁用状态
- 上传遮罩层
- 弹框中的处理中状态
这一步非常关键,能明显减少异步状态异常。
六、总结
前端接口请求,看起来只是“调一下接口”,但真正决定代码质量的,是请求背后的组织方式。
一个更规范的接口请求结构,通常具备这些特点:
- 请求工具统一封装
- 业务接口按模块统一管理
- 页面调用逻辑清晰简单
loading状态有统一收尾- 页面不直接堆砌请求细节
如果只是小 demo,怎么写都能跑;但如果是实际业务项目,接口请求这部分越早规范,后续维护成本就越低。
前端开发写到最后,拼的往往不是“能不能实现”,而是“能不能持续维护”。
而接口请求的封装方式,恰恰就是最能拉开代码质量差距的地方之一。
