上代码
根据以上思路,现在开始编码环节。笔者采用的是 Vue3,Vue2 的开发者还请根据情况做适当调整。
为了方便唤起用户授权登录逻辑,笔者打算将授权登录封住为一个 login 页面,这样做的好处是我们在任何判断到需要登录的地方直接通过 Vue Router 的 push 方法跳转到登录页面即可。
通常情况下,我们的应用并不是所有页面都需要登录后才能访问,只有在访问特定页面时候,才需要用户登录,那么我们就需要标识哪些页面需要进行登录鉴权。这里我们可以利用 Vue Router 的 meta 属性来进行标识,官方文档对 meta 解释如下:
有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的
meta
属性来实现,并且它可以在路由地址和导航守卫上都被访问到。
刚好 Vue Router 官方就有示例,如下:
const routes = [ { path: '/posts', component: PostsLayout, children: [ { path: 'new', component: PostsNew, // 需要登录后才能访问的页面 meta: { requiresAuth: true } }, { path: ':id', component: PostsDetail, // 任何人都可访问的页面 meta: { requiresAuth: false } } ] } ]
接下来我们就可以在 Vue Router 的全局守卫beforeEach
中获取到这个元信息从而做登录跳转了。
router.beforeEach((to, from) => { // 而不是去检查每条路由记录 // to.matched.some(record => record.meta.requiresAuth) if (to.meta.requiresAuth && !userStore.isLogin) { // 此路由需要授权,请检查是否已登录 // 如果没有,则重定向到登录页面 return { path: '/login', // 保存我们所在的位置,以便以后再来 query: { redirect: to.fullPath }, } } })
需要补充说明的是,userStore.isLogin
的实现。这里和我们实际采用的登录态维护方案有关,如果是采用token
方式的话,那就是检查token
是否已经存在。笔者采用了vuex
作为来保存token
,然后借助插件来将Store
中的数据持久化到localStorage
。
接下来我们来看具体的实现:
login.vue
: 登录组件
<template> <div ></div> </template> <script lang="ts"> import { defineComponent } from 'vue' import { jump2Auth, getUserInfo } from '@/hooks/useWechatAuth' import { userStore } from '@/store/modules/user' import { redirectTo, getRouteQuery } from '@/hooks/usePage' export default defineComponent({ name: 'Login', setup() { let code = getRouteQuery().code as string // 3.如果有 code,则已经授权 if (code) { getUserInfo(code as string).then((res: any) => { // 记录 token userStore.saveToken(res.access_token) const redirect = userStore.userState.landPageRoute || '/' // 跳转到授权前访问的页面 redirectTo(redirect) }) } else { // 1.记录上一个页面的地址 const { redirect } = getRouteQuery() if (redirect) { userStore.setLandPage(redirect as string) } // 2.跳转授权 const callbackUrl = window.location.origin + window.location.pathname jump2Auth(callbackUrl) } }, }) </script>
可以看到,login 页面其实并没有什么内容,跳转到该页面后,我们会直接重定向到微信授权的页面,授权回调回来也会回到该页面,此时我们再通过获取路由参数的方式获取 code 参数。
@/hooks/usePage.ts
: 该文件主要是封装了 router 相关的常用方法。
import router from '@/router' import { cloneDeep } from 'lodash' import { toRaw } from 'vue' /** * 重定向 * @param path 路径 */ export function redirectTo(path: string) { const { replace } = router replace({ path, }) } /** * 获取路由上 query 参数 */ export function getRouteQuery() { const { currentRoute } = router const { query } = currentRoute.value return cloneDeep(query) }
@/hooks/useWechatAuth.ts
:该文件封装了微信授权与后端交互的请求。
import { useAxios } from '@/hooks/useAxios' /** * 获取微信授权的跳转地址 * @param callbackUrl 授权后回调链接 * @returns */ export function jump2Auth(callbackUrl: string) { useAxios({ url: '/api/wechat/auth', params: { redirect_url: callbackUrl, }, }).then((authUrl: any) => { if (process.env.NODE_ENV === 'development') { window.location.href = callbackUrl + '?code=test' } else { window.location.href = authUrl } }) } /** * 提交 code 进行登录 * @param code * @returns */ export async function getUserInfo(code: string) { const userInfo = await useAxios({ method: 'POST', url: '/api/wechat/auth', params: { code, }, }) return userInfo }
@/store/modules/user.ts
: 全局状态存储,主要是记录 token 和登录前访问页面。
import { Module, VuexModule, Mutation, getModule, Action } from 'vuex-module-decorators' import store from '@/store' import { initialUnencryptedStorage } from '../globals' interface UserState { token: string landPageRoute: string } const NAME = 'user' // name: 模块名字 // namespaced 表示开启命名空间 // dynamic 设置为 true 时,表示创建动态模块,运行时将模块注册到存储中 // preserveState 如果数据有持久化,该变量为 true 时可以从 storage 中拿取初始值 @Module({ namespaced: true, name: NAME, dynamic: true, store, preserveState: Boolean(initialUnencryptedStorage[NAME]), }) export class User extends VuexModule { userState: UserState = { token: '', /** 登录前访问页面 */ landPageRoute: '', } get isLogin(): boolean { return !!this.userState.token } @Mutation saveToken(token: string): void { this.userState.token = token } @Mutation setLandPage(route: string): void { this.userState.landPageRoute = route } } export const userStore = getModule(User)
笔者借助vuex-persistedstate
插件将store
中数据存储到了localStorage
,这样做的好处是用户关闭页面后,再次访问,即不用重新触发微信授权流程,大大优化了用户体验。
总结
不得不说,Vue3 在代码抽象和复用这块上,写起来着实舒服很多,希望大家也也尝试着按照官方实践那样,多将逻辑代码解耦抽离,生成一个个的 hook,这样代码就显得优雅多啦。该方案经过笔者尝试论证,不论是代码整洁优雅程度,还是业务需求的实现上,都几乎完美(请容我装一波 b)。当然,这里可能存在我没发现的 bug 或者痛点,毕竟从来没有十全十美的架构嘛,这里也欢迎看官大佬们和我交流探讨,提供更好的方案和想法。