Code前端首页关于Code前端联系我们

一、刷新后页面直接白屏?先看路由模式和后端配置

terry 23小时前 阅读数 16 #Vue

做 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_TOKENSET_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 为了性能,会复用相同的组件实例,导致 createdmounted 这些生命周期不执行,数据没更新。

(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:给加 key,强制组件重建

在父组件的 <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> 从缓存中恢复,但子路由的状态可能没正确加载。解决方法是用 includeexclude 控制缓存范围,只缓存需要的组件:

<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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门