深入挖掘前端基础服务&中间件设计-请求封装

本文探讨了前端接口请求的三种方式:Ajax、Fetch和Axios,并重点分析了Axios的封装设计。通过介绍设计原则、API接口、Options和实际应用,展示了如何在项目中实现低耦合、高内聚的请求库。同时,文章还讨论了源码结构,包括入口文件、HTTP请求、中间件和日志处理,并提出了对不同业务场景的适应性思考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

项目中,对于接口请求,一般有三种方式,Ajax、Fetch、Axios

三者之间的关系如下:

上图中,Ajax是一个大的技术模型,内含很多技术,异步请求、局部刷新等

正常我们所说的Ajax异步请求,是基于浏览器内置的对象XMLHttpRequest实现的,Axios也是基于XHR的子集是实现。

简单理解:

  • Ajax - 异步请求 Javascript和XML,XMLHttpRequest 是浏览器内置的对象,是实现Ajax的一种方式。* Fetch -ES6新增API,提出Promise对象,是XMLHttpRequest实现方式的一种替代方案* Axios 是随着Vue而广泛使用,是基于Promise封装、基于XHR进行的二次封装请求库,是XHR的子集。特点:

  • Ajax-局部刷新* Fetch-基于Promise模块化设计,rep/res等对象分散开来,使用友好,使用数据流对象处理数据,性能较好* Axios是Promise API,使用友好,转换数据、取消数据、安全防御XSRF等,功能较多。上述我们了解目前网页中的网络请求,阐述了其特点,能更好的帮助我们在项目中选择以及封装。

设计

对于平台在请求的封装上,优先使用 Axios封装库,其优点很多,使用简单方便。

下面我们来看看,平台端的设计

从上图我们可以看出,请求封装库Request处于框架层的设计,服务于业务应用层。

所以设计上要遵守一定的约定:

1、低耦合,去业务性2、高内聚,功能相对独立,可提供完整的API功能3、使用简单,去框架性

API:

参数说明
$http基于 axios 封装的 ajax 请求方法
$httpXMLInstanceXML 请求方式
$httpMultiPartInstanceformData 文件传输请求
logger基于$http 封装的记录日志请求

Options

参数说明类型
url请求地址string
headers请求偷any
method请求方式string
requestId请求接口对应唯一标识string

应用

import { Request } from '@basic-library';
Request.$http({url: '/api/getScoreInfo',data,method: 'post',
})
.then((res) => {Request.logger.save({function: 105201,module: 105200,description: `查看xxx信息ID:${id}`,});return res;}).catch((e) => {return Promise.reject(e);}); 

上述代码是正常http请求方式,设计API中有涉及到 XML和 formData的方式,这两种在业务开发中实际应用场景为:

  • 文件上传,支持'Content-Type': 'multipart/form-data'
  • 导出功能,文件导出是以文件流的形式导出,responseType:"blob",指定响应类型为blob,服务端返回文件流后,转换为blob,然后使用a标签进行下载,这是目前导出功能的通用方式

源码:

入口文件 index

import { $http, $httpMultiPartInstance, $httpXMLInstance } from './http';
import { registerResponseMiddleware, registerResponseErrorMiddleware, registerResponseAbourtMiddleware } from './middleware';
import produce from 'immer';
import * as logger from './logger';

const ServiceInterface = {$http,$httpXMLInstance,$httpMultiPartInstance,registerResponseMiddleware,registerResponseErrorMiddleware,registerResponseAbourtMiddleware,logger,
};

let proxy = produce(ServiceInterface, () => { });

let ServiceProxy = (function () {if (window._SERVICE_) {return window._SERVICE_;} else {window._SERVICE_ = proxy;return proxy;}
})();

export default ServiceProxy; 

请求文件 http

import fetchAxios from 'fetch-like-axios';
import { responseMiddleware, responseErrorMiddleware } from './middleware';

const CancelToken = fetchAxios.CancelToken;

const config = {baseURL: '/',timeout: 60 * 1000,xhrMode: 'fetch',headers: {Accept: 'application/json; charset=utf-8','Content-Type': 'application/json; charset=utf-8',},
};

const httpInstance = fetchAxios.create(config);

/**
 * 请求之前拦截动作
 */
httpInstance.interceptors.request.use((response) => response,(error) => console.error(error)
);

/**
 * 请求之后拦截动作
 */
httpInstance.interceptors.response.use((response) => {// reqStore.setOkRequest(response.config.requestId);if (responseMiddleware.length === 0) {return response;} else {responseMiddleware.forEach((fn) => (response = fn(response)));return response;}},function httpUtilErrorRequest(error) {if (error.config) {// reqStore.setOkRequest(error.config.requestId);}if (responseErrorMiddleware.length !== 0) {responseErrorMiddleware.forEach((fn) => (error = fn(error)));return Promise.reject(error);}if (!error.response) {console.error(error);return Promise.reject(error);}return Promise.reject(error.response);}
);

export function $http(options) {let cancel;const cancelToken = new CancelToken((c) => {cancel = c;if (options.cancelHttp) {options.cancelHttp(cancel);}});if (<img src="httpInstance({ ...newOptions, cancelToken })" style="margin: auto" />
}

export const $httpMultiPartInstance = fetchAxios.create({xhrMode: 'fetch',timeout: 10 * 60 * 1000,headers: {'Content-Type': 'multipart/form-data',},
});

$httpMultiPartInstance.interceptors.response.use((response) => response,(error) => Promise.reject(error)
);

export const $httpXMLInstance = function xhrRequest({ url, method = 'GET', data, headers, cancelHttp, isAsync = false, requestId }) {return new Promise((resolve, reject) => {const xhr = new XMLHttpRequest();const cancel = () => xhr.abort();if (cancelHttp) {cancelHttp(cancel);}// reqStore.setPaddingRequest({ requestId, cancel });xhr.open(method, url, !isAsync);if (headers) {Object.keys(headers).forEach((key) => {xhr.setRequestHeader(key, headers[key]);});}xhr.responseType = headers?.responseType || 'blob';xhr.onreadystatechange = function () {// reqStore.setOkRequest(requestId);if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 304)) {let data;try {data = JSON.parse(xhr.response);} catch (e) {data = xhr.response;}resolve(data);}if (xhr.readyState === 4 && (xhr.status !== 200 || xhr.status !== 304)) {reject(xhr);}};xhr.send(data ? JSON.stringify(data) : null);});
}; 

中间件 middleware

export const responseAbourtMiddleware = [];
export const responseMiddleware = [];
export const responseErrorMiddleware = [];

export function registerResponseAbourtMiddleware(abortArr) {abortArr.forEach((_abort) => {if (!responseAbourtMiddleware.includes(_abort)) {responseAbourtMiddleware.push(_abort);}});
}

export function registerResponseMiddleware(fn) {if (!responseMiddleware.includes(fn)) {responseMiddleware.push(fn);}
}

export function registerResponseErrorMiddleware(fn) {if (!responseErrorMiddleware.includes(fn)) {responseErrorMiddleware.push(fn);}
} 

工具和日志

这部分主要是针对请求进行日志记录,业务上根据情况而定,从设计上看,这部分与业务有一定的耦合性,有必要的化,建议可以拿出去。

或者在业务hooks层包装一层,来做日志的记录和特殊业务处理。

功能开发中直接调用业务hooks来进行

import { $http } from './http';
export function save(data,token) {return $http({method: 'POST',url: `/api/log/v1/addLog`,data,requestId: 'addLog',headers : {Authorization: token,}});
}

export function formartDesc(desc, data) {try {Object.keys(data).forEach((key) => {desc = desc.replace(`<${key}>`, data[key]);});} catch (e) {console.warn('日志描述转换异常!', e);}return desc;
} 

核心代码解读:

1、设计之初,也已经规划好,需要输出的能力,中间件、日志记录、请求响应拦截、响应内容定制、不同的请求类型

2、入口文件中,主要是对于请求封装库,需要向外暴露那些能力,基本的就是正常请求、文件上传、导出相关的。

其他的几个,主要是以中间件的角色出现,针对响应体中,进行中间件的拦截,使其具备一定的扩展和定制能力。

  • 拦截响应体
  • 响应体异常情况
  • 中止响应

3、请求主体文件中使用到了 fetch-like-axios 这个和axios的库基本一样,只是增加了_fetch_的方式

const CancelToken = fetchAxios.CancelToken;

使用到了,CancelToken,这个的作用是取消请求使用。

const httpInstance = fetchAxios.create(config);

主要是实例化请求,和加载请求配置,请求配置基本的包含:

  • baseURL 基础的根url
  • timeout 请求超时时间
  • xhrMode 请求类型 fetch或者xhr
  • headers 请求头定义,比如内容类型、token之类

httpInstance.interceptors.request.use 请求之前拦截动作 httpInstance.interceptors.response.use 请求之后拦截动作

请求之前根据情况进行设置,请求之后一般设置比较多,因为不同的业务和设计,对响应体的定义方式不一样。

对于中间件的设置,目前是放置在响应体内进行,

responseMiddleware.forEach((fn) => (response = fn(response)));

暴露外部使用方式:

export function registerResponseMiddleware(fn) {if (!responseMiddleware.includes(fn)) {responseMiddleware.push(fn);}
} 

可以通过外部定义响应体的拦截,以及执行策略,目前支持数组方式。

Service.registerResponseMiddleware(function (Response) {return Promise.reject({});
});

Service.$http({ url: '/manifest.json' }).then((res) => {console.log(res);
}); 

讲解完毕,这是基本的请求封装库,基本满足所有使用场景。

总结思考

但是有必要深入思考一下,这种使用方式优雅吗?真的是这样吗?

Request.$http({url: '/api/getScoreInfo',data,method: 'post',
}).then((res) => {...return res;
})
.catch((e) => {return Promise.reject(e);}); 
const {res} = await Request.$http({url: '/api/getScoreInfo',data,method: 'post',
}) 

各有利弊,对于await异常拦截就有点捉急了。

对于这种使用方式,如果你的业务框架约定使用MVC模式,比如模块中有module层,这个我其他文章有提到过

models

定义了使用 CRUD 处理数据库的函数。每一个函数都代表了一种行为(读取一个数据、读取所有数据、编辑数据、删除数据等)

这样的话,这个功能的所有请求都可以写到一个文件中,有点类似egg的Controller层设计

const Controller = require('egg').Controller;

class BaseController extends Controller {
async success...
async error...
}

module.exports = BaseController; 
const BaseController = require('./base');

class ConfigController extends BaseController {//添加数据async add() {}
... 

简单列举下,有那么点意思,不能太深入。

如果是这种模式下,这种使用方式是挺好的。

但是对于无分层的业务框架,直接使用,还是有点繁琐,不够简洁,可以设想下

const {success, returnObj} = await Service.useHttp("resetPassword", { accountId });

尽量一句搞定,异常也统一进行拦截…

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值