参考:
- https://axios-http.com/
- https://cli.vuejs.org/zh/guide/mode-and-env.html
- https://codeantenna.com/a/B9ng1xY7vQ
- https://stackoverflow.com/questions/43013858/how-to-post-a-file-from-a-form-with-axios
- https://stackoverflow.com/questions/41938718/how-to-download-files-using-axios
- https://stackoverflow.com/questions/21660791/what-is-the-main-difference-between-patch-and-put-request#:~:text=According to HTTP terms%2C The,some portion of existing resource
- https://axios-http.com/docs/urlencoded
- https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/POST
一、前言
之前接手了实习生写的项目,我对很多地方做了完善,其中包括 API 接口封装,因此借此机会将这部分经验转化为文字分享给大家。看前须知,项目是用 VueCLI 搭建的。
一般来说,公司的后端采用 RESTful API 的规范来编写接口的。
项目前期接口较少,因此前后端联调时,请求数据时一般会写成这样:
// 拿取货物数据
fetchGoodsData () {
return axios.get('http://xxx.yyy.zzz:3000/api/product1/v1/goods')
}
然而,随着产品的功能迭代,接口自然也会越来越多,如果还是按照上面的方式去写,将会面临一系列的麻烦:
第一、永久性变量冗余。 例如, http://xxx.yyy.zzz:3000/api/product1/v1/ ,这一串几乎永远是冗余的;
第二、高强度心智负担。 例如,http://xxx.yyy.zzz:3000/api/product1/v1/carts?page=1&size=-1 ,一旦传递的参数过多,就会出现漏写、写错的情况;
第三、模块过于扁平化。 我们从上面几个接口上是无法直接判断当前接口请求的数据究竟属于哪一个功能模块,而且请求含义不明确,非常依赖开发人员编写的方法名。
为了解决以上几个难题,我们就需要对 API 进行封装。
二、问题分析
面对这样一个复杂的问题,思路往往是反其道而行之的,“分而治之”, 让其简单化。
打开后端给的 API 文档,举个例子,假设有以下几个模块:
- 用户模块
- 货物模块
- 购物车模块
- ……
回想前言中提到的第三个问题——模块过于扁平化,无法区分接口属于哪个功能模块,无法区分请求含义。
如何让我们在发请求时更清晰地分辨接口呢?
// 拿取货物数据
fetchGoodsData () {
return this.$api.goods.fetchGoodsData()
}
如上图,只是观察函数调用即可明确功能模块、请求的具体含义。
与此同时还解决了第一个问题——永久性冗余,冗余的信息被封装到了 axios 实例中。
对于第二个问题——请求参数过多,认知负担增加,我们把需要传递的数据传进去即可,像这样:
// 拿取货物数据
fetchCartsData (params) {
return this.$api.carts.fetchCartsData(params)
}
// 获取所有购物车数据,不分页
fetchCartsData({
pages: 1,
size: -1
})
.then(data => {
// ...
})
.catch(e => console.error(e))
看到这里,如果你是一位新手,应该会一头雾水,那就对了。因为也许你并不知道http://xxx.yyy.zzz:3000/api/product1/v1/ 去哪了,也不知道 ?page=1&size=-1 怎么就突然作为参数传到封装好的 API 函数里去了。
Take it easy,just go on.
三、Axios
我们简单过一下 Axios —— 基于 Promise 的 HTTP 客户端,可用于浏览器和 node.js,还可以拦截请求和响应,转化请求和响应数据,取消请求,自动转换 JSON 数据,防御 XSRF 攻击……
简而言之,它可以帮你获取数据,请求和响应时帮你拦截数据,总是返回一个 Promise,然后你从 Promise 里拿到数据,整理后展示到页面上。
回到第二节末尾提出的问题: http://xxx.yyy.zzz:3000/api/product1/v1/ 去哪了??page=1&size=-1 去哪了?它们都被放进了 axios 实例中。具体来说,前者被放到了 baseURL 中,后者以对象的形式被放到了 params 中。参考链接:https://axios-http.com/docs/req_config
如下图,一个 axios 实例主要由两个部分组成:拦截器、配置。
在正常的开发中,前端可能需要连接不同环境进行接口联调,因此其中,http://xxx.yyy.zzz:3000/api/product1/v1/ 作为 baseURL 就不能写死,需要根据本地服务器的模式来进行转换。
打个比方,如果是开发环境,baseURL 切换为 http://192.168.1.1:30000;如果是测试环境,baseURL 切换为 http://192.168.1.3:40000。因此,我们只要将 baseURL 的地址写在环境变量中就可以了。
现在我们需要设置模式及其环境变量,之后通过 process.env 就可以获取到对应变量。
四、模式与环境变量
VueCLI 提供了几种默认的模式,但是一般我们都会自己自定义。
通过 --mode 可以定义模式,例如,我要定义测试模式(其他模式以此类推)。
打开 package.json,找到 scripts 这一项,添加测试模式:
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test": "vue-cli-service serve --mode test"
}
另外,我们需要在根目录下(和 src 目录同级)新建一个 .env.test 文件,用来保存环境变量:
NODE_ENV = 'test'
VUE_APP_BASE_URL = 'http://192.168.1.3:4000'
之后,来到 main.js 中,添加一条 console.log(process.env); 。
打开控制台,执行命令:npm run test
项目运行后,在浏览器的控制台中就能看到 process.env 里保存着 NODE_ENV 和 VUE_APP_BASE_URL。
有了这两个变量,模式和环境变量就有了对应关系。然后将 process.env.VUE_APP_BASE_URL 赋值给 axios 实例的 baseURL,就能实现 baseURL 的灵活转换。
补充几点:
-
mode 的参数与 .env 文件后缀有关系。
.env # 在所有的环境中被载入 .env.local # 在所有的环境中被载入,但会被 git 忽略 .env.[mode] # 只在指定的模式中被载入 .env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略 -
VUEAPP* 是 VueCLI 通过
webpack.DefinePlugin静态地嵌入到客户端侧 的代码中的。 -
更多的疑惑,参考这篇文章:https://cli.vuejs.org/zh/guide/mode-and-env.html
五、Axios 封装
Axios 的封装需要管理两件事情:一是创建实例,二是设置拦截器。
找到 utils 目录,在里面新建 request.js
5.1 创建实例
import axios from "axios";
// create an axios instance: <https://axios-http.com/docs/instance>
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_URL,
timeout: 10 * 1000,
});
export default service;
可以看到这里的 baseURL 用的是就是对应模式下的环境变量了。
5.2 请求拦截器 & 响应拦截器
// ...
// request interceptor: <https://axios-http.com/docs/interceptors>
service.interceptors.request.use(
(config) => {
return config;
},
(error) => {
return Promise.reject(error);
},
);
service.interceptors.response.use(
(response) => {
return response;
},
(error) => {
return Promise.reject(error);
},
);
// ...
到此,axios 实例就准备完毕了,有 baseURL,有拦截器。
axios 的封装与本文主题无关,所以写得很简单。但是这里也给大家提供一篇参考。这是另外一种思维方式,也值得学习:https://developpaper.com/encapsulation-of-api-interface-and-axios-request-in-vue-project/#:~:text=Axios configuration (setting request header and response code,s passed in%2C and then returns a promise
六、API 接口封装
最后一步,对于 API 接口封装,我们使用面向对象的方式组织数据。
6.1 文件结构划分
在 src 目录下新建 api 目录,然后根据后端 API 接口文档划分模块:
src/api // api interface files
|_ user // 用户模块
|_ index.js
|_ goods // 货物模块
|_ index.js
|_ carts // 购物车模块
|_ index.js
|_ index.js
6.2 按模块编写
以货物模块为例,路径为:src/api/goods/index.js
import axios from "@/utils/request"; // axios 实例引入(第五节封装的)
const goods = {
// 1. 获取所有货物信息
getGoods: () =>
axios({
url: "/api/product1/v1/goods",
method: "get",
}),
// 2. 获取某条货物信息
getGoodsById: (id) =>
axios({
url: `/api/product1/v1/goods/${id}`,
method: "get",
}),
// 3. 新增一条货物信息
addGoods: (data) =>
axios({
url: "/api/product1/v1/goods",
method: "post",
data,
}),
// 4. 删除某条货物信息
deleteGoodsById: (id) =>
axios({
url: `/api/product1/v1/goods/${id}`,
method: "delete",
}),
// 5. 更新某条货物信息
// put 与 patch 的区别:put 所有字段均要传递;patch 可选字段传递,不用全部传
updateGoodsById: (id, data) =>
axios({
url: `/api/product1/v1/goods/${id}`,
method: "put",
data,
}),
// 6. 下载货物报表
downloadGoodsReport: (data) =>
axios({
url: `/api/product1/v1/goods/report`,
method: "post",
data,
responseType: "blob", // browser only: 'blob'
}),
// 7. 上传货物报表
uploadGoodsReport: (data) =>
axios({
url: `/api/product1/v1/goods/up_report`,
method: "post",
data,
headers: { "Content-Type": "multipart/form-data" },
}),
};
export default goods;
以上,展示了常用的请求形式,包括:GET, POST, PUT/PATCH, DELETE。
补充几点:
-
url = base url + request url,其中 base url 就是在 5.1 节中,axios 实例上的 baseURL: process.env.VUE_APP_BASE_URL -
第五条更新数据,用了 PUT,其实还有一个类似的 PATCH,他们的区别在上面已经给了注释。PUT 和 PATCH 的区别:https://stackoverflow.com/questions/21660791/what-is-the-main-difference-between-patch-and-put-request#:~:text=According to HTTP terms%2C The,some portion of existing resource
-
发送 POST 请求使用 data ,而发送 GET 请求时,可能需要带上 url 参数,这时候就要用到 params 了。
举个请求购物车分页数据的例子:
http://xxx.yyy.zzz:3000/api/product1/v1/carts?page=1&size=-1// src/api/carts/index.js import axios from "@/utils/request"; const carts = { // ... // 获取购物车信息 getCarts: (params) => axios({ url: "/api/product1/v1/carts", method: "get", params, }), }; export default carts; -
有时候我们还会看到,在发送 POST 请求时,有人会使用 qs ,它的作用是序列化 JSON 格式数据为 urlencoded 格式的字符串。
原因是后端语言处理 JSON 格式的数据比较麻烦,例如 Go 语言的 Gin 框架,传递给它
{Country: Brasil, City: Belo Horizonte},为了安全起见,需要开发人员据此单独做结构体,而传递'Country=Brasil&City=Belo Horizonte'这样的字符串则更为合适。这里改写一下货物模块中的第三个 POST 请求,记得首先要引入 qs 模块:
import axios from '@/utils/request' + import qs from 'qs' const goods = { // ... // 3. 新增一条货物信息 addGoods: (data) => axios({ url: '/api/product1/v1/goods', method: 'post', data: qs.stringify(data) }), // ... } export default goods -
关于 urlencoded 可以查看以下三篇参考:
6.3 导出 API 对象
当写完各个模块的 api 请求接口后,就需要导出了。回到第二节的构思,我们想要通过 this.$api.moduleName.methodName(…args) 的形式调用方法发起请求。
进入 src/api/index.js
import user from "./user";
import goods from "./goods";
import carts from "./carts";
// ...
class API {
constructor() {
this.user = user;
this.goods = goods;
this.carts = carts;
// ...
}
}
// 导出使用
export default new API();
6.4 小试牛刀
将 API 实例导出之后,将它挂载到 Vue 原型上,就能正常使用了。
// src/main.js
import API from "./api";
Vue.prototype.$api = API;
// Test.vue
<template>
<button @click="handleData">test request</button>
</template>
<script>
export default {
name: "Test",
data() {
return {
tableData: [],
};
},
methods: {
fetchData() {
return this.$api.goods.getGoods();
},
async handleData() {
try {
const response = await this.fetchData();
// ...
} catch (e) {
console.error(e);
}
},
},
};
</script>
6.5 冗余问题的解决
以上,其实还有一个问题尚未解决,就是每一个请求 api 的 url 里的 /api/product1/v1 都是重复的,这个容易,把它分离出去变成一个模块,自定义 url 前缀。
src/api/url_prefixes.js
function generatePrefix({ productName, version } = { productName: "product1", version: "v1" }) {
return `/api/${productName}/${version}`;
}
export const v1 = generatePrefix(); // '/api/product1/v1'
export const v2 = generatePrefix({ version: "v2" }); // '/api/product1/v2'
export const p2v2 = generatePrefix({ productName: "product2", version: "v2" }); // '/api/product2/v2'
从上面可以看到,除了接口版本可能会发生变化,产品名称也是有可能发生变化的,我们也用变量控制。之后,在各模块的 api 文件中就可以用使用变量来控制 url 前缀部分了。
例如改写货物模块的 api url 前缀:
import axios from "@/utils/request"; // axios 实例引入(第五节封装的)
import { v1 } from "../url_prefixes.js"; // 引入前缀:'/api/product1/v1'
const goods = {
// 1. 获取所有货物信息
getGoods: () =>
axios({
url: `${v1}/v1/goods`,
method: "get",
}),
// 2. 获取某条货物信息
getGoodsById: (id) =>
axios({
url: `${v1}/goods/${id}`,
method: "get",
}),
// 3. 新增一条货物信息
addGoods: (data) =>
axios({
url: `${v1}/v1/goods`,
method: "post",
data,
}),
// ...
};
export default goods;
至此,三个关键问题都已经得到了解决:
- 永久性变量冗余
- 高强度心智负担
- 模块过于扁平化
七、总结
请求的 API 封装到此就结束了,除了以上这种面向对象的设计以外,还有其他的方式。最后,整理一下封装的整体流程:
- 请求 API 结构设计,this.$api.moduleName.methodName(…args)
- 脚手架模式与环境变量设置
- Axios 封装(包括实例配置和拦截器),导出实例
- 请求 API 编写及导出,注意分模块编写
- 将 API 挂载到 Vue 实例,正常使用
文章里,我添加了很多参考链接,如有疑惑可以看一看,看完如果仍然有疑惑,那就找人讨论吧,之后这一块内容你应该就可以掌握了。
以上,如有谬误,还请斧正,希望这篇文章对你有所帮助,感谢您的阅读~