diff --git a/2026/board-advanced/completed/public/extra.js b/2026/board-advanced/completed/public/extra.js index 241099b9..8bad32a5 100644 --- a/2026/board-advanced/completed/public/extra.js +++ b/2026/board-advanced/completed/public/extra.js @@ -46,10 +46,3 @@ function showOffline() { source.addEventListener('open', showOnline); source.addEventListener('error', showOffline); - -// 5章の応用課題3: 接続数を画面に出す -function showCount(e) { - document.getElementById('count').textContent = `${e.data}人が接続中`; -} - -source.addEventListener('count', showCount); diff --git a/2026/board-advanced/completed/public/index.html b/2026/board-advanced/completed/public/index.html index e7cf62d5..3cba6187 100644 --- a/2026/board-advanced/completed/public/index.html +++ b/2026/board-advanced/completed/public/index.html @@ -18,7 +18,6 @@

みんなの掲示板

みんなの投稿

- —
diff --git a/2026/board-advanced/completed/server.js b/2026/board-advanced/completed/server.js index 08c33caf..ddbe9903 100644 --- a/2026/board-advanced/completed/server.js +++ b/2026/board-advanced/completed/server.js @@ -25,13 +25,6 @@ function sendPing() { } setInterval(sendPing, 15000); -// いまの接続数を、接続している全員に送る -function sendCount() { - for (let i = 0; i < connections.length; i++) { - connections[i].write(`event: count\ndata: ${connections.length}\n\n`); - } -} - const server = createServer(async (req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); @@ -48,12 +41,9 @@ const server = createServer(async (req, res) => { res.flushHeaders(); connections.push(res); console.log(`接続数: ${connections.length}`); - sendCount(); - function removeConnection() { connections.splice(connections.indexOf(res), 1); console.log(`接続数: ${connections.length}`); - sendCount(); } req.on('close', removeConnection); return; diff --git a/2026/board-advanced/examples/snapshots/ch1.js b/2026/board-advanced/examples/snapshots/ch1.js index 1c42a441..cc7ca044 100644 --- a/2026/board-advanced/examples/snapshots/ch1.js +++ b/2026/board-advanced/examples/snapshots/ch1.js @@ -3,6 +3,8 @@ const API = 'https://example.deno.net'; +// ▼ 3章: ここに posts と loadPosts を追加する + async function showPosts() { const res = await fetch(`${API}/posts`); const posts = await res.json(); diff --git a/2026/board-advanced/examples/snapshots/ch2.js b/2026/board-advanced/examples/snapshots/ch2.js index df08b806..2b5295ce 100644 --- a/2026/board-advanced/examples/snapshots/ch2.js +++ b/2026/board-advanced/examples/snapshots/ch2.js @@ -3,6 +3,8 @@ const API = 'https://example.deno.net'; +// ▼ 3章: ここに posts と loadPosts を追加する + async function showPosts() { const res = await fetch(`${API}/posts`); const posts = await res.json(); diff --git a/2026/board-advanced/slide.md b/2026/board-advanced/slide.md index 67afa9f1..061f695a 100644 --- a/2026/board-advanced/slide.md +++ b/2026/board-advanced/slide.md @@ -468,18 +468,22 @@ style: | 手を動かしてもらうスライドには、右上に 記述 のバッジが出ます。 黄色い行だけを書き、赤く取り消された行は消します。 -`______` は空欄です。前の説明のスライドを見て、自分で埋めます。 +`______` は空欄で、前の説明のスライドを見て自分で埋めます。`// …` は省いた行です。 ```javascript {data-file=public/script.js} -const posts = []; // 色なし: すでにある行。この下に書く +const posts = []; // 色なし: すでにある行 (書く場所の目印) @@let connection = null;@@ // 黄色: 書いてもらう行 @@const max = ______;@@ // 空欄: 自分で埋める %%setInterval(showPosts, 10000);%% // 赤: 消してもらう行 +showPosts(); // 色なし: 目印が書く行の下に来ることもある ``` 早く終わった人は、章の最後のスライドの左上にあるリンクから応用課題に進んでください。 書きかけのコードがエラーで止まっても本編はそのまま動くので、次の章が始まったら途中のまま戻ってきて大丈夫です。 +プレビューが「Unable to connect to port 3000」の画面になったら、コードを書き終えてから、その画面の Reload を押してください。 +それでも戻らないときは、ブラウザで StackBlitz のページごと再読み込みしてください。書いたコードは消えません。 + --- @@ -693,6 +697,7 @@ setInterval(showPosts, 10000);
10 秒ごとに `showPosts()` を実行して、投稿一覧を読み直します。 +`public/script.js` の一番下に1行書き足します。 ```javascript {data-file=public/script.js} document.getElementById('reload-btn').addEventListener('click', showPosts); @@ -962,6 +967,7 @@ source.onmessage = handleMessage; // 関数を登録する。() は付けな
一定間隔の自動更新をやめて、サーバーからの通知に切り替えます。 +ファイルの一番下にある `setInterval` の行を消して、そこに書きます。 ```javascript {data-file=public/script.js} document.getElementById('reload-btn').addEventListener('click', showPosts); @@ -1167,6 +1173,7 @@ source.@@onmessage@@ = showPosts;
```javascript {data-file=public/script.js} +// ▼ 3章: ここに posts と loadPosts を追加する @@const posts = [];@@ // ブラウザ側で持つ投稿。画面と同じ並び @@async function loadPosts() {@@ // 開いたときに1回だけ呼ぶ @@ -1210,6 +1217,8 @@ const source = new EventSource(`${API}/events`); // すでにある行 **成功**: ページを再読み込みすると、一覧が出る +この時点では、投稿しても再読み込みするまで一覧に出ません。 + --- ## 3-2. 届いた1件は `e.data` に文字列で入っている @@ -1236,8 +1245,10 @@ const source = new EventSource(`${API}/events`); // すでにある行
`posts` に追加してから `showPosts` を呼ぶと、その1件が一覧に表示されます。 +ファイルの一番下にある `source.onmessage` の行を置き換えます。 ```javascript {data-file=public/script.js} +const source = new EventSource(`${API}/events`); // すでにある行 %%source.onmessage = showPosts;%% @@function receivePost(e) {@@ @@ posts.push(JSON.parse(e.data));@@ // 届いた文字列をオブジェクトに戻して追加する @@ -1297,7 +1308,9 @@ const source = new EventSource(`${API}/events`); // すでにある行 投稿した1件は `receivePost` が一覧に出すので、この `showPosts` は消します。 ```javascript {data-file=public/script.js} - document.getElementById('text-input').value = ''; // ここから下 +async function addPost() { + // … + document.getElementById('text-input').value = ''; %% showPosts();%% } ``` @@ -1357,7 +1370,7 @@ const source = new EventSource(`${API}/events`); // すでにある行
-共有サーバーの URL が入っている `API` を書き換えます。 +ファイルの先頭にある、共有サーバーの URL が入っている `API` を書き換えます。 ```javascript {data-file=public/script.js} %%const API = 'https://example.deno.net';%% // 共有サーバーの URL が入っている @@ -1474,7 +1487,8 @@ res.setHeader('Cache-Control', 'no-cache');
```javascript {data-file=server.js} -const posts = []; // すでにある行 +// 投稿の置き場。再起動すると空に戻る。 +const posts = []; // すでにある行。ファイルの上のほう @@let connection = null;@@ // 現在の接続。新しい接続が来ると上書きされる ``` @@ -1618,6 +1632,8 @@ data: {"name":"たろう","text":"やっほー"} ``` ```javascript {data-file=server.js} + if (req.method === 'GET' && url.pathname === '/events') { + // … %% connection = res;%% %% console.log('接続を受け付けた');%% @@ connections.push(res);@@ @@ -1640,7 +1656,7 @@ data: {"name":"たろう","text":"やっほー"}
-1本に書いていたところを、配列ぶん繰り返します。 +`POST /posts` の中で1本に書いていたところを、配列ぶん繰り返します。 ```javascript {data-file=server.js} const data = JSON.stringify(post); // 変わらない行 @@ -1694,15 +1710,18 @@ data: {"name":"たろう","text":"やっほー"} ## 5-3. 切れた接続を消す -
+
```javascript {data-file=server.js} - console.log(`接続数: ${connections.length}`); // すでにある行 + if (req.method === 'GET' && url.pathname === '/events') { + // … + connections.push(res); + console.log(`接続数: ${connections.length}`); @@ function removeConnection() {@@ @@ connections.splice(connections.indexOf(res), 1);@@ @@ console.log(`接続数: ${connections.length}`);@@ @@ }@@ -@@ req.on(______);@@ // 接続が切れたら removeConnection を呼ぶ +@@ req.on('close', removeConnection);@@ // 接続が切れたら removeConnection を呼ぶ ``` | 書き方 | 意味 | @@ -1710,23 +1729,9 @@ data: {"name":"たろう","text":"やっほー"} | `<配列>.indexOf(<値>)` | <値>が<配列>の何番目にあるかを返す。先頭は `0` | | `<配列>.splice(<位置>, <個数>)` | <配列>の<位置>から、<個数>ぶんの要素を抜く | -**成功**: タブを閉じると、ターミナルの接続数が1減る - ---- - -## 5-3. 答え - -```javascript {data-file=server.js} - console.log(`接続数: ${connections.length}`); // すでにある行 - function removeConnection() { - connections.splice(connections.indexOf(res), 1); - console.log(`接続数: ${connections.length}`); - } - req.on(@@'close', removeConnection@@); -``` +**成功**: `server.js` を保存したあと、ターミナルにエラーが出ない -- 1つ目はイベントの名前 `'close'`、2つ目は切れたときに呼ぶ関数 `removeConnection` -- 関数は `()` を付けずに渡す +StackBlitz の中では、タブを閉じても切断がサーバーに伝わらないので、接続数は減りません。ふつうの Node で動かすと、閉じたときに1減ります。 --- @@ -1736,7 +1741,6 @@ data: {"name":"たろう","text":"やっほー"} - 接続を配列 `connections` に持ち、投稿を全員に書き込むようにした - 接続が切れたら、`close` イベントで `connections` から抜くようにした -- ターミナルの接続数が、開いているページの数と同じになる (StackBlitz のプレビューも1つと数える) 早く終わった人は → [5章の応用課題](#adv-ch5) {.jump #ch5-end} @@ -1765,7 +1769,7 @@ data: {"name":"たろう","text":"やっほー"} @@const API = 'https://example.deno.net';@@ ``` -共有サーバーの URL を入れ直します。 +ファイルの先頭にある `API` に、共有サーバーの URL を入れ直します。 **成功**: ページを再読み込みすると他の人の投稿が表示される @@ -2164,7 +2168,6 @@ data: {"name":"たろう","text":"やっほー"} 1. ハートビートを送る: 15秒ごとに、接続している全員へコメント行 `: ping\n\n` を書く関数 `sendPing` を作る。データが流れない時間が続くと、プロキシなどが接続を切ることがある ([付録: SSE のコメント行](#comment-line)) 2. 空の投稿を受け付けない: `text` が空なら保存せず、ステータス `400` と `{"message": "..."}` を返す。Network タブで、空の投稿の `posts` の行が 400 になれば成功 -3. 接続数を全員に送る: 接続したときと切れたときに、`event: count\ndata: 3\n\n` を全員に書く関数 `sendCount` を呼ぶ。ブラウザ側は、受け取って画面に出す関数 `showCount` を `source.addEventListener('count', showCount)` で登録する --- @@ -2207,56 +2210,6 @@ const connections = []; // すでにある行 - `readPost` は `text` がないときも `''` にするので、`''` と比べれば足りる - `sendJson` はステータスが `200` で決まっているので、`res.writeHead` で直接書く ---- - - - -## 5章の応用課題の答え3: 接続数を全員に送る - -[← 5章のふりかえりに戻る](#ch5-end) {.jump} - -```javascript {data-file=server.js} -const connections = []; // すでにある行 -@@function sendCount() {@@ -@@ for (let i = 0; i < connections.length; i++) {@@ -@@ connections[i].write(`event: count\ndata: ${connections.length}\n\n`);@@ -@@ }@@ -@@}@@ -``` - -```javascript {data-file=server.js} - console.log(`接続数: ${connections.length}`); // すでにある行 -@@ sendCount();@@ - - function removeConnection() { - connections.splice(connections.indexOf(res), 1); - console.log(`接続数: ${connections.length}`); -@@ sendCount();@@ - } -``` - ---- - - - -## 5章の応用課題の答え3: 接続数を画面に出す - -[← 5章のふりかえりに戻る](#ch5-end) {.jump} - -```html {data-file=public/index.html} -

みんなの投稿

-@@ @@ -``` - -```javascript {data-file=public/extra.js} -@@function showCount(e) {@@ -@@ document.getElementById('count').textContent = `${e.data}人が接続中`;@@ -@@}@@ -@@source.addEventListener('count', showCount);@@ -``` - -- `event:` の行を付けたデータは `onmessage` には届かず、同じ名前で登録した関数だけが呼ばれる -