feat(fes-cli): 路由支持模糊匹配和智能路由匹配

1. 路由支持模糊匹配,例如"pages/*.vue"解析成"*",通过模糊匹配可以实现404 2.根据精准匹配优先原则实现智能路由 3.
pages下的名称为componets的文件夹被忽略,其中的vue文件不会被解析成路由

re #32
This commit is contained in:
harrywan 2020-10-22 10:09:09 +08:00
parent 357575247b
commit f6c3f5fd88
8 changed files with 291 additions and 123 deletions

View File

@ -1,121 +1,164 @@
// pages
// ├── index.fes # 根路由页面 路径 index.html#/
// ├── a.fes # 路径 /a
// ├── index.vue # 根路由页面 路径 /
// ├── *.vue # 模糊匹配 路径 *
// ├── a.vue # 路径 /a
// ├── b
// │ ├── index.fes # 路径 /b
// │ ├── @id.fes # 动态路由 /b/:id
// │ └── c.fes # 路径 /b/c
// └── layout.fes # 根路由下所有page共用的外层
// │ ├── index.vue # 路径 /b
// │ ├── @id.vue # 动态路由 /b/:id
// │ └── c.vue # 路径 /b/c
// └── layout.vue # 根路由下所有page共用的外层
const fs = require('fs');
const Path = require('path');
let pagesDir;
let outputPageDir;
let components = [];
function checkHasLayout(path) {
const isProcessFile = function (path) {
const ext = Path.extname(path);
return fs.statSync(path).isFile() && ['.fes', '.vue'].includes(ext);
};
const isProcessDirectory = function (path, item) {
const component = Path.posix.join(path, item);
return fs.statSync(component).isDirectory() && !['components'].includes(item);
};
const checkHasLayout = function (path) {
const dirList = fs.readdirSync(path);
let hasLayout = false;
dirList.forEach((item) => {
if (fs.statSync(`${path}/${item}`).isFile()
&& item[0] !== '.' && ['.fes', '.vue'].indexOf(Path.extname(item)) !== -1
&& Path.basename(item, Path.extname(item)) === 'layout') {
hasLayout = true;
return dirList.some((item) => {
if (!isProcessFile(Path.posix.join(path, item))) {
return false;
}
const ext = Path.extname(item);
const fileName = Path.basename(item, ext);
return fileName === 'layout';
});
return hasLayout;
}
};
function routeUrlFormmter(str) {
return str.replace(/@/g, ':');
}
const getRouteName = function (parentRoutePath, fileName) {
const routeName = Path.posix.join(parentRoutePath, fileName);
return routeName.slice(1).replace(/\//g, '_').replace(/@/g, '_').replace(/\*/g, 'FUZZYMATCH');
};
function genRoute(path, prePathUrl, preRoutes) {
const hasLayout = checkHasLayout(path);
const getRoutePath = function (parentRoutePath, fileName) {
// /index.vue -> /
if (fileName === 'index') {
fileName = '';
}
let routePath = Path.posix.join(parentRoutePath, fileName);
// /@id.vue -> /:id
routePath = routePath.replace(/@/g, ':');
// /*.vue -> *
if (routePath === '/*') {
routePath = '*';
}
return routePath;
};
const build = function (components, parentRoutes, path, parentRoutePath) {
const dirList = fs.readdirSync(path);
let childRoutes = {};
const parentRoutes = {};
const preRouteUrl = routeUrlFormmter(prePathUrl);
const hasLayout = checkHasLayout(path);
const layoutRoute = {
children: []
};
if (hasLayout) {
parentRoutes[preRouteUrl] = {
subRoutes: childRoutes
};
} else {
childRoutes = parentRoutes;
layoutRoute.path = parentRoutePath;
parentRoutes.push(layoutRoute);
}
dirList.forEach((item) => {
if (fs.statSync(`${path}/${item}`).isFile()
&& item[0] !== '.' && ['.fes', '.vue'].indexOf(Path.extname(item)) !== -1) {
const fileName = Path.basename(item, Path.extname(item));
const preRouteName = path.slice(pagesDir.length + 1);
let routePath = Path.posix.join(preRouteUrl, (fileName === 'index' ? '' : fileName.replace(/@/g, ':')));
const filePath = Path.resolve(path.replace(pagesDir, outputPageDir), item);
let routeName = preRouteName ? `${preRouteName}_${fileName}` : fileName;
routeName = routeName.replace(/\//g, '_').replace(/@/g, '');
routePath = routePath.replace(/@/g, ':');
if (hasLayout && fileName === 'index') {
routePath = '/';
} else if (hasLayout && fileName !== 'index') {
routePath = routePath.split(preRouteUrl)[1];
}
// 文件或者目录的绝对路径
const component = Path.posix.join(path, item);
if (isProcessFile(component)) {
const ext = Path.extname(item);
const fileName = Path.basename(item, ext);
// 路由的path
const routePath = getRoutePath(parentRoutePath, fileName);
// 路由名称
const routeName = getRouteName(parentRoutePath, fileName);
components.push({
name: routeName,
path: filePath.replace(/\\/g, '\\\\')
path: component
});
if (fileName === 'layout') {
parentRoutes[preRouteUrl].component = routeName;
return;
if (hasLayout) {
if (fileName === 'layout') {
layoutRoute.component = routeName;
} else {
layoutRoute.children.push({
path: routePath,
component: routeName,
name: routeName
});
}
} else {
parentRoutes.push({
path: routePath,
component: routeName,
name: routeName
});
}
childRoutes[routePath] = {
name: routeName || 'index',
component: routeName
};
}
});
preRoutes = Object.assign(preRoutes, parentRoutes);
const toNextRoutes = hasLayout ? childRoutes : preRoutes;
dirList.forEach((item) => {
if (fs.statSync(`${path}/${item}`).isDirectory()) {
let toNextPreRouteUrl = Path.posix.join(prePathUrl, item);
if (isProcessDirectory(path, item)) {
// 文件或者目录的绝对路径
const component = Path.posix.join(path, item);
const nextParentRouteUrl = Path.posix.join(parentRoutePath, item);
if (hasLayout) {
toNextPreRouteUrl = Path.posix.join('/', item);
build(components, layoutRoute.children, component, nextParentRouteUrl);
} else {
build(components, parentRoutes, component, nextParentRouteUrl);
}
genRoute(`${path}/${item}`, toNextPreRouteUrl, toNextRoutes, outputPageDir, components);
}
});
}
};
function fixRouter2(routes, newRoutes, f) {
Object.keys(routes).forEach((p) => {
const item = {
path: f ? p.slice(1) : p,
component: routes[p].component
};
if (routes[p].name) {
item.name = routes[p].name;
/**
* 智能路由
* 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('/');
// console.log(arr);
if (arr[0] === '') {
arr = arr.slice(1);
}
if (routes[p].subRoutes) {
item.children = [];
fixRouter2(routes[p].subRoutes, item.children, true);
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);
}
newRoutes.push(item);
});
}
routes = routes.sort((a, b) => b.count - a.count);
};
module.exports = function (_pagesDir, _outputPageDir) {
const routes = {};
const newRoutes = [];
components = [];
pagesDir = _pagesDir;
outputPageDir = _outputPageDir;
genRoute(pagesDir, '/', routes);
fixRouter2(routes, newRoutes);
module.exports = function (pageDir) {
const components = [];
const routes = [];
build(components, routes, pageDir, '/');
fix(routes);
return {
routes,
newRoutes,
components
components,
routes
};
};

View File

@ -14,22 +14,22 @@ function generateRoute(config) {
export default {{routes}};
`;
const routes = getRoute(config.folders.PROJECT_PAGE_DIR, config.folders.PROJECT_PAGE_DIR);
const { components, routes } = getRoute(config.folders.PROJECT_PAGE_DIR);
const componentsTemplate = [];
let template = '';
if (config.lazyRouter) {
const componentsObj = {};
routes.components.forEach((item) => {
components.forEach((item) => {
componentsObj[item.name] = item.path;
});
// component: () => import( /* webpackChunkName: "home" */ '../views/Home.vue')
template = render(MAIN_TEMPLATE, {
include: '',
routes: JSON.stringify(routes.newRoutes).replace(/"component":"(.+?)"/g, ($0, $1) => `"component": () => import( /* webpackChunkName: "${$1}" */ '${componentsObj[$1]}')`)
routes: JSON.stringify(routes).replace(/"component":"(.+?)"/g, ($0, $1) => `"component": () => import( /* webpackChunkName: "${$1}" */ '${componentsObj[$1]}')`)
});
} else {
routes.components.forEach((item) => {
components.forEach((item) => {
componentsTemplate.push(render(IMPORT_TEMPLATE, {
name: item.name,
path: item.path
@ -38,7 +38,7 @@ export default {{routes}};
template = render(MAIN_TEMPLATE, {
include: componentsTemplate.join(endOfLine),
routes: JSON.stringify(routes.newRoutes).replace(/"component":"(.+?)"/g, '"component": $1')
routes: JSON.stringify(routes).replace(/"component":"(.+?)"/g, '"component": $1')
});
}

View File

@ -29,6 +29,8 @@ fes-project
│   ├── home
│   │   └── index.vue
│   └── list
│   ├── components
│   │ └── common.vue
│   ├── edit
│   │   └── index.vue
│   └── index.vue

View File

@ -6,6 +6,7 @@
```
pages
├── index.vue # 根路由页面 路径 index.html#/
├── *.vue # 模糊匹配 路径 *
├── a.vue # 路径 /a
├── b # 文件夹b
│ ├── index.vue # 路径 /b
@ -14,41 +15,19 @@
└── layout.vue # 根路由下所有page共用的外层
```
1. 如果目录下有`layout.vue`,则子目录对应的路径是当前目录对应路径的子路由。如果没有则子目录对应的路径和当前目录对应路径是平级的。
2. 带参数的路径使用`@[filename].vue`的方式
2. 带参数的路径使用`@[filename].vue`的方式,例如@id.vue
3. 支持模糊匹配,例如`pages/*.vue`对应的路径是`*`可以通过此路由实现404效果
4. pages下的components文件夹下的.vue不被解析成路由可以存放跟业务相关的公共组件。
<br>
Fes编译代码之前会根据 pages 目录结构生成下面的配置代码:
## 智能路由匹配
根据精准匹配优先算法原则设计出路由排名算法,对匹配到的路由打分,选择分数最高的路由:
- 路由的路径每个子项得到4分
- 子项为静态细分(/list)再加3分
- 子项为动态细分(/:orderId再加2分
- 根段加1分
- 通配符匹配到的减去1分
```javascript
import layout from 'D:\\git\\fes-template\\src\\pages\\layout.vue';
import index from 'D:\\git\\fes-template\\src\\pages\\index.vue';
import a from 'D:\\git\\fes-template\\src\\pages\\a.vue';
import b_index from 'D:\\git\\fes-template\\src\\pages\\b\\index.vue';
import b__id from 'D:\\git\\fes-template\\src\\pages\\b\\@id.vue';
import b_c from 'D:\\git\\fes-template\\src\\pages\\b\\b_c';
export default {
'/': {
component: layout,
subRoutes: {
'/' : {
name: 'index', component: index
},
'/a' : {
name: 'a', component: a
},
'/b' : {
name: 'b_index', component: b_index
},
'/b/:id' : {
name: 'b__id', component: b__id
},
'/c' : {
name: 'b_c', component: b_c
}
}
}
};
```
通过智能路由匹配可以解决类似`/list/create``/list/:id`到底匹配什么路由的问题。
## 跳转路由
API参考[Vue-router](https://router.vuejs.org/zh-cn/)。[路由实例](https://router.vuejs.org/zh-cn/api/router-instance.html)路由实例会挂载在FesApp对象上。

View File

@ -380,6 +380,28 @@
"vue-eslint-parser": "^6.0.4"
}
},
"@webank/fes-core": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@webank/fes-core/-/fes-core-0.2.4.tgz",
"integrity": "sha512-sBpNzpp5EJrsEh2fx8wQGtYiZdRG0eSWe9cr+LTCtou119zFZ3Ed9Bsth0mgLAx//Z57c0/FyxMsSsK3q2Lrmg==",
"requires": {
"axios": "^0.16.2",
"lodash": "^4.17.15",
"vue": "^2.6.10",
"vue-i18n": "^8.4.0",
"vue-router": "^2.6.0",
"vue-template-compiler": "^2.6.10"
}
},
"@webank/fes-ui": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@webank/fes-ui/-/fes-ui-0.2.4.tgz",
"integrity": "sha512-ovemdhoY1w65Op7RYnMnGv2IZkj+LD1cMxzyjyb48LhZaQj2ZkO3y8ViLUHVxAUpapEQ2qlCrd5y1IFs9bwefg==",
"requires": {
"async-validator": "^1.8.2",
"xss": "^1.0.7"
}
},
"abs-svg-path": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
@ -508,6 +530,20 @@
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
"dev": true
},
"async-validator": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-1.12.2.tgz",
"integrity": "sha512-57EETfCPFiB7M4QscvQzWSGNsmtkjjzZv318SK1CBlstk+hycV72ocjriMOOM48HjvmoAoJGpJNjC7Z76RlnZA=="
},
"axios": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz",
"integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=",
"requires": {
"follow-redirects": "^1.2.3",
"is-buffer": "^1.1.5"
}
},
"babel-eslint": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
@ -687,6 +723,11 @@
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
"dev": true
},
"cssfilter": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
"integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4="
},
"d3-array": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
@ -817,6 +858,11 @@
"lodash": "^4.17.15"
}
},
"de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@ -1316,6 +1362,11 @@
"integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
"dev": true
},
"follow-redirects": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
"integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -1394,6 +1445,11 @@
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
"dev": true
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
@ -1504,6 +1560,11 @@
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"is-callable": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz",
@ -2482,6 +2543,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"vue": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
},
"vue-eslint-parser": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-6.0.5.tgz",
@ -2496,6 +2562,25 @@
"lodash": "^4.17.11"
}
},
"vue-i18n": {
"version": "8.22.1",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.22.1.tgz",
"integrity": "sha512-JNgiEJ5a8YPfk5y2lKyfOAGLmkpAVfhaUi+T4wGpSppRYZ3XSyawSDDketY5KV2CsAiBLAGEIO6jO+0l2hQubg=="
},
"vue-router": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-2.8.1.tgz",
"integrity": "sha512-MC4jacHBhTPKtmcfzvaj2N7g6jgJ/Z/eIjZdt+yUaUOM1iKC0OUIlO/xCtz6OZFFTNUJs/1YNro2GN/lE+nOXA=="
},
"vue-template-compiler": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz",
"integrity": "sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg==",
"requires": {
"de-indent": "^1.0.2",
"he": "^1.1.0"
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@ -2530,6 +2615,15 @@
"requires": {
"mkdirp": "^0.5.1"
}
},
"xss": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz",
"integrity": "sha512-3MgPdaXV8rfQ/pNn16Eio6VXYPTkqwa0vc7GkiymmY/DqR1SE/7VPAAVZz1GJsJFrllMYO3RHfEaiUGjab6TNw==",
"requires": {
"commander": "^2.20.3",
"cssfilter": "0.0.10"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,42 @@
<template>
<div class="page-404">
<img class="page-404-bg" src="~assets/images/404.png" />
<div class="page-404-title">404</div>
<div class="page-404-subtilte">对不起, 你访问的页面不存在</div>
<Wb-button @click="goHome" class="page-404-button" type="primary">返回首页</Wb-button>
</div>
</template>
<script>
export default {
methods: {
goHome() {
this.FesApp.router.push('/dashboard/console');
}
}
};
</script>
<style lang="scss" scoped>
.page-404{
text-align: center;
.page-404-bg{
display: block;
width: 600px;
margin: 50px auto;
}
.page-404-title{
color: rgba(0, 0, 0, 0.85);
font-size: 24px;
line-height: 1.8;
text-align: center;
}
.page-404-subtilte{
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
line-height: 1.6;
text-align: center;
}
.page-404-button{
margin-top: 24px;
}
}
</style>

View File

@ -0,0 +1,8 @@
<template>
<div>card</div>
</template>
<script>
export default {
};
</script>