paint-brush
如何使用 Web Workers 加速你的 Angular 应用经过@mesciusinc
173 讀數

如何使用 Web Workers 加速你的 Angular 应用

经过 MESCIUS inc.16m2024/06/26
Read on Terminal Reader

太長; 讀書

了解如何将长时间运行的进程添加到 Angular Web Worker 并享受不会冻结的应用程序的所有好处。
featured image - 如何使用 Web Workers 加速你的 Angular 应用
MESCIUS inc. HackerNoon profile picture

为什么需要Web Worker ?Web Worker 是 Web 应用程序的代码组件。它允许开发人员为 JavaScript 任务创建新的执行线程,这样就不会中断主应用程序的执行。


乍一看,浏览器似乎天生就支持线程,开发人员不必做任何特殊的事情。不幸的是,事实并非如此。Web Workers 解决了真正的并发问题。


Web Workers 是 Web 浏览器预期功能标准的一部分,其规范已在W3C中制定。Angular 框架已为我们封装了Web Workers,我们可以使用 Angular 命令行界面 (CLI) 轻松地将它们添加到我们的应用中。


在本文中,我们将首先检查一些关于浏览器中使用 JavaScript 进行线程并发的误解。然后,我们将创建一个功能示例,演示使用 Angular 实现 Web Workers 有多么容易,这可以在网站上实现并发线程。

JavaScript 不是天生就具有并发性吗?

一些开发人员认为 JavaScript 在浏览器中天生就具有并发性,因为当浏览器连接到网站并检索页面的 HTML 时,它可以同时打开多个连接(大约六个)并提取资源(图像、链接的 CSS 文件、链接的 JavaScript 文件等)。看起来浏览器同时执行了多个线程和许多任务(通过上下文切换)。


对于初学 Web 的开发者来说,这似乎表明浏览器可以并发工作。然而,对于 JavaScript 来说,浏览器实际上每次只能执行一个进程。


大多数现代网站、单页应用(SPA) 和更现代的渐进式 Web 应用(PWA) 都依赖于 JavaScript,并且通常包含大量 JavaScript 模块。但是,只要 Web 应用正在运行 JavaScript,浏览器就只能运行一个线程。正常情况下,JavaScript 都不会同时运行。


这意味着,如果我们在某个 JavaScript 模块中定义了一个长时间运行或进程密集型的任务,用户可能会遇到应用卡顿或挂起的情况。同时,浏览器会等待该进程完成后才能更新用户界面 (UI)。这种行为会让用户对我们的 Web 应用或 SPA 失去信心,而我们谁也不想看到这种情况。

用于 JavaScript 并发的 Web Worker

我们将创建一个包含两个窗格的示例页面,以使我们的 Web 应用能够运行并发 JavaScript 线程。在一个窗格中,我们的应用的 UI 由一个带圆圈的网格表示,该网格不断更新图片并对鼠标点击做出反应。第二个窗格将承载一个长时间运行的进程,该进程通常会阻止 UI 线程并阻止 UI 执行其工作。


并发 JavaScript 线程


为了使我们的 UI 具有响应能力,我们将在 Web Worker 中运行我们的长进程,它将在单独的线程上执行它,并且不会以这种方式阻止 UI 逻辑执行。我们将使用 Angular 作为构建应用程序的框架,因为它使构建 Web Workers 成为一个简单的单命令任务。

设置 Angular 应用

要使用 Angular CLI,我们需要安装Node.jsNPM (Node 包管理器)。确保安装了 Node 和 NPM 后,打开控制台窗口,然后通过 NPM 安装Angular CLI (这是一次性操作):


 npm install -g @angular/cli


将目录更改为我们要创建新应用程序模板的目标目录。现在,我们准备创建应用程序。我们使用“ng new”命令来执行此操作。我们将项目命名为 NgWebWorker:


 ng new NgWebWorker --no-standalone


项目向导询问我们是否要在项目中包含路由。本例中我们不需要路由,因此输入 n。


然后它会询问我们要使用哪种类型的样式表格式。Angular 支持使用样式表处理器,例如 Sass 和 Less,但在本例中,我们将使用简单的 CSS,因此只需按 Enter 即可使用默认值。


样式表格式


然后,当 NPM 提取所需的包并且 CLI 创建模板项目时,我们将看到一些 CREATE 消息。最后,当它完成时,我们会在命令行中再次看到一个闪烁的光标。


此时,Angular CLI 已创建了一个项目并将其放在 NgWebWorker 文件夹中。将目录更改为 NgWebWorker。Angular CLI 创建了我们在 Angular 项目上工作所需的一切,包括安装 Node HTTP 服务器。这意味着我们要做的就是启动模板应用程序:


 ng serve 


Angular CLI


Angular CLI 会编译您的项目并启动 Node HTTP 服务器。现在,您可以通过将应用程序指向<a href="http://localhost:4200"target="_blank"> URL 来在浏览器中加载该应用程序。

我们的第一个来自 CLI 的 Angular 应用

当我们加载页面时,我们会看到顶部带有项目名称的基本模板。


运行“ng serve”的好处是,对代码所做的任何更改都会自动导致网站在浏览器中刷新,从而更容易看到更改生效。


我们将重点关注的大部分代码位于 /src/app 目录下。


/src/app 目录


app.component.html包含当前用于显示主页的一个组件的 HTML。组件代码在app.component.ts (TypeScript) 文件中表示。


我们将删除 app.component.html 的内容并添加我们自己的布局。我们将创建一个分割页面,在左侧显示我们长时间运行的流程值,并在右侧绘制一些随机圆圈。这将允许我们的浏览器运行两个额外的 Web Worker 线程,它们将独立工作,以便您可以看到 Angular Web Workers 的运行情况。


本文其余部分的所有代码都可以从GitHub 存储库中获取。


这是绘制随机圆圈时(在长时间运行的过程开始之前)最终页面的样子。


使用 Web Workers 加速你的 Angular 应用


app.component.html中的代码替换为以下内容:


 <div id="first"> <div class="innerContainer"> <button (click)="longLoop()">Start Long Process</button> </div> <div class="innerContainer"> <textarea rows="20" [value]="(longProcessOutput)"></textarea> </div> </div>


代码下载还包括styles.css中的一些ID和CSS类以及相关样式,它们用于UI的非常简单的格式化,因此我们有两个部分(左和右)和其他基本样式。


 .container { width: 100%; margin: auto; padding: 1% 2% 0 1%; } .innerContainer{ padding: 1%; } #first { width: 50%; height: 405px; float:left; background-color: lightblue; color: white; } #second { width: 50%; float: right; background-color: green; color: white; }


这里需要注意的重要一点是,我们为按钮添加了 Angular 事件绑定(单击)。当用户单击按钮时,将通过调用组件 TypeScript 文件app.component.ts中的 longLoop 方法启动长流程。


 title = 'NgWebWorker'; longProcessOutput: string = 'Long\nprocess\noutput\nwill\nappear\nhere\n'; fibCalcStartVal: number; longLoop() { this.longProcessOutput = ''; for (var x = 1; x <= 1000000000; x++) { var y = x / 3.2; if (x % 20000000 == 0) { this.longProcessOutput += x + '\n'; console.log(x); } } }


这将运行 100 亿次迭代,将内容写入我们组件的成员变量 longProcessOutput。


由于我们已经在app.component.html中(在 textarea 元素上)绑定了该成员变量,因此每次更新变量时,UI 都会反映更新。我们在 HTML 中设置的值就是我们绑定成员变量的位置。


 <textarea rows="20" [value]="longProcessOutput"></textarea>


运行它。我们会看到,当我们点击按钮时,什么也没有发生,然后突然,文本区域被更新为一堆值。如果我们打开控制台,我们会看到代码运行时写入的值。

使用 Angular CLI 添加随机圆形组件

接下来,我们将添加一个“圆圈”组件来绘制随机圆圈。我们可以使用 Angular CLI 执行以下命令:


 ng generate component circle


该命令创建了一个名为circle的新文件夹,并创建了四个新文件:

  • 圆圈.component.html

  • circle.component.spec.ts (单元测试)

  • circle.component.ts (TypeScript 代码)

  • circle.component.css (仅适用于此组件的相关 HTML 的样式)


生成元件圆


HTML 很简单。我们只需要代表组件的 HTML。在我们的例子中,这是页面右侧的组件,它将显示浅绿色网格并绘制圆圈。此绘制是通过 HTML Canvas 元素完成的。


 <div id="second"> <canvas #mainCanvas (mousedown)="toggleTimer()"></canvas> </div>


我们通过添加 Angular 事件绑定来获取 mousedown 事件,从而开始和停止绘制圆圈。如果用户在 Canvas 区域内的任何位置单击,则圆圈将开始绘制(如果该过程尚未启动)。如果该过程已启动,则 toggleTimer 方法(位于circle.component.ts中)将清除网格并停止绘制圆圈。


toggleTimer 只是使用 setInterval 每 100 毫秒(每秒 10 个圆圈)在随机位置以随机选择的颜色绘制一个圆圈。


 toggleTimer(){ if (CircleComponent.IntervalHandle === null){ CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles,50); } else{ clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } }


circle.component.ts中还有更多代码用于设置 Canvas 元素、初始化成员变量以及进行绘制。添加后,您的代码应如下所示:


 import { ViewChild, Component, ElementRef, AfterViewInit } from '@angular/core'; @Component({ selector: 'app-circle', templateUrl: './circle.component.html', styleUrl: './circle.component.css', }) export class CircleComponent implements AfterViewInit { title = 'NgWebWorker'; static IntervalHandle = null; static ctx: CanvasRenderingContext2D; GRID_LINES: number = 20; lineInterval: number = 0; gridColor: string = 'lightgreen'; static CANVAS_SIZE: number = 400; @ViewChild('mainCanvas', { static: false }) mainCanvas: ElementRef; constructor() { console.log('ctor complete'); } ngAfterViewInit(): void { CircleComponent.ctx = (<HTMLCanvasElement>( this.mainCanvas.nativeElement )).getContext('2d'); this.initApp(); this.initBoard(); this.drawGrid(); this.toggleTimer(); } initApp() { CircleComponent.ctx.canvas.height = CircleComponent.CANVAS_SIZE; CircleComponent.ctx.canvas.width = CircleComponent.ctx.canvas.height; } initBoard() { console.log('initBoard...'); this.lineInterval = Math.floor( CircleComponent.ctx.canvas.width / this.GRID_LINES ); console.log(this.lineInterval); } drawGrid() { console.log('drawGrid...'); CircleComponent.ctx.globalAlpha = 1; // fill the canvas background with white CircleComponent.ctx.fillStyle = 'white'; CircleComponent.ctx.fillRect( 0, 0, CircleComponent.ctx.canvas.height, CircleComponent.ctx.canvas.width ); for (var lineCount = 0; lineCount < this.GRID_LINES; lineCount++) { CircleComponent.ctx.fillStyle = this.gridColor; CircleComponent.ctx.fillRect( 0, this.lineInterval * (lineCount + 1), CircleComponent.ctx.canvas.width, 2 ); CircleComponent.ctx.fillRect( this.lineInterval * (lineCount + 1), 0, 2, CircleComponent.ctx.canvas.width ); } } toggleTimer() { if (CircleComponent.IntervalHandle === null) { CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles, 100); } else { clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } } static generateRandomPoints() { var X = Math.floor(Math.random() * CircleComponent.CANVAS_SIZE); // gen number 0 to 649 var Y = Math.floor(Math.random() * CircleComponent.CANVAS_SIZE); // gen number 0 to 649 return { x: X, y: Y }; } drawRandomCircles() { var p = CircleComponent.generateRandomPoints(); CircleComponent.drawPoint(p); } static drawPoint(currentPoint) { var RADIUS: number = 10; var r: number = Math.floor(Math.random() * 256); var g: number = Math.floor(Math.random() * 256); var b: number = Math.floor(Math.random() * 256); var rgbComposite: string = 'rgb(' + r + ',' + g + ',' + b + ')'; CircleComponent.ctx.strokeStyle = rgbComposite; CircleComponent.ctx.fillStyle = rgbComposite; CircleComponent.ctx.beginPath(); CircleComponent.ctx.arc( currentPoint.x, currentPoint.y, RADIUS, 0, 2 * Math.PI ); // allPoints.push(currentPoint); CircleComponent.ctx.stroke(); CircleComponent.ctx.fill(); } }


不要忘记将圆形组件添加到index.html文件中:


 <body> <app-root></app-root> <app-circle></app-circle> </body>


当页面加载时,圆圈将开始绘制。当我们点击[开始长进程]按钮时,我们将看到绘制暂停。这是因为所有工作都在同一个线程上完成。


让我们通过添加 Web Worker 来解决该问题。

添加 Angular Web Worker

要使用 CLI 添加新的 Web Worker,我们只需转到项目文件夹,然后执行以下命令:


 ng generate web-worker app


最后一个参数(app)是包含我们长时间运行的进程的组件的名称,我们希望将其放入我们的 Web Worker 中。


生成 Web Worker 应用程序


Angular 将向app.component.ts添加一些代码,如下所示:


 if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. }


新的代码有什么作用?我们可以看到,此代码引用了该命令还添加的新 app.worker 组件。此时,代码:


  1. 确保浏览器支持 Web Workers。
  2. 创建一个新的工人。
  3. 向工作人员发送一条消息(位于app.worker.ts中)。
  4. 当工作者收到“hello”消息时,EventListener将会触发(如下面的代码片段所示)。
  5. 当 EventListener 触发时(在app.worker.ts中),它将创建一个响应对象并将其发回给调用者。


以下是app.worker.ts的全部内容:


 /// <reference lib="webworker" /> addEventListener('message', ({ data }) => { const response = `worker response to ${data}`; postMessage(response); });


我们将在控制台中看到这些步骤的结果消息,它们看起来像以下控制台输出的最后一行:


控制台输出


这是在原始 Worker 对象上创建的 EventHandler 中出现的 console.log:


 worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); };


这告诉我们 app.component 向 app.worker 发送了一条消息,并且 app.worker 用它自己的消息回复了。


我们希望使用 Worker 在另一个线程上运行我们的长时间运行的进程,这样我们的圆形绘制代码就不会被中断。


首先,让我们将与 UI 元素相关的代码移动到 app.component 类的构造函数中。


 constructor() { if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); this.longProcessOutput += `page got message: ${data}` + '\n'; }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. } }


现在,我们可以引用 longProcessOutput 变量。这样,我们就可以访问该变量;我们有 worker.onmessage,它将基本数据添加到文本区域,而不是写入控制台(仅作为初始测试)。


您可以看到左侧突出显示的文本是收到的消息。


收到消息

Web Worker 中的 LongLoop

我们仍然需要将长时间运行的循环移至 Web Worker,以确保循环运行时它将在自己的线程上运行。


以下是我们最终的app.component.ts中的大部分代码:


 constructor() { if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); this.longProcessOutput += `page got message: ${data}` + '\n'; }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. } } longLoop() { this.longProcessOutput = ''; for (var x = 1; x <= 1000000000; x++) { var y = x / 3.2; if (x % 20000000 == 0) { this.longProcessOutput += x + '\n'; console.log(x); } } }


我们将 worker 变量移到了类中,现在它已成为成员变量。这样,我们就可以在 AppComponent 类的任何地方轻松引用它。


接下来,让我们更仔细地看一下如何使用构造函数中的代码在工作对象上定义消息事件处理程序:


 this.worker.onmessage = ({ data }) => { this.longProcessOutput += `${data}` + "\n"; };


该代码将在 Web Worker 类(位于app.worker.ts中)调用 postMessage(data) 时运行。每次调用 postMessage 方法时,longProcessOutput(绑定到 textarea 的模型)将使用数据加回车符(“\n”)进行更新,这样每个值都将写在 textarea 元素中的一行上。


以下是实际的 Web Worker( app.worker.ts )中的所有代码:


 addEventListener('message', ({ data }) => { console.log(`in worker EventListener : ${data}`); for (var x = 1; x <=1000000000;x++){ var y = x/3.2; if ((x % 20000000) == 0){ // posts the value back to our worker.onmessage handler postMessage(x); // don't need console any more --> console.log(x); } } });


当用户单击 [运行长进程] 按钮时,将触发此事件处理程序。Web Worker ( app.worker.ts ) 中的所有代码都在新线程上运行。这就是 Web Worker 的价值所在;它的代码在单独的线程上运行。这就是为什么它不再影响 Web 应用程序的主线程。


当用户单击按钮时,就会触发 Web Worker 代码,因为我们现在在 longLoop 方法中有了以下代码。


 longLoop(){ this.longProcessOutput = ""; // the following line starts the long process on the Web Worker // by sending a message to the Web Worker this.worker.postMessage("start looping..."); }


当消息被发布到工作者时,EventListener 将触发并运行我们放置在那里的原始 longLoop 中的代码。

使用 Web Worker 封装 Angular 应用

运行应用程序时,您会发现单击 [Start Long Process] 按钮不再会导致绘制圆形暂停。您还可以直接与绘制圆形的 Canvas 组件交互,这样如果在 longLoop 仍在运行时单击它,Canvas 将立即重新绘制。以前,如果您这样做,应用程序的行为就像被冻结了一样。


现在,您可以将长时间运行的进程添加到 Angular Web Worker,并享受不会冻结的应用程序的所有好处。


如果你想看一个用 JavaScript Web Workers 解决的示例,你可以在Plunker上看到它。


您是否在寻找与框架无关的 UI 组件?MESCIUS 拥有一整套 JavaScript UI 组件,包括数据网格、图表、仪表和输入控件。我们还提供强大的电子表格组件报告控件增强的演示视图


我们对Angular (以及 React 和 Vue)提供深度支持,并致力于扩展我们的组件以用于现代 JavaScript 框架。