1. 别把维护页做成“系统挂了”的公告板很多人第一次接到“系统升级维护提示页”这个需求时脑子里蹦出来的画面大概是这样的白底黑字居中一行“系统维护中请稍后访问”顶多加个预计恢复时间。做完交差运维那边一挂完事。我早期也这么干过。结果有一次升级比预期多拖了两个小时客服电话被打爆用户以为网站跑路了甚至有人在社群里发截图说“这公司是不是倒闭了”。那次之后我才意识到维护提示页根本不是一张“公告”它是系统在不可用状态下唯一还能跟用户对话的窗口。这个窗口做得好不好直接决定了用户是安静等待还是转身离开。这篇内容就是围绕“系统升级维护时友好提示页如何做”这件事把我在多个项目里踩过的坑、试过的方案、最后沉淀下来的做法完整讲一遍。核心关键词就几个系统升级、维护、友好提示页、HTML、CSS。适合谁看前端同学、运维同学、独立开发者以及任何需要给系统做“停机维护”这件事收尾的人。哪怕你只会写最基础的 HTML 和 CSS看完也能直接抄出一套能用的方案。先说一个反直觉的结论维护提示页的技术难度几乎为零但做好它靠的不是技术是对“用户此刻在想什么”的判断。一个只会写h1维护中/h1的人和一个会写完整状态页的人差距不在 CSS 水平而在于有没有想过用户刷新了几次他是不是刚提交了订单他会不会以为自己的账号出了问题把这些想清楚了页面自然就“友好”了。下面我按“为什么这么做—具体怎么做—怎么避坑—怎么验证”的顺序展开中间会穿插可直接复制的代码和参数说明。2. 维护页到底要解决用户的哪几个疑问在动手写代码之前得先搞清楚这张页面要回答用户什么问题。我把它总结成四个层次从低到高缺一层都会让体验打折。2.1 第一层现在是什么状态这是最基础的。用户打开页面第一眼必须知道“系统正在维护不是我的问题”。很多维护页失败就失败在这一层——页面长得像 404或者干脆是浏览器默认的错误页用户根本分不清是网站挂了还是自己网络有问题。判断标准很简单把页面截图给一个完全不知情的人看他能不能在 3 秒内说出“哦这个网站在维护”。如果他说“这网站是不是坏了”那这一层就没做到。2.2 第二层要等多久“请稍后访问”是最偷懒的写法因为它没有给用户任何预期。人对不确定的等待是最焦虑的心理学上有个说法叫“等待焦虑”本质是不知道要等多久。所以维护页必须给出时间信息哪怕是个区间。这里有个细节时间要给区间不要给精确到秒的承诺。写“预计 14:00 恢复”结果 14:05 还没好用户会觉得你不可靠写“预计 13:30–14:30 之间恢复”只要在这个区间内完成用户就不会有被欺骗的感觉。这是我在实际运维配合中反复验证过的经验。2.3 第三层我的数据/操作怎么办这一层最容易被忽略但恰恰是用户最关心的。如果用户刚提交了一个表单、刚付了款、刚上传了文件然后看到维护页他脑子里第一个念头是“我刚才那步算不算数”。所以维护页里最好有一句话专门安抚这类用户比如“您已提交的操作均已保存维护完成后可正常查看”。哪怕技术上你并不确定也要在升级前确认好这个前提再写上去。不要写自己都不确定的话这是维护页的诚信底线。2.4 第四层我现在能做什么好的维护页不只是让用户“等”还会告诉用户“现在可以做什么”。比如提供客服联系方式、引导关注公告渠道、或者给一个“维护完成后通知我”的入口。这一层是加分项能把一次负面体验转化成一次正向互动。把这四层想清楚页面的信息架构就出来了。下面进入具体实现。3. 从零搭一个维护页结构、样式与状态逻辑这一节给一套可以直接用的方案。我按“HTML 结构—CSS 样式—JS 状态逻辑”三块讲每块都说明为什么这么设计。3.1 HTML 结构语义化比好看更重要先看结构。很多人写维护页喜欢用一堆div堆出来但维护页恰恰应该用语义化标签因为它在某些场景下会被搜索引擎、监控系统、甚至无障碍读屏软件解析。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 meta namerobots contentnoindex, nofollow title系统维护中 - 我们很快回来/title link relstylesheet hrefmaintenance.css /head body main classmaintenance-wrap section classmaintenance-card div classstatus-icon aria-hiddentrue/div h1 classtitle系统正在升级维护/h1 p classdesc为了给您提供更稳定的服务我们正在进行系统升级。/p p classtime预计恢复时间time今日 13:30 – 14:30/time/p p classnotice您已提交的操作均已保存维护完成后可正常查看。/p div classactions a classbtn primary href/status查看实时状态/a a classbtn ghost hrefmailto:supportexample.com联系客服/a /div /section /main script srcmaintenance.js/script /body /html几个关键点解释一下langzh-CN别省影响字体渲染和读屏发音。meta robots设为noindex, nofollow避免维护页被搜索引擎收录否则用户搜到你的站点结果第一条是维护页体验很差。用main和section而不是纯div语义清晰。time标签包裹时间机器可读。状态图标用 CSS 画不依赖图片加载快且不会因为图片 404 而破相。3.2 CSS 样式居中、呼吸感与移动端适配维护页的视觉目标只有一个让用户在等待时感到平静而不是焦躁。所以配色要柔和留白要足动效要慢。* { margin: 0; padding: 0; box-sizing: border-box; } body { min-height: 100vh; display: flex; align-items: center; justify-content: center; font-family: -apple-system, PingFang SC, Microsoft YaHei, sans-serif; background: linear-gradient(135deg, #eef2f7 0%, #dfe7f1 100%); color: #2c3e50; padding: 24px; } .maintenance-card { width: 100%; max-width: 480px; background: #fff; border-radius: 16px; padding: 48px 32px; text-align: center; box-shadow: 0 12px 40px rgba(44, 62, 80, 0.08); } .status-icon { width: 64px; height: 64px; margin: 0 auto 24px; border-radius: 50%; border: 4px solid #dfe7f1; border-top-color: #4a90d9; animation: spin 1.2s linear infinite; } keyframes spin { to { transform: rotate(360deg); } } .title { font-size: 22px; margin-bottom: 12px; } .desc { font-size: 15px; color: #5a6b7b; line-height: 1.7; margin-bottom: 16px; } .time { font-size: 15px; color: #4a90d9; font-weight: 600; margin-bottom: 12px; } .notice { font-size: 13px; color: #8a99a8; line-height: 1.6; margin-bottom: 28px; } .actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; } .btn { padding: 10px 22px; border-radius: 8px; font-size: 14px; text-decoration: none; transition: all 0.2s ease; } .btn.primary { background: #4a90d9; color: #fff; } .btn.primary:hover { background: #3a7bc0; } .btn.ghost { border: 1px solid #cfd8e3; color: #5a6b7b; } .btn.ghost:hover { border-color: #4a90d9; color: #4a90d9; } media (max-width: 480px) { .maintenance-card { padding: 36px 20px; } .title { font-size: 19px; } }这里有几个我踩过坑才加上的细节min-height: 100vh配合 flex 居中比position: absolute那套稳得多尤其是移动端浏览器地址栏收起展开时不会跳。旋转图标用border-top-color做比引入 SVG 或 GIF 轻量而且颜色能跟着主题走。media断点设在 480px覆盖绝大多数手机竖屏卡片内边距缩小避免内容贴边。按钮用a而不是button因为它们是跳转行为语义上更准确也自带键盘可访问性。3.3 JS 状态逻辑让页面“活”起来静态维护页有个问题用户不知道维护是不是还在进行会反复刷新。加一点 JS 逻辑让页面能反映真实状态体验会好很多。// maintenance.js (function () { // 模拟从后端接口获取维护状态 // 实际项目中替换为真实接口地址 const STATUS_API /api/maintenance/status; function updateStatus() { fetch(STATUS_API, { cache: no-store }) .then(res res.json()) .then(data { if (data.status online) { // 维护结束自动跳转回首页 window.location.href /; } else if (data.expectedEnd) { const timeEl document.querySelector(.time time); if (timeEl) timeEl.textContent data.expectedEnd; } }) .catch(() { // 接口不可用时静默失败不影响页面展示 }); } updateStatus(); // 每 60 秒轮询一次 setInterval(updateStatus, 60000); })();这段逻辑的价值在于维护结束后用户不需要手动刷新页面会自己跳回去。我实测过这个细节能显著降低用户“以为还没好”的困惑。轮询间隔设 60 秒是个平衡点太频繁会给后端压力太慢用户等得久。注意轮询接口本身要能承受维护期间的流量最好做成静态 JSON 文件或者走 CDN别让它依赖正在维护的主服务。4. 那些让维护页“翻车”的细节我一个个踩过技术实现讲完了但真正决定维护页成败的往往是那些不起眼的细节。这一节我把踩过的坑列出来你对照着检查。4.1 缓存问题用户看到的可能是旧页面这是最隐蔽的坑。你更新了维护页但用户浏览器缓存了旧版本看到的还是上一版内容。更糟的是维护结束后用户访问的还是缓存的维护页以为系统没恢复。解决办法是在维护页的响应头里明确设置缓存策略。如果是 Nginx可以这样配location /maintenance.html { add_header Cache-Control no-cache, no-store, must-revalidate; add_header Pragma no-cache; expires 0; }no-store是关键它告诉浏览器和中间代理都不要缓存这个页面。维护页本身很小不缓存带来的流量成本可以忽略但换来的是用户永远看到最新状态。4.2 状态码别用 200 返回维护页很多团队图省事维护时直接把维护页内容用 200 状态码返回。这在技术上是错的会带来两个问题一是监控系统检测不到异常二是搜索引擎会把维护页当成正常页面收录。正确做法是返回503 Service Unavailable并带上Retry-After头告诉客户端多久后重试location / { if (-f /var/www/maintenance.flag) { return 503; } } error_page 503 /maintenance.html; location /maintenance.html { internal; add_header Retry-After 3600; }Retry-After: 3600表示建议 1 小时后重试。这个头对搜索引擎爬虫特别有用它会按这个时间再来而不是频繁抓取。4.3 移动端字体中文在部分安卓机上会“发虚”这个问题我在好几个项目里遇到过。维护页在 iPhone 上很好看到了某些安卓机上一看中文字体发虚、粗细不均。原因是这些设备默认字体渲染策略不同加上没有指定合适的字体栈。我的做法是在font-family里把系统中文字体排在前面并且给正文加一个-webkit-font-smoothing: antialiasedbody { font-family: -apple-system, BlinkMacSystemFont, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Source Han Sans SC, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }字体栈的顺序是有讲究的先苹果系再微软雅黑最后思源黑体兜底。这样各平台都能命中本地已有字体不会触发网络字体加载首屏更快。4.4 深色模式不做会“闪瞎眼”现在很多系统默认深色模式如果维护页只有浅色版本用户半夜打开会被白底闪一下。加一段prefers-color-scheme媒体查询就能解决media (prefers-color-scheme: dark) { body { background: #1a1f26; color: #d8e0e8; } .maintenance-card { background: #232a33; box-shadow: none; } .desc { color: #9aa8b6; } .notice { color: #6b7885; } .btn.ghost { border-color: #3a4450; color: #9aa8b6; } }这段代码不复杂但体现的是对用户使用场景的考虑。我见过太多维护页在深色模式下白得刺眼用户第一反应就是关掉页面。4.5 别在维护页放外链资源维护期间主服务可能不可用如果你的维护页引用了主域名下的 CSS、JS、图片那页面可能加载不出来变成裸 HTML。所以维护页的所有资源要么内联要么放在独立的 CDN 或静态服务器上。我的习惯是维护页做成单文件CSS 和 JS 全部内联。这样它不依赖任何外部请求哪怕整个机房都断了只要这个文件能返回页面就能正常显示。文件大一点无所谓维护页访问量有限。5. 把维护页接入真实运维流程页面做好了怎么让它在该出现的时候出现、该消失的时候消失这是运维层面的问题。这一节讲接入方式。5.1 开关式一个文件控制维护状态最简单的方案是用一个标志文件。Nginx 检测到这个文件存在就返回维护页文件删除恢复正常。# 开启维护 touch /var/www/maintenance.flag # 结束维护 rm /var/www/maintenance.flag配合前面 Nginx 配置里的if (-f ...)判断就能实现一键切换。这个方案的好处是简单、可靠、不依赖任何服务运维同学在任何一台能 SSH 的机器上都能操作。5.2 灰度式只对部分用户展示维护页有时候升级是分批次进行的不能全量停机。这时候可以按 IP 段或用户 ID 做灰度只让一部分用户看到维护页。# 按 IP 段灰度只对测试网段展示维护页 geo $maintenance { default 0; 192.168.1.0/24 1; } server { if ($maintenance) { return 503; } }这个方案适合内部测试或者小范围验证。等确认没问题了再把default改成 1全量生效。5.3 自动式根据健康检查结果切换更进阶的做法是让维护页的展示自动化。比如后端有个健康检查接口Nginx 或者负载均衡器定期探测探测失败就自动切到维护页。这种方式适合那种“不希望人工介入”的场景但要注意设置合理的失败阈值避免网络抖动导致误切。我一般设连续 3 次失败才切换恢复则需要连续 5 次成功防止状态来回跳。5.4 维护页的“退出”同样重要很多人只关心怎么进入维护状态忽略了怎么退出。我遇到过维护结束后标志文件忘了删用户访问了好几个小时维护页的情况。所以退出流程要设计好要么在升级脚本里自动删除标志文件要么设置一个定时任务兜底比如“如果标志文件存在超过 4 小时自动删除并告警”。任何需要人工记得去做的收尾动作迟早会有人忘。6. 上线前必须验证的几件事维护页不像普通页面它平时不出现一旦出现就是关键时刻。所以上线前必须做一轮完整验证不能等真维护时才发现问题。6.1 断网测试模拟最坏情况把维护页单独部署到一个静态服务器然后断开主服务直接访问维护页地址。检查页面能否正常加载有没有依赖外部资源导致白屏CSS 是否生效布局有没有错乱移动端和桌面端显示是否都正常深色模式下是否可读这一步的目的是确认维护页在“主服务完全不可用”的情况下依然能工作。如果它自己都加载不出来那维护页就失去意义了。6.2 状态码与响应头检查用curl命令检查返回的状态码和响应头curl -I https://example.com/maintenance.html确认返回503并且有Retry-After和Cache-Control: no-store。这几个头是维护页能否被正确识别的关键不能少。6.3 自动跳转测试如果维护页带了轮询逻辑要测试维护结束后能否自动跳转。可以手动把状态接口的返回值改成online看页面是否在 60 秒内跳回首页。这个测试能避免“维护结束了用户还卡在维护页”的尴尬。6.4 多浏览器与多设备覆盖至少覆盖这几类环境环境检查重点Chrome 桌面布局、动效、深色模式Safari 桌面字体渲染、flex 兼容性iOS Safari地址栏收起展开时是否跳动Android Chrome中文字体是否发虚微信内置浏览器是否被拦截、按钮是否可点微信内置浏览器要特别测因为它的内核和标准 Chrome 有差异而且很多用户是在微信里点开链接的。我遇到过维护页在微信里按钮点不动的情况原因是某些 CSS 属性被微信的 X5 内核处理方式不同。7. 维护页还能怎么玩几个进阶思路基础方案讲完了如果你的项目对体验要求更高可以试试下面这些进阶做法。7.1 进度条把“等待”变成“可见的进展”如果升级过程有明确的阶段可以在维护页上放一个进度条实时反映升级进度。比如“数据备份中 30%”“服务重启中 70%”。这比单纯给个时间区间更能缓解焦虑因为用户能看到事情在推进。实现上可以让升级脚本在每个阶段往一个状态文件或接口写进度维护页轮询读取。注意进度条不要做得太精确否则卡在 99% 不动反而更让人抓狂。7.2 公告订阅把用户留下来维护页可以放一个“维护完成后通知我”的入口用户留下邮箱或手机号维护结束后自动通知。这既是服务也是一次用户触达机会。当然前提是你要真的发通知否则就是消耗信任。7.3 品牌化维护页也是品牌的一部分维护页是用户在你系统不可用时唯一能看到的东西它其实是一次品牌曝光。把品牌色、logo、语气风格融入进去让用户即使在等待时也能感受到你的专业和用心。我见过一些做得好的维护页用户甚至会截图分享说“这个维护页做得真好看”——这就是把负面场景做成了正面印象。7.4 多语言面向国际用户时别偷懒如果你的用户有海外群体维护页至少要提供中英双语。可以用navigator.language做简单判断或者直接做成双语并列展示。别让海外用户看到一屏看不懂的中文那体验比 404 还差。8. 我个人的几条实操心得最后分享几条从实际项目里攒下来的经验都是文档里不会写、但真能救命的。第一条维护页要提前做好别等要维护了才临时写。我见过太多团队在升级前半小时才开始做维护页结果手忙脚乱页面粗糙还容易出 bug。维护页应该作为基础设施的一部分平时就准备好需要时一键启用。第二条维护时间宁可说长不要说短。说 1 小时结果 40 分钟搞定用户觉得你效率高说 30 分钟结果拖到 1 小时用户觉得你不靠谱。预期管理是维护页的核心而预期管理的秘诀就是留余量。第三条维护结束后记得验证用户能正常访问。我踩过一次坑维护页的标志文件删了但 CDN 缓存了 503 响应导致部分用户还是看到维护页。后来我在退出流程里加了一步“刷新 CDN 缓存”才彻底解决。第四条把维护页的访问日志单独记录。维护期间有多少用户访问、停留多久、点了哪些按钮这些数据能帮你判断维护页的效果也能在事后复盘时提供依据。别小看这些数据它能告诉你用户到底在关心什么。第五条维护页的文案要有人情味。“系统维护中”和“我们正在给系统做一次升级很快回来”传达的感觉完全不同。前者是冷冰冰的通知后者是有人在跟你说话。维护页是系统“生病”时跟用户的对话语气温和一点用户的理解和耐心也会多一点。这套方案我在好几个项目里用过从简单的静态页到带轮询和灰度的完整方案都有。核心思路就一句话把维护页当成一次跟用户的沟通而不是一张技术公告。技术实现只是手段真正决定体验的是你有没有站在用户的角度想过他此刻的处境。