Skip to content

Next.js

Shiki 并没有为 Next.js 提供官方集成,但在 Next.js 应用中使用 Shiki 其实非常简单。

INFO

在边缘运行时上使用 Shiki 可能会导致意外问题,Shiki 依赖于懒导入来加载语言和主题。

建议使用无服务器运行时。

React 服务器组件

由于服务器组件仅在服务器上运行,因此您可以使用捆绑的高亮器,而不必担心捆绑大小。

tsx
import type { BundledLanguage } from 'shiki'
import { codeToHtml } from 'shiki'

export default function Page() {
  return (
    <main>
      <CodeBlock lang="ts">
        {[
          'console.log("Hello")',
          'console.log("World")',
        ].join('\n')}
      </CodeBlock>
    </main>
  )
}

interface Props {
  children: string
  lang: BundledLanguage
}

async function CodeBlock(props: Props) {
  const out = await codeToHtml(props.children, {
    lang: props.lang,
    theme: 'github-dark'
  })

  return <div dangerouslySetInnerHTML={{ __html: out }} />
}

自定义组件

您还可以调用 codeToHast 来获取 HTML 抽象语法树,并使用 hast-util-to-jsx-runtime 渲染它。通过这种方法,您可以渲染自己的 precode 组件。

tsx
import type { JSX } from 'react'
import type { BundledLanguage } from 'shiki'
import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
import { Fragment } from 'react'
import { jsx, jsxs } from 'react/jsx-runtime'
import { codeToHast } from 'shiki'

export default function Page() {
  return (
    <main>
      <CodeBlock lang="ts">
        {[
          'console.log("Hello")',
          'console.log("World")',
        ].join('\n')}
      </CodeBlock>
    </main>
  )
}

interface Props {
  children: string
  lang: BundledLanguage
}

async function CodeBlock(props: Props) {
  const out = await codeToHast(props.children, {
    lang: props.lang,
    theme: 'github-dark'
  })

  return toJsxRuntime(out, {
    Fragment,
    jsx,
    jsxs,
    components: {
      // 您自定义的 `pre` 元素
      pre: props => <pre data-custom-codeblock {...props} />
    },
  }) as JSX.Element
}

React 客户端组件

对于客户端组件,它们在服务器上预渲染并在客户端进行水合/渲染。 我们可以先创建一个客户端 CodeBlock 组件。

为高亮器创建 shared.ts

ts
import type { JSX } from 'react'
import type { BundledLanguage } from 'shiki/bundle/web'
import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
import { Fragment } from 'react'
import { jsx, jsxs } from 'react/jsx-runtime'
import { codeToHast } from 'shiki/bundle/web'

export async function highlight(code: string, lang: BundledLanguage) {
  const out = await codeToHast(code, {
    lang,
    theme: 'github-dark'
  })

  return toJsxRuntime(out, {
    Fragment,
    jsx,
    jsxs,
  }) as JSX.Element
}

在您的 codeblock.tsx 中:

tsx
'use client'
import { JSX, useLayoutEffect, useState } from 'react'
import { highlight } from './shared'

export function CodeBlock({ initial }: { initial?: JSX.Element }) {
  const [nodes, setNodes] = useState(initial)

  useLayoutEffect(() => {
    void highlight('console.log("Rendered on client")', 'ts').then(setNodes)
  }, [])

  return nodes ?? <p>Loading...</p>
}

initial 属性可以从服务器组件传递,以便在服务器上预渲染代码块。

在您的 page.tsx 中:

tsx
import { CodeBlock } from './codeblock'
import { highlight } from './shared'

export default async function Page() {
  // `initial` 是可选的。
  return (
    <main>
      <CodeBlock initial={await highlight('console.log("Rendered on server")', 'ts')} />
    </main>
  )
}

INFO

上述示例使用了 shiki/bundle/web 包。您可以将其更改为 细粒度捆绑,以完全控制捆绑的语言/主题。

性能

Shiki 会懒加载请求的语言和主题,Next.js 打包器可以自动处理懒导入。 导入 shiki 或其 web 包对于大多数 Next.js 应用来说效率足够高,细粒度捆绑不会显著影响捆绑大小。

此外,您可以使用 createHighlighter API 预加载特定的语言和主题。 请参阅 高亮器使用 以获取更多详细信息。

高亮器实例

如果您将高亮器(不带 await)定义为全局变量,您可以直接从服务器和客户端组件中引用它。

ts
import { createHighlighter } from 'shiki'

const highlighter = createHighlighter({
  themes: ['nord'],
  langs: ['javascript'],
})

// 在异步服务器组件内部,或客户端的 `useEffect`
const html = (await highlighter).codeToHtml('const a = 1', {
  lang: 'javascript',
  theme: 'nord'
})