この記事何?
エンジニア歴1年半。業務で利用しているHonoが大好きだが、TSもプログラミング知識も弱々すぎて上手く使いこなせない。速いはずが多分生かせてない。
ちゃんと理解するためにソースコードリーディングしたい。
でも目的なくソースコード眺めても意味ないし……ねむい……
そうだ!PRを全部読めば、変遷やなぜ変更されているかストーリー的に分かるのでは。
と思ったのでチャレンジしてみる。PR2000以上あるので、全部できるかは知らん。
https://github.com/honojs/hono
#10 New API
src/methods.js
27種類のHTTPメソッドに対応。リファクタリング
変更前:
// hono.js内で直接実装
class Hono {
get(...args) { return this.addRoute('GET', ...args) }
post(...args) { return this.addRoute('POST', ...args) }
put(...args) { return this.addRoute('PUT', ...args) }
delete(...args) { return this.addRoute('DELETE', ...args) }
patch(...args) { return this.addRoute('PATCH', ...args) }
}
変更後:
// src/methods.js(新規ファイル)
const methods = [
'get', 'post', 'put', 'head', 'delete', 'options',
'trace', 'copy', 'lock', 'mkcol', 'move', 'patch',
'purge', 'propfind', 'proppatch', 'unlock', 'report',
'mkactivity', 'checkout', 'merge', 'm-search', 'notify',
'subscribe', 'unsubscribe', 'search', 'connect'
]
module.exports = methods
// src/hono.js
const methods = require('./methods')
class Hono {
constructor() {
for (const method of methods) {
this[method] = (...args) => {
return this.addRoute(method, ...args)
}
}
}
}
mkcol、purge、propfind、proppatch、mkactivity全く知らなかったので調べる。
WebDAVのメソッドらしい。WebDAVなんぞや。
WebDAV(Web-based Distributed Authoring and Versioning、ウェブダブ)はHypertext Transfer Protocolを拡張したもので、Webサーバ上のファイル管理を目的とした分散ファイルシステムを実現するプロトコルである。
利用中のWebサーバーを活用して、ファイル共有と編集ができる技術や機能をWebDAV(ウェブダブ)といいます。現在、多くのレンタルサーバーのプランで利用可能です。VPSなどで、利用者自身が設定する方法もあります。
現在普及しているGoogleドライブなどのオンラインストレージ・サービスにWebDAVは似ており、クラウド上で複数の利用者が、ファイル共有とその場での編集作業を比較的安全に行うことができます。
WebDAVについてご存知ない方もいると思われ、地味な印象がありますが、実はマイクロソフトが1999年に発表した歴史のある技術です。おかげでセキュリティを保ちつつ、簡単で便利な機能が使えるようになりました。
mkactivityはSubversionのメソッドらしい。
ほえー。
各メソッドの意味
MKCOL(Make Collection)
- 意味: 新しいコレクション(ディレクトリ)を作成する
- 用途: WebDAV 上にフォルダを作るとき
-
例:
MKCOL /documents/reports/
「フォルダ作って!」というリクエスト。
PURGE
- 意味: キャッシュサーバ(特に Varnish Cache)に「このキャッシュ消して!」と要求する
- 用途: Web サイトでキャッシュを即座に無効化したいとき
-
例:
PURGE /index.html
WebDAV ではなく キャッシュ無効化 のための“拡張”HTTPメソッド。
PROPFIND(Property Find)
-
意味: ファイル/フォルダのメタデータを取得する
-
用途:
- WebDAV でファイル一覧を取得する
- 更新日時、サイズ、カスタムプロパティを読む
-
例:
PROPFIND /docs/
「そのファイルの属性(情報)教えて」というリクエスト。
PROPPATCH(Property Patch)
- 意味: メタデータ(プロパティ)を書き換える
- 用途: WebDAV でタイトル・タグ・作成者などの属性を更新する
- 例: 権限やカスタムプロパティを変更
「そのファイルの属性を書き換えて」というリクエスト。
MKACTIVITY(Make Activity)
- 意味: Subversion(SVN)が使うメソッドで、「アクティビティ(コミット単位の作業領域)」を作る
- 用途: SVN がコミット操作の前に内部的に呼ぶ
-
例: SVN クライアントが自動で
MKACTIVITY /!svn/act/xxxを送る
SVN が「コミット用の作業箱を作りますね」と使う内部メソッド。
| メソッド | 用途 | 分類 |
|---|---|---|
| MKCOL | フォルダ作成 | WebDAV |
| PURGE | キャッシュ削除 | Varnish/拡張HTTP |
| PROPFIND | メタデータ取得 | WebDAV |
| PROPPATCH | メタデータ更新 | WebDAV |
| MKACTIVITY | SVN のコミット準備 | Subversion |
はい。とりあえず。なんとなく。そういうのがある。わかった。完全に理解。
次。
変更前:
const Hono = require('hono')
const app = Hono() // 関数として呼ぶ
変更後:
const { Hono } = require('hono')
const app = new Hono() // クラスとしてnewで作る
- Named importに対応({ Hono }でimport可能に)
- クラスは型パラメータを渡せるので、TypeScriptとの親和性向上する
- ES Modules対応への第一歩
Named Exportだと何が嬉しいか
| # | メリット | Named Export | Default Export | 説明 |
|---|---|---|---|---|
| 1 | 複数 export 可能 | ○(複数OK) | △(1つだけ) | Named は1ファイルからいくつでも export できる |
| 2 | 名前の明確さ | ○(名前固定、asで変更可) | ○(自由に名前変更可だが混乱の元) | Named は API の名前が明示的で分かりやすい |
| 3 | Tree Shaking | ◎(不要な export は削除されやすい) | △(まとめてimportされがち) | Named のほうが最適化されやすくバンドルが軽くなる |
| 4 | ESM / CommonJS 互換 | ○(書き方ほぼ同じ) | △(CJSとESMで書き方が違う) | Named だと両者の書き方が揃って扱いやすい |
| 5 | IDE 補完が強力 | ◎({ } 内に候補表示) | △(一度変数に入れないと補完されない) | Named の方が自動補完が効きやすい |
らしい。この辺もなんとなく使ってるから勉強していきたいところ。
TypeScript型定義ファイルの追加
新規: src/hono.d.ts
export class Hono {
router: Router
middlewareRouters: Router[]
getRouter(): Router
addRoute(method: string, args: any[]): Hono
matchRoute(method: string, path: string): Node
createContext(req: Request, res: Response): Context
dispatch(req: Request, res: Response): Response
handleEvent(event: FetchEvent): Response
fire(): void
notFound(): Response
route(path: string): Hono
use(path: string, middleware: any): void
all(path: string, handler: any): Hono
get(path: string, handler: any): Hono
post(path: string, handler: any): Hono
put(path: string, handler: any): Hono
head(path: string, handler: any): Hono
delete(path: string, handler: any): Hono
}
export class Middleware {}
#11
なし
#12 Create CODE_OF_CONDUCT.md
CODE_OF_CONDUCT.mdの追加。ハラスメント防止などプロジェクトの行動規範(Code of Conduct)を記述したファイル。
Contributor Covenant(バージョン2.0)に基づいて作成。
コミュニティ影響ガイドラインはMozilla の Code of Conduct Enforcement Ladderに着想を得ている
こういう模範的ガイドラインがあるのを知らなかった。
#13 Add keywords to package.json
"keywords": [
"web",
"app",
"http",
"application",
"framework",
"router",
"cloudflare",
"workers",
"fastly",
"compute@edge"
]
package.jsonにkeywordsフィールド追加。知らんかった。npm searchで表示されるので、パッケージの検索エンジンでの発見性が高まる。
#14 Update README, Add instruction
Honoを使って、ゼロからCloudflare Workersを作成する手順の追加
#15 Update d.ts
Web標準APIをinterfaceに変更
変更前:
declare class FetchEvent {}
declare class Request {}
declare class Response {}
declare class Context {}
変更後:
declare interface FetchEvent {}
declare interface Request {}
declare interface Response {}
- ブラウザやCloudflare Workersが提供
- classではなくinterfaceとして定義することで、実際の型との衝突を避ける??
Handlerの型定義を追加
type Handler = (c: Context, next: () => void) => Response | void
- これまでanyだったハンドラーの型を明確化することで、ユーザーがハンドラーを書くときに型補完が効く
// 変更前: 型がない
app.get('/hello', (c, next) => {
// c, next の型が any
return new Response('Hello');
});
// 変更後: 型がある
app.get('/hello', (c, next) => {
// c: Context
// next: () => void
// 戻り値: Response | void
return new Response('Hello');
});
Nodeクラスの内部構造を定義
変更前:
declare class Node {}
変更後:
declare class Node {
method: string
handler: any
children: Node[]
middlewares: any[]
insert(method: string, path: string, handler: any): Node
search(method: string, path: string): Result
}
type Result = {
handler: any
params: {}
}
- Nodeクラス(ルーティングツリーのノード)の内部構造を明確化
- デバッグ時にNodeの中身が分かる
- 型推論がより正確になる
Contextクラスの詳細化
変更前:
declare class Context {}
変更後:
declare class Context {
req: Request
res: Response
newResponse(params: {}): Response
}
使用例:
app.get('/api', (c) => {
c.req // Request 型
c.res // Response 型
c.newResponse({ ... }) // Response を返す
});
非同期処理(async)対応
変更前:
matchRoute(method: string, path: string): Node
createContext(req: Request, res: Response): Context
dispatch(req: Request, res: Response): Response
handleEvent(event: FetchEvent): Response
変更後:
matchRoute(method: string, path: string): Promise<Node>
createContext(req: Request, res: Response): Promise<Context>
dispatch(req: Request, res: Response): Promise<Response>
handleEvent(event: FetchEvent): Promise<Response>
Middlewareクラスの型改善
変更前:
declare interface BuiltinMiddleware {}
export class Middleware {
static defaultFilter: BuiltinMiddleware
static poweredBy: BuiltinMiddleware
}
変更後:
export class Middleware {
static defaultFilter: Handler
// Add builtin middlewares
static poweredBy: Handler
}
- ビルトインミドルウェアもHandler型であることを明確化
その他、メソッドの並び順を整理。
分からん。眺めただけだと、何してるかわかんない。
型むずい。次。