中文文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
service worker的作用
在JavaScript
中引用服务工作者的作用是启用该网站的离线支持。服务工作者是一种网络代理,它位于Web应用程序和浏览器网络层之间。服务工作者能够拦截并处理来自Web应用程序的网络请求,并决定如何响应这些请求,包括从缓存中提供数据以提供离线支持。
引用服务工作者的JavaScript
代码通常会检查浏览器是否支持服务工作者,并注册服务工作者,以便在Web
应用程序可用时对其进行安装和激活。一旦服务工作者注册成功,它可以缓存应用程序的资源以提供离线支持,也可以执行其他高级操作,例如推送通知和后台同步。
总的来说,引用服务工作者的JavaScript
代码可以为Web
应用程序提供更加丰富和动态的用户体验,并改善网站的性能和可用性。
在 JavaScript 中实现 Service Worker,一般需要遵循以下步骤:
1.注册 Service Worker
首先要在你的 Web 应用程序中注册 Service Worker。可以在应用程序 javascript 文件中编写以下代码:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(function(registration) {
console.log('Service Worker registered with scope:', registration.scope);
}).catch(function(error) {
console.error('Service Worker registration failed:', error);
});
}
上述代码主要作用是检查浏览器是否支持 Service Worker,并在浏览器支持的情况下注册 Service Worker。
- 定义 Service Worker 生命周期
在 Service Worker 中,有三个关键的生命周期事件:install 、 activate 和 fetch。
install 事件被用于在 Service Worker 安装后预先缓存应用程序的资源。通过监听这个事件来缓存资源,例如 CSS、JS、图片等等。以下是一个例子:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('myCache')
.then(function(cache) {
return cache.addAll([
'/index.html',
'/bundle.js',
'/styles.css'
]);
})
);
});
上述代码主要作用是在 Service Worker 安装的时候预先缓存指定的文件,当 Service Worker 安装完成之后,这些文件就可以被缓存到浏览器 local storage 中了。
activate 事件被用于清除旧的缓存。可以在 activate 事件中比较新老缓存文件的文件名,如果最新的缓存文件名和旧缓存文件名不一致,则删除旧的缓存。以下是一个例子:
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
return cacheName !== 'myCache';
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
});
在 Service Worker 确定哪些资源该被缓存之后,就可以使用 fetch 事件来拦截网络请求,并返回缓存资源。以下是一个例子:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
在上述代码中,当网络请求与缓存的文件匹配时,会从缓存中直接返回该文件。如果请求未被缓存,它会使用 JavaScript 的 fetch 函数来从网络中获取资源。
以上就是 Service Worker 基本实现的流程和代码示例。当 Service Worker
安装完成后,浏览器会自动注册并监听Service Worker
的生命周期事件,根据代码逻辑实现离线访问和页面加载速度优化等功能。
const workerReady = new Promise((resolve, reject) => {
if (!areServiceWorkersEnabled()) {
return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.'));
}
const swPath = `service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`;
navigator.serviceWorker.register(swPath)
.then(() => navigator.serviceWorker.ready)
.then(async registration => {
/**
* @param {MessageEvent} event
*/
const versionHandler = async (event) => {
if (event.data.channel !== 'version') {
return;
}
navigator.serviceWorker.removeEventListener('message', versionHandler);
if (event.data.version === expectedWorkerVersion) {
return resolve();
} else {
console.log(`Found unexpected service worker version. Found: ${event.data.version}. Expected: ${expectedWorkerVersion}`);
console.log(`Attempting to reload service worker`);
// If we have the wrong version, try once (and only once) to unregister and re-register
// Note that `.update` doesn't seem to work desktop electron at the moment so we use
// `unregister` and `register` here.
return registration.unregister()
.then(() => navigator.serviceWorker.register(swPath))
.then(() => navigator.serviceWorker.ready)
.finally(() => { resolve(); });
}
};
navigator.serviceWorker.addEventListener('message', versionHandler);
const postVersionMessage = (/** @type {ServiceWorker} */ controller) => {
controller.postMessage({ channel: 'version' });
};
// At this point, either the service worker is ready and
// became our controller, or we need to wait for it.
// Note that navigator.serviceWorker.controller could be a
// controller from a previously loaded service worker.
const currentController = navigator.serviceWorker.controller;
if (currentController?.scriptURL.endsWith(swPath)) {
// service worker already loaded & ready to receive messages
postVersionMessage(currentController);
} else {
// either there's no controlling service worker, or it's an old one:
// wait for it to change before posting the message
const onControllerChange = () => {
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
postVersionMessage(navigator.serviceWorker.controller);
};
navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
}
}).catch(error => {
reject(new Error(`Could not register service workers: ${error}.`));
});
});
service-worker.js
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
/// <reference no-default-lib="true"/>
/// <reference lib="webworker" />
const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {any} */ (self));
const VERSION = 4;
const resourceCacheName = `vscode-resource-cache-${VERSION}`;
const rootPath = sw.location.pathname.replace(/\/service-worker.js$/, '');
const searchParams = new URL(location.toString()).searchParams;
const remoteAuthority = searchParams.get('remoteAuthority');
/**
* Origin used for resources
*/
const resourceBaseAuthority = searchParams.get('vscode-resource-base-authority');
const resolveTimeout = 30000;
/**
* @template T
* @typedef {{
* resolve: (x: T) => void,
* promise: Promise<T>
* }} RequestStoreEntry
*/
/**
* Caches
* @template T
*/
class RequestStore {
constructor() {
/** @type {Map<number, RequestStoreEntry<T>>} */
this.map = new Map();
this.requestPool = 0;
}
/**
* @param {number} requestId
* @return {Promise<T> | undefined}
*/
get(requestId) {
const entry = this.map.get(requestId);
return entry && entry.promise;
}
/**
* @returns {{ requestId: number, promise: Promise<T> }}
*/
create() {
const requestId = ++this.requestPool;
/** @type {undefined | ((x: T) => void)} */
let resolve;
/** @type {Promise<T>} */
const promise = new Promise(r => resolve = r);
/** @type {RequestStoreEntry<T>} */
const entry = { resolve: /** @type {(x: T) => void} */ (resolve), promise };
this.map.set(requestId, entry);
const dispose = () => {
clearTimeout(timeout);
const existingEntry = this.map.get(requestId);
if (existingEntry === entry) {
return this.map.delete(requestId);
}
};
const timeout = setTimeout(dispose, resolveTimeout);
return { requestId, promise };
}
/**
* @param {number} requestId
* @param {T} result
* @return {boolean}
*/
resolve(requestId, result) {
const entry = this.map.get(requestId);
if (!entry) {
return false;
}
entry.resolve(result);
this.map.delete(requestId);
return true;
}
}
/**
* @typedef {{ readonly status: 200; id: number; path: string; mime: string; data: Uint8Array; etag: string | undefined; mtime: number | undefined; }
* | { readonly status: 304; id: number; path: string; mime: string; mtime: number | undefined }
* | { readonly status: 401; id: number; path: string }
* | { readonly status: 404; id: number; path: string }} ResourceResponse
*/
/**
* Map of requested paths to responses.
*
* @type {RequestStore<ResourceResponse>}
*/
const resourceRequestStore = new RequestStore();
/**
* Map of requested localhost origins to optional redirects.
*
* @type {RequestStore<string | undefined>}
*/
const localhostRequestStore = new RequestStore();
const unauthorized = () =>
new Response('Unauthorized', { status: 401, });
const notFound = () =>
new Response('Not Found', { status: 404, });
const methodNotAllowed = () =>
new Response('Method Not Allowed', { status: 405, });
sw.addEventListener('message', async (event) => {
switch (event.data.channel) {
case 'version':
{
const source = /** @type {Client} */ (event.source);
sw.clients.get(source.id).then(client => {
if (client) {
client.postMessage({
channel: 'version',
version: VERSION
});
}
});
return;
}
case 'did-load-resource':
{
/** @type {ResourceResponse} */
const response = event.data.data;
if (!resourceRequestStore.resolve(response.id, response)) {
console.log('Could not resolve unknown resource', response.path);
}
return;
}
case 'did-load-localhost':
{
const data = event.data.data;
if (!localhostRequestStore.resolve(data.id, data.location)) {
console.log('Could not resolve unknown localhost', data.origin);
}
return;
}
default:
console.log('Unknown message');
return;
}
});
sw.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
if (requestUrl.protocol === 'https:' && requestUrl.hostname.endsWith('.' + resourceBaseAuthority)) {
switch (event.request.method) {
case 'GET':
case 'HEAD': {
const firstHostSegment = requestUrl.hostname.slice(0, requestUrl.hostname.length - (resourceBaseAuthority.length + 1));
const scheme = firstHostSegment.split('+', 1)[0];
const authority = firstHostSegment.slice(scheme.length + 1); // may be empty
return event.respondWith(processResourceRequest(event, {
scheme,
authority,
path: requestUrl.pathname,
query: requestUrl.search.replace(/^\?/, ''),
}));
}
default:
return event.respondWith(methodNotAllowed());
}
}
// If we're making a request against the remote authority, we want to go
// back through VS Code itself so that we are authenticated properly
if (requestUrl.host === remoteAuthority) {
switch (event.request.method) {
case 'GET':
case 'HEAD':
return event.respondWith(processResourceRequest(event, {
path: requestUrl.pathname,
scheme: requestUrl.protocol.slice(0, requestUrl.protocol.length - 1),
authority: requestUrl.host,
query: requestUrl.search.replace(/^\?/, ''),
}));
default:
return event.respondWith(methodNotAllowed());
}
}
// See if it's a localhost request
if (requestUrl.origin !== sw.origin && requestUrl.host.match(/^(localhost|127.0.0.1|0.0.0.0):(\d+)$/)) {
return event.respondWith(processLocalhostRequest(event, requestUrl));
}
});
sw.addEventListener('install', (event) => {
event.waitUntil(sw.skipWaiting()); // Activate worker immediately
});
sw.addEventListener('activate', (event) => {
event.waitUntil(sw.clients.claim()); // Become available to all pages
});
/**
* @param {FetchEvent} event
* @param {{
* scheme: string;
* authority: string;
* path: string;
* query: string;
* }} requestUrlComponents
*/
async function processResourceRequest(event, requestUrlComponents) {
const client = await sw.clients.get(event.clientId);
if (!client) {
console.error('Could not find inner client for request');
return notFound();
}
const webviewId = getWebviewIdForClient(client);
if (!webviewId) {
console.error('Could not resolve webview id');
return notFound();
}
const shouldTryCaching = (event.request.method === 'GET');
/**
* @param {ResourceResponse} entry
* @param {Response | undefined} cachedResponse
*/
const resolveResourceEntry = (entry, cachedResponse) => {
if (entry.status === 304) { // Not modified
if (cachedResponse) {
return cachedResponse.clone();
} else {
throw new Error('No cache found');
}
}
if (entry.status === 401) {
return unauthorized();
}
if (entry.status !== 200) {
return notFound();
}
/** @type {Record<string, string>} */
const headers = {
'Content-Type': entry.mime,
'Content-Length': entry.data.byteLength.toString(),
'Access-Control-Allow-Origin': '*',
};
if (entry.etag) {
headers['ETag'] = entry.etag;
headers['Cache-Control'] = 'no-cache';
}
if (entry.mtime) {
headers['Last-Modified'] = new Date(entry.mtime).toUTCString();
}
const response = new Response(entry.data, {
status: 200,
headers
});
if (shouldTryCaching && entry.etag) {
caches.open(resourceCacheName).then(cache => {
return cache.put(event.request, response);
});
}
return response.clone();
};
const parentClients = await getOuterIframeClient(webviewId);
if (!parentClients.length) {
console.log('Could not find parent client for request');
return notFound();
}
/** @type {Response | undefined} */
let cached;
if (shouldTryCaching) {
const cache = await caches.open(resourceCacheName);
cached = await cache.match(event.request);
}
const { requestId, promise } = resourceRequestStore.create();
for (const parentClient of parentClients) {
parentClient.postMessage({
channel: 'load-resource',
id: requestId,
scheme: requestUrlComponents.scheme,
authority: requestUrlComponents.authority,
path: requestUrlComponents.path,
query: requestUrlComponents.query,
ifNoneMatch: cached?.headers.get('ETag'),
});
}
return promise.then(entry => resolveResourceEntry(entry, cached));
}
/**
* @param {FetchEvent} event
* @param {URL} requestUrl
* @return {Promise<Response>}
*/
async function processLocalhostRequest(event, requestUrl) {
const client = await sw.clients.get(event.clientId);
if (!client) {
// This is expected when requesting resources on other localhost ports
// that are not spawned by vs code
return fetch(event.request);
}
const webviewId = getWebviewIdForClient(client);
if (!webviewId) {
console.error('Could not resolve webview id');
return fetch(event.request);
}
const origin = requestUrl.origin;
/**
* @param {string | undefined} redirectOrigin
* @return {Promise<Response>}
*/
const resolveRedirect = async (redirectOrigin) => {
if (!redirectOrigin) {
return fetch(event.request);
}
const location = event.request.url.replace(new RegExp(`^${requestUrl.origin}(/|$)`), `${redirectOrigin}$1`);
return new Response(null, {
status: 302,
headers: {
Location: location
}
});
};
const parentClients = await getOuterIframeClient(webviewId);
if (!parentClients.length) {
console.log('Could not find parent client for request');
return notFound();
}
const { requestId, promise } = localhostRequestStore.create();
for (const parentClient of parentClients) {
parentClient.postMessage({
channel: 'load-localhost',
origin: origin,
id: requestId,
});
}
return promise.then(resolveRedirect);
}
/**
* @param {Client} client
* @returns {string | null}
*/
function getWebviewIdForClient(client) {
const requesterClientUrl = new URL(client.url);
return requesterClientUrl.searchParams.get('id');
}
/**
* @param {string} webviewId
* @returns {Promise<Client[]>}
*/
async function getOuterIframeClient(webviewId) {
const allClients = await sw.clients.matchAll({ includeUncontrolled: true });
return allClients.filter(client => {
const clientUrl = new URL(client.url);
const hasExpectedPathName = (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html` || clientUrl.pathname === `${rootPath}/index-no-csp.html`);
return hasExpectedPathName && clientUrl.searchParams.get('id') === webviewId;
});
}
https://blog.csdn.net/qq_41581588/article/details/126739689
https://www.cnblogs.com/dojo-lzz/p/8047336.html下面有视频
demo
搭建了一个demo,给大家贴一下代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" type="text/css" href="css/index.css" />
<script src="js/index.js"></script>
<script src="js/indexeddb.js"></script>
</head>
<body>
Cache Demo
</body>
</html>
index.js
注册serviceWorker
(function () {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("./sw.js");
}
})();
sw.js
// 用来定义 service worker 的生命周期
var CACHE_PREFIX = "cms-sw-cache";
var CACHE_VERSION = "0.0.20";
var CACHE_NAME = CACHE_PREFIX + "-" + CACHE_VERSION;
// 写一个资源注册表
var allAssets = ["./js/index.js", "./css/index.css"];
// 注册 service worker
self.addEventListener("install", function (event) {
// 安装阶段跳过等待,直接进入 active
self.skipWaiting();
// 拦截请求
event.waitUntil(
// 打开一个缓存
caches.open(CACHE_NAME).then(function (cache) {
// 缓存资源
return cache.addAll(allAssets);
})
);
});
// 请求代理
self.addEventListener("fetch", function (event) {
// 匹配到了缓存资源
caches.match(event.request).then((response) => {
console.log(response);
// 如果匹配到了
if (response) {
return response;
}
// 1.0
// return fetch(event.request);
// 2.0
return fetch(event.request).then(networkResponse => {
return caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone()); // 更新缓存
return networkResponse;
});
});
});
});
这样就可以看到数据是从serviceWorker缓存里面拿数据