WebPush通知をMAMPで動かす方法 (1/2)

JavaScriptは基本的にシングルスレッドで動作しているので、ある時点では何か1つの処理しか実行していません。しかしブラウザ上で動作する「Web Worker」という仕組みを使用すると、本当に並列的にJavaScriptを実行することが可能です。

Web Workerは比較的実装が簡単なので、Web Workerを応用して実装されている「Service Worker」という仕組みの中で使用できる「Webプッシュ通知」という機能をWindows版のMAMPで動かしてみます。WSL2を使用しても動作します。

今回はWeb Workerの実装方法をメモとして残します。次回からService Workerを実装していきます。

Web Workerを実装します

まずはWorkerという仕組みの基本になっているWeb Workerを実装してみます。こちらの記事を参考にして、MAMPに含まれているPHPをコマンドラインから実行できる様にしておきます。以降はMAMPがインストールされている前提でご説明いたします。MAMPを使用せずに個別にPHPをインストールしても実行できそうですが、次回ご説明する「Service Worker」ではOpenSSLが必要になるため、個別にインストールするとセットアップが大変と思われます。MAMPを使用しない場合はWSL2等の使用をご検討ください。

「C:¥MAMP¥htdocs」フォルダ内に「WebWorker」というフォルダを作成して、VSCodeで開きます。その後「index.html」、「main.js」、「worker.js」というファイルを作成して、以下の内容を書き込んで保存します。「main.js」ファイルがブラウザから直接参照される(メインスレッドから呼ばれる)ファイルになっています。「worker.js」ファイルがバックグラウンドで動作するファイルになっています。

<!DOCTYPE html>
<head>
    <title>Web Worker</title>
</head>
<body>
    <h1></h1>
    <script src="main.js"></script>
</body>
</html>
const worker = new Worker('worker.js');

worker.postMessage('Hello, world');

worker.addEventListener('message', (e) => {
  console.log('Workerからデータを受信しました: ', e.data);
  const h1 = document.querySelector('h1');
  h1.textContent = e.data;

}, false);
self.addEventListener('message', (e) => {
  self.postMessage(e.data);
}, false);

次にVSCodeにCtrl + @と入力してターミナル画面を表示します。「php -S localhost:8080」と入力してリターンキーを押して、PHPに組み込まれている簡易的なWEBサーバーを起動します。MAMP自体は起動しなくて大丈夫です。この状態でブラウザを起動してURL欄に「localhost:8080」と入力すれば実行できますが、せっかくなのでデバッグ環境も整えておきます。

左側にある「実行とデバッグ」ボタンを押して、表示された画面から「launch.json ファイルを作成します。」をクリックします。

launch.jsonファイルの保存場所を聞かれるので、自分のフォルダ(WebWorker)を選択します。

次にデバッガーの種類を聞かれるので、「Webアプリ (Chrome)」を選択します。

「Esc」キーを押してデバッグタイプのリストを閉じます。

以下の様な設定があるか確認します。特に11行目のurl項目が、先ほど起動したPHPサーバーの設定(localhost:8080)と合っているか確認します。

{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "chrome",
            "request": "launch",
            "name": "localhost に対して Chrome を起動する",
            "url": "http://localhost:8080",
            "webRoot": "${workspaceFolder}"
        }
    ]
}

もし設定がない場合は、launch.json画面内にある「構成の追加...」ボタンを押して、表示されたデバッガーのリストから、「{} Chrome: 起動」を選択します。

「実行とデバッグ」ドロップダウンで有効な設定が選択されているのを確認して、「デバッグの開始」ボタンを押すとブラウザが起動します。

以下の様な文字列が表示されれば成功です。Web Workerが正常に実行されています。

ブレークポイントを設定しておけば、メイン側のスクリプトでもバックグラウンド側のスクリプトでも同じように停止します。

Web Workerの使用方法
・Web Workerを作成するには

Web Workerを作成するには以下の様に記述します。作成する個数の制限は特にない様ですが、Worker内でさらにWorkerは作成できない様です。オプションでコンストラクタの第2引数にワーカー名を追加しておくと、ワーカースレッド内で「self.name」プロパティでアクセスできるのでデバッグ時に便利かもしれません。その他のオプションも設定できる様ですが詳しくはこちらをご覧ください。

const worker1 = new Worker('worker.js');
const worker2 = new Worker('worker.js', { name: 'worker2' });
・Web Workerを終了するには

メインスレッドからもワーカースレッド内からも終了できるようです。メインスレッドからは「terminate」、ワーカースレッド内からは「close」メソッドを呼び出します。

/* メインスレッド */
worker.terminate();
/* ワーカースレッド */
self.close(); // selfは省略可能です。
・Web Workerと通信するには

どちらからも「postMessage」メソッドでデータの送信が可能です。データを受信するにはイベントリスナー等で「message」イベントにコールバック関数を登録しておきます。送信されたデータはコールバック関数の引数(e)の「data」というプロパティで取得できます。

/* メインスレッド */
const worker = new Worker('worker.js');

worker.postMessage('Hello, world');

worker.addEventListener('message', (e) => {
  console.log('Workerからデータを受信しました: ', e.data);
}, false);
/* ワーカースレッド */
self.addEventListener('message', (e) => { // selfは省略可能です。
  self.postMessage(e.data); // selfは省略可能です。
}, false);
・複数のデータを送受信するには

postMessageメソッドは一度に1つのオブジェクトしか送信できないので、送信データを配列または連想配列の形式で送信します。送信側と受信側でデータの形を揃える必要があります。

/* メインスレッド (送信側) */
const worker = new Worker('worker.js');

worker.postMessage({ id: 'message', param: 'Hello, world1'});
worker.postMessage({ id: 'command', param: 'データ'});
/* ワーカースレッド (受信側) */
addEventListener('message', (e) => {
  const { id, param } = e.data;

  switch (id) {
    case 'message':
      // 何かの処理
      break;
    case 'command':
      // 何かの処理
      break;
  }
}, false);
・Web Workerのエラーをキャッチするには

メインスレッドから参照されているスクリプトファイル内で「error」イベントにイベントハンドラ(コールバック関数)を登録します。

/* メインスレッド */
const worker = new Worker('worker.js');

worker.addEventListener('error', (e) => {
  console.log(e)
}, false);
・オブジェクトを転送するには

通常postMessageの引数はコピーされて相手側のスレッド(コンテキスト)に渡されますが、移譲可能オブジェクト(Transferable objects)と呼ばれるオブジェクトを作成すると、別のWorkerにオリジナルのオブジェクトを渡すことができる様です。このオブジェクトにアクセスできるのは移譲を受けたスレッド(コンテキスト)だけですが、どちら方向にも移譲できるのでバックグラウンドでの描画処理等に応用できそうです。移譲可能なオブジェクトはこちらをご覧ください。

実際にオブジェクトを移譲するには、postMessageメソッドで通常のメッセージと同様に送信すればいい様です。(Worker側でオブジェクトを作成してメイン側に送信することも可能な様です。)

/* メインスレッド (送信側) */
const worker = new Worker('worker.js', { name: 'worker' });
const buffer = new ArrayBuffer(1048576*128);

worker.postMessage(buffer); 
・その他の制限事項など

Web Worker内ではブラウザの「window」オブジェクトにアクセスしたり、DOM操作を行うことが出来ません。また、Worker同士の直接の通信もできない様です。(メインスレッドを経由すれば可能かもしれません。) ネットワーク通信 (fetch や XMLHttpRequest等)は使用できますが、応答 (promiseやresponseXML等) はNullになる様です。WebSocketやIndexedDB(ブラウザのストレージ)は使用できる様です。

勉強させていただいたサイト

この記事を作成するにあたり、以下のサイト様で勉強させていただきました。

https://illacloud.com/ja/blog/web-worker-tutorial/#%E9%80%9A%E4%BF%A1
https://qiita.com/AtsushiYamashita/items/c94f8b42e34ab48bd9c7
https://qiita.com/irico/items/4a4049fbda7bfd654498

以上です。次回はService Workerを実装していきます。