mcpField
H
hono-jsx

hono-jsx

github.com/honojs开发16378 次安装2 周前更新
代码仓库

关于 hono-jsx

hono-jsx 是一份指导使用 hono/jsx 构建界面的技能文档,面向采用 Hono 的开发者。内容侧重服务端渲染优先的项目结构,并说明如何按需加入客户端交互。

主要功能

  1. 01服务端渲染优先的页面结构
  2. 02Cloudflare Workers 与 Vite 配置
  3. 03布局、页面及共享组件组织
  4. 04表单提交与路由处理
  5. 05按需添加客户端交互
  6. 06流式渲染与样式处理

适用场景

  1. 01搭建 Hono JSX 应用
  2. 02在 Cloudflare Workers 项目中配置 Vite
  3. 03实现无需客户端脚本的表单
  4. 04为特定界面添加交互状态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/honojs/skills --skill hono-jsx

安装后在 Claude Code 中输入/hono-jsx确认已加载。

详细介绍

Hono JSX 技能

使用 hono/jsx 构建 UI:优先采用服务端渲染 HTML,仅在浏览器需要状态时使用少量客户端 JSX。hono 技能介绍 JSX 语法和 c.html();本技能介绍如何组织应用。默认使用 Cloudflare Workers 和 Vite 设置;其他运行时请参阅“其他运行时”。

不使用 React

hono/jsx 是 JSX 运行时。不要引入 React:

  • 不要安装 react、react-dom、@types/react 或 @vitejs/plugin-react。不要从 react 导入内容。
  • tsconfig.json 中设置了 "jsx": "react-jsx" 和 "jsxImportSource": "hono/jsx"。react-jsx 是转换器名称,不代表依赖 React。
  • 服务端组件是普通函数,每个请求渲染一次。服务端不使用钩子、状态或事件处理器。需要数据时:将组件设为 async 并使用 await。需要交互时:仅在对应部分使用基于 hono/jsx/dom 的客户端组件。
  • 使用 class,不要使用 className。
  • vite-ssr-components 同时导出两种版本:从 vite-ssr-components/hono 导入,绝不要从 /react 导入。

项目布局(Cloudflare Workers + Vite)

从 create-hono 使用 cloudflare-workers 模板生成的内容开始(运行 npm create hono@next,或者在现有目录中运行 npx hono init --template cloudflare-workers):其中包含返回文本响应的 src/index.ts、供 cf CLI 使用的 cloudflare.config.ts,以及仅配置了 cloudflare() 的 vite.config.ts。模板没有 JSX 设置;请添加以下内容。对于现有项目,也应采用此布局,而不是另行设计结构。使用 hono 技能中的 Hono CLI 验证路由(npx hono request /);它是模板的开发依赖,会通过 Vite 运行应用,因此 c.env 会自动包含本地绑定。

src/
  index.tsx      # Hono app, routes (renamed from index.ts)
  renderer.tsx   # jsxRenderer layout: <html>, <head>, Vite assets
  client.ts      # optional: browser-side code
  style.css
public/          # static files served as-is
cloudflare.config.ts   # cf CLI: worker name, entrypoint, bindings
vite.config.ts
// package.json (scripts, from the template)
{
  "dev": "cf dev",
  "build": "cf build",
  "deploy": "cf deploy",
  "typecheck": "cf workers types && tsc"
}

在模板基础上:

  1. 将 src/index.ts 重命名为 src/index.tsx,并在 cloudflare.config.ts 中将入口导入指向该文件:

    import * as entrypoint from './src/index.tsx' with { type: 'cf-worker' }
    
  2. 将 vite-ssr-components 安装为开发依赖,并添加其插件:

    // vite.config.ts
    import { cloudflare } from '@cloudflare/vite-plugin'
    import { defineConfig } from 'vite'
    import ssrPlugin from 'vite-ssr-components/plugin'
    
    export default defineConfig({
      plugins: [cloudflare(), ssrPlugin()],
    })
    
  3. 添加 src/renderer.tsx、src/style.css 和 public/:

// src/renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
import { Link, Script, ViteClient } from 'vite-ssr-components/hono'

export const renderer = jsxRenderer(({ children }) => {
  return (
    <html>
      <head>
        <ViteClient />
        <Link href="/src/style.css" rel="stylesheet" />
        <Script src="/src/client.ts" />
      </head>
      <body>{children}</body>
    </html>
  )
})
// src/index.tsx
import { Hono } from 'hono'
import { renderer } from './renderer'

const app = new Hono<{ Bindings: Env }>()

app.use(renderer)

app.get('/', (c) => {
  return c.render(<h1>Hello!</h1>)
})

export default app

vite-ssr-components 负责 Vite 的相关配置:<ViteClient /> 会注入 Vite 客户端和开发环境中的 SSR 热重载,生产环境下则不渲染任何内容;<Script> 和 <Link> 指向源文件,并通过清单重写为带哈希的构建输出。插件会扫描这些组件并将每个引用的文件添加为构建入口,因此无需维护 rollupOptions.input。保留 <Script> 和 <Link> 作为组件名称,或配置插件的 components 选项。

npm run dev(cf dev)供人工使用;请改用 Hono CLI 进行验证。绑定在 cloudflare.config.ts 中声明,其 Env 类型来自 npm run typecheck;切勿手动编写。在现有的 wrangler 项目中,按 hono 技能中的说明使用 wrangler、npm run cf-typegen 和 CloudflareBindings。

布局和页面

jsxRenderer 设置布局;处理器通过 c.render() 传入页面内容。

  • 每页的 head 内容:在页面组件中编写 <title>、<meta> 和 <link>。这些元素会被提升到 <head> 中。现有的 head 元素会保留,因此如果页面设置了自己的标题,就不要在布局中保留 <title>。
  • 传给布局的 props(例如标题、当前导航项的键):扩展一次 ContextRenderer,然后将 props 作为 c.render() 的第二个参数传入。
declare module 'hono' {
  interface ContextRenderer {
    (content: string | Promise<string>, props: { title: string }): Response
  }
}

export const renderer = jsxRenderer(({ children, title }) => (
  <html>
    <head>
      <title>{title}</title>
    </head>
    <body>{children}</body>
  </html>
))

app.get('/about', (c) => c.render(<About />, { title: 'About' }))
  • 嵌套布局:子应用可以注册自己的 jsxRenderer,并将父级布局作为 Layout 接收。
const blog = new Hono()
blog.use(
  jsxRenderer(({ children, Layout }) => (
    <Layout>
      <nav>Blog</nav>
      {children}
    </Layout>
  ))
)
app.route('/blog', blog)
  • 在组件中使用请求数据:hono/jsx-renderer 中的 useRequestContext() 会返回 c,因此深层组件可以读取 c.req、c.env 或 c.var,无需逐层传递 props。如果数据仅在一个位置使用,建议在处理器中加载数据并通过 props 传入。
import { useRequestContext } from 'hono/jsx-renderer'

const UserMenu = async () => {
  const c = useRequestContext<{ Bindings: Env }>()
  const user = await c.env.KV.get('user')
  return <span>{user}</span>
}
  • 共享组件:将可复用部分放入 src/components/。它们是接收 props 的函数;可使用 hono/jsx 中的 FC<Props> 和 PropsWithChildren<Props> 为其添加类型。

表单和变更

默认使用 HTML 表单向 Hono 路由提交,并进行重定向。无需客户端 JavaScript,且在任何脚本加载之前即可正常工作。

app.get('/todos', async (c) => {
  const todos = await listTodos(c.env)
  return c.render(
    <>
      <form method="post" action="/todos">
        <input name="title" required />
        <button type="submit">Add</button>
      </form>
      <ul>{todos.map((t) => <li>{t.title}</li>)}</ul>
    </>
  )
})

app.post('/todos', async (c) => {
  const body = await c.req.parseBody()
  await addTodo(c.env, String(body.title))
  return c.redirect('/todos')
})

使用 validator('form', ...) 或 hono 技能中的 schema 验证器进行验证;验证失败时,重新渲染页面并显示错误消息,而不是返回 JSON。

客户端行为

大多数页面不需要客户端 JavaScript。如果确实需要,先使用纯 DOM 代码;只有对于需要维护状态并重新渲染的 UI,才使用 hono/jsx/dom。这两种方式都不涉及 React。

纯脚本——默认方式。将代码放在 src/client.ts 中,并通过渲染器中的 <Script src="/src/client.ts" /> 加载。

// src/client.ts
/// <reference lib="dom" />
document.querySelector('#menu-toggle')?.addEventListener('click', () => {
  document.querySelector('#menu')?.classList.toggle('open')
})

hono/jsx/dom——适用于计数器、实时筛选器或多步骤表单等有状态的部分。服务器会渲染一个空容器;客户端使用 render() 在其中渲染。这里没有水合,因此该部分会一直为空,直到脚本运行。页面的实际内容应保持由服务器渲染。

// src/client.tsx
/// <reference lib="dom" />
import { useState } from 'hono/jsx'
import { render } from 'hono/jsx/dom'

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

render(<Counter />, document.getElementById('counter')!)

钩子(useState、useEffect、useRef,……)的签名与 React 兼容,且从 hono/jsx 导入。从客户端调用服务器时,使用 fetch 请求 JSON 路由;当 API 接口值得进行类型标注时,hono/client 中的 hc 可提供带类型的调用。

**客户端文件的类型检查。**模板的 tsconfig.json 中设置了 lib: ["ESNext"],且未包含 DOM,因此 document 会导致 tsc 报错。不要修改 tsconfig.json,而是在每个客户端文件顶部添加 lib 引用:

/// <reference lib="dom" />

Vite 不会读取 lib,因此这只会影响 tsc。

样式

  • 在 src/style.css 中放置样式表,并在渲染器中通过 <Link href="/src/style.css" rel="stylesheet" /> 引入。Vite 会处理它,因此 CSS 导入和 PostCSS 均可正常使用。
  • 使用 hono/css 为组件设置作用域样式:css 会返回类名,<Style /> 会在 <head> 中输出收集到的规则。
import { css, Style } from 'hono/css'

const card = css`
  padding: 1rem;
  &:hover { background: #eee; }
`
// <head><Style /></head> ... <div class={card}>...</div>

流式传输

如果页面中的某个部分加载较慢,可启用渲染器的 stream: true,并用 Suspense 包裹耗时的异步组件,让页面外壳即时显示。

import { Suspense } from 'hono/jsx/streaming'

export const renderer = jsxRenderer(({ children }) => <html><body>{children}</body></html>, { stream: true })

app.get('/', (c) =>
  c.render(
    <Suspense fallback={<p>Loading...</p>}>
      <SlowList />
    </Suspense>
  )
)

文档将 Suspense 和 ErrorBoundary 标记为实验性功能。仅在数据确实加载缓慢时使用,不要默认启用。

转义和原始 HTML

JSX 会转义插值字符串。要插入可信 HTML,请使用 dangerouslySetInnerHTML={{ __html }} 或 hono/html 中的 raw()。切勿将用户输入传入其中。

其他运行时

如果不使用 Cloudflare Vite 插件,请使用 @hono/vite-dev-server 进行开发,并使用 @hono/vite-build/<runtime> 进行构建。此时,客户端资源会通过单独的 Vite 模式构建,并通过固定路径引用:

// vite.config.ts
import build from '@hono/vite-build/node' // or /bun, /cloudflare-workers
import devServer from '@hono/vite-dev-server'
import { defineConfig } from 'vite'

export default defineConfig(({ mode }) => {
  if (mode === 'client') {
    return {
      build: { rollupOptions: { input: './src/client.tsx', output: { entryFileNames: 'static/client.js' } } },
    }
  }
  return {
    plugins: [
      // staticRoot is a Node adapter option: it serves dist/static from the built server
      build({ entry: 'src/index.tsx', staticRoot: './dist' }),
      devServer({ entry: 'src/index.tsx' }),
    ],
  }
})
// renderer: pick the script path by mode
{import.meta.env.PROD ? (
  <script type="module" src="/static/client.js" />
) : (
  <script type="module" src="/src/client.tsx" />
)}

使用 vite build --mode client && vite build 进行构建,然后运行 node dist/index.js(Node 构建会打包 @hono/node-server,请将其安装为依赖项)。如果开发时需要绑定或运行时 API,@hono/vite-dev-server 也提供适配器(/cloudflare、/node、/bun)。

文档

使用 Accept: text/markdown 获取(参见 hono 技能):

检查清单

  • 不使用 React 包;jsxImportSource 为 hono/jsx;使用 class 属性。
  • 在 jsxRenderer 中设置布局,通过 c.render() 渲染页面,并在页面内设置各页的 <title>。
  • 表单提交到路由并重定向;仅在需要时使用客户端 JavaScript,优先使用纯 DOM。
  • 在 Cloudflare 上,Vite 资源通过 vite-ssr-components(ViteClient、Script、Link)加载,不要手写 <script> 标签。
  • 绑定类型由工具生成,绝不手写:Env 来自 npm run typecheck(cf),CloudflareBindings 来自 npm run cf-typegen(wrangler)。

评论

支持 Markdown