WHAT - 前端 Web Worker 和 Service Worker(含工作者线程概念)

一、介绍

在日常方案调研中,经常会遇到 Web Worker 和 Service Worker,那他们究竟是什么?以及是否有什么关联?今天我们来一探究竟。

Web Worker 和 Service Worker 是两种在 Web 开发中用于提升性能和功能的技术,它们虽然在名称上很相似,但在功能和用途上有所不同。

  1. Web Worker:

用途:Web Worker 主要用于在后台线程中执行耗时的 JavaScript 任务,以避免阻塞主线程,从而提高页面的响应性能。它允许你在单独的线程中执行 JavaScript 代码,而不会影响到页面的主线程。

工作原理:Web Worker 是通过创建一个独立的后台线程来工作的。在主线程中可以创建和管理 Web Worker,并向其发送消息,Worker 线程接收消息并执行相应的任务,然后将结果返回给主线程。这样可以避免在主线程中执行耗时的任务,提高页面的性能和响应速度。

适用场景:适用于需要进行大量计算、数据处理或其他耗时任务的情况,例如数据分析、图像处理、加密解密等。

  1. Service Worker:

用途:Service Worker 是一种特殊的 Web Worker,它主要用于创建离线体验、网络代理和推送通知等功能。Service Worker 可以在用户关闭网页后仍然运行,可以拦截和处理页面发出的网络请求,从而实现离线缓存、推送通知等功能。

工作原理:Service Worker 作为一个独立的 JavaScript 运行环境,可以在用户关闭页面后继续运行。它可以拦截页面发出的网络请求,并根据需要从缓存中返回响应,或者向服务器发出请求并缓存响应。这样可以实现离线缓存、网络代理等功能。

适用场景:适用于需要实现离线缓存、网络代理、推送通知等高级功能的情况,例如离线应用、即时通讯应用、推送通知服务等。

总之,Web Worker 主要用于在后台线程中执行耗时的 JavaScript 任务,以提高页面的响应性能;而 Service Worker 则主要用于创建离线体验、网络代理和推送通知等功能,可以实现离线缓存、网络代理、推送通知等高级功能。两者都是在 Web 开发中用于提升性能和功能的重要技术。

二、工作者线程

1. 工作者线程是什么

JavaScript 单线程就意味着不能像多线程语言那样把工作委托给独立的线程或进程去做。JavaScript 的单线程 可以保证它与不同浏览器 API 兼容。假如 JavaScript 可以多线程执行并发更改,那么像 DOM 这样的 API 就会出现问题。

因此,POSIX 线程Java 的 Thread 类等传统并发结构都不适合 JavaScript。而这也正是工作者线程的价值所在:允许把主线程的工作转嫁给独立的实体,而不会改变现有的单线程模型。

使用工作者线程,浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境。这个子环境不能与依赖单线程交互的 API(如 DOM)互操作,但可以与父环境并行执行代码

2. 工作者线程类型

工作者线程的类型?

  • 专用工作者线程(Web Worker)

专用工作者线程,通常简称为工作者线程、Web Worker 或 Worker,是一种实用的工具,可以让脚本单独创建一个 JavaScript 线程,以执行委托的任务。专用工作者线程,顾名思义,只能被创建它的页面使用。

  • 共享工作者线程

共享工作者线程与专用工作者线程非常相似。主要区别是共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息。

  • 服务工作者线程(Service Worker)

服务工作者线程与专用工作者线程和共享工作者线程截然不同。它的主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色。

3. 工作者线程与线程

  • 工作者线程是以实际线程实现的

例如,Blink 浏览器引擎实现工作者线程的 WorkerThread 就对应着底层的线程。

  • 工作者线程可以并行执行

虽然页面和工作者线程都是单线程 JavaScript 环境,每个环境中的指令则可以并行执行。

  • 工作者线程可以共享某些内存

工作者线程能够使用 SharedArrayBuffer 在多个环境间共享内容。线程使用实现并发控制,但 JavaScript 使用 Atomics 接口(原子操作)实现并发控制。

在传统的多线程编程中,锁(Lock)是常用的并发控制机制之一,用于保护共享资源,防止多个线程同时访问而引发竞态条件(Race Condition)。但是,在 JavaScript 中,由于其单线程的特性,传统的锁机制并不适用。

JavaScript 中的并发控制通常使用 Atomics 接口来实现。Atomics 接口提供了一组原子操作,用于在共享内存上执行原子操作,从而实现并发控制和线程同步。

Atomics 接口提供了多种原子操作,包括 Atomics.add(), Atomics.sub(), Atomics.compareExchange() 等,这些操作可以保证在执行过程中不会被中断,从而避免了竞态条件的发生。

以下是一个简单的示例,演示了如何使用 Atomics 接口来实现简单的并发控制:

const sharedArray = new Int32Array(new SharedArrayBuffer(4));

// 创建一个工作线程
const worker = new Worker('worker.js');

// 在工作线程中使用原子操作
worker.postMessage(sharedArray);

// worker.js
onmessage = function(event) {
    const sharedArray = new Int32Array(event.data);

    // 在共享内存上执行原子操作
    Atomics.add(sharedArray, 0, 1);
    console.log('当前值:', sharedArray[0]);
};

在这个示例中,主线程创建了一个共享内存的数组 sharedArray,并将其传递给工作线程。工作线程使用 Atomics.add() 方法对共享内存进行原子加操作,保证了在执行过程中不会被中断,从而避免了竞态条件的发生。

总之,JavaScript 使用 Atomics 接口来实现并发控制和线程同步,通过原子操作在共享内存上执行操作,保证了操作的原子性,从而避免了竞态条件的发生。

  • 工作者线程不共享全部内存

在传统线程模型中,多线程有能力读写共享内存空间。对于工作者线程来说,除了 SharedArrayBuffer 外,从工作者线程进出的数据需要复制或转移。

  • 工作者线程不一定在同一个进程里

通常,一个进程可以在内部产生多个线程。根据浏览器引擎的实现,工作者线程可能与页面属于同一进程,也可能不属于。例如,Chrome 的 Blink 引擎对共享工作者线程和服务工作者线程使用独立的进程。

  • 创建工作者线程的开销更大

工作者线程有自己独立的事件循环、全局对象、事件处理程序和其他 JavaScript 环境必需的特性。

HTML Web 工作者线程规范是这样说的:工作者线程相对比较重,不建议大量使用。例如,对一张 400 万像素的图片,为每个像素都启动一个工作者线程是不合适的。通常,工作者线程应该是长期运行的,启动成本比较高, 每个实例占用的内存也比较大

总之,无论形式还是功能,工作者线程都不是用于替代线程的。

三、Web Worker

Web Worker 是最常见的工作者线程,它用于在后台执行耗时的 JavaScript 任务,如数据处理、计算、图像处理等。通过 Web Worker,开发者可以将这些任务放在独立的线程中执行,以保持页面的响应性能。

前面演示如何使用 Atomics 接口来实现简单的并发控制就是用的 Web Worker。

四、Service Worker

Service Worker 则是 Web Worker 的一种特殊形式,它具有更多的功能,如离线缓存、网络代理、推送通知等。Service Worker 在用户关闭页面后仍然可以运行,可以拦截和处理页面的网络请求,从而实现离线体验和其他高级功能。

1. 离线缓存

Service Worker 是运行在浏览器背后的独立线程,与浏览器其他内建缓存机制不同,它支持:

  • 自由控制缓存哪些文件
  • 如何匹配缓存
  • 如何读取缓存
  • 并且缓存是持续性的
  • 可以用于实现离线存储

Service Worker 实现缓存功能分为三个步骤:

  1. 首先需要注册 Service Worker
  2. 然后监听 install 事件,缓存所需文件
  3. 最后在下次用户访问时就可以通过拦截请求的方式查询缓存是否存在

注意,当 Service Worker 没有命中缓存时,会根据缓存查找优先级去查找数据,但不管是从 Memory Cache 还是从远程服务器请求获取的数据,浏览器都会显示是从 Service Worker 里获取的。

使用 Service Worker 缓存:

  • 限制:存储策略复杂,存储请求无法对结果进行操作。
  • 优势:对页面加载速度有较大提升,配合 CSR 有明显提升。

下面是一个简单的示例,演示了如何使用 Service Worker 实现离线缓存:

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Offline Cache Example</title>
</head>
<body>
    <h1>Offline Cache Example</h1>
    <p>This page will be cached for offline access.</p>

    <script>
        if ('serviceWorker' in navigator) {
            window.addEventListener('load', () => {
                navigator.serviceWorker.register('/service-worker.js')
                    .then(registration => {
                        console.log('Service Worker registered:', registration);
                    })
                    .catch(error => {
                        console.error('Service Worker registration failed:', error);
                    });
            });
        }
    </script>
</body>
</html>

service-worker.js:

const CACHE_NAME = 'offline-cache-v1';
const CACHE_URLS = [
    '/',
    '/index.html',
    '/styles.css',
    '/script.js',
    '/image.jpg'
];

self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(CACHE_URLS))
            .then(() => {
                console.log('Cache installed');
            })
    );
});

self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                // 如果缓存中存在请求的资源,则直接返回缓存的响应
                if (response) {
                    return response;
                }
                // 否则,通过网络请求获取资源,并将其缓存起来
                return fetch(event.request)
                    .then(response => {
                        // 检查响应是否有效,如果有效则将其添加到缓存中
                        if (!response || response.status !== 200 || response.type !== 'basic') {
                            return response;
                        }
                        const clonedResponse = response.clone();
                        caches.open(CACHE_NAME)
                            .then(cache => {
                                cache.put(event.request, clonedResponse);
                            });
                        return response;
                    });
            })
    );
});

在这个示例中,当用户第一次访问页面时,Service Worker 将会注册并立即安装。在安装过程中,Service Worker 将预先缓存一些静态资源。每当用户访问页面中的资源时,Service Worker 将会拦截这些请求,并检查是否存在缓存。如果存在缓存,则直接返回缓存的响应;否则,通过网络请求获取资源,并将其缓存起来。

注意:这只是一个简单的示例,实际的离线缓存策略可能需要更复杂的逻辑,例如缓存策略的更新缓存版本控制等。

2. 网络代理

Service Worker 作为一个在浏览器背后运行的 JavaScript 线程,可以拦截和处理页面发出的网络请求,从而实现网络代理的功能。

通过网络代理,你可以控制和修改页面的网络请求和响应,从而实现一些高级的网络操作,例如离线缓存、请求拦截、响应修改等。

下面是一个简单的示例,演示了如何使用 Service Worker 实现网络代理:

// service-worker.js

self.addEventListener('fetch', event => {
    // 拦截页面发出的所有网络请求
    event.respondWith(
        // 处理请求并返回响应
        fetch(event.request)
            .then(response => {
                // 可以在这里对响应进行修改
                // 例如:修改响应头、修改响应内容等
                // 这里只是简单地输出响应的状态码
                console.log('Response status:', response.status);
                
                // 返回原始的响应
                return response;
            })
            .catch(error => {
                // 如果请求失败,则可以在这里处理错误
                console.error('Fetch error:', error);
                
                // 返回自定义的错误响应
                return new Response('Network request failed', {
                    status: 500,
                    statusText: 'Internal Server Error'
                });
            })
    );
});

在这个示例中,Service Worker 监听了 fetch 事件,当页面发出网络请求时,Service Worker 将会拦截这些请求并处理。在处理过程中,你可以访问到请求对象 event.request 和响应对象 event.respondWith(),从而可以对请求和响应进行自定义的操作。

在实际的应用中,你可以根据需求来实现各种复杂的网络代理逻辑,例如实现离线缓存请求转发响应修改等功能。需要注意的是,Service Worker 的网络代理功能只能在 HTTPS 环境下使用,以确保安全性和隐私保护。

3. 推送通知

是的,Service Worker 可以用于实现推送通知功能。

推送通知允许你向用户发送消息,即使用户没有打开你的网站,也能接收到通知。这对于实时更新、提醒和消息推送等场景非常有用。

以下是一个简单的示例,演示了如何使用 Service Worker 实现推送通知:

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Push Notification Example</title>
</head>
<body>
    <h1>Push Notification Example</h1>
    <button id="subscribeButton">Subscribe to Notifications</button>
    
    <script>
        if ('serviceWorker' in navigator && 'PushManager' in window) {
            window.addEventListener('load', () => {
                navigator.serviceWorker.register('/service-worker.js')
                    .then(registration => {
                        console.log('Service Worker registered:', registration);
                    })
                    .catch(error => {
                        console.error('Service Worker registration failed:', error);
                    });
            });

            // 订阅按钮点击事件
            document.getElementById('subscribeButton').addEventListener('click', () => {
                subscribeToNotifications();
            });
        }

        async function subscribeToNotifications() {
            try {
                const registration = await navigator.serviceWorker.ready;
                const subscription = await registration.pushManager.subscribe({
                    userVisibleOnly: true,
                    applicationServerKey: 'YOUR_PUBLIC_KEY'
                });
                console.log('Push subscription:', JSON.stringify(subscription));
            } catch (error) {
                console.error('Failed to subscribe to notifications:', error);
            }
        }
    </script>
</body>
</html>

service-worker.js:

self.addEventListener('push', event => {
    // 解析推送消息
    const pushData = event.data.json();
    
    // 显示通知
    self.registration.showNotification(pushData.title, {
        body: pushData.body,
        icon: 'icon.png',
        vibrate: [200, 100, 200],
        data: {
            url: pushData.url
        }
    });
});

self.addEventListener('notificationclick', event => {
    event.notification.close();
    
    // 打开链接
    event.waitUntil(
        clients.openWindow(event.notification.data.url)
    );
});

在这个示例中,当用户点击页面上的订阅按钮时,页面会请求订阅推送通知。一旦订阅成功,Service Worker 就可以接收到推送事件,并在收到推送时显示通知。当用户点击通知时,页面会跳转到指定的链接。

需要注意的是,为了使用推送通知功能,你需要在 Service Worker 中监听 push 事件,并在其中显示通知。此外,你还需要处理 notificationclick 事件,以便在用户点击通知时执行相应的操作。

4. PWA 和 Service Worker

PWA(Progressive Web App)是一种使用现代Web技术构建的Web应用程序,具有类似于原生移动应用的功能和体验。

PWA具有以下特点:

  1. 可靠性(Reliability): PWA能够在不稳定的网络环境下正常工作,并且具有快速加载的特性,即使在没有网络连接的情况下也可以提供基本的功能。
  2. 快速(Fast): PWA具有快速的加载速度和响应速度,可以快速呈现内容,给用户带来良好的体验。
  3. 优雅(Engaging): PWA能够提供类似于原生应用的用户体验,包括平滑的动画、沉浸式的用户界面等,使用户更加愿意与应用进行交互。
  4. 安全(Secure): PWA通过HTTPS协议进行通信,确保用户数据的安全性,同时具备原生应用所具有的权限控制机制。
  5. 可发现性(Discoverable): PWA可以被搜索引擎索引,用户可以通过搜索引擎找到并访问PWA应用。
  6. 可安装性(Installable): 用户可以将PWA添加到设备的主屏幕,就像安装原生应用一样,以便快速访问。

总的来说,PWA结合了Web应用的灵活性和原生应用的性能优势,是一种更加现代化、功能强大的应用开发方式。

而 Service Worker 是实现 Progressive Web App (PWA) 的关键技术之一。

通过使用 Service Worker,开发者可以让网页在用户离线时仍然可访问,并且可以提供更快的加载速度和更好的性能。Service Worker 还可以让开发者实现诸如消息推送、后台同步等功能,这些功能都是 PWA 所具备的。因此,Service Worker 是实现 PWA 的重要组成部分,它使得 Web 应用可以具备类似原生应用的体验和功能。

五、长期运行的 Worker

通常,工作者线程应该是长期运行的,启动成本比较高, 每个实例占用的内存也比较大。

是的,通常情况下,工作者线程应该是长期运行的。

由于工作者线程通常用于执行一些耗时的任务,因此它们应该是长期运行的,以便有效地利用它们的并行执行能力。如果工作者线程频繁地启动和销毁,会造成额外的开销和性能损失,并且可能会导致页面的性能下降。

因此,在设计和使用工作者线程时,应该考虑到它们的长期运行特性,并尽量避免频繁地启动和销毁工作者线程。通常情况下,可以通过创建一个长期运行的工作者线程,并在其中处理多个任务,以提高性能和效率。

最后,工作者线程的销毁并不是由开发者直接控制的,而是由浏览器自动管理的。因此,开发者应该根据自己的需求和场景来合理设计和使用工作者线程,以确保它们的生命周期符合预期。

因此,工作者线程在以下情况下可能会被销毁

  1. 任务完成:当工作者线程完成了它的任务,并且没有其他任务需要执行时,它可能会被销毁。这通常发生在工作者线程执行一些一次性任务或者周期性任务后。

  2. 关闭页面:如果用户关闭了包含工作者线程的页面,那么与该页面相关的工作者线程可能会被销毁。

  3. 浏览器资源管理:浏览器可能会根据系统资源的使用情况和策略来管理工作者线程的生命周期。例如,当系统资源紧张时,浏览器可能会选择关闭一些空闲的工作者线程以释放资源。

  4. 手动关闭:在某些情况下,开发者可能会通过编程的方式显式关闭工作者线程。这通常是为了优化资源使用或者在不需要时释放资源。你可以通过一些技巧来让工作者线程在特定条件下自行关闭。以下是一个示例代码,演示了如何在工作者线程内部监听消息,并在收到特定消息时关闭工作者线程:

// worker.js
// 监听消息
self.addEventListener('message', event => {
    // 判断收到的消息类型是否为关闭消息
    if (event.data === 'close') {
        // 关闭工作者线程
        self.close();
    }
});

在这个示例中,工作者线程监听了 message 事件,并在收到消息后判断消息内容是否为 'close'。如果是,则调用 self.close() 方法关闭工作者线程。

在主线程中,你可以通过向工作者线程发送 'close' 消息来触发关闭操作:

// 主线程
const worker = new Worker('worker.js');
// 发送关闭消息
worker.postMessage('close');

这样,在工作者线程内部处理消息的逻辑中,会收到 'close' 消息,并触发关闭操作。

  • 18
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值