この記事何?
エンジニア歴1年半。業務で利用しているHonoが大好きだが、TSもプログラミング知識も弱々すぎて上手く使いこなせない。速いはずが多分生かせてない。
ちゃんと理解するためにソースコードリーディングしたい。
でも目的なくソースコード眺めても意味ないし……ねむい……
そうだ!PRを全部読めば、変遷やなぜ変更されているかストーリー的に分かるのでは。
と思ったのでチャレンジしてみる。PR2000以上あるので、全部できるかは知らん。
https://github.com/honojs/hono
#56 なし
#57 feat: add content-length header
55で削除したContent-Lengthヘッダーを改善版として再追加。
遅いのをどうやって修正したんだろう。
// src/middleware/default.ts
if (c.res.body) {
// await c.res.clone().arrayBuffer()だと遅すぎたのでclone()を使わないように修正されている
const buffer = await c.res.arrayBuffer()
const res = new Response(buffer, {
status: c.res.status,
statusText: c.res.statusText,
headers: c.res.headers,
})
res.headers.append('Content-Length', buffer.byteLength.toString())
c.res = res
}
改善点:
- clone() を使わないため、パフォーマンスが向上
- arrayBuffer() を1回だけ呼ぶ
clone() は Request インターフェイスのメソッドで、現在の Request オブジェクトのコピーを生成します。
ArrayBuffer オブジェクトは、一般的な生のバイナリーデータバッファーを表現するために使用します。
これはバイトの配列で、他の言語ではよく「バイト配列」と呼ばれます。ArrayBuffer の内容を直接操作することはできません。代わりに、バッファーを特定の形式で表現する型付き配列オブジェクトまたは DataView オブジェクトのいずれかを作成して、バッファーの内容を読み書きします。
clone() を使う場合、内部で ReadableStream.tee() が実行される。これが重いぽい。
ReadableStream インターフェイスの tee() メソッドは、現在の読み取り可能なストリームを分配し、結果の 2 つの分岐を新しい ReadableStream インスタンスとして含む 2 つの要素の配列を返します。
これは、2 つのリーダーがストリームを同時に、おそらく異なる速度で読み取ることができるようにする場合に便利です。 例えば、サーバーからレスポンスを取得してブラウザーにストリーム入力したいが、サービスワーカーのキャッシュにもストリーム入力したい場合は、サービスワーカーでこれを行うことができます。レスポンスの本体を複数回使用することはできないため、これを行うには 2 つのコピーが必要です。
Response { body: ReadableStream }
↓
res.clone()
↓
[ReadableStream.tee()] ← ここが重い!
/ \
↓ ↓
stream1 stream2
↓ ↓
元のres 複製したres
↓
arrayBuffer()
↓
ArrayBuffer
clone() を使わない場合
Response { body: ReadableStream }
↓
arrayBuffer()
↓
ArrayBuffer
↓
new Response(buffer) ← 同じArrayBufferを再利用
ピエー、こんな1行なのに。むずい。
#58 feat(content-length): add Content-Length in log output
ロガーミドルウェアにContent-Lengthを表示機能を追加。
metrueさん。
// src/middleware/logger/logger.ts
export const logger = (fn = console.log) => {
return async (c: Context, next: Function) => {
const { method, url } = c.req
const start = Date.now()
await next()
// Content-Lengthを取得
const contentLength = c.res.headers.get('Content-Length')
let cl = ''
if (contentLength) {
const num = parseInt(contentLength)
if (num < 1024) {
cl = ` ${num}b` // バイト
} else {
cl = ` ${(num / 1024).toFixed(1)}kB` // キロバイト
}
}
const end = Date.now()
fn(`${method} ${url} ${c.res.status}${cl} ${end - start}ms`)
}
}
出力例:
GET /api/users 200 1.5kB 45ms
POST /api/posts 201 245b 123ms
#59 refactor(utils): better management for utils modules to avoid kitchen sink issue
utilsモジュールの整理 - "kitchen sink issue"を回避。
kitchen sink issueって何??
- 何でもかんでも1つのファイルに詰め込むこと
ぽい
// 変更前
src/
util.ts ← すべてのユーティリティ関数が1ファイルに
// 変更後
src/utils/
url.ts ← URL関連のユーティリティ
buffer.ts ← Buffer関連のユーティリティ
#60 feat: Use KV for blog example
Blog サンプルで Cloudflare KV を使用するように変更。
Cloudflare KVとは
https://www.cloudflare.com/ja-jp/developer-platform/products/workers-kv/
Cloudflareのアプリケーション向けサーバーレス Key-Value ストレージ
// 基本的な使い方
await env.MY_KV.put('key', 'value') // 保存
const value = await env.MY_KV.get('key') // 取得
await env.MY_KV.delete('key') // 削除
変更前(メモリ内保存)
// src/model.ts
const posts: { [key: string]: Post } = {} // ← メモリ内
export const getPosts = (): Post[] => {
return Object.values(posts)
}
変更後(KV使用)
// src/model.ts
export const getPosts = async (kv: KVNamespace): Promise<Post[]> => {
const list = await kv.list()
const posts: Post[] = []
for (const key of list.keys) {
const post = await kv.get(key.name, 'json')
if (post) posts.push(post)
}
return posts
}
export const createPost = async (kv: KVNamespace, param: Param): Promise<Post> => {
const id = crypto.randomUUID()
const newPost: Post = { id, title: param.title, body: param.body }
await kv.put(id, JSON.stringify(newPost))
return newPost
}
- データが永続化される
- 複数インスタンス間で共有可能