一、刷新后页面直接白屏?先看路由模式和后端配置
做 Vue 项目时,只要涉及路由跳转,刷新页面后总会碰到各种“幺蛾子”——要么页面白屏、要么登录状态没了、要么路由参数变了页面却没反应…这些 vue-router 刷新相关的坑,是不是让你头大?今天咱就把常见场景拆开来,一个个说清楚解法,再聊聊背后原理,以后遇到刷新问题心里就有数啦!
很多同学第一次用 history 模式部署项目,刷新页面直接 404 或白屏,本质是前端路由和浏览器请求机制的冲突。
先搞懂 vue-router 的两种路由模式:
-
hash 模式:URL 长这样
http://xxx.com/#/home
, 后面的内容是“哈希值”,浏览器发送请求时,只会把 前面的部分(http://xxx.com
)发给服务器,所以不管 后面怎么变,服务器都返回 index.html,前端再解析哈希值渲染对应组件,这种模式下,刷新页面不会白屏,因为服务器永远返回 index.html。 -
history 模式:URL 是
http://xxx.com/home
这种“干净”的形式,靠 HTML5 的history.pushState()
等 API 改变 URL,但刷新时,浏览器会把整个 URL(http://xxx.com/home
)发给服务器,要是服务器没配置过这个路径,就会返回 404,页面自然白屏。
所以解决 history 模式刷新白屏的核心是 让服务器把所有路由请求都指向 index.html,不同服务器配置方式不同:
-
Nginx:在配置文件里加
try_files $uri $uri/ /index.html;
,意思是“如果请求的资源存在($uri),就返回;不存在就找 $uri/;还不存在就返回 index.html”,这样不管用户访问/home
还是/about
,最终都返回 index.html,前端路由再处理。 -
Apache:需要开启
mod_rewrite
模块,然后在项目根目录的.htaccess
文件写:<IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L] </IfModule>
原理和 Nginx 类似,判断文件/目录不存在时,转发到 index.html。
-
Tomcat:得改
web.xml
,配置Filter
转发请求,这种场景相对少,因为 Tomcat 更偏向 Java 后端,一般前端项目不用它部署,但如果遇到,思路也是“所有未知请求转发到 index.html”。
要是你不想折腾后端配置,开发阶段用 hash 模式最省心,生产环境再根据需求切换,比如公司内部系统,对 URL 美观要求不高,用 hash 模式省事儿;面向用户的官网,用 history 模式更专业。
刷新后登录状态、表单数据没了?状态持久化是关键
做项目时,用户登录后刷新页面,突然变成未登录;填了一半的表单,刷新后全没了…这是因为 SPA 的状态存在内存里,刷新后内存被清空。
(1)哪些状态会丢?
- vuex 里的用户信息、权限路由
- 组件里的临时数据(比如表单输入、分页参数)
- 路由元信息(meta)里的临时标记
(2)怎么让状态“活”过刷新?
方法 1:把状态存到 localStorage/sessionStorage
这俩属于浏览器存储,刷新后数据还在,但要注意区别:
localStorage
:永久存储(除非手动清除),适合存不常变的信息,比如用户头像、昵称。sessionStorage
:会话级存储,关闭标签页就清除,适合存敏感信息(token),降低 XSS 攻击风险。
代码示例(vuex 持久化用户信息):
// store/user.js const userStore = { state: { token: sessionStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('userInfo')) || {} }, mutations: { SET_TOKEN(state, token) { state.token = token; sessionStorage.setItem('token', token); // 同步到 sessionStorage }, SET_USER_INFO(state, info) { state.userInfo = info; localStorage.setItem('userInfo', JSON.stringify(info)); // 同步到 localStorage } } }
登录成功时,调用 SET_TOKEN
和 SET_USER_INFO
,把状态存到存储;页面刷新后,state 里的初始值会从存储里读,状态就恢复了。
方法 2:路由守卫里重新拉取数据
适合那些不适合存在存储里的临时数据(比如表单草稿,存存储怕占空间)。
比如用户填了一半的表单,刷新后从接口重新请求草稿数据:
// 路由守卫(全局前置守卫) router.beforeEach((to, from, next) => { if (to.name === 'FormPage') { // 假设表单页面叫 FormPage // 调用接口拉取草稿 getFormDraft().then(res => { store.commit('SET_FORM_DRAFT', res.data); next(); }); } else { next(); } });
这样刷新进入表单页时,会自动拉取草稿数据,填充到表单里。
方法 3:keep-alive + 存储 组合拳
keep-alive
能缓存组件实例,让组件不销毁,刷新前的数据理论上能保留?但实际刷新页面后,keep-alive
的缓存也没了(因为页面刷新是全量重新加载),所以得结合存储:
<template> <div> <keep-alive> <router-view v-if="showRouterView" /> </keep-alive> </div> </template> <script> export default { data() { return { showRouterView: true } }, created() { // 刷新后,从存储恢复状态,再显示 router-view const hasState = localStorage.getItem('formState'); if (hasState) { this.showRouterView = true; } } } </script>
这种方式适合对组件状态缓存要求很高的场景,但代码复杂度高,得权衡使用。
路由参数变了,页面却没更新?组件复用的“锅”得解决
场景:路由是 /user/:id
,从 /user/1
跳到 /user/2
,页面还是用户 1 的信息,这是因为 vue 为了性能,会复用相同的组件实例,导致 created
、mounted
这些生命周期不执行,数据没更新。
(1)为啥 vue 要复用组件?
比如列表页点不同商品进详情页,组件结构一样,只是数据不同,复用组件能减少 DOM 操作和资源消耗,提升性能,但也带来了“参数变化页面不更新”的问题。
(2)三种解法,按需选择
解法 1:watch $route 监听路由变化
在组件里监听 $route
对象,路由变化时重新请求数据:
<template> <div>{{ userInfo.name }}</div> </template> <script> export default { data() { return { userInfo: {} } }, watch: { '$route'(to, from) { // to.params.id 是新的用户 id this.getUserInfo(to.params.id); } }, created() { this.getUserInfo(this.$route.params.id); }, methods: { getUserInfo(id) { // 调用接口拿用户信息 getUserApi(id).then(res => { this.userInfo = res.data; }); } } } </script>
这样不管是首次进入页面,还是路由参数变化,都会重新请求数据。
解法 2:给
在父组件的 <router-view>
上绑定 key
,值为 $route.fullPath
(包含路径和参数的完整 URL),这样每次路由变化,key
不同,组件会被销毁重建,生命周期重新执行:
<!-- 父组件模板 --> <template> <div> <router-view :key="$route.fullPath" /> </div> </template>
但要注意:组件重建会有性能开销,如果路由变化很频繁(比如分页组件,每次页码变都重建),可能影响体验,所以只在必要时用,比如路由参数变化但组件逻辑复杂,watch 不好处理的情况。
解法 3:beforeRouteUpdate 导航守卫
这个守卫在“组件复用、路由参数变化”时触发(比如从 /user/1
到 /user/2
),比 watch 更“主动”控制逻辑:
<script> export default { data() { return { userInfo: {} } }, created() { this.getUserInfo(this.$route.params.id); }, beforeRouteUpdate(to, from, next) { // to 是目标路由,拿到新的 id this.getUserInfo(to.params.id); next(); // 必须调用 next() 放行 }, methods: { getUserInfo(id) { getUserApi(id).then(res => { this.userInfo = res.data; }); } } } </script>
beforeRouteUpdate
里处理数据更新,逻辑更集中,适合对组件生命周期有强控制需求的场景。
嵌套路由刷新后,子路由页面不显示?检查父组件和路由配置
嵌套路由的结构一般是:父组件里有 <router-view>
,用来渲染子路由组件,比如后台管理系统的侧边栏(父组件)和内容区(子路由),刷新后子路由不显示,常见原因有三个:
(1)父组件的被“藏”起来了
父组件的 <router-view>
可能在条件渲染里,刷新后条件不满足,导致 <router-view>
没渲染。
比如父组件里写了:
<template> <div> <div v-if="isLogin"> <router-view /> <!-- 登录后才显示子路由 --> </div> </div> </template> <script> export default { data() { return { isLogin: false } }, created() { // 刷新后,isLogin 初始化为 false,子路由被隐藏 const token = sessionStorage.getItem('token'); if (token) { this.isLogin = true; } } } </script>
刷新后,isLogin
初始是 false,<router-view>
被隐藏,子路由没地方渲染。解决方法是确保 <router-view>
始终存在,或者在条件渲染里处理好状态恢复。
(2)路由配置的 children 数组写错了
嵌套路由的核心是父路由的 children
配置,举个错误示例:
// 错误配置:children 里的 path 没写对 const routes = [ { path: '/layout', component: Layout, children: [ { path: '/home', component: Home }, // 错误!子路由 path 不能以 / 开头 { path: '/about', component: About } ] } ]
子路由的 path 不能以 开头,因为父路由的 path 是 /layout
,子路由的 path 会自动拼接成 /layout/home
,正确配置是:
const routes = [ { path: '/layout', component: Layout, children: [ { path: 'home', component: Home }, // 正确:path 是 home,拼接后是 /layout/home { path: 'about', component: About } ] } ]
children 配置错误,刷新后子路由匹配不到,自然不显示。
(3)keep-alive 把嵌套路由“缓存死”了
如果父组件用 <keep-alive>
包裹 <router-view>
,刷新后缓存可能导致子路由不渲染。
<template> <div> <keep-alive> <router-view /> <!-- 父组件的 router-view 被缓存 --> </keep-alive> </div> </template>
刷新后,父组件的 <router-view>
从缓存中恢复,但子路由的状态可能没正确加载。解决方法是用 include
或 exclude
控制缓存范围,只缓存需要的组件:
<template> <div> <keep-alive include="Home,About"> <!-- 只缓存 Home 和 About 组件 --> <router-view /> </keep-alive> </div> </template>
或者在特定场景下,去掉 keep-alive,避免缓存影响嵌套路由。
从原理上理解:vue-router 刷新时到底发生了啥?
想彻底解决刷新问题,得明白 SPA 和前端路由的运作逻辑:
(1)SPA 的“单页”本质
传统多页应用(PHP 项目),每个页面都是独立的 HTML,刷新页面时浏览器请求新的 HTML,而 SPA 只有一个 HTML 文件,页面切换靠 JS 动态替换 <div id="app">
里的内容,vue-router 就是负责“根据 URL 切换内容”的工具。
(2)刷新时,浏览器和 vue-router 的交互
-
hash 模式:刷新时,浏览器只请求 前面的 URL(
http://xxx.com
),服务器返回 index.html,vue-router 解析 后的路径(#/home
),匹配路由规则,渲染对应组件,所以不会白屏,状态丢失是因为内存数据清空。 -
history 模式:刷新时,浏览器请求整个 URL(
http://xxx.com/home
),如果服务器没配置,返回 404;配置后返回 index.html,vue-router 再解析 URL 路径(/home
)匹配路由。
(3)状态丢失的本质
vuex 里的状态、组件的 data 都存在内存里,刷新页面时,浏览器会销毁之前的 JS 执行环境,重新加载所有资源,内存被清空,所以状态丢失,要恢复状态,必须把数据存到浏览器存储(localStorage/sessionStorage)或重新请求接口。
vue-router 刷新场景的最佳实践建议
结合前面的场景和原理,总结几个实用技巧:
(1)路由模式:开发和生产区别对待
- 开发环境:用 hash 模式,
const router = createRouter({ history: createWebHashHistory(), ... })
,不用配后端,刷新不白屏。 - 生产环境:如果要 URL 美观,用 history 模式,必须让后端配置路由转发(如 Nginx 的 try_files);对 URL 美观没要求,继续用 hash 模式更稳定。
(2)状态持久化:分层处理
- 核心状态(如用户 token、权限):存 sessionStorage,配合 vuex 在页面加载时恢复。
- 临时状态(如表单草稿、分页参数):短期存 sessionStorage,或在路由守卫里重新请求。
- 敏感信息(如密码、支付凭证):坚决不存浏览器存储,靠接口重新获取或加密传输。
(3)路由参数变化:优先用 watch 或守卫
- 简单场景:用
watch $route
,代码简洁。 - 复杂场景:用
beforeRouteUpdate
,逻辑更集中。 - 极端场景(比如需要组件重建):给
<router-view>
加 key,但要评估性能。
(4)嵌套路由:保证稳定+配置准确
- 父组件的
<router-view>
别放条件渲染里,确保刷新后能渲染。 - 路由配置的
children
数组,path 别写错(不以 / 开头,和父路由 path 正确拼接)。 - 嵌套路由用 keep-alive 时,严格控制缓存范围,避免意外。
(5)测试环节:模拟真实场景
开发时,每次改路由逻辑后,做这几个测试:
- 刷新页面,看是否白屏、状态是否丢失。
- 路由参数变化(比如从 /user/1 到 /user/2),看页面是否更新。
- 嵌套路由页面刷新,看子路由是否显示。
vue-router 的刷新问题,看似五花八门,其实绕不开“前端路由的运作逻辑”和“浏览器存储的特性”,理解了 hash/history 模式的区别、状态存储的原理、组件复用的机制,再结合场景选对方法(比如后端配置、状态持久化、watch 路由、检查嵌套路由),就能把刷新时的坑一个个填上。
下次遇到刷新白屏,先查路由模式
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。