Appearance
JSX
你可以使用 hono/jsx 用 JSX 语法编写 HTML。
🌐 You can write HTML with JSX syntax with hono/jsx.
尽管 hono/jsx 可以在客户端使用,但你可能更多时候会在服务器端渲染内容时使用它。以下是一些与 JSX 相关的在服务器和客户端都很常见的内容。
🌐 Although hono/jsx works on the client, you will probably use it most often when rendering content on the server side. Here are some things related to JSX that are common to both server and client.
设置
🌐 Settings
要使用 JSX,修改 tsconfig.json:
🌐 To use JSX, modify the tsconfig.json:
tsconfig.json:
json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}或者,使用 pragma 指令:
🌐 Alternatively, use the pragma directives:
ts
/** @jsx jsx */
/** @jsxImportSource hono/jsx */对于 Deno,你必须修改 deno.json 而不是 tsconfig.json:
🌐 For Deno, you have to modify the deno.json instead of the tsconfig.json:
json
{
"compilerOptions": {
"jsx": "precompile",
"jsxImportSource": "@hono/hono/jsx"
}
}用法
🌐 Usage
INFO
如果你直接来自快速开始,主文件的扩展名是 .ts —— 你需要将其更改为 .tsx —— 否则你根本无法运行应用。你还应该修改 package.json(如果你使用 Deno 则为 deno.json)以反映该更改(例如,在开发脚本中不应是 bun run --hot src/index.ts,而应是 bun run --hot src/index.tsx)。
index.tsx:
tsx
import { Hono } from 'hono'
import type { FC } from 'hono/jsx'
const app = new Hono()
const Layout: FC = (props) => {
return (
<html>
<body>{props.children}</body>
</html>
)
}
const Top: FC<{ messages: string[] }> = (props: {
messages: string[]
}) => {
return (
<Layout>
<h1>Hello Hono!</h1>
<ul>
{props.messages.map((message) => {
return <li>{message}!!</li>
})}
</ul>
</Layout>
)
}
app.get('/', (c) => {
const messages = ['Good Morning', 'Good Evening', 'Good Night']
return c.html(<Top messages={messages} />)
})
export default app元数据提升
🌐 Metadata hoisting
你可以直接在组件内部编写文档元数据标签,例如 <title>、<link> 和 <meta>。这些标签将会自动提升到文档的 <head> 部分。当 <head> 元素渲染的位置远离决定适当元数据的组件时,这尤其有用。
🌐 You can write document metadata tags such as <title>, <link>, and <meta> directly inside your components. These tags will be automatically hoisted to the <head> section of the document. This is especially useful when the <head> element is rendered far from the component that determines the appropriate metadata.
tsx
import { Hono } from 'hono'
const app = new Hono()
app.use('*', async (c, next) => {
c.setRenderer((content) => {
return c.html(
<html>
<head></head>
<body>{content}</body>
</html>
)
})
await next()
})
app.get('/about', (c) => {
return c.render(
<>
<title>About Page</title>
<meta name='description' content='This is the about page.' />
about page content
</>
)
})
export default appINFO
当发生提升时,现有的元素不会被移除。随后出现的元素会被添加到末尾。例如,如果你的 <head> 中有 <title>Default</title>,并且一个组件渲染了 <title>Page Title</title>,两个标题都会出现在头部。
片段
🌐 Fragment
使用 Fragment 对多个元素进行分组,而无需添加额外的节点:
🌐 Use Fragment to group multiple elements without adding extra nodes:
tsx
import { Fragment } from 'hono/jsx'
const List = () => (
<Fragment>
<p>first child</p>
<p>second child</p>
<p>third child</p>
</Fragment>
)或者如果它正确设置,你可以用 <></> 来写。
🌐 Or you can write it with <></> if it sets up properly.
tsx
const List = () => (
<>
<p>first child</p>
<p>second child</p>
<p>third child</p>
</>
)PropsWithChildren
你可以使用 PropsWithChildren 在函数组件中正确推断子元素。
🌐 You can use PropsWithChildren to correctly infer a child element in a function component.
tsx
import { PropsWithChildren } from 'hono/jsx'
type Post = {
id: number
title: string
}
function Component({ title, children }: PropsWithChildren<Post>) {
return (
<div>
<h1>{title}</h1>
{children}
</div>
)
}插入原始 HTML
🌐 Inserting Raw HTML
要直接插入 HTML,请使用 dangerouslySetInnerHTML:
🌐 To directly insert HTML, use dangerouslySetInnerHTML:
tsx
app.get('/foo', (c) => {
const inner = { __html: 'JSX · SSR' }
const Div = <div dangerouslySetInnerHTML={inner} />
})记忆化
🌐 Memoization
通过使用 memo 对计算的字符串进行记忆化来优化你的组件:
🌐 Optimize your components by memoizing computed strings using memo:
tsx
import { memo } from 'hono/jsx'
const Header = memo(() => <header>Welcome to Hono</header>)
const Footer = memo(() => <footer>Powered by Hono</footer>)
const Layout = (
<div>
<Header />
<p>Hono is cool!</p>
<Footer />
</div>
)上下文
🌐 Context
通过使用 useContext,你可以在组件树的任何层级全局共享数据,而无需通过 props 传递值。
🌐 By using useContext, you can share data globally across any level of the Component tree without passing values through props.
tsx
import type { FC } from 'hono/jsx'
import { createContext, useContext } from 'hono/jsx'
const themes = {
light: {
color: '#000000',
background: '#eeeeee',
},
dark: {
color: '#ffffff',
background: '#222222',
},
}
const ThemeContext = createContext(themes.light)
const Button: FC = () => {
const theme = useContext(ThemeContext)
return <button style={theme}>Push!</button>
}
const Toolbar: FC = () => {
return (
<div>
<Button />
</div>
)
}
// ...
app.get('/', (c) => {
return c.html(
<div>
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
</div>
)
})异步组件
🌐 Async Component
hono/jsx 支持异步组件,所以你可以在你的组件中使用 async/await。如果你用 c.html() 渲染它,它会自动等待。
tsx
const AsyncComponent = async () => {
await new Promise((r) => setTimeout(r, 1000)) // sleep 1s
return <div>Done!</div>
}
app.get('/', (c) => {
return c.html(
<html>
<body>
<AsyncComponent />
</body>
</html>
)
})悬疑 Experimental
🌐 Suspense Experimental
类似 React 的 Suspense 功能已可用。 如果你用 Suspense 封装异步组件,fallback 中的内容会先被渲染,一旦 Promise 被解决,等待的内容将会显示。 你可以将它与 renderToReadableStream() 一起使用。
🌐 The React-like Suspense feature is available. If you wrap the async component with Suspense, the content in the fallback will be rendered first, and once the Promise is resolved, the awaited content will be displayed. You can use it with renderToReadableStream().
tsx
import { renderToReadableStream, Suspense } from 'hono/jsx/streaming'
//...
app.get('/', (c) => {
const stream = renderToReadableStream(
<html>
<body>
<Suspense fallback={<div>loading...</div>}>
<Component />
</Suspense>
</body>
</html>
)
return c.body(stream, {
headers: {
'Content-Type': 'text/html; charset=UTF-8',
'Transfer-Encoding': 'chunked',
},
})
})错误边界 Experimental
🌐 ErrorBoundary Experimental
你可以使用 ErrorBoundary 捕获子组件中的错误。
🌐 You can catch errors in child components using ErrorBoundary.
在下面的示例中,如果发生错误,它将显示 fallback 中指定的内容。
🌐 In the example below, it will show the content specified in fallback if an error occurs.
tsx
function SyncComponent() {
throw new Error('Error')
return <div>Hello</div>
}
app.get('/sync', async (c) => {
return c.html(
<html>
<body>
<ErrorBoundary fallback={<div>Out of Service</div>}>
<SyncComponent />
</ErrorBoundary>
</body>
</html>
)
})ErrorBoundary 也可以与异步组件和 Suspense 一起使用。
tsx
async function AsyncComponent() {
await new Promise((resolve) => setTimeout(resolve, 2000))
throw new Error('Error')
return <div>Hello</div>
}
app.get('/with-suspense', async (c) => {
return c.html(
<html>
<body>
<ErrorBoundary fallback={<div>Out of Service</div>}>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
</body>
</html>
)
})StreamingContext Experimental
你可以使用 StreamingContext 为像 Suspense 和 ErrorBoundary 这样的流式组件提供配置。这对于向这些组件生成的脚本标签添加用于内容安全策略(CSP)的 nonce 值很有用。
🌐 You can use StreamingContext to provide configuration for streaming components like Suspense and ErrorBoundary. This is useful for adding nonce values to script tags generated by these components for Content Security Policy (CSP).
tsx
import { Suspense, StreamingContext } from 'hono/jsx/streaming'
// ...
app.get('/', (c) => {
const stream = renderToReadableStream(
<html>
<body>
<StreamingContext
value={{ scriptNonce: 'random-nonce-value' }}
>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent />
</Suspense>
</StreamingContext>
</body>
</html>
)
return c.body(stream, {
headers: {
'Content-Type': 'text/html; charset=UTF-8',
'Transfer-Encoding': 'chunked',
'Content-Security-Policy':
"script-src 'nonce-random-nonce-value'",
},
})
})scriptNonce 值将自动添加到由 Suspense 和 ErrorBoundary 组件生成的任何 <script> 标签中。
🌐 The scriptNonce value will be automatically added to any <script> tags generated by Suspense and ErrorBoundary components.
与 html 集成中间件
🌐 Integration with html Middleware
将 JSX 和 HTML 中间件结合起来,实现强大的模板功能。 有关详细信息,请查阅 HTML 中间件文档。
🌐 Combine the JSX and HTML middlewares for powerful templating. For in-depth details, consult the HTML middleware documentation.
tsx
import { Hono } from 'hono'
import { html } from 'hono/html'
const app = new Hono()
interface SiteData {
title: string
children?: any
}
const Layout = (props: SiteData) =>
html`<!doctype html>
<html>
<head>
<title>${props.title}</title>
</head>
<body>
${props.children}
</body>
</html>`
const Content = (props: { siteData: SiteData; name: string }) => (
<Layout {...props.siteData}>
<h1>Hello {props.name}</h1>
</Layout>
)
app.get('/:name', (c) => {
const { name } = c.req.param()
const props = {
name: name,
siteData: {
title: 'JSX with html sample',
},
}
return c.html(<Content {...props} />)
})
export default app使用 JSX 渲染器中间件
🌐 With JSX Renderer Middleware
JSX 渲染中间件 让你可以更轻松地使用 JSX 创建 HTML 页面。
🌐 The JSX Renderer Middleware allows you to create HTML pages more easily with the JSX.
覆盖类型定义
🌐 Override type definitions
你可以覆盖类型定义以添加自定义元素和属性。
🌐 You can override the type definition to add your custom elements and attributes.
ts
declare module 'hono/jsx' {
namespace JSX {
interface IntrinsicElements {
'my-custom-element': HTMLAttributes & {
'x-event'?: 'click' | 'scroll'
}
}
}
}