Anker Solix C300 Portable Power Station with Anker Solix PS60 Compact Portable Solar Panel 288Whポータブル電源と60Wソーラーパネルセット ぽーたぶる電源 定格出力300W 小型軽量 持ち運び便利 コンパクト パワフル 急速充電 リン酸鉄 太陽光発電セット 防災 キャンプ
¥26,970 (2025年4月25日 13:08 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)KEEPRO iPad ペンシル 2018年以降iPad対応 アップルペンシル Type-C急速充電 タッチペン ぺアリング不要 傾き感知/誤作動防止/超高感度/磁気吸着 iPad Pro 11/12.9インチ、iPad Pro 11/13インチ(M4)、iPad 6/7/8/9/10、iPad Air 3/4/5、iPad Air 11/13インチ(M2)、iPad Mini 5/6/7に対応
¥1,499 (2025年4月25日 13:08 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)PHILIPS EVNIA ゲーミングモニター (23.8インチ/180Hz/フルHD/Fast IPS/超高速0.5ms/HDR10/G-Sync Compatible対応/FPS向け/Adaptive Sync/5年保証/チルト/HDMI2.0×1、DisplayPort1.4×1 /1920x1080/フリッカーフリー/ブルーライト軽減/電源内蔵) 24M2N3200L/11
¥16,121 (2025年4月25日 13:08 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)
(ハンズオン資料なので足りない部分は口頭で捕捉します)
$ npx create cloudflare@latest
- Hello World
- Worker + Durable + Assets を選択
- TypeScript
Workers Assets を知る
(TODO 別の記事に分割したほうがいいかも)
プロキシサーバーをつくる人には興味がないかもしれませんが、Cloudflare Workers は静的アセット配信が組み込まれています。
wrangler.json
"assets": {
"binding": "ASSETS",
"directory": "./public",
// "run_worker_first": true // あとで解説
},
静的アセット(この例だと public/*
) を優先的に解決します。マッチした場合、Worker は起動しません。これは静的アセットを配信するのに有利です。
/
は public/index.html
にマッチします。
今回のテンプレートだと、次のような HTML が解決されます。
DOCTYPE html>
html lang="en">
head>
meta charset="UTF-8" />
meta name="viewport" content="width=device-width, initial-scale=1.0" />
title>Hello, World!title>
head>
body>
h1 id="heading">h1>
script>
fetch("/message")
.then((resp) => resp.text())
.then((text) => {
const h1 = document.getElementById("heading");
h1.textContent = text;
});
script>
body>
html>
読み込まれると /message
にリクエストを投げます。
run_worker_first: true
"run_worker_first": true
を設定すると、静的アセットに同名の解決可能なパスがあったとしても、必ず fetch
ハンドラーを経由します。
そのうえで、更に元のアセットにルーティングしたい場合、次のようになります。
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/message")) {
return new Response(JSON.stringify({ name: "Cloudflare" }), {
headers: { "Content-Type": "application/json" },
});
}
return env.ASSETS.fetch(request);
},
};
env.ASSETS.
を元のリソースの静的サーバーと見なして、フォールバックするイメージです。
DurableObject
Cloudflare Workers を学ぶなら、Durable Objects について知っておく必要があります。これは(おそらく)他サービスの基盤であり、これで実現できるかどうかが Cloudflare で実現可能か考える際の物差しになります。
- Durable Objects は強整合で一意な ID を持ちます
- クラススコープ内の変数が、インメモリのステートとしてシリアライズされます
先に設定した DurableObject のテンプレートなら次の設定が wrangler.json にあるはずです。
"durable_objects": {
"bindings": [
{
"class_name": "MyDurableObject",
"name": "MY_DURABLE_OBJECT"
}
]
},
import { DurableObject } from "cloudflare:workers";
export class MyDurableObject extends DurableObject {
async sayHello(name: string): Promisestring> {
return `Hello, ${name}!`;
}
}
export default {
async fetch(request, env, ctx): PromiseResponse> {
const id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName("foo");
const stub = env.MY_DURABLE_OBJECT.get(id);
const greeting = await stub.sayHello("world");
return new Response(greeting);
},
} satisfies ExportedHandlerEnv>;
これはリクエストに応じて名前空間の上の foo
の Durable Objects インスタンスを解決します。
Durable Objects は RPC になっており、サーバー上では別のインスタンスとして起動しますが、JSON や Request/Response がシリアライズされて疎通します。
Counter の実装
/message
にアクセスされるたびに値をインクリメントする Durable Objects を作ってみます。
import { DurableObject } from "cloudflare:workers";
export class MyDurableObject extends DurableObject {
async increment() {
let value: number = (await this.ctx.storage.get("value")) || 0;
const nextValue = value + 1;
await this.ctx.storage.put("value", nextValue);
return nextValue;
}
}
export default {
async fetch(request, env, ctx): PromiseResponse> {
const url = new URL(request.url);
if (url.pathname === "/message") {
const id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName("foo");
const counter = env.MY_DURABLE_OBJECT.get(id);
const current = await counter.increment();
return new Response(`Hello World ${current}`, {
headers: {
"content-type": "text/html;charset=UTF-8",
},
});
}
return new Response("error", {
status: 404,
});
},
} satisfies ExportedHandlerEnv>;
Counter with Cookie
今までは foo というキーで Worker を初期化していました。
これをユーザー個別のカウンタにしましょう。リクエストユーザーごとに cookie を読み取る、または Set-Cookie
を返して、ユーザーごとにアクセスカウンタを設定します。
import { DurableObject } from "cloudflare:workers";
export class MyDurableObject extends DurableObject {
async increment() {
let value: number = (await this.ctx.storage.get("value")) || 0;
const nextValue = value + 1;
await this.ctx.storage.put("value", nextValue);
return nextValue;
}
}
export default {
async fetch(request, env, ctx): PromiseResponse> {
const url = new URL(request.url);
if (url.pathname === "/message") {
console.log("message", request.headers.get("cookie"));
const cookie: Recordstring, string> = Object.fromEntries(
request.headers
.get("cookie")
?.split(";")
?.map((c) => c.split("=")) ?? []
);
const isNew = !cookie["uid"];
const uid = cookie["uid"] ?? Math.random().toString(36).slice(2);
const id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName(uid);
const counter = env.MY_DURABLE_OBJECT.get(id);
const current = await counter.increment();
return new Response(`${uid} ${current} ${isNew ? "(New)" : ""}`, {
headers: {
"set-cookie": `uid=${uid}; Path=/; HttpOnly`,
},
});
}
return new Response("error", {
status: 404,
});
},
} satisfies ExportedHandlerEnv>;
これだと 1 ユニークユーザーごとに Durable Objects を生成しています。1 ワーカーあたり、128MB までの状態が割り振られます。
これは無料プランだと 1 位 10 万リクエストまでです。同接が多いウェブサイトだと耐えきれなくなります。
- Free: 100,000 per day
- Standard: 10 million included per month
+$0.30 per additional million
例えば 1 日 1000 万アクセスのサイトの場合、$87/Month になります。巨大サイトなら、個人サイトならともかく、企業規模ならまあ別にって感じですね。
ケチるために UUID でシャーディングして同じインスタンスに載せてしまう方法などもあります。
Durable Objects に WebSocket から繋ぐ
WebSocket コネクションとして直接 Durable Objects にコネクションを張ります。
"durable_objects": {
"bindings": [
{
"class_name": "MyDurableObject",
"name": "MY_DURABLE_OBJECT"
},
{
"class_name": "WebSocketHibernationServer",
"name": "WEBSOCKET_HIBERNATION_SERVER",
}
]
},
こんな感じの Worker 実装を追加します。
export class WebSocketHibernationServer extends DurableObject {
async fetch(request: Request): PromiseResponse> {
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
this.ctx.acceptWebSocket(server);
return new Response(null, {
status: 101,
webSocket: client,
});
}
async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
ws.send(
`[Durable Object] message: ${message}, connections: ${
this.ctx.getWebSockets().length
}`
);
}
async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean
) {
ws.close(code, "Durable Object is closing WebSocket");
}
}
export default {
async fetch(request, env: Env, ctx): PromiseResponse> {
if (request.method === "GET" && request.url.endsWith("/websocket")) {
const upgradeHeader = request.headers.get("Upgrade");
if (!upgradeHeader || upgradeHeader !== "websocket") {
return new Response(null, {
status: 426,
statusText: "Durable Object expected Upgrade: websocket",
headers: {
"Content-Type": "text/plain",
},
});
}
let id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName("foo");
let stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id);
return stub.fetch(request);
}
},
};
Web 標準オブジェクトを生で扱ってるので少し見辛いですが、やってることは単純です。
-
GET /websocket
で Upgrade ヘッダがない場合、426
で WebSocket Upgrade リクエストを返す -
GET /websocket
に WebSokect Upgrade ヘッダ付きでがリクエストが来た場合、WebSocketHibernationServer で101
を返してコネクション確立
fetch
と WebSocketHibernationServer
はインメモリ空間は共有してませんが、 Request/Response は疎通します。そこで WebSocket のソケットインスタンスを引き回しています。
これだけです。今まで WebSocket をやったことない、知らない人にとってはそうなんだ、って感じだと思いますが、今まで自力で WebSocket をやってた人にはこれが感動ポイントです。
今まではユーザーの状態を応じてリクエストを振り分ける Sticky Session 自分で作る必要がありましたが、Durable Objects は強整合なので Durable Objects にルーティングするだけです。
クライアントから繋いでみます。public/index.html
にこれだけ追記
script>
const socket = new WebSocket("/websocket");
socket.addEventListener("message", (event) => {
console.log("Message from server ", event.data);
});
socket.addEventListener("open", (event) => {
socket.send("Hello Server!");
});
setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send("Ping");
}
}, 1000);
script>
これだけで WebSokect 通信ができます。
Sqlite in D1
Durable Objects では sqlite
のインメモリインスタンスを持つことができます。
export class MyDurableObject extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS task (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
`);
}
async addTask(name: string) {
this.ctx.storage.sql.exec(
`
INSERT INTO task (name) VALUES (?);
`,
[name]
);
}
async getTasks() {
const tasks = await this.ctx.storage.sql
.exec(`SELECT * FROM task;`)
.toArray();
return tasks.map((task: any) => ({
id: task.id,
name: task.name,
}));
}
}
これを単に CRUD として使います。
POST /tasks/new
と GET /tasks
を実装します。
if (request.method === "POST" && request.url.endsWith("/tasks/new")) {
const id = env.MY_DURABLE_OBJECT.idFromName("foo");
const stub = env.MY_DURABLE_OBJECT.get(id);
const form = await request.formData();
const name = form.get("name");
if (!name) {
return new Response("name is required", { status: 400 });
}
await stub.addTask(name as string);
return new Response("", {
status: 302,
headers: {
Location: "https://zenn.dev/",
},
});
}
if (request.method === "GET" && request.url.endsWith("/tasks")) {
const id = env.MY_DURABLE_OBJECT.idFromName("foo");
const stub = env.MY_DURABLE_OBJECT.get(id);
const tasks = await stub.getTasks();
return new Response(JSON.stringify(tasks), {
headers: {
"Content-Type": "application/json",
},
});
}
public/index.html
にこれを叩くフォームと View を実装します。
form id="form" action="/tasks/new" method="POST">
input type="text" name="name" placeholder="todo..." required />
button type="submit">Submitbutton>
form>
script type="module">
const tasks = await fetch("/tasks").then((resp) => resp.json());
const ul = document.createElement("ul");
document.body.appendChild(ul);
for (const task of tasks) {
const li = document.createElement("li");
li.textContent = task.name;
ul.appendChild(li);
}
script>
Cloudflare D1 との違いは、D1 がレプリケーションがあるのに対して、sqlite in durable_objects にはありません。