前端 Vue 专栏 09:Vue Router 与前端路由

前端 Vue 专栏 09:Vue Router 与前端路由

前言

在传统多页面网站中,从用户列表进入用户详情,浏览器往往会重新向服务器请求一份 HTML:

text 复制代码
/users
    ↓ 点击用户
/users/1001
    ↓ 浏览器重新请求 HTML
服务器返回新的页面

但在 Vue 单页应用中,我们点击 <RouterLink> 后通常会发现:

text 复制代码
地址栏发生变化;
浏览器没有刷新整张页面;
页面中只有 RouterView 区域切换了组件;
组件仍然可以读取 URL 中的参数。

这背后就是前端路由。

这一篇将围绕秋招和实际开发最常见的问题展开:

text 复制代码
什么是 SPA 和前端路由;
前端路由为什么不刷新页面;
RouterLink 和 RouterView 分别做什么;
route 和 router 有什么区别;
params 和 query 怎么选择;
hash 与 history 模式有什么区别;
history 模式刷新为什么可能 404;
导航守卫怎样实现登录校验;
路由懒加载为什么能减小首屏体积。

一、什么是路由

路由最基本的作用是:

根据当前 URL,决定应该展示什么内容。

例如:

text 复制代码
/                 → 首页组件
/users            → 用户列表组件
/users/1001       → 用户详情组件
/settings/profile → 个人设置组件

可以把路由表理解成 URL 与组件的映射表:

js 复制代码
const routes = [
  {
    path: '/',
    component: HomeView
  },
  {
    path: '/users',
    component: UserListView
  },
  {
    path: '/users/:id',
    component: UserDetailView
  }
]

当地址是:

text 复制代码
/users/1001

Vue Router 会匹配:

text 复制代码
/users/:id

然后把 UserDetailView 渲染到 <RouterView> 中,并得到:

js 复制代码
route.params.id === '1001'

二、什么是 SPA

SPA 是 Single Page Application,中文叫单页应用。

"单页"不是指应用只有一个业务页面,而是指浏览器通常只在第一次进入应用时获取主要 HTML 文档:

text 复制代码
第一次访问
    ↓
服务器返回 index.html
    ↓
加载 Vue、路由和业务 JavaScript
    ↓
Vue 接管页面

后续从首页进入用户列表时,通常不会重新请求一份完整 HTML,而是由前端路由切换当前展示的组件:

text 复制代码
HomeView
    ↓
UserListView

当然,"不刷新整页"不代表完全没有网络请求。路由切换时仍然可能请求:

text 复制代码
接口数据;
懒加载的 JavaScript 文件;
图片、字体等资源。

只是浏览器没有把当前文档全部销毁后再加载一份新 HTML。


三、前端路由为什么可以不刷新页面

前端路由主要完成四件事:

text 复制代码
监听或修改浏览器 URL;
根据 URL 匹配路由记录;
更新当前 route 对象;
让 RouterView 渲染匹配到的组件。

以 history 模式为例,简化过程是:

text 复制代码
用户点击 RouterLink
    ↓
Vue Router 阻止链接的默认整页跳转
    ↓
调用 History API 修改地址栏
    ↓
根据新地址匹配路由表
    ↓
更新响应式的当前路由
    ↓
RouterView 切换组件

浏览器提供了:

js 复制代码
history.pushState()
history.replaceState()

它们可以修改地址栏和历史记录,但不会像普通页面导航一样自动重新请求整份 HTML。

浏览器前进、后退时,Vue Router 还会监听相应历史变化,再次匹配并切换组件。

因此不是 Vue Router 拦截了所有网络请求,而是:

它利用浏览器提供的 URL 与 History 能力,在不重新加载文档的情况下维护地址,并让 URL 驱动组件切换。


普通链接:

html 复制代码
<a href="/users">用户列表</a>

浏览器默认会把它当作页面导航,向 /users 发起文档请求。

Vue Router 提供:

vue 复制代码
<RouterLink to="/users">
  用户列表
</RouterLink>

它最终通常仍然渲染为 <a>,因此保留链接语义;但普通左键导航时,Vue Router 会接管跳转,避免整页刷新。

它还会处理:

text 复制代码
URL 生成和编码;
history 记录;
当前路由激活样式;
对象形式的命名路由;
浏览器前进和后退的配合。

例如:

vue 复制代码
<RouterLink
  :to="{
    name: 'user-detail',
    params: { id: user.id }
  }"
>
  查看详情
</RouterLink>

如果是站外地址、下载链接或确实需要整页导航,仍然可以使用普通 <a>


五、RouterView 是什么

<RouterView> 是路由组件的出口:

当前 URL 匹配到了哪个路由组件,就把它渲染到 RouterView 所在的位置。

例如 App.vue

vue 复制代码
<template>
  <header>
    <RouterLink to="/">首页</RouterLink>
    <RouterLink to="/users">用户管理</RouterLink>
  </header>

  <main>
    <RouterView />
  </main>
</template>

访问 / 时:

text 复制代码
RouterView → HomeView

访问 /users 时:

text 复制代码
RouterView → UserListView

页面中的 header 不需要重新创建,主要变化的是 RouterView 对应区域。

如果只配置路由却没有写 <RouterView>,Vue Router 虽然可以匹配地址,但没有位置展示匹配到的组件。


六、在 Vue 3 项目中配置 Vue Router

常见目录结构:

text 复制代码
src/
├── router/
│   └── index.js
├── views/
│   ├── HomeView.vue
│   ├── UserListView.vue
│   └── UserDetailView.vue
├── App.vue
└── main.js

1. 创建路由实例

js 复制代码
// src/router/index.js
import {
  createRouter,
  createWebHistory
} from 'vue-router'

import HomeView from '@/views/HomeView.vue'
import UserListView from '@/views/UserListView.vue'
import UserDetailView from '@/views/UserDetailView.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/users',
    name: 'user-list',
    component: UserListView
  },
  {
    path: '/users/:id',
    name: 'user-detail',
    component: UserDetailView
  }
]

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

export default router

2. 安装路由插件

js 复制代码
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(router)
app.mount('#app')

app.use(router) 会把路由能力安装到整个 Vue 应用,使组件中可以使用:

text 复制代码
RouterLink;
RouterView;
useRouter;
useRoute;
路由相关的全局属性。

安装顺序通常是:

js 复制代码
app.use(router)
app.mount('#app')

因为应用挂载前就应该准备好路由能力。


七、路由配置中常见字段

js 复制代码
const routes = [
  {
    path: '/users/:id',
    name: 'user-detail',
    component: UserDetailView,
    props: true,
    meta: {
      requiresAuth: true,
      title: '用户详情'
    }
  }
]

常见字段:

字段 作用
path 要匹配的 URL 路径
name 路由名称,方便稳定跳转
component 匹配后渲染的组件
children 嵌套路由
redirect 重定向到另一个地址
alias 为同一路由提供其他访问地址
props 把路由参数作为组件 props 传递
meta 保存权限、标题等附加信息
beforeEnter 当前路由独享守卫

秋招阶段优先掌握:

text 复制代码
path、name、component、children、meta、redirect

其余字段知道使用场景即可。


八、route 和 router 有什么区别

这是 Vue Router 最常见的问题之一。

vue 复制代码
<script setup>
import {
  useRoute,
  useRouter
} from 'vue-router'

const route = useRoute()
const router = useRouter()
</script>

route

route 表示当前路由信息,是"当前地址的结果":

js 复制代码
console.log(route.path)
console.log(route.fullPath)
console.log(route.name)
console.log(route.params)
console.log(route.query)
console.log(route.meta)

例如当前地址是:

text 复制代码
/users/1001?tab=posts

可能得到:

js 复制代码
route.path
// '/users/1001'

route.params
// { id: '1001' }

route.query
// { tab: 'posts' }

router

router 是路由器实例,是"控制导航的工具":

js 复制代码
router.push('/users')
router.replace('/login')
router.back()
router.forward()
router.go(-1)

最简洁的记忆方式:

text 复制代码
route:当前在哪里
router:控制去哪里

在选项式 API 中对应:

text 复制代码
this.$route  → 当前路由信息
this.$router → 路由器实例

九、编程式导航:push、replace 和 go

除了 <RouterLink>,还可以在 JavaScript 中跳转:

vue 复制代码
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

function openUser(userId) {
  router.push({
    name: 'user-detail',
    params: { id: userId }
  })
}
</script>

1. push

js 复制代码
router.push('/users')

向浏览器历史栈增加一条记录:

text 复制代码
/home
    ↓ push('/users')
/users

用户点击浏览器后退,可以回到 /home

2. replace

js 复制代码
router.replace('/home')

替换当前历史记录,不额外增加一条:

text 复制代码
/login
    ↓ replace('/home')
/home

登录成功后经常使用 replace,避免用户后退又回到登录页。

3. go、back 和 forward

js 复制代码
router.go(-1)  // 后退一条
router.go(1)   // 前进一条
router.back()  // 后退
router.forward() // 前进

对比:

方法 是否新增历史记录 常见用途
push 普通页面跳转
replace 否,替换当前记录 登录完成、纠正无效地址
go 不创建,移动历史指针 前进或后退

十、动态路由参数 params

假设所有用户详情都使用同一个组件:

text 复制代码
/users/1001
/users/1002
/users/1003

不需要为每个用户写一条路由,可以使用动态参数:

js 复制代码
{
  path: '/users/:id',
  name: 'user-detail',
  component: UserDetailView
}

其中:

text 复制代码
:id 是动态路径参数

在组件中读取:

vue 复制代码
<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()

console.log(route.params.id)
</script>

<template>
  <h1>用户 {{ route.params.id }}</h1>
</template>

访问:

text 复制代码
/users/1001

得到:

js 复制代码
route.params.id === '1001'

注意 URL 中的 params 通常是字符串,而不是自动转换成数字:

js 复制代码
Number(route.params.id)

如果业务需要数字,应当主动转换和校验。


十一、为什么从 users/1 到 users/2 没有重新 mounted

路由配置:

js 复制代码
{
  path: '/users/:id',
  component: UserDetailView
}

从:

text 复制代码
/users/1

跳到:

text 复制代码
/users/2

前后匹配的都是 UserDetailView。Vue Router 通常会复用同一个组件实例,而不是先卸载用户 1 组件,再创建用户 2 组件。

因此:

text 复制代码
setup 不会重新执行;
onMounted 不会重新执行;
组件中的局部状态可能继续保留。

如果最初只在 onMounted 中请求数据:

js 复制代码
onMounted(() => {
  loadUser(route.params.id)
})

/users/1 跳到 /users/2 时,可能不会重新请求。

更合理的是监听具体参数:

vue 复制代码
<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()

watch(
  () => route.params.id,
  id => {
    loadUser(id)
  },
  { immediate: true }
)
</script>

也可以使用组件内守卫:

js 复制代码
import { onBeforeRouteUpdate } from 'vue-router'

onBeforeRouteUpdate(to => {
  return loadUser(to.params.id)
})

面试时应该说明原因:

路由参数变化不一定意味着组件类型变化。相同路由组件会被复用,所以不能只依赖 mounted,需要 watch 具体参数或使用 onBeforeRouteUpdate。


十二、查询参数 query

查询参数位于 ? 后面:

text 复制代码
/users?page=2&keyword=vue

读取方式:

js 复制代码
const route = useRoute()

console.log(route.query.page)
// '2'

console.log(route.query.keyword)
// 'vue'

跳转时可以写:

js 复制代码
router.push({
  path: '/users',
  query: {
    page: 2,
    keyword: 'vue'
  }
})

生成类似:

text 复制代码
/users?page=2&keyword=vue

query 也通常以字符串形式读取,因此分页参数常需要转换:

js 复制代码
const page = Number(route.query.page ?? 1)

常见用途:

text 复制代码
分页页码;
搜索关键词;
筛选条件;
当前标签;
排序方式。

把筛选条件放在 URL 中,可以让刷新、复制链接和浏览器前进后退更符合用户预期。


十三、params 和 query 有什么区别

例如用户详情:

text 复制代码
/users/1001

这里的 1001 是路径的一部分,适合使用 params:

js 复制代码
path: '/users/:id'

例如用户列表筛选:

text 复制代码
/users?page=2&status=active

分页和状态是附加条件,适合使用 query。

对比:

对比项 params query
URL 位置 路径内部 ? 后面
示例 /users/1001 /users?page=2
路由是否需要声明 通常要在 path 中声明 :id 不需要预先声明字段
常见用途 资源身份、页面层级 搜索、分页、筛选、排序
读取方式 route.params.id route.query.page

判断原则:

text 复制代码
缺少它就无法确定访问哪个资源 → params
它只是当前页面的附加筛选状态 → query

但这不是强制语法规则,最终还要结合接口设计和产品 URL 规范。


十四、为什么推荐命名路由

可以直接使用路径跳转:

js 复制代码
router.push(`/users/${userId}`)

也可以使用命名路由:

js 复制代码
router.push({
  name: 'user-detail',
  params: {
    id: userId
  }
})

命名路由的优点是:

text 复制代码
不需要手动拼接 URL;
参数会由路由器进行编码;
path 结构变化时,调用方仍可通过稳定 name 跳转;
代码语义更清晰。

一个常见错误是同时传入 pathparams

js 复制代码
router.push({
  path: '/users',
  params: { id: 1001 }
})

不要期待它自动生成:

text 复制代码
/users/1001

需要动态 params 时,优先使用:

js 复制代码
router.push({
  name: 'user-detail',
  params: { id: 1001 }
})

或者自己提供完整 path:

js 复制代码
router.push({
  path: '/users/1001'
})

十五、为什么可以把 params 作为 props

直接在组件中使用 route.params

js 复制代码
const route = useRoute()
const userId = route.params.id

会让组件与路由系统耦合。

可以在路由配置中写:

js 复制代码
{
  path: '/users/:id',
  name: 'user-detail',
  component: UserDetailView,
  props: true
}

然后组件像接收普通 prop 一样接收 id

vue 复制代码
<script setup>
const props = defineProps({
  id: {
    type: String,
    required: true
  }
})
</script>

<template>
  <h1>用户 {{ id }}</h1>
</template>

这样组件更容易:

text 复制代码
脱离路由单独复用;
编写组件测试;
明确声明输入数据。

这是工程上的加分项,但不是每个简单页面都必须使用。


十六、嵌套路由

管理后台常见结构:

text 复制代码
/settings/profile
/settings/security

外层设置布局保持不变,内部区域切换不同页面。

路由配置:

js 复制代码
const routes = [
  {
    path: '/settings',
    component: SettingsLayout,
    children: [
      {
        path: '',
        redirect: { name: 'settings-profile' }
      },
      {
        path: 'profile',
        name: 'settings-profile',
        component: ProfileSettings
      },
      {
        path: 'security',
        name: 'settings-security',
        component: SecuritySettings
      }
    ]
  }
]

父组件 SettingsLayout.vue 中还需要一个 <RouterView>

vue 复制代码
<template>
  <div class="settings-layout">
    <aside>
      <RouterLink to="/settings/profile">
        个人资料
      </RouterLink>

      <RouterLink to="/settings/security">
        安全设置
      </RouterLink>
    </aside>

    <section>
      <RouterView />
    </section>
  </div>
</template>

访问 /settings/security 时:

text 复制代码
App.vue 的 RouterView
    ↓ 渲染 SettingsLayout
SettingsLayout 的 RouterView
    ↓ 渲染 SecuritySettings

需要注意,子路由的 path 通常不以 / 开头:

js 复制代码
path: 'security'

它会与父路径组合为:

text 复制代码
/settings/security

十七、hash 模式是什么

hash 模式使用:

js 复制代码
import {
  createRouter,
  createWebHashHistory
} from 'vue-router'

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

URL 类似:

text 复制代码
https://example.com/#/users/1001

# 后面的内容属于 URL hash。浏览器向服务器请求文档时,不会把 hash 部分发送给服务器。

也就是说直接刷新:

text 复制代码
https://example.com/#/users/1001

服务器主要收到的是:

text 复制代码
GET /

所以服务器只要能够返回根路径的 index.html,前端再读取 hash 并展示用户详情即可。

hash 模式的特点:

text 复制代码
部署简单;
通常不需要额外 history 回退配置;
URL 中带有 #;
URL 形式不如 history 模式自然;
对 SEO 场景通常不够理想。

十八、history 模式是什么

history 模式使用:

js 复制代码
import {
  createRouter,
  createWebHistory
} from 'vue-router'

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

URL 类似普通地址:

text 复制代码
https://example.com/users/1001

应用内部跳转时,Vue Router 利用 History API 修改 URL,不重新加载整张页面。

特点:

text 复制代码
URL 干净自然;
更接近传统网站路径;
通常是现代 Web 应用常用模式;
服务器必须配置 SPA 回退。

对比:

对比项 hash history
URL /#/users/1 /users/1
核心能力 hash 变化 History API
hash 后内容是否发给服务器 不适用
刷新深层地址 通常直接返回根页面 需要服务器 fallback
URL 形式 # 更自然

秋招回答不能只说"hash 丑、history 好看",还要说出服务器请求的区别。


十九、history 模式刷新为什么会 404

假设用户先访问:

text 复制代码
https://example.com/

服务器返回 index.html,Vue 应用启动。然后前端跳转到:

text 复制代码
https://example.com/users/1001

此时只是前端路由切换,所以没有问题。

但是用户按下刷新,浏览器会真正向服务器发送:

http 复制代码
GET /users/1001 HTTP/1.1
Host: example.com

服务器文件系统中通常没有:

text 复制代码
/users/1001

它可能直接返回 404,Vue 应用甚至没有机会启动。

解决方式是让服务器执行 SPA 回退:

text 复制代码
如果请求的是现有静态资源 → 返回对应文件
如果请求路径找不到文件 → 返回 index.html

Nginx 常见配置:

nginx 复制代码
location / {
  try_files $uri $uri/ /index.html;
}

流程变成:

text 复制代码
浏览器请求 /users/1001
    ↓
Nginx 找不到对应静态文件
    ↓
回退返回 index.html
    ↓
Vue 应用启动
    ↓
Vue Router 读取 /users/1001
    ↓
渲染 UserDetailView

如果同一 Nginx 还代理接口,通常需要把 API 规则单独配置:

nginx 复制代码
location /api/ {
  proxy_pass http://backend-server/;
}

location / {
  try_files $uri $uri/ /index.html;
}

这样:

text 复制代码
/api/users/1001 → 转发给后端
/users/1001     → 返回前端 index.html

服务器 fallback 解决的是"让 Vue 应用有机会启动",前端仍应配置自己的 404 路由,负责显示用户能看到的未找到页面。


二十、什么是导航守卫

导航守卫是在路由切换过程中执行的函数,用来决定:

text 复制代码
是否允许进入;
是否取消导航;
是否重定向;
是否等待异步校验完成。

最常见的是全局前置守卫:

js 复制代码
router.beforeEach((to, from) => {
  console.log('准备从', from.fullPath)
  console.log('进入', to.fullPath)
})

其中:

text 复制代码
to:准备进入的目标路由
from:即将离开的当前路由

常见返回值:

js 复制代码
return true
// 或不返回:继续导航

return false
// 取消导航

return { name: 'login' }
// 重定向到登录页

守卫也可以是异步函数:

js 复制代码
router.beforeEach(async to => {
  const allowed = await checkPermission(to)

  if (!allowed) {
    return { name: 'forbidden' }
  }
})

现代写法优先通过 return 控制导航。旧代码中常见的第三个参数 next 仍被支持,但分支复杂时容易重复调用或漏调用,不建议新代码机械沿用。


二十一、使用 meta 实现登录校验

在路由配置中标记需要登录的页面:

js 复制代码
const routes = [
  {
    path: '/login',
    name: 'login',
    component: LoginView
  },
  {
    path: '/dashboard',
    name: 'dashboard',
    component: DashboardView,
    meta: {
      requiresAuth: true
    }
  }
]

全局守卫统一判断:

js 复制代码
router.beforeEach(to => {
  const token = localStorage.getItem('token')

  if (to.meta.requiresAuth && !token) {
    return {
      name: 'login',
      query: {
        redirect: to.fullPath
      }
    }
  }
})

过程是:

text 复制代码
访问 /dashboard
    ↓
beforeEach 检查 requiresAuth
    ↓
发现没有 token
    ↓
跳转 /login?redirect=/dashboard

登录成功后可以返回原页面:

js 复制代码
const redirect = route.query.redirect

router.replace(
  typeof redirect === 'string'
    ? redirect
    : '/dashboard'
)

必须注意:

前端路由守卫只负责页面访问体验,不能代替后端权限校验。

用户可以修改前端代码、localStorage 或直接请求接口,所以后端仍然必须验证 token 和用户权限。


二十二、三类常见导航守卫

1. 全局守卫

js 复制代码
router.beforeEach((to, from) => {})
router.beforeResolve((to, from) => {})
router.afterEach((to, from, failure) => {})

常见用途:

text 复制代码
全局登录校验;
全局权限判断;
页面标题设置;
埋点和访问统计。

afterEach 在导航完成后执行,不能再取消当前导航,适合埋点等后置逻辑。

2. 路由独享守卫

js 复制代码
{
  path: '/admin',
  component: AdminView,
  beforeEnter: to => {
    if (!isAdmin()) {
      return { name: 'forbidden' }
    }
  }
}

只与某条路由相关的特殊校验,可以放在 beforeEnter

3. 组件内守卫

组合式 API 常用:

js 复制代码
import {
  onBeforeRouteLeave,
  onBeforeRouteUpdate
} from 'vue-router'

onBeforeRouteUpdate(to => {
  loadUser(to.params.id)
})

onBeforeRouteLeave(() => {
  if (formChanged.value) {
    return window.confirm('内容尚未保存,确定离开吗?')
  }
})

对应场景:

text 复制代码
onBeforeRouteUpdate:组件被复用,但路由参数变化
onBeforeRouteLeave:离开当前页面前确认或清理

选项式 API 中还存在 beforeRouteEnter。它执行时目标组件实例还没有创建,所以不能直接访问组件 this


二十三、路由懒加载

静态导入:

js 复制代码
import UserListView from '@/views/UserListView.vue'

const routes = [
  {
    path: '/users',
    component: UserListView
  }
]

构建时,这个页面代码可能被合并到初始 JavaScript 中,即使用户没有进入用户页面,也需要在首屏下载相关代码。

路由懒加载使用动态 import:

js 复制代码
const routes = [
  {
    path: '/users',
    component: () => import('@/views/UserListView.vue')
  },
  {
    path: '/users/:id',
    component: () => import('@/views/UserDetailView.vue')
  }
]

构建工具通常会把这些页面拆成独立代码块:

text 复制代码
首次进入首页
→ 不必立即下载所有用户页面代码

第一次进入 /users
→ 加载 UserListView 对应代码块

它的价值是:

text 复制代码
减少首屏一次性 JavaScript 体积;
把低频页面代码延后到真正访问时加载;
改善大型应用首屏加载压力。

路由懒加载不要写成:

js 复制代码
component: defineAsyncComponent(
  () => import('@/views/UserListView.vue')
)

Vue Router 自己支持返回 Promise 的组件加载函数,路由组件直接写:

js 复制代码
component: () => import('@/views/UserListView.vue')

即可。


二十四、前端 404 路由

可以使用兜底路由匹配没有配置的地址:

js 复制代码
const routes = [
  // 其他路由
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('@/views/NotFoundView.vue')
  }
]

当用户访问:

text 复制代码
/this-page-does-not-exist

Vue Router 会渲染 NotFoundView

这里需要区分两层 404:

text 复制代码
服务器 404:index.html 都没有返回,Vue 没有启动
前端路由 404:Vue 已经启动,但没有匹配到业务路由

history 模式通常需要:

text 复制代码
服务器 fallback 返回 index.html
        +
Vue Router 兜底路由展示 NotFoundView

二者共同工作。


二十五、页面标题和滚动位置

1. 页面标题

路由配置:

js 复制代码
{
  path: '/users',
  component: UserListView,
  meta: {
    title: '用户管理'
  }
}

导航完成后设置标题:

js 复制代码
router.afterEach(to => {
  document.title = to.meta.title
    ? `${to.meta.title} - 管理系统`
    : '管理系统'
})

2. 滚动位置

创建路由时可以配置:

js 复制代码
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    }

    return { top: 0 }
  }
})

这样浏览器前进后退时可以恢复保存的位置,普通进入新页面时回到顶部。

这属于常见工程体验优化,面试中知道配置入口即可。


二十六、常见错误理解

1. SPA 完全不会发送页面请求

错误。

首次进入和刷新时仍然需要请求 index.html,路由切换也可能加载异步代码和接口数据。SPA 主要是不在每次内部导航时重新加载整份文档。

不准确。

RouterLink 是 Vue Router 组件,但默认通常会渲染具有链接语义的 <a>,并接管适合由前端路由处理的导航。

3. route 和 router 是同一个对象

错误。

route 保存当前路由信息,router 提供跳转和路由管理方法。

4. params 和 query 会自动转换类型

错误。

URL 参数通常按字符串读取,需要根据业务主动转换、校验和提供默认值。

5. params 变化一定重新创建组件

错误。

如果仍然匹配相同组件,组件实例通常会被复用,mounted 不会重新执行。

6. history 模式不需要服务器配置

错误。

刷新深层地址时浏览器会直接请求对应 path,服务器通常需要回退到 index.html

7. 前端守卫能保证接口安全

错误。

前端守卫只能改善访问流程,真正的身份认证和权限控制必须由后端执行。

8. afterEach 可以取消导航

错误。

它发生在导航确认之后,适合标题、埋点等后置操作,不能再取消已经完成的导航。

9. 修改整个 route 对象最适合监听参数

不推荐。

route 是响应式对象,但通常应该监听真正关心的字段:

js 复制代码
watch(
  () => route.params.id,
  id => loadUser(id)
)

这样依赖和业务意图更清晰。


二十七、面试高频问题

1. 什么是前端路由

可以这样回答:

前端路由根据浏览器 URL 匹配并渲染对应组件。在 SPA 内部导航时,它通过 hash 或 History API 更新地址,维护当前路由状态,再让 RouterView 切换组件,通常不需要重新加载整份 HTML 文档。

2. Vue Router 为什么切换页面不刷新

可以这样回答:

RouterLink 会接管适合前端处理的链接导航。history 模式利用 pushState、replaceState 和历史事件维护 URL,hash 模式监听 hash 变化;路由器根据新地址匹配路由记录并更新响应式 route,RouterView 随后渲染对应组件,所以不需要浏览器重新请求并加载整份 HTML。

可以这样回答:

RouterLink 用于声明路由链接,默认通常生成 a 标签,并由 Vue Router 接管内部导航;RouterView 是路由组件出口,当前 URL 匹配到的组件会渲染在它所在的位置。嵌套路由需要在父路由组件中继续提供 RouterView。

4. route 和 router 有什么区别

可以这样回答:

route 表示当前路由信息,可以读取 path、params、query 和 meta;router 是 createRouter 创建的路由器实例,用于 push、replace、back 等导航操作。简单说,route 表示当前在哪里,router 用来控制去哪里。

5. params 和 query 有什么区别

可以这样回答:

params 通常是路径结构的一部分,例如 /users/:id 中的用户 id,用于标识具体资源;query 位于问号后,例如分页、搜索和筛选条件。二者从 URL 读取时通常都是字符串,需要业务层进行类型转换和校验。

6. hash 和 history 有什么区别

可以这样回答:

hash 模式的路由位于 # 后,hash 不会随文档请求发送到服务器,因此部署简单,但 URL 带井号;history 模式利用 History API,URL 更自然,但刷新 /users/1 时服务器会收到这个路径,所以必须配置 SPA fallback,把未命中的前端路由返回到 index.html。

7. history 模式刷新 404 怎样解决

可以这样回答:

应在静态服务器或网关上配置回退规则:现有静态资源正常返回,其他前端路径统一返回 index.html,让 Vue 应用启动后再由 Vue Router 匹配 URL。Nginx 常用 try_files $uri $uri/ /index.html,同时 API 路径要单独配置反向代理。

8. 路由守卫有哪些

可以这样回答:

守卫分为全局守卫、路由独享守卫和组件内守卫。beforeEach 常做全局登录校验,beforeEnter 处理某条路由的特殊规则,onBeforeRouteUpdate 处理组件复用时的参数变化,onBeforeRouteLeave 处理离开确认,afterEach 用于导航完成后的标题或埋点逻辑。

9. 路由守卫能代替后端权限校验吗

可以这样回答:

不能。前端代码和本地 token 都可能被用户修改,守卫只能阻止正常界面流程,攻击者仍然可以直接请求接口。后端必须独立验证身份和权限,前端守卫只负责用户体验和页面入口控制。

10. 路由参数改变为什么 mounted 不执行

可以这样回答:

/users/1/users/2 仍然匹配同一个用户详情组件,Vue Router 会复用原组件实例,因此 setup 和 mounted 不会重新执行。应该 watch route.params.id,或使用 onBeforeRouteUpdate 响应参数变化。

11. 路由懒加载是什么

可以这样回答:

路由懒加载把路由组件写成 () => import('./Page.vue')。构建工具会进行代码分割,在第一次访问对应路由时才加载组件代码,从而减少首屏一次性下载的 JavaScript。路由组件不需要再套 defineAsyncComponent。

12. push 和 replace 有什么区别

可以这样回答:

push 会向浏览器历史栈增加一条记录,用户可以后退回来;replace 会替换当前记录,不新增历史。普通页面进入常用 push,登录完成或纠正地址时经常使用 replace。


二十八、本文知识地图

1. 路由工作流程

text 复制代码
URL 变化
    ↓
Vue Router 匹配 routes
    ↓
更新当前 route
    ↓
RouterView 切换组件

2. 两个核心组件

text 复制代码
RouterLink:发起前端路由导航
RouterView:展示匹配到的路由组件

3. 两个核心对象

text 复制代码
route:path、params、query、meta
router:push、replace、back、go

4. URL 参数

text 复制代码
params:资源身份和路径结构
query:分页、搜索、筛选等附加状态

5. 路由模式

text 复制代码
hash:# 后的路由不发送给服务器,部署简单
history:URL 自然,但服务器需要回退 index.html

6. 守卫

text 复制代码
beforeEach:全局前置判断
beforeEnter:路由独享判断
onBeforeRouteUpdate:复用组件的路由变化
onBeforeRouteLeave:离开页面前判断
afterEach:导航完成后的处理

7. 性能

text 复制代码
component: () => import('./Page.vue')
→ 路由级代码分割
→ 访问时再加载

二十九、总结

Vue Router 的核心不是"切换几个组件",而是建立 URL 和页面状态之间的关系:

text 复制代码
URL 决定应该展示哪个页面;
RouterLink 或 router 发起导航;
route 保存当前地址信息;
RouterView 渲染匹配到的组件。

SPA 内部跳转不重新加载整份 HTML,是因为 Vue Router 使用 hash 或 History API 维护 URL,再通过响应式路由状态驱动组件更新。

动态路由使用 params 表达资源身份,query 适合分页、搜索和筛选。相同路由组件在 params 变化时通常会被复用,因此要监听具体参数,而不能只依赖 onMounted

history 模式的 URL 更自然,但刷新深层地址时浏览器会真正请求该路径,所以服务器必须配置回退到 index.html;前端还需要自己的兜底路由显示 404 页面。

导航守卫可以改善登录和权限页面的访问流程,但不能代替后端鉴权。路由懒加载则通过动态 import,把页面代码延迟到访问时加载。

对于秋招,最终应该能独立回答这条链路:

text 复制代码
点击 RouterLink
→ URL 无刷新变化
→ Vue Router 匹配路由
→ 守卫确认导航
→ 必要时加载路由代码
→ RouterView 更新页面

参考资料

相关推荐
無限進步D2 小时前
Java Web 前端 简介
java·开发语言·前端·css·html·css3·web
码士集团小青2 小时前
从落地到“半停用“:qiankun 微前端在一个 Vue 3 + Vite 项目中的完整实践与反思
前端
the局外人2 小时前
掌控自己的 K 线数据:TradingView 本地部署与数据源接入
前端·vue.js·websocket
deli0073 小时前
配色对比度检查器:一眼看清文字和背景谁看不清
前端
梦诺3 小时前
vue3 keepAlive+记录滚动条
前端·javascript·vue.js
葡萄城技术团队3 小时前
SpreadJS V19.2 新特性揭秘:甘特表的进度线
前端
计算机魔术师3 小时前
黄仁勋台上接特朗普电话开免提,全场听到一句话:AI 不会减速
前端
yivifu4 小时前
HTML元素的textContent和innerText两个属性的差别
前端·html
咸鱼老弟4 小时前
Speculative Decoding(投机采样):大模型"先猜后验",生成速度翻倍
前端·算法·ai编程