feat: 优化路由加载机制
parent
46c6c9a3a7
commit
d8230ad3b9
|
|
@ -0,0 +1,4 @@
|
|||
# 参见 .env
|
||||
|
||||
VITE_BASE = ./
|
||||
VITE_HISTORY = hash
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -18,5 +18,7 @@ COPY --from=builder /app/dist /usr/share/nginx/html
|
|||
# 复制nginx配置
|
||||
COPY --from=builder /app/.github/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 显式暴露端口
|
||||
EXPOSE 80
|
||||
# 启动,关闭后台运行启动前台运行,不然 docker 会结束运行
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
143
README.md
143
README.md
|
|
@ -53,20 +53,153 @@ pnpm dev
|
|||
|
||||
根据 src/pages 目录生成路由数组,包含以下以下规则:
|
||||
|
||||
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹
|
||||
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹作为嵌套路由
|
||||
- 在 src/pages 的文件夹层级,作为菜单层级,路由层级最终只有 2 层(配合 keep-alive 缓存)
|
||||
- 在 src/pages 目录下,以 _ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
|
||||
- 子文件夹下,只有 index.vue 文件有效,其他文件会忽略,这些文件可以作为子组件使用
|
||||
- components 目录会被忽视
|
||||
- xxx.xx.xx 文件会被忽视,例如 index.my.vue
|
||||
- components 目录会被忽视。
|
||||
- xxx.xx.xx 文件会被忽视,例如 index.my.vue 文件。
|
||||
|
||||
对应目录下的 index.vue 文件中定义如下路由配置:
|
||||
|
||||
```jsonc
|
||||
<route lang="json">
|
||||
{
|
||||
"parentMeta": {
|
||||
// 具体属性查阅 src/types/vue-router.d.ts
|
||||
// 其他 Route 参数
|
||||
"meta": {
|
||||
// 请看下面
|
||||
}
|
||||
}
|
||||
</route>
|
||||
```
|
||||
|
||||
目前支持的参数,如下:
|
||||
|
||||
```ts
|
||||
interface RouteMeta {
|
||||
/**
|
||||
* 页面标题
|
||||
* @description
|
||||
* 菜单和导航面包屑等地方会用到
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* 页面图标
|
||||
* @description
|
||||
* 使用 icon-park-outline 图标集的图标类名
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* 显示顺序
|
||||
* @description
|
||||
* 在菜单中的显示顺序,越小越靠前
|
||||
*/
|
||||
sort?: number;
|
||||
/**
|
||||
* 是否隐藏
|
||||
* @description
|
||||
* - false // 不隐藏(默认)
|
||||
* - true // 在路由和菜单中隐藏,即忽略且不打包
|
||||
* - 'menu' // 在菜单中隐藏,通过其他方式访问
|
||||
* - 'prod' // 在生产环境下隐藏
|
||||
*/
|
||||
hide?: boolean | 'menu' | 'prod';
|
||||
/**
|
||||
* 所需权限
|
||||
* @example
|
||||
* ```js
|
||||
* ['system:user']
|
||||
* ```
|
||||
*/
|
||||
auth?: string[];
|
||||
/**
|
||||
* 是否缓存
|
||||
* @description
|
||||
* 是否使用 keep-alive 缓存
|
||||
*/
|
||||
cache?: boolean;
|
||||
/**
|
||||
* 组件名字
|
||||
* @description
|
||||
* 组件名字,当 cache为true 时必须
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* 是否显示loading
|
||||
* @description
|
||||
* 可以自定义 loading 文本
|
||||
*/
|
||||
loading?: boolean | string;
|
||||
/**
|
||||
* 链接
|
||||
* @description
|
||||
* ```js
|
||||
* 'https://juetan.cn'
|
||||
* ```
|
||||
*/
|
||||
link?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 嵌套布局
|
||||
|
||||
默认情况下,嵌套路由会使用父级 index.vue 作为布局文件,如果不需要布局,只需在父级路由指定 component 为 null 即可,如下:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"component": null,
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
这样,其层级仅作为菜单层级,在路由上表现为扁平。
|
||||
|
||||
### 路由权限
|
||||
|
||||
在每个路由的 index.vue 文件中,通过 meta.auth 字段指定访问该路由所需的权限,示例如下:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"meta": {
|
||||
"auth": ["system:user", "system:menu"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
默认全部需要登陆才可访问,其中有 2 个比较特殊的权限:
|
||||
|
||||
- `*` 表示无需登陆即可访问,适合挂一些比较通用的页面。
|
||||
- `unlogin` 表示未登录才可以访问。例如登录页,登陆后访问该页面会被拒绝。
|
||||
|
||||
用户登陆后获取的权限,应存储在 userStore.auth 字段中,在路由的 beforeEach 守卫中,会比较两个是否匹配,匹配上则继续,否则会显示如下 403 页面:
|
||||
|
||||

|
||||
|
||||
### 动态路由
|
||||
|
||||
相比于比较流行的加法挂载,我更倾向于减法挂载。即默认加载完所有路由,在 beforeEach 钩子根据权限移除不必要的路由。
|
||||
|
||||
### 动态首页
|
||||
|
||||
在作为首页路由的 index.vue 文件中,指定 alias 为 '/' 即可,默认是 home/index.vue 文件。如需动态更新首页,在 beforeEach 获取完菜单信息,通过 removeRoute 移除旧的首页路由,通过 addRoute 添加新的首页路由即可。
|
||||
|
||||
### 路由缓存
|
||||
|
||||
在路由的 index.vue 文件,首先指定好组件的名字,再通过 cache 字段开启缓存,示例如下:
|
||||
|
||||
```html
|
||||
<script>
|
||||
defineOptions({
|
||||
name: "MyPage"
|
||||
})
|
||||
</script>
|
||||
<route>
|
||||
{
|
||||
"meta": {
|
||||
// 组件名字
|
||||
"name": "MyPage",
|
||||
// 开启缓存
|
||||
"cache": true
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ export default defineComponent({
|
|||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
selectedKeys.value = route.matched.map(i => i.path);
|
||||
selectedKeys.value = route.matched.map(i => i.aliasOf?.path ?? i.path);
|
||||
},
|
||||
{ immediate: true }
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function goto(route: MenuItem) {
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ const buttons = [
|
|||
|
||||
<route lang="json">
|
||||
{
|
||||
"redirect": "/",
|
||||
"meta": {
|
||||
"name": "LayoutPage",
|
||||
"sort": 101,
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ const stat = {
|
|||
|
||||
<route lang="json">
|
||||
{
|
||||
"alias": "/",
|
||||
"meta": {
|
||||
"sort": 1000,
|
||||
"title": "首页",
|
||||
|
|
|
|||
|
|
@ -173,7 +173,6 @@ const { component: UserTable } = useTable({
|
|||
{
|
||||
"meta": {
|
||||
"name": "SystemDepartmentPage",
|
||||
"keepAlive": true,
|
||||
"sort": 10301,
|
||||
"title": "部门管理",
|
||||
"icon": "icon-park-outline-group"
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ const { component: RoleTable } = useTable({
|
|||
"name": "SystemRolePage",
|
||||
"sort": 10302,
|
||||
"title": "角色管理",
|
||||
"auth": ["role"],
|
||||
"icon": "icon-park-outline-shield"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,6 @@ const { component: UserTable } = useTable({
|
|||
"cache": true,
|
||||
"sort": 10301,
|
||||
"title": "用户管理",
|
||||
"auth": ["*"],
|
||||
"icon": "icon-park-outline-user"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,16 @@ import { api } from '@/api';
|
|||
import { env } from '@/config/env';
|
||||
import { store, useUserStore } from '@/store';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import { treeEach, treeFilter, treeFind } from '@/utils/listToTree';
|
||||
import { treeEach } from '@/utils/listToTree';
|
||||
import { Notification } from '@arco-design/web-vue';
|
||||
import { Router } from 'vue-router';
|
||||
import { menus } from '../menus';
|
||||
import { APP_HOME_NAME } from '../routes/base';
|
||||
import { APP_ROUTE_NAME, routes } from '../routes/page';
|
||||
import { appRoutes } from '../routes/page';
|
||||
|
||||
/**
|
||||
* 权限守卫
|
||||
* @param to 路由
|
||||
* @description store不能放在外面,否则 pinia-plugin-peristedstate 插件会失效
|
||||
* @returns
|
||||
*/
|
||||
export function useAuthGuard(router: Router) {
|
||||
api.expireHandler = () => {
|
||||
|
|
@ -39,17 +37,17 @@ export function useAuthGuard(router: Router) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// 直接访问跳转回首页(非路由跳转)
|
||||
if (!from.matched.length) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
// 提示已登陆
|
||||
Notification.warning({
|
||||
title: '跳转提示',
|
||||
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
||||
});
|
||||
|
||||
// 直接访问跳转回首页(不是从路由跳转)
|
||||
if (!from.matched.length) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
// 已登陆不允许
|
||||
return false;
|
||||
}
|
||||
|
|
@ -64,37 +62,24 @@ export function useAuthGuard(router: Router) {
|
|||
|
||||
// 未获取权限进行获取
|
||||
if (!menuStore.menus.length) {
|
||||
// 菜单处理
|
||||
const authMenus = treeFilter(menus, item => {
|
||||
if (item.path === env.homePath) {
|
||||
item.path = '/';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
menuStore.setMenus(authMenus);
|
||||
menuStore.setMenus(menus);
|
||||
menuStore.setHome(env.homePath);
|
||||
|
||||
// 路由处理
|
||||
for (const route of routes) {
|
||||
router.addRoute(route);
|
||||
}
|
||||
|
||||
// 缓存处理
|
||||
treeEach(routes, (item, level) => {
|
||||
treeEach(appRoutes, item => {
|
||||
const { cache, name } = item.meta ?? {};
|
||||
if (cache && name) {
|
||||
menuStore.caches.push(name);
|
||||
}
|
||||
// if (item.path === menuStore.home) {
|
||||
// item.alias = '/';
|
||||
// }
|
||||
// if (!router.hasRoute(item.name!)) {
|
||||
// const route = { ...item, children: undefined } as any;
|
||||
// router.addRoute(route.parentName!, route);
|
||||
// }
|
||||
});
|
||||
|
||||
// 首页处理
|
||||
const home = treeFind(routes, i => i.path === menuStore.home);
|
||||
if (home) {
|
||||
const route = { ...home, name: APP_HOME_NAME, alias: '/' };
|
||||
router.removeRoute(home.name!);
|
||||
router.addRoute(APP_ROUTE_NAME, route);
|
||||
return router.replace(to.path);
|
||||
}
|
||||
return to.fullPath;
|
||||
}
|
||||
|
||||
// 兜底处理
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { createRouter } from 'vue-router';
|
|||
import { useAuthGuard } from '../guards/auth';
|
||||
import { useProgressGard } from '../guards/progress';
|
||||
import { useTitleGuard } from '../guards/title';
|
||||
import { baseRoutes } from '../routes/base';
|
||||
import { historyMode } from './util';
|
||||
import { routes } from '../routes/page';
|
||||
|
||||
|
|
@ -11,7 +10,7 @@ import { routes } from '../routes/page';
|
|||
*/
|
||||
export const router = createRouter({
|
||||
history: historyMode(),
|
||||
routes: [...baseRoutes, ...routes],
|
||||
routes: routes,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
export const APP_HOME_NAME = '__APP_HOME__';
|
||||
|
||||
/**
|
||||
* 基本路由
|
||||
*/
|
||||
export const baseRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: APP_HOME_NAME,
|
||||
component: () => 'Home Page',
|
||||
},
|
||||
];
|
||||
|
|
@ -15,8 +15,8 @@ function treeRoutes(list: RouteRecordRaw[]) {
|
|||
for (const item of list) {
|
||||
const parentPath = item.path.split('/').slice(0, -1).join('/');
|
||||
const parent = map[parentPath];
|
||||
item.parentName = (parent?.name as string) || APP_ROUTE_NAME;
|
||||
if (parent) {
|
||||
(item as any).parentPath = parentPath;
|
||||
(parent.children || (parent.children = [])).push(item);
|
||||
} else {
|
||||
tree.push(item);
|
||||
|
|
@ -47,12 +47,14 @@ function sortRoutes(routes: RouteRecordRaw[]) {
|
|||
const transformRoutes = (routes: RouteRecordRaw[]) => {
|
||||
const topRoutes: RouteRecordRaw[] = [];
|
||||
const appRoutes: RouteRecordRaw[] = [];
|
||||
let app: RouteRecordRaw;
|
||||
|
||||
for (const route of routes) {
|
||||
if ((route.name as string)?.startsWith(TOP_ROUTE_PREF)) {
|
||||
if (route.name === APP_ROUTE_NAME) {
|
||||
app = route;
|
||||
route.children = appRoutes;
|
||||
}
|
||||
if ((route.name as string)?.startsWith(TOP_ROUTE_PREF)) {
|
||||
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
||||
topRoutes.push(route);
|
||||
continue;
|
||||
|
|
@ -60,7 +62,8 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
|
|||
appRoutes.push(route);
|
||||
}
|
||||
|
||||
return [topRoutes, sortRoutes(treeRoutes(appRoutes))];
|
||||
app!.children = sortRoutes(treeRoutes(appRoutes));
|
||||
return [topRoutes, app!.children];
|
||||
};
|
||||
|
||||
export const [routes, appRoutes] = transformRoutes(generatedRoutes);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: "user",
|
||||
id: 'user',
|
||||
state: (): UserStore => {
|
||||
return {
|
||||
id: 0,
|
||||
username: "juetan",
|
||||
nickname: "绝弹",
|
||||
avatar: "https://github.com/juetan.png",
|
||||
accessToken: "",
|
||||
username: 'juetan',
|
||||
nickname: '绝弹',
|
||||
avatar: 'https://github.com/juetan.png',
|
||||
accessToken: '',
|
||||
refreshToken: undefined,
|
||||
auth: []
|
||||
auth: [],
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
|
|
@ -48,7 +48,10 @@ export const useUserStore = defineStore({
|
|||
accessToken && (this.accessToken = accessToken);
|
||||
},
|
||||
},
|
||||
persist: true,
|
||||
persist: {
|
||||
key: '__APP_USER__',
|
||||
paths: ['accessToken'],
|
||||
},
|
||||
});
|
||||
|
||||
export interface UserStore {
|
||||
|
|
@ -65,11 +68,11 @@ export interface UserStore {
|
|||
*/
|
||||
nickname: string;
|
||||
/**
|
||||
* 用户头像地址
|
||||
* 头像地址
|
||||
*/
|
||||
avatar?: string;
|
||||
/**
|
||||
* JWT令牌
|
||||
* 访问令牌
|
||||
*/
|
||||
accessToken?: string;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,30 @@
|
|||
import "vue-router";
|
||||
import 'vue-router';
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteRecordRaw {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
interface RouteRecordSingleViewWithChildren {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
interface RouteRecordSingleView {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
interface RouteRecordMultipleViews {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
interface RouteRecordMultipleViewsWithChildren {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
interface RouteRecordRedirect {
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
declare module "vue-router" {
|
||||
interface RouteMeta {
|
||||
/**
|
||||
* 页面标题
|
||||
|
|
|
|||
Loading…
Reference in New Issue