关于 hono-jsx
hono-jsx 是一份指导使用 hono/jsx 构建界面的技能文档,面向采用 Hono 的开发者。内容侧重服务端渲染优先的项目结构,并说明如何按需加入客户端交互。
主要功能
- 01服务端渲染优先的页面结构
- 02Cloudflare Workers 与 Vite 配置
- 03布局、页面及共享组件组织
- 04表单提交与路由处理
- 05按需添加客户端交互
- 06流式渲染与样式处理
适用场景
- 01搭建 Hono JSX 应用
- 02在 Cloudflare Workers 项目中配置 Vite
- 03实现无需客户端脚本的表单
- 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"
}
在模板基础上:
-
将
src/index.ts重命名为src/index.tsx,并在cloudflare.config.ts中将入口导入指向该文件:import * as entrypoint from './src/index.tsx' with { type: 'cf-worker' } -
将
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()], }) -
添加
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 技能):
- https://hono.dev/docs/guides/jsx
- https://hono.dev/docs/guides/jsx-dom
- https://hono.dev/docs/middleware/builtin/jsx-renderer
- https://hono.dev/docs/helpers/css
- https://hono.dev/docs/helpers/html
- https://github.com/yusukebe/vite-ssr-components
- https://github.com/honojs/vite-plugins(dev-server、build)
检查清单
- 不使用 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)。