mirror of
https://github.com/WeBankFinTech/fes.js.git
synced 2025-08-07 18:39:45 +08:00
feat: request to fetch
This commit is contained in:
parent
828a58d885
commit
3c66904896
@ -1,6 +1,6 @@
|
||||
# @fesjs/plugin-request
|
||||
|
||||
基于 axios 封装的 request,内置防止重复请求、请求缓存、错误处理等功能。
|
||||
基于 fetch 封装的 request,内置防止重复请求、请求缓存、错误处理等功能。
|
||||
|
||||
## 启用方式
|
||||
|
||||
@ -10,35 +10,39 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@fesjs/fes": "^3.0.0",
|
||||
"@fesjs/plugin-request": "^3.0.0"
|
||||
"@fesjs/plugin-request": "^4.0.0-rc.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 运行时配置
|
||||
|
||||
入口文件的全局配置,具体请求的配置参数会覆盖全局配置,支持 [axios](https://axios-http.com/zh/docs/req_config) 所有的参数。
|
||||
入口文件的全局配置,具体请求的配置参数会覆盖全局配置,支持 [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included) 所有的参数。
|
||||
|
||||
```js
|
||||
import { defineRuntimeConfig } from '@fesjs/fes';
|
||||
|
||||
export default defineRuntimeConfig({
|
||||
request: {
|
||||
// API 前缀
|
||||
baseURL: '',
|
||||
dataHandler(data, response) {
|
||||
timeout: 10000, // 默认 10s
|
||||
method: 'POST', // 默认 post
|
||||
mergeRequest: false, // 是否合并请求
|
||||
responseType: null, // 可选 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData',默认根据 content-type 处理
|
||||
credentials: 'include', // 默认 include, 'include' | 'same-origin' | 'omit'
|
||||
headers: {}, // 传给服务器的 header
|
||||
cacheData: false, // 是否缓存
|
||||
requestInterceptor: (config: Config) => Config,
|
||||
responseInterceptor: (response: RequestResponse) => RequestResponse,
|
||||
transformData(data, response) {
|
||||
// 处理响应内容异常
|
||||
if (data.code !== '0') {
|
||||
if (isPlainObject(data)) {
|
||||
if (data.code === '10000') {
|
||||
FMesseage.error('hello world');
|
||||
return Promise.reject(data);
|
||||
}
|
||||
if (data.code === '20000') {
|
||||
FMesseage.error('hello world');
|
||||
}
|
||||
throw new Error(response);
|
||||
}
|
||||
// 响应数据格式化
|
||||
return data?.result ? data.result : data;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
// http 异常,和插件异常
|
||||
errorHandler(error) {
|
||||
@ -47,13 +51,7 @@ export default defineRuntimeConfig({
|
||||
console.log(error.response.data);
|
||||
console.log(error.response.status);
|
||||
console.log(error.response.headers);
|
||||
} else if (error.request) {
|
||||
// 请求已经成功发起,但没有收到响应
|
||||
// `error.request` 在浏览器中是 XMLHttpRequest 的实例,
|
||||
// 而在node.js中是 http.ClientRequest 的实例
|
||||
console.log(error.request);
|
||||
} else if (error.type) {
|
||||
// 插件异常
|
||||
} else if (error.msg) {
|
||||
console.log(error.msg);
|
||||
} else {
|
||||
// 发送请求时出了点问题
|
||||
@ -61,11 +59,7 @@ export default defineRuntimeConfig({
|
||||
}
|
||||
console.log(error.config);
|
||||
},
|
||||
// 请求拦截器
|
||||
requestInterceptors: [],
|
||||
// 响应拦截器
|
||||
responseInterceptors: [],
|
||||
// 支持其他 axios 配置
|
||||
// 支持其他 fetch 配置
|
||||
...otherConfigs,
|
||||
},
|
||||
});
|
||||
@ -82,7 +76,7 @@ export default defineRuntimeConfig({
|
||||
|
||||
- url: 后端接口 url
|
||||
- data: 参数
|
||||
- options: 配置支持 [axios](https://axios-http.com/zh/docs/req_config) 所有的参数,和插件扩展参数。
|
||||
- options: 配置支持 [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included) 所有的参数,和插件扩展参数。
|
||||
|
||||
- **返回值**: Promise
|
||||
|
||||
@ -148,7 +142,7 @@ request(
|
||||
password: '123456',
|
||||
},
|
||||
{
|
||||
cache: {
|
||||
cacheData: {
|
||||
cacheType: 'ram', // ram: 内存,session: sessionStorage,local:localStorage
|
||||
cacheTime: 1000 * 60 * 3, // 缓存时间:默认3min
|
||||
},
|
||||
@ -162,7 +156,37 @@ request(
|
||||
});
|
||||
```
|
||||
|
||||
若 `cache` 传 `true`,则默认使用 `ram` 缓存类型,缓存时间 3min。
|
||||
若 `cacheData` 传 `true`,则默认使用 `ram` 缓存类型,缓存时间 3min。
|
||||
|
||||
### 请求 abort
|
||||
|
||||
```javascript
|
||||
import { request } from '@fesjs/fes';
|
||||
|
||||
const controller = new AbortController();
|
||||
request('/url/abort', null, {
|
||||
signal: controller.signal,
|
||||
}).then((response) => {
|
||||
console.log('process response: ' + response);
|
||||
});
|
||||
// cancel the request
|
||||
controller.abort();
|
||||
```
|
||||
|
||||
### 获取 response headers
|
||||
|
||||
```javascript
|
||||
import { rawRequest } from '@fesjs/fes';
|
||||
|
||||
const controller = new AbortController();
|
||||
rawRequest('/url/abort', null, {
|
||||
signal: controller.signal,
|
||||
}).then((response) => {
|
||||
console.log('process headers: ' + response.headers);
|
||||
});
|
||||
// cancel the request
|
||||
controller.abort();
|
||||
```
|
||||
|
||||
### 结合 use 使用
|
||||
|
||||
@ -184,3 +208,11 @@ export default {
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 3.x 升级到 4.x
|
||||
|
||||
1. 缓存参数 cache 改成 cacheData(避免与 fetch 原本的 cache 冲突)
|
||||
2. dataHandler 改成 transformData
|
||||
3. requestInterceptors 改为 requestInterceptor,不在支持数组,只支持函数
|
||||
4. responseInterceptors 改为 responseInterceptor,不在支持数组,只支持函数
|
||||
5. 其他 axios 特有的配置不在支持
|
||||
|
@ -1,17 +1,22 @@
|
||||
# @fesjs/plugin-windicss
|
||||
|
||||
::: warning 即将废弃
|
||||
由于 windicss 不怎么维护了,本插件即将废弃,推荐使用 [tailwindcss](https://tailwindcss.com/)。
|
||||
:::
|
||||
|
||||
## 介绍
|
||||
|
||||
`windicss` 支持
|
||||
|
||||
## 启用方式
|
||||
|
||||
在 `package.json` 中引入依赖:
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"@fesjs/plugin-windicss": "^2.0.0"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -23,6 +28,6 @@
|
||||
export default {
|
||||
windicss: {
|
||||
root: './',
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
@ -46,7 +46,7 @@
|
||||
"dependencies": {
|
||||
"@fesjs/fes": "^3.0.0-rc.1",
|
||||
"@fesjs/plugin-icon": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-request": "^3.0.0-rc.3",
|
||||
"@fesjs/plugin-request": "^4.0.0-rc.0",
|
||||
"@fesjs/builder-webpack": "^3.0.0-rc.1",
|
||||
"vue": "^3.2.37",
|
||||
"core-js": "^3.27.0"
|
||||
|
@ -59,7 +59,7 @@
|
||||
"style-loader": "^2.0.0",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"vue-loader": "^16.1.2",
|
||||
"webpack": "^5.69.0",
|
||||
"webpack": "^5.76.2",
|
||||
"webpack-bundle-analyzer": "^4.4.0",
|
||||
"webpack-chain": "^6.5.1",
|
||||
"webpack-dev-server": "^4.8.1",
|
||||
|
@ -93,8 +93,8 @@ export default function createCssWebpackConfig({ isDev, config, webpackConfig, b
|
||||
if (!isDev) {
|
||||
webpackConfig.plugin('extra-css').use(require.resolve('mini-css-extract-plugin'), [
|
||||
{
|
||||
filename: '[name].[contenthash:8].css',
|
||||
chunkFilename: '[id].[contenthash:8].css',
|
||||
filename: 'css/[name].[contenthash:8].css',
|
||||
chunkFilename: 'css/[id].[contenthash:8].css',
|
||||
},
|
||||
]);
|
||||
webpackConfig.optimization.minimizer('css').use(require.resolve('css-minimizer-webpack-plugin'), [{}]);
|
||||
|
@ -77,8 +77,8 @@ export default async function getConfig({ api, cwd, config, env, entry = {}, mod
|
||||
webpackConfig.output
|
||||
.path(absoluteOutput)
|
||||
.publicPath(publicPath || '/')
|
||||
.filename('[name].[contenthash:8].js')
|
||||
.chunkFilename('[name].[contenthash:8].chunk.js');
|
||||
.filename('js/[name].[contenthash:8].js')
|
||||
.chunkFilename('js/[name].[contenthash:8].chunk.js');
|
||||
|
||||
// --------------- resolve -----------
|
||||
webpackConfig.resolve.extensions.merge(['.mjs', '.js', '.jsx', '.vue', '.ts', '.tsx', '.json', '.wasm']);
|
||||
|
@ -29,7 +29,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fesjs/fes": "3.0.0-rc.4",
|
||||
"@fesjs/plugin-request": "3.0.0-rc.6",
|
||||
"@fesjs/plugin-request": "^4.0.0-rc.0",
|
||||
"vue": "^3.2.37"
|
||||
},
|
||||
"typings": "./types.d.ts"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fesjs/plugin-request",
|
||||
"version": "3.0.0-rc.5",
|
||||
"version": "4.0.0-rc.0",
|
||||
"description": "@fesjs/plugin-request",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
@ -33,7 +33,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fesjs/utils": "3.0.0-rc.2",
|
||||
"axios": "^1.0.0-alpha.1"
|
||||
"@qlin/request": "^0.1.1"
|
||||
},
|
||||
"typings": "./types.d.ts"
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
import { isObject, isString, isURLSearchParams, checkHttpRequestHasBody } from './helpers';
|
||||
/**
|
||||
* 缓存实现的功能
|
||||
* 1. 唯一定位一个请求(url, data | params, method)
|
||||
* 其中请求参数根据请求方法使用其中一个就够了
|
||||
* 一个请求同时包含 data | params 参数的设计本身不合理
|
||||
* 不对这种情况进行兼容
|
||||
* 2. 控制缓存内容的大小,localStorage 只有5M
|
||||
* 3. 控制缓存时间
|
||||
* session(存在内存中)
|
||||
* expireTime 存在localStoreage 中
|
||||
* 4. 成功的、且响应内容为json的请求进行缓存
|
||||
*/
|
||||
|
||||
/**
|
||||
* 配置数据
|
||||
* type: 'ram' | 'sessionStorage' | 'localStorage'
|
||||
* cacheTime: ''
|
||||
*/
|
||||
|
||||
/**
|
||||
* 缓存数据结构
|
||||
* cache: {
|
||||
* url: 'url', // 缓存 url
|
||||
* data: data, // 数据
|
||||
* expire: '' // 缓存时间
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* 请求参数可以为如下类型
|
||||
* - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
|
||||
* - Browser only: FormData, File, Blob
|
||||
* 只缓存参数类型为: string、plain object、URLSearchParams 或者无参数的 请求
|
||||
*/
|
||||
|
||||
const CACHE_KEY_PREFIX = '__FES_REQUEST_CACHE:';
|
||||
const CACHE_TYPE = {
|
||||
ram: 'ram',
|
||||
session: 'sessionStorage',
|
||||
local: 'localStorage',
|
||||
};
|
||||
|
||||
const CACHE_DATA_MAP = new Map();
|
||||
|
||||
function genInnerKey(key, cacheType = 'ram') {
|
||||
if (cacheType !== CACHE_TYPE.ram) {
|
||||
return `${CACHE_KEY_PREFIX}${key}`;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function canCache(data) {
|
||||
return !data || isObject(data) || isString(data) || Array.isArray(data) || isURLSearchParams(data);
|
||||
}
|
||||
|
||||
function setCacheData({ key, cacheType = 'ram', data, cacheTime = 1000 * 60 * 3 }) {
|
||||
const _key = genInnerKey(key, cacheType);
|
||||
|
||||
const currentCacheData = {
|
||||
cacheType,
|
||||
data,
|
||||
cacheTime,
|
||||
expire: Date.now() + cacheTime,
|
||||
};
|
||||
if (cacheType !== CACHE_TYPE.ram) {
|
||||
const cacheInstance = window[CACHE_TYPE[cacheType]];
|
||||
try {
|
||||
cacheInstance.setItem(_key, JSON.stringify(currentCacheData));
|
||||
} catch (e) {
|
||||
// setItem 出现异常,清理缓存
|
||||
for (const item in cacheInstance) {
|
||||
if (item.startsWith(CACHE_KEY_PREFIX) && Object.prototype.hasOwnProperty.call(cacheInstance, item)) {
|
||||
cacheInstance.removeItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CACHE_DATA_MAP.set(_key, currentCacheData);
|
||||
}
|
||||
}
|
||||
|
||||
function isExpire({ expire, cacheTime }) {
|
||||
if (!cacheTime || expire >= Date.now()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getCacheData({ key, cacheType = 'ram' }) {
|
||||
const _key = genInnerKey(key, cacheType);
|
||||
if (cacheType !== CACHE_TYPE.ram) {
|
||||
const cacheInstance = window[CACHE_TYPE[cacheType]];
|
||||
const text = cacheInstance.getItem(_key) || null;
|
||||
try {
|
||||
const currentCacheData = JSON.parse(text);
|
||||
if (currentCacheData && !isExpire(currentCacheData)) {
|
||||
return currentCacheData.data;
|
||||
}
|
||||
cacheInstance.removeItem(_key);
|
||||
return null;
|
||||
} catch (e) {
|
||||
cacheInstance.removeItem(_key);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
const currentCacheData = CACHE_DATA_MAP.get(_key);
|
||||
if (currentCacheData && !isExpire(currentCacheData)) {
|
||||
return currentCacheData.data;
|
||||
}
|
||||
CACHE_DATA_MAP.delete(_key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 存储缓存队列
|
||||
const cacheStartFlag = new Map();
|
||||
const cachingQueue = new Map();
|
||||
|
||||
/**
|
||||
* 等上一次请求结果
|
||||
* 1. 如果上一次请求成功,直接使用上一次的请求结果
|
||||
* 2. 如果上一次请求失败,重启本次请求
|
||||
*/
|
||||
function handleCachingStart(ctx, config) {
|
||||
const _key = genInnerKey(ctx.key, config.cache.cacheType);
|
||||
const caching = cacheStartFlag.get(_key);
|
||||
if (caching) {
|
||||
return new Promise((resolve) => {
|
||||
const queue = cachingQueue.get(_key) || [];
|
||||
cachingQueue.set(_key, queue.concat(resolve));
|
||||
});
|
||||
}
|
||||
cacheStartFlag.set(_key, true);
|
||||
}
|
||||
|
||||
// 有请求成功的
|
||||
function handleCachingQueueSuccess(ctx, config) {
|
||||
// 移除首次缓存 flag
|
||||
const _key = genInnerKey(ctx.key, config.cache.cacheType);
|
||||
const queue = cachingQueue.get(_key);
|
||||
if (queue && queue.length > 0) {
|
||||
queue.forEach((resolve) => {
|
||||
resolve({
|
||||
response: ctx.response,
|
||||
});
|
||||
});
|
||||
}
|
||||
cachingQueue.delete(_key);
|
||||
cacheStartFlag.delete(_key);
|
||||
}
|
||||
|
||||
// 处理请求失败
|
||||
function handleCachingQueueError(ctx, config) {
|
||||
const _key = genInnerKey(ctx.key, config.cache.cacheType);
|
||||
const queue = cachingQueue.get(_key);
|
||||
if (queue && queue.length > 0) {
|
||||
const firstResolve = queue.shift();
|
||||
firstResolve();
|
||||
cachingQueue.set(_key, queue);
|
||||
} else {
|
||||
cachingQueue.delete(_key);
|
||||
cacheStartFlag.delete(_key);
|
||||
}
|
||||
}
|
||||
|
||||
export default async (ctx, next) => {
|
||||
const { config } = ctx;
|
||||
if (config.cache) {
|
||||
const cacheData = getCacheData({ key: ctx.key, cacheType: config.cache.cacheType });
|
||||
if (cacheData) {
|
||||
ctx.response = {
|
||||
data: cacheData,
|
||||
};
|
||||
return;
|
||||
}
|
||||
const result = await handleCachingStart(ctx, config);
|
||||
if (result) {
|
||||
Object.keys(result).forEach((key) => {
|
||||
ctx[key] = result[key];
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
await next();
|
||||
|
||||
if (config.cache) {
|
||||
const requestdata = checkHttpRequestHasBody(config.method) ? config.data : config.params;
|
||||
if (!ctx.error && ctx.response && canCache(requestdata) && canCache(ctx.response.data)) {
|
||||
handleCachingQueueSuccess(ctx, config);
|
||||
|
||||
setCacheData({
|
||||
key: ctx.key,
|
||||
data: ctx.response.data,
|
||||
...config.cache,
|
||||
});
|
||||
} else {
|
||||
handleCachingQueueError(ctx, config);
|
||||
}
|
||||
}
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
import { isURLSearchParams } from './helpers';
|
||||
/**
|
||||
* 唯一定位一个请求(url, data | params, method)
|
||||
* 其中请求参数(data, params)根据请求方法,只使用其中一个
|
||||
* 一个请求同时包含 data | params 参数的设计本身不合理
|
||||
* 不对这种情况进行兼容
|
||||
*/
|
||||
|
||||
const getQueryString = (data) => {
|
||||
if (isURLSearchParams(data)) {
|
||||
return data.toString();
|
||||
}
|
||||
return data ? JSON.stringify(data) : '';
|
||||
};
|
||||
|
||||
export default async function genRequestKey(ctx, next) {
|
||||
const { url, data, params, method } = ctx.config;
|
||||
|
||||
ctx.key = `${url}${getQueryString(data)}${getQueryString(params)}${method}`;
|
||||
|
||||
await next();
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
/**
|
||||
*判断类型
|
||||
* @param {*} obj 需要判断的对象
|
||||
*/
|
||||
export function typeOf(obj) {
|
||||
const map = {
|
||||
'[object Boolean]': 'boolean',
|
||||
'[object Number]': 'number',
|
||||
'[object String]': 'string',
|
||||
'[object Function]': 'function',
|
||||
'[object Array]': 'array',
|
||||
'[object Date]': 'date',
|
||||
'[object RegExp]': 'regExp',
|
||||
'[object Undefined]': 'undefined',
|
||||
'[object Null]': 'null',
|
||||
'[object Object]': 'object',
|
||||
'[object URLSearchParams]': 'URLSearchParams',
|
||||
};
|
||||
return map[Object.prototype.toString.call(obj)];
|
||||
}
|
||||
|
||||
export function isFunction(obj) {
|
||||
return typeOf(obj) === 'function';
|
||||
}
|
||||
|
||||
export function isDate(obj) {
|
||||
return typeOf(obj) === 'date';
|
||||
}
|
||||
|
||||
export function isString(obj) {
|
||||
return typeOf(obj) === 'string';
|
||||
}
|
||||
|
||||
export function isArray(obj) {
|
||||
return typeOf(obj) === 'array';
|
||||
}
|
||||
|
||||
export function isObject(obj) {
|
||||
return typeOf(obj) === 'object';
|
||||
}
|
||||
|
||||
export function isURLSearchParams(obj) {
|
||||
return typeOf(obj) === 'URLSearchParams';
|
||||
}
|
||||
|
||||
export function checkHttpRequestHasBody(method) {
|
||||
method = method.toUpperCase();
|
||||
const HTTP_METHOD = {
|
||||
GET: {
|
||||
request_body: false,
|
||||
},
|
||||
POST: {
|
||||
request_body: true,
|
||||
},
|
||||
PUT: {
|
||||
request_body: true,
|
||||
},
|
||||
DELETE: {
|
||||
request_body: true,
|
||||
},
|
||||
HEAD: {
|
||||
request_body: false,
|
||||
},
|
||||
OPTIONS: {
|
||||
request_body: false,
|
||||
},
|
||||
PATCH: {
|
||||
request_body: true,
|
||||
},
|
||||
};
|
||||
return HTTP_METHOD[method].request_body;
|
||||
}
|
||||
|
||||
export function trimObj(obj) {
|
||||
if (isObject(obj)) {
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
if (isString(value)) {
|
||||
obj[key] = value.trim();
|
||||
} else if (isObject(value)) {
|
||||
trimObj(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { checkHttpRequestHasBody, trimObj } from './helpers';
|
||||
|
||||
export default async (ctx, next) => {
|
||||
const config = ctx.config;
|
||||
if (checkHttpRequestHasBody(config.method)) {
|
||||
trimObj(config.data);
|
||||
} else {
|
||||
trimObj(config.params);
|
||||
}
|
||||
await next();
|
||||
};
|
@ -1,64 +0,0 @@
|
||||
const requestMap = new Map();
|
||||
|
||||
const mergeRequestMap = new Map();
|
||||
const requestQueue = new Map();
|
||||
|
||||
function handleCachingStart(ctx) {
|
||||
const isRequesting = mergeRequestMap.get(ctx.key);
|
||||
if (isRequesting) {
|
||||
return new Promise((resolve) => {
|
||||
const queue = requestQueue.get(ctx.key) || [];
|
||||
requestQueue.set(ctx.key, queue.concat(resolve));
|
||||
});
|
||||
}
|
||||
mergeRequestMap.set(ctx.key, true);
|
||||
}
|
||||
|
||||
function handleRepeatRequest(ctx) {
|
||||
const queue = requestQueue.get(ctx.key);
|
||||
if (queue && queue.length > 0) {
|
||||
queue.forEach((resolve) => {
|
||||
if (ctx.error) {
|
||||
resolve({
|
||||
error: ctx.error,
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
response: ctx.response,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
requestQueue.delete(ctx.key);
|
||||
mergeRequestMap.delete(ctx.key);
|
||||
}
|
||||
|
||||
export default async (ctx, next) => {
|
||||
if (ctx.config.mergeRequest) {
|
||||
const result = await handleCachingStart(ctx);
|
||||
if (result) {
|
||||
Object.keys(result).forEach((key) => {
|
||||
ctx[key] = result[key];
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (requestMap.get(ctx.key) && !ctx.config.mergeRequest) {
|
||||
ctx.error = {
|
||||
type: 'REPEAT',
|
||||
msg: '重复请求',
|
||||
config: ctx.config,
|
||||
};
|
||||
return;
|
||||
}
|
||||
requestMap.set(ctx.key, true);
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
if (ctx.config.mergeRequest) {
|
||||
handleRepeatRequest(ctx);
|
||||
} else {
|
||||
requestMap.delete(ctx.key);
|
||||
}
|
||||
};
|
@ -1,131 +1,34 @@
|
||||
import axios from 'axios';
|
||||
import { ApplyPluginsType, plugin } from '@fesjs/fes';
|
||||
import { ref } from 'vue';
|
||||
import scheduler from './scheduler';
|
||||
import { checkHttpRequestHasBody, isFunction } from './helpers';
|
||||
|
||||
import paramsProcess from './paramsProcess';
|
||||
import genRequestKey from './genRequestKey';
|
||||
import preventRepeatReq from './preventRepeatReq';
|
||||
import cacheControl from './cacheControl';
|
||||
|
||||
function addInterceptors(instance, interceptors, type = 'request') {
|
||||
interceptors.forEach((fn) => {
|
||||
if (Array.isArray(fn)) {
|
||||
instance.interceptors[type].use(...fn);
|
||||
} else if (isFunction(fn)) {
|
||||
instance.interceptors[type].use(fn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addRequestInterceptors(instance, interceptors) {
|
||||
addInterceptors(instance, interceptors, 'request');
|
||||
}
|
||||
|
||||
function addResponseInterceptors(instance, interceptors) {
|
||||
addInterceptors(instance, interceptors, 'response');
|
||||
}
|
||||
|
||||
async function axiosMiddleware(context, next) {
|
||||
try {
|
||||
context.response = await context.instance.request(context.config);
|
||||
} catch (error) {
|
||||
context.error = error;
|
||||
}
|
||||
await next();
|
||||
}
|
||||
import { createRequest } from '@qlin/request';
|
||||
|
||||
function getRequestInstance() {
|
||||
const {
|
||||
dataHandler,
|
||||
errorHandler,
|
||||
requestInterceptors = [],
|
||||
responseInterceptors = [],
|
||||
...otherConfigs
|
||||
} = plugin.applyPlugins({
|
||||
const defaultConfig = plugin.applyPlugins({
|
||||
key: 'request',
|
||||
type: ApplyPluginsType.modify,
|
||||
initialValue: {},
|
||||
initialValue: {
|
||||
timeout: 10000,
|
||||
},
|
||||
});
|
||||
|
||||
const defaultConfig = Object.assign(
|
||||
{
|
||||
timeout: 10000,
|
||||
withCredentials: true,
|
||||
},
|
||||
otherConfigs,
|
||||
);
|
||||
const instance = axios.create(defaultConfig);
|
||||
|
||||
addRequestInterceptors(instance, requestInterceptors);
|
||||
addResponseInterceptors(instance, responseInterceptors);
|
||||
|
||||
// 洋葱模型内部应该这是对数据的处理,避免有副作用调用
|
||||
scheduler.use(paramsProcess).use(genRequestKey).use(cacheControl).use(preventRepeatReq).use(axiosMiddleware);
|
||||
|
||||
return {
|
||||
context: {
|
||||
errorHandler,
|
||||
dataHandler: dataHandler || ((data) => data),
|
||||
instance,
|
||||
defaultConfig,
|
||||
},
|
||||
request: scheduler.compose(),
|
||||
};
|
||||
return createRequest(defaultConfig);
|
||||
}
|
||||
|
||||
function userConfigHandler(url, data, options = {}) {
|
||||
options.url = url;
|
||||
options.method = (options.method || 'post').toUpperCase();
|
||||
if (checkHttpRequestHasBody(options.method)) {
|
||||
options.data = data;
|
||||
} else {
|
||||
options.params = data;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
const currentRequest = getRequestInstance();
|
||||
|
||||
let currentRequestInstance = null;
|
||||
|
||||
function createContext(userConfig) {
|
||||
return {
|
||||
...currentRequestInstance.context,
|
||||
config: {
|
||||
...currentRequestInstance.context.defaultConfig,
|
||||
...userConfig,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getCustomerHandler(ctx, options = {}) {
|
||||
const { dataHandler, errorHandler } = ctx;
|
||||
return {
|
||||
dataHandler: options.dataHandler || dataHandler,
|
||||
errorHandler: options.errorHandler || errorHandler,
|
||||
};
|
||||
}
|
||||
|
||||
export const request = (url, data, options = {}) => {
|
||||
export const rawRequest = (url, data, options = {}) => {
|
||||
if (typeof options === 'string') {
|
||||
options = {
|
||||
method: options,
|
||||
};
|
||||
}
|
||||
if (!currentRequestInstance) {
|
||||
currentRequestInstance = getRequestInstance();
|
||||
}
|
||||
const userConfig = userConfigHandler(url, data, options);
|
||||
const context = createContext(userConfig);
|
||||
const { dataHandler, errorHandler } = getCustomerHandler(context, options);
|
||||
return currentRequest(url, data, options);
|
||||
};
|
||||
|
||||
return currentRequestInstance.request(context).then(async () => {
|
||||
if (!context.error) {
|
||||
return dataHandler(context.response.data, context.response);
|
||||
}
|
||||
errorHandler && errorHandler(context.error);
|
||||
return Promise.reject(context.error);
|
||||
});
|
||||
export const request = async (url, data, options = {}) => {
|
||||
const response = await rawRequest(url, data, options);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
function isPromiseLike(obj) {
|
||||
|
@ -1,32 +0,0 @@
|
||||
class Scheduler {
|
||||
constructor() {
|
||||
this.middlewares = [];
|
||||
}
|
||||
|
||||
use(fn) {
|
||||
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
|
||||
this.middlewares.push(fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
compose() {
|
||||
return (context, next) => {
|
||||
let index = -1;
|
||||
const dispatch = (i) => {
|
||||
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
|
||||
index = i;
|
||||
let fn = this.middlewares[i];
|
||||
if (index === this.middlewares.length) fn = next;
|
||||
if (!fn) return Promise.resolve();
|
||||
try {
|
||||
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
};
|
||||
return dispatch(0);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new Scheduler();
|
35
packages/fes-plugin-request/types.d.ts
vendored
35
packages/fes-plugin-request/types.d.ts
vendored
@ -1,39 +1,18 @@
|
||||
import { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
|
||||
import type { Config, ParamsType } from '@qlin/request/dist/interface';
|
||||
import { Ref } from 'vue';
|
||||
|
||||
type RequestInterceptor = (value: AxiosRequestConfig) => AxiosRequestConfig | [(value: AxiosRequestConfig) => AxiosRequestConfig, (error: any) => any];
|
||||
type ResponseInterceptor = (value: AxiosResponse) => AxiosResponse | [(value: AxiosResponse) => AxiosResponse, (error: any) => any];
|
||||
export * from '@qlin/request';
|
||||
|
||||
export type RequestResponse = AxiosResponse;
|
||||
export type RequestError = AxiosError & { type: string; msg: string; [key: string]: any };
|
||||
interface RequestPluginOption {
|
||||
mergeRequest?: boolean;
|
||||
dataHandler?(data: any, response: AxiosResponse): any;
|
||||
errorHandler?(error: RequestError): void;
|
||||
cache?:
|
||||
| boolean
|
||||
| {
|
||||
type?: 'ram' | 'sessionStorage' | 'localStorage';
|
||||
cacheTime?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type RequestOptions = AxiosRequestConfig & RequestPluginOption;
|
||||
|
||||
export function request(url: string, data?: null | Record<string, any>, options?: AxiosRequestConfig & RequestPluginOption): Promise<any>;
|
||||
export function request(url: string, data?: ParamsType | null, options?: Partial<Config>): Promise<any>;
|
||||
export function rawRequest(url: string, data?: ParamsType | null, options?: Partial<Config>): Promise<any>;
|
||||
export function useRequest(
|
||||
url: string,
|
||||
data?: null | Record<string, any>,
|
||||
options?: AxiosRequestConfig & RequestPluginOption,
|
||||
data?: null | ParamsType,
|
||||
options?: Partial<Config>,
|
||||
): { loadingRef: Ref<boolean>; errorRef: Ref<Error>; dataRef: Ref<any> };
|
||||
|
||||
declare module '@fesjs/fes' {
|
||||
interface PluginRuntimeConfig {
|
||||
request?: {
|
||||
dataHandler?(data: any, response: AxiosResponse): any;
|
||||
errorHandler?(error: RequestError): void;
|
||||
requestInterceptors?: RequestInterceptor[];
|
||||
responseInterceptors?: ResponseInterceptor[];
|
||||
} & AxiosRequestConfig;
|
||||
request?: Partial<Config>;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ export default (api) => {
|
||||
config: {
|
||||
default: '/',
|
||||
schema(joi) {
|
||||
return joi.string().regex(/\/$/).error(new Error('config.publicPath must end with /.'));
|
||||
return joi.string();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -43,7 +43,7 @@
|
||||
"@ttou/postcss-px-to-viewport": "1.1.4",
|
||||
"@fesjs/fes": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-icon": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-request": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-request": "^4.0.0-rc.0",
|
||||
"@fesjs/builder-vite": "^3.0.0-rc.1",
|
||||
"core-js": "^3.27.0",
|
||||
"vue": "^3.2.37"
|
||||
|
@ -56,7 +56,7 @@
|
||||
"@fesjs/plugin-model": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-monaco-editor": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-pinia": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-request": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-request": "^4.0.0-rc.0",
|
||||
"@fesjs/plugin-sass": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-windicss": "^3.0.0-rc.0",
|
||||
"core-js": "^3.27.0",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"@fesjs/plugin-monaco-editor": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-pinia": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-qiankun": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-request": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-request": "^4.0.0-rc.0",
|
||||
"@fesjs/plugin-sass": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-watermark": "^3.0.0-rc.0",
|
||||
"@fesjs/plugin-windicss": "^3.0.0-rc.0",
|
||||
|
48
yarn.lock
48
yarn.lock
@ -2103,6 +2103,14 @@
|
||||
resolved "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
|
||||
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
|
||||
|
||||
"@qlin/request@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmmirror.com/@qlin/request/-/request-0.1.1.tgz#265026e2741208640522161eb9621077aae05487"
|
||||
integrity sha512-3bQ3XaD4I4/EYZC15CR1TIHFCWQtZfYz0oleXlf4RS1xIW+7zna3OT5c0VKPiDn/aK1wZAH0mYODChIaRKSciA==
|
||||
dependencies:
|
||||
"@types/lodash-es" "^4.17.6"
|
||||
lodash-es "^4.17.21"
|
||||
|
||||
"@rollup/plugin-babel@^5.2.0":
|
||||
version "5.3.1"
|
||||
resolved "https://registry.npmmirror.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
|
||||
@ -2571,6 +2579,13 @@
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash-es@^4.17.6":
|
||||
version "4.17.7"
|
||||
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.7.tgz#22edcae9f44aff08546e71db8925f05b33c7cc40"
|
||||
integrity sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*":
|
||||
version "4.14.191"
|
||||
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
|
||||
@ -3696,15 +3711,6 @@ aws4@^1.8.0:
|
||||
resolved "https://registry.npmmirror.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
|
||||
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
|
||||
|
||||
axios@^1.0.0-alpha.1:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024"
|
||||
integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.0"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
babel-jest@^27.0.6, babel-jest@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.npmmirror.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444"
|
||||
@ -6156,7 +6162,7 @@ flatted@^3.1.0:
|
||||
resolved "https://registry.npmmirror.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
|
||||
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.15.0:
|
||||
follow-redirects@^1.0.0:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
||||
@ -6182,15 +6188,6 @@ form-data@^3.0.0:
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmmirror.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||
@ -9588,11 +9585,6 @@ proxy-addr@~2.0.7:
|
||||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
prr@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
|
||||
@ -11609,10 +11601,10 @@ webpack-virtual-modules@^0.5.0:
|
||||
resolved "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c"
|
||||
integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==
|
||||
|
||||
webpack@^5.69.0:
|
||||
version "5.75.0"
|
||||
resolved "https://registry.npmmirror.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152"
|
||||
integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==
|
||||
webpack@^5.76.2:
|
||||
version "5.76.2"
|
||||
resolved "https://registry.npmmirror.com/webpack/-/webpack-5.76.2.tgz#6f80d1c1d1e3bf704db571b2504a0461fac80230"
|
||||
integrity sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.3"
|
||||
"@types/estree" "^0.0.51"
|
||||
|
Loading…
x
Reference in New Issue
Block a user