Pourquoi avez-vous besoin d'un Web Worker ? Un Web Worker est un composant de code pour une application Web. Il permet au développeur de créer un nouveau thread d'exécution pour une tâche JavaScript afin de ne pas interrompre l'exécution de l'application principale.
À première vue, il peut sembler que les navigateurs prennent automatiquement en charge le threading et que le développeur ne devrait rien faire de spécial. Malheureusement, ce n'est pas le cas. Les Web Workers résolvent un réel problème de concurrence.
Les Web Workers font partie des standards fonctionnels attendus des navigateurs Web, et leurs spécifications ont été rédigées au W3C . Le framework Angular a enveloppé les Web Workers pour nous, et nous pouvons facilement les ajouter à notre application à l'aide de l'interface de ligne de commande (CLI) Angular.
Dans cet article, nous examinerons d'abord quelques idées fausses sur la concurrence des threads avec JavaScript dans le navigateur. Ensuite, nous créerons un exemple fonctionnel démontrant à quel point il est facile d'implémenter des Web Workers avec Angular, qui permet le threading simultané sur un site Web.
Certains développeurs pensent que JavaScript est intrinsèquement concurrent dans le navigateur, car lorsque le navigateur se connecte à un site Web et récupère le HTML d'une page, il peut ouvrir plusieurs connexions (environ six) et extraire des ressources (images, fichiers CSS liés, fichiers JavaScript liés, et ainsi de suite) simultanément. Il semble que le navigateur exécute plusieurs threads et de nombreuses tâches simultanément (via un changement de contexte).
Pour le développeur Web non initié, cela semble indiquer que le navigateur peut effectuer un travail simultané. Cependant, lorsqu’il s’agit de JavaScript, le navigateur n’exécute en réalité qu’un seul processus à la fois.
La plupart des sites Web modernes, les applications à page unique (SPA) et les applications Web progressives (PWA) plus modernes dépendent de JavaScript et contiennent généralement de nombreux modules JavaScript. Cependant, à chaque fois que l'application Web exécute JavaScript, le navigateur est limité à un seul fil d'activité. Aucun code JavaScript ne s'exécutera simultanément dans des circonstances normales.
Cela signifie que si une tâche de longue durée ou à forte intensité de processus est définie dans l'un de nos modules JavaScript, l'utilisateur peut rencontrer un bégaiement ou un blocage de l'application. Dans le même temps, le navigateur attend la fin du processus avant de pouvoir mettre à jour l'interface utilisateur (UI). Ce type de comportement fait perdre confiance aux utilisateurs dans nos applications Web ou nos SPA, et aucun de nous ne le souhaite.
Nous allons créer un exemple de page avec deux volets pour permettre à notre application Web d'exécuter des threads JavaScript simultanés. Dans un volet, l'interface utilisateur de notre application est représentée par une grille avec des cercles, qui met constamment à jour l'image et réagit aux clics de souris. Le deuxième volet hébergera un processus de longue durée, qui bloque normalement le thread de l'interface utilisateur et empêche l'interface utilisateur de faire son travail.
Pour rendre notre interface utilisateur réactive, nous exécuterons notre long processus dans un Web Worker, qui l'exécutera sur un thread séparé et ne bloquera pas l'exécution de la logique de l'interface utilisateur de cette façon. Nous utiliserons Angular comme cadre pour créer l'application, car cela fait de la construction des Web Workers une tâche simple à une seule commande.
Pour utiliser Angular CLI, nous devons installer Node.js et NPM (Node Package Manager). Une fois que nous nous sommes assurés que Node et NPM sont installés, ouvrez une fenêtre de console, puis installez Angular CLI via NPM (c'est une chose ponctuelle) :
npm install -g @angular/cli
Remplacez le répertoire par le répertoire cible dans lequel nous souhaitons créer notre nouveau modèle d'application. Nous sommes maintenant prêts à créer notre application. Nous utilisons la commande « ng new » pour ce faire. Nous nommerons notre projet NgWebWorker :
ng new NgWebWorker --no-standalone
L'assistant de projet nous demande si nous souhaitons inclure le routage dans notre projet. Nous n'avons pas besoin de routage pour cet exemple, alors tapez n.
Il nous demandera ensuite quel type de format de feuille de style nous souhaitons utiliser. Angular prend en charge l'utilisation de processeurs de feuilles de style tels que Sass et Less, mais dans ce cas, nous utiliserons du CSS simple, il suffit donc d'appuyer sur Entrée pour la valeur par défaut.
Nous verrons ensuite des messages CREATE lorsque NPM extraira les packages requis et que la CLI créera le projet modèle. Enfin, une fois l'opération terminée, nous obtenons à nouveau un curseur clignotant sur la ligne de commande.
À ce stade, la CLI angulaire a créé un projet et l'a placé dans le dossier NgWebWorker. Changez le répertoire en NgWebWorker. La CLI Angular a créé tout ce dont nous avions besoin pour travailler sur notre projet Angular, y compris l'installation du serveur HTTP Node. Cela signifie que tout ce que nous avons à faire pour démarrer l'application modèle est le suivant :
ng serve
La CLI angulaire compile votre projet et démarre le serveur HTTP Node. Vous pouvez désormais charger l'application dans votre navigateur en la pointant vers l'URL <a href="http://localhost:4200"target="_blank"> .
Lorsque nous chargeons la page, nous voyons le modèle de base avec le nom du projet en haut.
L'avantage d'exécuter « ng serve » est que toute modification apportée au code entraînera automatiquement l'actualisation du site dans le navigateur, ce qui permettra de voir beaucoup plus facilement que les modifications prennent effet.
La plupart du code sur lequel nous allons nous concentrer se trouve dans le répertoire /src/app.
app.component.html contient du HTML pour le composant actuellement utilisé pour afficher la page principale. Le code du composant est représenté dans le fichier app.component.ts (TypeScript).
Nous supprimerons le contenu de app.component.html et ajouterons notre propre mise en page. Nous allons créer une page divisée qui affiche nos valeurs de processus de longue durée sur le côté gauche et dessiner des cercles aléatoires sur le côté droit. Cela permettra à notre navigateur d'exécuter deux threads Web Worker supplémentaires qui fonctionneront indépendamment afin que vous puissiez voir Angular Web Workers en action.
Tout le code du reste de l'article peut être obtenu à partir du référentiel GitHub.
Voici à quoi ressemblera la page finale pendant qu'elle dessine les cercles aléatoires (avant le début du long processus).
Remplacez le code dans app.component.html par ce qui suit :
<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>
Le téléchargement du code comprend également certains identifiants et classes CSS ainsi que les styles associés dans styles.css , qui sont utilisés pour le formatage très simple de l'interface utilisateur, nous avons donc deux sections (gauche et droite) et d'autres styles de base.
.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; }
La chose importante à noter ici est que nous avons ajouté une liaison d'événement angulaire (clic) au bouton. Lorsque l'utilisateur clique sur le bouton, le processus long est démarré en appelant la méthode longLoop trouvée dans le fichier TypeScript du composant, 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); } } }
Cela exécute 10 milliards d'itérations en écrivant dans une variable membre de notre composant, longProcessOutput.
Étant donné que nous avons lié cette variable membre dans app.component.html (sur l'élément textarea), l'interface utilisateur reflétera la mise à jour à chaque fois que la variable est mise à jour. La valeur que nous définissons dans le HTML est l'endroit où nous lions la variable membre.
<textarea rows="20" [value]="longProcessOutput"></textarea>
Exécuter. Nous verrons que rien ne se passe lorsque nous cliquons sur le bouton, et puis tout à coup, la zone de texte est mise à jour avec un tas de valeurs. Si nous ouvrons la console, nous voyons les valeurs qui y sont écrites pendant l'exécution du code.
Ensuite, nous ajouterons un composant « cercle » pour dessiner des cercles aléatoires. Nous pouvons le faire en utilisant la CLI angulaire avec la commande suivante :
ng generate component circle
La commande a créé un nouveau dossier nommé circle et créé quatre nouveaux fichiers :
cercle.component.html
circle.component.spec.ts (tests unitaires)
circle.component.ts (code TypeScript)
circle.component.css (styles qui ne seront appliqués qu'au HTML associé à ce composant)
Le HTML est simple. Nous avons juste besoin du HTML qui représentera notre composant. Dans notre cas, il s'agit du composant situé à droite de la page, qui affichera la grille vert clair et dessinera les cercles. Ce dessin est réalisé via l'élément HTML Canvas.
<div id="second"> <canvas #mainCanvas (mousedown)="toggleTimer()"></canvas> </div>
Nous commençons et arrêtons le dessin des cercles en ajoutant une liaison d'événement angulaire pour récupérer l'événement mousedown. Si l'utilisateur clique n'importe où dans la zone Canvas, les cercles commenceront à se dessiner si le processus n'a pas déjà commencé. Si le processus est déjà démarré, alors la méthode toggleTimer (trouvée dans circle.component.ts ) efface la grille et arrête le dessin des cercles.
toggleTimer utilise simplement setInterval pour dessiner un cercle dans un emplacement aléatoire avec une couleur sélectionnée au hasard toutes les 100 millisecondes (10 cercles/seconde).
toggleTimer(){ if (CircleComponent.IntervalHandle === null){ CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles,50); } else{ clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } }
Il y a plus de code dans circle.component.ts qui configure l'élément Canvas, initialise les variables membres et effectue le dessin. Une fois ajouté, votre code devrait ressembler à ceci :
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'oubliez pas d'ajouter le composant cercle au fichier index.html :
<body> <app-root></app-root> <app-circle></app-circle> </body>
Lorsque la page se charge, les cercles commenceront à se dessiner. Lorsque nous cliquons sur le bouton [Démarrer le processus long], nous verrons la pause du dessin. C'est parce que tout le travail est effectué sur le même fil.
Résolvons ce problème en ajoutant un Web Worker.
Pour ajouter un nouveau Web Worker à l'aide de la CLI, nous allons simplement dans notre dossier de projet et exécutons la commande suivante :
ng generate web-worker app
Ce dernier paramètre (app) est le nom du composant qui contient notre processus de longue durée, que nous souhaitons placer dans notre Web Worker.
Angular ajoutera du code au app.component.ts qui ressemble à ce qui suit :
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. }
A quoi sert le nouveau code ? Nous pouvons voir que ce code fait référence au nouveau composant app.worker que la commande a également ajouté. A ce stade, le code :
Voici l'intégralité du contenu de app.worker.ts :
/// <reference lib="webworker" /> addEventListener('message', ({ data }) => { const response = `worker response to ${data}`; postMessage(response); });
Nous verrons des messages dans la console à la suite de ces étapes, qui ressembleront à la dernière ligne de la sortie suivante de la console :
Il s'agit du fichier console.log qui apparaît dans le gestionnaire d'événements créé sur l'objet Worker d'origine :
worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); };
Cela nous indique que app.component a posté un message à app.worker et que app.worker a répondu avec son propre message.
Nous souhaitons utiliser le Worker pour exécuter notre processus Long Running sur un autre thread afin que notre code de dessin de cercle ne soit pas interrompu.
Tout d’abord, déplaçons le code impliqué dans nos éléments d’interface utilisateur vers un constructeur de notre 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. } }
Cela nous permet de référencer maintenant la variable longProcessOutput. Avec cela, nous pouvons accéder à cette variable ; nous avons worker.onmessage, qui ajoute les données de base à la zone de texte au lieu d'écrire dans la console juste à titre de test initial.
Vous pouvez voir que le texte en surbrillance à gauche est le message reçu.
Nous devons toujours déplacer notre boucle de longue durée vers Web Worker pour nous assurer que, lorsque la boucle s'exécutera, elle s'exécutera sur son propre thread.
Voici l'essentiel du code que nous aurons dans notre 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); } } }
Nous avons déplacé la variable travailleur dans la classe, qui est désormais une variable membre. De cette façon, nous pouvons facilement le référencer n'importe où dans notre classe AppComponent.
Examinons ensuite de plus près comment nous avons défini un gestionnaire d'événements de message sur l'objet travailleur avec le code dans le constructeur :
this.worker.onmessage = ({ data }) => { this.longProcessOutput += `${data}` + "\n"; };
Ce code s'exécutera lorsque la classe Web Worker (trouvée dans app.worker.ts ) appellera postMessage(data). Chaque fois que la méthode postMessage est appelée, le longProcessOutput (le modèle lié à la zone de texte) sera mis à jour avec les données plus un retour chariot (« \n »), ce qui signifie simplement que chaque valeur sera écrite sur sa propre ligne dans le élément de zone de texte.
Voici tout le code trouvé dans le Web Worker actuel ( 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); } } });
Ce gestionnaire d'événements est déclenché lorsque l'utilisateur clique sur le bouton [Exécuter un processus long]. Tout le code trouvé dans Web Worker ( app.worker.ts ) s'exécute sur un nouveau thread. C'est la valeur du Web Worker ; son code s'exécute sur un thread séparé. C'est pourquoi cela n'affecte plus le thread principal de l'application Web.
Le code Web Worker est déclenché lorsque l'utilisateur clique sur le bouton car nous avons maintenant le code suivant dans notre méthode 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..."); }
Lorsque le message est envoyé au travailleur, EventListener déclenche et exécute le code de notre longLoop d'origine que nous y avons placé.
Lorsque vous exécutez l'application, vous constaterez que cliquer sur le bouton [Démarrer le processus long] ne provoque plus la pause du dessin du cercle. Vous pourrez également interagir directement avec le composant Canvas de dessin de cercles de sorte que si vous cliquez dessus pendant que longLoop est toujours en cours d'exécution, le Canvas sera redessiné immédiatement. Auparavant, l'application se comportait comme si elle était gelée si vous faisiez cela.
Désormais, vous pouvez ajouter vos processus de longue durée à un Angular Web Worker et profiter de tous les avantages d'une application qui ne se bloque pas.
Si vous souhaitez voir un exemple résolu avec JavaScript Web Workers, vous pouvez le voir sur Plunker .
Recherchez-vous des composants d’interface utilisateur indépendants du framework ? MESCIUS dispose d'un ensemble complet de composants d'interface utilisateur JavaScript, notamment des grilles de données, des graphiques, des jauges et des contrôles de saisie . Nous proposons également de puissants composants de feuille de calcul , des contrôles de reporting et des vues de présentation améliorées .
Nous prenons en charge Angular (ainsi que React et Vue) et nous nous engageons à étendre nos composants pour une utilisation dans les frameworks JavaScript modernes.