接口相关配置
接口特指前后端分离开发中,前端向后端发起 HTTP 请求的接口,是前后端分离开发的重要环节。组织合理,配置妥当的接口配置,可以极大提高开发效率,降低维护成本。因此,接口相关的配置是项目开发中必不可少的一环。
在进行接口配置时,我们需要尽可能的考虑到各种情况,还需保证接口配置的灵活性,以适应项目需求的变化。具体来说,可以从以下几个方面着手:
- 接口相关文件组织
- 接口地址配置
- 请求拦截器
- 响应拦截器(响应状态码配置)
- 请求超时时间
- 跨域配置
- 接口数据处理(请求数据转化、响应数据转化、请求头封装)
- 并发优化
本文将基于 Vue 和 Axios,介绍接口相关配置的内容。
接口相关文件组织
接口相关文件的组织目的是为了使得接口文件结构清晰,便于维护与阅读。涉及的内容包括文件夹组织、文件内容组织等。
下面,将依次介绍文件内容组织、文件夹组织内容。
文件内容组织
接口相关内容主要包括接口配置、接口请求函数两部分。接口配置包括请求/响应拦截器的封装、请求超时时间设置、跨域配置、请求头封装等,这些配置通常整合为一个config.js
文件;接口请求函数是接口请求的封装,通常与项目的模块划分方式保持一致,一个模块对应一个接口请求函数文件。例如,项目包含用户管理、商品管理、订单管理三个模块,那么应创建user.js
、product.js
、order.js
三个接口文件。每个接口文件中定义与对应模块相关的所有接口。此外,一个基础接口函数需要包括请求方法和请求地址。复杂的接口函数还可以包括请求参数封装、请求头设置、响应数据处理等。
配置文件示例:
// 文件地址:api/config.js
import axios from 'axios'
const instance = axios.create({
baseURL: 'http://localhost:3000/api',
timeout: 5000,
withCredentials: true
})
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么
// 可以在这里设置请求头、请求参数等
return config
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// 响应拦截器
instance.interceptors.response.use(
(response) => {
// 对响应数据做点什么
// 可以在这里处理响应数据
return response.data
},
(error) => {
// 对响应错误做点什么
return Promise.reject(error)
}
)
export default instance
请求函数文件示例:
// 文件地址:api/user.js
import instance from './config'
// 用户登录
export const login = (data) => {
return instance.post('/user/login', data)
}
// 用户注册
export const register = (data) => {
return instance.post('/user/register', data)
}
// 获取用户信息
export const getUserInfo = () => {
return instance.get('/user/info')
}
文件夹组织
大多数情况下,接口相关文件会放在项目的 api
文件夹中。该文件夹可以在项目的根目录下创建,也可以在项目的 src
文件夹下创建。如果某个模块的接口特别多,可以在api
文件夹下创建与模块对应的子文件夹,用于对接口文件进行更细致的分类。
src
├── api
│ ├── config.js
│ ├── user.js
│ ├── product
│ │ ├── 3c.js
│ │ ├── general.js
│ │ └── index.js
│ └── order.js
在上述组织方式中,config.js
是配置文件。user.js
、product/
、order.js
分别对应用户管理、商品管理、订单管理的接口文件。其中商品管理接口文件product/
下又分划分3c.js
、general.js
两个文件,分别对应 3C 产品、一般商品的接口。而product/index.js
则是商品接口的汇总。
接口配置封装
明确了接口文件的组织方式后,接下来,我们来看一下如何进行接口配置的封装。
接口配置封装的目的是为了使得接口配置更加通用、灵活,降低接口配置的维护成本。在进行接口配置封装时,不但要尽可能的让配置项兼顾到各种情况,还要保证配置的灵活性,以适应项目需求的变化。换句话说,接口配置封装要兼顾通用性和灵活性。通用性要求封装好的接口配置能够使得开发者只需要关注接口请求函数的编写,而无需关心接口配置的细节;灵活性则要求接口配置能够根据项目需求的变化进行灵活配置,覆盖默认的配置项。
接口配置主要包括请求拦截器、响应拦截器、请求超时时间、跨域配置、请求头封装和接口地址配置。下面将介绍如何完成上述接口配置封装。
接口地址配置
在 Axios 中,接口地址分为接口请求的基础地址和接口请求的路径。接口请求的基础地址通常为接口请求的域名和端口(如:http://localhost:3000/api
),主要通过baseURL
配置项进行设置。接口请求的路径则是在接口请求的基础地址的基础上,添加具体的接口路径(如:/user/login
),通常在接口请求函数中设置。需要注意的是,Axios 规定,如果接口请求的路径是一个绝对路径(即,完整的 url),则不会拼接基础地址。
// 文件地址:api/config.js
import axios from 'axios'
const instance = axios.create({
baseURL: 'http://localhost:3000/api'
})
TIP
如果接口地址是以/
开头,则baseURl
不能以/
结尾,否则会拼接出错。
多环境的接口地址配置
在项目开发过程中,通常需要根据不同的环境(如:开发环境、测试环境、生产环境)来配置不同的接口地址。因此,接口地址配置需要支持多环境配置。 在 Vue 项目中,可以通过import.meta.env
对象来获取环境变量。因此,我们可以将接口地址配置为环境变量,然后在config.js
文件中根据环境变量来设置接口请求的基础地址。
# 文件地址:.env.development
VITE_APP_API_BASE_URL=http://localhost:3000/api
# 文件地址:.env.production
VITE_APP_API_BASE_URL=https://api.example.com
// 文件地址:api/config.js
import axios from 'axios'
const instance = axios.create({
baseURL: import.meta.env.VITE_APP_API_BASE_URL
})
多服务器的接口地址配置
如果项目需要同时请求多个服务器,则需要实现多个基础地址的配置。有三种实现方法:
- 方法一:创建多个
axios.create
实例,每个实例对应一个基础地址。 - 方法二:动态设置
axios.create
实例的baseURL
属性。 - 方法三:每个接口请求函数提供完整的接口地址,不再在创建
axios.create
实例时设置默认的baseURL
。
方法一示例:
// 文件地址:api/config.js
import axios from 'axios'
// 基础地址1的实例
const instance1 = axios.create({
baseURL: 'http://localhost:3000/api'
})
// 基础地址2的实例
const instance2 = axios.create({
baseURL: 'http://localhost:4000/api'
})
export { instance1, instance2 }
方法二示例:
// 文件地址:api/config.js
import axios from 'axios'
// 创建实例时设置默认的基础地址
const instance = axios.create({
baseURL: 'http://localhost:3000/api'
})
// 动态设置基础地址方法,如果接口请求的URL需要采用不同的基础地址,则调用该方法设置基础地址
export const setBaseUrl = (baseUrl) => {
instance.defaults.baseURL = baseUrl
}
// 文件地址:api/user.js
import { setBaseUrl } from './config'
// 用户登录
export const login = (data) => {
setBaseUrl('http://localhost:4000/api') // 覆盖基础地址
return instance.post('/user/login', data)
}
尽管方法一和方法二都能满足多服务器接口地址配置的需求,但它们都有一定的局限性。
方法一的缺陷:
- 需要创建多个
axios.create
实例,增加了项目的复杂度。 - 新增服务器时,需要新增
axios.create
实例,增加了接口配置的维护成本。
方法二的缺陷:
- 虽然只需要创建一个
axios.create
实例,但需要调用setBaseUrl
方法来动态设置基础地址,增加了接口请求函数的复杂度。 - 由于只有一个实例,每调用一次接口都需要更新实例的基础地址。如果某个接口请求函数忘记调用
setBaseUrl
,则会复用前一次接口请求的基础地址,从而导致请求地址错误。
综上,推荐使用方法三。
方法三:每个接口请求函数提供完整的接口地址
方法三借用了 Axios 的特性,即接口请求的路径是一个绝对路径时不会拼接基础地址。因此,方法三要求我们在每个接口请求函数中提供完整的接口地址,从而实现多服务器接口地址配置。采用方法三,我们只需要在项目中创建一个axios.create
实例,因此也不需要在新增服务器时创建额外实例。由于每个接口请求函数都提供了完整的接口地址,因此也不会存在错误复用基础地址的问题。采用方法三,接口请求函数的复杂度与之前相同,但接口配置的维护成本大大降低。
方法三示例:
// 文件地址:api/config.js
import axios from 'axios'
// 创建实例时设置默认的基础地址
const instance = axios.create({
// 方法三不需要设置基础地址
baseURL: 'http://localhost:3000/api'
})
export default instance
// 文件地址:api/user.js
import instance from './config'
// 用户登录
export const login = (data) => {
return instance.post('http://localhost:4000/api/user/login', data)
}
除了提供完整的接口地址外,Axios 也支持在接口请求函数中设置基础地址,即通过baseURL
配置项来设置基础地址。因此,我们也可以在接口请求函数中设置基础地址,从而实现多服务器接口地址配置。
// 文件地址:api/user.js
import instance from './config'
// 用户登录
export const login = (data) => {
return instance.post('/user/login', data, {
baseURL: 'http://localhost:4000/api' // 设置基础地址
})
}
结合多环境配置,方法三的完整示例:
# 文件地址:.env.development
VITE_APP_API_BASE_URL=http://localhost:3000/api
# 文件地址:.env.production
VITE_APP_API_BASE_URL=https://api.example.com
// 文件地址:api/config.js
import axios from 'axios'
// 创建实例时设置默认的基础地址
const instance = axios.create({
// 方法三不需要设置基础地址
baseURL: 'http://localhost:3000/api'
})
export default instance
// 文件地址:api/user.js
import instance from './config'
// 用户登录
export const login = (data) => {
return instance.post(`${import.meta.env.VITE_APP_API_BASE_URL}/user/login`, data)
}
请求超时时间和跨域配置
请求超时时间
在开发过程中,如果接口请求时间过长,会导致用户体验不佳。因此,我们需要为接口请求设置超时时间,当接口请求超过超时时间时,自动中断请求,并提示用户请求超时。如下:
// 文件地址:api/config.js
import axios from 'axios'
const instance = axios.create({
timeout: 5000 // 将请求超时时间设置为5000毫秒,默认值为1000毫秒
})
export default instance
请求超时时间具体多少合适,需要根据项目的具体情况来决定。如果服务器响应时间较长,则可以适当增加超时时间;如果服务器响应时间较短,则可以适当减小超时时间。大多数情况下,默认值 1000ms 已经基本满足需求。此外,在考量超时时间时,只需要考虑正常情况下主要的多数接口即可,对于极少数响应时间较长的接口,可以在接口请求配置中单独设置超时时间。
跨域配置
跨域请求是前端开发中常见的问题,由于浏览器的同源策略限制,前端无法直接请求不同域名下的接口。因此,我们需要在接口请求配置中设置跨域配置,从而实现跨域请求。如下:
// 文件地址:api/config.js
import axios from 'axios'
const instance = axios.create({
timeout: 5000, // 将请求超时时间设置为5000毫秒,默认值为1000毫秒
withCredentials: true // 设置跨域请求时是否需要使用凭证
})
export default instance
withCredentials
配置项用于设置跨域请求时是否需要使用凭证。默认情况下,跨域请求时不会携带凭证,即不会携带 Cookie、HTTP 认证信息等。如果接口请求需要携带凭证,则需要设置withCredentials
为true
。
需要注意的是,如果接口请求需要携带凭证,则服务器也需要设置允许携带凭证。否则,浏览器会阻止跨域请求。 关于跨域请求的详细内容,请参考《跨域请求》或《跨域问题浅析 | Jay 的博客》。
出于安全考虑,我们不能将全部请求的withCredentials
配置项都设置为true
,否则可能会泄露敏感信息。合理的做法是,对于需要携带凭证的请求,单独设置withCredentials
为true
。即,接口请求函数中设置;对于不需要携带凭证的请求,保持默认值false
。
// 文件地址:api/user.js
import instance from './config'
// 用户登录
export const login = (data) => {
return instance.post('/user/login', data, {
withCredentials: true // 设置跨域请求时需要使用凭证
})
}
不过这样一来就增加了接口请求函数的复杂度。好在 Axios 提供了请求拦截器这一利器,我们可以通过拦截器统一设置withCredentials
为true
,在简化接口请求函数的复杂度的同时实现域名层级的控制。大体思路:在请求拦截器中维护一个域名白名单,对于在白名单中的域名,设置withCredentials
为true
,否则设置为false
。如下:
请求拦截器
关于请求拦截器,将在下一节详细介绍。
# 文件地址:.env
# 域名白名单
VITE_APP_WHITE_LIST=http://localhost:4000/api,http://localhost:5000/api
// 文件地址:api/config.js
import axios from 'axios'
const instance = axios.create({
timeout: 5000 // 将请求超时时间设置为5000毫秒,默认值为1000毫秒
})
// 读取域名白名单
const whitelist = import.meta.env.VITE_APP_WHITE_LIST.split(',')
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 判断是否在白名单中
if (whitelist.includes(config.baseURL)) {
config.withCredentials = true
}
return config
},
(error) => {
return Promise.reject(error)
}
)
export default instance
请求拦截器
请求拦截器是 Axios 提供的一个功能,可以在请求发送之前对请求进行拦截,从而实现请求预处理。例如,可以在请求拦截器中统一设置请求头、请求参数等。请求拦截器的使用是为了统一处理请求,避免在每个接口请求函数中重复编写相同的代码,简化接口请求代码和提高相关代码的可维护性。具体要做哪些处理,需要根据项目的具体需求来决定。除了上文提到的设置withCredentials
之外,常见的处理还有:
设置 HTTP Authorization 请求标头
HTTP Authorization 请求标头用于提供服务器验证用户代理身份的凭据,以获取受保护资源的访问权限。
语法如下:
Authorization: <auth-scheme> <authorization-parameters>
其中,<auth-scheme>
表示认证方案,如Basic
、Bearer
等;<authorization-parameters>
表示认证参数,如username:password
、token
等。
HTTP Authorization
关于 HTTP Authorization 的详细内容,请参考《HTTP Authorization | MDN》。
HTTP Authorization 请求标头结合 JWT(JSON Web Token) 是最常见的用法,JWT(JSON Web Token)是一种用于在客户端和服务器之间传递信息的紧凑、安全的方式。JWT 会生成一个加密的身份信息令牌(token),需要在请求时将此令牌作为 HTTP Authorization 请求标头的值发送给服务器,服务器通过验证令牌来验证用户的身份和权限。
// 文件地址:api/config.js
import axios from 'axios'
const instance = axios.create({})
// 请求拦截器
instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
// 设置 HTTP Authorization 请求标头
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
注意
上述代码中,localStorage.getItem('token')
是从本地存储中获取 token,实际项目中,token 可能会存储在 cookie、sessionStorage 或其他地方,具体取决于项目的需求。
设置公共请求参数
公共请求参数是指在每次请求中都需要传递的参数,例如 API 密钥、时间戳、签名等。这些参数可以在请求拦截器中统一设置,避免在每个接口请求函数中重复编写相同的代码。
# 文件地址:.env
VITE_APP_APP_KEY=your_app_key
VITE_APP_APP_SECRET=your_app_secret
// 文件地址:api/config.js
import axios from 'axios'
const instance = axios.create({})
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 添加公共参数
config.params = {
...config.params,
appKey: import.meta.env.VITE_APP_APP_KEY,
appSecret: import.meta.env.VITE_APP_APP_SECRET,
}
return config
}),
响应拦截器
与请求拦截器相对应的是响应拦截器。响应拦截器是在请求响应之后,但在响应数据被处理之前执行的。响应拦截器可以用于处理响应数据,多用于实现错误信息的统一处理、响应数据格式规范化和等场景。
处理错误
造成接口请求失败的原因有很多,例如网络问题、服务器错误、权限问题、参数问题等等。我们可以借助响应拦截器来统一处理这些错误,从而简化错误处理代码和提高代码的可维护性。针对不同的错误,采取不同的处理方式。具体来说,可以按照 HTTP 状态码将错误分为以下几类:
- 客观原因造成的错误,例如网络问题、服务器错误等,这类错误无法通过修改代码来解决,需要通知用户重新请求或联系管理员。
- 主观原因造成的错误,且有固定的处理逻辑,例如权限问题、登录问题这类错误可以编写固定的处理逻辑来引导用户。
- 主观原因造成的错误,但没有固定的处理逻辑,例如参数错误、数据格式错误等,这类错误需要根据具体的错误信息进行处理。
针对以上三类问题,前两类错误可以在接口拦截器中统一处理;第三类错误则需在具体的接口请求代码根据业务需求进行处理。此外,尽管前两类错误的处理逻辑已经在响应拦截器中进行了统一处理,但为了避免特殊的业务需求,在编写响应拦截器时需预留对应错误的统一处理逻辑的开关控制变量,且控制的颗粒度要尽可能小,以便在业务代码中根据需求进行灵活的调整。
HTTP 状态码
关于 HTTP 状态码,可查看:HTTP 响应状态码
以下是一个简单的示例,演示了如何使用响应拦截器处理错误:
// 文件地址:api/config.js
import axios from 'axios'
import { Message } from 'element-ui'
/**
* 创建 axios 实例,由于不同接口需要配置不同的errorConfig,所以需要为每个接口创建一个实例
*
* @param {{[status: number]: {default: boolean, message: string|boolean}}} errorConfig 错误配置
* @returns {AxiosInstance} axios 实例
*/
const createInstance = (errorConfig) => {
const instance = axios.create({})
// 响应拦截器
instance.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
// 处理错误信息
const { response } = error
if (response) {
// 处理响应错误
const { status, data } = response
switch (status) {
case 401:
// 未登录,跳转到登录页并给出提示
const config = errorConfig['401']
if (!config || config.default !== false) this.$router.push('/login')
if (config.message !== false) Message({ message: config.message || '未登录', type: 'error' })
break
case 403:
// 无权限,跳转首页并给出提示
const config = errorConfig['403']
if (!config || config.default !== false) this.$router.push('/')
if (config.message !== false) Message({ message: config.message || '无权限访问', type: 'error' })
break
case 404:
// 给出提示
const config = errorConfig['404']
if (config.message !== false) Message({ message: config.message || '接口不存在', type: 'error' })
break
case 500:
// 给出提示
const config = errorConfig['500']
if (config.message !== false) Message({ message: config.message || '服务器错误', type: 'error' })
break
default:
// 处理其他错误,在具体的接口请求代码中根据业务需求进行处理
break
}
} else {
// 处理请求错误,在具体的接口请求代码中根据业务需求进行处理
}
return Promise.reject(error)
}
)
}
export default createInstance
并发优化
前端的常用的并发优化策略有三种:1. 请求队列;2. 请求合并;3. 防抖节流。防抖节流须在在 DOM 事件中控制。故,不再本文中介绍。下面简单讲讲请求队列和请求合并。
请求队列
请求队列的主要思路是:全局维护两个变量,当前请求数量running
和请求阻塞队列Queue
。在请求发出前先判断,当前请求数量是否超过上限,如果超过,则通过await
阻塞当前请求,然后将resolve
存入阻塞队列Queue
中。当任意请求完成后,则从阻塞队列Queue
中取出一个resolve
,并执行它,从而释放被阻塞请求。
/**
* 请求队列
* @class RequestQueue
*/
export class RequestQueue {
max = 100 // 默认值前置声明
running = 0
queue = []
constructor() {
const maxCount = Number(import.meta.env.VITE_APP_API_MAX_REQUEST_COUNT ?? 100) // 使用空值合并运算符并确保数值合法
this.max = Number.isInteger(maxCount) && maxCount >= 0 ? maxCount : 100
}
/**
* 添加一个请求到队列中。
* @param {Function} request - 返回 Promise 的异步请求函数。
* @returns {Promise} - 请求的结果。
*/
async add(request) {
// 如果当前运行的请求数已达到最大值,将请求加入等待队列
if (this.running >= this.max) {
await new Promise((resolve) => this.queue.push(() => resolve())) // 使用函数包装 resolve,避免直接存储 resolve 引发潜在问题
}
this.running++
try {
return await request()
} finally {
this.running--
// 从队列中取出下一个等待的请求并执行
const nextResolve = this.queue.shift()
if (nextResolve) {
nextResolve() // 调用 resolve 解锁等待的请求
}
}
}
}
关于请求队列和接口合并
现在的浏览器性能已经非常高,短时间的高并发请求已经基本不存在了。如果请求队列阈值设置的不当,反而会增加请求队列的等待时间,从而影响用户体验。所以,除非真的遇到极端情况,否则不建议使用请求队列。相反,请求合并策略非常推荐使用。一来,可以避免重复请求,二来,可以减少服务器的负载,再者,可以提高响应速度。
接口合并
当在极短的时间内以同样的参数和配置多次请求同一个接口时,我们可以将这些接口合并成一个。具体来说就是,假设一个接口 A 从发起请求到响应之间的需要经过 10 秒,如果在这 10 内该请求再次发起,那么我们可以将后面发起的请求取消,转而使用第一次请求的响应,减少请求,提高效率。
具体思路如下:全局维护一个请求 Map,用来存储请求的键值对,键为请求的 URL 和参数的组合,值为请求的 Promise 对象。当发起一个请求时,先检查该请求是否已经存在,如果存在,则直接返回该 Promise 对象,否则发起请求,并将该请求的 Promise 对象存储到 Map 中,并返回。当收到响应时,根据请求的 URL 和参数的组合,从 Map 中删除该键值对。
/**
* HTTP请求合并
* @class RequestMerge
*/
export class RequestMerge {
constructor() {
this.requestMap = new Map() // 创建一个Map对象,用于存储请求的键值对
}
/**
* 合并重复请求的通用方法
* @param {Object} instance - Axios实例对象,用于发起网络请求
* @param {string} method - HTTP请求方法(get/post/put/delete等)
* @param {string} url - 请求接口地址
* @param {Object} data - 请求参数对象(GET请求时自动转为params参数)
* @param {Object} config - Axios请求配置项(优先级高于axiosConfig)
* @param {Object} axiosConfig - 基础Axios配置项
* @returns {Promise} 合并后的Promise对象(已缓存的请求实例或新建的请求)
*/
request(instance, method, url, data, config, axiosConfig) {
// 验证HTTP方法是否合法
const validMethods = ['get', 'post', 'put', 'delete', 'patch']
if (!validMethods.includes(method.toLowerCase())) {
throw new Error(`Invalid HTTP method: ${method}`)
}
// 生成唯一缓存键
const generateCacheKey = (url, data, config, axiosConfig) => {
const stableStringify = (obj) => JSON.stringify(obj, Object.keys(obj || {}).sort())
return `${url}-${stableStringify(data)}-${stableStringify(config)}-${stableStringify(axiosConfig)}`
}
const key = generateCacheKey(url, data, config, axiosConfig)
// 检查缓存中是否存在相同请求
// 使用Map对象来存储正在进行的请求,避免重复请求
if (this.requestMap.has(key)) {
// 如果缓存中已经存在相同的请求,则直接返回该请求的Promise对象
return this.requestMap.get(key)
} else {
let promise
// 根据请求方法类型处理参数格式
// 对于GET和DELETE请求,参数需要放在params中
if (['get', 'delete'].includes(method.toLowerCase())) {
promise = instance[method](url, { params: data, ...config })
} else {
// 对于其他请求方法,参数直接放在data中
promise = instance[method](url, data, config)
}
promise.finally(() => {
this.requestMap.delete(key) // 接口请求完成时删除该键值对
})
this.requestMap.set(key, promise) // 将新请求实例存入缓存Map
return promise
}
}
}
与事件的防抖接口的对比
借助防抖节流同样可实现
写在最后
接口配置是前端开发中非常重要的一部分,一个好的接口配置可以帮助我们更好地管理接口请求,提高代码的可维护性和可读性。在进行接口配置时,需要考虑全面,尽可能涵盖各种可能出现的情况。此外,可配置性也需要给予足够的重视,保证接口配置可以根据具体需求进行细致的调整。本文简单地介绍了一些常见的接口配置方法,更加完善的接口配置需要根据具体业务需求进行定制。