- 前端项目难免需要和后端交互、请求数据,所以如何做到统一的
请求配置、请求处理是需要解决的问题。 - 这里是基于
axios库的二次封装
需要实现什么?
- 根据环境(主站、测试站、开发站等)确定请求的baseURL
- 统一处理请求参数配置,如token
- 通过拦截器统一处理响应
- 封装一个构造函数(类)提供常用的接口请求:构造函数的目的是,可以针对不同环境(这里的环境指的是一个前端可能访问不同的后端接口)实现特有的解析和配置
- 取消请求
实现步骤
基础配置
确定环境对应的不同配置(如baseURL)
// serverConfig.js
function getEnvUrl() {
const env = localStorage.getItem("environment");
switch(env) {
case "test":
return "xxxxx"; // 测试站地址
case "product":
return "xxxx" // 主站地址
default:
return "xxx"
}
}
export const serverConfig = {
// 当前环境下,对应的后端url
domain: getEnvUrl();
}
基于axios的类封装
// http-client.js
import Axios, { AxiosResponse, AxiosRequestConfig } from "axios";
// 抛错处理
function rejectedInterceptor(error: any) {
throw error;
}
// 请求的拦截器
Axios.interceptors.request.use(async (arc, AxiosRequestConfig) => {
return arc;
}, rejectedInterceptor)
// 响应结果拦截器
Axios.interceptors.response.use(async (ar, AxiosResponse) => {
return ar;
}, rejectedInterceptor)
// 封装的类,未来所有的请求都是这个类的实例
export class HttpClient {
async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return Axios.get<T>(url, config)
}
}
封装的实例化
import { HttpClient } from "http-client"
class BaseServer {
http = new HttpClient();
constructor(domain:string) {}
async get<T>(url: string, setting:AxiosRequestConfig) {
return this.http.get<T>(url, { ...setting, baseURL: this.domain }).then(res => res.data)
}
}
- 下面的引用,就可以通过对BaseServer的继承,实现设置baseURL。
- 这里有点多余,为什么要不停的继承,处理请求配置和结果,有两个原因
- 一个前端页面,可能存在不同后端接口的请求,就可以通过下面的继承实现对应的
请求配置 - 不同的后端,往往对应着不同的返回结构,可以对
响应结果进行统一处理
- 一个前端页面,可能存在不同后端接口的请求,就可以通过下面的继承实现对应的
// business.server.js
class Business extends BaseServer {
constructor() {
super(serverConfig.domain)
}
async get<T>(url: string, setting?: AxiosRequestConfig) {
return super.get<T>(url, setting).then(res => res.data)
}
}
最终调用
不同的业务逻辑,应该对应着不同的请求。这里定义为provider。比如用户模块、新闻模块等,应该使用不同的provider(这样才是一个比较优秀的代码)
// baseProvider.ts
// 这个目录统一管理服务器
export class BaseProvider<T = {}> {
protected server: Business = new Business(); // 服务器1
protected mockServer: Business = new BusinessMock(); // mock服务器
}
class UserProvider extends BaseProvider<User> {
async login(userName:string, password:string) {
try {
const res = await this.server.post<any>("login", { params: {} })
return res.data;
} catch(e) {
throw e;
}
}
}
扩展(取消请求)
以上看似复杂的封装,已经满足了network请求的封装、保留了可扩展性。但是又会遇到另外一个问题:
- 如果我有一个搜索的功能,我在搜索发送的间隙,又发送了请求。这个情况,我希望把原来的请求给主动取消掉
- 这个功能就可以在拦截器中实现
思路
- 通过map保存当前正在请求的接口
- 在请求的拦截器中,发现了重复的接口请求,就将上一次的请求取消掉。并从map中移除
- 如果正常返回,则从map中移除
实现
const onRequest = new Map(); // key:能代表请求的唯一标志,由getKey函数获取;value:axios.CancalToken回调函数的返回
function rejectedInterceptor(error: any) {
if(Axios.isCancel(error)) {
// 取消请求抛出错误吧就
return Promise.reject(error)
}
throw error;
}
function getKey() {
// 返回当前请求的key唯一标志,能代表这个请求的。也就是地址 + 请求方式
return key;
}
// 检查是否有重复的,如果有有重复,取消请求
function remove(config) {
const key = getKey(config)
if (onRequest.has(key)) {
// 有重复请求
const cancelToken = onRequest.get(key);
cancelToken()
onRequest.delete(key)
}
}
// 把当前请求加入到map中
function add(config) {
const key = getKey(config)
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel =>{
onRequest.set(key, cancel)
})
}
// 请求的拦截器
Axios.interceptors.request.use(async (arc, AxiosRequestConfig) => {
remove(arc) // 移除重复的
addConfig(arc) // 把当前请求加入
return arc;
}, rejectedInterceptor)
// 响应结果拦截器
Axios.interceptors.response.use(async (ar, AxiosResponse) => {
remove(ar) // 返回成功了,移除
return ar;
}, rejectedInterceptor)