跳转到正文

JSX ​

可以通过 hono/jsx 使用 JSX 语法编写 HTML。

虽然 hono/jsx 可以在客户端运行,但它最常用于服务端内容渲染。下面介绍服务端和客户端共有的一些 JSX 功能。

配置 ​

要使用 JSX,请修改 tsconfig.json:

tsconfig.json:

json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}

也可以使用 pragma 指令:

ts
/** @jsx jsx */
/** @jsxImportSource hono/jsx */

对于 Deno,需要修改 deno.json,而非 tsconfig.json:

json
{
  "compilerOptions": {
    "jsx": "precompile",
    "jsxImportSource": "@hono/hono/jsx"
  }
}

用法 ​

说明

如果直接从快速入门开始,主文件的扩展名为 .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

元数据提升 ​

可以直接在组件中编写 <title>、<link> 和 <meta> 等文档元数据标签。这些标签会自动提升到文档的 <head> 部分。当 <head> 元素的渲染位置与决定元数据内容的组件相距较远时,这尤其有用。

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 app

说明

提升过程中不会移除已有元素。后出现的元素会添加到末尾。例如,如果 <head> 中已有 <title>Default</title>,且某个组件渲染了 <title>Page Title</title>,那么两个标题都会出现在 head 中。

Fragment ​

使用 Fragment 可以将多个元素组合起来,而不添加额外节点:

tsx
import { Fragment } from 'hono/jsx'

const List = () => (
  <Fragment>
    <p>first child</p>
    <p>second child</p>
    <p>third child</p>
  </Fragment>
)

配置正确时,也可以使用 <></> 简写。

tsx
const List = () => (
  <>
    <p>first child</p>
    <p>second child</p>
    <p>third child</p>
  </>
)

PropsWithChildren ​

可以使用 PropsWithChildren,正确推断函数组件中的子元素类型。

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 ​

要直接插入 HTML,请使用 dangerouslySetInnerHTML:

tsx
app.get('/foo', (c) => {
  const inner = { __html: 'JSX &middot; SSR' }
  const Div = <div dangerouslySetInnerHTML={inner} />
})

记忆化 ​

使用 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>
)

上下文 ​

通过 useContext,可以在组件树任意层级间全局共享数据,无需通过 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>
  )
})

异步组件 ​

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>
  )
})

Suspense Experimental ​

可以使用类似 React 的 Suspense 功能。 如果使用 Suspense 包裹异步组件,会先渲染 fallback 中的内容;Promise 完成后,再显示等待得到的内容。 可以将其与 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',
    },
  })
})

ErrorBoundary Experimental ​

可以使用 ErrorBoundary 捕获子组件中的错误。

在下面的示例中,如果发生错误,会显示 fallback 指定的内容。

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 等流式组件提供配置。例如,为这些组件生成的 script 标签添加 nonce 值,以支持内容安全策略(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> 标签中。

与 html 中间件集成 ​

结合 JSX 和 HTML 中间件,可以实现强大的模板功能。 详细信息请参阅 HTML 中间件文档。

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 渲染器中间件 ​

JSX 渲染器中间件让你更轻松地使用 JSX 创建 HTML 页面。

覆盖类型定义 ​

可以覆盖类型定义,添加自定义元素和属性。

ts
declare module 'hono/jsx' {
  namespace JSX {
    interface IntrinsicElements {
      'my-custom-element': HTMLAttributes & {
        'x-event'?: 'click' | 'scroll'
      }
    }
  }
}

基于 MIT 许可证发布。