路由构建
介绍
Teek 在完成页面渲染的期间,进行了很多的逻辑处理,那么如果要想二次开发 Teek,则必须了解 Teek 的构建流程,这样才能根据需求切入源码,进行二次开发。
入口文件
Teek 的入口和所有的 Vue 项目一样,都是 main.ts
文件,这个文件引用并注册了全局的依赖:
- Router
- Pinia
- Directives
- I18n
- Element Plus
- Error Handler
- Icon 组件
- Auth 组件
- Role 组件
- ...
这些都是 Teek 的全局依赖,也是 Teek 运行的基础环境。
路由
在入口文件初始化后,就开启了路由功能,因此就会先走路由的前置拦截器 beforeEach,在 src/router/index.ts
里。
在 beforeEach 中,先引入了部分变量
// 用户相关信息
const userStore = useUserStore();
// 权限相关信息
const permissionStore = useRouteStore();
// 初始化动态路由函数
const { initDynamicRoutes } = useRouteFn();
// 认证 token
const accessToken = userStore.accessToken;
然后开启了 NProgress 进度条
NProgress.start();
接下来进行一系列的链接、白名单、accessToken 判断:
// 判断是访问登陆页,有 Token 就在当前页面,没有 Token 重置路由并放行到登陆页
if (to.path === "/login") {
if (accessToken) return next(from.fullPath);
resetRouter();
return next();
}
// 判断访问页面是否在路由白名单地址中,如果存在直接放行
if (whiteList.includes("*")) {
if (!permissionStore.loadedRouteList.length) {
// 初始化路由流程函数
await initDynamicRoutes(["*"]);
// 初始化结束后才放行
return next({ ...to, replace: true });
}
return next();
} else if (whiteList.includes("next") || whiteList.includes(to.path)) return next();
// 判断是否有 Token,没有重定向到 login
if (!accessToken) return next({ path: "/login", replace: true });
如果都通过了判断,代表权限已经满足访问条件,那么此时面临两个选择:
- 如果路由已经初始化,即已经进入了页面,因为此时是切换路由,则直接放行
- 如果路由没有初始化,则需要初始化路由,因此下面的流程都是进入 初始化路由流程 里进行分析
// 判断是否存在角色或加载过路由,如果不存在,则加载路由
if (!permissionStore.loadedRouteList.length) {
try {
// 获取用户信息
const userInfo = await userStore.getUserInfo();
// 初始化路由流程函数
await initDynamicRoutes(userInfo.roles);
// 初始化结束后才放行
return next({ ...to, replace: true });
} catch (error) {
// 出现异常,则清除权限,并跳转登录页面
userStore.clearPermission();
router.replace(LOGIN_URL);
return Promise.reject(error);
}
}
// 已经加载过路由,则直接放行
next();
最后在 afterEach
关闭进度条:
NProgress.done();
如果出现异常,则捕获并关闭进度条:
/**
* 路由跳转错误
**/
router.onError(error => {
NProgress.done();
console.warn("路由错误", error.message);
});
初始化路由流程
初始化路由流程是 src/router/index.ts
的 router.beforeEach
里触发 initDynamicRoutes
函数,因此走进 initDynamicRoutes
函数,位于 src/composables/core/use-route-fn.ts
文件下,这个文件是路由初始化、操作的核心 hooks 文件,里面包含项目路由操作的各个封装函数。
initDynamicRoutes
initDynamicRoutes
函数主要是对路由进行初始化的入口,接收两个参数:
- 第一个是角色,用来筛选权限路由
- 第二个参数是 api,如果传了 api,则调用 api 去后台获取动态路由
第一次进来该函数将面临两个选择:
- 如果缓存路由功能开启,则去 localstorage 里获取缓存的路由,如果存在则直接初始化路由
- 如果缓存不存在或者缓存功能不开启,则根据
VITE_ROUTE_ACCESS_MODE
的规则去路由文件/后台获取路由,然后再根据是否开启缓存功能进行缓存,最终初始化获取的路由
const initDynamicRoutes = async (roles?: string[], api?: BackendApi) => {
let routeList = getDynamicRoutesFromStorage();
if (!routeList) {
const isFrontendMode = import.meta.env.VITE_ROUTE_ACCESS_MODE === "frontend";
const isBackendMode = import.meta.env.VITE_ROUTE_ACCESS_MODE === "backend";
const isBothMode = import.meta.env.VITE_ROUTE_ACCESS_MODE === "both";
// 前端控制模式
if (isFrontendMode) routeList = rolesRoutes;
// 后端控制模式
else if (isBackendMode) routeList = api ? await getDynamicRoutesFromBackend(api) : [];
// 前后端控制模式
else if (isBothMode) routeList = [...rolesRoutes, ...(api ? await getDynamicRoutesFromBackend(api) : [])];
}
if (routeList?.length) {
// 缓存路由
if (cacheDynamicRoutes) cacheOperator.setDynamicRoutes(routeList);
// 加载路由
return loadDynamicRoutes(routeList, roles || []);
}
ElNotification.warning({
title: "无权限访问",
message: "当前账号无任何菜单权限,请联系系统管理员!",
duration: 3000,
});
userStore.clearPermission();
router.replace(LOGIN_URL);
};
/**
* 从浏览器 storage 缓存获取动态路由
*/
const getDynamicRoutesFromStorage = () => {
if (cacheDynamicRoutes) return cacheOperator.getDynamicRoutes();
};
/**
* 从后台接口获取动态路由
*/
const getDynamicRoutesFromBackend = async (api: BackendApi) => {
const routeList = await api();
if (!routeList) return [];
return routeList;
};
至于是去后台还是静态文件获取路由(VITE_ROUTE_ACCESS_MODE
配置选择),则取决于自己的业务,可以二选一,也可以都选,最终 Teek 只接收一个 router 数组,都选的话则会组装成一个数组。
如果是后台获取路由,且后台返回的路由规则和官方需要的不一致,则自行编写函数进行转换,如:
/**
* 从后台接口获取动态路由
*/
const getDynamicRoutesFromBackend = async (api: BackendApi) => {
const routeList = await api();
if (!routeList) return [];
return transformRoutes(routeList);
};
const transformRoutes = (routeList: RouterConfigRaw[]) => {
// ...
};
transformRoutes
这里给一个 Demo,但是项目里没有提供该函数,可以参考使用。
transformRoutes
函数处理后台返回的路由,当然如果你后台返回的路由规则和官方需要的一致,则无需该函数进行转换。
假设后台返回的路由形式是:
[
{
"menuUrl": "/components",
"menuCode": "Components",
"menuName": "组件",
"parentMenuCode": "", // 代表一级菜单
"imageIcon": "Opportunity",
"sel": 1
},
{
"menuUrl": "message",
"menuCode": "MessageDemo",
"pagePath": "/components/message/index",
"menuName": "消息组件",
"parentMenuCode": "Components", // 和上面的 menuCode 关联
"imageIcon": "StarFilled",
"sel": 2
}
]
则我们需要转换成:
{
"path": "/components",
"name": "Components",
"meta": { "title": "组件", "icon": "Opportunity", "rank": 1 },
"children": [
{
"path": "message",
"name": "MessageDemo",
"component": "/components/message/index",
"meta": { "title": "消息组件", "icon": "StarFilled", "rank": 2 }
}
]
}
那么 transformRoutes
函数的内容是:
const transformRoutes = (routerList: BackstageMenuList[], menuCode = "") => {
const dynamicRouterList: RouterConfigRaw[] = [];
routerList.forEach(item => {
if (item.parentMenuCode === menuCode) {
const children = transformRoutes(
routerList.filter(v => v.menuCode !== menuCode),
item.menuCode
);
const menu = {
path: item.menuUrl,
name: item.menuName,
component: item.pagePath,
meta: {
title: item.menuName,
icon: item.imageIcon,
rank: item.seq,
},
};
if (children.length) dynamicRouterList.push({ ...menu, children });
else dynamicRouterList.push({ ...menu });
}
});
return dynamicRouterList;
};
后台返回的路由规则 Teek 无法统一,所以 transformRoutes 提供转换方式也需要根据自己的场景进行修改。
loadDynamicRoutes
在 initDynamicRoutes
函数里,我们进行了缓存功能的操作、路由的获取,那么最终调用 loadDynamicRoutes
方法,将获取到的路由和角色权限传进去。
loadDynamicRoutes(routeList, roles || []);
loadDynamicRoutes
函数将执行动态加载路由,但是在执行动态加载路由的前面,会执行四个函数来进行路由的处理,最后将处理后的路由进行动态加载:
/**
* 加载动态路由
*
* @param routers 路由表信息
* @param roles 权限角色
* @param r 路由对象
*/
const loadDynamicRoutes = (routers: RouterConfigRaw[], roles: string[], r = router) => {
// 权限路由筛选
const rolesRoutes = filterOnlyRolesRoutes(routers, roles);
// 路由元数据处理
const resolveRouters = processDynamicRoutes(processRouteMeta(rolesRoutes));
// 传到 routeStore 持久化,并拿到扁平化的路由数组(所有二级以上的路由拍成一级,keep-alive 只支持到二级缓存(Layout 默认是一级,加起来就是二级))
const flatRouteList = routeStore.loadPermissionRoutes(resolveRouters as RouterConfig[]);
// ...
};
因此这四个函数是:
filterOnlyRolesRoutes
:根据传来的 roles 对路由进行角色路由筛选,筛选出只有该 roles 才能看到的路由processRouteMeta
:对路由的配置项进行处理,专门处理 meta 的配置项processDynamicRoutes
:对路由的元数据进行处理,如 redirect、name,如果开发者没有填写这些配置,则自动按照 Teek 规则生成loadPermissionRoutes
:将处理后的动态路由持久化到 Pinia,并返回一个扁平化的路由
提示
什么是扁平化路由,就是将所有二级及以上的路由拍成一级路由。
最后将扁平化的路由进行动态加载:
// ...
flatRouteList.forEach(flatRoute => {
// 解除响应式
const item = { ...flatRoute };
// 防止加载 children 而不加载提取出来变成的一级路由
if (item.children) item.children = [];
// 加载动态路由时,子路由的 path 可能不带 /,这样会依赖父路由来拼接(vue-route 规则),但是 template 实现多级路由缓存,所以都拍成二级路由,则 path 加载时要求是完整的,不再放到父路由的 children 里
item.path = item.meta._fullPath || item.path;
if (!item.name || !r.hasRoute(item.name)) {
// 当重置路由后,静态路由也需要二次添加
if (item.meta.isFull || item.name === LAYOUT_NAME) r.addRoute(item as RouteRecordRaw);
else r.addRoute(LAYOUT_NAME, item as RouteRecordRaw);
}
});
// 最后添加 notFoundRouter
if (!r.hasRoute(notFoundRouter.name)) router.addRoute(notFoundRouter as RouteRecordRaw);
动态加载路由有两个选择:
- 如果路由已经存在则不加载
- 如果路由不存在:
- 如果路由配置了全屏配置项
isFull: true
,则将路由作为一级路由进行注册 - 如果路由没有配置全屏配置项
isFull: true
,则将路由加载到 Layout 路由下,也就是 Layout 是一级路由,其他路由都为二级路由、三级路由
- 如果路由配置了全屏配置项
提示
Layout 路由是布局路由,顶部栏、标签栏、菜单栏等都是布局内容,二级路由则在 page-content
内容区里。
最后添加错误路由,这是 404 路由。
filterOnlyRolesRoutes
filterOnlyRolesRoutes
函数是来过滤出当前系统角色的路由权限,因此需要接收两个参数,第一个参数是路由表,第二个参数是 roles 角色。
过滤出权限路由,取决于路由表的 meta.roles
配置,如果配置了 meta.roles
,则与第二个参数 roles 比对,如果存在则通过,反之则去掉。如果没有配置 meta.roles
,则默认通过。
因为路由表存在多级路由,所以采用递归形式遍历每个路由。
/**
* 过滤出当前系统角色的路由权限
*
* @param routers 路由表
* @param roles 权限角色
*/
const filterOnlyRolesRoutes = (routers: RouterConfigRaw[], roles: string[]) => {
const rolesRoutes: RouterConfigRaw[] = [];
routers.forEach(router => {
const r = { ...router };
if (hasPermission(r, roles)) {
if (r.children && r.children.length) r.children = filterOnlyRolesRoutes(r.children, roles);
rolesRoutes.push(r);
}
});
return rolesRoutes;
};
/**
* 该系统角色是否有权限访问当前路由
* roles 带有 * 的代表所有路由都能访问
*/
const hasPermission = (router: RouterConfigRaw, roles: string[]) => {
if (roles.includes("*")) return true;
if (router.meta?.roles) return roles.some(role => router.meta?.roles?.includes(role));
return true;
};
最终返回 meta.roles
存在第二个参数 roles 的路由或者没有配置 meta.roles
的路由。
如:
const permissionRoutes = {
path: "/role",
component: () => import("@/views/permission/rolePermission.vue"),
name: "RolePermission",
meta: {
title: "权限编辑",
roles: ["admin"],
},
};
当第二个参数 roles
也存在 admin
的时候,则该路由允许加载到 Teek 里。
processRouteMeta
processRouteMeta
函数处理路由的 meta:拼接每个路由的完整路径 fullPath,处理国际化 title 显示,判断是否使用国际化等等。
该函数是专门处理 meta 配置项,这些配置项是 Teek 项目需要,并非官方的配置。
/**
* 处理路由的 meta:拼接每个路由的完整路径 fullPath,处理国际化 title 显示,判断是否使用国际化
*
* @param routers 路由表
* @param basePath 路由 fullPath
*/
const processRouteMeta = (routers: RouterConfigRaw[], basePath = "/") => {
routers.forEach(route => {
const fullPath = route.path.startsWith("/") ? route.path : (basePath + "/" + route.path).replace(/\/+/g, "/");
// 处理成后面布局要用到的 title。title 如果为函数,则只能访问时处理,这里无法处理
if (route.meta) {
const { title, useI18n, isKeepAlive, isFull, useTooltip } = route.meta;
const {
routeUseI18n,
isKeepAlive: isKeepAliveGlobal,
isFull: isFullGlobal,
routeUseTooltip,
} = SystemConfig.routerConfig;
route.meta._fullPath = fullPath;
// 这两个顺序不能互换,因为 translateTitle 函数需要 route.meta.useI18n
if (useI18n === undefined && routeUseI18n !== undefined) route.meta.useI18n = routeUseI18n;
if (!isFunction(title)) {
route.meta.title = translateTitle(String(title), route.name as string, route.meta.useI18n);
}
if (isKeepAlive === undefined && isKeepAliveGlobal !== undefined) route.meta.isKeepAlive = isKeepAliveGlobal;
if (isFull === undefined && isFullGlobal !== undefined) route.meta.isFull = isFullGlobal;
if (useTooltip === undefined && routeUseTooltip !== undefined) route.meta.useTooltip = routeUseTooltip;
}
if (route.children && route.children.length) {
if (isValidURL(fullPath)) route.children = processRouteMeta(route.children, "");
else route.children = processRouteMeta(route.children, fullPath);
}
});
return routers;
};
该函数将会和 src/common/config/service/base-config
的全局配置进行比较,这些全局配置包括了一些路由的配置,因此有两种情况发生:
- 如果 meta 没有使用某个配置项,但是
base-config
全局使用了该配置项,则将base-config
的配置项放到 meta 里 - 如果 meta 使用了某个配置项,则不管
base-config
文件有没有配置,都以 meta 存在的配置项为主
除了和 base-config
文件配置项进行比较,该函数还处理一个非常重要的配置项:_fullPath
。
_fullPath
记录当前路由的完整路径,如:
const tableRoutes: RouterConfigRaw = {
path: "/table",
name: "Table",
meta: { title: "表格" },
children: [
{
path: "drag-table",
name: "DragTable",
component: () => import("@/views/table/dragTable/index.vue"),
meta: { title: "表格拖拽" },
},
],
};
那么子级路由的完整路径应该是 /table/drag-table
,即拼上父级的 path,这是 Route 官方规则。
因此开发者在项目开发的过程,可以使用 route.meta._fullPath
来获取该路由的完整路径。
如果子级路由的 path
带有 /
开头,则 Teek 不会拼接父级路由的 path,因为带有 /
的 path,表示该路由直接访问,这是 Route 官方规则。
processDynamicRoutes
processDynamicRoutes
函数用来过滤动态路由,重新生成规范路由,这里的规范路由是指帮助开发者自动生成没有使用的路由官方配置项。
/**
* 过滤动态路由,重新生成规范路由
*
* @param routers 路由
*/
const processDynamicRoutes = (routers: RouterConfigRaw[]) => {
if (!routers || !routers.length) return [];
routers.forEach(r => {
// 将 dynamic 属性加入 meta,标识此路由为动态路由
if (r.meta) r.meta._dynamic = true;
if (r.children?.length) {
// 父级的 redirect 属性取值:如果子级存在且父级的 redirect 属性不存在,默认取第一个子级的 path;如果子级存在且父级的 redirect 属性存在,取存在的 redirect 属性,会覆盖默认值
if (!r.redirect) r.redirect = (r.children[0].meta?._fullPath as string) || r.children[0].path;
// 父级的 name 属性取值:如果子级存在且父级的 name 属性不存在,默认取第一个子级的 name;如果子级存在且父级的 name 属性存在,取存在的 name 属性,会覆盖默认值(注意:测试中发现父级的 name 不能和子级 name 重复,如果重复会造成重定向无效(跳转 404),所以这里给父级的 name 起名的时候后面会自动加上 `Parent`,避免重复)
if (!r.name && r.children[0].name) r.name = (r.children[0].name as string) + "Parent";
}
if (r.meta?.iframeOpen && r.meta.iframeSrc && isValidURL(r.meta.iframeSrc)) r.path = r.meta.iframeSrc;
else {
if (r.meta?.iframeKeepAlive) r.component = IFrameBlank;
else if (!r.meta?.iframeKeepAlive && r.meta?.iframeSrc) r.component = IFrameView;
else {
// 如果动态路由的 component 存在且为 string,则必须是 views 下的目录,以 / 分割,如果/home/index,则是 views/home/index.vue 组件,如果不存在 component,则读取 path 来获取 component
if (r.component) {
if (isType(r.component) === "string") r.component = modules["/src/views" + r.component + ".vue"];
} else r.component = modules["/src/views" + r.path + ".vue"];
}
}
if (r.children?.length) processDynamicRoutes(r.children);
});
return routers;
};
- name:如果有二级路由,且一级路由没有填写
name
,则该函数自动在已经路由生成name
,name
值是二级路由的第一个 children 的name + Parent
- redirect:如果有二级路由,且一级路由没有填写
redirect
,则该函数自动在一级路由生成redirect
,指向二级路由的第一个 children 的完整路径
如:
const tableRoutes: RouterConfigRaw = {
path: "/table",
meta: { title: "表格" },
children: [
{
path: "drag-table",
name: "DragTable",
component: () => import("@/views/table/dragTable/index.vue"),
meta: { title: "表格拖拽" },
},
],
};
则最终生成:
const tableRoutes: RouterConfigRaw = {
path: "/table",
name: "DragTableParent"
redirect: "/table/drag-table",
meta: { title: "表格" },
// children ...
};
该函数还有一个主要的功能,如果你看过 路由,就知道路由有三种方式读取组件:
- 官方形式:
component: () => import("@/views/xx/index.vue")
- 字符串形式:
component: () => xx/index
- path 形式:
path: () => xx/index
后两种形式是该函数处理后变成官方形式的:
// 如果动态路由的 component 存在且为 string,则必须是 views 下的目录,以 / 分割,如果/home/index,则是 views/home/index.vue 组件,如果不存在 component,则读取 path 来获取 component
if (r.component) {
if (isType(r.component) === "string") r.component = modules["/src/views" + r.component + ".vue"];
} else r.component = modules["/src/views" + r.path + ".vue"];
loadPermissionRoutes
loadPermissionRoutes 函数不在 use-route-fn.ts
文件里,而是在 src/pinia/stores/core/route.ts
文件里。
export const useRouteStore = defineStore("routeStore", () => {
const loadedRouteList = ref<RouterConfig[]>([]);
const flatRouteList = ref<RouterConfig[]>([]);
const { processRouteMeta, findRouteByName, filterFlatRoutes, ascending } = useRouteFn();
// 路由里首页的 name 值,必须填且正确,默认为 Home
const homeRoute = computed(() => findRouteByName(loadedRouteList.value, HOME_NAME));
/**
* 加载权限路由
*
* @param routers 路由配置
*/
const loadPermissionRoutes = (routers: RouterConfig[]) => {
loadedRouteList.value = ascending(
processRouteMeta(staticRoutes).concat(routers).concat(errorRouter).concat(notFoundRouter)
);
flatRouteList.value = filterFlatRoutes(
processRouteMeta(staticRoutes).concat(routers).concat(errorRouter).concat(notFoundRouter) as RouterConfig[]
);
return flatRouteList.value;
};
return {
loadedRouteList,
homeRoute,
flatRouteList,
loadPermissionRoutes,
};
});
将上面最终处理完的路由传入 loadPermissionRoutes
,让该函数先将静态路由和传过来的路径进行处理:
- processRouteMeta 处理 staticRoutes 静态路由的
meta
,该函数在src/composables/core/use-routes-fn.ts
文件下 ascending
处理排序路由的顺序,取决于meta.rank
的数字大小,数字越小越靠前,该函数在src/composables/core/use-routes-fn.ts
文件下- 最后赋值给
loadedRouteList
,这样 Teek 随时使用loadedRouteList
来获取最终需要的路由表信息
ascending
函数按照路由中 meta 的 rank
等级升序来排序路由(仅处理以及一级路由):
/**
* 按照路由中 meta 下的 rank 等级升序来排序路由(仅处理以及一级路由)
*
* @param routeList 路由表
*/
const ascending = (routeList: any[]) => {
routeList.forEach((r, index) => {
if (!r.meta) r.meta = {};
// 当 rank 不存在时,根据顺序自动创建,首页路由永远在第一位
if (r.name === HOME_NAME && !r.meta?.rank) r.meta.rank = 0;
else if (handRank(r)) r.meta.rank = index + 5;
});
return routeList.sort((a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
return a?.meta.rank - b?.meta.rank;
});
};
/**
* 过滤不需要的排序的路由
*
* @param route 路由
*/
const handRank = (route: RouterConfigRaw) => {
const { name, path, meta } = route;
if (!meta?.rank || (meta?.rank === 0 && name !== HOME_NAME && path !== "/")) return true;
return false;
};
filterFlatRoutes
是处理路由扁平化的函数
/**
* 扁平化数组对象,将多级嵌套路由处理成一维数组(主要用来处理路由菜单)
*
* @param routeList 所有路由表
*/
const filterFlatRoutes = (routeList: RouterConfig[]) => {
return routeList.reduce((pre: RouterConfig[], current: RouterConfig) => {
let flatArr = [...pre, current];
if (current.children) flatArr = [...flatArr, ...filterFlatRoutes(current.children)];
return flatArr;
}, []);
};
findRouteByName
在路由初始化的时候,Teek 需要获取首页的路由,这在面包屑功能需要,因此在 src/pinia/stores/core/route.ts
文件有这段代码:
import { HOME_NAME } from "@/common/config";
// 路由里首页的 name 值,必须填且正确,默认为 Home
const homeRoute = computed(() => findRouteByName(loadedRouteList.value, HOME_NAME));
HOME_NAME 是一个变量,确保这里和首页路由使用的是这个变量,这样已修改 HOME_NAME
的值,则首页都能扫描到。
findRouteByName
是一个路由工具函数,功能是查找 name
对应的路由信息,在 src/composables/core/use-routes-fn.ts
文件下:
/**
* 查找 name 对应的路由信息
*
* @param routes 路由表
* @param name 查找的 name
*/
const findRouteByName = (routes: RouterConfigRaw[], name: string): RouterConfigRaw | null => {
let res = routes.find(item => item.name === name) || null;
if (res) return isProxy(res) ? toRaw(res) : res;
else {
for (let i = 0; i < routes.length; i++) {
if (routes[i].children instanceof Array && routes[i].children?.length) {
res = findRouteByName(routes[i].children || [], name);
if (res) return isProxy(res) ? toRaw(res) : res;
}
}
return null;
}
};
总结
如果你没有二次开发的想法,可以不需要了解整个初始化流程,上面的流程并不需要你去做任何操作,你只需要和正常开发一样,在 view
下面写业务组件,然后里配置该路由,就可以在页面看到效果了,上面的流程在配好路由自动执行。
大致总结下上面的流程,其中第一条是你需要操作,其他都是自动执行:
- 写好自己的路由表(开发人员需要自己操作)
- 路由
beforeEach
执行初始化流程initDynamicRoutes
函数 - 判断是否有缓存,有则读取缓存的路由执行初始化
- 初始化之前先进行路由权限的扫描,在
filterOnlyRolesRoutes
函数中执行,只获取满足roles
的路由,其他路由去掉 - 处理路由的
meta
配置项,在processRouteMeta
函数中执行 - 处理路由的元数据,在
processDynamicRoutes
函数中执行 - 存储到
Pinia
,并处理成扁平化路由,在loadPermissionRoutes
函数中执行 - 将扁平化路由初始化加载
addRoute
到Layout
布局路由下作为二级路由,如果存在meta.isFull
,则addRoute
到一级路由下,最后并加载404
路由