onion-middleware的由来
嗯。。。闲来无事瞎搞的!!!!主要用来实现请求/相应拦截,当然队列性的数据操作都是可以的
直接上使用教程
-
安装
npm install onion-middleware
-
使用
import { OnionMiddleware } from 'onion-middleware'
// vite等模块化加载的工具需调用clearMiddleware
app.clearMiddleware()// 创建一个中间件应用
const app = new OnionMiddleware()// 添加中间件
app.use(async (ctx, next) => {
// Middleware Before to do something
next()
// Middleware After to do something
}) -
实战使用
import { BaseResponse, CtxType, FetchOptionsType } from './type';
import storage from '../storage';
import { sortObject } from '../common';
import { aesDecrypt, aesEncrypt } from './cryproUtil';
import config from '@/configs/config';
import { message } from 'antd';
import { OnionMiddleware } from 'onion-middleware'
const pendingPromises: { [key: string]: any } = {};
export const fetchApi = (options: FetchOptionsType) => {
const { url, timeout, headers, method, data = {}, ...args } = options || {};
const queryData =
data instanceof FormData ? data : JSON.parse(JSON.stringify(data)); // 去掉undefined的属性值
let conpleteUrl = url;
const reqOptions = { ...args, headers, method };if (['GET', 'DELETE'].includes(method?.toUpperCase())) {
conpleteUrl = url + '?';
for (let key in queryData) {
const val = queryData[key]
conpleteUrl +=${key}=${typeof val === 'object' ? JSON.stringify(val) : encodeURIComponent(queryData[key])}&
;
}
} else {
reqOptions.body =
typeof queryData === 'string' || queryData instanceof FormData
? queryData
: JSON.stringify(queryData);
if (queryData instanceof FormData) {
delete reqOptions.headers['Content-Type'];
}
}return Promise.race([
fetch(conpleteUrl, reqOptions)
.then((response) => {
return response.json();
})
.catch((e) => {
return JSON.stringify({
code: 504,
msg: '连接不到服务器',
});
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
),
]);
};const app = new OnionMiddleware()
app.clearMiddleware();// 特殊业务处理中间件(401),以及相应数据的ts类型定义
app.use(async (ctx: CtxType, next: (arg?: any) => void) => {
// Middleware Before
await next();
// Middleware After
const { response } = ctx;
// 401,1401 重新静默登录
if ([401, 1401].includes(response?.code)) {
// 未登录
storage.clearAll();
message.error('登录过期,请重新登陆!');
// to do something
}
});// 重复请求复用中间件
app.use(async (ctx: CtxType, next: (arg?: any) => void) => {
// Middleware Before
const { request } = ctx;
// 使用请求信息作为唯一的请求key,缓存正在请求的promise对象
// 相同key的请求将复用promise
const requestKey = JSON.stringify([
request.method.toUpperCase(),
sortObject(request.data),
]);
if (pendingPromises[requestKey]) {
console.log('重复请求,已合并请求并返回共享的第一次请求,参数:', request);
await next({
shouldAbort: true,
callback: () => {
return pendingPromises[requestKey];
},
});
} else {
await next();
}
// Middleware After
delete pendingPromises[requestKey];
});// 请求/相应信息输出中间件
app.use(async (ctx: CtxType, next: (arg?: any) => void) => {
// Middleware Before
await next();
// Middleware After
const { request, response } = ctx;
if (request?.errorTip && ![200, 401, 1401].includes(response.code)) {
message.error(response.msg);
}
console.log('请求参数', {
url: request?.url,
data: request?.originData,
method: request?.method,
});
console.log('响应参数', response);
});// header处理中间件(此中间件位于)
app.use(async (ctx: CtxType, next: (arg?: any) => void) => {
// Middleware Before
const { request, response } = ctx;
const token = storage.getItem('token');
if (token) {
// 请求头token信息,请根据实际情况进行修改
request.headers['Authorization'] = 'Bearer ' + token;
}
ctx.request = request;await next();
// Middleware After
});// 加解密
app.use(async (ctx: CtxType, next: (arg?: any) => void) => {
// Middleware Before
const { request } = ctx;
request.originData = request.data;
if (config.OPEN_ENCRYPTION) {
const aesData = aesEncrypt(request.data);
// 加密
request.data = { encryptedReqData: aesData[0] };
// sign
request.headers['sign'] = aesData[1];
}
await next();
// Middleware After
if (config.OPEN_ENCRYPTION) {
if (ctx.response?.encryptedResData) {
ctx.response = aesDecrypt(ctx.response?.encryptedResData || '') || '{}';
}
}
});// 公共请求发送方法(简约)
const send = <T>(options: any): Promise<T> => {
const { errorTip = true } = options;
// HTTP请求上下文
const ctx: CtxType = {
request: {
...options,
errorTip,
config: {
timeout: 60 * 1000,
baseUrl: config.VITE_API_URL + config.VITE_BASE_URL,
},
headers: {
'Content-Type': 'application/json',
},
},
promise: { resolve: () => { }, reject: () => { } },
};const baseRequest = new Promise((resolve: (arg: T) => void, reject) => {
// 打破promise回调作用域,在其他地方实现调用
ctx.promise.resolve = resolve;
ctx.promise.reject = reject;// 执行中间件 app .execute(ctx, () => { let { config } = ctx?.request || {}; const { data, method, url, headers } = ctx?.request || {}; const fetchPromise = new Promise(async (_resolve) => { config = { ...config, data, headers, url: config.baseUrl + (url || ''), method: method.toUpperCase(), // 配置method方法 }; const res = await fetchApi({ ...options, ...config, }); _resolve(res); }); return fetchPromise; }) .then(() => { }) .catch((err) => { console.log(err); });
});
// 使用请求信息作为唯一的请求key,缓存正在请求的promise对象
// 相同key的请求将复用promise
const requestKey = JSON.stringify([
options.method.toUpperCase(),
options.url,
sortObject(options.data),
]);
// 存储第一次请求引用 (重复请求判断需要)
if (!pendingPromises[requestKey]) {
pendingPromises[requestKey] = baseRequest;
}
return baseRequest;
};// 公共请求发送方法
const sendApi = <T = BaseResponse>(options: FetchOptionsType): Promise<T> => {
return send(options);
};export default sendApi;
效果简单截个图吧,拿请求/相应信息输出中间件为例,效果如下:
结尾
轻点喷😂😂😂