diff --git a/.fatherrc.js b/.fatherrc.js index 025525e9..f16ce608 100644 --- a/.fatherrc.js +++ b/.fatherrc.js @@ -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) => diff --git a/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes.js b/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes.js index 6e01e5cb..8227ec33 100644 --- a/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes.js +++ b/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes.js @@ -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' + })); } diff --git a/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes.tpl b/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes.tpl index b0ff866e..1203a7d2 100644 --- a/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes.tpl +++ b/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes.tpl @@ -1,7 +1,22 @@ - +import { createRouter, createWebHashHistory } from '{{{ runtimePath }}}'; export function getRoutes() { const routes = {{{ routes }}}; // TODO 支持动态变更路由 return routes; -} \ No newline at end of file +} + +let router = null; +export const createHistory = () => { + if (router) { + return router; + } + router = createRouter({ + history: createWebHashHistory(), + routes: getRoutes() + }); + + return router; +}; + +export { router }; diff --git a/packages/fes-plugin-built-in/src/plugins/generateFiles/fes.tpl b/packages/fes-plugin-built-in/src/plugins/generateFiles/fes.tpl index 587d992b..8c2a99d6 100644 --- a/packages/fes-plugin-built-in/src/plugins/generateFiles/fes.tpl +++ b/packages/fes-plugin-built-in/src/plugins/generateFiles/fes.tpl @@ -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); diff --git a/packages/fes-plugin-request/.fatherrc.js b/packages/fes-plugin-request/.fatherrc.js new file mode 100644 index 00000000..a199d7c0 --- /dev/null +++ b/packages/fes-plugin-request/.fatherrc.js @@ -0,0 +1,6 @@ +export default { + target: 'browser', + cjs: { type: 'rollup', lazy: false }, + esm: { type: 'rollup' }, + disableTypeCheck: false, +}; diff --git a/packages/fes-plugin-request/package.json b/packages/fes-plugin-request/package.json index a1d7db99..96ff096d 100644 --- a/packages/fes-plugin-request/package.json +++ b/packages/fes-plugin-request/package.json @@ -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" diff --git a/packages/fes-plugin-request/src/index.js b/packages/fes-plugin-request/src/index.js index 1b7f6bb3..f51719c2 100644 --- a/packages/fes-plugin-request/src/index.js +++ b/packages/fes-plugin-request/src/index.js @@ -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` + } + ]); +}; diff --git a/packages/fes-plugin-request/src/reqInterceptors.js b/packages/fes-plugin-request/src/reqInterceptors.js deleted file mode 100644 index d9a23805..00000000 --- a/packages/fes-plugin-request/src/reqInterceptors.js +++ /dev/null @@ -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; - }); -} diff --git a/packages/fes-plugin-request/src/request.js b/packages/fes-plugin-request/src/request.js deleted file mode 100644 index 9d8032fe..00000000 --- a/packages/fes-plugin-request/src/request.js +++ /dev/null @@ -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; diff --git a/packages/fes-plugin-request/src/resInterceptors.js b/packages/fes-plugin-request/src/resInterceptors.js deleted file mode 100644 index b1264347..00000000 --- a/packages/fes-plugin-request/src/resInterceptors.js +++ /dev/null @@ -1,4 +0,0 @@ - -export default function resInterceptors() { - -} diff --git a/packages/fes-plugin-request/src/helpers.js b/packages/fes-plugin-request/src/template/helpers.js similarity index 83% rename from packages/fes-plugin-request/src/helpers.js rename to packages/fes-plugin-request/src/template/helpers.js index 3f0930f4..e28d7de7 100644 --- a/packages/fes-plugin-request/src/helpers.js +++ b/packages/fes-plugin-request/src/template/helpers.js @@ -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); + } + }); + } } diff --git a/packages/fes-plugin-request/src/template/request.js b/packages/fes-plugin-request/src/template/request.js new file mode 100644 index 00000000..6785e258 --- /dev/null +++ b/packages/fes-plugin-request/src/template/request.js @@ -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); +}; diff --git a/packages/fes-template/src/common/service.js b/packages/fes-template/src/common/service.js index ec2ca70c..06447dc2 100644 --- a/packages/fes-template/src/common/service.js +++ b/packages/fes-template/src/common/service.js @@ -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: {