feat: 添加[...slug].vue方式替换*.vue实现模糊匹配 (#183)

This commit is contained in:
听海 2023-04-07 20:46:33 +08:00 committed by GitHub
parent a6a4f36162
commit c3969f8d87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 226 additions and 167 deletions

View File

@ -5,21 +5,24 @@
## 路由配置 ## 路由配置
在配置文件 `.fes.js`中通过 `router` 进行配置。 在配置文件 `.fes.js`中通过 `router` 进行配置。
```js ```js
export default { export default {
router: { router: {
routes: [], routes: [],
mode: 'hash' mode: 'hash',
} },
} };
``` ```
### routes ### routes
`routes` 是配置添加到路由的初始路由列表,格式为路由信息的数组。具体使用参考 [Vue Router 文档](https://next.router.vuejs.org/zh/guide/) 中关于路由配置、路由匹配相关内容。 `routes` 是配置添加到路由的初始路由列表,格式为路由信息的数组。具体使用参考 [Vue Router 文档](https://next.router.vuejs.org/zh/guide/) 中关于路由配置、路由匹配相关内容。
### mode ### mode
创建历史记录的类型: 创建历史记录的类型:
- **history**,对应 [createWebHistory](https://next.router.vuejs.org/zh/api/#createwebhistory) - **history**,对应 [createWebHistory](https://next.router.vuejs.org/zh/api/#createwebhistory)
- **hash**,对应 [createWebHashHistory](https://next.router.vuejs.org/zh/api/#createWebHashHistory) - **hash**,对应 [createWebHashHistory](https://next.router.vuejs.org/zh/api/#createWebHashHistory)
- **memory**,对应 [createMemoryHistory](https://next.router.vuejs.org/zh/api/#createWebHashHistory) - **memory**,对应 [createMemoryHistory](https://next.router.vuejs.org/zh/api/#createWebHashHistory)
@ -27,100 +30,128 @@ export default {
默认是`hash`模式。 默认是`hash`模式。
## 约定式路由 ## 约定式路由
约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。 约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。
### 约定规范 ### 约定规范
比如以下文件结构: 比如以下文件结构:
``` ```
pages pages
├── index.vue # 根路由页面 路径为 / ├── index.vue # 根路由页面 路径为 /
├── *.vue # 模糊匹配 路径为 * ├── [...slug].vue # 模糊匹配 路径为 /:slug(.*)
├── a.vue # 路径 /a ├── a.vue # 路径 /a
├── b # 文件夹b ├── b # 文件夹b
│ ├── index.vue # 路径 /b │ ├── index.vue # 路径 /b
│ ├── @id.vue # 动态路由 /b/:id │ ├── [slug].vue # 动态路由 /b/:slug
│ ├── c.vue # 路径 /b/c │ ├── c.vue # 路径 /b/c
│ └── layout.vue # /b 路径下所有页面公共的布局组件 │ └── layout.vue # /b 路径下所有页面公共的布局组件
└── layout.vue # 根路由下所有页面共用的布局组件 └── layout.vue # 根路由下所有页面共用的布局组件
``` ```
编译后会得到以下路由配置: 编译后会得到以下路由配置:
```js ```js
[ [
{ {
"path": "/", path: '/',
"component": require('@/pages/layout').default, component: require('@/pages/layout').default,
"count": 5, count: 5,
"children": [ children: [
{ {
"path": "/a", path: '/a',
"component": require('@/pages/a').default, component: require('@/pages/a').default,
"name": "a", name: 'a',
"meta": {}, meta: {},
"count": 7 count: 7,
}, },
{ {
"path": "/b", path: '/b',
"component": require('@/pages/b/layout').default, component: require('@/pages/b/layout').default,
"count": 7, count: 7,
"children": [ children: [
{ {
"path": "/b/c", path: '/b/c',
"component": require('@/pages/b/c').default, component: require('@/pages/b/c').default,
"name": "b_c", name: 'b_c',
"meta": {}, meta: {},
"count": 14 count: 14,
}, },
{ {
"path": "/b/:id", path: '/b/:id',
"component": require('@/pages/b/@id').default, component: require('@/pages/b/@id').default,
"name": "b__id", name: 'b__id',
"meta": {}, meta: {},
"count": 13 count: 13,
}, },
{ {
"path": "/b", path: '/b',
"component": require('@/pages/b/index').default, component: require('@/pages/b/index').default,
"name": "b_index", name: 'b_index',
"meta": {}, meta: {},
"count": 7 count: 7,
} },
] ],
}, },
{ {
"path": "/", path: '/',
"component": require('@/pages/index').default, component: require('@/pages/index').default,
"name": "index", name: 'index',
"meta": {}, meta: {},
"count": 5 count: 5,
}, },
{ {
"path": "/:pathMatch(.*)", path: '/:pathMatch(.*)',
"component": require('@/pages/*').default, component: require('@/pages/*').default,
"name": "FUZZYMATCH", name: 'FUZZYMATCH',
"meta": {}, meta: {},
"count": 3 count: 3,
} },
] ],
} },
] ];
``` ```
**需要注意的是,满足以下任意规则的文件不会被注册为路由** **需要注意的是,满足以下任意规则的文件不会被注册为路由**
- 不是 `.vue .jsx` 文件 - 不是 `.vue .jsx` 文件
- `components` 目录中的文件 - `components` 目录中的文件
### 动态路由 ### 动态路由
Fes.js 里约定以 `@` 开头的文件或文件夹映射为动态路由。
Fes.js 里约定名称为 `[slug]`格式的文件或文件夹映射为动态路由。
比如: 比如:
- `src/pages/users/@id.vue` 会成为 `/users/:id` - `src/pages/users/[id].vue` 会成为 `/users/:id`
- `src/pages/users/@id/settings.vue` 会成为 `/users/:id/settings` - `src/pages/users/[id]/settings.vue` 会成为 `/users/:id/settings`
:::warning
`@slug`形式下版本会弃用,请替换为`[slug]`~
:::
### 模糊匹配
Fes.js 里约定名称为 `[...slug]`格式的文件或文件夹映射为动态路由中的模糊匹配形式。
比如:
- `src/pages/users/[...].vue` 会成为 `/users/:pathMatch(.*)`
- `src/pages/users/[...id].vue` 会成为 `/users/:id(.*)`
- `src/pages/users/[...id]/settings.vue` 会成为 `/users/:id(.*)/settings`
:::warning
`*`形式下版本会弃用,请替换为`[...slug]`~
:::
### 嵌套路由 ### 嵌套路由
Fes.js 里约定目录下有 `layout.vue` 时会生成嵌套路由,以 `layout.vue` 为该目录的公共父组件,`layout.vue` 中必须实现 `RouterView` Fes.js 里约定目录下有 `layout.vue` 时会生成嵌套路由,以 `layout.vue` 为该目录的公共父组件,`layout.vue` 中必须实现 `RouterView`
比如以下目录结构: 比如以下目录结构:
``` ```
pages pages
└── users └── users
@ -128,20 +159,24 @@ pages
├── index.vue ├── index.vue
└── list.vue └── list.vue
``` ```
会生成路由: 会生成路由:
```js ```js
[ [
{ {
path: '/users', component: require('@/pages/users/layout').default, path: '/users',
component: require('@/pages/users/layout').default,
children: [ children: [
{ path: '/users', component: require('@/pages/users/index').default }, { path: '/users', component: require('@/pages/users/index').default },
{ path: '/users/list', component: require('@/pages/users/list').default }, { path: '/users/list', component: require('@/pages/users/list').default },
] ],
} },
] ];
``` ```
### 模糊匹配 ### 模糊匹配
Fes.js 下约定文件名为 `*` 的路由是模糊匹配路由,可以用此特性实现 [404 路由](https://next.router.vuejs.org/zh/guide/essentials/dynamic-matching.html#%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E8%B7%AF%E7%94%B1%E6%88%96-404-not-found-%E8%B7%AF%E7%94%B1)。 Fes.js 下约定文件名为 `*` 的路由是模糊匹配路由,可以用此特性实现 [404 路由](https://next.router.vuejs.org/zh/guide/essentials/dynamic-matching.html#%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E8%B7%AF%E7%94%B1%E6%88%96-404-not-found-%E8%B7%AF%E7%94%B1)。
比如以下目录结构: 比如以下目录结构:
@ -151,21 +186,30 @@ pages
├── index.vue # 根路由页面 路径为 / ├── index.vue # 根路由页面 路径为 /
└── *.vue # 模糊匹配 路径为 * └── *.vue # 模糊匹配 路径为 *
``` ```
会生成路由: 会生成路由:
```js ```js
[ [
{ {
path: '/', component: require('@/pages/index').default, count: 5 path: '/',
component: require('@/pages/index').default,
count: 5,
}, },
{ {
path: '/:pathMatch(.*)', component: require('@/pages/**').default, count: 3 path: '/:pathMatch(.*)',
} component: require('@/pages/**').default,
] count: 3,
},
];
``` ```
这样,如果访问 `/foo``/` 不能匹配,会 fallback 到 `*` 路由,通过 `src/pages/*.vue` 进行渲染。 这样,如果访问 `/foo``/` 不能匹配,会 fallback 到 `*` 路由,通过 `src/pages/*.vue` 进行渲染。
### 智能路由 ### 智能路由
可以看到,编译后路由都会有 `count` 属性,这是我们根据精准匹配优先算法原则设计出路由排名算法,对匹配到的路由打分: 可以看到,编译后路由都会有 `count` 属性,这是我们根据精准匹配优先算法原则设计出路由排名算法,对匹配到的路由打分:
- 路由的路径每个子项得到 4 分 - 路由的路径每个子项得到 4 分
- 子项为静态细分(`/list`)再加 3 分 - 子项为静态细分(`/list`)再加 3 分
- 子项为动态细分(`/:orderId`)再加 2 分 - 子项为动态细分(`/:orderId`)再加 2 分
@ -189,23 +233,22 @@ const router = new VueRouter({
path: 'bar', path: 'bar',
component: Bar, component: Bar,
// a meta field // a meta field
meta: { requiresAuth: true } meta: { requiresAuth: true },
} },
] ],
} },
] ],
}) });
``` ```
我们使用`defineRouteMeta` 配置 `meta` 我们使用`defineRouteMeta` 配置 `meta`
```js ```js
import { defineRouteMeta } from '@fesjs/fes'; import { defineRouteMeta } from '@fesjs/fes';
defineRouteMeta({ defineRouteMeta({
name: "store", name: 'store',
title: "vuex测试" title: 'vuex测试',
}) });
``` ```
当然在单文件组件中,还可以通过`<config></config>`配置 `meta` 当然在单文件组件中,还可以通过`<config></config>`配置 `meta`
@ -223,9 +266,8 @@ defineRouteMeta({
推荐使用`defineRouteMete`,有更好的提示。 推荐使用`defineRouteMete`,有更好的提示。
::: :::
路由元信息在编译后会附加到路由配置中: 路由元信息在编译后会附加到路由配置中:
```js{5-8} ```js{5-8}
[ [
{ {
@ -240,9 +282,11 @@ defineRouteMeta({
``` ```
## 路由跳转 ## 路由跳转
想学习更多,可以查看 [Vue Router 官方文档](https://next.router.vuejs.org/zh/guide/essentials/navigation.html#%E6%9B%BF%E6%8D%A2%E5%BD%93%E5%89%8D%E4%BD%8D%E7%BD%AE)。 想学习更多,可以查看 [Vue Router 官方文档](https://next.router.vuejs.org/zh/guide/essentials/navigation.html#%E6%9B%BF%E6%8D%A2%E5%BD%93%E5%89%8D%E4%BD%8D%E7%BD%AE)。
### 声明式 ### 声明式
```vue ```vue
<template> <template>
<router-link to="/home">Home</router-link> <router-link to="/home">Home</router-link>
@ -250,6 +294,7 @@ defineRouteMeta({
``` ```
### 命令式 ### 命令式
页面跳转 API 由 `router` 实例提供,查看 [Vue Rouer 文档](https://next.router.vuejs.org/zh/api/#router-%E6%96%B9%E6%B3%95)了解更多。 页面跳转 API 由 `router` 实例提供,查看 [Vue Rouer 文档](https://next.router.vuejs.org/zh/api/#router-%E6%96%B9%E6%B3%95)了解更多。
```js ```js
@ -259,15 +304,15 @@ export default {
setup() { setup() {
const router = useRouter(); const router = useRouter();
// 这三种形式是等价的 // 这三种形式是等价的
router.push('/users/posva#bio') router.push('/users/posva#bio');
router.push({ path: '/users/posva', hash: '#bio' }) router.push({ path: '/users/posva', hash: '#bio' });
router.push({ name: 'users', params: { username: 'posva' }, hash: '#bio' }) router.push({ name: 'users', params: { username: 'posva' }, hash: '#bio' });
// 只改变 hash // 只改变 hash
router.push({ hash: '#bio' }) router.push({ hash: '#bio' });
// 只改变 query // 只改变 query
router.push({ query: { page: '2' } }) router.push({ query: { page: '2' } });
// 只改变 param // 只改变 param
router.push({ params: { username: 'jolyne' } }) router.push({ params: { username: 'jolyne' } });
// 跳转到上一个路由 // 跳转到上一个路由
router.goBack(); router.goBack();
@ -277,7 +322,6 @@ export default {
// 替换历史堆栈中的记录 // 替换历史堆栈中的记录
router.replace('/new'); router.replace('/new');
} },
} };
``` ```

View File

@ -6,11 +6,11 @@ export default function () {
require.resolve('./plugins/registerType'), require.resolve('./plugins/registerType'),
// generate files // generate files
require.resolve('./plugins/generateFiles/core/plugin'), require.resolve('./plugins/core/plugin'),
require.resolve('./plugins/generateFiles/core/exports/coreExports'), require.resolve('./plugins/core/exports/coreExports'),
require.resolve('./plugins/generateFiles/core/exports/pluginExports'), require.resolve('./plugins/core/exports/pluginExports'),
require.resolve('./plugins/generateFiles/fes'), require.resolve('./plugins/core/entry'),
require.resolve('./plugins/generateFiles/genType'), require.resolve('./plugins/core/route'),
// bundle configs // bundle configs
require.resolve('./plugins/features/alias'), require.resolve('./plugins/features/alias'),
@ -31,9 +31,6 @@ export default function () {
require.resolve('./plugins/features/terserOptions'), require.resolve('./plugins/features/terserOptions'),
require.resolve('./plugins/features/title'), require.resolve('./plugins/features/title'),
// route
require.resolve('./plugins/route'),
// commands // commands
require.resolve('./plugins/commands/help'), require.resolve('./plugins/commands/help'),
require.resolve('./plugins/commands/info'), require.resolve('./plugins/commands/info'),

View File

@ -1,7 +1,7 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import generateExports from '../../../../utils/generateExports'; import generateExports from '../../../utils/generateExports';
import { runtimePath } from '../../../../utils/constants'; import { runtimePath } from '../../../utils/constants';
export default function (api) { export default function (api) {
api.onGenerateFiles(async () => { api.onGenerateFiles(async () => {

View File

@ -1,11 +1,11 @@
import generateExports from '../../../../utils/generateExports'; import generateExports from '../../../utils/generateExports';
export default function (api) { export default function (api) {
api.onGenerateFiles(async () => { api.onGenerateFiles(async () => {
const fesExports = await api.applyPlugins({ const fesExports = await api.applyPlugins({
key: 'addPluginExports', key: 'addPluginExports',
type: api.ApplyPluginsType.add, type: api.ApplyPluginsType.add,
initialValue: [] initialValue: [],
}); });
const fesExportsHook = {}; // repeated definition const fesExportsHook = {}; // repeated definition
@ -13,11 +13,13 @@ export default function (api) {
api.writeTmpFile({ api.writeTmpFile({
path: absoluteFilePath, path: absoluteFilePath,
content: `${fesExports content: `${fesExports
.map(item => generateExports(absoluteFilePath, { .map((item) =>
generateExports(absoluteFilePath, {
item, item,
fesExportsHook fesExportsHook,
})) }),
.join('\n')}\n` )
.join('\n')}\n`,
}); });
}); });
} }

View File

@ -1,7 +1,7 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { winPath } from '@fesjs/utils'; import { winPath } from '@fesjs/utils';
import { runtimePath } from '../../../../utils/constants'; import { runtimePath } from '../../../utils/constants';
export default function (api) { export default function (api) {
const { const {

View File

@ -2,15 +2,15 @@ import { readdirSync, statSync, readFileSync } from 'fs';
import { join, extname, basename } from 'path'; import { join, extname, basename } from 'path';
import { lodash, parser, generator, logger, winPath } from '@fesjs/utils'; import { lodash, parser, generator, logger, winPath } from '@fesjs/utils';
import { parse } from '@vue/compiler-sfc'; import { parse } from '@vue/compiler-sfc';
import { runtimePath } from '../../utils/constants'; import { runtimePath } from '../../../utils/constants';
// pages // pages
// ├── index.vue # 根路由页面 路径 / // ├── index.vue # 根路由页面 路径 /
// ├── *.vue # 模糊匹配 路径 * // ├── [...slug].vue # 模糊匹配 路径 /:slug(.*)
// ├── a.vue # 路径 /a // ├── a.vue # 路径 /a
// ├── b // ├── b
// │ ├── index.vue # 路径 /b // │ ├── index.vue # 路径 /b
// │ ├── @id.vue # 动态路由 /b/:id // │ ├── [slug].vue # 动态路由 /b/:slug
// │ └── c.vue # 路径 /b/c // │ └── c.vue # 路径 /b/c
// └── layout.vue # 根路由下所有page共用的外层 // └── layout.vue # 根路由下所有page共用的外层
@ -38,7 +38,14 @@ const checkHasLayout = function (path) {
const getRouteName = function (parentRoutePath, fileName) { const getRouteName = function (parentRoutePath, fileName) {
const routeName = winPath(join(parentRoutePath, fileName)); const routeName = winPath(join(parentRoutePath, fileName));
return routeName.slice(1).replace(/\//g, '_').replace(/@/g, '_').replace(/:/g, '_').replace(/\*/g, 'FUZZYMATCH'); return routeName
.slice(1)
.replace(/\//g, '_')
.replace(/@/g, '_')
.replace(/:/g, '_')
.replace(/\*/g, 'FUZZYMATCH')
.replace(/\[([a-zA-Z]+)\]/, '_$1')
.replace(/\[...([a-zA-Z]*)\]/, 'FUZZYMATCH-$1');
}; };
const getRoutePath = function (parentRoutePath, fileName, isFile = true) { const getRoutePath = function (parentRoutePath, fileName, isFile = true) {
@ -48,12 +55,22 @@ const getRoutePath = function (parentRoutePath, fileName, isFile = true) {
} }
// /@id.vue -> /:id // /@id.vue -> /:id
if (fileName.startsWith('@')) { if (fileName.startsWith('@')) {
logger.warn(`[WARNING]: ${fileName} is deprecated, please use [slug]`);
fileName = fileName.replace(/@/, ':'); fileName = fileName.replace(/@/, ':');
} }
// /*.vue -> :pathMatch(.*) // /*.vue -> :pathMatch(.*)
if (fileName.includes('*')) { if (fileName.includes('*')) {
logger.warn(`[WARNING]: ${fileName} is deprecated, please use [...slug]`);
fileName = fileName.replace('*', ':pathMatch(.*)'); fileName = fileName.replace('*', ':pathMatch(.*)');
} }
// /[slug].vue -> /:slug
if (/\[[a-zA-Z]+\]/.test(fileName)) {
fileName = fileName.replace(/\[([a-zA-Z]+)\]/g, ':$1');
}
// /[...slug].vue -> /:slug(.*)
if (/\[...[a-zA-Z]*\]/.test(fileName)) {
fileName = fileName.replace(/\[...([a-zA-Z]*)\]/, ':$1(.*)').replace(':(.*)', ':pathMatch(.*)');
}
return winPath(join(parentRoutePath, fileName)); return winPath(join(parentRoutePath, fileName));
}; };
@ -186,9 +203,9 @@ const rank = function (routes) {
let count = 0; let count = 0;
arr.forEach((sonPath) => { arr.forEach((sonPath) => {
count += 4; count += 4;
if (sonPath.indexOf(':') !== -1 && sonPath.indexOf(':pathMatch(.*)') === -1) { if (sonPath.indexOf(':') !== -1 && sonPath.indexOf('(.*)') === -1) {
count += 2; count += 2;
} else if (sonPath.indexOf(':pathMatch(.*)') !== -1) { } else if (sonPath.indexOf('(.*)') !== -1) {
count -= 1; count -= 1;
} else if (sonPath === '') { } else if (sonPath === '') {
count += 1; count += 1;

View File

@ -1,33 +0,0 @@
function importsToStr(imports) {
return imports.map((imp) => {
const { source } = imp;
return `export * from '${source}';`;
});
}
function genTypeContent(imports) {
return {
imports: importsToStr(imports).join('\n'),
};
}
export default function (api) {
const {
utils: { Mustache },
} = api;
api.onGenerateFiles(async () => {
const typeTpl = `
{{{ imports }}}
`;
const importSources = await api.applyPlugins({
key: 'addConfigType',
type: api.ApplyPluginsType.add,
initialValue: [],
});
api.writeTmpFile({
path: 'configType.d.ts',
content: Mustache.render(typeTpl, genTypeContent(importSources)),
});
});
}

View File

@ -1,9 +1,41 @@
import { name } from '../../package.json'; import { name } from '../../package.json';
function importsToStr(imports) {
return imports.map((imp) => {
const { source } = imp;
return `export * from '${source}';`;
});
}
function genTypeContent(imports) {
return {
imports: importsToStr(imports).join('\n'),
};
}
export default function (api) { export default function (api) {
const {
utils: { Mustache },
} = api;
api.addConfigType(() => ({ api.addConfigType(() => ({
source: name, source: name,
runtime: ['InnerRuntimeConfig'], runtime: ['InnerRuntimeConfig'],
build: ['InnerBuildConfig'], build: ['InnerBuildConfig'],
})); }));
api.onGenerateFiles(async () => {
const typeTpl = `
{{{ imports }}}
`;
const importSources = await api.applyPlugins({
key: 'addConfigType',
type: api.ApplyPluginsType.add,
initialValue: [],
});
api.writeTmpFile({
path: 'configType.d.ts',
content: Mustache.render(typeTpl, genTypeContent(importSources)),
});
});
} }