前端核心知识点 06
路由模式
Vue Router 有哪几种路由模式?区别是什么?
Vue Router 提供了三种路由模式:
Hash 模式:
- 使用 URL 的 hash 部分(即
#后面的内容)来模拟一个完整的 URL - 当 hash 改变时,页面不会重新加载
- 兼容性好,支持所有浏览器(包括 IE9 及以上)
- 示例:
http://example.com/#/user/id
- 使用 URL 的 hash 部分(即
History 模式:
- 利用 HTML5 History API(pushState、replaceState 和 popstate 事件)来实现 URL 的变化而无需重新加载页面
- URL 更加美观,没有
#符号 - 需要服务器配置支持,否则刷新页面会出现 404 错误
- 示例:
http://example.com/user/id
Abstract 模式:
- 不依赖浏览器的 URL 相关 API
- 在 Node.js 环境中使用,或者当浏览器不支持 History API 时作为回退方案
- 所有路由跳转都会被记录在内存中
配置示例:
const router = new VueRouter({
mode: 'history', // 或 'hash' | 'abstract'
routes
})为什么 History 模式刷新会 404?如何解决?
原因:
- History 模式下,URL 看起来像真实的路径,例如
http://example.com/user/123 - 当用户刷新页面时,浏览器会向服务器请求这个真实的路径
- 但服务器上并没有对应
/user/123的文件,因此返回 404 错误
解决方案:
- Nginx 配置:
location / {
try_files $uri $uri/ /index.html;
}- Apache 配置:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>- Node.js (Express) 配置:
app.use(history())
app.use(express.static(path.join(__dirname, 'dist')))核心思想是:让服务器在找不到对应资源时,返回 index.html,由前端 Vue Router 处理路由。
在 Electron 或本地 file:// 环境下能用 History 模式吗?为什么?
不推荐使用,原因如下:
Electron 环境:
- Electron 中默认使用 file 协议加载本地文件
- History API 在 file 协议下可能有限制或行为不一致
- 推荐使用 Hash 模式或在 Electron 中配置本地服务器
file:// 环境:
- 浏览器对 file 协议的安全限制更多
- History API 在 file 协议下可能无法正常工作
- 用户体验差,URL 难以管理
建议:
- 在 Electron 应用中,使用 Hash 模式或者配置本地服务器
- 在本地开发时,使用开发服务器而非直接打开 HTML 文件
路由匹配与配置
如何定义动态路由?参数变化时组件为什么不重新渲染?怎么解决?
定义动态路由:
const routes = [
{ path: '/user/:id', component: User }
]参数变化时组件不重新渲染的原因:
- 当路由参数(如
/user/1到/user/2)变化时,组件实例会被复用以提高性能 - 由于组件实例未被销毁和重建,生命周期钩子(如 created、mounted)不会被再次调用
解决方案:
- 使用
watch监听$route对象:
export default {
watch: {
'$route'(to, from) {
// 处理路由变化
this.fetchUser(to.params.id)
}
}
}- 使用
beforeRouteUpdate导航守卫(Vue Router 2.2+):
export default {
async beforeRouteUpdate(to, from, next) {
// 处理路由变化
this.user = await fetchUser(to.params.id)
next()
}
}- 为 router-view 添加 key 属性,强制重新渲染:
<router-view :key="$route.fullPath"></router-view>嵌套路由怎么配置?父组件需要做什么?
配置嵌套路由:
const routes = [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功
path: 'posts',
component: UserPosts
}
]
}
]父组件需要包含 <router-view> :
<!-- User.vue -->
<template>
<div>
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
</template>注意事项:
- 子路由的路径会自动拼接到父路由路径之后
- 父组件必须包含
<router-view>才能渲染子路由组件 - 空路径的子路由会作为默认子路由渲染
如何实现 404 页面?Vue Router 4 的通配符写法是什么?
Vue Router 3 实现 404:
const routes = [
// 其他路由...
{
path: '*',
component: NotFound
}
]Vue Router 4 实现 404:
const routes = [
// 其他路由...
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound
}
]注意事项:
- 404 路由应放在路由配置的最后
- 在 Vue Router 4 中,通配符
*已被移除,使用正则表达式替代 - 可以通过
$route.params.pathMatch获取匹配的路径
命名路由有什么好处?
命名路由有以下好处:
解耦合:
- 不再依赖固定的 URL 结构
- 路由结构变化时,只需修改路由配置,无需修改所有链接
可读性:
- 通过名称可以直观理解路由的用途
- 代码更易于理解和维护
便捷性:
- 使用
router.push({ name: 'user', params: { id: 123 }})进行编程式导航 - 在模板中使用
<router-link :to="{ name: 'user', params: { id: 123 }}">
- 使用
示例:
// 路由配置
const routes = [
{
path: '/user/:id',
name: 'user',
component: User
}
]
// 编程式导航
router.push({ name: 'user', params: { id: 123 } })
// 模板中使用
<router-link :to="{ name: 'user', params: { id: 123 }}">User</router-link>编程式导航与路径解析
router.push() 和 router.replace() 有什么区别?
router.push():
- 向浏览器历史记录中添加新记录
- 用户可以点击后退按钮返回上一个页面
- 相当于
<router-link :to="...">
router.replace():
- 替换当前历史记录,而不是添加新记录
- 用户不能点击后退按钮返回上一个页面
- 相当于
<router-link :to="..." replace>
示例:
// 添加新记录
router.push('/users')
router.push({ path: '/users', query: { id: 1 } })
// 替换当前记录
router.replace('/users')
router.replace({ path: '/users', query: { id: 1 } })如何实现带查询参数或 hash 的跳转?
带查询参数:
// 字符串形式
router.push('/user?id=123')
// 对象形式
router.push({ path: '/user', query: { id: '123', name: 'john' } })
// 命名路由
router.push({ name: 'user', query: { id: '123' } })带 hash:
// 字符串形式
router.push('/user#section1')
// 对象形式
router.push({ path: '/user', hash: '#section1' })
// 结合查询参数和 hash
router.push({
path: '/user',
query: { id: '123' },
hash: '#section1'
})如何避免重复点击导致多次跳转?
问题: 重复导航到当前路由会抛出 NavigationDuplicated 错误。
解决方案:
- 捕获错误:
router.push('/user').catch(err => {
if (err.name !== 'NavigationDuplicated') {
throw err
}
})- 检查当前路由:
const goToUser = () => {
if (this.$route.path !== '/user') {
router.push('/user')
}
}- Vue Router 3.1+ 的改进:
// 新版本中 push 和 replace 返回 promise
// 可以直接使用 catch 处理
router.push('/user').catch(() => {})路由守卫
Vue Router 有哪些类型的路由守卫?执行顺序是怎样的?
路由守卫类型:
全局守卫:
beforeEach:全局前置守卫beforeResolve:全局解析守卫(2.5+)afterEach:全局后置钩子
路由独享守卫:
beforeEnter:在路由配置中定义
组件内守卫:
beforeRouteEnter:进入路由前beforeRouteUpdate:路由参数变化时beforeRouteLeave:离开路由前
执行顺序:
1. 导航被触发
2. 调用组件内守卫 beforeRouteLeave
3. 调用全局 beforeEach
4. 调用路由独享 beforeEnter
5. 解析异步路由组件
6. 调用组件内守卫 beforeRouteEnter
7. 调用全局 beforeResolve
8. 导航被确认
9. 调用全局 afterEach
10. 触发 DOM 更新
11. 调用 beforeRouteEnter 中传给 next 的回调函数如何实现登录权限控制?
实现方式:
- 全局前置守卫:
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('token')
// 需要认证的路由
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isAuthenticated) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next()
}
})- 路由配置:
const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true }
}
]- 更复杂的权限控制:
router.beforeEach(async (to, from, next) => {
const user = await getCurrentUser()
if (to.matched.some(record => record.meta.requiresAdmin)) {
if (user && user.role === 'admin') {
next()
} else {
next('/403')
}
} else {
next()
}
})全局守卫和组件内守卫分别适用什么场景?
全局守卫适用于:
- 全局身份验证检查
- 页面访问权限控制
- 页面加载进度条显示
- 日志记录
组件内守卫适用于:
- 组件级别的权限控制
- 进入组件前的数据预加载
- 离开组件前的确认提示
- 组件内特定的业务逻辑处理
示例:
// 全局守卫 - 认证检查
router.beforeEach((to, from, next) => {
if (to.matched.some(r => r.meta.requiresAuth) && !isAuthenticated()) {
next('/login')
} else {
next()
}
})
// 组件内守卫 - 数据预加载
export default {
async beforeRouteEnter(to, from, next) {
const data = await fetchData(to.params.id)
next(vm => {
vm.setData(data)
})
}
}在守卫中发起异步请求(如获取用户信息),会影响导航吗?
会,异步操作会影响导航:
- 导航在守卫 resolve 前一直处于等待状态
- 页面不会跳转,直到所有守卫调用 next()
- 可以在等待期间显示加载指示器
示例:
router.beforeEach(async (to, from, next) => {
// 显示加载指示器
showLoading()
if (to.matched.some(record => record.meta.requiresAuth)) {
try {
// 异步获取用户信息
const user = await fetchUser()
store.commit('SET_USER', user)
next()
} catch (error) {
next('/login')
} finally {
hideLoading()
}
} else {
next()
}
})高级特性与实战问题
什么是路由元信息(meta)?怎么用?
路由元信息是在路由配置中通过 meta 字段传递的自定义信息。
用途:
- 权限控制
- 页面标题设置
- 页面缓存控制
- 过渡动画名称
- 布局组件选择
示例:
const routes = [
{
path: '/admin',
component: Admin,
meta: {
requiresAuth: true,
requiresAdmin: true,
title: '管理后台',
transition: 'slide-left'
}
}
]
// 在守卫中使用
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login')
} else {
document.title = to.meta.title || '默认标题'
next()
}
})如何实现页面滚动行为控制(如回到顶部)?
Vue Router 提供了 scrollBehavior 配置项:
const router = new VueRouter({
mode: 'history',
routes,
scrollBehavior(to, from, savedPosition) {
// savedPosition 在通过浏览器前进/后退按钮触发时才可用
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
})更复杂的滚动控制:
scrollBehavior(to, from, savedPosition) {
// 如果路由有 hash,滚动到对应元素
if (to.hash) {
return {
selector: to.hash,
offset: { x: 0, y: 60 }
}
}
// 路由元信息控制滚动行为
if (to.meta.scrollToTop !== false) {
return { x: 0, y: 0 }
}
}Vue Router 如何与 Vuex/Pinia 配合做数据预取?
结合 Vuex:
// 路由守卫中预取数据
router.beforeEach(async (to, from, next) => {
if (to.name === 'user' && to.params.id) {
// 检查数据是否已存在
if (!store.getters.userById(to.params.id)) {
await store.dispatch('fetchUser', to.params.id)
}
}
next()
})
// 组件中使用数据
export default {
computed: {
user() {
return this.$store.getters.userById(this.$route.params.id)
}
}
}结合 Pinia:
// 在路由守卫中
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
if (to.name === 'user' && to.params.id) {
if (!userStore.hasUser(to.params.id)) {
await userStore.fetchUser(to.params.id)
}
}
next()
})在 SSR(如 Nuxt.js)中,路由是如何工作的?和客户端路由有何不同?
SSR 路由特点:
- 服务端渲染:首次访问页面时,服务器会根据路由渲染完整 HTML
- 客户端接管:页面加载后,客户端 Vue Router 接管路由控制
- 统一配置:服务端和客户端使用相同的路由配置
与客户端路由的区别:
- 首次加载:SSR 首次加载更快,因为返回的是完整 HTML
- SEO 友好:搜索引擎可以直接抓取完整内容
- 复杂性:需要处理服务端和客户端状态同步
- 生命周期:SSR 有特殊的生命周期钩子(如 asyncData、fetch)
Nuxt.js 示例:
// pages/user/_id.vue
export default {
// 服务端和客户端都会执行
async asyncData({ params, $axios }) {
const user = await $axios.$get(`/api/users/${params.id}`)
return { user }
},
// 只在客户端执行
mounted() {
// 客户端逻辑
}
}陷阱与调试类问题
路由参数变化时组件不更新的问题
常见场景: 从 /user/1 导航到 /user/2 时,组件不会重新创建。
解决方案已在前面详细说明:
- 使用 watch 监听
$route - 使用 beforeRouteUpdate 守卫
- 为 router-view 添加 key
路由懒加载配置错误
正确配置:
const routes = [
{
path: '/user',
component: () => import('@/views/User.vue')
}
]常见错误:
// 错误:立即执行
component: import('@/views/User.vue')
// 错误:缺少箭头函数
component: () => import('@/views/User.vue').then(m => m.default)路由守卫中的 this 指向问题
问题: 在导航守卫中,this 不指向 Vue 实例。
解决方案:
// 错误
router.beforeEach((to, from, next) => {
this.fetchData() // this 为 undefined
})
// 正确
router.beforeEach((to, from, next) => {
// 通过其他方式访问 store 或工具函数
store.dispatch('fetchData')
next()
})嵌套路由中子路由不显示
常见原因:
- 父组件缺少
<router-view> - 子路由路径配置错误
- 路由匹配不正确
检查清单:
- 确保父组件有
<router-view> - 检查路由路径是否正确拼接
- 使用 Vue DevTools 检查路由匹配情况
History 模式部署问题
问题: 刷新页面出现 404 错误。
解决方案已在前面详细说明: 需要配置服务器将所有路由请求重定向到 index.html。