paint-brush
Как ускорить ваше приложение Angular с помощью веб-воркеровк@mesciusinc
178 чтения

Как ускорить ваше приложение Angular с помощью веб-воркеров

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

Слишком долго; Читать

Узнайте, как добавить свои долговыполняющиеся процессы в Angular Web Worker и воспользоваться всеми преимуществами приложения, которое не зависает.
featured image - Как ускорить ваше приложение Angular с помощью веб-воркеров
MESCIUS inc. HackerNoon profile picture

Зачем вам нужен веб-воркер ? Веб-воркер — это компонент кода веб-приложения. Это позволяет разработчику создать новый поток выполнения для задачи JavaScript, чтобы он не прерывал выполнение основного приложения.


На первый взгляд может показаться, что браузеры изначально поддерживают многопоточность и что разработчику не нужно делать ничего особенного. К сожалению, это не так. Веб-воркеры решают реальную проблему параллелизма.


Веб-воркеры являются частью ожидаемых функциональных стандартов веб-браузеров, и спецификации для них были написаны в W3C . Фреймворк Angular подготовил для нас веб-воркеры, и мы можем легко добавить их в наше приложение с помощью интерфейса командной строки Angular (CLI).


В этой статье мы сначала рассмотрим некоторые заблуждения относительно параллельного выполнения потоков с помощью JavaScript в браузере. Затем мы создадим функциональный пример, демонстрирующий, насколько легко реализовать веб-работников с помощью Angular, который обеспечивает параллельную обработку потоков на веб-сайте.

Разве JavaScript не является параллельным по своей сути?

Некоторые разработчики полагают, что JavaScript по своей сути является параллельным в браузере, поскольку, когда браузер подключается к веб-сайту и получает HTML-код страницы, он может открывать несколько соединений (около шести) и извлекать ресурсы (изображения, связанные файлы CSS, связанные файлы JavaScript и т. д.). и так далее) одновременно. Похоже, что браузер одновременно выполняет несколько потоков и множество задач (через переключение контекста).


Для непосвященного веб-разработчика это означает, что браузер может выполнять параллельную работу. Однако когда дело доходит до JavaScript, браузер на самом деле выполняет только один процесс за раз.


Большинство современных веб-сайтов, одностраничные приложения (SPA) и более современные прогрессивные веб-приложения (PWA) зависят от JavaScript и обычно содержат множество модулей JavaScript. Однако в любой момент, когда веб-приложение выполняет JavaScript, браузер ограничен одним потоком активности. Ни один из JavaScript-кодов не будет работать одновременно при нормальных обстоятельствах.


Это означает, что если у нас есть длительная или трудоемкая задача, определенная в одном из наших модулей JavaScript, пользователь может столкнуться с заиканием или зависанием приложения. В то же время браузер ожидает завершения процесса, прежде чем можно будет обновить пользовательский интерфейс (UI). Такое поведение приводит к тому, что пользователи теряют доверие к нашим веб-приложениям или SPA, а никто из нас этого не хочет.

Веб-воркер для параллелизма JavaScript

Мы создадим пример страницы с двумя панелями, чтобы наше веб-приложение могло запускать параллельные потоки JavaScript. На одной панели пользовательский интерфейс нашего приложения представлен сеткой с кружками, которая постоянно обновляет картинку и реагирует на щелчки мыши. На второй панели будет размещен длительный процесс, который обычно блокирует поток пользовательского интерфейса и не позволяет пользовательскому интерфейсу выполнять свою работу.


Параллельные потоки JavaScript


Чтобы сделать наш пользовательский интерфейс отзывчивым, мы запустим наш длинный процесс в веб-воркере, который выполнит его в отдельном потоке и не будет таким образом блокировать выполнение логики пользовательского интерфейса. Мы будем использовать Angular в качестве основы для создания приложения, поскольку он делает создание веб-воркеров простой задачей, выполняемой одной командой.

Настройка приложения Angular

Чтобы использовать Angular CLI, нам необходимо установить Node.js и NPM (менеджер пакетов Node). Убедившись, что Node и NPM установлены, откройте окно консоли, затем установите Angular CLI через NPM (это делается единоразово):


 npm install -g @angular/cli


Измените каталог на целевой каталог, в котором мы хотим создать новый шаблон приложения. Теперь мы готовы создать наше приложение. Для этого мы используем команду «ng new». Назовем наш проект NgWebWorker:


 ng new NgWebWorker --no-standalone


Мастер проекта спросит, хотим ли мы включить маршрутизацию в наш проект. В этом примере нам не нужна маршрутизация, поэтому введите n.


Затем он спросит, какой тип формата таблицы стилей мы хотим использовать. Angular поддерживает использование процессоров таблиц стилей, таких как Sass и Less, но в данном случае мы будем использовать простой CSS, поэтому просто нажмите Enter, чтобы получить значение по умолчанию.


Формат таблицы стилей


Затем мы увидим несколько сообщений CREATE, когда NPM извлекает необходимые пакеты, а CLI создает проект шаблона. Наконец, когда все будет завершено, мы снова увидим мигающий курсор в командной строке.


На этом этапе Angular CLI создал проект и поместил его в папку NgWebWorker. Измените каталог на NgWebWorker. Angular CLI создал все необходимое для работы над нашим проектом Angular, включая установку HTTP-сервера Node. Это означает, что все, что нам нужно сделать, чтобы запустить приложение-шаблон, это следующее:


 ng serve 


Угловой интерфейс командной строки


Angular CLI компилирует ваш проект и запускает HTTP-сервер Node. Теперь вы можете загрузить приложение в браузере, указав ему URL-адрес <a href="http://localhost:4200"target="_blank"> .

Наше первое Angular-приложение из CLI

Когда мы загружаем страницу, мы видим базовый шаблон с названием проекта вверху.


Преимущество запуска «ngserve» заключается в том, что любые изменения, внесенные в код, автоматически заставят сайт обновиться в браузере, что значительно облегчит просмотр изменений, вступивших в силу.


Большая часть кода, на котором мы сосредоточимся, находится в каталоге /src/app.


каталог /src/приложение


app.comComponent.html содержит HTML для одного компонента, который в настоящее время используется для отображения главной страницы. Код компонента представлен в файле app.comComponent.ts (TypeScript).


Мы удалим содержимое app.comComponent.html и добавим собственный макет. Мы создадим разделенную страницу, на которой слева будут отображаться значения наших длительных процессов, а справа нарисуем несколько случайных кругов. Это позволит нашему браузеру запускать два дополнительных потока Web Worker, которые будут работать независимо, поэтому вы сможете увидеть Angular Web Workers в действии.


Весь код оставшейся части статьи можно получить из репозитория GitHub.


Вот как будет выглядеть конечная страница во время рисования случайных кругов (до начала длительного процесса).


Ускорьте свое приложение Angular с помощью Web Workers


Замените код в app.comComponent.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>


Загруженный код также включает в себя некоторые идентификаторы и классы CSS и связанные стили в Styles.css , которые используются для очень простого форматирования пользовательского интерфейса, поэтому у нас есть два раздела (левый и правый) и другие базовые стили.


 .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 (щелчок). Когда пользователь нажимает кнопку, длинный процесс запускается путем вызова метода longLoop, найденного в файле TypeScript компонента app.comComponent.ts .


 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); } } }


При этом выполняется 10 миллиардов итераций записи в переменную-член нашего компонента longProcessOutput.


Поскольку мы связали эту переменную-член в app.comComponent.html (в элементе textarea), пользовательский интерфейс будет отражать обновление каждый раз, когда обновляется переменная. Значение, которое мы устанавливаем в HTML, — это то место, где мы привязываем переменную-член.


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


Запустить его. Мы увидим, что ничего особенного не происходит, когда мы нажимаем кнопку, а затем внезапно текстовое поле обновляется множеством значений. Если мы откроем консоль, мы увидим записанные там значения во время выполнения кода.

Добавьте компонент случайного круга с помощью Angular CLI

Далее мы добавим компонент «круг», чтобы рисовать случайные круги. Мы можем сделать это с помощью Angular CLI с помощью следующей команды:


 ng generate component circle


Команда создала новую папку с именем Circle и четыре новых файла:

  • круг.компонент.html

  • круг.компонент.спец.ц (юнит-тесты)

  • Circle.comComponent.ts (код TypeScript)

  • Circle.comComponent.css (стили, которые будут применяться только к связанному HTML-коду этого компонента)


Создать круг компонентов


HTML прост. Нам просто нужен HTML, который будет представлять наш компонент. В нашем случае это компонент в правой части страницы, который будет отображать светло-зеленую сетку и рисовать круги. Этот рисунок выполняется с помощью элемента HTML Canvas.


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


Мы начинаем и останавливаем рисование кругов, добавляя привязку события Angular для захвата события mousedown. Если пользователь щелкнет в любом месте области холста, круги начнут рисоваться, если процесс еще не начался. Если процесс уже запущен, то метод toggleTimer (находится в Circle.comComponent.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.comComponent.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, мы просто заходим в папку нашего проекта и выполняем следующую команду:


 ng generate web-worker app


Этот последний параметр (приложение) — это имя компонента, содержащего наш длительный процесс, который мы хотим поместить в наш веб-воркер.


Создать приложение веб-работника


Angular добавит в app.comComponent.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. Гарантирует, что браузер поддерживает веб-работников.
  2. Создает нового работника.
  3. Отправляет сообщение работнику (находится в app.worker.ts ).
  4. Когда работник получит сообщение «привет», сработает EventListener (показано в следующем фрагменте кода).
  5. Когда EventListener срабатывает (в app.worker.ts ), он создает объект ответа и отправляет его обратно вызывающему объекту.


Вот все содержимое app.worker.ts :


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


В результате этих шагов мы увидим сообщения в консоли, которые будут выглядеть как последняя строка в следующем выводе консоли:


Консольный вывод


Это файл console.log, который появляется в EventHandler, созданном в исходном объекте Worker:


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


Это говорит нам о том, что app.comment отправил сообщение app.worker, а app.worker ответил собственным сообщением.


Мы хотим использовать Worker для запуска нашего длительного процесса в другом потоке, чтобы наш код рисования круга не прерывался.


Во-первых, давайте переместим код, связанный с нашими элементами пользовательского интерфейса, в конструктор нашего класса app.comComponent.


 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, который добавляет основные данные в текстовую область вместо записи в консоль в качестве первоначального теста.


Вы можете видеть, что выделенный текст слева — это полученное сообщение.


Получено сообщение

LongLoop в веб-воркере

Нам все равно нужно переместить наш длительный цикл в Web Worker, чтобы гарантировать, что при выполнении цикла он будет выполняться в своем собственном потоке.


Вот основная часть кода, который будет в нашем окончательном файле app.comComponent.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); } } }


Мы переместили рабочую переменную в класс, который теперь является переменной-членом. Таким образом, мы можем легко ссылаться на него в любом месте нашего класса AppComponent.


Далее давайте более подробно рассмотрим, как мы определили обработчик событий сообщения в рабочем объекте с помощью кода в конструкторе:


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


Этот код будет выполняться, когда класс Web Worker (находится в app.worker.ts ) вызывает postMessage(data). Каждый раз, когда вызывается метод postMessage, longProcessOutput (модель, привязанная к текстовой области) будет обновляться данными плюс возврат каретки («\n»), то есть каждое значение будет записываться на отдельной строке в элемент текстовой области.


Вот весь код, найденный в реальном 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 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, который мы туда поместили.

Завершение приложений Angular с помощью веб-воркеров

Запустив приложение, вы обнаружите, что нажатие кнопки [Начать длительный процесс] больше не приводит к приостановке рисования круга. Вы также сможете напрямую взаимодействовать с компонентом Canvas, рисующим круг, поэтому, если вы щелкнете по нему во время работы longLoop, Canvas будет немедленно перерисован. Раньше приложение вело себя так, как будто оно зависало, если вы это делали.


Теперь вы можете добавить свои долговыполняющиеся процессы в Angular Web Worker и воспользоваться всеми преимуществами приложения, которое не зависает.


Если вы хотите увидеть пример, решенный с помощью JavaScript Web Workers, вы можете увидеть его на Plunker .


Вы ищете компоненты пользовательского интерфейса, не зависящие от платформы? MESCIUS имеет полный набор компонентов пользовательского интерфейса JavaScript, включая сетки данных, диаграммы, датчики и элементы управления вводом . Мы также предлагаем мощные компоненты электронных таблиц , средства управления отчетами и улучшенные представления презентаций .


У нас есть глубокая поддержка Angular (а также React и Vue) и мы стремимся расширить наши компоненты для использования в современных средах JavaScript.