Appearance
大厂分层模板
本节目标:理解“大厂分层模板”的核心思路,并能把它用于实际开发或面试表达。 给你一份大厂分层模板(可直接新建
src/router/guards/index.ts使用)。我把复杂逻辑都放在上一行注释里;beforeEach / beforeResolve / afterEach三段分工清晰、即插即用。
ts
// src/router/guards/index.ts
import type { NavigationGuard, Router } from 'vue-router'
import { useUserStore } from '@/store' // 按你项目实际路径
// import NProgress from 'nprogress' // 可选:如需页面顶部进度条
/** ---------------------------
* 工具:取安全回跳地址(仅站内)
* -------------------------- */
// 复杂逻辑:将 to.FullPath/URL 统一为“同源相对路径”;否则回首页,避免 open redirect
const getSafeRedirect = (raw?: string) => {
if (!raw) return '/'
try {
const decoded = decodeURIComponent(raw)
if (decoded.startsWith('/') && !/^(https?:)?\/\//i.test(decoded)) return decoded
} catch {}
return '/'
}
/** ---------------------------
* 全局前置:快速同步拦截(登录/白名单/基础角色)
* -------------------------- */
const beforeEachGuards: NavigationGuard[] = [
// 复杂逻辑:公共页快速放行(meta.public === true)
(to, _from, next) => {
if (to.matched.some((r) => r.meta?.public === true)) return next()
next()
},
// 复杂逻辑:登录态校验(默认需要登录:meta.requiresAuth !== false)
(to, from, next) => {
const needAuth = to.matched.some((r) => r.meta?.requiresAuth !== false) // 默认收紧
if (!needAuth) return next()
const user = useUserStore()
const isLoggedIn = Boolean(user.isLoggedIn)
if (isLoggedIn) return next()
// 复杂逻辑:未登录 → 去登录;带上 redirect
const redirect = encodeURIComponent(to.fullPath || '/')
// 复杂逻辑:若已在登录页且带 redirect,尽量复用(避免跳转风暴)
if (to.name === 'user' || to.name === 'user.login') {
return next()
}
return next({ name: 'user', query: { redirect } })
},
// 复杂逻辑:基础角色校验(当任一路由记录声明了 meta.roles)
(to, _from, next) => {
const needRoles: string[] = to.matched.flatMap((r) => (r.meta?.roles as string[]) || [])
if (needRoles.length === 0) return next()
const user = useUserStore()
// 复杂逻辑:你的项目里角色存在 user.userInfo.role(单角色),或本地存储 roles(多角色)任选其一
const myRoles = ((): string[] => {
const one = user.userInfo?.role
if (one) return [one]
try {
return JSON.parse(localStorage.getItem('roles') || '[]')
} catch {
return []
}
})()
// 复杂逻辑:需要的角色只要命中其一即可通过
if (needRoles.some((r) => myRoles.includes(r))) return next()
// 复杂逻辑:无权限 → 去 404(或你单独的 403)
return next({ name: 'not-found' })
},
// 可选:页面进度条开始
// (_to, _from, next) => { NProgress.start(); next() },
]
/** ---------------------------
* 全局解析:可取消的异步校验/预取(确认前最后一道)
* -------------------------- */
const beforeResolveGuards: NavigationGuard[] = [
// 复杂逻辑:示例一——手机号验证拦截(登录后首要合规检查)
async (to, from, next) => {
const user = useUserStore()
const needAuth = to.matched.some((r) => r.meta?.requiresAuth !== false)
if (!needAuth) return next()
// 复杂逻辑:示例规则:若用户未完成手机号验证,且当前目标不是“手机号验证弹窗所在页/模块”,则引导
// 你的项目里是“弹窗而非页面”,所以这里仅设置一个“标记”,由 layout 触发弹窗显示
if (user.isLoggedIn && user.userInfo?.phone_verify === false) {
// 复杂逻辑:在 query 打上 phone_verify=0,让 layout 感知后弹窗;并避免无限循环
if (to.query.phone_verify !== '0') {
return next({
name: String(to.name || ''),
params: to.params,
query: { ...to.query, phone_verify: '0' },
replace: true,
})
}
}
next()
},
// 复杂逻辑:示例二——页面数据预取(进入前确保必要数据就绪,避免闪烁)
async (to, _from, next) => {
try {
// 复杂逻辑:当某些页面声明了 meta.prefetch=true 时,统一在这里预拉取
const toPrefetch = to.matched.find((r) => r.meta?.prefetch === true)
if (!toPrefetch) return next()
// 举例:根据路由元信息决定预取哪些数据
// await Promise.all([ api.fetchSomething(), api.fetchAnotherThing() ])
return next()
} catch (e) {
console.error('[beforeResolve] prefetch failed:', e)
// 复杂逻辑:预取失败可阻断本次导航(或者回到安全页)
return next(false)
}
},
// 复杂逻辑:示例三——登录回跳归口 /redirect(如你采用 redirect 中转模式)
(to, from, next) => {
const user = useUserStore()
if ((from.name === 'user' || from.name === 'user.login') && user.isLoggedIn) {
// 复杂逻辑:登录成功后,在 resolve 阶段把 redirect 参数交给 /redirect 统一处理
const safe = getSafeRedirect(to.query.redirect as string | undefined)
if (safe && to.name === 'user.login') {
return next({ name: 'redirect', query: { to: safe }, replace: true })
}
}
next()
},
]
/** ---------------------------
* 全局后置:不可取消(埋点/标题/进度条结束)
* -------------------------- */
const afterEachGuards: Array<(to: any, from: any) => void> = [
// 复杂逻辑:设置页面标题(优先 meta.title,否则用 i18n key)
(to) => {
const raw = to.matched.find((r) => r.meta?.title)?.meta?.title as string | undefined
if (raw) document.title = typeof raw === 'string' ? raw : 'App'
},
// 可选:进度条结束
// () => { NProgress.done() },
// 复杂逻辑:PV 埋点(可替换为你的统计 SDK)
// (to, from) => reportPV({ path: to.fullPath, from: from.fullPath }),
]
/** 暴露为和你现有用法一致的结构 */
export default {
before: beforeEachGuards,
after: afterEachGuards,
}
/** 供 router.beforeResolve 使用(你在 router/index.ts 里有 resolveGuards.forEach) */
export const resolveGuards = beforeResolveGuards怎么接入
你的 router/index.ts 里改两处(如果你沿用原结构):
ts
// 复杂逻辑:从本文件引入 default.before / default.after
import guards, { resolveGuards } from '@/router/guards' // ← 现在 resolveGuards 也从这里出ts
// 复杂逻辑:注册顺序:beforeEach(快拦截) → beforeResolve(异步预取/细校验) → afterEach(收尾)
guards.before.forEach(router.beforeEach)
resolveGuards.forEach((guard) => router.beforeResolve(guard))
guards.after.forEach(router.afterEach)> 说明: > > - beforeEach:快速决策(登录/白名单/基础角色),尽量同步、轻量; > - beforeResolve:异步与细节(手机号验证标记、数据预取、统一回跳); > - afterEach:不可取消(标题、进度条、埋点)。 如果你想把手机号未验证时在 (public).layout.vue 弹窗拉起的逻辑也一起给我,我可以给一个watch 路由 query 的最小实现片段。
