vscode里面的service worker

中文文档: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。

  1. 定义 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下面有视频

navigator.serviceWorker.register 和 new Worker 区别

navigator.serviceWorker.register 和 new Worker 是两种完全不同的 API。

  • navigator.serviceWorker.register: 这是 Service Worker 的注册 API。通过这个 API,我们可以将一段 JavaScript 代码文件作为 Service Worker 注册到浏览器中,并控制网页的缓存、离线支持、推送通知、后台数据同步等功能。
  • new Worker: 这是 Web Worker 的创建 API。通过这个 API,我们可以将一段 JavaScript 代码文件作为 Worker 注册到浏览器中,并在后台线程中运行它。Web Worker 可以与主线程并行运行,所以非常适合处理大量计算密集型操作。
    简而言之,navigator.serviceWorker.register 是用来注册 Service Worker,用于控制网页的缓存、离线支持等功能;而 new Worker 则是创建 Web Worker,用于在后台线程中并行运行一段 JavaScript 代码文件。

如何想了解 new worker的使用,可以看我的另一篇文章
https://blog.csdn.net/woyebuzhidao321/article/details/126618420

扫描二维码关注公众号,回复: 15241543 查看本文章

猜你喜欢

转载自blog.csdn.net/woyebuzhidao321/article/details/130089154