前言
项目中,对于接口请求,一般有三种方式,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 请求方法 |
$httpXMLInstance | XML 请求方式 |
$httpMultiPartInstance | formData 文件传输请求 |
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的提问。
有需要的小伙伴,可以点击下方卡片领取,无偿分享