この記事何?
エンジニア歴1年半。業務で利用しているHonoが大好きだが、TSもプログラミング知識も弱々すぎて上手く使いこなせない。速いはずが多分生かせてない。
ちゃんと理解するためにソースコードリーディングしたい。
でも目的なくソースコード眺めても意味ないし……ねむい……
そうだ!PRを全部読めば、変遷やなぜ変更されているかストーリー的に分かるのでは。
と思ったのでチャレンジしてみる。PR2000以上あるので、全部できるかは知らん。
https://github.com/honojs/hono
#111 feat: app.onError for error handling
// 変更前: エラー処理が面倒だった
app.use('*', async (c, next) => {
try {
await next()
} catch (err) {
// 毎回try-catchを書く必要がある
c.res = c.text('Custom Error Message', { status: 500 })
}
})
// 変更後: Contextを受け取るようになった
onError(err: Error, c: Context) {
console.error(`${err.message}`)
const message = 'Internal Server Error'
return c.text(message, 500) // ← c.text() が使える!
}
// ユーザーが簡単にカスタマイズできる
app.onError = (err, c) => {
// デバッグ用にエラーメッセージをヘッダーに追加
c.header('X-Error-Message', err.message)
// 本番ではユーザーに見せたくない詳細を隠す
return c.json({
error: 'Something went wrong',
// err.stack は送らない(セキュリティ)
}, 500)
}
使い方の例
const app = new Hono()
// グローバルエラーハンドラー
app.onError = (err, c) => {
if (err instanceof ValidationError) {
return c.json({ error: err.message }, 400)
}
if (err instanceof NotFoundError) {
return c.json({ error: 'Not found' }, 404)
}
// その他のエラー
return c.json({ error: 'Internal error' }, 500)
}
app.get('/users/:id', async (c) => {
const user = await db.findUser(c.req.param('id'))
if (!user) {
throw new NotFoundError('User not found') // ← throw するだけ!
}
return c.json(user)
})
#112 basic auth middleware supports multiple users
変更前: 1人のユーザーしか設定できない
app.use('/admin/*', basicAuth({
username: 'admin',
password: 'secret'
}))
変更前のコード
export const basicAuth = (options: {
username: string
password: string
realm?: string
}) => {
return async (ctx: Context, next: Function) => {
const user = auth(ctx.req)
// 1人のユーザーとだけ比較
const usernameEqual = await timingSafeEqual(options.username, user.username)
const passwordEqual = await timingSafeEqual(options.password, user.password)
if (!usernameEqual || !passwordEqual) {
// 401 Unauthorized
}
return next()
}
}
変更後のコード
export const basicAuth = (
options: { username: string; password: string; realm?: string },
...users: { username: string; password: string }[] // ← 可変長引数で追加ユーザー
) => {
// 最初のユーザーも配列に追加
users.unshift({ username: options.username, password: options.password })
return async (ctx: Context, next: Function) => {
const requestUser = auth(ctx.req)
if (requestUser) {
// 全ユーザーをループでチェック
for (const user of users) {
const usernameEqual = await timingSafeEqual(user.username, requestUser.username)
const passwordEqual = await timingSafeEqual(user.password, requestUser.password)
if (usernameEqual && passwordEqual) {
return next() // どれか1人でもマッチすればOK
}
}
}
// 401 Unauthorized
}
}
使い方
// 複数ユーザーを許可
app.use('/admin/*', basicAuth(
{ username: 'admin', password: 'admin123' }, // 1人目
{ username: 'editor', password: 'editor456' }, // 2人目
{ username: 'viewer', password: 'viewer789' } // 3人目
))
#113 Make ParamMap Array<[key, value]>.
お、新しいコントリビューター。Amanoさん
https://github.com/usualoma
ParamMapをオブジェクトから配列に変更
変更前: ParamMapはオブジェクト
export interface ParamMap {
[key: string]: number
}
// 使い方
const paramMap: ParamMap = {
'id': 0,
'name': 1
}
// オブジェクトのキーを取得するのが遅い!
for (const key of Object.keys(paramMap)) { // ← Object.keys() が遅い
console.log(key, paramMap[key])
}
変更後: ParamMapを配列に
export type ParamMap = Array<[string, number]>
// 使い方
const paramMap: ParamMap = [
['id', 0],
['name', 1]
]
Object.keys() の内部動作:
- オブジェクトのプロパティを列挙
- プロトタイプチェーンをチェック
- 新しい配列を作成
- キーをコピー
→ オーバーヘッドが大きい
配列のループ:
- インデックスでアクセス
→ シンプルで速い
114 refactor: directory structure
ディレクトリ構造のリファクタリング
#115 feat: exports RegExpRouter
RegExpRouterをエクスポート