index.ts 整体分析
在 main.ts
文件中使用 import router from './router'
,这会导入并执行 ./router/index.ts
文件中的所有顶级代码。现在让我们详细分析 ./router/index.ts
的内容。
-
导入模块和依赖:
这些模块和依赖会在文件的顶部被导入,但不会执行其中的逻辑,除非它们本身在顶级代码中有执行逻辑。
1import { createRouter, createWebHashHistory } from 'vue-router'; 2import NProgress from 'nprogress'; 3...
-
执行的顶级代码:
const storesThemeConfig = useThemeConfig(pinia);
:调用useThemeConfig
函数并传入pinia
。const { themeConfig } = storeToRefs(storesThemeConfig);
:调用storeToRefs
函数。const { isRequestRoutes } = themeConfig.value;
:从themeConfig
获取isRequestRoutes
属性。
-
创建路由实例:
使用
createRouter
创建一个新的路由实例,这个实例使用了createWebHashHistory
和staticRoutes
作为路由配置。1export const router = createRouter({ 2 history: createWebHashHistory(), 3 routes: staticRoutes, 4});
-
定义几个辅助函数:
formatFlatteningRoutes(arr: any)
formatTwoStageRoutes(arr: any)
这些函数在此时并不会执行,只是被定义。
-
路由钩子:
在此处定义了两个路由钩子,
router.beforeEach
和router.afterEach
。当路由发生变化时,这些钩子函数会被调用。1router.beforeEach(async (to, from, next) => {...}); 2router.afterEach(() => {...});
-
导出路由实例:
文件的底部导出了
router
实例,这样其他文件可以导入并使用它。1export default router;
总结:
当你导入 ./router/index.ts
,这将执行文件中的所有顶级代码,包括导入的模块/依赖、创建路由实例以及定义的路由钩子。此外,还定义了几个辅助函数和导出了路由实例供其他文件使用。
router.beforeEach和router.afterEach 传入的回调函数分别在什么时候执行
在Vue Router中,router.beforeEach
和 router.afterEach
是两个非常常用的全局导航守卫(Global Guards)。它们允许你定义在路由切换之前或之后要执行的行为。具体来说:
-
router.beforeEach:
beforeEach
是一个全局前置守卫。- 当一个导航触发时,全局前置守卫是首先被调用的。
- 这个守卫的回调函数接收三个参数:
to
,from
, 和next
。 - 在导航确认之前,这个回调会在所有守卫中被调用。
- 通过调用
next
函数来解析这个守卫。执行next()
将会继续导航到目标路由,next(false)
会中断当前的导航,如果传入一个新的路由地址,例如next('/new-path')
,将会重定向到一个不同的地址。 - 这个守卫常常被用来检查用户的登录状态,进行身份验证或者实现一些前置条件检查。
-
router.afterEach:
afterEach
是一个全局后置守卫。- 与
beforeEach
不同的是,它不接受next
函数来改变导航的行为。这是因为它被调用的时候,确认导航已经完成。 - 它的回调函数接收两个参数:
to
和from
。 - 这个守卫没有终止或更改导航的能力,它只是一个“后处理”步骤,常常被用来执行如结束进度条、分析页面或设置页面标题等后置操作。
为 router.beforeEach
和 router.afterEach
函数中的每一行代码添加注释:
1// 定义全局前置守卫,当一个导航触发时首先执行
2router.beforeEach(async (to, from, next) => {
3 // 配置进度条不显示旋转的动画
4 NProgress.configure({ showSpinner: false });
5 // 如果即将进入的路由对象的元数据中有 title,那么开始显示进度条
6 if (to.meta.title) NProgress.start();
7 // 从 session 存储中获取 token
8 const token = Session.get('token');
9 // 检查即将进入的路由是否是登录页并且没有 token
10 if (to.path === '/login' && !token) {
11 // 允许导航到目标路由
12 next();
13 // 结束并隐藏进度条
14 NProgress.done();
15 } else { // 开始处理其他的路由导航情况
16 // 如果没有 token
17 if (!token) {
18 // 重定向到登录页面,并在 URL 中附带重定向的参数以及相关的查询参数或路由参数
19 next(`/login?redirect=${to.path}¶ms=${JSON.stringify(to.query ? to.query : to.params)}`);
20 // 清除 session 存储
21 Session.clear();
22 // 结束并隐藏进度条
23 NProgress.done();
24 } else if (token && to.path === '/login') { // 如果有 token 并且即将进入的路由是登录页面
25 // 重定向到主页
26 next('/home');
27 // 结束并隐藏进度条
28 NProgress.done();
29 } else { // 开始处理其他情况
30 // 从 pinia 存储中获取路由列表
31 const storesRoutesList = useRoutesList(pinia);
32 // 从存储中解构获取路由列表
33 const { routesList } = storeToRefs(storesRoutesList);
34 // 检查路由列表是否为空
35 if (routesList.value.length === 0) {
36 // 如果配置中设置为从后端请求路由
37 if (isRequestRoutes) {
38 // 初始化从后端控制的路由
39 await initBackEndControlRoutes();
40 // 允许导航到目标路由,并替换当前的历史记录
41 next({ ...to, replace: true });
42 } else { // 如果不是从后端请求路由
43 // 初始化前端控制的路由
44 await initFrontEndControlRoutes();
45 // 允许导航到目标路由,并替换当前的历史记录
46 next({ ...to, replace: true });
47 }
48 } else { // 如果路由列表不为空
49 // 允许导航到目标路由
50 next();
51 }
52 }
53 }
54});
55
56// 定义全局后置守卫,当导航被确认之后执行
57router.afterEach(() => {
58 // 结束并隐藏进度条
59 NProgress.done();
60});
在 router.beforeEach
中,有一个检查逻辑来确定用户是否已登录(通过检查是否存在 token
)。如果用户未登录(即 token
不存在),那么用户将被重定向到 /login
页面,并带有两个查询参数:redirect
和 params
。redirect
参数用于存储原始目标路由的路径,而 params
参数存储原始路由的查询参数或路径参数。
这意味着:
- 当用户首次访问应用程序并尝试访问任何受保护的页面(例如主页)时,由于缺少
token
,他们将被重定向到登录页面。 - 一旦用户成功登录并获得
token
,他们可以再次尝试访问之前尝试访问的页面(通过redirect
和params
参数)。
所以,如果用户首次打开应用程序并尝试访问主页,但未登录,他们会看到登录页面而不是主页。只有在成功登录后,他们才能看到主页或其他受保护的页面。
总结:
router.beforeEach
: 在路由切换之前执行,可以用来进行身份验证或其他前置条件检查,并有能力修改或中断导航。router.afterEach
: 在路由切换之后执行,用来执行如进度条结束、页面分析或设置页面标题等后续操作。
formatFlatteningRoutes 与 formatTwoStageRoutes
代码分析
当然可以,让我们逐个分析这两个函数:
1. formatFlatteningRoutes
这个函数的主要目的是将一个多级嵌套的路由数组处理成一个一维数组。
1export function formatFlatteningRoutes(arr: any) {
2 if (arr.length <= 0) return false;
3 for (let i = 0; i < arr.length; i++) {
4 if (arr[i].children) {
5 arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
6 }
7 }
8 return arr;
9}
-
if (arr.length <= 0) return false;: 如果传入的数组是空的,函数直接返回
false
。 -
for (let i = 0; i < arr.length; i++): 遍历数组中的每一个路由项。
-
if (arr[i].children): 如果当前路由项有子路由(
children
属性)…- arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));: 这行代码做了以下事情:
- 取数组的前
i+1
个元素。 - 将这些元素与当前路由项的子路由合并。
- 再将结果与数组的剩余部分合并。
- 取数组的前
- arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));: 这行代码做了以下事情:
最终,arr
会变成一个一维数组,其中包含了所有路由项(不再有嵌套的children
属性)。
2. formatTwoStageRoutes
此函数的目标是将一个一维数组处理成只有两级的嵌套数组(即,如果原数组中有超过两级的路由,它们会被处理成只有两级)。
1export function formatTwoStageRoutes(arr: any) {
2 // ...
3}
-
if (arr.length <= 0) return false;: 同上,如果传入数组是空的,直接返回
false
。 -
const newArr: any = [];: 初始化一个空数组,用于存储处理后的嵌套路由。
-
arr.forEach((v: any) => {…}): 遍历一维数组的每一个路由项。
-
if (v.path === ‘/’): 如果当前路由项的路径是根路径,将其添加到
newArr
中,但确保其children
属性是一个空数组。 -
else: 对于非根路径的路由项…
-
判断它是否是动态路由(如
xx/:id/:name
)。如果是,为其meta
属性添加isDynamic
和isDynamicPath
属性。 -
将该路由项添加到
newArr[0].children
中(即,它变成了顶级路由项的子路由)。 -
如果需要缓存该路由项(根据其
meta
属性的isKeepAlive
值),将其name
属性添加到cacheList
中,并更新存储器。
-
-
最终,newArr
将是一个只有两级的嵌套路由数组。
总之,这两个函数共同完成了将多级嵌套的路由数组处理成只有两级的嵌套数组的工作。
数据运行例子
我们以你提供的路由JSON为例,来描述调用formatFlatteningRoutes
和formatTwoStageRoutes
时的输入输出及中间关键步骤。
1. formatFlatteningRoutes
输入: 你提供的多级嵌套路由JSON,例如:
1[
2 {
3 "path": "/system/dict",
4 "children": [
5 { "path": "/system/dict/type/list" },
6 { "path": "/system/dict/data/list/:dictType" }
7 ]
8 },
9 ...
10]
关键步骤:
- 对于每一个路由项,检查它是否有
children
属性。 - 如果有,将这些子路由放到一维数组中的当前位置,然后继续处理其它路由。
输出: 一个扁平化的一维数组,例如:
1[
2 { "path": "/system/dict" },
3 { "path": "/system/dict/type/list" },
4 { "path": "/system/dict/data/list/:dictType" },
5 ...
6]
2. formatTwoStageRoutes
输入: 上一步输出的扁平化的一维数组。
关键步骤:
- 遍历每一个路由项。
- 对于根路径(例如
/
),创建一个顶级路由项并为其初始化一个空的children
数组。 - 对于非根路径的路由项,将其作为顶级路由项的子路由添加到
children
数组中。
输出: 一个只有两级的嵌套数组,例如:
1[
2 {
3 "path": "/",
4 "children": [
5 { "path": "/system/dict" },
6 { "path": "/system/dict/type/list" },
7 { "path": "/system/dict/data/list/:dictType" },
8 ...
9 ]
10 }
11]
这样,无论原始的路由结构有多少级嵌套,经过这两个函数的处理后,它都会被转换为只有两级的嵌套结构。
arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
这行代码的目的是将多级嵌套的数组转换为一个扁平的一维数组。让我们仔细分析这行代码。
首先,考虑以下嵌套数组作为例子:
1[
2 { "path": "/A" },
3 {
4 "path": "/B",
5 "children": [
6 { "path": "/B1" },
7 { "path": "/B2" }
8 ]
9 },
10 { "path": "/C" }
11]
假设我们当前正在处理路径为/B
的项,它的索引i
为1,并且它有子路由。
arr.slice(0, i + 1) 这部分会获取从数组开始到当前项(包括当前项)的所有元素。对于上面的例子,结果为:
1[
2 { "path": "/A" },
3 {
4 "path": "/B",
5 "children": [
6 { "path": "/B1" },
7 { "path": "/B2" }
8 ]
9 }
10]
arr[i].children
这是当前项的children
属性,即子路由。对于上面的例子,结果为:
1[
2 { "path": "/B1" },
3 { "path": "/B2" }
4]
arr.slice(i + 1) 这部分会获取从当前项之后到数组末尾的所有元素。对于上面的例子,结果为:
1[
2 { "path": "/C" }
3]
现在,使用concat
函数将这三部分合并:
1[
2 { "path": "/A" },
3 {
4 "path": "/B",
5 "children": [
6 { "path": "/B1" },
7 { "path": "/B2" }
8 ]
9 },
10 { "path": "/B1" },
11 { "path": "/B2" },
12 { "path": "/C" }
13]
通过这种方式,/B1
和/B2
被添加到数组中,而原始的/B
项保持不变。下一次循环时,由于/B
项已经处理过,所以它的子项/B1
和/B2
将被处理,但因为它们没有子路由,所以数组不会发生变化。
这个过程将继续,直到整个数组被处理完,结果是一个扁平的一维数组。