diff --git a/packages/fes-core/src/index.js b/packages/fes-core/src/index.js index 0301811a..250d25cd 100644 --- a/packages/fes-core/src/index.js +++ b/packages/fes-core/src/index.js @@ -7,8 +7,6 @@ import PluginAPI from './service/pluginAPI'; import { PluginType } from './service/enums'; import { isPlugin } from './service/utils/pluginUtils'; -export * from './route'; - export { Config, Service, diff --git a/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes/index.js b/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes/index.js index 96145815..3aff2e95 100644 --- a/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes/index.js +++ b/packages/fes-plugin-built-in/src/plugins/generateFiles/core/routes/index.js @@ -1,9 +1,5 @@ - import { readFileSync } from 'fs'; -import { - join -} from 'path'; -import { routesToJSON } from '@webank/fes-core'; +import { join } from 'path'; import { runtimePath } from '../../../../utils/constants'; export default function (api) { @@ -15,12 +11,12 @@ export default function (api) { api.onGenerateFiles(async () => { const routesTpl = readFileSync(join(__dirname, 'routes.tpl'), 'utf-8'); - const routes = await api.getRoutes(); + const routes = await api.getRoutesJSON(); api.writeTmpFile({ path: absoluteFilePath, content: Mustache.render(routesTpl, { runtimePath, - routes: routesToJSON({ routes, config: api.config }), + routes, config: api.config }) }); diff --git a/packages/fes-plugin-built-in/src/plugins/misc/routes.js b/packages/fes-plugin-built-in/src/plugins/misc/routes.js index 523cd8f9..00268450 100644 --- a/packages/fes-plugin-built-in/src/plugins/misc/routes.js +++ b/packages/fes-plugin-built-in/src/plugins/misc/routes.js @@ -1,4 +1,208 @@ -import { getRoutes } from '@webank/fes-core'; +import { readdirSync, statSync } from 'fs'; +import { + join, extname, posix, basename +} from 'path'; +import { lodash } from '@umijs/utils'; + +// pages +// ├── index.vue # 根路由页面 路径 / +// ├── *.vue # 模糊匹配 路径 * +// ├── a.vue # 路径 /a +// ├── b +// │ ├── index.vue # 路径 /b +// │ ├── @id.vue # 动态路由 /b/:id +// │ └── c.vue # 路径 /b/c +// └── layout.vue # 根路由下所有page共用的外层 + +const isProcessFile = function (path) { + const ext = extname(path); + return statSync(path).isFile() && ['.vue'].includes(ext); +}; + +const isProcessDirectory = function (path, item) { + const component = join(path, item); + return statSync(component).isDirectory() && !['components'].includes(item); +}; + +const checkHasLayout = function (path) { + const dirList = readdirSync(path); + return dirList.some((item) => { + if (!isProcessFile(join(path, item))) { + return false; + } + const ext = extname(item); + const fileName = basename(item, ext); + return fileName === 'layout'; + }); +}; + +const getRouteName = function (parentRoutePath, fileName) { + const routeName = posix.join(parentRoutePath, fileName); + return routeName + .slice(1) + .replace(/\//g, '_') + .replace(/@/g, '_') + .replace(/\*/g, 'FUZZYMATCH'); +}; + +const getComponentPath = function (parentRoutePath, fileName) { + return posix.join('@/pages/', parentRoutePath, fileName); +}; + +const getRoutePath = function (parentRoutePath, fileName) { + // /index.vue -> / + if (fileName === 'index') { + fileName = ''; + } + let routePath = posix.join(parentRoutePath, fileName); + // /@id.vue -> /:id + routePath = routePath.replace(/@/g, ':'); + // /*.vue -> * + if (routePath === '/*') { + routePath = '*'; + } + return routePath; +}; + +// TODO 约定 layout 目录作为布局文件夹, +const genRoutes = function (parentRoutes, path, parentRoutePath) { + const dirList = readdirSync(path); + const hasLayout = checkHasLayout(path); + const layoutRoute = { + children: [] + }; + if (hasLayout) { + layoutRoute.path = parentRoutePath; + parentRoutes.push(layoutRoute); + } + dirList.forEach((item) => { + // 文件或者目录的绝对路径 + const component = join(path, item); + if (isProcessFile(component)) { + const ext = extname(item); + const fileName = basename(item, ext); + // 路由的path + const routePath = getRoutePath(parentRoutePath, fileName); + // 路由名称 + const routeName = getRouteName(parentRoutePath, fileName); + const componentPath = getComponentPath(parentRoutePath, fileName); + if (hasLayout) { + if (fileName === 'layout') { + layoutRoute.component = componentPath; + } else { + layoutRoute.children.push({ + path: routePath, + component: componentPath, + name: routeName + }); + } + } else { + parentRoutes.push({ + path: routePath, + component: componentPath, + name: routeName + }); + } + } + }); + + dirList.forEach((item) => { + if (isProcessDirectory(path, item)) { + // 文件或者目录的绝对路径 + const component = join(path, item); + const nextParentRouteUrl = posix.join(parentRoutePath, item); + if (hasLayout) { + genRoutes(layoutRoute.children, component, nextParentRouteUrl); + } else { + genRoutes(parentRoutes, component, nextParentRouteUrl); + } + } + }); +}; + +/** + * 智能路由 + * 1、路由的路径是多个“/”组成的字符串,使用“/”分割后得到不同的子项 + * 2、计算子项个数,用个数乘以4,计入得分 + * 3、判断子项是否是静态的,即不包含“:”、“*”等特殊字符串,若是计入3分。 + * 4、判断子项是否是动态的,即包含“:”特殊字符,若是计入2分。 + * 5、判断子项是否是模糊匹配,即包含“*”特殊字符,若是扣除1分。 + * 6、判断子项是否是根端,即只是“/”,若是计入1分。 + + * @param {*} routes + */ +const fix = function (routes) { + routes.forEach((item) => { + const path = item.path; + let arr = path.split('/'); + if (arr[0] === '') { + arr = arr.slice(1); + } + let count = 0; + arr.forEach((sonPath) => { + count += 4; + if (sonPath.indexOf(':') !== -1) { + count += 2; + } else if (sonPath.indexOf('*') !== -1) { + count -= 1; + } else if (sonPath === '') { + count += 1; + } else { + count += 3; + } + }); + item.count = count; + if (item.children && item.children.length) { + fix(item.children); + } + }); + routes = routes.sort((a, b) => b.count - a.count); +}; + +const getRoutes = function ({ config, absPagesPath }) { + // 用户配置了routes则使用用户配置的 + if (config.routes) return config.routes; + + const routes = []; + genRoutes(routes, absPagesPath, '/'); + fix(routes); + return routes; +}; + +const getRoutesJSON = function ({ routes, config }) { + // 因为要往 routes 里加无用的信息,所以必须 deep clone 一下,避免污染 + const clonedRoutes = lodash.cloneDeep(routes); + + function isFunctionComponent(component) { + // eslint-disable-next-line + return ( + /^\((.+)?\)(\s+)?=>/.test(component) + || /^function([^\(]+)?\(([^\)]+)?\)([^{]+)?{/.test(component) + ); + } + + function replacer(key, value) { + switch (key) { + case 'component': + if (isFunctionComponent(value)) return value; + if (config.dynamicImport) { + // TODO 针对目录进行 chunk 划分,import(/* webpackChunkName: "group-user" */ './UserDetails.vue') + return `() => import('${value}')`; + } + return `require('${value}').default`; + default: + return value; + } + } + + return JSON.stringify(clonedRoutes, replacer, 2) + .replace( + /"component": ("(.+?)")/g, + (global, m1, m2) => `"component": ${m2.replace(/\^/g, '"')}` + ) + .replace(/\\r\\n/g, '\r\n') + .replace(/\\n/g, '\r\n'); +}; export default function (api) { api.describe({ @@ -18,4 +222,12 @@ export default function (api) { }); } }); + + api.registerMethod({ + name: 'getRoutesJSON', + async fn() { + const routes = await api.getRoutes(); + return getRoutesJSON({ routes, config: api.config }); + } + }); }