Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions 2026/board-advanced/completed/public/extra.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
1 change: 0 additions & 1 deletion 2026/board-advanced/completed/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ <h1>みんなの掲示板</h1>

<div class="list-header">
<h2>みんなの投稿</h2>
<span id="count"></span>
<span id="status" class="status">—</span>
<button id="reload-btn">更新する</button>
</div>
Expand Down
10 changes: 0 additions & 10 deletions 2026/board-advanced/completed/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);

Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions 2026/board-advanced/examples/snapshots/ch1.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions 2026/board-advanced/examples/snapshots/ch2.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
109 changes: 31 additions & 78 deletions 2026/board-advanced/slide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 のページごと再読み込みしてください。書いたコードは消えません。

---


Expand Down Expand Up @@ -693,6 +697,7 @@ setInterval(showPosts, 10000);
<div class="timer" data-seconds="60"></div>

10 秒ごとに `showPosts()` を実行して、投稿一覧を読み直します。
`public/script.js` の一番下に1行書き足します。

```javascript {data-file=public/script.js}
document.getElementById('reload-btn').addEventListener('click', showPosts);
Expand Down Expand Up @@ -962,6 +967,7 @@ source.onmessage = handleMessage; // 関数を登録する。() は付けな
<div class="timer" data-seconds="180"></div>

一定間隔の自動更新をやめて、サーバーからの通知に切り替えます。
ファイルの一番下にある `setInterval` の行を消して、そこに書きます。

```javascript {data-file=public/script.js}
document.getElementById('reload-btn').addEventListener('click', showPosts);
Expand Down Expand Up @@ -1167,6 +1173,7 @@ source.@@onmessage@@ = showPosts;
<div class="timer" data-seconds="300"></div>

```javascript {data-file=public/script.js}
// ▼ 3章: ここに posts と loadPosts を追加する
@@const posts = [];@@ // ブラウザ側で持つ投稿。画面と同じ並び

@@async function loadPosts() {@@ // 開いたときに1回だけ呼ぶ
Expand Down Expand Up @@ -1210,6 +1217,8 @@ const source = new EventSource(`${API}/events`); // すでにある行

**成功**: ページを再読み込みすると、一覧が出る

この時点では、投稿しても再読み込みするまで一覧に出ません。

---

## 3-2. 届いた1件は `e.data` に文字列で入っている
Expand All @@ -1236,8 +1245,10 @@ const source = new EventSource(`${API}/events`); // すでにある行
<div class="timer" data-seconds="180"></div>

`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));@@ // 届いた文字列をオブジェクトに戻して追加する
Expand Down Expand Up @@ -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();%%
}
```
Expand Down Expand Up @@ -1357,7 +1370,7 @@ const source = new EventSource(`${API}/events`); // すでにある行

<div class="timer" data-seconds="120"></div>

共有サーバーの URL が入っている `API` を書き換えます。
ファイルの先頭にある、共有サーバーの URL が入っている `API` を書き換えます。

```javascript {data-file=public/script.js}
%%const API = 'https://example.deno.net';%% // 共有サーバーの URL が入っている
Expand Down Expand Up @@ -1474,7 +1487,8 @@ res.setHeader('Cache-Control', 'no-cache');
<div class="timer" data-seconds="360"></div>

```javascript {data-file=server.js}
const posts = []; // すでにある行
// 投稿の置き場。再起動すると空に戻る。
const posts = []; // すでにある行。ファイルの上のほう
@@let connection = null;@@ // 現在の接続。新しい接続が来ると上書きされる
```

Expand Down Expand Up @@ -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);@@
Expand All @@ -1640,7 +1656,7 @@ data: {"name":"たろう","text":"やっほー"}

<div class="timer" data-seconds="180"></div>

1本に書いていたところを、配列ぶん繰り返します。
`POST /posts` の中で1本に書いていたところを、配列ぶん繰り返します。

```javascript {data-file=server.js}
const data = JSON.stringify(post); // 変わらない行
Expand Down Expand Up @@ -1694,39 +1710,28 @@ data: {"name":"たろう","text":"やっほー"}

## 5-3. 切れた接続を消す

<div class="timer" data-seconds="240"></div>
<div class="timer" data-seconds="180"></div>

```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 を呼ぶ
```

| 書き方 | 意味 |
|---|---|
| `<配列>.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減ります。

---

Expand All @@ -1736,7 +1741,6 @@ data: {"name":"たろう","text":"やっほー"}

- 接続を配列 `connections` に持ち、投稿を全員に書き込むようにした
- 接続が切れたら、`close` イベントで `connections` から抜くようにした
- ターミナルの接続数が、開いているページの数と同じになる (StackBlitz のプレビューも1つと数える)

早く終わった人は → [5章の応用課題](#adv-ch5) {.jump #ch5-end}

Expand Down Expand Up @@ -1765,7 +1769,7 @@ data: {"name":"たろう","text":"やっほー"}
@@const API = 'https://example.deno.net';@@
```

共有サーバーの URL を入れ直します。
ファイルの先頭にある `API` に、共有サーバーの URL を入れ直します。

**成功**: ページを再読み込みすると他の人の投稿が表示される

Expand Down Expand Up @@ -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)` で登録する

---

Expand Down Expand Up @@ -2207,56 +2210,6 @@ const connections = []; // すでにある行
- `readPost` は `text` がないときも `''` にするので、`''` と比べれば足りる
- `sendJson` はステータスが `200` で決まっているので、`res.writeHead` で直接書く

---

<!-- _class: extra -->

## 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();@@
}
```

---

<!-- _class: extra -->

## 5章の応用課題の答え3: 接続数を画面に出す

[← 5章のふりかえりに戻る](#ch5-end) {.jump}

```html {data-file=public/index.html}
<h2>みんなの投稿</h2> <!-- すでにある行 -->
@@ <span id="count"></span>@@
```

```javascript {data-file=public/extra.js}
@@function showCount(e) {@@
@@ document.getElementById('count').textContent = `${e.data}人が接続中`;@@
@@}@@
@@source.addEventListener('count', showCount);@@
```

- `event:` の行を付けたデータは `onmessage` には届かず、同じ名前で登録した関数だけが呼ばれる

<script>
document.querySelectorAll('.timer[data-seconds]').forEach(el => {
if (el.closest('.timer-box')) return;
Expand Down
2 changes: 2 additions & 0 deletions 2026/board-advanced/template/public/script.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const API = 'https://example.deno.net';

// ▼ 3章: ここに posts と loadPosts を追加する

async function showPosts() {
const res = await fetch(`${API}/posts`);
const posts = await res.json();
Expand Down
Loading