前言
项目中,对于接口请求,一般有三种方式,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 (!options.requestId) {
// console.warn('缺少requestId');
} else {
// reqStore.setPaddingRequest({ requestId: options.requestId, cancel });
}
const { cancelHttp, ...newOptions } = options;
return httpInstance({ ...newOptions, cancelToken });
}
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 });
尽量一句搞定,异常也统一进行拦截....
想想吧,我们下章来讲这块....
老铁们,我们一起关注系列设计:
深入挖掘前端基础服务&中间件设计-basic-library
深入挖掘前端基础服务&中间件设计-字典设计
深入挖掘前端基础服务&中间件设计-配置设计
深入挖掘前端基础服务&中间件设计-用户信息
深入挖掘前端基础服务&中间件设计-权限控制
# 深入挖掘前端基础服务&中间件设计-请求封装