この記事何?
エンジニア歴1年半。業務で利用しているHonoが大好きだが、TSもプログラミング知識も弱々すぎて上手く使いこなせない。速いはずが多分生かせてない。
ちゃんと理解するためにソースコードリーディングしたい。
でも目的なくソースコード眺めても意味ないし……ねむい……
そうだ!PRを全部読めば、変遷やなぜ変更されているかストーリー的に分かるのでは。
と思ったのでチャレンジしてみる。PR2000以上あるので、全部できるかは知らん。
https://github.com/honojs/hono
36 Feature/redirect
c.redirect() メソッドの追加
export class Context {
text(text: string, status: number = 200, headers: Headers = {}): Response
json(object: object, status: number = 200, headers: Headers = {}): Response
html(html: string, status: number = 200, headers: Headers = {}): Response
// 追加
redirect(location: string, status: number = 302, headers: Headers = {}): Response {
if (typeof location !== 'string') {
throw new TypeError('location must be a string!')
}
headers['Location'] = location
return this.newResponse('', {
status: status,
headers: headers,
})
}
}
app.get('/old-path', (c) => c.redirect('/new-path'))
app.get('/permanent', (c) => c.redirect('/new-path', 301))
ほえー便利。使ったことなかった。
37 Fix redirect method
リダイレクトの修正
相対URL → 絶対URLに自動変換
// 変更前:
redirect(location: string, status: number = 302, headers: Headers = {}): Response {
headers['Location'] = location // そのまま設定
return this.newResponse('', {
status: status,
headers: headers,
})
}
// 使用例
app.get('/redirect', (c) => c.redirect('/destination'))
// Location: /destination (相対URL)
// 変更後:
redirect(location: string, status: number = 302): Response {
if (!isAbsoluteURL(location)) {
// 相対URLを絶対URLに変換
const url = new URL(this.req.url)
url.pathname = location
location = url.toString()
}
return this.newResponse(null, {
status: status,
headers: {
Location: location, // 絶対URL
},
})
}
// 使用例
app.get('/redirect', (c) => c.redirect('/destination'))
// Location: https://example.com/destination (絶対URL)
isAbsoluteURLは正規表現で絶対URLかを判定
絶対URLにするのはなんでなんだろ。
#38 feat(body-parse): a body parse middleware
metrueさんのPR。リクエストボディをパースするミドルウェアを追加。
import type { Middleware } from '../../hono'
export const bodyParse = (): Middleware => {
return async (c, next) => {
const contentType = c.req.headers.get('Content-Type') || ''
if (contentType.includes('application/json')) {
c.req.json = await c.req.raw.json()
} else if (contentType.includes('application/x-www-form-urlencoded')) {
c.req.body = await c.req.raw.formData()
} else if (contentType.includes('text/')) {
c.req.body = await c.req.raw.text()
}
await next()
}
}
// 使用例 (example/basic/index.js):
import { bodyParse } from '../../dist/middleware.js'
app.use('*', bodyParse())
app.post('/entry', async (c) => {
const body = await c.req.json()
return c.json({ message: 'Created!', data: body }, 201)
})
PRの議論では、body-parseミドルウェアを組み込みで提供するのはどうか?と提案して、Fetch APIには既に「...text(), json(), and formData() methods」があるけど、あらゆる形式のリクエストボディを透過的に処理できると便利だからミドルウェアで実装していいよ、とやりとりしている。
透過的にってどういう意味だろ。中身を意識しなくていいってことかな。
#39 Feature/example blog
CRUD APIのブログアプリケーションのサンプルを追加
example/blog/
├── src/
│ ├── index.ts # ルーティング定義
│ ├── controller.ts # ハンドラー実装
│ ├── model.ts # データモデル
│ └── controller.test.ts # テストコード
├── package.json
├── tsconfig.json
├── wrangler.toml
└── README.md
MVCになってて歓喜....読みやすい...
// src/index.ts - ルーティング定義:
import { Hono } from '../../../dist'
import * as Controller from './controller'
export const app = new Hono()
app.get('/', Controller.root)
// RESTful API
app.get('/posts', Controller.list) // 一覧取得
app.post('/posts', Controller.create) // 新規作成
app.get('/posts/:id', Controller.show) // 個別取得
app.put('/posts/:id', Controller.update) // 更新
app.delete('/posts/:id', Controller.destroy) // 削除
app.fire()
// src/model.ts - データモデル:
export interface Post {
id: string
title: string
body: string
}
export type Param = {
title: string
body: string
}
const posts: { [key: string]: Post } = {}
export const getPosts = (): Post[] => {
return Object.values(posts)
}
export const createPost = (param: Param): Post | undefined => {
if (!(param.title && param.body)) return
const id = crypto.randomUUID() // Cloudflare Workers の crypto API
const newPost: Post = { id: id, title: param.title, body: param.body }
posts[id] = newPost
return newPost
}
export const updatePost = (id: string, param: Param): boolean => {
const post = posts[id]
if (post) {
post.title = param.title
post.body = param.body
return true
}
return false
}
export const deletePost = (id: string): boolean => {
if (posts[id]) {
delete posts[id]
return true
}
return false
}
// src/controller.ts - ハンドラー実装:
import type { Handler } from '../../../dist'
import * as Model from './model'
export const list: Handler = (c) => {
const posts = Model.getPosts()
return c.json({ posts: posts, ok: true })
}
export const create: Handler = async (c) => {
const param = (await c.req.json()) as Model.Param
const newPost = Model.createPost(param)
if (!newPost) {
return c.json({ error: 'Can not create new post', ok: false }, 200)
}
return c.json({ post: newPost, ok: true }, 201)
}
export const show: Handler = async (c) => {
const id = c.req.params('id')
const post = Model.getPost(id)
if (!post) {
return c.json({ error: 'Not Found', ok: false }, 404)
}
return c.json({ post: post, ok: true })
}
export const update: Handler = async (c) => {
const id = c.req.params('id')
if (!Model.getPost(id)) {
return c.json({ ok: false }, 204) // No Content
}
const param = (await c.req.json()) as Model.Param
const success = Model.updatePost(id, param)
return c.json({ ok: success })
}
export const destroy: Handler = async (c) => {
const id = c.req.params('id')
if (!Model.getPost(id)) {
return c.json({ ok: false }, 204)
}
const success = Model.deletePost(id)
return c.json({ ok: success })
}
#40 なし
Honoがどんどんシンプルに書けるように整ってきた感じ。次はどんなPRか楽しみになってきた。今日はここまで。