这篇文章的思路来源于掘金:
SPA项目频繁上线导致白屏?最佳实践来了!背景是我们团队有个项目,上线后,偶尔会听到有人反馈说页面白屏了,刷新页面才好, - 掘金 (juejin.cn)
SPA的这种特性(就一个页面,路由也是前端路由),如果要使用history模式的话,服务器需要额外配置。以Nginx为例,需要添加配置
try_files:location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }
History和Hash模式
- Hash 模式 yuming.com/#/user
- 对SEO不友好,搜索引擎可能不会抓取
#后面的内容 - 兼容性很好,支持旧的浏览器
- 无需服务器特殊配置
- 所有请求都返回同一个 HTML 文件(通常是 index.html)
- 底层 API:依赖
window.onhashchange事件 —— 当 hash 值发生变化时(比如点击路由链接、手动修改 #后的内容),浏览器会触发该事件,React Router /Vue Router监听此事件,解析 hash 值并渲染对应组件
- History 模式 yuming.com/user
- 对SEO友,URL 结构更清晰,便于搜索引擎索引
- 兼容性较差,用户最好使用现代浏览器
- 需要服务器配置
- 服务器需要对所有路由返回同一个 HTML 文件
- 否则直接访问子路由会返回 404
- 底层 API:
history.pushState(state, title, url):修改 URL 并将新状态加入历史记录,不发送请求;history.replaceState(state, title, url):修改 URL 但替换当前历史记录(不新增),不发送请求;window.onpopstate:当用户点击浏览器的「前进 / 后退」按钮时触发,React Router/Vue Router 监听此事件解析 URL,渲染对应组件。
✨ 关键注意:pushState/replaceState 本身不会触发 popstate,框架 Router 内部会封装这两个方法,在调用时同步更新路由状态。
✨ 关键注意:如果 Vue 项目部署在服务器的子目录,在history模式下,需配置base参数;同时后端配置需对应调整(如 Nginx 的location /app/)
location / {
try_files $uri $uri/ /index.html;
}
History模式为什么需要nginx配置路由转发
关键问题:页面刷新会404
- 正常跳转:用户在页面内点击「用户页」,前端路由把 URL 改成
/user,但不会向服务器发请求,只是 JS 渲染对应组件,一切正常; - 刷新页面:用户手动刷新
/user时,浏览器会直接向服务器发送GET https://xxx.com/user请求 —— 但服务器的静态资源里只有index.html,没有/user这个文件 / 路径,因此返回 404。
配置的核心目的
让服务器在接收到「前端路由路径」的请求时(如 /user//order/123),不返回 404,而是进行重定向返回唯一的index.html ,再由前端路由接管 URL 解析,渲染对应页面。在nginx的配置中,会按顺序检查文件 / 目录,兜底返回 index.html。$uri用于检查js/css等静态资源,index.html用于处理路由导航。
| 部分 | 含义 |
|---|---|
$uri | Nginx 内置变量,代表「请求的路径」(比如请求 /user,$uri 就是 /user;请求 /js/app.js,$uri 就是 /js/app.js);检查 root 目录下是否有「名为 $uri 的文件」(比如 /usr/share/nginx/html/user 文件)。 |
$uri/ | 检查 root 目录下是否有「名为 $uri 的目录」(比如 /usr/share/nginx/html/user/ 目录)。 |
/index.html | 若 $uri(文件)和 $uri/(目录)都不存在,就返回 root 目录下的 index.html(即 /usr/share/nginx/html/index.html)。 |
页面白屏可能的原因
-
代码错误。如果页面出现JS错误(语法错误、逻辑错误或运行时错误),可能会导致页面无法正确加载,继而引发白屏问题;或者是分支过多前端没有进行代码安全兜底,导致对象上没有本次项目需要的属性,比如后端意外传了resdata={}对象,而前端取的是resdata.attr,这个时候就会崩溃导致白屏问题
-
网络问题。如果服务器或网络出现问题,可能导致资源无法加载,从而导致白屏。
-
资源加载错误。如果在页面加载过程中某个关键资源(如 JavaScript文件、CSS文件或图片)加载失败,没有else方案,可能会导致白屏。
-
异步加载问题。如果SPA使用了延迟加载或异步加载的模块,可能会导致页面白屏,特别是在加载过程中出现错误。
异步加载有两种常见的case问题:
- 前端代码异步,导致走到了拿到接口赋值前的数据,于是崩溃白屏。
比如,一个模块A打开加载完后,B模块的attr才会被赋值,但是因为网络延迟等问题,A打开还没加载完,用户就点击B模块,导致attr没拿到预期的数据,于是代码崩溃,页面陷入白屏。
- 打包产生异步,走到了旧的版本文件。
因为现在普遍打包为了优化性能会使用contentHash或者hash作为文件名的一部分,这也导致代码修改之后重新打包文件名也会随之变化,也就是会出现用户在旧页面停留太久,中途代码更新,导致再次执行操作的时候找不到之前的文件,这也是为什么很多项目会在代码更新的时候强制要求用户进行刷新操作。
开头文章的作者就遇到了这个case:
为了更好的SPA体验,我们通常会根据路由分包,不同路由的代码延迟(懒)加载,首页只加载首页相关的代码,等到切换不同路由时再用异步加载的方式加载对应路由的JS文件或CSS文件。
而我们的线上服务部署方式是K8S+Docker,前端代码在一个Nginx镜像里,每次新的服务上线后,旧的大部分资源都请求不到了(因为JS、CSS文件合并压缩后的文件都带了hash值,重新打包后就变化了)。
如果一个用户停留在一个页面上,从来没有访问过B路由(本地浏览器没有缓存),这时想要切换到B路由,请求B路由的
chunk-b.js,但服务重新上线后这个JS文件已经变成了chunk-c.js,这时请求就会失败,页面就会白屏。
他也总结了这个case常见的先置条件:
这个白屏问题出现需要同时满足以下几个条件:
- 项目为SPA
- 开启了路由懒加载或者组件懒加载
- 用户在某页面停留期间,新的服务上线了
- 新的服务中丢失了hash过的chunk文件
- 用户未访问过将要切换的路由(浏览器没有该路由所需的缓存文件,导致走不到本地缓存兜底)
对此提出了三种解决方案,我们项目之前也遇到过这个问题,我用的是第三种方案,文章作者采用了第二种:
- 保留hash的chunk文件。增加多余的缓存体积,没有必要,如果一定有用户需要走旧代码,直接在代码里进行分支改造是更有效并且简单的方案。
- 全局监听错误,捕获到这个错误后,刷新页面。
- 通知用户页面有更新。考虑过往public目录下注入一个JSON文件,内含版本号,定时获取这个文件,发现版本号有变化了,提示用户进行刷新。
衍生:为什么hash模式就不需要配置nginx路由转发
关键在于浏览器对 URL 中 #(哈希)的处理规则——# 是「浏览器端专属标识」,不会随 HTTP 请求发送到服务器,这从根本上避免了 history 模式的 404 问题。
URL 中 # 的核心特性(浏览器级规则)
# 是 URL 的「锚点分隔符」,# 后面的所有内容(称为 hash 值)有两个关键特点:
- 仅在浏览器端生效:
hash是给浏览器解析用的,不会被包含在 HTTP 请求中发送到服务器; - 不触发页面刷新:修改
#后的内容(比如从/#/home改成/#/user),浏览器只会更新页面的锚点定位,不会向服务器发起任何请求。
这是 HTTP 协议和浏览器的通用规则,和前端框架(Vue/React)无关 —— 只要 URL 带 #,服务器就永远收不到 # 后面的内容。
因此对于hash模式下请求的永远是index.html文件,不需要服务端去进行重定向