Skip to content

手机网页挂后台页面白屏

约 2785 字大约 9 分钟

WebApache配置前端排障

2026-09-12

统计数据加载中...

前言

网站上线之后,电脑上访问正常,手机第一次打开也正常,但切走一段时间再回来,页面突然白了,地址栏还一直转。手动刷新一下,又恢复了

触发原因可能是:

  • 旧页面引用了已经下线的入口脚本
  • 第三方脚本拖住了应用启动。

适用范围:使用带 hash 文件名的构建产物、主要依赖 JavaScript 渲染的 Web 应用。文中的 Hono / Node 示例用于说明服务端处理方式,不是直接放进所有项目就能运行的配置。VuePress 这类预渲染网站还要区分“正文没有显示”和“正文正常但交互失效”

先查访问日志

先记下白屏发生的时间和页面地址,再去服务器上看请求。用于区分是旧文件丢了,还是文件已经下载、应用却没有启动

先对齐手机记录与日志的时区,再找当时的请求序列。Apache 的日志路径看生效站点配置中的 CustomLog;有 CDN 或反向代理时,也要检查对应层的记录。

# Apache 可用此命令定位域名对应的配置文件
sudo apache2ctl -S

# 以下路径、日期和时间段均按实际日志填写
date
ACCESS_LOG='/var/log/apache2/example.com_access.log'
sudo grep -E '04/Sep/2026:17:(3[5-9]|4[0-5]):' "$ACCESS_LOG" \
  | grep -iE 'iphone|android|mobile'

没有结果时,先去掉移动端筛选、检查日志轮转,不要直接判断请求没到服务器。查的是用户实际打开的 HTML 路径,不只是首页的 GET /

请求表现排查方向
没有新的文档请求,却请求旧入口并返回 404旧文档恢复、浏览器或其他缓存层仍引用旧资源
新文档仍引用旧入口,或当前版本资源也缺失HTML 缓存、发布顺序、资源上传和版本混用
HTML、入口 JS 均成功返回,但页面仍白屏脚本阻塞、运行时报错、接口或样式
没有相关请求日志是否完整,以及 CDN、Service Worker、网络

例如正常访问请求 /assets/index-NEW123.js,恢复标签页后却请求已经删除的 /assets/index-OLD456.js,就应沿着旧入口排查。没有 HTML 请求、监控没有事件都只是线索,不能单独证明具体原因。

确定需要配置的内容

修复前先确定 请求 URL 由哪一层处理、映射到哪个文件。下面这些值来自实际配置,示例中的目录和文件名不能直接当默认值。

配置项确定方法与约束
页面与入口 URL查看复现地址、构建后 HTML 和实际请求;相对地址按页面及 <base> 解析,保留部署子路径
静态资源处理位置查看站点配置、代理和静态中间件,找到真正返回文件的服务
入口匹配规则根据当前及历史入口命名生成,锚定 ^...$,排除普通 chunk;命名不够区分时使用已知入口清单
恢复文件的位置放进构建会复制的公共目录,确认发布后的 URL;不要只手动写入 dist
重试参数 GUARD确认不占用业务参数,同一实现保持一致,重定向不得丢失
缓存与路由顺序找到应用、Web 服务、CDN、Service Worker 的相关配置;真实文件优先,恢复处理在 HTML 兜底之前
日志和测试环境确认日志时区;测试使用空闲端口和实际挂载标记,不假设端口或容器名称

例如入口确实为 /assets/index-ABC123.js,且历史命名相同,路径规则才是 ^/assets/index-[A-Za-z0-9_-]+\.js$。若有 /portal/ 前缀,入口、恢复文件和验证地址要一起对应调整;Apache 目录内规则还需去掉该目录映射的 URL 前缀。

恢复失效的入口

旧标签页可能仍引用部署前的文件。入口被删除后,应用和其中的监控代码都可能没有机会启动。

处理方式是:真实文件正常返回;仅对缺失的入口返回恢复 JS,引导页面重新获取当前 HTML。 其他缺失资源继续返回 404,不扩大到所有 .js

恢复脚本

保存为 stale-entry.js,随构建发布。除了确认 GUARD 不与业务参数冲突,其余逻辑可保留:保留当前路径、查询参数和锚点,只自动导航一次。

(function () {
  var GUARD = 'stale_entry_retry'

  try {
    var url = new URL(location.href)
    if (url.searchParams.has(GUARD)) {
      console.error('[stale-entry] 重试后仍未恢复,停止自动刷新')
      return
    }

    url.searchParams.set(GUARD, Date.now().toString(36))
    location.replace(url.pathname + url.search + url.hash)
  } catch (error) {
    console.error('[stale-entry] 自动恢复中止', error)
  }
})()

异常分支不继续 reload(),避免绕过守卫。服务端重定向要保留守卫参数,恢复成功前不要删除它。

Apache 接入

以下适用于 Apache 2.4 直接托管静态文件,示例站点位于域名根路径。先确认 rewriteheaders 模块已加载,再合并进站点已有的 <Directory>,保留原权限、证书等配置。

<Directory /var/www/example.com>
    # 保留这里原有的访问权限等配置
    RewriteEngine On

    # 只接管不存在的入口;真实文件继续正常返回
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^assets/index-[A-Za-z0-9_-]+\.js$ /stale-entry.js [END]

    <Files "stale-entry.js">
        ForceType application/javascript
        Header onsuccess unset Cache-Control
        Header always set Cache-Control "no-store"
    </Files>

    <FilesMatch "\.html$">
        Header onsuccess unset Cache-Control
        Header always set Cache-Control "no-cache"
    </FilesMatch>
</Directory>

目录、入口规则和恢复 URL 使用前表确定的值。!-f 保证真实文件优先,目录内匹配路径不带开头的 /。规则放在 SPA 通配重写之前,其他缺失资源仍应返回 404。具体语法见 Apache 重写文档

应用服务接入

若静态资源由 Node 等应用服务返回,就在该服务的未命中处理处实现,不必再启动一个服务。Hono 接入片段如下,沿用同一入口规则和恢复文件:

import { readFileSync } from 'node:fs'

// 改成部署后 stale-entry.js 的实际位置
const recovery = readFileSync('/path/to/dist/stale-entry.js', 'utf8')

// app 是已有 Hono 实例。
// 此路由放在静态文件中间件之后、SPA 的 HTML 兜底之前。
// 静态文件未命中时,需要继续调用后续处理。
app.get('/assets/*', (c) => {
  const path = new URL(c.req.url).pathname
  if (!/^\/assets\/index-[A-Za-z0-9_-]+\.js$/.test(path)) {
    return c.notFound()
  }

  return c.body(recovery, 200, {
    'Content-Type': 'application/javascript; charset=utf-8',
    'Cache-Control': 'no-store',
  })
})

注册顺序为:缓存中间件 → 真实静态文件 → 恢复路由 → HTML 兜底。静态中间件未命中时要继续后续处理;如果它直接结束 404 响应,就在其未命中处理处接入。

统一缓存中间件在 await next() 后设置头部时,保留路由已有策略:

if (c.res.status !== 200) return
if (c.res.headers.has('Cache-Control')) return
// 后面接原有缓存逻辑

缓存要求

资源Cache-Control
HTMLno-cache,复用前验证版本
已存在、内容变化就更换 URL 的 hash 文件public, max-age=31536000, immutable
恢复响应no-store,不保存临时脚本

这些要求在各层都要一致,尤其不能把恢复响应重新覆盖成 immutable。CDN 或 Service Worker 仍返回旧 HTML 时,加查询参数也未必有效。缓存头语义可用于核对实际响应。

托管平台可用“真实文件未命中后重写”或边缘函数实现同样的处理;不要无条件覆盖所有入口。入口带 integrity 校验时,替换脚本会被拦截,需要另行设计;跨域资源也要核对 CORS。

保留有限历史资源可以减少旧页面失效,但补不回早已删除的文件。发布时先上传资源再更新 HTML,恢复处理用于补上过期入口这一环。

排除第三方脚本阻塞

入口没有缺失时,检查最终 HTML、Network 和 Console,找出入口之前的同步脚本或一直挂起的延迟脚本。

独立运行、不依赖执行顺序的统计脚本可以使用 async

<!-- 按延迟脚本顺序执行 -->
<script defer src="https://analytics.example.com/script.js"></script>

<!-- 独立脚本异步执行 -->
<script async src="https://analytics.example.com/script.js"></script>

defer 会影响 DOMContentLoaded,前面的延迟脚本挂起时可能拖住后续入口。async 不要求按文档顺序执行,但仍会占用主线程。有依赖的 SDK 按接入要求组织,或在应用挂载后加载,不要全部机械替换。script 属性说明可用于确认行为。

复现与验证

在独立测试环境保存修改前后的对照。端口先确认空闲,缺失请求必须经过实际待测规则,而不是另写一份测试专用恢复逻辑。

旧入口缺失

让第一次文档引用不存在的入口,后续文档引用当前入口。修改前应无法正常挂载或交互;修改后应只导航一次,取得新 HTML 并恢复。

再让文档始终引用缺失入口,确认一次重试后停止。带业务查询参数和锚点重复测试,确认没有丢失。预渲染页面需验证交互,已有正文不代表客户端启动成功。

脚本挂起

让一个同源测试脚本延迟约 10 秒返回,保持其他条件一致,分别测试 deferasync。禁用缓存,对比 HTML 响应、应用挂载和 DOMContentLoaded 时间。

只有属性变化确实消除了入口等待,才能支持阻塞判断。事件未发生时计时值可能为 0,不要当成瞬间完成;实验延迟也不是线上故障时长。

响应检查

以下地址按实际配置填写。当前入口来自本次构建,缺失入口符合规则但确实不存在:

PAGE_URL='https://example.com/'
ENTRY_URL='https://example.com/assets/index-ABC123.js'
MISSING_ENTRY_URL='https://example.com/assets/index-NOTEXIST.js'
OTHER_MISSING_URL='https://example.com/assets/not-found.png'

# 当前入口:200,响应体仍与构建文件一致
curl -sS -D - "$ENTRY_URL"

# 缺失入口:200、JavaScript 类型、no-store、恢复脚本正文
curl -sS -D - "$MISSING_ENTRY_URL"

# 无关缺失资源:404
curl -sS -o /dev/null -w '%{http_code}\n' "$OTHER_MISSING_URL"

# HTML:检查缓存验证策略
curl -sS -D - -o /dev/null "$PAGE_URL"

不要只看状态码,确认 JS 没被返回成 HTML、缓存头没有冲突。存在 CDN 时同时检查源站和公开地址,再从浏览器验证执行结果。

发布前备份配置和对应构建,配置检查通过才重载。上线后重复上述检查,并在手机上重做后台恢复操作。hash 未变化或旧文件仍存在时不一定触发问题,不要为复现删除生产文件。

验收应同时满足:当前入口不受影响、旧入口能恢复、无关资源保持 404、重复失败不循环刷新。真机结果单独记录;需要回退时恢复原配置和对应构建,再检查并重载。

暂无评论

暂无评论,来添加第一条评论吧!