查看原文
科技

【第3206期】Service Worker:离线应用与后台同步的解决方案

龚思晗 前端早读课 2024-03-07

前言

介绍了 Service Worker 的基本概念和生命周期,以及如何利用 Service Worker 实现离线缓存和缓存更新的功能。同时,还介绍了 Service Worker 的注册、安装、激活、以及缓存策略等方面的内容。最后,讲解了如何通过 Service Worker 实现消息推送、多页面通信等功能,提升用户体验。今日前端早读课文章由 @龚思晗分享,公号:哈啰技术授权。

正文从这开始~~

前端常用缓存技术

前端常用缓存技术一般分为 http 缓存和浏览器缓存。

HTTP 缓存

Expires

HTTP1.0 的内容,服务器使用 Expires 头来告诉 Web 客户端它可以使用当前副本,直到指定的时间为止;

Cache-Control

HTTP1.1 引入了 Cathe-Control,它使用 max-age 指定资源被缓存多久,主要是解决了 Expires 一个重大的缺陷,就是它设置的是一个固定的时间点,客户端时间和服务端时间可能有误差;

Last-Modified / If-Modified-Since

Last-Modified 是服务器告诉浏览器该资源的最后修改时间,If-Modified-Since 是请求头带上的,上次服务器给自己的该资源的最后修改时间。然后服务器拿去对比。

若 Last-Modified 大于 If-Modified-Since,说明资源有被改动过,则响应整片资源内容,返回状态码 200;

若 Last-Modified 小于或等于 If-Modified-Since,说明资源无新修改,则响应 HTTP 304,告知浏览器继续使用当前版本。

Etag / If-None-Match

Etag 是服务器根据每个资源生成的唯一标识符,当文件内容被修改时标识符就会重新生成。服务器存储着文件的 Etag 字段,可以在与每次客户端传送 If-none-match 的字段进行比较。如果相等,则表示未修改,响应 304;反之,则表示已修改,响应 200 状态码,返回数据。

浏览器缓存

Storage

简单的缓存方式有 cookie,localStorage 和 sessionStorage,都是浏览器内置储存功能。

mainfest

html5 引入的新标准,可以离线缓存静态文件。

Service Worker

Service Worker 介绍

什么是 Service Worker

Service Worker 本质上是充当 web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们的目的是创建有效的离线体验、拦截网络请求并根据网络是否可用采取适当的操作,以及更新服务器上的资产。它们还允许访问推送通知和后台同步 API。

【第2598期】ServiceWorker 缓存与 HTTP 缓存

ServiceWorker 特性

Service Worker 本质上是一个 Web Worker,它独立于 Javascript 主线程,因此它不能直接访问 DOM,也不能直接访问 window 对象,但是可以访问 navigator 对象,也可以通过消息传递的方式(如 postMessage)与 Javascript 主线程进行通信。

Service Worker 独立于 Javascript 主线程,所以不会造成阻塞。它设计为完全异步,同步 API(如 XHR 和 localStorage 不能在 Service Worker 中使用。

Service Worker 是基于 HTTPS 的,因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。如果是本地调试的话,localhost 是可以的。

Service Worker 拥有独立的生命周期,与页面无关 (关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动)。注册 Service Worker 后,浏览器会默默地在背后安装 Service Worker。

Service Worker 生命周期

Service Worker 的生命周期可以分为 6 个阶段:解析 (parsed)、安装 (installing)、安装完成 (installed)、激活 (activating)、激活完成 (activated)、闲置 (redundant)。

1. Parsed

当我们第一次尝试注册 Service Worker 时,用户代理会解析脚本并获取入口点。如果解析成功(并且满足其他一些要求,例如 HTTPS),我们将可以访问 Service Worker 注册对象。其中包含有关 Service Worker 的状态及其作用域的信息。

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js')
.then(function(registration) {
console.log("Service Worker Registered", registration);
})
.catch(function(err) {
console.log("Service Worker Failed to Register", err);
})
}

Service Worker 注册成功并不意味着它已安装完毕或处于激活状态,而只是意味着脚本已成功解析,它与文档处于同一源上,且源为 HTTPS。注册完成后,服务 Worker 将进入下一个状态。

【第2255期】Service Worker 实践指南

2. Installing

一旦 Service Worker 脚本被解析,用户代理就会尝试安装它,并进入安装状态。在 Service Worker 的 registration 对象中,我们可以在 installing 属性中检查此状态。

并且,在 installing 状态下,install 事件会被触发,我们一般会在这个回调中处理缓存事件。

navigator.serviceWorker.register('./sw.js').then(function(registration) {
if (registration.installing) {
// Service Worker is Installing
}
})
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(currentCacheName).then(function(cache) {
return cache.addAll(arrayOfFilesToCache);
})
);
});

如果事件中有 event.waitUntil() 方法,其中的 Promise 只有在 resolve 后,install 事件才会成功。如果 Promise 被拒绝,install 就会失败,Service Worker 就会变为 redundant 状态。

self.addEventListener('install', function(event) {
event.waitUntil(
return Promise.reject(); // Failure
);
});

3. Installed / Waiting

如果安装成功,Service Worker 的状态变为 installed (也叫 waiting )。处于这个状态时, Service Worker 是有效的但是是未激活的 worker,暂时没有控制页面的权力,需要等待从当前 worker 获得控制权。

我们可以在 registration 对象的 waiting 属性中检测到此状态。

navigator.serviceWorker.register('./sw.js').then(function(registration) {
if (registration.waiting) {
// Service Worker is Waiting
}
})

我们可以在这个时机去更新新版本或自动更新缓存。

4. Activating

在以下情况之一时,处于 Waiting 状态的 worker 的 Activating 状态会被触发:

  • 当前没有处于激活状态的 worker

  • self.skipWaiting() 在 sw.js 中被调用,直接跳过 waiting 阶段

  • 用户导航离开当前页面,从而释放了前一个 active worker

  • 经过了指定时间段,从而释放了前一个 active worker

在当前状态下,activate 事件会被触发,在这个回调中我们通常用于清除旧缓存。

self.addEventListener('activate', function(event) {
event.waitUntil(
// Get all the cache names
caches.keys().then(function(cacheNames) {
return Promise.all(
// Get all the items that are stored under a different cache name than the current one
cacheNames.filter(function(cacheName) {
return cacheName != currentCacheName;
}).map(function(cacheName) {
// Delete the items
return caches.delete(cacheName);
})
); // end Promise.all()
}) // end caches.keys()
); // end event.waitUntil()
});

同 install 事件,如果 Promise 被 reject 了,则 activate 事件失败,Service Worker 变为 redundant 状态。

5. Activated

如果激活成功,Service Worker 状态会变成 active ,在这个状态下,Service Worker 是一个可以完全控制网页的激活 worker,我们可以在 registration 对象的 active 属性中检测到此状态。

navigator.serviceWorker.register('./sw.js').then(function(registration) {
if (registration.active) {
// Service Worker is Active
}
})

当 Service Worker 被成功激活后,即可处理绑定的 fetch 和 message 事件。

self.addEventListener('fetch', function(event) {
// Do stuff with fetch events
});

self.addEventListener('message', function(event) {
// Do stuff with postMessages received from document
});

6. Redundant

以下任一情况,Service Worker 都会变成 redundant。

  • install 失败

  • activate 失败

  • 有新的 Service Worker 将其替代成为现有的激活 worker

Service Worker 离线缓存

Service Worker 最重要的功能之一,就是可以通过缓存静态资源来实现离线访问我们的页面。

Service Worker 的缓存基于 CacheStorage,它是一个 Promise 对象,我们可以通过 caches 来获取它。CacheStorage 提供了一些方法,我们可以通过这些方法来对缓存进行操作。

caches.open(currentCacheName).then(function (cache) {
/** 可以通过cache.put来添加缓存
* 它接收两个参数,第一个参数是Request对象或URL字符串,第二个参数是Response对象
*/

cache.put(new Request('/'), new Response('Hello World'));

/** 可以通过cache.addAll来添加缓存资源数组
* 它接收一个参数,这个参数可以是Request对象数组,也可以是URL字符串数组
*/

cache.addAll(['/'])

/** 可以通过cache.match来获取缓存
* 它接收一个参数,这个参数可以是Request对象,也可以是URL字符串
*/

cache.match('/').then(function (response) {
console.log(response);
});

/** 可以通过cache.delete来删除缓存
* 它接收一个参数,这个参数可以是Request对象,也可以是URL字符串
*/

cache.delete('/').then(function () {
console.log('删除成功');
});

/** 可以通过cache.keys来获取缓存的key
* 然后通过cache.delete来删除缓存
*/

cache.keys().then(function (keys) {
keys.forEach(function (key) {
cache.delete(key);
});
});
});
缓存资源

我们在介绍生命周期的时候我们介绍了在 installing 状态下会调用 install 方法,通常我们会在 install 事件中缓存一些资源。

self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(currentCacheName).then(function (cache) {
return cache.addAll([
'/',
'/index.css',
'/axios.js',
'/index.html'
]);
})
);
});

上面的代码中我们缓存了一些资源,所以我们可以在 fetch 事件中获取并返回刚刚缓存的资源。

self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});

上面的代码中我们使用 caches.match 来匹配请求,如果匹配到了,那么就返回缓存的资源,如果没有匹配到,那么就从网络中获取资源。

缓存更新

在上面的步骤中,我们已经缓存了我们的资源,并且该资源并不会随着我们代码或者资源的更改而更新缓存。因此,我们可以通过版本号来控制更新。

介绍生命周期时,我们有了解到在 activating 状态下会触发 activate 回调,在该回调中我们可以清除旧缓存,然后在 install 事件中缓存新的资源。

const version = '2.0';
const currentCache = 'my-cache' + version;

self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
if (cacheName !== currentCache) {
return caches.delete(cacheName);
}
})
);
})
);
});
卸载

当我们的页面不再需要 Service Worker 的时候,可以通过在新版本里使用 unregister 进行卸载。

if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}

需要注意的是,Service Worker 卸载并不会删掉我们之前缓存的资源,所以在卸载之前我们需要清除所有的缓存。

缓存策略

从上面的例子可以看出,Service Worker 的缓存是通过 Cache 接口和 fetch 事件共同实现的。通过 Cache 接口和 fetch 事件可以实现多种缓存策略。

1. 仅缓存 (Cache only)

适用于你认为属于该 “版本” 网站静态内容的任何资源,匹配的请求将只会进入缓存。

2. 仅网络 (Network only)

与 “仅缓存” 相反,“仅限网络” 是指请求通过 Service Worker 传递到网络,而无需与 Service Worker 缓存进行任何交互。

3. 缓存优先 (Cache first)

该策略流程如下:

  • 请求到达缓存。如果资源位于缓存中,请从缓存中提供。

  • 如果请求不在缓存中,请转到网络。

  • 网络请求完成后,将其添加到缓存中,然后从网络返回响应。

该策略适用于静态资源的缓存,它可以绕过 HTTP 缓存可能启动的服务器执行任何内容新鲜度检查,从而加快不可变资源的速度。

4. 网络优先 (Network first)

该策略如下:

  • 先前往网络请求一个请求,然后将响应放入缓存中。

  • 如果您日后处于离线状态,则会回退到缓存中该响应的最新版本。

此策略非常适合 HTML 或 API 请求,当您想在线获取资源的最新版本,同时又希望离线可以访问到最新的可用版本。

5. 延迟验证 (Stale-while-revalidate)

该机制与最后两种策略类似,但其过程优先考虑资源访问速度,同时还在后台保持更新。策略大致如下:

  • 在第一次请求获取资源时,从网络中提取资源,将其放入缓存中并返回网络响应。

  • 对于后续请求,首先从缓存提供资源,然后 “在后台” 从网络重新请求该资源,并更新资源的缓存条目。

  • 对于此后的请求,您将收到在上一步中从缓存中放置的最后一个网络提取的版本。

Service Worker 后台同步

假设用户在我们的页面上操作了数据并提交,此时正好进入一个网络极差甚至断网的环境里,用户只能看着一直处于 loading 状态的页面,直到失去耐心关闭页面,这时请求就已经被中断了。

上面这种情况暴露了两个问题:

  • 普通页面会随着页面关闭而终止

  • 网络极差或无网络情况下没用一种解决方案能够解决并维持当前请求以待有网时恢复请求

后台同步是构建在 Service Worker 进程之上的另一个功能,它允许一次性或以一个时间间隔请求后台数据同步。我们可以充分利用这一功能规避以上问题。

工作流程

  • 在 Service Worker 中监听 sync 事件

  • 在浏览器中发起后台同步 sync

  • 就会触发 Service Worker 的 sync 事件,在该监听的回调中进行操作,例如向后端发起请求

  • 然后可以在 Service Worker 中对服务端返回的数据进行处理

1. 页面触发同步

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js')

navigator.serviceWorker.ready.then(function (registration) {
let tag = "data_sync";
document.getElementById('submit-btn').addEventListener('click', function () {
registration.sync.register(tag).then(function () {
console.log('后台同步已触发', tag);
}).catch(function (err) {
console.log('后台同步触发失败', err);
});
});
})
}

由于后台同步功能需要在 Service Worker 注册完成后触发,所以我们可以使用 navigator.serviceWorker.ready 等待注册完成准备好之后使用 registration.sync.register 注册同步事件。

registration.sync 会返回一个 SyncManager 对象其中包含 register 方法和 getTags 方法。

2. SW 监听同步事件

当页面触发同步事件后,我们需要通过 Service Worker 来处理 sync 事件。

self.addEventListener('sync', function (e) {
let init = { method: 'GET' };

switch (e.tag){
case "data_sync":
let request = new Request(`xxxxx/sync`, init);
e.waitUntil(
fetch(request).then(function (response) {
return response;
})
);
break;
}
});

Taro 项目集成

理论说完了,接下来我们可以在 taro 项目里实践接入 Service Worker。

俗话说,站在巨人的肩膀上看世界。

现在市面上实现 SW 的工具非常多,其中 google 团队提供了一个十分强大且完善的插件 workbox-webpack-plugin ,接下来我们将通过这个插件实现 Service Worker 的离线缓存功能。

插件配置

workbox-webpack-plugin 提供了两个类名为 GenerateSW 和 InjectManifest,接下来我们通过使用 GenerateSW 来实现预缓存文件和简单的运行时缓存需求。

在 taro 项目负责打包的 config 文件中加入以下配置:

const { GenerateSW } = require('workbox-webpack-plugin');

const config = {
...
h5: {
...
webpackChain(chain) {
...
chain.plugin('generateSW').use(new GenerateSW({
clientsClaim: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /.\/*/, // 需要缓存的路径
handler: 'StaleWhileRevalidate', // 缓存策略
options: {
cacheName: 'my-webcache',
expiration: {
maxEntries: 2000,
},
},
}],
}));
}
}
}

加入以上配置后,我们运行 build 命令可以发现该插件为我们自动生成了 Service Worker 文件。

Service Worker 注册

生成 Service Worker 文件之后我们需要在项目中进行注册。

在 register 文件中处理 Service Worker 的生命周期、状态等信息。

import { register } from 'register-service-worker';

register('./service-worker.js', {
registrationOptions: { scope: './' },
ready(registration) {
console.log('Service worker is active.', registration);
},
registered(registration) {
console.log('Service worker has been registered.', registration);
},
cached(registration) {
console.log('Content has been cached for offline use.', registration);
},
updatefound(registration) {
console.log('New content is downloading.', registration);
},
updated(registration) {
console.log('New content is available; please refresh.', registration);
},
offline() {
console.log('No internet connection found. App is running in offline mode.');
},
error(error) {
console.error('Error during service worker registration:', error);
},
});

在 app.ts 中引入该文件,我们就完成了简单的 Service Worker 的引入。接下来把项目启动,让我们看看 SW 是否生效。

在正常网络环境中,可以看到我们发起第一次访问的请求列表。

在把网络设置成离线状态后,可以看到我们的请求依然正常返回,并走的是 Service Worker 的缓存。

我们也可以在控制台看到所有缓存的文件列表。

总的来说,Service Worker 是一个非常强大的功能,除了以上介绍的离线缓存和后台同步功能,还可以通过 SW 实现消息推送、多页面通信等等功能。

关于本文
作者:@龚思晗
原文:https://mp.weixin.qq.com/s/nMnNrFSW2L3UfHBfWyxBMQ

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。


继续滑动看下一个

【第3206期】Service Worker:离线应用与后台同步的解决方案

向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存