浏览器原理 · 47/51
1. addEventListener 第三个参数 2. addEventListener 与 attachEvent 区别 3. 浏览器兼容性测试与内核 4. 浏览器兼容性问题 5. 浏览器内核与引擎 6. 浏览器图层创建条件 7. 浏览器多进程架构 8. 浏览器渲染机制 9. 浏览器存储方案 10. 浏览器版本检测方法 11. children 与 childNodes 区别 12. 常见浏览器兼容性问题 13. Chrome 页面进程数量 14. 坐标系统对比 15. 多标签页通讯方案 16. 删除 Cookie 17. 自定义事件 18. DOM 事件处理方式演进 19. 元素尺寸属性对比 20. DOM 节点操作 21. DOM 事件机制 22. addEventListener 与 attachEvent 的区别 23. 获取页面所有复选框 24. HTMLCollection 与 NodeList 区别 25. Hybrid 应用开发 26. 强缓存命中机制 27. 浏览器缓存机制 28. 页面编码与资源编码不一致处理 29. jQuery 事件绑定方法对比 30. Input 点击触发的事件顺序 31. JavaScript 浏览器兼容性问题 32. jQuery 多事件绑定实现 33. JSBridge 原理 34. 链接点击后 Hover 失效解决方案 35. 减少重绘和回流的性能优化 36. 移动端 300ms 点击延迟问题 37. 移动端视口配置 38. 移动端点击穿透问题解决 39. 移动端兼容性问题 40. JSBridge 原理与实现 41. 移动端 1px 像素问题解决方案 42. 浏览器渲染流程 43. 页面加载完成事件对比 44. Offset、Scroll、Client 属性对比 45. 同源策略与跨域解决方案 46. Script 标签位置对页面加载的影响 47. Service Worker 与 PWA 48. 存储方案对比:Cookie、Storage、IndexedDB 49. 强缓存默认时间 50. URL 到页面显示的完整过程 51. V8 引擎 JavaScript 执行过程

Service Worker 与 PWA

Service Worker 的工作原理、生命周期及 PWA 应用场景

问题

什么是 Service Worker?它有哪些应用场景?如何用它构建 PWA?

解答

什么是 Service Worker

Service Worker 是运行在浏览器后台的脚本,独立于网页,充当浏览器与网络之间的代理。它可以拦截网络请求、缓存资源、实现离线访问。

特点:

  • 运行在独立线程,不阻塞主线程
  • 无法直接访问 DOM
  • 只能在 HTTPS 环境下运行(localhost 除外)
  • 完全异步,基于 Promise

生命周期

// sw.js - Service Worker 文件

// 1. 安装阶段:缓存静态资源
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/app.js',
        '/offline.html'
      ]);
    })
  );
  // 跳过等待,立即激活
  self.skipWaiting();
});

// 2. 激活阶段:清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== 'v1')
          .map((name) => caches.delete(name))
      );
    })
  );
  // 立即控制所有页面
  self.clients.claim();
});

// 3. 拦截请求
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // 缓存命中,返回缓存
      if (response) {
        return response;
      }
      // 缓存未命中,发起网络请求
      return fetch(event.request);
    })
  );
});

注册 Service Worker

// main.js - 主页面注册

if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('SW 注册成功,作用域:', registration.scope);
    } catch (error) {
      console.log('SW 注册失败:', error);
    }
  });
}

缓存策略

// 策略1:缓存优先(适合静态资源)
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request);
    })
  );
});

// 策略2:网络优先(适合 API 请求)
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // 请求成功,更新缓存
        const clone = response.clone();
        caches.open('v1').then((cache) => {
          cache.put(event.request, clone);
        });
        return response;
      })
      .catch(() => {
        // 网络失败,返回缓存
        return caches.match(event.request);
      })
  );
});

// 策略3:Stale While Revalidate(先返回缓存,后台更新)
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open('v1').then((cache) => {
      return cache.match(event.request).then((cached) => {
        const fetchPromise = fetch(event.request).then((response) => {
          cache.put(event.request, response.clone());
          return response;
        });
        return cached || fetchPromise;
      });
    })
  );
});

PWA 完整示例

// manifest.json - PWA 配置文件
{
  "name": "My PWA App",
  "short_name": "PWA",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#007bff",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- PWA 必需的 meta 标签 -->
  <meta name="theme-color" content="#007bff">
  <link rel="manifest" href="/manifest.json">
  <link rel="apple-touch-icon" href="/icon-192.png">
  <title>PWA Demo</title>
</head>
<body>
  <h1>PWA 示例</h1>
  <script src="/main.js"></script>
</body>
</html>

推送通知

// 请求通知权限并订阅
async function subscribePush() {
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return;

  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY'
  });
  
  // 发送订阅信息到服务器
  await fetch('/api/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription),
    headers: { 'Content-Type': 'application/json' }
  });
}

// sw.js - 接收推送
self.addEventListener('push', (event) => {
  const data = event.data.json();
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icon-192.png'
    })
  );
});

// 点击通知
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  event.waitUntil(
    clients.openWindow('/')
  );
});

后台同步

// 主页面:注册同步任务
async function syncData() {
  const registration = await navigator.serviceWorker.ready;
  await registration.sync.register('sync-data');
}

// sw.js - 处理同步
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-data') {
    event.waitUntil(
      // 从 IndexedDB 读取待同步数据,发送到服务器
      syncPendingData()
    );
  }
});

关键点

  • 独立线程:Service Worker 运行在独立线程,不能访问 DOM,通过 postMessage 与页面通信
  • 生命周期:install → waiting → activate → fetch,更新时新 SW 会等待旧 SW 释放控制权
  • HTTPS 必需:出于安全考虑,只能在 HTTPS 或 localhost 下运行
  • 缓存策略:根据资源类型选择缓存优先、网络优先或 Stale While Revalidate
  • PWA 三要素:HTTPS + manifest.json + Service Worker,满足后可安装到桌面