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を読んでいく#71 - #75

0
Last updated at Posted at 2025-12-14

この記事何?

エンジニア歴1年半。業務で利用しているHonoが大好きだが、TSもプログラミング知識も弱々すぎて上手く使いこなせない。速いはずが多分生かせてない。
ちゃんと理解するためにソースコードリーディングしたい。

でも目的なくソースコード眺めても意味ないし……ねむい……
そうだ!PRを全部読めば、変遷やなぜ変更されているかストーリー的に分かるのでは。
と思ったのでチャレンジしてみる。PR2000以上あるので、全部できるかは知らん。
https://github.com/honojs/hono

#71 なし

#72 feat: Mustache Middleware

Mustache テンプレートエンジンのミドルウェアを追加。KV Sites からテンプレートを読み込んでレンダリング

Mustache テンプレートエンジンとは

様々な言語で組み合わせて使えるテンプレートエンジンです。
Loopなどロジック地味たことをする構文が存在しないので、Logic-Less templatesと呼ばれています。
タグの記法が{{hoge}}みたいになっていて、 { が口ひげみたいに見えるためmustache(口ひげ)というらしいです。

テンプレートエンジンとは

テンプレートエンジンとはデータとテンプレートを合体させ、文字列を作る仕組みのことです。

RailsのslimとかerbみたいなやつのJS版か。理解。

主な変更

  1. Mustache Middleware の実装
  src/middleware/mustache/mustache.ts:

  import type { Context } from '../../context'
  import { getContentFromKVAsset } from '../../utils/cloudflare'

  const EXTENSION = '.mustache'

  export const mustache = () => {
    return async (c: Context, next: Function) => {
      let Mustache: any

      try {
        Mustache = await import('mustache')  // 動的インポート
      } catch (e) {
        console.error(`Mustache is not found! ${e}`)
        throw new Error(`${e}`)
      }

      // c.render() を追加
      c.render = async (filename, view = {}, options?) => {
        // KV Sites からテンプレートを取得
        const content = await getContentFromKVAsset(`${filename}${EXTENSION}`)
        if (!content) {
          throw new Error(`Template "${filename}${EXTENSION}" is not found`)
        }

        // Partials(部分テンプレート)の処理
        const partialArgs: { [name: string]: string } = {}
        if (options) {
          const partials = options as Partials
          for (const key of Object.keys(partials)) {
            const partialContent = await getContentFromKVAsset(`${partials[key]}${EXTENSION}`)
            if (!partialContent) {
              throw new Error(`Partial Template "${partials[key]}${EXTENSION}" is not found`)
            }
            partialArgs[key] = partialContent
          }
        }

        // Mustache でレンダリング
        const output = Mustache.render(content, view, partialArgs)
        return c.html(output)
      }
      await next()
    }
  }
  1. Context の変更(c.body() の追加)
  src/context.ts:

  export class Context {
    // render メソッドの型定義を追加
    render: (template: string, params?: object, options?: object) => Promise<Response>

    constructor(req: Request, opts?: { res: Response; env: Env; event: FetchEvent }) {
      this._headers = {}
      // this.body = this.newResponse  // ← 削除
    }

    // c.body() をメソッドとして実装
    body(data: any, status: number = this._status, headers: Headers = this._headers): Response {
      return this.newResponse(data, {
        status: status,
        headers: headers,
      })
    }

    // text(), json(), html() が c.body() を使うように変更
    text(text: string, status: number = this._status, headers: Headers = {}): Response {
      headers['Content-Type'] = 'text/plain'
      return this.body(text, status, headers)  // ← 統一
    }

    json(object: object, status: number = this._status, headers: Headers = {}): Response {
      const body = JSON.stringify(object)
      headers['Content-Type'] = 'application/json; charset=UTF-8'
      return this.body(body, status, headers)  // ← 統一
    }

    html(html: string, status: number = this._status, headers: Headers = {}): Response {
      headers['Content-Type'] = 'text/html; charset=UTF-8'
      return this.body(html, status, headers)  // ← 統一
    }
  }
  1. Cloudflare ユーティリティの追加
  src/utils/cloudflare.ts:

  declare const __STATIC_CONTENT: KVNamespace, __STATIC_CONTENT_MANIFEST: string

  export const getContentFromKVAsset = async (path: string): Promise<string> => {
    let ASSET_MANIFEST: { [key: string]: string }

    // マニフェストをパース
    if (typeof __STATIC_CONTENT_MANIFEST === 'string') {
      ASSET_MANIFEST = JSON.parse(__STATIC_CONTENT_MANIFEST)
    } else {
      ASSET_MANIFEST = __STATIC_CONTENT_MANIFEST
    }

    const ASSET_NAMESPACE = __STATIC_CONTENT
    const key = ASSET_MANIFEST[path] || path

    if (!key) {
      return
    }

    // KV からテキストとして取得
    let content: string = await ASSET_NAMESPACE.get(key, { type: 'text' })

    if (content) {
      content = content as string
    }
    return content
  }

使い方

例:

  import { Hono, Middleware } from 'hono'

  const app = new Hono()

  // Mustache ミドルウェアを使用
  app.use('*', Middleware.mustache())

  app.get('/', async (c) => {
    // c.render() でテンプレートをレンダリング
    return await c.render('index', {
      title: 'Hello Mustache',
      message: 'Welcome to Hono!',
    }, {
      // Partials(ヘッダー、フッターなど)
      header: 'header',
      footer: 'footer',
    })
  })

  export default app

テンプレート(KV Sites に配置):

  <!-- view/index.mustache -->
  {{> header}}
  <h1>{{title}}</h1>
  <p>{{message}}</p>
  {{> footer}}

  <!-- view/header.mustache -->
  <!DOCTYPE html>
  <html>
  <head><title>{{title}}</title></head>
  <body>

  <!-- view/footer.mustache -->
  </body>
  </html>
  1. c.body() がメソッドになった
    - 前: this.body = this.newResponse(プロパティ)
    - 後: body(data, status, headers)(メソッド)
  2. c.render() が追加
    - Mustache テンプレートをレンダリング
    - KV Sites から自動で読み込み
    - Partials(部分テンプレート)対応
  3. Cloudflare専用
    - KV Sites に依存
    - __STATIC_CONTENT と __STATIC_CONTENT_MANIFEST を使用

#73 fix: diable mustache middleware

Mustache ミドルウェアを一時的に無効化(コメントアウト)

#74 fix: mustache middleware

Mustache ミドルウェアの修正。動的インポートから require() に変更し、再度有効化。

なんでこう修正したかわかんなかった。多分パフォーマンスなんだろうな。ううむ。

#75 chore: tweak

細かい調整

(chore = 雑用的な変更)。

Mustache テンプレートエンジン知らなかったので面白かった。

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?