この記事何?
エンジニア歴1年半。業務で利用しているHonoが大好きだが、TSもプログラミング知識も弱々すぎて上手く使いこなせない。速いはずが多分生かせてない。
ちゃんと理解するためにソースコードリーディングしたい。
でも目的なくソースコード眺めても意味ないし……ねむい……
そうだ!PRを全部読めば、変遷やなぜ変更されているかストーリー的に分かるのでは。
と思ったのでチャレンジしてみる。PR2000以上あるので、全部できるかは知らん。
https://github.com/honojs/hono
#101 chore: update examples
サンプルプロジェクトを更新。相対パスインポートを修正し、不要な設定ファイルを削除。
#102 feat: add type to c.req.param key
ルートパラメータの型推論を追加。c.req.param('id') の引数に型チェックが効くように。
問題
変更前:
app.get('/user/:id', (c) => {
const id = c.req.param('id') // OK
const foo = c.req.param('foo') // コンパイル通るが期待値はエラー
return c.text(`User: ${id}`)
})
問題: 存在しないパラメータ名を指定してもエラーにならない。
修正
src/hono.ts:
// パスからパラメータ名を抽出する型
type ParamKeyName<NameWithPattern> =
NameWithPattern extends `${infer Name}{${infer _Pattern}`
? Name // :id{[0-9]+} → id
: NameWithPattern // :id → id
type ParamKey<Component> =
Component extends `:${infer NameWithPattern}`
? ParamKeyName<NameWithPattern> // :id → id
: never
type ParamKeys<Path> =
Path extends `${infer Component}/${infer Rest}`
? ParamKey<Component> | ParamKeys<Rest> // 再帰的に抽出
: ParamKey<Path>
// ハンドラーの型定義を変更
get<Path extends string>(path: Path, ...args: Handler<ParamKeys<Path>>[]): Hono
動作:
// '/user/:id' から 'id' を抽出
type Keys = ParamKeys<'/user/:id'>
// → 'id'
// '/book/:category/:id' から 'category' | 'id' を抽出
type Keys = ParamKeys<'/book/:category/:id'>
// → 'category' | 'id'
使い方
変更後:
app.get('/user/:id', (c) => {
const id = c.req.param('id') // OK - 'id' は有効
const foo = c.req.param('foo') // エラー
return c.text(`User: ${id}`)
})
TypeScript の Template Literal Types
// パターンマッチング
type ParamKey<Component> =
Component extends `:${infer Name}` // ':' で始まる場合
? Name // → Name を抽出
: never // そうでなければ never
// 例
type A = ParamKey<':id'> // → 'id'
type B = ParamKey<'users'> // → never
type C = ParamKey<':name'> // → 'name'
#103: feat: add option for no strict routing
strict routing オプションを追加。末尾スラッシュの扱いを設定可能に。
問題
デフォルト動作(strict = true):
app.get('/hello', (c) => c.text('Hello'))
// GET /hello → 200 OK
// GET /hello/ → 404 Not Found(別のルートとして扱われる)
修正
src/hono.ts:
type Init = {
strict?: boolean
}
export class Hono {
router: Router<Handler[]>
middlewareRouters: Router<MiddlewareHandler>[]
tempPath: string
+ strict: boolean
constructor(init: Init = { strict: true }) {
this.router = new Router()
this.middlewareRouters = []
this.tempPath = '/'
+ this.strict = init.strict // strict routing - デフォルトは true
}
async dispatch(request: Request, env?: Env, event?: FetchEvent): Promise<Response> {
- const [method, path] = [request.method, getPathFromURL(request.url)]
+ const path = getPathFromURL(request.url, { strict: this.strict })
const method = request.method
// ...
}
}
src/utils/url.ts:
type Params = {
strict: boolean
}
export const getPathFromURL = (url: string, params: Params = { strict: true }): string => {
// strict が false なら末尾の / を削除
if (!params.strict && url.endsWith('/')) {
url = url.slice(0, -1)
}
const match = url.match(URL_REGEXP)
if (match) {
return match[5]
}
return ''
}
#104 refactor: do some refactoring
コードのリファクタリング。型定義の改善とコードの整理。
#105 feat: nested route
BREAKING CHANGE
ネストルート機能を追加。app.route() の動作を変更。
変更前(チェーンルート):
// これはもう動かない
app.route('/')
.get((c) => c.text('get /'))
.post((c) => c.text('post /'))
変更後(ネストルート):
// 新しい書き方
const book = app.route('/book')
book.get('/', (c) => c.text('List books')) // GET /book
book.get('/:id', (c) => {
return c.text('Get Book: ' + c.req.param('id')) // GET /book/:id
})
book.post('/', (c) => c.text('Create book')) // POST /book
この辺はまだ今の形と違うなぁ。