MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

使用Service Worker实现离线访问能力

2024-06-012.0k 阅读

什么是 Service Worker

1. 基本概念

Service Worker 本质上是一种在后台运行的脚本,它能够拦截和处理网络请求,就像是一个在网络请求与浏览器之间的“中间人”。它独立于网页主线程运行,这意味着它不会阻塞页面的渲染或其他脚本的执行。Service Worker 基于 Promise 设计,这使得异步操作的处理变得更加简洁和高效。

从功能上来说,它有点类似于代理服务器,但它是运行在浏览器环境中的。它可以拦截页面发出的所有网络请求,无论是获取 HTML、CSS、JavaScript 文件,还是加载图片、调用 API 等。通过 Service Worker,我们可以根据预先设定的规则,决定是从网络获取资源,还是从本地缓存中读取资源,这为实现离线访问能力奠定了基础。

2. 生命周期

Service Worker 有着自己独特的生命周期,主要包括以下几个阶段:

  • 安装(Install):当注册 Service Worker 脚本时,浏览器会尝试下载并解析该脚本。如果下载和解析成功,就会进入安装阶段。在这个阶段,通常会进行缓存初始化操作,比如将一些必要的静态资源缓存到本地。这个过程由 install 事件触发。例如:
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('my-cache-v1')
    .then(cache => cache.addAll([
      '/index.html',
      '/styles.css',
      '/script.js'
    ]))
  );
});

在上述代码中,event.waitUntil 方法会阻塞安装过程,直到 caches.opencache.addAll 操作完成。这样可以确保缓存的资源都被正确添加,安装过程才会结束。

  • 激活(Activate):安装完成后,Service Worker 会进入激活阶段。这个阶段主要用于清理旧的缓存版本,以确保使用的是最新的缓存策略。activate 事件在这个阶段触发。示例代码如下:
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys()
    .then(cacheNames => {
      return Promise.all(
        cacheNames.filter(cacheName => {
          return cacheName.startsWith('my-cache-') && cacheName !== 'my-cache-v1';
        }).map(cacheName => {
          return caches.delete(cacheName);
        })
      );
    })
  );
});

这里通过 caches.keys 获取所有缓存的名称,然后过滤出旧版本的缓存名称,并使用 caches.delete 删除它们。

  • 空闲(Idle):激活完成后,Service Worker 进入空闲状态,等待拦截网络请求。

  • 拦截请求(Fetch):当页面发出网络请求时,fetch 事件会被触发,Service Worker 可以在这个事件处理函数中决定如何响应请求。这是实现离线访问的关键部分,后面会详细介绍。

注册 Service Worker

1. 基础注册

在网页中注册 Service Worker 是启用其功能的第一步。在主脚本(通常是 JavaScript 文件)中,使用 navigator.serviceWorker.register 方法进行注册。代码如下:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service - worker.js')
    .then(registration => {
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    })
    .catch(err => {
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

在这段代码中,首先检查浏览器是否支持 Service Worker。然后,在页面加载完成后(window.addEventListener('load'...)进行注册。navigator.serviceWorker.register 方法接受 Service Worker 脚本的路径作为参数。如果注册成功,then 回调函数会被执行,打印出注册成功的信息以及 Service Worker 的作用域。如果注册失败,catch 回调函数会捕获错误并打印错误信息。

2. 注册参数详解

navigator.serviceWorker.register 方法还可以接受第二个参数,用于配置 Service Worker 的一些选项。其中最重要的是 scope 选项,它定义了 Service Worker 的作用域。例如:

navigator.serviceWorker.register('/service - worker.js', { scope: '/my - app/' })

上述代码中,将 Service Worker 的作用域设置为 /my - app/,这意味着它只能拦截 /my - app/ 及其子目录下的网络请求。如果不设置 scope 选项,默认作用域是 Service Worker 脚本所在的目录。

另外,updateViaCache 选项可以控制 Service Worker 脚本的更新方式。默认值是 'imports',表示只有当 Service Worker 脚本所依赖的导入脚本发生变化时才会更新。如果设置为 'all',则只要 Service Worker 脚本文件本身发生变化就会更新。

使用 Service Worker 实现离线访问

1. 缓存策略

实现离线访问需要合理选择缓存策略。常见的缓存策略有以下几种:

  • 缓存优先(Cache - First):这种策略优先从缓存中获取资源。如果缓存中有请求的资源,则直接返回缓存的资源,而不发起网络请求。只有当缓存中没有该资源时,才会尝试从网络获取。这种策略适用于那些不经常变化的静态资源,比如样式表、脚本和图片等。示例代码如下:
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
    .then(response => {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});

在上述代码中,caches.match(event.request) 尝试从缓存中匹配请求的资源。如果匹配成功,就返回缓存的响应;否则,使用 fetch(event.request) 发起网络请求。

  • 网络优先(Network - First):与缓存优先相反,这种策略优先尝试从网络获取资源。如果网络请求成功,则返回网络响应,并将其缓存起来,以便下次使用。只有当网络请求失败时,才会从缓存中获取资源。这种策略适用于那些需要实时数据的情况,比如 API 接口调用。示例代码如下:
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
    .catch(() => {
      return caches.match(event.request);
    })
  );
});

这里先使用 fetch(event.request) 发起网络请求,如果请求失败(进入 catch 块),则从缓存中获取资源。

  • Stale - While - Revalidate:这种策略在返回缓存资源的同时,发起网络请求更新缓存。也就是说,用户可以立即得到缓存中的旧数据,同时后台更新缓存,下次请求就可以得到最新的数据。这种策略适用于对数据实时性要求不是特别高,但又希望尽量保持数据新鲜的场景。示例代码如下:
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cachedResponse => {
      const fetchPromise = fetch(event.request)
      .then(networkResponse => {
        caches.open('my - cache - v1')
        .then(cache => cache.put(event.request, networkResponse.clone()));
        return networkResponse;
      });
      return cachedResponse || fetchPromise;
    })
  );
});

在这段代码中,首先尝试从缓存中获取资源(caches.match(event.request))。同时,发起网络请求(fetch(event.request)),如果网络请求成功,将响应克隆一份并缓存到 my - cache - v1 中。最后,返回缓存的响应(如果有)或者网络请求的响应。

2. 离线页面缓存

为了实现完全的离线访问,不仅要缓存静态资源,还需要缓存页面本身。在 Service Worker 的 install 事件中,可以将 HTML 页面添加到缓存中。例如:

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('my - cache - v1')
    .then(cache => cache.addAll([
      '/index.html',
      '/about.html',
      // 其他页面
    ]))
  );
});

这样,当用户离线访问这些页面时,Service Worker 可以从缓存中直接提供页面内容。

3. 动态缓存更新

随着应用的发展,资源可能会发生变化。为了确保缓存中的资源是最新的,需要动态更新缓存。在 Service Worker 的 fetch 事件中,可以实现动态缓存更新。例如,当网络请求成功时,将新的响应缓存起来:

self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
    .then(response => {
      caches.open('my - cache - v1')
      .then(cache => cache.put(event.request, response.clone()));
      return response;
    })
    .catch(() => {
      return caches.match(event.request);
    })
  );
});

在上述代码中,当 fetch(event.request) 成功获取到响应后,先将响应克隆一份(因为响应流只能被消费一次),然后使用 cache.put 将其缓存到 my - cache - v1 中,最后返回响应。

与页面通信

1. 从 Service Worker 向页面发送消息

Service Worker 可以向关联的页面发送消息。在 Service Worker 脚本中,可以使用 clients 对象获取所有关联的页面窗口,然后通过 postMessage 方法发送消息。例如:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
    .then(response => {
      if (response) {
        clients.matchAll({ type: 'window' })
        .then(clientsList => {
          clientsList.forEach(client => {
            client.postMessage({
              type: 'cached - response',
              message: 'Using cached response for this request'
            });
          });
        });
      }
      return response || fetch(event.request);
    })
  );
});

在上述代码中,当使用缓存响应请求时,通过 clients.matchAll 获取所有类型为 window 的客户端(即页面窗口),然后遍历这些窗口并使用 postMessage 发送消息。

在页面端,可以通过监听 message 事件来接收消息:

window.addEventListener('message', event => {
  if (event.data.type === 'cached - response') {
    console.log(event.data.message);
  }
});

2. 从页面向 Service Worker 发送消息

页面也可以向 Service Worker 发送消息。在页面脚本中,可以使用 navigator.serviceWorker.controller.postMessage 方法(前提是 Service Worker 已经激活并控制了该页面)。例如:

if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
  navigator.serviceWorker.controller.postMessage({
    type: 'update - cache',
    message: 'Please update the cache'
  });
}

在 Service Worker 脚本中,通过监听 message 事件来接收页面发送的消息:

self.addEventListener('message', event => {
  if (event.data.type === 'update - cache') {
    // 执行缓存更新逻辑
  }
});

Service Worker 的兼容性与调试

1. 兼容性

虽然 Service Worker 是现代 Web 开发中的一项强大技术,但并不是所有浏览器都支持。截至目前,主流浏览器如 Chrome、Firefox、Safari(部分版本开始支持)和 Edge 都提供了对 Service Worker 的支持。为了确保应用在不同浏览器中的兼容性,可以在代码中进行特性检测,如前面注册 Service Worker 时检查 'serviceWorker' in navigator

2. 调试

在开发过程中,调试 Service Worker 至关重要。Chrome 浏览器提供了强大的调试工具。可以在 Chrome 开发者工具的“Application”标签中找到“Service Workers”面板。在这个面板中,可以查看 Service Worker 的状态(如是否注册、激活等),查看缓存的内容,以及模拟离线状态进行测试。

另外,可以在 Service Worker 脚本中使用 console.log 输出调试信息,这些信息可以在浏览器的控制台中查看。例如:

self.addEventListener('install', event => {
  console.log('Service Worker is installing...');
  event.waitUntil(
    caches.open('my - cache - v1')
    .then(cache => cache.addAll([
      '/index.html',
      '/styles.css',
      '/script.js'
    ]))
  );
});

通过这些调试手段,可以快速定位和解决 Service Worker 开发过程中遇到的问题,确保离线访问功能的正常实现。

高级应用场景

1. 后台同步

Service Worker 可以实现后台同步功能。这意味着即使页面关闭或者网络暂时不可用,仍然可以将数据发送到服务器。例如,在用户离线填写表单后,当网络恢复时,Service Worker 可以自动将表单数据提交到服务器。

首先,在页面脚本中注册后台同步任务:

if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
  const form = document.getElementById('my - form');
  form.addEventListener('submit', event => {
    event.preventDefault();
    const formData = new FormData(form);
    navigator.serviceWorker.controller.ready
    .then(serviceWorker => {
      serviceWorker.sync.register('form - submission')
      .then(() => {
        console.log('Background sync registered successfully');
      })
      .catch(err => {
        console.log('Background sync registration failed: ', err);
      });
    });
  });
}

在上述代码中,当表单提交时,阻止默认提交行为,然后通过 navigator.serviceWorker.controller.sync.register 注册一个名为 form - submission 的后台同步任务。

在 Service Worker 脚本中,监听 sync 事件来处理后台同步任务:

self.addEventListener('sync', event => {
  if (event.tag === 'form - submission') {
    event.waitUntil(
      fetch('/submit - form', {
        method: 'POST',
        body: new FormData() // 这里需要实际的表单数据,可从缓存等地方获取
      })
      .then(response => {
        if (response.ok) {
          console.log('Form submitted successfully');
        } else {
          console.log('Form submission failed');
        }
      })
    );
  }
});

这里在 sync 事件处理函数中,当 event.tagform - submission 时,发起一个 POST 请求将表单数据提交到服务器。

2. 推送通知

Service Worker 还可以实现推送通知功能。这允许服务器在用户离线或者未打开应用时,向用户发送通知。

首先,在页面脚本中获取推送权限并注册推送服务:

if ('serviceWorker' in navigator && 'PushManager' in window) {
  navigator.serviceWorker.register('/service - worker.js')
  .then(registration => {
    return registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY')
    });
  })
  .then(subscription => {
    // 将 subscription 发送到服务器
    fetch('/subscribe', {
      method: 'POST',
      headers: {
        'Content - Type': 'application/json'
      },
      body: JSON.stringify(subscription)
    });
  })
  .catch(err => {
    console.log('Push registration failed: ', err);
  });
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
  .replace(/-/g, '+')
  .replace(/_/g, '/');
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

在上述代码中,首先注册 Service Worker,然后使用 registration.pushManager.subscribe 获取推送订阅信息。这里需要一个 VAPID(Voluntary Application Server Identification)公钥,将其转换为 Uint8Array 格式。获取到订阅信息后,将其发送到服务器。

在 Service Worker 脚本中,监听 push 事件来显示推送通知:

self.addEventListener('push', event => {
  event.waitUntil(
    self.registration.showNotification('New Update', {
      body: 'There is a new update available',
      icon: '/icon.png'
    })
  );
});

这里当接收到 push 事件时,使用 self.registration.showNotification 显示一个通知。服务器端负责在合适的时候向用户发送推送消息,触发 Service Worker 的 push 事件。

通过以上对 Service Worker 的深入介绍,包括其基本概念、生命周期、注册方法、离线访问实现、与页面通信、兼容性与调试以及高级应用场景等方面,相信开发者能够充分掌握并利用 Service Worker 为 Web 应用增添强大的离线访问能力和其他高级功能。