在本文中,我将通过一个示例向您展示如何在 WebSockets 的帮助下在 JavaScript 中运行 Web Worker。

我认为使用实际用例会很有帮助,因为当你能够将概念与现实生活联系起来时,理解概念会简单得多。

因此,在本指南中,您将了解 JavaScript 中的 Web 工作器是什么,您将获得 WebSockets 的简要介绍,并且您将了解如何以正确的方式管理套接字。

这篇文章非常注重应用/实践,所以我建议您在阅读过程中尝试一下示例,以获得更好的理解。

让我们开始吧。

目录

  • 先决条件
  • JavaScript 中的 Web Worker 是什么?
  • Web Socket 简介
  • 用例描述
  • 项目结构
  • 客户端和服务器架构
  • 工人制度
  • 通过 Web Worker 在 UI 和套接字之间进行通信
  • 概括

先决条件

在开始阅读本文之前,您应该对以下主题有基本的了解:

  • 类图:我们将使用它们来展示我们的示例。以下是一些可用于详细了解它们的资源:
    • Class diagrams
    • UML Diagram course
  • 上下文图和容器图
  • 反应
  • Web 套接字
    • Introduction to sockets
    • How JavaScript works: Deep dive into WebSockets and HTTP/2 with SSE + how to pick the right path
  • 范围和上下文之间的区别
  • 全局对象

JavaScript 中的 Web Worker 是什么?

Web Worker 是浏​​览器的一项功能。它是真正的操作系统线程,可以在当前页面的后台生成,以便执行复杂且资源密集型的任务。

想象一下,您需要从服务器获取一些大数据,或者需要在 UI 上进行一些复杂的渲染。如果您直接在网页上执行此操作,那么页面可能会变得很卡顿,并会影响 UI。

为了缓解这种情况,您可以简单地创建一个线程(即 Web 工作者),然后让 Web 工作者处理复杂的事情。

您可以以一种非常简单的方式与 Web 工作进程进行通信,这种方式可用于在工作进程和 UI 之间来回传输数据。

Web Worker 的常见示例包括:

  • 显示股票价格、实时活跃用户等实时数据的仪表盘页面
  • 从服务器获取大文件
  • 自动保存功能

您可以使用以下语法创建 Web Worker:

const worker = new Worker(".js");

Worker 是一个 API 接口,让你可以在后台创建一个线程。我们需要传递一个参数,即一个 <worker_file>.js 文件。这指定了 API 需要执行的工作文件。

注意 :一旦发起调用,就会创建一个线程 Worker 。此线程仅与其创建者(即创建此线程的文件)通信。

一个 worker 可以被多个消费者/脚本共享或使用。这些被称为共享 worker。共享 worker 的语法与上面提到的 worker 非常相似。

const worker = new SharedWorker(".js");

您可以 SharedWorker 本指南 中阅读有关 s 的 .

Web Worker 的历史

Web Worker 在不同的上下文中执行,也就是说它们不在全局范围内执行,例如窗口上下文。Web Worker 有自己专用的 Worker 上下文,称为 DedicatedWorkerGlobalScope .

不过,有些情况下您无法使用 Web Worker。例如,您无法使用它们来操作 DOM 或窗口对象的属性。这是因为 Worker 无权访问窗口对象。

Web Worker 还可以生成新的 Web Worker。Web Worker 使用某些方法(如 postMessage , onmessage 和 ) onerror 。我们将在本文后面的部分中仔细研究这些方法。

Web Socket 简介

Web 套接字是使用 WebSocket 协议在两个实体之间进行的一种通信。它实际上提供了一种以持久方式在两个连接的实体之间进行通信的方法。

您可以创建一个简单的 Web 套接字,如下所示:

const socket = new WebSocket("ws://example.com");

这里我们创建了一个简单的套接字连接。您会注意到我们已将一个参数传递给构造 WebSocket 函数。此参数是应建立连接的 URL。

您可以通过参考 先决条件中的 Websockets

用例描述

注意: 本博文中绘制的上下文、容器和类图并未准确遵循这些图的确切惯例。这里对其进行了近似处理,以便您能够理解基本概念。

在开始之前,我建议先阅读 c4models、容器图和上下文图。您可以在先决条件部分找到有关它们的资源。

在本文中,我们将考虑以下用例:通过套接字协议使用 Web 工作进程进行数据传输。

我们将构建一个 Web 应用程序,该应用程序将每 1.5 秒在折线图上绘制一次数据。Web 应用程序将通过 Web Worker 从套接字连接接收数据。下面是我们用例的上下文图:

c4_webworker.drawio--2-
Container Diagram

从上图可以看出,我们的用例有 4 个主要组件:

  1. 人员:将要使用我们的应用程序的用户
  2. 软件系统:客户端应用程序 – 这是我们应用程序的 UI。它由 DOM 元素和 Web 工作器组成。
  3. 软件系统:工作系统 – 这是驻留在客户端应用程序中的工作文件。它负责创建工作线程并建立套接字连接。
  4. 软件系统:服务器应用程序 – 这是一个简单的 JavaScript 文件,可以通过执行它来 node 创建套接字服务器。它包含有助于从套接字连接读取消息的代码。

现在我们了解了用例,让我们深入研究每个模块并看看整个应用程序是如何工作的。

项目 结构

请点击此 链接 获取我为本文开发的项目的完整代码。

我们的项目分为两个文件夹。第一个是服务器文件夹,其中包含服务器代码。第二个是客户端文件夹,其中包含客户端 UI(即 React 应用程序)和 Web Worker 代码。

以下是目录结构:

client
    package.json
    package-lock.json
    public
       favicon.ico
       index.html
       logo192.png
       logo512.png
       manifest.json
       robots.txt
    README.md
    src
       App.css
       App.jsx
       components
          LineChartSocket.jsx
          Logger.jsx
       index.css
       index.js
       pages
          Homepage.jsx
       wdyr.js
       workers
           main.worker.js
    yarn.lock
 server
     package.json
     package-lock.json
     server.mjs

要运行该应用程序,首先需要启动套接字服务器。逐个执行以下命令来启动套接字服务器(假设您位于父目录中):

cd server
node server.mjs

然后通过运行以下命令启动客户端应用程序(假设您在父目录中):

cd client
yarn run start

打开 http://localhost:3000 即可启动 Web 应用程序。

客户端和服务器应用程序

客户端应用程序是一个简单的 React 应用程序,即 CRA app ,它由一个主页组成。此主页由以下元素组成:

  • 两个按钮: start connection 和, stop connection 它们将有助于根据需要启动和停止套接字连接。
  • 折线图组件——该组件将绘制我们定期从套接字接收的数据。
  • 记录的消息——这是一个简单的 React 组件,它将显示我们的 Web 套接字的连接状态。

下面是我们的客户端应用程序的容器图。

Untitled
Container Diagram: Client Application

UI 的外观如下:

Screenshot-from-2021-12-28-08-32-06
Actual UI

要查看客户端 UI 的代码,请转到客户端文件夹。这是一个常规的 create-react-app,但我删除了一些我们不需要的样板代码。

App.jsx 实际上是起始代码。如果你仔细查看,就会发现我们 <Homepage /> 在其中调用了组件。

现在让我们看一下该 Homepage 组件。

const Homepage = () => {
  const [worker, setWorker] = useState(null);
  const [res, setRes] = useState([]);
  const [log, setLog] = useState([]);
  const [buttonState, setButtonState] = useState(false);

  const hanldeStartConnection = () => {
    // Send the message to the worker [postMessage]
    worker.postMessage({
      connectionStatus: "init",
    });
  };

  const handleStopConnection = () => {
    worker.postMessage({
      connectionStatus: "stop",
    });
  };
	
	//UseEffect1
  useEffect(() => {
    const myWorker = new Worker(
      new URL("../workers/main.worker.js", import.meta.url)
    ); //NEW SYNTAX
    setWorker(myWorker);

    return () => {
      myWorker.terminate();
    };
  }, []);

	//UseEffect2
  useEffect(() => {
    if (worker) {
      worker.onmessage = function (e) {
        if (typeof e.data === "string") {
          if(e.data.includes("[")){
            setLog((preLogs) => [...preLogs, e.data]);
          } else {
            setRes((prevRes) => [...prevRes, { stockPrice: e.data }]);
          }
        }

        if (typeof e.data === "object") {
          setButtonState(e.data.disableStartButton);
        }
      };
    }
  }, [worker]);

  return (
    <>
      

WebWorker Websocket example

 
); };

如你所见,它只是一个常规的功能组件,可呈现两个按钮——一个折线图和一个自定义组件 Logger .

现在我们知道了主页组件的样子,让我们深入了解 Web 工作线程的实际创建方式。在上面的组件中,您可以看到 useEffect 使用了两个钩子。

第一个用于创建新的工作线程。 Worker 正如我们在本文上一节中看到的那样,它只是使用 new 运算符对构造函数的简单调用。

但这里有一些不同:我们向 worker 构造函数传递了一个 URL 对象,而不是在字符串中传递 worker 文件的路径。

const myWorker = new Worker(new URL("../workers/main.worker.js", import.meta.url));

您可以 在此处 .

如果您尝试像下面这样导入此 Web Worker,那么我们的 create-react-app 将无法正确加载/捆绑它,因此您会收到错误,因为它在捆绑期间未找到 Worker 文件:

const myWorker = new Worker("../workers/main.worker.js");

接下来,我们也不希望我们的应用程序在刷新后运行工作线程,或者不希望在刷新页面时生成多个线程。为了缓解这种情况,我们将在同一个 useEffect 中返回一个回调。我们使用此回调在组件卸载时执行清理。在本例中,我们将终止工作线程。

我们用它 useEffect2 来处理从工作人员收到的消息。

Web Worker 有一个内置属性,名为 onmessage ,可帮助接收工作线程发送的任何消息。这 onmessage 是 Worker 接口的事件处理程序。每当触发消息事件时,它就会被触发。此消息事件通常在执行处理程序时触发 postMessage (我们将在后面的部分中对此进行更深入的介绍)。

因此,为了能够向工作线程发送消息,我们创建了两个处理程序。第一个是 handleStartConnection ,第二个是 handleStopConnection 。它们都使用 postMessage worker 接口的方法将消息发送到工作线程。

在下一部分中 {connectionStatus: init} 讨论该信息

在以下资源中 onmessage postMessage 内部工作原理的更多信息

  • 留言
  • 留言

既然我们现在对客户端代码的工作方式有了基本的了解,那么让我们继续了解 上面上下文图中的工作系统。

工人制度

要理解本节中的代码,请务必阅读文件 src/workers/main.worker.js .

为了帮助您理解这里发生的事情,我们将把这段代码分为三个部分:

  1. 一段 self.onmessage
  2. 函数 socketManagement() 管理套接字连接
  3. 为什么我们需要 socketInstance 在顶部添加变量

工作 self.onmessage 原理

每当您创建 Web Worker 应用程序时,您通常都会编写一个 Worker 文件来处理您希望 Worker 执行的所有复杂场景。这一切都发生在文件中 main.worker.js 。此文件就是我们的 Worker 文件。

在上一节中,我们看到我们在 中建立了一个新的工作线程 useEffect 。创建线程后,我们还将两个处理程序附加到相应的 start stop 连接按钮。

按钮 start connection 将执行 postMessage 带有消息的方法: {connectionStatus: init} 。这会触发消息事件,并且由于触发了消息事件,因此所有消息事件都将被属性捕获 onmessage

在我们的 main.worker.js 文件中,我们已为该 onmessage 属性附加了一个处理程序:

self.onmessage = function (e) {
  const workerData = e.data;
  postMessage("[WORKER] Web worker onmessage established");
  switch (workerData.connectionStatus) {
    case "init":
      socketInstance = createSocketInstance();
      socketManagement();
      break;

    case "stop":
      socketInstance.close();
      break;

    default:
      socketManagement();
  }
}

因此,无论何时在客户端触发任何消息事件,它都会被此事件处理程序捕获。

消息 {connectionStatus: init} 在事件中接收 e 。根据 connectionStatus 的值,我们使用 switch case 来处理逻辑。

注意: 我们添加了这个 switch case,因为我们需要隔离一些我们不想一直执行的代码(我们将在后面的部分中讨论这个问题)。

函数 socketManagement() 管理套接字连接

我将创建和管理套接字连接的逻辑转移到单独的函数中,这是有原因的。以下是代码,以便更好地理解我的观点:

function socketManagement() {
  if (socketInstance) {
    socketInstance.onopen = function (e) {
      console.log("[open] Connection established");
      postMessage("[SOCKET] Connection established");
      socketInstance.send(JSON.stringify({ socketStatus: true }));
      postMessage({ disableStartButton: true });
    };

    socketInstance.onmessage = function (event) {
      console.log(`[message] Data received from server: ${event.data}`);
      postMessage( event.data);
    };

    socketInstance.onclose = function (event) {
      if (event.wasClean) {
        console.log(`[close] Connection closed cleanly, code=${event.code}`);
        postMessage(`[SOCKET] Connection closed cleanly, code=${event.code}`);
      } else {
        // e.g. server process killed or network down
        // event.code is usually 1006 in this case
        console.log('[close] Connection died');
        postMessage('[SOCKET] Connection died');
      }
      postMessage({ disableStartButton: false });
    };

    socketInstance.onerror = function (error) {
      console.log(`[error] ${error.message}`);
      postMessage(`[SOCKET] ${error.message}`);
      socketInstance.close();
    };
  }
}

这是一个可以帮助您管理套接字连接的函数:

  • 为了从套接字服务器接收消息,我们具有 onmessage 分配了事件处理程序的属性。
  • 每当打开套接字连接时,您都可以执行某些操作。为此,我们将属性 onopen 分配给事件处理程序。
  • 如果发生任何错误或者当我们关闭连接时,我们将使用 onerror 套接字的属性 onclose

为了创建套接字连接,有一个单独的函数:

function createSocketInstance() {
  let socket = new WebSocket("ws://localhost:8080");

  return socket;
}

现在,所有这些函数都在文件中如下所示的 switch case 中调用 main.worker.js

self.onmessage = function (e) {
  const workerData = e.data;
  postMessage("[WORKER] Web worker onmessage established");
  switch (workerData.connectionStatus) {
    case "init":
      socketInstance = createSocketInstance();
      socketManagement();
      break;

    case "stop":
      socketInstance.close();
      break;

    default:
      socketManagement();
  }
}

因此,根据客户端 UI 发送给 worker 的消息,将执行相应的函数。根据上述代码,应该触发哪个消息和哪个特定函数是非常不言自明的。

现在考虑一个场景,我们将所有代码放在里面 self.onmessage .

self.onmessage = function(e){
    console.log("Worker object present ", e);
    postMessage({isLoading: true, data: null});

    let socket = new WebSocket("ws://localhost:8080");

		socket.onopen = function(e) {
		  console.log("[open] Connection established");
		  console.log("Sending to server");
		  socket.send("My name is John");
		};
		
		socket.onmessage = function(event) {
		  console.log(`[message] Data received from server: ${event.data}`);
		};
		
		socket.onclose = function(event) {
		  if (event.wasClean) {
		    console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
		  } else {
		    // e.g. server process killed or network down
		    // event.code is usually 1006 in this case
		    console.log('[close] Connection died');
		  }
		};

			socket.onerror = function(error) {
			  console.log(`[error] ${error.message}`);
			};
}

这会导致以下问题:

  1. 每次 postMessage 调用时都会有一个新的套接字实例。
  2. 关闭套接字连接将会很困难。

由于这些原因,所有套接字管理代码都写在一个函数中 socketManagement ,并使用 switch case 来处理。

为什么我们需要 socketInstance 在顶部添加变量

我们确实需要 socketInstance 在顶部添加一个变量,因为它将存储之前创建的套接字实例。这是一种安全的做法,因为没有人可以从外部访问此变量,因为它 main.worker.js 是一个单独的模块。

通过 Web Worker 在 UI 和套接字之间进行通信

现在我们了解了代码的哪一部分负责哪个部分,我们将看看如何通过 webworker 建立套接字连接。我们还将了解如何通过套接字服务器进行响应以在 UI 上显示折线图。

Untitled--1-
End-to-end flow of the application

注意: 有些调用故意没有显示在图中,因为这会使图变得混乱。请确保在参考此图时也参考代码。

现在我们首先了解一下当你点击 start connection UI 上的按钮时会发生什么:

  1. 这里要注意的一点是,我们的 Web 工作线程在组件安装后创建,并在组件卸载时被删除/终止。
  2. 一旦 start connection 按钮被点击, postMessage 就会调用 {connectionStatus: init}
  3. Web Worker onmessage 事件处理程序知道它已收到 connectionStatus 作为 init。 它匹配 case,即的 switch case 中的情况 main.worker.js 。然后它调用 createSocketInstance() 在 URL 处返回新的套接字连接: ws://localhost:8080
  4. 此后, socketManagement() 将调用一个函数检查套接字是否已创建,然后执行几个操作。
  5. 在这个流程中,由于套接字连接刚刚建立,因此 onpen 执行socketInstance的事件处理程序。
  6. 这将向 {socketStatus: true} 套接字服务器发送一条消息。这还将向客户端 UI 发送回一条消息,通过 postMessage({ disableStartButton: true}) 该消息告诉客户端 UI 禁用“开始”按钮。
  7. 每当建立套接字连接时,就会调用服务器套接字 on('connection', ()=>{}) 。因此在步骤 3 中,服务器端会调用此函数。
  8. Socket on('message', () => {}) 。因此,在步骤 6 中,服务器端会调用此函数。这将检查是否为 socketStatus 真,然后它将开始每 1.5 秒通过 Web Worker 向客户端 UI 发送一个随机整数。

现在我们了解了如何建立连接,让我们继续了解套接字服务器如何将数据发送到客户端 UI:

  1. 正如上面所讨论的,套接字服务器每1.5秒收到发送数据的消息,即一个随机数。
  2. 该数据是使用处理程序在 Web 工作进程端接收的 onmessage
  3. 然后,该处理程序调用该 postMessage 函数并将该数据发送到 UI。
  4. 接收数据后,它将数据作为对象附加到数组中 stockPrice
  5. 它充当我们的折线图组件的数据源,每 1.5 秒更新一次。

现在我们了解了如何建立连接,让我们继续了解套接字服务器如何将数据发送到客户端 UI:

  1. 如上所述,套接字服务器每 1.5 秒接收一次发送数据的消息,即一个随机数。
  2. 该数据是使用套接字的 onmessage 处理程序在 Web 工作进程端接收的。
  3. 然后,该处理程序调用 postMessage Web 工作程序的函数并将该数据发送到 UI。
  4. 通过它接收数据后, useEffect2 将其作为对象附加到数组中 stockPrice
  5. 它充当我们的折线图组件的数据源,每 1.5 秒更新一次。

注意: 我们使用 recharts 绘制折线图。你可以在 官方文档 .

我们的应用程序实际运行起来是这样的:

ezgif.com-gif-maker
Working Example

概括

以上就是关于什么是 Web Worker 以及如何使用它们来解决复杂问题和创建更好的 UI 的简要介绍。您可以在项目中使用 Web Worker 来处理复杂的 UI 场景。

如果您想优化您的工作人员,请阅读以下库:

  • 康利康
  • 线程.js

感谢您的阅读!

和 twitter , github 上关注我 linkedIn .

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信小程序

微信扫一扫体验

立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部