ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

vue-router 路由配置与重定向实战:从入门到避坑指南

vue-router 路由配置与重定向实战:从入门到避坑指南 1. 项目概述与核心需求拆解做 Vue 开发绕不开一个话题路由。哪怕你只是搭一个简单的后台管理界面也一定会遇到页面切换、地址栏变化、浏览器返回键失效这类问题。vue-router 就是专门解决这些问题的一套前端路由方案。先说清楚它到底解决什么问题。传统多页面应用里页面切换是浏览器发请求、服务器返回新 HTML 的过程每跳一次页面就要刷新一次。而 Vue 是单页应用所有页面其实都在同一个 HTML 文件里组件之间的切换靠 JavaScript 控制。那问题就来了用户怎么知道当前处于哪个页面浏览器刷新时怎么保持状态分享链接时对方看到的是不是同一个界面vue-router 干的就是这件事——通过监听 URL 变化把地址栏里的路径映射到对应的组件同时把组件渲染到页面上期间不发生真正的页面刷新。这篇文章适合谁看刚接触 Vue 不久、手头正在做 Vue 项目的初学者还有那些已经用了 vue-router 但只是照着文档复制粘贴、遇到问题不知道从哪下手的开发者。我会从路由的底层思路讲起再到实际配置、重定向、路由模式的选择最后附上我在实际项目中踩过的坑和排查方法尽量做到看完能直接用。我自己用过 vue-router 从 3.x 到 4.x也经历过 vue2 迁移 vue3 过程中路由写法的变化。很多东西光看文档和实际跑一遍是完全不同的感受这篇文章里的内容都是我实际敲过、调试过的不是抄文档。2. 环境准备与安装依赖2.1 Vue 项目搭建基础要说 vue-router 的使用得先有一个能跑起来的 Vue 项目。最常用的方式是用官方脚手架执行下面的命令npm create vuelatest这里稍微提一嘴现在官方推荐的是 create-vue 这个脚手架它会引导你选择需要的功能包括 TypeScript、JSX、Vue Router 等。如果你用的是比较老的 vue-cli 方式也一样能跑但建议新项目直接用新的脚手架。创建一个 Vue 3 Vite 的项目在命令行里一路回车选择默认配置就好✔ Project name: vue-router-demo ✔ Add TypeScript: No ✔ Add JSX Support: No ✔ Add Vue Router for Single Page Application: Yes ✔ Add Pinia for state management: No ✔ Add Vitest for Unit Testing: No ✔ Add Cypress for End-to-End Testing: No ✔ Add ESLint: Yes ✔ Add Prettier: Yes要注意的是在脚手架里直接勾选 Vue Router它会把路由的基础配置目录都生成好省得自己去创建文件。不勾选的话后续手动安装配置也完全可以核心逻辑是一样的。2.2 vue-router 的安装与版本选择如果脚手架阶段没有勾选 Vue Router或者你的项目已经搭好了、现在想补上路由那就手动安装npm install vue-router4这里有一个初学者最容易踩的坑版本匹配问题。vue-router 3.x 对应 Vue 2vue-router 4.x 对应 Vue 3。如果你在 Vue 3 项目里装成了 3.x 的 vue-router会有各种莫名其妙的错误比如路由配置后不生效、组件不渲染等。安装的时候如果拿不准最新版本是多少直接npm view vue-router version查看一下当前发布的最新版本号确保是 4 开头的就行。安装完成之后项目结构里通常要手动创建 router 目录和 index.js 文件。我习惯的文件结构是这样的src/ ├── router/ │ └── index.js ├── views/ │ ├── HomeView.vue │ └── AboutView.vue ├── App.vue └── main.jsviews 目录存放页面级组件router 目录只负责路由配置。这样分层的好处是后续项目变大之后路由配置不需要去每个组件里找统一在一个地方管理就行。3. 路由配置与核心原理3.1 最小可用配置从入口到页面渲染装的好了先来一个最基础的路由配置跑通流程。在 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)对应 router/index.js 里的配置import { createRouter, createWebHashHistory } from vue-router import HomeView from ../views/HomeView.vue const routes [ { path: /, name: home, component: HomeView }, { path: /about, name: about, component: () import(../views/AboutView.vue) } ] const router createRouter({ history: createWebHashHistory(), routes }) export default router这段配置里有两个细节值得展开说。第一HomeView 是直接 import 进来的AboutView 用了动态 import 的方式。这就是懒加载项目打包时AboutView 会被单独拆成一个 chunk 文件只有用户访问/about这个路径时才会去加载它。大型项目里如果不做代码分割首屏加载的 JS 文件会非常庞大影响用户体验。所以一般做法是首屏必需的组件直接 import其余页面尽量懒加载。第二createRouter 是 Vue Router 4 的构造函数接收一个配置对象其中 history 字段决定了路由模式。这块内容后面专门讲先用 createWebHashHistory 跑通是最省事的。在 App.vue 里页面组件通过router-view /渲染出来template div classmain-container nav router-link to/首页/router-link router-link to/about关于/router-link /nav router-view / /div /template3.2 为什么用 router-link 而不是a href这里有一点特别容易让新手误解既然路由最终改变的是 URL那直接用a href/about行不行本质上不行或者说能行但效果不对。a href会触发浏览器完整的页面刷新整个 Vue 应用会被重新加载白白浪费了单页应用的意义。而router-link做的事情是拦截点击事件阻止浏览器默认跳转行为然后通过 router 的内部机制修改 URL 并匹配新组件渲染。整个过程是不刷新页面的这也是路由能实现页面无感切换的关键。router-link还额外提供了一些能力比如默认会加上router-link-active和router-link-exact-active类名做导航高亮的时候直接用 CSS 控制就行不需要自己写 JS 去判断当前路径。3.3 嵌套路由和参数传递实际项目里通常不会只有顶层的几个页面更多时候是嵌套的布局结构。比如一个后台管理系统侧边栏是固定的菜单只有中间的内容区随路由变化。这种场景就需要嵌套路由。const routes [ { path: /admin, component: () import(../views/AdminLayout.vue), children: [ { path: dashboard, component: () import(../views/admin/Dashboard.vue) }, { path: users, component: () import(../views/admin/Users.vue) } ] } ]AdminLayout.vue 里同样放一个router-view /子路由的组件就会渲染到这个嵌套的路由出口里。这里有一个新手容易忽视的点子路由的 path 不能以/开头否则会被当成绝对路径脱离父级路由的结构。参数传递也是路由使用的高频场景。最常见的有两种路径参数和查询参数。路径参数通常用于详情页这类需要携带 ID 的场景{ path: /product/:id, name: product-detail, component: () import(../views/ProductDetail.vue) }组件内部通过route.params.id拿到这个参数。要注意的是从/product/1跳转到/product/2时组件的 created 或 mounted 钩子不会重新执行因为 Vue 复用了同一个组件实例。这时要么在组件里监听route对象的变化要么在路由配置中使用:key强制组件重新渲染router-view :key$route.fullPath /查询参数就是 URL 里?userId123这种形式通过route.query.userId读取。这种传参方式适合不要求整洁 URL 的场景比如筛选条件。4. 重定向的实现与业务场景4.1 静态重定向配置方式重定向是个看着简单但实际很常用的功能。它的核心场景是当用户访问某个 URL 时我们希望把用户送到另一个 URL。基础写法是这样const routes [ { path: /home, redirect: / }, { path: /, component: HomeView } ]当用户访问/home路由会自动跳转到/地址栏也会同步变化。还有一种写法是给 redirect 传一个对象可以指定不同的路由模式{ path: /home, redirect: { name: home } }这种写法通过路由名称跳转好处是即使以后把/路径改成其他地址redirect不受影响维护性更好。4.2 动态重定向的应用有些场景下重定向的目标需要根据实际情况决定比如根据用户是否登录跳转到登录页或首页。这时 redirect 可以写成函数{ path: /profile, redirect: (to) { const token localStorage.getItem(token) if (token) { return { path: /user-center } } else { return { path: /login } } } }redirect 函数接收一个to参数就是用户当前访问的路由信息可以根据业务逻辑返回新的路由地址。这里有个常见的业务场景用户访问一个不存在的历史遗留链接。之前的项目改版后很多旧链接已经失效了但搜索引擎和用户收藏夹里还留着。不能直接返回 404 页面否则流失了大量流量。统一用动态重定向把旧链接映射到新页面上用户体验不受影响。4.3 重定向和别名alias的区别这个点很容易混。重定向会改变地址栏 URL用户从/home被送到/地址栏显示的是/。而别名是让同一个组件能够对应多个路径地址栏保持不变。{ path: /, component: HomeView, alias: /home }这种情况下访问/和/home都会渲染 HomeView但地址栏显示的 URL 是各走各的访问/home就显示/home。什么时候用重定向什么时候用别名我的经验是如果你希望表达这个页面彻底换地址了旧地址不要再被使用用重定向如果你希望这个页面有多个名字哪个都能访问但布局和内容完全相同用别名。4.4 实践中的坑重定向次数过多在用重定向的时候最容易踩的坑是将您重定向的次数过多这个浏览器错误。出现这个问题的原因几乎永远是重定向形成了一个环路。举例来说{ path: /home, redirect: /dashboard } { path: /dashboard, redirect: /home }用户访问/home被重定向到/dashboard访问/dashboard又被重定向回/home如此循环浏览器就会报错。排查这类问题的方法打开浏览器的开发者工具切到网络面板查看那些 302/301 响应。看到跳转链成环了基本就能定位问题。如果业务逻辑上觉着自己没写环路那再仔细看下是不是某个路由的 redirect 和 alias 指向了自己或者嵌套路由里父子路径重定向一致导致的。4.5 实战未找到路由的兜底处理404 页面也是重定向的一个应用场景。如果用户访问一个根本没有匹配的路由默认情况下页面内容区域会空白用户不知道发生了什么。更好的体验是跳转到一个 404 页面。{ path: /:pathMatch(.*)*, name: not-found, component: () import(../views/NotFoundView.vue) }这里/:pathMatch(.*)*是 Vue Router 4 的通配语法替代了旧版的*。必须放在 routes 数组的最后一项否则它会拦截所有路由匹配导致其他页面无法访问。配合重定向{ path: /:pathMatch(.*)*, redirect: /404 } { path: /404, component: () import(../views/NotFoundView.vue) }这样地址栏会变成/404同时渲染出 404 页面。5. 路由模式的选择与底层机制5.1 hash 模式和 history 模式的区别vue-router 提供两种路由模式hash 模式和 history 模式。这也是面试官最爱问的问题之一。hash 模式的 URL 长这样http://localhost:8080/#/abouthistory 模式的 URL 长这样http://localhost:8080/about差异看起来只是有没有#但底层的实现机制完全不同。hash 模式依靠的是window.location.hash的变化触发hashchange事件进而驱动路由切换。hash 是 URL 的锚点部分它的变化不会导致浏览器像服务器发送请求所以完全在前端控制。history 模式依靠的是 HTML5 新增的 History API核心是pushState和replaceState。这两个方法可以修改浏览器的历史记录和地址栏 URL但不触发页面刷新。既然 history 模式看起来更干净为什么不都用 history因为这个模式有一个很重要的前提条件服务器必须配置兜底。当用户直接访问http://example.com/about时服务器是没有/about这个物理文件或接口的如果服务器不支持把未知路径重定向到前端的 index.html就会返回 404。hash 模式没有这个问题因为#后面的内容永远不会被发送到服务器。这也是为什么我建议初学者和内部项目先用 hash 模式的原因——省心。5.2 两种模式的配置与切换在 vue-router 4 中通过 createWebHashHistory 和 createWebHistory 来指定模式import { createRouter, createWebHashHistory } from vue-router // 或 import { createRouter, createWebHistory } from vue-routerconst router createRouter({ history: createWebHashHistory(), // 或 createWebHistory() routes })vue-router 3 中的写法是在 new Router 时通过mode: hash或mode: history来指定。这个迁移要注意旧写法在 vue-router 4 中已经不适用了。5.3 部署 history 模式时的服务器配置如果你在正式环境用了 history 模式服务器的配置是必须处理的一环。以 Nginx 为例典型的配置是在 server 块里加一个 location 规则server { listen 80; server_name example.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }这一句try_files $uri $uri/ /index.html的意思是先尝试按当前的 URI 去找静态文件找不到就去找对应的目录再找不到就把请求重写到 index.html让前端路由接管这次请求。如果漏了这一步直接访问http://example.com/about就会得到 Nginx 的默认 404 页面但用户从首页点击导航跳转到/about却能正常打开。这个现象很迷惑人排错的时候容易先怀疑前端代码实际上问题在服务器。其他服务器也类似Node.js 部署时可以借助中间件Apache 可以配置.htaccess文件RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L]5.4 路由模式的选型建议聊完两种模式的区别聊一下我的选择经验。纯前端项目、Mock 环境、或者不需要用户直接输入深层 URL 的场景直接用 hash 模式运行最简单不会出幺蛾子。如果你的项目部署在可控的服务器上比如自己团队维护的 Nginx、Node 服务并且希望 URL 对用户友好、利于分享那建议用 history 模式。具体配置方案就是上面说的 try_files 那一段。还有一点如果项目部署在对象存储服务或者 CDN 的静态页面上很多这类服务不支持改写路径那就只能用 hash 模式。这是从部署平台反推选型的一个典型案例。6. 实战完整路由配置示例6.1 场景设定与路由规划为了把上面的内容串起来我准备用一个后台管理系统的路由配置来做演示。这个场景可以覆盖常用功能嵌套布局、首屏懒加载、参数传递、重定向、404 兜底。场景假设这样一个后台系统功能模块如下登录页/login管理后台/admin下面是仪表盘、用户列表、用户详情、设置访问未定义路径时重定向到 404 页面未登录用户访问后台页面时重定向到登录页按这个需求规划路由表路径组件说明/loginLoginView登录页公开访问/adminAdminLayout后台布局含侧边栏和内容区/admin/dashboardDashboardView仪表盘/admin/usersUserListView用户列表/admin/users/:idUserDetailView用户详情/admin/settingsSettingsView设置页/redirect 到 /admin/dashboard根路径兜底/:pathMatch(.)NotFoundView404 页面6.2 路由表代码实现按照上面的规划完整的路由配置如下import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, name: login, component: () import(../views/LoginView.vue), meta: { title: 登录, requiresAuth: false } }, { path: /admin, component: () import(../layouts/AdminLayout.vue), redirect: /admin/dashboard, meta: { requiresAuth: true }, children: [ { path: dashboard, name: dashboard, component: () import(../views/dashboard/index.vue), meta: { title: 仪表盘 } }, { path: users, name: user-list, component: () import(../views/users/list.vue), meta: { title: 用户列表 } }, { path: users/:id, name: user-detail, component: () import(../views/users/detail.vue), meta: { title: 用户详情 } }, { path: settings, name: settings, component: () import(../views/settings/index.vue), meta: { title: 设置 } } ] }, { path: /, redirect: /admin/dashboard }, { path: /:pathMatch(.*)*, name: not-found, component: () import(../views/error/NotFoundView.vue), meta: { title: 页面不存在 } } ]6.3 导航守卫中的登录校验逻辑上面配置里出现了meta.requiresAuth这个字段配合路由守卫可以实现登录校验。这是后台管理系统里几乎必用的能力。全局前置守卫的写法如下router.beforeEach((to, from, next) { const token localStorage.getItem(token) document.title to.meta.title ? ${to.meta.title} - 后台管理系统 : 后台管理系统 if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })这段代码做了两件事。第一根据路由的 meta.title 动态设置浏览器标签页的标题。第二检查目标路由是否要求登录如果要求且没有 token就跳转到登录页并且通过 query 参数把原始目标地址带上这样登录成功后可以自动跳回用户原本想访问的页面。登录页里跳回的代码const redirectPath route.query.redirect || /admin/dashboard router.push(redirectPath)这样整套后台的访问控制就闭环了。6.4 优化细节动态路由或静态路由的选择刚上面的配置是静态路由即所有路由在应用启动时就全部注册完成。还有一种是动态路由登录时根据用户的权限把可见的路由追加到路由表里。这个在后端管理系统中也很常用实现方式是 router.addRoute()。router.addRoute({ path: /admin/reports, name: reports, component: () import(../views/reports/index.vue) })动态路由的好处是可以精确控制不同角色能访问的页面。但要注意动态添加的路由在用户退出登录时需要清掉否则下次登录的人还会看到之前的路由。清空的方式通常是路由表重置或者刷新页面因为刷新后整个应用会重新初始化。如果项目角色权限不复杂我建议直接用静态路由加按钮级权限控制简单可靠。动态路由虽然看起来灵活但它会引入路由表维护、权限比对、退出清理等多一套逻辑复杂度会上升不少。7. 常见问题排查与避坑指南7.1 路由配置了但不生效这是一个高频问题新手的页面怎么点击都跳转不了或者跳转了但内容是空白的。绝大部分原因是以下两种第一路由组件没有写router-view /。路由只是匹配规则真正把组件渲染到页面上需要router-view组件的存在而且要注意嵌套路由中每一层父组件都要有自己的router-view。如果只配置了一级路由但 App.vue 里没有写 router-view那匹配到了组件也没有出口渲染页面就是空白。第二路由实例没有挂载到应用上也就是 main.js 里漏了app.use(router)。这种情况组件渲染正常但是 router-link 的跳转不会工作页面始终停留在初始状态。排查建议先打开页面看地址栏是否随点击变化如果地址变了但内容没变问题出在 router-view如果地址也没变问题出在路由挂载或 router-link 上。7.2 刷新页面出现 404这个问题的场景在 history 模式下会出现在/about页面刷新浏览器直接显示 404或者白屏。原因前面已经提到浏览器向服务器请求了/about但服务器没有这个资源也没有做 rewrites 到 index.html 的配置。遇到这个问题第一件事不是改代码而是检查服务器配置。Nginx 加 try_files 规则Node.js 确保所有的路由都返回 index.htmlApache 加 .htaccess。配置改完记得重启服务再试。7.3 页面跳转后滚动条位置不对单页应用有个体验问题从一个很长的列表页跳转到另一个页面新页面的滚动位置可能保留在之前的位置用户还得手动滚回顶部。这在小的内部项目里看着无伤大雅但面向用户的公开页面体验很差。解决方法是设置路由的 scrollBehaviorvue-router 4 中这样配置const router createRouter({ history: createWebHashHistory(), routes, scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition } else { return { top: 0 } } } })这样切页时默认回到顶部浏览器前进后退时恢复原滚动位置体验会顺畅很多。7.4 vue-router 3 迁移到 4 时的常见变化从 Vue 2 项目升级到 Vue 3 时路由迁移有几个地方容易漏。路由模式配置从mode: hash变成了createWebHashHistory()或createWebHistory()构造函数从new Router()变成了createRouter()。这两个变化如果不改代码直接跑不起来。*通配符路由变成了/:pathMatch(.*)*。router-link上的tag属性被移除了以前可以用 tagli 让 router-link 渲染成 li 标签现在这个属性不生效需要用custom属性配合插槽来实现。navigation guards的返回值语义也有一些调整可以返回一个路由地址来重定向返回值false表示取消导航这些细节建议迁移时看一遍官方 migration 文档。7.5 团队协作中的路由管理建议代码层面之外路由配置这块在团队项目里有几条经验值得分享。路由配置文件的变动在 code review 中经常被忽略但这恰恰是影响面最大的文件之一。我建议团队里约定路由表按模块拆分不要全部堆在 index.js 里借助 es module 的导出功能把不同模块的路由文件汇总到一起。从工程角度说URL 一旦对外发布它的修改成本就非常高因为用户收藏、搜索引擎收录、其他系统跳转都有可能依赖。所以路由路径的规划要在一开始就做仔细尤其是线上项目。改路径的时候别忘了用 redirect 保留旧路径的兼容不要直接删除。8. 扩展常见面试题角度解析聊点题外话。如果你正在准备前端面试vue-router 是 Vue 方向几乎必考的知识点。我梳理几个常见的问题角度帮助你把上面的知识串起来。第一个问题vue-router 的实现原理是什么答的时候要点出 hash 模式和 history 模式各自的机制hash 靠 hashchange 事件history 靠着 pushState/replaceState并且能提到 history 模式下刷新页面需要服务端配合。能把这个说清楚面试官基本觉得你掌握到位了。第二个问题动态路由和权限控制怎么做这个问题考察的是 addRoute、路由守卫配合 meta 信息。把文中第 6.4 节的内容讲清楚能说出路由表重置的必要性就比只会说登录后 addRoute的人强很多。第三个问题路由守卫的执行流程是什么全局前置守卫、路由级守卫、组件内守卫的执行顺序以及常见的登录校验写法。这个只要把 beforeEach 的代码写出来再把不同守卫的执行时机讲一遍就够了。第四个问题如何解决路由切换时组件不刷新的问题这就是第 3.3 节提到的参数变化组件复用问题答出key绑定即可。这些面试题本质上是变相考察对路由机制的深刻理解而不仅仅是背答案。9. 最小可行工程实用模板建立一个最小可用的路由示例是学习最快的路径。我把自己常用的一个模板整理在这里方便直接复制修改使用。项目根目录内的src/main.jsimport { createApp } from vue import App from ./App.vue import router from ./router const app createApp(App) app.use(router) app.mount(#app)src/router/index.jsimport { createRouter, createWebHashHistory } from vue-router const routes [ { path: /, redirect: /home }, { path: /home, component: () import(../views/Home.vue), meta: { title: 首页 } }, { path: /detail/:id, component: () import(../views/Detail.vue), meta: { title: 详情 } }, { path: /:pathMatch(.*)*, component: () import(../views/NotFound.vue) } ] const router createRouter({ history: createWebHashHistory(), routes, scrollBehavior() { return { top: 0 } } }) export default routersrc/App.vuetemplate div nav router-link to/home首页/router-link router-link to/detail/1详情页/router-link /nav router-view / /div /templatesrc/views/Home.vue和src/views/Detail.vue是最简单的页面组件template div h1首页/h1 p这是首页内容/p /div /template script setup /scripttemplate div h1详情页/h1 p当前 ID{{ route.params.id }}/p /div /template script setup import { useRoute } from vue-router const route useRoute() /script这个模板虽然简单但覆盖了重定向、懒加载、动态路由参数、404 兜底、路由模式切换日常小项目里基本可以直接用起来。10. 我在实际使用中的几个经验心得最后聊几点实战经验也是我在项目里踩过多次坑之后总结出来的。一个比较常用的实践是路由的统一命名规范。我和团队约定路由的 name 必须以模块名为前缀比如用户模块下的列表页就叫 user-list 而不是 list。突然有十个模块的列表页都叫 list 的时候你就知道统一命名有多重要了。路由跳转时优先用 name而不是直接写路径字符串这样后面改路径不会牵连到一堆跳转代码。另外路由文件的组织方式建议是按模块拆分再汇总。项目里的路由文件从一开始就不要只依赖一个 index.js 装下所有代码。拆分好模块梳理逻辑时不会一团乱。我的常用方式是在 router/modules 目录下放每个业务模块的路由文件然后在 index.js 里用展开运算符合并。再有一个常常被忽略的事情权限配置尽量用 meta 字段收敛管理。不要到处在守卫里写死逻辑判断把需要登录、需要管理员、需要特定权限这些标记统一放在 meta 里守卫里只处理统一的逻辑判断。这样路由的表意清晰改权限规则时也不会在代码里大海捞针。这些经验都是在实践中慢慢积累的。如果你刚接触路由这块不用着急把所有高级用法都学会先把基础的路由配置、跳转、路由模式跑通用一个小的项目把流程串起来然后再慢慢接触权限控制、路由守卫、动态路由这些进阶功能会自然很多。
返回列表