Por que você precisa de um Web Worker ? Um Web Worker é um componente de código para um aplicativo da web. Ele permite ao desenvolvedor criar um novo thread de execução para uma tarefa JavaScript para não interromper a execução do aplicativo principal.
À primeira vista, pode parecer que os navegadores suportam inerentemente threading e que o desenvolvedor não deveria ter que fazer nada de especial. Infelizmente, esse não é o caso. Web Workers resolvem um problema real de simultaneidade.
Web Workers fazem parte dos padrões funcionais esperados dos navegadores da web, e as especificações para eles foram escritas no W3C . A estrutura Angular preparou Web Workers para nós, e podemos adicioná-los facilmente ao nosso aplicativo usando a Angular Command Line Interface (CLI).
Neste artigo, examinaremos primeiro alguns equívocos sobre a simultaneidade de threads com JavaScript no navegador. Em seguida, criaremos um exemplo funcional demonstrando como é fácil implementar Web Workers com Angular, que permite threading simultâneo em um site.
Alguns desenvolvedores acreditam que o JavaScript é inerentemente concorrente no navegador porque quando o navegador se conecta a um site e recupera o HTML de uma página, ele pode abrir múltiplas conexões (cerca de seis) e extrair recursos (imagens, arquivos CSS vinculados, arquivos JavaScript vinculados, e assim por diante) simultaneamente. Parece que o navegador executa vários threads e inúmeras tarefas simultaneamente (via troca de contexto).
Para o desenvolvedor web não iniciado, isso parece indicar que o navegador pode realizar trabalho simultâneo. No entanto, quando se trata de JavaScript, o navegador executa apenas um processo por vez.
A maioria dos sites modernos, aplicativos de página única (SPA) e os aplicativos Web progressivos (PWA) mais modernos dependem de JavaScript e normalmente contêm vários módulos JavaScript. No entanto, sempre que o aplicativo da web estiver executando JavaScript, o navegador estará limitado a um único thread de atividade. Nenhum JavaScript será executado simultaneamente em circunstâncias normais.
Isso significa que se tivermos uma tarefa de longa execução ou de processamento intensivo definida em um de nossos módulos JavaScript, o usuário poderá experimentar o aplicativo travando ou parecendo travar. Ao mesmo tempo, o navegador aguarda a conclusão do processo antes que a interface do usuário (IU) possa ser atualizada. Esse tipo de comportamento faz com que os usuários percam a confiança em nossos aplicativos web ou SPAs, e nenhum de nós deseja isso.
Criaremos uma página de exemplo com dois painéis para permitir que nosso aplicativo web execute threads JavaScript simultâneos. Em um painel, a UI do nosso aplicativo é representada por uma grade com círculos, que atualiza constantemente a imagem e reage aos cliques do mouse. O segundo painel hospedará um processo de longa execução, que normalmente bloqueia o thread da UI e impede que a UI faça seu trabalho.
Para tornar nossa UI responsiva, executaremos nosso longo processo em um Web Worker, que o executará em um thread separado e não bloqueará a execução da lógica da UI dessa forma. Usaremos Angular como estrutura para construir o aplicativo porque torna a construção dos Web Workers uma tarefa simples de um comando.
Para usar o Angular CLI, precisamos ter o Node.js e o NPM (Node Package Manager) instalados. Depois de garantir que o Node e o NPM estejam instalados, abra uma janela do console e instale o Angular CLI via NPM (isso é algo único):
npm install -g @angular/cli
Mude o diretório para o diretório de destino onde queremos criar nosso novo modelo de aplicativo. Agora estamos prontos para criar nosso aplicativo. Usamos o comando “ng new” para fazer isso. Chamaremos nosso projeto de NgWebWorker:
ng new NgWebWorker --no-standalone
O assistente de projeto pergunta se queremos incluir roteamento em nosso projeto. Não precisamos de roteamento para este exemplo, então digite n.
Em seguida, ele perguntará que tipo de formato de folha de estilo queremos usar. Angular suporta o uso de processadores de folha de estilo como Sass e Less, mas neste caso usaremos CSS simples, então basta pressionar Enter para o padrão.
Veremos então algumas mensagens CREATE enquanto o NPM extrai os pacotes necessários e a CLI cria o projeto modelo. Finalmente, quando estiver concluído, teremos um cursor piscando novamente na linha de comando.
Neste ponto, o Angular CLI criou um projeto e o colocou na pasta NgWebWorker. Mude o diretório para NgWebWorker. A CLI Angular criou tudo o que precisávamos para trabalhar em nosso projeto Angular, incluindo a instalação do servidor Node HTTP. Isso significa que tudo o que precisamos fazer para iniciar o aplicativo de modelo é o seguinte:
ng serve
O Angular CLI compila seu projeto e inicia o servidor Node HTTP. Agora, você pode carregar o aplicativo em seu navegador apontando-o para a URL <a href="http://localhost:4200"target="_blank"> .
Ao carregarmos a página, vemos o template básico com o nome do projeto no topo.
A vantagem de executar “ng serve” é que quaisquer alterações feitas no código farão com que o site seja atualizado automaticamente no navegador, tornando muito mais fácil ver as alterações entrarem em vigor.
A maior parte do código em que nos concentraremos está no diretório /src/app.
app.component.html contém HTML para o componente usado atualmente para exibir a página principal. O código do componente é representado no arquivo app.component.ts (TypeScript).
Excluiremos o conteúdo de app.component.html e adicionaremos nosso próprio layout. Criaremos uma página dividida que exibe nossos valores de processo de longa execução no lado esquerdo e desenharemos alguns círculos aleatórios no lado direito. Isso permitirá que nosso navegador execute dois threads adicionais do Web Worker que funcionarão de forma independente para que você possa ver os Angular Web Workers em ação.
Todo o código do restante do artigo pode ser obtido no repositório GitHub.
Esta é a aparência da página final enquanto desenha os círculos aleatórios (antes do início do longo processo).
Substitua o código em app.component.html pelo seguinte:
<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>
O download do código também inclui alguns IDs e classes CSS e estilos associados em estilos.css , que são usados para a formatação muito simples da UI, portanto temos duas seções (esquerda e direita) e outros estilos básicos.
.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; }
O importante a notar aqui é que adicionamos uma ligação de evento Angular (clique) ao botão. Quando o usuário clica no botão, o longo processo é iniciado chamando o método longLoop encontrado no arquivo TypeScript do componente, app.component.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); } } }
Isso executa 10 bilhões de iterações gravando em uma variável membro de nosso componente, longProcessOutput.
Como vinculamos essa variável de membro em app.component.html (no elemento textarea), a UI refletirá a atualização cada vez que a variável for atualizada. O valor que definimos no HTML é onde vinculamos a variável membro.
<textarea rows="20" [value]="longProcessOutput"></textarea>
Executá-lo. Veremos que nada acontece quando clicamos no botão e, de repente, a área de texto é atualizada com vários valores. Se abrirmos o console, veremos os valores escritos lá conforme o código é executado.
A seguir, adicionaremos um componente “círculo” para desenhar círculos aleatórios. Podemos fazer isso usando o Angular CLI com o seguinte comando:
ng generate component circle
O comando criou uma nova pasta chamada círculo e criou quatro novos arquivos:
círculo.component.html
círculo.component.spec.ts (testes de unidade)
círculo.component.ts (código TypeScript)
círculo.component.css (estilos que serão aplicados apenas ao HTML associado a este componente)
O HTML é direto. Precisamos apenas do HTML que representará nosso componente. No nosso caso, este é o componente do lado direito da página, que irá exibir a grade verde clara e desenhar os círculos. Este desenho é feito através do elemento HTML Canvas.
<div id="second"> <canvas #mainCanvas (mousedown)="toggleTimer()"></canvas> </div>
Começamos e paramos o desenho dos círculos adicionando uma ligação de evento Angular para capturar o evento mousedown. Se o usuário clicar dentro da área do Canvas em qualquer lugar, os círculos começarão a ser desenhados, caso o processo ainda não tenha sido iniciado. Se o processo já tiver sido iniciado, o método toggleTimer (encontrado em circle.component.ts ) limpa a grade e interrompe o desenho de círculos.
toggleTimer simplesmente usa setInterval para desenhar um círculo em um local aleatório com uma cor selecionada aleatoriamente a cada 100 milissegundos (10 círculos/segundo).
toggleTimer(){ if (CircleComponent.IntervalHandle === null){ CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles,50); } else{ clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } }
Há mais código em circle.component.ts que configura o elemento Canvas, inicializa variáveis de membro e faz o desenho. Quando adicionado, seu código deverá ficar assim:
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(); } }
Não se esqueça de adicionar o componente círculo ao arquivo index.html :
<body> <app-root></app-root> <app-circle></app-circle> </body>
Quando a página carregar, os círculos começarão a ser desenhados. Ao clicar no botão [Iniciar processo longo], veremos a pausa do desenho. Isso porque todo o trabalho está sendo feito no mesmo thread.
Vamos resolver esse problema adicionando um Web Worker.
Para adicionar um novo Web Worker usando a CLI, simplesmente vamos até a pasta do nosso projeto e executamos o seguinte comando:
ng generate web-worker app
Esse último parâmetro (app) é o nome do componente que contém nosso processo de longa execução, que desejaremos colocar em nosso Web Worker.
Angular adicionará algum código ao app.component.ts semelhante ao seguinte:
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. }
O que o novo código faz? Podemos ver que este código faz referência ao novo componente app.worker que o comando também adicionou. Neste ponto, o código:
Aqui está todo o conteúdo de app.worker.ts :
/// <reference lib="webworker" /> addEventListener('message', ({ data }) => { const response = `worker response to ${data}`; postMessage(response); });
Veremos mensagens no console como resultado dessas etapas, que serão semelhantes à última linha na seguinte saída do console:
Esse é o console.log que ocorre no EventHandler que foi criado no objeto Worker original:
worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); };
Isso nos diz que app.component postou uma mensagem para app.worker e app.worker respondeu com sua própria mensagem.
Queremos usar o Worker para executar nosso processo Long Running em outro thread para que nosso código de desenho circular não seja interrompido.
Primeiro, vamos mover o código envolvido com nossos elementos de UI para um construtor de nossa classe 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. } }
Isso nos permite fazer referência à variável longProcessOutput agora. Com isso podemos acessar essa variável; temos trabalhador.onmessage, que adiciona os dados básicos à área de texto em vez de gravar no console apenas como um teste inicial.
Você pode ver que o texto destacado à esquerda é a mensagem recebida.
Ainda precisamos mover nosso loop de longa duração para o Web Worker para garantir que, quando o loop for executado, ele estará sendo executado em seu próprio thread.
Aqui está a maior parte do código que teremos em nosso app.component.ts final:
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); } } }
Movemos a variável de trabalho para a classe, que agora é uma variável de membro. Dessa forma, podemos referenciá-lo facilmente em qualquer lugar da nossa classe AppComponent.
A seguir, vamos examinar mais de perto como definimos um manipulador de eventos de mensagem no objeto trabalhador com o código no construtor:
this.worker.onmessage = ({ data }) => { this.longProcessOutput += `${data}` + "\n"; };
Esse código será executado quando a classe Web Worker (encontrada em app.worker.ts ) chamar postMessage(data). Cada vez que o método postMessage for chamado, o longProcessOutput (o modelo vinculado à área de texto) será atualizado com os dados mais um retorno de carro (“\n”), que serve simplesmente para que cada valor seja escrito em sua própria linha no elemento de área de texto.
Aqui está todo o código encontrado no Web Worker real ( 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); } } });
Este manipulador de eventos é acionado quando o usuário clica no botão [Executar processo longo]. Todo o código encontrado no Web Worker ( app.worker.ts ) é executado em um novo thread. Esse é o valor do Web Worker; seu código é executado em um thread separado. É por isso que não afeta mais o thread principal do aplicativo web.
O código do Web Worker é acionado quando o usuário clica no botão porque agora temos o seguinte código em nosso método 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..."); }
Quando a mensagem é postada no trabalhador, o EventListener é acionado e executa o código do nosso longLoop original que colocamos lá.
Ao executar o aplicativo, você descobrirá que clicar no botão [Iniciar processo longo] não faz mais uma pausa no desenho do círculo. Você também poderá interagir diretamente com o componente Canvas de desenho circular para que, se clicar nele enquanto o longLoop ainda estiver em execução, o Canvas seja redesenhado imediatamente. Anteriormente, o aplicativo se comportava como se estivesse congelado se você fizesse isso.
Agora, você pode adicionar seus processos de longa execução a um Angular Web Worker e colher todos os benefícios de um aplicativo que não congela.
Se você quiser ver um exemplo resolvido com JavaScript Web Workers, você pode vê-lo no Plunker .
Você está procurando componentes de UI independentes de estrutura? MESCIUS possui um conjunto completo de componentes de UI JavaScript, incluindo grades de dados, gráficos, medidores e controles de entrada . Também oferecemos componentes avançados de planilhas , controles de relatórios e visualizações aprimoradas de apresentações .
Temos profundo suporte para Angular (bem como React e Vue) e nos dedicamos a estender nossos componentes para uso em estruturas JavaScript modernas.