参考
※この投稿は、↑のページを理解したくて、私が勉強してみたことをまとめた内容になります
やりたいこと
WebSocketを使うとサーバーとクライアントで双方向にデータを送信できます。
クライアントはサーバーとの接続を開きっぱなしにします。
なので例えばAさんが、WebSocketと通信するページを10タブ開いた場合は、Aさんだけでサーバーと10接続することになります。
そこでSharedWorkerからWebSocketと接続することで、Aさんが10タブ開いたとしてもサーバーとの接続は1つだけにすることができます。
今回私が最終的に作成したコードはこちらです
SharedWorkerとは
SharedWorkerはWeb Workerの一種です。
Web Workerとはメイン実行スレッドとは別に、バックグラウンドスレッドでスクリプト操作を実行できます。負荷の高い処理をメインスレッドで実行したくない場合に、Web Workerで処理を行うといった用途で使います。
SharedWorkerは複数ウィンドウ、複数タブからアクセスできる共有のWorkerです。
使い方もWeb Workerとは微妙に違います。
1. WebSocketサンプル
1-1. 簡単にWebSocketと通信
Nodejsのwsで簡単にWebSocketを使ってみます。
※まだSharedWorkerは出てきません。
yarn add ws
const http = require("http");
const fs = require("fs");
const WebSocket = require("ws");
// httpサーバー作成
const server = http.createServer();
server.on("request", async (req, res) => {
try {
res.write(await fs.promises.readFile(`${__dirname}${req.url === "/" ? "/index.html" : req.url}`));
} catch(e) {
console.log(e.message)
res.writeHead(404)
}
res.end();
});
server.listen(8000);
console.log("http server listening ...");
// websockerサーバー作成
const wss = new WebSocket.Server({ port: 8001 });
wss.on("connection", ws => {
console.log("Socket connected successfully");
ws.on("message", message => {
console.log(`Received ${message}`);
for (client of wss.clients) {
client.send(`${message} from server!`);
client.send(`現在のクライアントとの接続数 : ${[...wss.clients].length}`);
}
});
ws.on("close", () => {
console.log("I lost a client");
});
});
<!DOCTYPE html>
<meta charset="UTF-8">
<title>Document</title>
<script src="client.js" defer></script>
<input type="text" id="text" value="hello">
<input type="button" id="button" value="送信">
const ws = new WebSocket("ws://localhost:8001");
ws.addEventListener("open", e => {
console.log("Socket Opne");
});
// サーバーからデータを受け取る
ws.addEventListener("message", e => {
console.log(e.data);
});
// サーバーにデータを送る
button.addEventListener("click", e => {
ws.send(text.value);
});
$ node server.js # サーバー起動
http server listening ...
この状態で http://localhost:8000 を開いて、送信ボタンを押すと、WebSocketと通信できました。
3つのウィンドウから開いた状態だと、WebSocketと接続している数が3つになることも確認できました。
1-2. SharedWorkerからWebSocketと通信
次に、
メインスレッドのjs -> SharedWorker -> サーバー
とデータ送信する動作と、
サーバー -> SharedWorker -> メインスレッドのjs
とデータ送信する動作に変更します。
client.js
を変更し、新しくworker.js
を作成します。
const worker = new SharedWorker("worker.js")
worker.port.start();
button.addEventListener("click", e => {
console.log(`メインスクリプトからSharedWorkerにデータ送信 「${text.value}」`)
worker.port.postMessage(text.value)
});
worker.port.addEventListener("message", e => {
console.log(`メインスクリプトでSharedWorkerからのデータ受信 「${e.data}」`)
});
const ws = new WebSocket("ws://localhost:8001");
ws.addEventListener("open", e => {
console.log("Socket Opne");
});
// サーバーからデータを受け取る
ws.addEventListener("message", e => {
console.log(`SharedWorkerでサーバーからデータ受信し、メインスクリプトへデータ送信 : 「${e.data}」`)
// 開いている全ての複数ウィンドウ、複数タブへメッセージ送信
for (const connection of connections) {
connection.postMessage(e.data);
}
});
let connections = [];
self.addEventListener("connect", e => {
const port = e.ports[0];
// タブやウィンドウを開くたびに接続を配列に保存します。
// ※しかし、これだとウィンドウを閉じても配列に残り続けてしまったり、
// リロードすると前の接続が残ったまま新しい接続が追加されてしまうといった問題があります。
// Broadcast Channel API を使うと簡単に解決できるのでそちらを使った方が良いと思います。
// Broadcast Channel API を使ったサンプルは次に説明します。
connections.push(port);
port.addEventListener("message", function (e) {
console.log(`SharedWorkerでメインスクリプトからデータ受信し、サーバーへデータ送信 : 「${e.data}」`)
ws.send(e.data);
});
port.start(); // addEventListenerを使用する場合に必要、onmessageを使う場合は暗黙的に呼び出される
})
SharedWorkerからWebSockerに接続することで、複数のウィンドウを開いている場合でも、WebSocketとの接続は1つだけにすることができました。
※SharedWorker内でconsole.log
を使用した場合は、普通に開発者ツールのコンソールには出てきません。
chrome://inspect/#workers
を開き、そこから開けるコンソールに表示されます。
1-3. Broadcast Channel API を使ってSharedWorkerからメインスクリプトへデータを送信するようにする
先ほどの例ではタブやウィンドウを開くたびに、connections
という配列にe.ports[0]
が追加されていくようにしたのですが、
ブラウザを閉じても配列に残ったままになってしまう、
リロードした場合も、前の接続が残ったまま新しい接続が追加されてしまうといった問題があります。
// ...
let connections = [];
self.addEventListener("connect", e => {
const port = e.ports[0];
connections.push(port);
// ...
SharedWorkerからBroadcast Channel API を使ってメインスクリプトへデータを送信するのが良いようです。
Broadcast Channel API - Web APIs | MDN
BroadcastChannel
を使うことで、開いている全てのタブ、ウィンドウ、iframeにデータ送信できます。
※実際にデータを送信したウィンドウ(またはタブなど)では受信しません。自分以外の全てでデータを受信できます。
client.js
とworker.js
をBroadcastChannel
を使うように変更します。
+ const bc = new BroadcastChannel("WebSocketChannel");
const worker = new SharedWorker("worker.js")
worker.port.start();
button.addEventListener("click", e => {
console.log(`メインスクリプトからSharedWorkerにデータ送信 「${text.value}」`)
worker.port.postMessage(text.value)
});
+ bc.addEventListener("message", e => {
- worker.port.addEventListener("message", e => {
console.log(`メインスクリプトでSharedWorkerからのデータ受信 「${e.data}」`)
});
+ const bc = new BroadcastChannel("WebSocketChannel");
const ws = new WebSocket("ws://localhost:8001");
ws.addEventListener("open", e => {
console.log("Socket Opne");
});
// サーバーからデータを受け取る
ws.addEventListener("message", e => {
console.log(`SharedWorkerでサーバーからデータ受信し、メインスクリプトへデータ送信 : 「${e.data}」`)
// 開いている全ての複数ウィンドウ、複数タブへメッセージ送信
- for (const connection of connections) {
- connection.postMessage(e.data);
- }
+ bc.postMessage(e.data);
});
- let connections = [];
self.addEventListener("connect", e => {
const port = e.ports[0];
- connections.push(port);
port.addEventListener("message", function (e) {
console.log(`SharedWorkerでメインスクリプトからデータ受信し、サーバーへデータ送信 : 「${e.data}」`)
ws.send(e.data);
});
port.start(); // addEventListenerを使用する場合に必要、onmessageを使う場合は暗黙的に呼び出される
})
こちらでも同じように動作することができました。
2. SSE (Server-Sent Event) でもやってみた
const http = require("http");
const fs = require("fs");
// httpサーバー作成
const server = http.createServer();
server.on("request", async (req, res) => {
if (req.url === "/events") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
});
setInterval(() => {
res.write(`data: ${JSON.stringify({ time: new Date().toLocaleTimeString() })}\n\n`)
res.flushHeaders();
}, 1000)
} else {
try {
res.write(await fs.promises.readFile(`${__dirname}${req.url === "/" ? "/index.html" : req.url}`));
} catch(e) {
console.log(e.message)
res.writeHead(404)
}
res.end();
}
});
server.listen(8000);
console.log("http server listening ...");
<!DOCTYPE html>
<meta charset="UTF-8">
<title>Document</title>
<script src="event.js" defer></script>
<ul id="sample"></ul>
const bc = new BroadcastChannel("WebSocketChannel");
const worker = new SharedWorker("worker.js")
worker.port.start();
bc.addEventListener("message", e => {
sample.appendChild(document.createElement("li")).textContent = e.data;
});
const bc = new BroadcastChannel("WebSocketChannel");
const es = new EventSource("./events");
es.addEventListener("message", e => {
const { time } = JSON.parse(e.data);
bc.postMessage(`${time} from SharedWorker!`);
});
SSEでもSharedWorkerから接続して、複数タブを開いても接続は1つだけにすることができました。
最後まで読んでいただいてありがとうございました。