この記事何?
エンジニア歴1年半。業務で利用しているHonoが大好きだが、TSもプログラミング知識も弱々すぎて上手く使いこなせない。速いはずが多分生かせてない。
ちゃんと理解するためにソースコードリーディングしたい。
でも目的なくソースコード眺めても意味ないし……ねむい……
そうだ!PRを全部読めば、変遷やなぜ変更されているかストーリー的に分かるのでは。
と思ったのでチャレンジしてみる。PR2000以上あるので、全部できるかは知らん。
https://github.com/honojs/hono
#76 feat: serve static middleware (Cloudflare only)
静的ファイル配信のミドルウェアを追加。
src/middleware/serve-static/serve-static.ts:
import type { Context } from '../../context'
import { getContentFromKVAsset } from '../../utils/cloudflare'
import { getMimeType } from '../../utils/mime'
type Options = {
root: string
}
const DEFAULT_DOCUMENT = 'index.html'
// Cloudflare Workers 専用
export const serveStatic = (opt: Options = { root: '' }) => {
return async (c: Context, next: Function) => {
await next() // ← まず他のルートを試す
const url = new URL(c.req.url)
const path = getKVPath(url.pathname, opt.root)
// KV Sites からファイルを取得
const content = await getContentFromKVAsset(path)
if (content) {
// Content-Type を自動判定
const mimeType = getMimeType(path)
if (mimeType) {
c.header('Content-Type', mimeType)
}
c.res = c.body(content)
} else {
// ファイルが見つからない場合は何もしない
// console.debug(`Static file: ${path} is not found`)
}
}
}
// パスを正規化する関数
const getKVPath = (filename: string, root: string): string => {
if (filename.endsWith('/')) {
// /top/ => /top/index.html
filename = filename.concat(DEFAULT_DOCUMENT)
} else if (!getMimeType(filename)) {
// /top => /top/index.html(拡張子がない場合)
filename = filename.concat('/' + DEFAULT_DOCUMENT)
}
// /foo.html => foo.html(先頭のスラッシュを削除)
filename = filename.replace(/^\//, '')
// assets/ => assets(末尾のスラッシュを削除)
root = root.replace(/\/$/, '')
// ./assets/foo.html => assets/foo.html
let path = root + '/' + filename
path = path.replace(/^\.?\//, '')
return path
}
パス変換の例:
// 例1: ディレクトリアクセス
getKVPath('/blog/', 'assets')
// → 'assets/blog/index.html'
// 例2: 拡張子なし
getKVPath('/about', 'static')
// → 'static/about/index.html'
// 例3: ファイル指定
getKVPath('/style.css', 'public')
// → 'public/style.css'
- MIME Type判定の実装
src/utils/mime.ts:
export const getMimeType = (filename: string): string => {
const regexp = /\.([a-zA-Z0-9]+?)$/
const match = filename.match(regexp)
if (!match) {
return
}
let mimeType = mimes[match[1]]
// テキストファイルには charset を追加
if (mimeType.startsWith('text') || mimeType === 'application/json') {
mimeType += '; charset=utf-8'
}
return mimeType
}
const mimes: { [extension: string]: string } = {
html: 'text/html',
css: 'text/css',
js: 'text/javascript',
json: 'application/json',
png: 'image/png',
jpg: 'image/jpeg',
gif: 'image/gif',
svg: 'image/svg+xml',
ico: 'image/vnd.microsoft.icon',
txt: 'text/plain',
pdf: 'application/pdf',
// ... 89種類の拡張子に対応
}
動作例:
getMimeType('index.html')
// → 'text/html; charset=utf-8'
getMimeType('style.css')
// → 'text/css; charset=utf-8'
getMimeType('image.png')
// → 'image/png'
getMimeType('data.json')
// → 'application/json; charset=utf-8'
使い方
import { Hono } from 'hono'
import { serveStatic } from 'hono/middleware'
const app = new Hono()
// 静的ファイル配信ミドルウェアを適用
app.use('/static/*', serveStatic({ root: './assets' }))
// 動的ルート
app.get('/api/hello', (c) => c.json({ message: 'Hello' }))
export default app
むずい
#77 fix: mustache template encoding
Mustache テンプレートのエンコーディング問題を修正.
KV Sites から取得したテンプレートが ArrayBuffer 型になることがある。
const content = await getContentFromKVAsset('template.mustache')
// content = ArrayBuffer { ... } ← 文字列じゃない
Mustache.render(content, data) // ← エラー!
// ArrayBuffer の場合
const buffer = new ArrayBuffer(10)
bufferToString(buffer)
// → TextDecoder で UTF-8 文字列に変換
// すでに文字列の場合
const str = "Hello"
bufferToString(str)
// → そのまま返す
#78 fix: about parsedBody on Request
Request.parsedBody の型定義をグローバルに移動。重複した型定義を削除。
#79 example: fix blog example
Blog サンプルの修正。ビルド設定とテストコードを改善。