宝塔 Nginx 构建 Service Worker

当网站后端暂时无法访问时,可以让 Nginx 自动显示一个静态备用页面,提示用户检查网络,并提供售后信息。

一、准备文件

将以下两个文件上传到宝塔网站根目录:

text 复制代码
/www/wwwroot/test.zhangsan.cool/offline.html
/www/wwwroot/test.zhangsan.cool/sw.js

文件说明(供参考,具体自行修改):

  • offline.html:网站无法访问时显示的备用页面。
bash 复制代码
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
  <meta name="theme-color" content="#f5f7fb">
  <meta name="description" content="网站备用入口和联系方式。">
  <title>网站暂时无法访问|备用入口</title>
  <style>
    :root { color-scheme: light; --ink:#182235; --muted:#687386; --blue:#2563eb; --line:#e2e7f0; --surface:#fff; }
    * { box-sizing:border-box; }
    html, body { margin:0; min-height:100%; }
    body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",Arial,sans-serif; color:var(--ink); background:#f5f7fb; }
    .shell { min-height:100vh; padding:24px 16px calc(28px + env(safe-area-inset-bottom)); display:flex; align-items:center; justify-content:center; }
    .page { width:min(100%,620px); }
    .card { background:var(--surface); border:1px solid var(--line); border-radius:18px; padding:28px 20px 22px; box-shadow:0 12px 35px rgba(30,45,75,.07); }
    h1 { margin:0; font-size:clamp(28px,7vw,42px); line-height:1.2; letter-spacing:-.03em; }
    .status { display:inline-flex; align-items:center; gap:7px; margin-bottom:14px; color:#a16207; font-size:13px; font-weight:700; }
    .status-dot { width:8px; height:8px; border-radius:50%; background:#eab308; box-shadow:0 0 0 4px #fef3c7; }
    .intro { margin:10px 0 22px; color:var(--muted); font-size:15px; line-height:1.65; }
    .nav-link { display:flex; align-items:center; justify-content:center; gap:9px; min-height:54px; padding:14px 18px; border-radius:12px; color:#fff; background:var(--blue); text-decoration:none; font-size:17px; font-weight:700; box-shadow:0 8px 16px rgba(37,99,235,.2); }
    .nav-link:hover, .nav-link:focus-visible { background:#1d4ed8; }
    .nav-link svg { width:19px; height:19px; }
    .section-title { margin:26px 0 10px; font-size:17px; }
    .contacts { display:grid; gap:10px; }
    .contact { display:flex; align-items:center; gap:12px; min-width:0; padding:14px; border:1px solid var(--line); border-radius:12px; }
    .contact-icon { flex:0 0 36px; width:36px; height:36px; display:grid; place-items:center; border-radius:10px; color:var(--blue); background:#eef4ff; }
    .contact-icon svg { width:20px; height:20px; }
    .contact-copy { flex:1; min-width:0; }
    .label { margin:0 0 3px; color:var(--muted); font-size:13px; }
    .value { margin:0; font-size:19px; font-weight:700; overflow-wrap:anywhere; }
    .copy { flex:0 0 auto; min-height:40px; padding:0 14px; border:1px solid #bfd2f6; border-radius:9px; color:var(--blue); background:#fff; font:inherit; font-size:14px; font-weight:700; cursor:pointer; }
    .copy:hover, .copy:focus-visible { background:#eef4ff; }
    .copy.copied { border-color:#b7dfc9; color:#147a4b; background:#edf9f2; }
    .contact--featured { border-color:#bfd2f6; background:#f8fbff; }
    :is(.nav-link, .copy):focus-visible { outline:3px solid rgba(37,99,235,.28); outline-offset:3px; }
    .note { margin:18px 0 0; color:#8a94a5; font-size:12px; line-height:1.6; text-align:center; }
    .toast { position:fixed; left:50%; bottom:calc(22px + env(safe-area-inset-bottom)); transform:translate(-50%,16px); opacity:0; pointer-events:none; padding:9px 13px; border-radius:9px; color:#fff; background:#1d293d; font-size:13px; transition:opacity .18s,transform .18s; }
    .toast.show { opacity:1; transform:translate(-50%,0); }
    @media (max-width:599px) { .shell { padding:14px 12px calc(20px + env(safe-area-inset-bottom)); } .card { padding:18px 14px 22px; border-radius:15px; } .intro { margin-bottom:18px; } .section-title { margin-top:22px; } }
    @media (min-width:760px) { .card { padding:40px 42px 30px; } .contacts { grid-template-columns:1fr 1fr; } }
    @media (prefers-reduced-motion: reduce) { .toast { transition:none; } }
  </style>
</head>
<body>
  <main class="shell">
    <section class="page card" aria-labelledby="title">
      <div class="status"><span class="status-dot" aria-hidden="true"></span>当前连接可能不稳定</div>
      <h1 id="title">网站暂时无法访问</h1>
      <p class="intro">请先检查网络连接并刷新页面;如果仍无法访问,可通过下方联系方式获取帮助。</p>
      <a class="nav-link" href="https://vlink.cc/uu5wok0k" target="_blank" rel="noopener noreferrer">打开最新网址<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M14 5h5v5M19 5l-8 8"/><path d="M18 13v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/></svg></a>
      <h2 class="section-title">备用联系方式</h2>
      <div class="contacts">
        <div class="contact contact--featured"><span class="contact-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 7h10v8H7z"/><path d="M7 10H4v5h3M17 10h3v5h-3M9 18h6M12 15v3"/></svg></span><div class="contact-copy"><p class="label">售后QQ群 · 进群获取最新资讯</p><p class="value">815440815</p></div><button class="copy" type="button" data-copy="815440815" data-default-label="复制" aria-label="复制QQ群号 815440815">复制</button></div>
        <div class="contact"><span class="contact-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 11.5a7.5 7.5 0 0 1-11.8 6.1L4 19l1.5-3.9A7.5 7.5 0 1 1 20 11.5Z"/><path d="M9 10.5c.6 1.3 1.7 2.4 3 3l1-.8 1.5.7-.3 1.3c-.2.6-.8.8-1.4.6a8.3 8.3 0 0 1-5.1-5.1c-.2-.6 0-1.2.6-1.4l1.3-.3.7 1.5-.8 1Z"/></svg></span><div class="contact-copy"><p class="label">售后微信</p><p class="value">15682448658</p></div><button class="copy" type="button" data-copy="15682448658" data-default-label="复制" aria-label="复制售后微信号 15682448658">复制</button></div>
      </div>
      <p class="note">建议收藏本页,方便下次快速找到入口。</p>
    </section>
  </main>
  <div class="toast" role="status" aria-live="polite"></div>
  <script>
    (() => {
      const toast = document.querySelector('.toast');
      let timer;
      const show = (message) => { toast.textContent = message; toast.classList.add('show'); clearTimeout(timer); timer = setTimeout(() => toast.classList.remove('show'), 1800); };
      const fallbackCopy = (text) => { const area = document.createElement('textarea'); area.value = text; area.setAttribute('readonly', ''); area.style.position = 'fixed'; area.style.opacity = '0'; document.body.appendChild(area); area.select(); const ok = document.execCommand('copy'); area.remove(); return ok; };
      document.querySelectorAll('[data-copy]').forEach((button) => button.addEventListener('click', async () => { const text = button.dataset.copy; try { if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text); else if (!fallbackCopy(text)) throw new Error('copy failed'); button.textContent = '已复制 ✓'; button.classList.add('copied'); clearTimeout(button._resetTimer); button._resetTimer = setTimeout(() => { button.textContent = button.dataset.defaultLabel; button.classList.remove('copied'); }, 1800); show('已复制:' + text); } catch { show('复制失败,请手动复制'); } }));
    })();
  </script>
  <script>
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js', { scope: '/' })
          .then((registration) => {
            console.log('Service Worker 注册成功:', registration.scope);
            return registration.update();
          })
          .catch((error) => console.error('Service Worker 注册失败:', error));
      });
    } else {
      console.warn('当前浏览器不支持 Service Worker');
    }
  </script>
</body>
</html>
  • sw.js:负责缓存备用页面,并在断网时返回缓存内容。
bash 复制代码
const CACHE_NAME = 'offline-contact-v5';
const OFFLINE_URL = '/offline.html';

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.add(OFFLINE_URL))
  );
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) => Promise.all(
      keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
    ))
  );
  self.clients.claim();
});

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (
    event.request.method !== 'GET' ||
    event.request.mode !== 'navigate' ||
    url.origin !== self.location.origin ||
    url.pathname.startsWith('/service-api/')
  ) return;

  event.respondWith(
    fetch(event.request)
      .then((response) => {
        if (![502, 503, 504].includes(response.status)) return response;
        return caches.match(OFFLINE_URL).then((cached) => cached || response);
      })
      .catch(() => caches.match(OFFLINE_URL).then((cached) => cached || Response.error()))
  );
});

二、配置 HTTPS

Service Worker 必须运行在 HTTPS 环境下。

在宝塔中进入:

text 复制代码
网站 → 你的域名 → SSL

申请并启用 SSL,之后使用下面的地址访问网站:

text 复制代码
https://你的域名/

不要使用普通 HTTP 或服务器 IP 地址测试。

三、添加宝塔自定义配置

进入:

text 复制代码
网站 → 你的域名 → 配置文件 → 自定义配置文件

不要修改"主配置文件",将下面完整内容粘贴到"自定义配置文件"中:

nginx 复制代码
location = /sw.js {
    root /www/wwwroot/test.zhangsan.cool;
    default_type application/javascript;
    add_header Cache-Control "no-cache" always;
    add_header Service-Worker-Allowed "/" always;
    try_files $uri =404;
}

location = /offline.html {
    root /www/wwwroot/test.zhangsan.cool;
    default_type text/html;
    add_header Cache-Control "no-cache" always;
    try_files $uri =404;
}

error_page 502 503 504 =200 /offline.html;

proxy_set_header Accept-Encoding "";

sub_filter_types text/html;
sub_filter_once on;

sub_filter '</body>' '<script>if("serviceWorker" in navigator){window.addEventListener("load",function(){navigator.serviceWorker.register("/sw.js",{scope:"/"}).then(function(r){console.log("Service Worker 注册成功",r.scope);return r.update();}).catch(function(e){console.error("Service Worker 注册失败",e);});});}</script></body>';

其中反向代理后端使用本机地址:

text 复制代码
127.0.0.1:8080

如果宝塔已经自动生成了反向代理配置,不要重复添加第二个 location ^~ /,只添加上面的自定义内容即可。

四、修改页面后更新缓存

编辑修改 offline.htmlsw.js 后,需要修改 sw.js 中的缓存版本号,例如:

js 复制代码
const CACHE_NAME = 'offline-contact-v4';

然后重新上传文件,联网刷新页面,再测试断网。

相关推荐
Irene19918 个月前
前端缓存方式 对比 和 Service Worker 缓存详解
缓存·service worker
明天最后10 个月前
使用 Service Worker 限制请求并发数
前端·service worker
伍哥的传说1 年前
Vite Plugin PWA – 零配置构建现代渐进式Web应用
开发语言·前端·javascript·web app·pwa·service worker·workbox