完善路由功能路由

在线接口文档


大纲链接 §

[toc]


动态路由'/edit/:blogId'

  • 在请求URL 为 '/edit/:blogId''/detail/:blogId''/user/:userId'、时需要额外传入动态URL信息
  • '/detail/:blogId' 博客详情,需要blogId信息
  • '/edit/:blogId' 博客编辑,需要blogId信息
  • '/user/:userId' 博客编辑,需要userId信息
  • 其他比如创建'/create'就无需:blogId

vue-router 动态路由匹配


添加路由元信息

如何将任意信息附加到路由上,例如:过渡名称、谁可以访问路由等

  • meta字段可以用来附加 是否需要访问权限,使得只有经过身份验证的用户才能访问该路由
    • 否则就重定向到登录页面/login
    • 并且在地址栏中增加重定向信息/login?redirect=/myblog,登录后URL仍可以定向到之前希望访问的页面
  • 在路由列表routes中需要身份认证的路由对象上,添加meta字段来实现:
    • 在路由配置中添加meta字段
    • meta: { requiresAuth: true },
    • 在路由全局前置守卫中设置判断条件
    • 访问需要身份认证的 URL to.matched.some(record => (record.meta.requiresAuth))
    • 查找配置列表中meta字段的requiresAuth属性
  • 可以在路由地址和导航守卫上都被访问到,配合导航守卫钩子,处理身份认证的逻辑

@/router/index.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import {createRouter, /*createWebHistory*/ createWebHashHistory, RouteRecordRaw} from 'vue-router';
import useStore from '@/store';
import useAuthStore from '@/store/modules/auth';
import {storeToRefs} from 'pinia';

/* 动态加载组件会报警告 component: () => import('@/pages/blog/index/BlogIndex')  改为静态导入 */
// components
import BlogIndex from '@/pages/blog/index/BlogIndex';
import Login from '@/pages/login/Login';
import Register from '@/pages/register/Register';
import CreateBlog from '@/pages/blog/create/CreateBlog';
import EditBlog from '@/pages/blog/edit/EditBlog';
import BlogDetail from '@/pages/blog/detail/BlogDetail';
import User from '@/pages/user/User';
import MyBlog from '@/pages/myBlog/MyBlog';
import About from '@/pages/About';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'BlogIndex',
    // import('') 必须是静态字符串,不可动态拼接
    // The above dynamic import cannot be analyzed by vite
    component: BlogIndex, // 注意这里如果是.vue文件必须要带上 文件后缀.vue
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
  },
  {
    path: '/register',
    name: 'Register',
    component: Register,
  },
  {
    path: '/create',
    name: 'CreateBlog',
    component: CreateBlog,
    // 只有经过身份验证的用户才能创建帖子
    meta: {requiresAuth: true},
  },
  {
    path: '/edit/:blogId',
    name: 'EditBlog',
    component: EditBlog,
    meta: {requiresAuth: true},
  },
  {
    path: '/detail/:blogId',
    name: 'BlogDetail',
    component: BlogDetail,
  },
  {
    path: '/user/:blogId',
    name: 'User',
    component: User,
    meta: {requiresAuth: true},
  },
  {
    path: '/myblog',
    name: 'MyBlog',
    component: MyBlog,
    meta: {requiresAuth: true},
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: About
  }
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

export default router;

  • to.matched.some(record => { return record.meta.requiresAuth; }) 遍历匹配, 每一个需要身份认证的 URL
  • 编程式导航 router.push('/users/posva#bio')
  • 标准化的路由地址
  • 匹配的组件可以从 router.currentRoute.value.matched 中获取

路由重定向

处于未登录状态时,访问需要身份验证(meta: {requiresAuth: true},)的 URL,如何设置 登录后重定向回该 URL

  • 在全局路由守卫中设置回调
  • 取得store中存储的登录信息(使用 pinia 作为状态管理库代替vuex)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...
// 路由全局前置守卫
router.beforeEach((to, from, next) => {
  // 匹配路由元信息
  // 判断是需要登录
  const {getIsLogin,} = storeToRefs(useAuthStore());

  if (to.matched.some(record => (record.meta.requiresAuth))) {
  
    if (!getIsLogin.value) {
    // 未登录
      next(...);
    } else {
    // 已登录
      next();
    }
  } else {
  // 访问无需身份认证的 URL
    next();
  }

});

// 路由全局后置守卫
router.afterEach((/* to, from, failure */) => {
  // console.log('路由全局后置守卫', to, from);
});
...

举例:

  • 在登录组件Login.tsxonLogin方法中
    • 获取当前页面URL的重定向属性值router.currentRoute.value.query.redirect
    • routerconst router = useRouter();定义
    • 调用接口的方法authStore.login(logString).then()
    • return router.push({path: (router.currentRoute.value.query.redirect as string) || '/' /* 首页保底 */});
    • 成功,跳转重定向页面 或者 首页 作为保底
  • 回顾在路由全局前置钩子中设置:
    • 判断是需要登录,即匹配路由元信息
    • 访问需要身份认证的 URL
      • to.matched.some(record => (record.meta.requiresAuth))\
        • 判断 store 的登录状态
          • const {getIsLogin,} = storeToRefs(useAuthStore());
        • 未登录!getIsLogin.value
          • 执行重定向到登录页面:next({ path: '/login', query: {redirect: to.fullPath} });
          • 保留原重定向路径query: {redirect: to.fullPath},设置到/login
        • 已登录,直接next()
    • 访问无需身份认证的 URL
      • 直接next()
  • 组件中使用router.currentRoute.value.query.redirect获取查询参数中的重定向字段

路由重定向的bug

处于已登录状态时,在编辑博客页面刷新,仍会跳转至登录页面

  • 原因是的全局路由钩子中没有向服务器验证当前登录状态 store.checkLogin()
  • 全局路由钩子中直接获取store中的登录状态的时机出错,此时BlogHeader刚发出请求,还未更新store中的登录状态信息
  • 而是直接使用了store中的登录状态来判断,即store.getIsLogin
  • 依据当前登录状态来判断是否需要重定向到登录页面

Login.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import {defineComponent, ref} from 'vue';
import {useRouter} from 'vue-router';
import useAuthStore from '@/store/modules/auth';
import {logString} from '@/store/modules/auth/interface';
import cssAuth from '@/styles/auth.module.scss';
import UserInput from '@/components/user-authentication/UserInput';
import UserSubmitBtnTip from '@/components/user-authentication/UserSubmitBtnTip';
import useIdentifyCompName from '@/hooks/useIdentifyCompName';

const LoginProps = {
  // onHandleSubmit: Function as PropType<() => void>,
};

export default defineComponent({
  name: 'Login',
  props: LoginProps,
  components: {},
  setup(/*props, ctx*/) {
    useIdentifyCompName();
    const authStore = useAuthStore();
    const router = useRouter();
    const username = ref('');
    const password = ref('');

    const userLoginInfo = computed(() => {
      return {
        username: username.value,
        password: password.value
      };
    });

    // resolve => router.push
    const onLogin = (logString: logString) => {
      authStore.login(logString)
        .then(() => {
          // 成功,跳转重定向页面 或者 首页 作为保底
          return router.push({path: (router.currentRoute.value.query.redirect as string) || '/'  /* 首页保底 */});
        }, /* reject */);
    };

    // watch keyup Enter
    const keyUpHandler = (e: KeyboardEvent) => {
      ;['Enter'].includes(e.key) && onLogin({
        username: username.value,
        password: password.value
      });
    };

    const clickHandler = (logString: logString) => {
      onLogin(logString);
    };

    return {
      username,
      password,
      userLoginInfo,
      onLogin,
      keyUpHandler,
      clickHandler
    };
  },
  render() {
    return (
      <section class={cssAuth.login}>
        <UserInput title="用户名"
                   errorText="当前用户名已注册"
                   v-model={[this.username, 'username']}/>

        <UserInput title="密码"
                   inputType="password"
                   errorText="当前用户名或密码不匹配"
                   v-model={[this.password, 'password']}
                   onKeyUp={this.keyUpHandler}/>

        <UserSubmitBtnTip btnName="立即登录"
                          tipText="没有账号?"
                          linkTo="/register"
                          linkText="注册新用户"
                          onHandleSubmit={() => {this.clickHandler(this.userLoginInfo);}}/>
      </section>
    );
  }

});


异步加载

@/router/index.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'BlogIndex',
    // import('') 必须是静态字符串,不可动态拼接
    // The above dynamic import cannot be analyzed by vite
    component: () => import('@/pages/blog/index/BlogIndex.vue'), // 注意这里如果是.vue文件必须要带上 文件后缀.vue
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/pages/login/Login.vue'),
  },
  {
    path: '/register',
    name: 'Register',
    component: () => import('@/pages/register/Register.vue'),
  },
  {
    path: '/create',
    name: 'CreateBlog',
    component: () => import('@/pages/blog/create/CreateBlog.vue'),
    // 只有经过身份验证的用户才能创建帖子
    meta: { requiresAuth: true },
  },
  {
    path: '/edit/:blogId',
    name: 'EditBlog',
    component: () => import('@/pages/blog/edit/EditBlog.vue'),
    meta: { requiresAuth: true },
  },
  {
    path: '/detail/:blogId',
    name: 'BlogDetail',
    component: () => import('@/pages/blog/detail/BlogDetail.vue'),
  },
  {
    path: '/user/:blogId',
    name: 'User',
    component: () => import('@/pages/user/User.vue'),
    meta: { requiresAuth: true },
  },
  {
    path: '/myblog',
    name: 'MyBlog',
    component: () => import('@/pages/myblog/MyBlog.vue'),
    meta: { requiresAuth: true },
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "About" */ '@/pages/About')
  }
];
const router = createRouter({
  history: createWebHashHistory(),
  // history: createWebHistory(process.env.BASE_URL),
  routes,
});

...

export default router;

  • 打包时会分成不同的.js文件

权限验证

  • 使用 JWT来进行身份验证
  • 相关API接口
  • 使用的组件

权限控制

  • 使用的 路由钩子
  • 相关API接口
  • 使用的组件

完整@/router/index.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import {createRouter, /*createWebHistory*/ createWebHashHistory, RouteRecordRaw} from 'vue-router';
import {storeToRefs} from 'pinia';
import useStore from '@/store';
import useAuthStore from '@/store/modules/auth';

/* 动态加载组件出现警告 component: () => import('@/pages/blog/index/BlogIndex')  改为静态导入 */
// components
import BlogIndex from '@/pages/blog/index/BlogIndex';
import Login from '@/pages/login/Login';
import Register from '@/pages/register/Register';
import CreateBlog from '@/pages/blog/create/CreateBlog';
import EditBlog from '@/pages/blog/edit/EditBlog';
import BlogDetail from '@/pages/blog/detail/BlogDetail';
import User from '@/pages/user/User';
import MyBlog from '@/pages/myBlog/MyBlog';
import About from '@/pages/About';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'BlogIndex',
    // import('') 必须是静态字符串,不可动态拼接
    // The above dynamic import cannot be analyzed by vite
    component: BlogIndex, // 注意这里如果是.vue文件必须要带上 文件后缀.vue
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
  },
  {
    path: '/register',
    name: 'Register',
    component: Register,
  },
  {
    path: '/detail/:blogId',
    name: 'BlogDetail',
    component: BlogDetail,
  },
  {
    path: '/create',
    name: 'CreateBlog',
    component: CreateBlog,
    // 只有经过身份验证的用户才能创建帖子
    meta: {requiresAuth: true},
  },
  {
    path: '/edit/:blogId',
    name: 'EditBlog',
    component: EditBlog,
    meta: {requiresAuth: true},
  },
  {
    path: '/user/:blogId',
    name: 'User',
    component: User,
    meta: {requiresAuth: true},
  },
  {
    path: '/myblog',
    name: 'MyBlog',
    component: MyBlog,
    meta: {requiresAuth: true},
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: About
  }
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 路由全局前置守卫
router.beforeEach((to, from, next) => {
  // 匹配路由元信息
  // 判断是需要登录
  // const {getIsLogin,} = storeToRefs(useAuthStore()); // 使用 store.checkLogin() 服务器验证 的登录状态 代替

  const store = useAuthStore();
  /*
  * if(to.path === 'login') return next();
  * if (to.path 受控页面或 未登录) return next('/login?');
  * next()
  * */

  const ifRequiresAuth: boolean = to.matched.some(record => { return record.meta.requiresAuth; });

  // URL 是否需要 身份验证
  ifRequiresAuth
    ? (// 需要身份验证的 URL
      store.checkLogin() // 向服务器请求,获取当前登录状态
        .then((isLogin) => {
          !isLogin
            ? next({path: '/login', query: {redirect: to.fullPath}})
            : next(); // 服务器响应验证已登录
        }))
    : next(); // 不需要身份验证的 URL;
  /* 确保最后执行且只一次 next() */

});

// 路由全局后置守卫
router.afterEach((/* to, from, failure */) => {
  // console.log('路由全局后置守卫', to, from);

  // 清除 记录的 router-view 中的 组件名
  const {routerCompName,} = storeToRefs(useStore());
  routerCompName.value = '';
});

export default router;


其他处理

处理导航结果

  • 导航是异步的,需要 await router.push 返回的 promise
1
2
await router.push('/my-profile')
this.isMenuOpen = false // 如果同步处理会马上关闭菜单
  • 通过导航结果来执行后续逻辑
    • 检测导航故障
    • 导航故障的属性
    • 检测重定向

路由懒加载

打包构建应用时,JavaScript 包会变得非常大,影响页面加载

  • 如果我们能把不同路由对应的组件分割成不同的代码块
  • 当路由被访问的时候才加载对应组件,这样就更高效
  • createRouter中的component选项 (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据
  • 如果使用的是 webpack vite 之类的打包器,它将自动从代码分割中受益

注意 不要在路由中使用异步组件

  • 异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的

过渡动效

滚动行为处理

自定义封装一个方法@/utils/scrollToTop.ts

1
2
3
4
5
6
7
8
export const scrollToTop = () => {
  const c = document.documentElement.scrollTop || document.body.scrollTop;
  if (c > 0) {
    window.requestAnimationFrame(scrollToTop);
    window.scrollTo(0, c - c / 8);
  }
}

  • window.requestAnimationFrame() 告诉浏览器希望执行一个动画
  • 并且要求浏览器在下次重绘之前调用指定的回调函数更新动画
  • 该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
  • requestAnimationFrame:优势:由系统决定回调函数的执行时机
  • 60Hz的刷新频率,那么每次刷新的间隔中会执行一次回调函数,不会引起丢帧,不会卡顿


参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名