なぜWeb Worker が必要なのでしょうか? Web Worker は、Web アプリケーションのコード コンポーネントです。開発者は、Web Worker を使用することで、メイン アプリの実行を中断することなく、JavaScript タスクの新しい実行スレッドを作成できます。
一見すると、ブラウザは本質的にスレッドをサポートしており、開発者は特別なことをする必要はないと思われるかもしれません。残念ながら、そうではありません。Web Workers は実際の同時実行の問題を解決します。
Web Workers は、Web ブラウザーの期待される機能標準の一部であり、その仕様はW3Cで作成されています。Angular フレームワークはWeb Workers をラップしており、Angular コマンド ライン インターフェース (CLI) を使用して簡単にアプリに追加できます。
この記事では、まず、ブラウザでの JavaScript によるスレッドの同時実行に関する誤解をいくつか検証します。次に、Web サイトでの同時スレッド化を可能にする Web Workers を Angular で実装するのがいかに簡単かを示す機能的な例を作成します。
一部の開発者は、ブラウザが Web サイトに接続してページの HTML を取得するときに、複数の接続 (約 6 つ) を開いてリソース (画像、リンクされた CSS ファイル、リンクされた JavaScript ファイルなど) を同時に取得できるため、JavaScript はブラウザ内で本質的に同時実行可能であると考えています。ブラウザは、複数のスレッドと多数のタスクを同時に実行しているように見えます (コンテキスト切り替え経由)。
経験の浅い Web 開発者にとっては、これはブラウザが同時処理を実行できることを示しているように思えます。しかし、JavaScript に関しては、ブラウザは実際には一度に 1 つのプロセスしか実行しません。
最新の Web サイト、シングル ページ アプリ(SPA)、および最新のプログレッシブ Web アプリ(PWA) のほとんどは JavaScript に依存しており、通常は多数の JavaScript モジュールが含まれています。ただし、Web アプリが JavaScript を実行している間は常に、ブラウザーのアクティビティは 1 つのスレッドに制限されます。通常の状況では、JavaScript は同時に実行されません。
つまり、JavaScript モジュールの 1 つに長時間実行またはプロセス集約型のタスクが定義されている場合、ユーザーはアプリが途切れたり、ハングしているように見えることがあります。同時に、ブラウザーはプロセスが完了するまで待機し、その後ユーザー インターフェイス (UI) を更新します。このような動作により、ユーザーは Web アプリや SPA への信頼を失いますが、これは誰も望んでいません。
2 つのペインを持つサンプル ページを作成し、Web アプリで JavaScript スレッドを同時に実行できるようにします。1 つのペインでは、アプリケーションの UI が円の付いたグリッドで表され、画像が常に更新され、マウス クリックに反応します。2 つ目のペインでは、長時間実行されるプロセスがホストされます。このプロセスは通常、UI スレッドをブロックし、UI がジョブを実行できないようにします。
UI をレスポンシブにするために、長いプロセスを Web Worker で実行します。これにより、プロセスは別のスレッドで実行され、UI ロジックの実行がブロックされなくなります。アプリを構築するためのフレームワークとして Angular を使用します。Angular を使用すると、Web Worker の構築が簡単な 1 つのコマンド タスクになるからです。
Angular CLI を使用するには、 Node.jsとNPM (Node Package Manager) をインストールする必要があります。Node と NPM がインストールされていることを確認したら、コンソール ウィンドウを開き、NPM 経由でAngular CLIをインストールします (これは 1 回限りの作業です)。
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 は、Node HTTP サーバーのインストールを含め、Angular プロジェクトで作業するために必要なものをすべて作成しました。つまり、テンプレート アプリを起動するために必要なことは、次のことだけです。
ng serve
Angular CLI はプロジェクトをコンパイルし、Node HTTP サーバーを起動します。これで、 <a href="http://localhost:4200"target="_blank"> URL を指定して、ブラウザーにアプリを読み込むことができます。
ページを読み込むと、プロジェクト名が上部に表示された基本テンプレートが表示されます。
「ng serve」を実行する利点は、コードに変更を加えるとブラウザでサイトが自動的に更新され、変更が有効になったことが簡単に確認できるようになることです。
ここで注目するコードのほとんどは、/src/app ディレクトリの下にあります。
app.component.htmlには、メイン ページを表示するために現在使用されている 1 つのコンポーネントの HTML が含まれています。コンポーネント コードは、 app.component.ts (TypeScript) ファイルで表されます。
app.component.html の内容を削除し、独自のレイアウトを追加します。左側に長時間実行中のプロセスの値を表示し、右側にランダムな円をいくつか描画する分割ページを作成します。これにより、ブラウザーで独立して動作する 2 つの Web Worker スレッドを追加で実行できるようになり、Angular Web Workers の動作を確認できます。
この記事の残りの部分にあるすべてのコードは、GitHub リポジトリから入手できます。
ランダムな円を描画している間(長時間実行プロセスが開始する前)、最終ページは次のようになります。
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 の非常にシンプルな書式設定に使用され、2 つのセクション (左と右) とその他の基本的なスタイルが存在します。
.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); } } }
これは、コンポーネントのメンバー変数 longProcessOutput への書き込みを 100 億回繰り返して実行します。
メンバー変数をapp.component.html (textarea 要素) にバインドしたので、変数が更新されるたびに UI に更新が反映されます。HTML で設定した値は、メンバー変数をバインドする場所です。
<textarea rows="20" [value]="longProcessOutput"></textarea>
実行します。ボタンをクリックしても何も起こらないことがわかりますが、その後突然、テキストエリアが一連の値で更新されます。コンソールを開くと、コードの実行時にそこに書き込まれた値が表示されます。
次に、ランダムな円を描くための「circle」コンポーネントを追加します。これは、Angular CLI を使用して次のコマンドで実行できます。
ng generate component circle
このコマンドは、circle という名前の新しいフォルダーを作成し、次の 4 つの新しいファイルを作成しました。
円コンポーネント.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>
mousedown イベントを取得する Angular イベント バインディングを追加して、円の描画を開始および停止します。ユーザーが Canvas 領域内の任意の場所をクリックすると、プロセスがまだ開始されていない場合は円の描画が開始されます。プロセスがすでに開始されている場合は、toggleTimer メソッド ( circle.component.ts内) によってグリッドがクリアされ、円の描画が停止します。
ToggleTimer は、setInterval を使用して、100 ミリ秒ごとにランダムに選択された色でランダムな場所に円を描画します (1 秒あたり 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 を追加してこの問題を解決しましょう。
CLI を使用して新しい Web Worker を追加するには、プロジェクト フォルダーに移動して、次のコマンドを実行するだけです。
ng generate web-worker app
最後のパラメーター (app) は、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 コンポーネントを参照していることがわかります。この時点で、コードは次のようになります。
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 に移動する必要があります。
最終的な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); } } }
ワーカー変数をクラスに移動しました。このクラスはメンバー変数になりました。これにより、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 Worker のコードは別のスレッドで実行されます。そのため、Web アプリのメイン スレッドには影響しません。
longLoop メソッドに次のコードが含まれているため、ユーザーがボタンをクリックすると Web Worker コードが実行されます。
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 のコードが実行されます。
アプリを実行すると、[Start Long Process] ボタンをクリックしても円の描画が一時停止しなくなりました。また、円を描画する Canvas コンポーネントを直接操作できるようになり、longLoop の実行中にクリックすると、Canvas がすぐに再描画されます。以前は、これを行うとアプリがフリーズしたかのように動作していました。
これで、長時間実行されるプロセスを Angular Web Worker に追加して、フリーズしないアプリのメリットをすべて享受できるようになります。
JavaScript Web Workers で解決された例を見たい場合は、 Plunkerで確認できます。
フレームワークに依存しない UI コンポーネントをお探しですか? MESCIUS には、データ グリッド、チャート、ゲージ、入力コントロールなど、 JavaScript UI コンポーネントの完全なセットがあります。また、強力なスプレッドシート コンポーネント、レポート コントロール、強化されたプレゼンテーション ビューも提供しています。
当社はAngular (および React と Vue) を強力にサポートしており、最新の JavaScript フレームワークで使用できるようにコンポーネントを拡張することに専念しています。