步骤四:network封装【React项目创建】

173 阅读3分钟
  • 前端项目难免需要和后端交互、请求数据,所以如何做到统一的请求配置请求处理是需要解决的问题。
  • 这里是基于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) {
    ifAxios.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)

传送门