feat: 添加request plugin

This commit is contained in:
bac-joker 2020-11-22 15:14:31 +08:00
parent 28498a30bc
commit 67903dab8b
13 changed files with 284 additions and 162 deletions

View File

@ -3,7 +3,7 @@ import { join } from 'path';
// utils must build before core
// runtime must build before renderer-react
const headPkgs = ['fes-runtime', 'fes-core', 'fes', 'fes-plugin-built-in'];
const headPkgs = ['fes-runtime', 'fes-core', 'fes', 'fes-plugin-built-in', 'fes-plugin-request'];
const tailPkgs = [];
// const otherPkgs = readdirSync(join(__dirname, 'packages')).filter(
// (pkg) =>

View File

@ -4,6 +4,7 @@ import {
join
} from 'path';
import { routesToJSON } from '@webank/fes-core';
import { runtimePath } from '../constants';
export default function (api) {
const {
@ -16,9 +17,15 @@ export default function (api) {
api.writeTmpFile({
path: 'core/routes.js',
content: Mustache.render(routesTpl, {
runtimePath,
routes: routesToJSON({ routes, config: api.config }),
config: api.config
})
});
});
api.addFesExports(() => ({
specifiers: ['router'],
source: './routes.js'
}));
}

View File

@ -1,7 +1,22 @@
import { createRouter, createWebHashHistory } from '{{{ runtimePath }}}';
export function getRoutes() {
const routes = {{{ routes }}};
// TODO 支持动态变更路由
return routes;
}
}
let router = null;
export const createHistory = () => {
if (router) {
return router;
}
router = createRouter({
history: createWebHashHistory(),
routes: getRoutes()
});
return router;
};
export { router };

View File

@ -6,8 +6,8 @@ import {
} from 'vue';
import { plugin } from './core/plugin';
import './core/pluginRegister';
import { ApplyPluginsType, createRouter, createWebHashHistory } from '{{{ runtimePath }}}';
import { getRoutes } from './core/routes';
import { ApplyPluginsType } from '{{{ runtimePath }}}';
import { createRouter } from './core/routes';
{{{ imports }}}
{{{ entryCodeAhead }}}
@ -23,10 +23,7 @@ const renderClient = (opts = {}) => {
}
});
const router = createRouter({
history: createWebHashHistory(),
routes: getRoutes()
});
const router = createRouter();
const app = createApp(rootContainer);
app.use(router);

View File

@ -0,0 +1,6 @@
export default {
target: 'browser',
cjs: { type: 'rollup', lazy: false },
esm: { type: 'rollup' },
disableTypeCheck: false,
};

View File

@ -6,10 +6,14 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"module": "dist/index.esm.js",
"keywords": [],
"author": "",
"type": "module",
"license": "MIT",
"peerDependencies": {
"@webank/fes": "^2.0.0"
},
"dependencies": {
"axios": "^0.20.0",
"throttle-debounce": "^2.3.0"

View File

@ -1,29 +1,64 @@
import { debounce } from 'throttle-debounce';
import initAxiosInstance from './request';
import { readFileSync, copyFileSync, statSync } from 'fs';
import { join } from 'path';
let request;
function _advanceRequest({ url, debounce: waitTime, options = {} }) {
return debounce((data, specialCaseOptions) => {
request(url, data, Object.assign(options, specialCaseOptions));
}, true, waitTime || 0);
}
export const requestWrap = (interfaces) => {
const obj = {};
Object.entries(interfaces).forEach(([key, value]) => {
if (value.url) {
obj[key] = _advanceRequest(value);
} else {
obj[key] = requestWrap(value);
export default (api) => {
api.addRuntimePluginKey(() => 'request');
// 配置
api.describe({
config: {
schema(joi) {
return joi.object({
dataField: joi
.string()
.pattern(/^[a-zA-Z]*$/)
.allow('')
});
},
default: {
dataField: 'result',
messageUI: 'ant-design-vue'
}
}
});
return obj;
};
export const createRequest = () => ({
install(app, options, ctx) {
request = initAxiosInstance(options, { router: ctx.router });
ctx.request = request;
}
});
const namespace = 'plugin-request';
const requestTemplate = readFileSync(join(__dirname, 'template', 'request.ts'), 'utf-8');
api.onGenerateFiles(() => {
// 文件写出
const { dataField = '', messageUI } = api.config.request;
api.writeTmpFile({
path: `${namespace}/request.js`,
content: requestTemplate
.replace('REPLACE_MESSAGE_UI', messageUI || 'ant-design-vue')
.replace('REPLACE_DATA_FIELD', dataField)
});
});
let generatedOnce = false;
api.onGenerateFiles(() => {
if (generatedOnce) return;
generatedOnce = true;
const cwd = join(__dirname, './template');
const files = api.utils.glob.sync('**/*', {
cwd
});
const base = join(api.paths.absTmpPath, namespace);
files.forEach((file) => {
if (['request.js'].includes(file)) return;
const source = join(cwd, file);
const target = join(base, file);
if (statSync(source).isDirectory()) {
api.utils.mkdirp.sync(target);
} else {
copyFileSync(source, target);
}
});
});
api.addFesExports(() => [
{
exportAll: true,
source: `../${namespace}/request.js`
}
]);
};

View File

@ -1,19 +0,0 @@
import { checkHttpRequestHasBody, trimObj } from './helpers';
export default function reqInterceptors(instance) {
// 将 http method 转换为大写
instance.interceptors.request.use((config) => {
config.method = config.method.toUpperCase();
return config;
});
// 清理请求值中的空格
instance.interceptors.request.use((config) => {
if (checkHttpRequestHasBody(config.method)) {
config.data = trimObj(config.data);
} else {
config.params = trimObj(config.params);
}
return config;
});
}

View File

@ -1,91 +0,0 @@
import axios from 'axios';
import reqInterceptors from './reqInterceptors';
import resInterceptors from './resInterceptors';
import { checkHttpRequestHasBody, isObject } from './helpers';
// TODO
// 响应体控制
// formData 控制
// 段时间内不能重复发送的请求
// 错误控制
// 跳错误页面 || 或者重新登录
let instance;
export function requestUse(before, error) {
return this.instance.interceptors.request.use(before, error);
}
export function requestEject(interceptor) {
this.instance.interceptors.request.eject(interceptor);
}
export function responseUse(after, error) {
return instance.interceptors.response.use(after, error);
}
export function responseEject(interceptor) {
instance.interceptors.response.eject(interceptor);
}
export function getRequestInstance() {
return instance;
}
function _failedHandler(error, customerErrorHandler) {
if (error.response) {
const status = error.response.status;
if (typeof customerErrorHandler[status] === 'function') {
customerErrorHandler(error);
}
} else if (error.request) {
// TODO 请求异常
} else {
console.error(error);
}
return Promise.reject(error);
}
function _successedHandler(response, responseDataStruct) {
const responseData = response.data;
if (responseDataStruct && isObject(responseData)) {
// TODO 响应体解构解析
return responseData;
}
return responseData;
}
function initAxiosInstance({ options: internalOptions, responseDataStruct, errorHandler }) {
const customerErrorHandler = errorHandler || {};
instance = axios.create(Object.assign({
timeout: 10000,
withCredentials: true
}, internalOptions));
// 设置请求拦截器
reqInterceptors(instance);
// 设置响应拦截器
resInterceptors(instance);
return (url, data, options = {}) => {
options.url = url;
options.method = options.method || 'post';
if (checkHttpRequestHasBody(options.method)) {
options.data = data;
} else {
options.params = data;
}
// 请求内容可能是一个json
// 一个 query
// formdata
// 响应内容可能是一个文件流
// 一个文本
// 一个 json
// eslint-disable-next-line
return this.instance.request(options).then(response => _successedHandler(response, responseDataStruct)).catch(error => _failedHandler(error, customerErrorHandler));
};
}
export default initAxiosInstance;

View File

@ -1,4 +0,0 @@
export default function resInterceptors() {
}

View File

@ -76,13 +76,15 @@ export function checkHttpRequestHasBody(method) {
}
export function trimObj(obj) {
Object.entries(obj).forEach(([key, value]) => {
if (isString(value)) {
obj[key] = value.trim();
} else if (isObject(value)) {
trimObj(value);
} else if (Array.isArray(value)) {
trimObj(value);
}
});
if (isObject(obj)) {
Object.entries(obj).forEach(([key, value]) => {
if (isString(value)) {
obj[key] = value.trim();
} else if (isObject(value)) {
trimObj(value);
} else if (Array.isArray(value)) {
trimObj(value);
}
});
}
}

View File

@ -0,0 +1,174 @@
import axios from 'axios';
// import { debounce } from 'throttle-debounce';
import { message } from 'REPLACE_MESSAGE_UI';
import { ApplyPluginsType, plugin, router } from '@webank/fes';
import {
checkHttpRequestHasBody,
trimObj,
isString,
isFunction,
isObject
} from './helpers';
/**
* 统一错误处理
* @param {object | string function} errorStruct
* {
* errorMessage: '', // 错误地址
* errorPage: '', // 错误页面地址
* showType: 1 // 0 不提示 | 1 警告 | 2 错误 | 9 页面跳转
* }
*/
function errrorHandler(error) {
if (isFunction(error)) {
error();
} else if (isString(error)) {
message.error(error);
} else if (isObject(error)) {
switch (error.showType) {
case 1:
message.warning(error.errorMessage);
break;
case 2:
message.error(error.errorMessage);
break;
case 9:
router.replace(error.errorPage);
break;
default:
throw new Error(error);
}
}
}
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');
}
function getRequestInstance() {
const {
responseDataAdaptor,
errorConfig,
requestInterceptors,
responseInterceptors,
...otherConfigs
} = plugin.applyPlugins({
key: 'request',
type: ApplyPluginsType.modify,
initialValue: {}
});
const _errorConfig = Object.assign({
401: {
showType: 9,
errorPage: '/login'
},
403: '用户得到授权,但访问是禁止的'
}, errorConfig);
const _requestInterceptors = [].concat([
(config) => {
config.method = config.method.toUpperCase();
return config;
},
(config) => {
if (checkHttpRequestHasBody(config.method)) {
config.data = trimObj(config.data);
} else {
config.params = trimObj(config.params);
}
return config;
}
], requestInterceptors);
const _responseInterceptors = [].concat([
[
function (response) {
if (isObject(response.data) && response.data.code !== '0') {
errrorHandler(_errorConfig[response.data.code] || response.data.msg || response.data.errorMessage || response.data.errorMsg || '服务异常');
return Promise.reject(response);
}
return response;
}, function (error) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
if (_errorConfig[error.response.status]) {
errrorHandler(_errorConfig[error.response.status]);
}
}
return Promise.reject(error);
}
]
], responseInterceptors);
if (responseDataAdaptor && isFunction(responseDataAdaptor)) {
_responseInterceptors.unshift((response) => {
// 响应内容可能是个文件流 or 普通文本
if (isObject(response.data)) {
response.data = responseDataAdaptor(response.data);
}
return response;
});
}
// 只把响应数据暴露出去
_responseInterceptors.push((response) => {
// eslint-disable-next-line
const dataField = REPLACE_DATA_FIELD;
if (isObject(response.data) && dataField) {
return response.data[dataField];
}
return response;
});
const instance = axios.create(Object.assign({
timeout: 10000,
withCredentials: true
}, otherConfigs));
addRequestInterceptors(instance, _requestInterceptors);
addResponseInterceptors(instance, _responseInterceptors);
return {
instance
};
}
// TODO 待实现能力
// formData 控制
// 轮询
// 并行请求 >> 通过定义 key 区分
// 防抖 & 节流
// 缓存 & SWR & 预加载
// loadingDelay
let currentRequestInstance = null;
export const request = (url, data, options = {}) => {
if (!currentRequestInstance) {
const { instance } = getRequestInstance();
currentRequestInstance = instance;
}
options.url = url;
options.method = options.method || 'post';
if (checkHttpRequestHasBody(options.method)) {
options.data = data;
} else {
options.params = data;
}
return currentRequestInstance.request(options);
};

View File

@ -1,4 +1,4 @@
import { requestWrap } from '@webank/fes-plugin-request';
import { requestWrap } from '@webank/fes';
// TODO
// 响应体控制
@ -7,10 +7,6 @@ import { requestWrap } from '@webank/fes-plugin-request';
// 跳错误页面 || 或者重新登录
// 段时间内不能重复发送的请求
// request(url, data, option).then(() => {
// });
// or
export default requestWrap({
login: {