前端路由这个东西我刚入行的时候觉得挺玄乎的不就是地址栏 URL 变一下吗后来真正接手一个大型管理系统从“一个页面一个 HTML”的老项目切到单页应用才意识到路由是整个前端架构的承重墙。它管的不只是“地址长什么样”还决定了代码怎么组织、权限怎么拦、资源怎么加载、页面间怎么跳转、浏览器前进后退怎么配合甚至连部署上线的方式都得跟着它走。这篇文章我就从一个实际项目的角度把前端路由系统从头到尾拆一遍先说清楚它到底解决什么问题再讲透 hash 和 history 两条技术路线的原理然后落到 Vue Router 的具体配置和实战代码上最后把我在生产环境里踩过的坑和排查思路整理成清单。不管你是刚接触 SPA 的新手还是被路由跳转、权限拦截、懒加载折腾过的老开发这份内容应该都能让你少走点弯路。前端路由虽然各家框架都有对应实现但底层逻辑是相通的。理解了原理再去看 Vue Router、React Router 甚至小程序路由基本都是一通百通的状态。1. 先搞清楚路由到底解决了什么问题1.1 没有路由的时代前端是怎么玩的我最早做网站的时候页面切换是真·跳页面。用户点一个链接浏览器向服务器发请求服务器返回一个新的 HTML整个页面重新加载白屏一下然后内容出现。这种模式现在叫 MPAMulti-Page Application多页应用对应的是服务端路由URL 和服务器上的文件路径一一对应。MPA 模式有个很直观的问题每次跳转都要请求一次完整文档。哪怕是局部变化很小比如只是切换一个 Tab整个 header、footer、脚本都要重新下一遍。更麻烦的是页面之间共享的 JavaScript 状态完全留不住——你在 A 页面填写到一半的输入框内容跳到 B 页面再返回就全没了因为浏览器重新加载了整个文档。后来 Ajax 普及了大家发现可以在不跳转页面的情况下获取数据、局部更新 DOM。最初的玩法很原始页面里放几个 div点击按钮动态切换 div 的显示隐藏或者用 JavaScript 改 innerHTML。这种做法和 jQuery 时代的“选项卡”是同一套思路。但问题马上出现了应用变得复杂之后纯靠 JavaScript 状态来切换视图地址栏始终是一个固定地址。用户在 A 视图刷新一下直接回到初始状态想把当前页面发给同事看对方看到的永远只是入口页浏览器前进后退也完全失效因为所有视图切换根本没改变历史记录。那时候各种临时方案都有比如把当前状态存进 cookie、存进 localStorage但都是在打补丁。这套混乱的局面本质原因就是一个视图和 URL 之间失去了对应关系。前端路由要解决的核心问题就是把这个对应关系重新建立起来。1.2 SPA 兴起后路由为什么变成了刚需单页应用SPASingle-Page Application的思路是把整个应用的所有视图都打包进同一组 JavaScript 文件页面加载一次之后后续的“跳转”不再向服务器要 HTML而是在内存里切视图、动态改 DOM。代价是应用启动时需要加载的资源更多收益是交互体验像原生应用一样顺滑切页面不刷新、没有白屏闪烁、内部状态在视图切换间可以保留。SPA 想要把视图切换和 URL 联动起来必须有这么几个基础能力地址栏发生变化时拦截浏览器默认的整页刷新行为改成“仅切换视图”根据当前的 URL 解析出对应的视图组件并渲染到指定区域视图切换时把新的 URL 推入浏览器历史栈让前进后退可用应用初始化或刷新时能根据当前 URL 恢复出正确的视图状态。这就是前端路由的核心职责。没有它SPA 只能算是一个“用脚手架搭起来的复杂单页页面”根本称不上“应用”。也正因如此现代框架把路由作为单独的一层来做比如 Vue 生态里的 Vue Router、React 生态里的 React Router它们不是简单的“URL 解析函数”而是一整套状态管理工具路由表、匹配规则、导航流程、守卫、懒加载机制全都有标准实现。前端路由其实是在回答一个问题当前 URL 应该渲染什么以及用户从哪里来、要到哪里去、能不能去。2. 路由的核心原理hash 与 history 两条路线2.1 hash 路由为什么 # 后面不会刷新页面Vue Router 默认用的就是 hash 模式地址长这样https://example.com/#/home。有人会疑惑为什么 URL 里带个井号就能实现无刷新跳转这得从 URL 规范说起。URL 里的#后面那段叫 fragment片段标识符它的原始设计意图是页面内的锚点定位比如https://example.com/doc#section-2表示定位到文档里 id 为section-2的元素。关键是fragment 部分的变化不会触发浏览器向服务器发送新的请求也不会导致页面重新加载只会在当前文档内部做锚点滚动。前端路由利用的正是这个机制把 # 后面的内容当作路由路径来用监听浏览器的hashchange事件一旦#后面的值变了就拿到这个值匹配对应的视图组件渲染到页面上。因为 URL 只是 fragment 变了服务器根本感知不到任何变化自然也不会发生整页刷新。手动实现一个最简单的 hash 路由核心代码其实就这部分window.addEventListener(hashchange, () { const hash window.location.hash.slice(1) || /; // 根据 hash 渲染对应视图 render(hash); }); // 编程式导航 function navigate(path) { window.location.hash path; }hash 路由的实现成本低兼容性极好浏览器端和服务器端都不需要特殊配置。这是它至今还是默认模式的原因。但它的缺点也很明显URL 里带着一个#在视觉上不够正规更关键的是它靠hashchange事件驱动而 hash 是不会被作为请求发送到服务器的导致 SEO 对这类页面内容的收录能力很弱。不过话说回来现在的单页应用如果在意 SEO通常会配合服务端渲染来做也不会单纯靠 hash 模式硬扛。2.2 history 路由利用 History API 接管地址栏HTML5 给浏览器引入了 History API让 JavaScript 可以修改浏览器的历史记录和当前地址而不触发页面刷新。Vue Router 的 history 模式就是基于这里的pushState、replaceState和popstate事件来实现的。history.pushState(state, title, url)可以把当前地址改成url并把这一条记录推入历史栈但不会触发浏览器跳转也不会重新加载页面。注意它甚至不会触发popstate事件因为popstate只在用户点击浏览器前进/后退按钮或者 JavaScript 调用history.back()、history.forward()时才触发。所以 history 路由的驱动逻辑就分成两部分编程式导航比如点击菜单先用pushState把地址改掉然后直接手动渲染对应的视图组件用户点击浏览器前进/后退监听popstate事件拿到当前地址后解析并渲染。一个最基本的 history 路由实现大概是这样的// 点击菜单时 document.querySelector(#menu-home).addEventListener(click, (e) { e.preventDefault(); history.pushState(null, , /home); render(/home); }); // 浏览器前进/后退时 window.addEventListener(popstate, () { const path location.pathname location.search; render(path); });history 模式的 URL 干净没有#前后端配合得好时可以直接做到“真地址访问”对 SEO 的支持也比 hash 模式更好。但它有一个致命前提由于pushState改的是真实路径用户一旦直接访问/home这个地址或者在这个地址下按 F5 刷新浏览器就会向服务器发起真实的 HTTP 请求。如果服务器没有对/home这个路径做相应的回退处理就会返回 404。这就是后面要重点讲的“部署配置”问题。2.3 hash 和 history 到底选哪个很多初学者会问“Vue Router 默认是 hash是不是用 hash 就行了”。我的建议是不要偷懒根据项目实际场景定。对比维度hash 模式history 模式URL 形式带#不带#部分不发给服务器干净和普通路径无区别部署要求任何静态服务器都行需要服务器把未知路径回退到入口页SEO 能力弱爬虫较难抓取内容配合 SSR/预渲染才有效兼容性老浏览器也支持现代浏览器均支持适合场景内网系统、快速开发、演示环境面向公网的业务系统、对 URL 有要求的场景我个人的做法是做管理后台这类对 SEO 完全无感的系统直接用 hash省心做公开访问的业务站点则上 history 模式并在部署时严格配置好 nginx 的 try_files 回退。需要特意说一句无论选哪种模式前端代码解析 URL 的逻辑都要有兜底。hash 模式要处理“带 #”和“不带 #”两种情况history 模式要处理路径末尾带不带斜杠、有没有 query、有没有 search 参数等边界情况。这些细节就是项目里路由 bug 的高发区。3. Vue Router 实战搭建一套完整的路由体系3.1 安装与基础配置Vue Router 4 对应 Vue 3Vue Router 3 对应 Vue 2这两个版本在 API 上有差异下面的示例统一按 Vue 3 Vue Router 4 来写。先用包管理器安装npm install vue-router4安装完成后在项目里新建一个router/index.js做集中配置。最基础的配置如下import { createRouter, createWebHistory } from vue-router; const routes [ { path: /, name: Home, component: () import(/views/Home.vue) }, { path: /about, name: About, component: () import(/views/About.vue) }, ]; const router createRouter({ history: createWebHistory(), // 或 createWebHashHistory() routes, }); export default router;然后在入口文件里注册import { createApp } from vue; import App from ./App.vue; import router from ./router; const app createApp(App); app.use(router); app.mount(#app);这里有几个细节如果在项目里没注意后面会很别扭。第一routes数组的顺序不是随意排的。Vue Router 在匹配时按顺序遍历路由记录当两个路由模式可能覆盖同一段路径时先声明的有优先权。比如{ path: /:pathMatch(.*)* }这种兜底路由必须放在最末尾否则它会拦截所有路径。第二component选项强烈建议用动态 import() import(...)而不是直接import进来。原因在懒加载部分专门讲这里先记住动态 import 是路由懒加载的基础。第三name字段别省略。后面编程式导航router.push({ name: About })比直接传 path 更稳定因为路径改了名字可以不动反过来如果依赖 path 字符串写到处跑项目一重构路径就得全文替换。3.2 页面骨架router-view 与 router-link配置了路由表之后页面里需要一个占位区域告诉 Vue Router “匹配到的组件渲染在哪里”这个区域就是router-view。它放在哪个组件里视图就渲染在那个组件的哪个位置。最典型的布局是一个根组件template div classapp-layout aside classsidebar router-link to/首页/router-link router-link to/about关于/router-link /aside main router-view / /main /div /templaterouter-link是 Vue Router 提供的导航组件它和写a href/about有本质区别。直接用原生a会导致浏览器发起完整请求、重新加载页面而router-link内部会拦截点击事件阻止默认跳转走前端路由的逻辑并且渲染出来的是一个加了正确href的a标签方便右键、新标签页打开等原生行为。router-link还有一个很实用的特性当前路由匹配到目标路径时它会自动加上一个router-link-active类如果匹配的是关联路径则加router-link-exact-active。做侧边栏菜单的高亮效果直接用这两个类写样式就行.router-link-active { color: #409eff; background: #ecf5ff; }需要注意router-link-active是“包含匹配”比如/about在访问/about/team时也会被激活。如果希望精确匹配用exact-active-class或者给组件传exact属性。我见过不少新手在这里犯迷糊为什么给导航加了类名却没有高亮效果多半是不知道这两个自动类名或者混用了active-class与exact-active-class的触发条件。把包含匹配和精确匹配的概念理清楚这个坑就基本避开了。3.3 嵌套路由与命名视图大部分真实项目都有多级页面结构比如后台管理系统里的“用户管理”下面有“用户列表”“用户详情”这两个页面共享一个二级菜单布局。Vue Router 用嵌套路由来实现这种结构核心是children配置和组件内再加一层router-view。const routes [ { path: /user, component: () import(/layouts/UserLayout.vue), children: [ { path: , name: UserList, component: () import(/views/user/List.vue) }, { path: :id, name: UserDetail, component: () import(/views/user/Detail.vue) }, ], }, ];注意几个细节children 里的path不要加开头的/加了之后 Vue Router 会把它当作绝对路径处理嵌套关系就不成立了父级组件这里是UserLayout.vue的模板里必须放router-view /否则子路由永远渲染不出来子路由的path: 表示访问/user时直接渲染这个子组件这样父路径本身也有页面响应。命名视图解决的是另一个问题一个路由同时要渲染多个区域的组件而且这些区域并不是嵌套关系。典型场景是后台页面的顶部栏、侧边栏、主内容区、底部区域同时切换或者详情页头部的操作条和正文区由不同组件实现。template router-view nameheader / router-view namesidebar / router-view / !-- 默认命名为 default -- /templateconst routes [ { path: /dashboard, components: { header: () import(/components/DashboardHeader.vue), sidebar: () import(/components/DashboardSidebar.vue), default: () import(/views/Dashboard.vue), }, }, ];命名视图用起来不复杂但它和嵌套路由容易混淆。我的区分方法是嵌套路由解决“同一块区域里上下级页面”的问题命名视图解决“同一页面多个兄弟区域”的问题。需要同时管理复杂布局时两者可以组合使用先用嵌套路由搭出层级再在某一层里用命名视图渲染多个区域。3.4 动态路由与参数传递真实项目里很少只靠静态路径完成全部页面管理。详情页地址一般是/user/123或者/article/tech/2024-08-01这种形态路径中间有一段是动态的。Vue Router 支持在路径里用冒号定义动态参数动态段const routes [ { path: /user/:id, name: UserDetail, component: () import(/views/user/Detail.vue) }, { path: /article/:category/:slug, name: ArticleDetail, component: () import(/views/article/Detail.vue) }, ];组件里通过route.params拿到动态参数。模板中可以直接用template div当前用户 ID{{ route.params.id }}/div /template在组合式 API 里获取import { useRoute, useRouter } from vue-router; const route useRoute(); const router useRouter(); // 编程式跳转 router.push({ name: UserDetail, params: { id: 123 } });这里有一个很容易踩的坑当用户从/user/123导航到/user/456时因为路由表匹配的是同一个组件Vue Router 会复用这个组件实例不会重新触发组件的setup()和生命周期钩子。这时如果只在onMounted里根据id发请求二次跳转时请求根本不会重新发页面内容还是上一个用户的。解决办法是在组件里监听route对象变化或者用onBeforeRouteUpdate守卫watch( () route.params.id, async (newId) { const data await fetchUser(newId); // 更新页面状态 } );同时传参时还要注意一个问题当目标路由名相同、参数不同如果参数是变化的尽量用完整对象触发router.push避免出现“看起来 push 了但页面没变”的错觉。关于组件复用我在实际开发里吃过一次亏当时是列表页点击不同行进入同一个详情组件第一行点的时候正常第二行却还是第一行的数据排查半天才发现是组件实例复用导致的。所以这个点一定要记牢。4. 路由守卫权限拦截是刚需4.1 三种守卫的工作时机Vue Router 提供了一套导航守卫机制核心作用是在路由跳转过程中插入逻辑用来做权限校验、登录状态检查、页面访问统计、路由切换前的数据预加载等。从触发顺序上守卫分成三大类全局守卫注册在 router 实例上任何路由跳转都会触发路由级守卫在路由配置对象里单独写beforeEnter只对进入该路由生效组件内守卫写在组件里分别处理进入、更新、离开本组件的情况。它们的执行顺序是有严格规定的。一次完整的导航流程大概是导航被触发在失活的组件里调用beforeRouteLeave调用全局的beforeEach在复用组件里调用beforeRouteUpdate调用路由配置里的beforeEnter解析异步路由组件在被激活的组件里调用beforeRouteEnter调用全局的beforeResolve导航被确认调用全局的afterEach触发 DOM 更新调用beforeRouteEnter中传给next的回调函数此时组件实例已经创建完成。如果不熟悉这个顺序可能会写出“以为在 A 步骤生效实际在 B 步骤才生效”的守卫代码。尤其是组件内beforeRouteEnter它在导航确认前执行此时拿不到组件实例this只能靠回调参数访问。4.2 登录态拦截的完整写法几乎所有业务系统都有“未登录不能访问首页、已登录不能回登录页”的需求。最常用的做法是把不需要登录的页面标记成public其余默认要求登录。在路由配置里加一个约定字段比如metaconst routes [ { path: /login, name: Login, component: () import(/views/Login.vue), meta: { public: true } }, { path: /dashboard, name: Dashboard, component: () import(/views/Dashboard.vue), meta: { requiresAuth: true } }, ];然后在全局前置守卫里做判断router.beforeEach((to, from, next) { const isLoggedIn Boolean(localStorage.getItem(token)); if (!to.meta.public !isLoggedIn) { // 记录原本想去的地址登录后跳回来 next({ name: Login, query: { redirect: to.fullPath } }); return; } if (to.name Login isLoggedIn) { next({ name: Home }); return; } next(); });这里有两个容易被忽略的细节。第一个是next的逻辑要确保所有分支都走且只走一次否则项目里经常出现“页面跳转卡住没反应”的 bug。Vue Router 4 已经弱化了next的使用我现在的写法更倾向于直接在守卫里返回目标路由对象或返回truerouter.beforeEach((to) { if (!to.meta.public !isLoggedIn) { return { name: Login, query: { redirect: to.fullPath } }; } return true; });这样不需要在多个分支里手动调用next()心智负担小很多也不会出现“下一次跳转被上一次守卫卡住”的情况。第二个是权限校验不能只依赖守卫后端的接口鉴权也必须做。前端拦截只是用户体验层面的“遮羞布”真正能不能访问哪个接口的数据接口自己得校验。我自己见过只靠前端路由守卫做权限控制的系统用户改一下 localStorage 里的状态就能进入管理页但好在接口层还有一层校验不然整个后台就是裸奔状态。4.3 路由元信息 meta 的实际用法meta字段是路由配置里的一个自由对象可以放任何自定义数据。很多团队只把它当成“能不能访问”的开关位其实它还有很多务实用法控制页面标题router.afterEach((to) { document.title to.meta.title ? ${to.meta.title} - 管理后台 : 管理后台; });控制面包屑每级路由的 meta 里放title组件里遍历route.matched生成面包屑层级控制菜单显隐meta 里放{ menu: true, icon: user }侧边栏生成直接由路由表驱动控制缓存开关meta 里放{ keepAlive: true }配合keep-alive决定哪些视图需要缓存按钮级权限meta 里放permissions: [user:delete]组件里通过权限指令判断是否渲染删除按钮。我用 meta 管理菜单的经历比较有代表性。刚接手一个旧后台菜单在前端写死了一份路由表又是一份新增一个页面要同步改两处地方漏改一次菜单和路由就对不上。后来我把菜单数据全部下沉为路由的 meta 配置侧边栏组件直接遍历router.options.routes生成菜单增删页面只需要改路由表一处问题就再没出现过。有一点要提醒meta 字段只在当前路由的最终匹配记录上生效但route.matched数组会包含父级路由记录。很多时候需要组合route.matched里所有层级的 meta 才能拿到完整信息做面包屑尤其如此。5. 路由懒加载与工程化组织5.1 动态 import 拆包单页应用最怕的就是首屏加载体积过大。把所有页面组件都打包到一个 JS 文件里首屏哪怕只需要展示登录页也得把整个应用几百 KB 甚至数 MB 的代码全部下载完。路由懒加载就是为了解决这个问题。如果路由配置里是这样写的import Home from /views/Home.vue; const routes [{ path: /, component: Home }];构建工具会把 Home 组件和其他所有静态 import 的组件一起打进主包。改成动态 import 之后const routes [ { path: /, component: () import(/views/Home.vue) }, { path: /about, component: () import(/views/About.vue) }, ];构建工具Vite、Webpack 都支持会自动识别这种写法按路由拆成独立的 chunk。用户访问首页时只加载主包和首页 chunk切换到关于页时才动态加载关于页的 chunk。首屏体积的降低非常明显。动态 import 还有一个额外的好处组件里再 import 体积较大的第三方库只要这个第三方库没有在其他静态 import 的模块里被引用也会跟随路由 chunk 一起拆分不会塞进主包。由于路由懒加载是异步的切换路由瞬间会有一个“加载中”的空白期。Vue 官方的处理方式是配合动态组件做 loading 态或者在router-view外套一层 SuspenseVue 3 内置的异步依赖处理组件。我个人的习惯更简单在router.beforeEach里如果检测到组件尚未加载完成显示一个全局 loading 遮罩避免用户以为卡死了。5.2 路由模块化拆分项目规模大了之后所有路由写在一个router/index.js里会变得不可维护。几百行路由配置塞在一个文件里找一条路由全靠搜索改起来还要小心翼翼不碰坏别人的配置。我的组织方式是按业务模块拆文件每个模块暴露一个路由数组最后在入口文件里合并import { createRouter, createWebHistory } from vue-router; import userRoutes from ./modules/user; import orderRoutes from ./modules/order; import reportRoutes from ./modules/report; const routes [ { path: /, component: () import(/layouts/BaseLayout.vue), children: [ ...userRoutes, ...orderRoutes, ...reportRoutes, ], }, ]; const router createRouter({ history: createWebHistory(), routes, });每个模块文件像这样export default [ { path: user/list, name: UserList, component: () import(/views/user/List.vue), meta: { title: 用户列表 } }, { path: user/detail/:id, name: UserDetail, component: () import(/views/user/Detail.vue), meta: { title: 用户详情 } }, ];这种组织方式有几个好处一是每个人的工作范围被清晰隔离合代码的时候不会连环冲突二是模块内部可以自行维护自己的嵌套层级入口文件只负责组装三是做路由权限、菜单生成时可以按模块循环处理逻辑非常统一。需要注意的是模块拆分之后要注意子路由的path写法。如果它们是挂在children下的path就不要画蛇添足写/user/list直接写user/list就能拼出/user/list。我在实际项目里看到过有人统一加了斜杠导致嵌套路由配置完全失效。6. 容易踩的坑与排查思路6.1 刷新 404 问题这个应该是 history 模式最经典的坑。本地开发时一切正常部署到服务器之后访问/没问题点菜单跳转也没问题但一旦直接刷新/user/detail/123就得到 404 了。原因前面已经铺垫过history 模式下前端把真实路径显示在地址栏但服务器上没有对应的文件浏览器向服务器请求这个路径时服务器自然返回 404。解决方案是在服务器端配置“兜底回退”。以 nginx 为例需要把前端路由涉及的所有路径都回退到index.htmllocation / { try_files $uri $uri/ /index.html; }如果项目不是部署在域名根路径下比如部署在https://example.com/admin/配置要对应调整location /admin/ { alias /var/www/myapp/; try_files $uri $uri/ /admin/index.html; }光配置好服务端还不够前端如果用了createWebHistory()也要跟着设置base参数const router createRouter({ history: createWebHistory(/admin/), routes, });如果用了 hash 模式这个坑天然不存在因为#后面的内容不会发给服务器。这也是很多后台系统宁可用 hash 的原因。但值得注意的是即使是非业务系统只要对外提供访问入口我仍然建议尽早配置好服务端回退防止某天切到 history 模式时出现大面积刷新 404 的问题。6.2 重复导航报错在 Vue Router 4 中使用编程式导航时如果重复点击当前已激活的路由控制台会抛一个Uncaught (in promise) TypeError: Avoided redundant navigation to current location的错误或者类似的“Navigation cancelled”。很多新人看到报错就慌了以为是代码写错了需要修改点击逻辑。其实这个是 Vue Router 的一个“特性”不是崩溃级别的错误——重复导航同一个地址时它会直接取消。不过如果每次都往控制台抛红色错误前端监控平台会持续告警烦得很。处理方式有几种。在调用router.push的地方统一加上 catchrouter.push(/user/list).catch(() {});或者按能力做一个判断如果目标路由的fullPath和当前一致就不执行导航const go (path) { if (path router.currentRoute.value.fullPath) return; router.push(path); };我自己更倾向给路由实例包一层封装统一在内部吞掉这种重复导航异常避免业务代码里到处散布 catch。6.3 白屏和参数丢失白屏是一个总称原因多种多样这里挑几个和路由强相关的展开说。第一个是路由匹配不到任何组件。路径写错了、路由表没有对应项、嵌套路由子项没写router-view都会导致页面渲染区域是空的。排查办法是在router.beforeEach里打印to.matched看看匹配链是否正常。必要时配置一个兜底 404 页面放在路由表最后{ path: /:pathMatch(.*)*, name: NotFound, component: () import(/views/NotFound.vue) }注意 Vue Router 4 的写法和 Vue Router 3 的*通配符不一样上面这个/:pathMatch(.*)*是官方推荐的写法。连404匹配都能配置错说明对正则语法不熟这个知识缺口值得补一下。第二个是刷新之后参数丢失。这个问题常出现在用 query 传参的场景比如列表页跳详情页时用router.push({ name: Detail, query: { id: row.id } })此时地址栏是/detail?id123。用户刷新页面URL 里的 query 还在理论上不会有问题。但如果有人把参数存在了组件内部的ref里页面一刷新状态就重置了再想拿id就拿到undefined。方案很明确涉及刷新后仍要保留的关键参数必须挂在 URL query 或 params 上而不是存内存状态。另一个判断是如果参数是敏感数据或者体积很大就不要放进 URL应改为主数据标识加后端查询。第三个是二级页面切换后父组件状态丢失。这类问题通常和路由组件没有加keep-alive有关。管理系统的列表页在切到详情页再返回列表页的滚动位置、筛选条件如果全丢了体验非常割裂。处理方法有两种要么用keep-alive包住router-view并按路由 name 白名单缓存要么把筛选条件同步到 URL query 里。我个人的经验是两者结合核心筛选条件进 URL非关键状态交给 keep-alive 缓存。7. 根据我的经验最后再啰嗦一句前端路由看着只是一个“地址映射”的小模块真正落在工程里它会牵扯到页面架构、权限模型、资源加载、部署方案、用户体验这些方方面面。能写好路由配置不代表就把路由搞明白了把路由背后的原理和边界摸透遇到问题时才有足够的视野去做判断。我个人在最开始做项目时偏好 hash 模式图省事后来做了一些公网业务切成 history 模式被 nginx 刷新 404 狠狠教育了一顿再往后开始做复杂的权限后台才真正体会到守卫顺序、meta 组织、组件复用的微妙之处。每一个坑都踩过之后再回头看路由体系才感觉它其实是一套很优雅的设计。如果只能给一条建议我希望是不管项目多小都要花点时间把路由配置规范起来命名、meta、守卫、懒加载这些约定在一开始就定好。小项目不觉得痛等到路由表超过几十条、多人协作时前期这些投入会十倍地回报给你。
阅读完成 · 觉得有帮助?