0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HonoのマージされたPRを読んでいく#36 - #40

0
Last updated at Posted at 2025-12-07

この記事何?

エンジニア歴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か楽しみになってきた。今日はここまで。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?