---
title: "Shiki v1.0 的演变"
description: "Shiki v1.0 带来了许多改进和特性 - 看看 Nuxt 如何推动 Shiki 的演变！"
canonical_url: "https://nuxt.zhcndoc.com/blog/shiki-v1"
---
# Shiki v1.0 的演变

> Shiki v1.0 带来了许多改进和特性 - 看看 Nuxt 如何推动 Shiki 的演变！

[Shiki](https://github.com/shikijs/shiki) 是一个使用 [TextMate 语法和主题](https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide#textmate-grammars) 的语法高亮器，与驱动 VS Code 的引擎相同。它为你的代码片段提供了最准确和最美观的语法高亮。它由 [Pine Wu](https://github.com/octref) 在 2018 年创建，当时他是 VS Code 团队的一部分。它最初是作为一个使用 [Oniguruma](https://github.com/microsoft/vscode-oniguruma) 进行语法高亮的实验开始的。

与 [Prism](https://prismjs.com/) 和 [Highlight.js](https://highlightjs.org/) 等现有的语法高亮器不同，它们设计为在浏览器中运行，Shiki 采取了不同的方法，通过 **提前高亮**。它将高亮后的 HTML 发送到客户端，实现了准确和美观的语法高亮，且 **零 JavaScript**。它很快流行起来，特别是对于静态网站生成器和文档站点来说。

<collapsible name="Shiki 示例">

例如，使用下面的代码片段：

```ts
export default defineNuxtConfig({
  modules: [
    '@nuxt/content',
  ],
})
```

Shiki 将生成以下 HTML：

```html
<pre class="shiki material-theme-palenight" style="background-color:#292D3E;color:#babed8" tabindex="0">
  <code>
    <span class="line"><span style="color:#89DDFF;font-style:italic">export</span><span style="color:#89DDFF;font-style:italic"> default</span><span style="color:#82AAFF"> defineNuxtConfig</span><span style="color:#BABED8">(</span><span style="color:#89DDFF">{</span></span>
    <span class="line"><span style="color:#F07178">  modules</span><span style="color:#89DDFF">:</span><span style="color:#BABED8"> [</span></span>
    <span class="line"><span style="color:#89DDFF">    '</span><span style="color:#C3E88D">@nuxt/content</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
    <span class="line"><span style="color:#BABED8">  ]</span><span style="color:#89DDFF">,</span></span>
    <span class="line"><span style="color:#89DDFF">}</span><span style="color:#BABED8">)</span></span>
  </code>
</pre>
```

如果你阅读它，可能会觉得有点压倒性，但 **这段 HTML 在任何地方都能正常工作，无需任何 JavaScript 或 CSS**。TextMate 语法具有非常丰富的每种标记类型的表示（TextMate 作用域）。由于 Shiki 将所有标记展平为样式化的 span，它实现了大多数传统基于 CSS 的高亮器难以实现的准确结果。

</collapsible>

虽然 Shiki 很出色，但它仍然是一个设计为在 Node.js 上运行的库。这意味着它仅限于仅能高亮静态代码，并且对于动态代码会有困难，因为 Shiki 不在浏览器中工作。此外，Shiki 依赖于 Oniguruma 的 WASM 二进制文件，以及 JSON 中的一堆沉重的语法和主题文件。它使用 Node.js 文件系统和路径解析来加载这些文件，这在浏览器中是不可用的。

为了改善这种情况，我 [启动了这个 RFC](https://github.com/shikijs/shiki/issues/91) 后来通过 [这个 PR](https://github.com/shikijs/shiki/pull/109) 并在 Shiki v0.9 中发布。虽然它抽象了文件加载层，以便根据环境使用 fetch 或文件系统，但你仍然需要手动在捆绑包或 CDN 中提供语法和主题文件的某个地方，然后调用 `setCDN` 方法来告诉 Shiki 在哪里加载这些文件。

解决方案并不完美，但至少它使 Shiki 能够在浏览器中运行以高亮动态内容。从那时起，我们一直在使用这种方法 - 直到本文的故事开始。

## 开始

Nuxt 正在大力推动 [web 走向边缘](https://nuxt.zhcndoc.com/blog/nuxt-on-the-edge)，通过降低延迟和提高性能使 web 更易于访问。像 CDN 服务器一样，边缘托管服务如 [CloudFlare Workers](https://workers.cloudflare.com/) 部署在世界各地。用户从最近的边缘服务器获取内容，而不需要往返数千英里之外的源服务器。它提供了惊人的好处，但也带来了一些权衡。例如，边缘服务器使用受限的运行时环境。CloudFlare Workers 也不支持文件系统访问，通常不保留请求之间的状态。虽然 Shiki 的主要开销是预先加载语法和主题，但这在边缘环境中可能不会很好地工作。

一切始于我和 [Sébastien](https://x.com/Atinux) 之间的一次聊天。我们正在尝试让使用 Shiki 高亮代码块的 [Nuxt Content](https://github.com/nuxt/content) 在边缘环境中工作。

![Sébastien 和 Anthony 之间的聊天记录](https://nuxt.zhcndoc.com/assets/blog/shiki-start-chat.png)

我开始通过修补 [`shiki-es`](https://github.com/pi0/shiki-es)（由 [Pooya Parsa](https://github.com/pi0) 提供的 Shiki 的 ESM 构建）来进行实验，将语法和主题文件转换为 [ECMAScript 模块 (ESM)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)，以便构建工具能够理解并打包。这是为了创建 CloudFlare Workers 可以消费的代码包，而无需使用文件系统或进行网络请求。

```ts [Before - 从文件系统读取 JSON 资源]
import fs from 'fs/promises'

const cssGrammar = JSON.parse(await fs.readFile('../langs/css.json', 'utf-8'))
```

```ts [After - 使用 ESM 导入]
const cssGrammar = await import('../langs/css.mjs').then(m => m.default)
```

我们需要将 JSON 文件包装成 ESM 的内联字面量，这样我们就可以使用 [`import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) 动态导入它们。不同之处在于 `import()` 是一个标准 JavaScript 特性，可以在任何地方工作，而 `fs.readFile` 是一个 Node.js 特定的 API，只在 Node.js 中工作。拥有静态的 `import()` 也会使像 [Rollup](https://rollupjs.org/) 和 [webpack](https://webpack.js.org/) 这样的打包器能够构建模块关系图，并 [将打包的代码作为块发出](https://rollupjs.org/tutorial/#code-splitting)。

然后，我意识到实际上需要更多才能使其在边缘运行时上工作。由于打包器期望在构建时解析导入（这意味着要支持所有语言和主题），我们需要在代码库中每个语法和主题文件中的所有导入语句列表。这最终会得到一个巨大的打包大小，其中包含许多你可能实际上并不使用的语法和主题。这个问题在边缘环境中尤为重要，打包大小对性能至关重要。

所以，我们需要找到一个更好的折中方案使其工作得更好。

## 派生 - Shikiji

知道这可能会根本改变 Shiki 的工作方式，并且因为我们不想冒险用我们的实验破坏现有的 Shiki 用户，我开始了 Shiki 的一个分支，名为 [Shikiji](https://github.com/antfu/shikiji)。我从头重写了代码，同时考虑到之前的 API 设计决策。目标是使 Shiki 运行时不可知，性能高且高效，就像我们在 [UnJS](https://github.com/unjs) 中的哲学一样。

为了实现这一点，我们需要使 Shikiji 完全 ESM 友好，纯净且 [可摇树](https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking)。这一直延伸到 Shiki 的依赖项，如 [`vscode-oniguruma`](https://github.com/microsoft/vscode-oniguruma) 和 [`vscode-textmate`](https://github.com/microsoft/vscode-textmate)，它们以 [Common JS (CJS)](https://requirejs.org/docs/commonjs.html) 格式提供。`vscode-oniguruma` 还包含由 [`emscripten`](https://github.com/emscripten-core/emscripten) 生成的 WASM 绑定，其中包含 [悬挂的承诺](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/no-floating-promises.md)，这将使 CloudFlare Workers 无法完成请求。我们最终通过将 WASM 二进制嵌入到 [base64 字符串](https://en.wikipedia.org/wiki/Base64) 中，并将其作为 ES 模块发货，手动重写 WASM 绑定以避免悬挂的承诺，并将 [`vscode-textmate`](https://github.com/shikijs/shiki/blob/main/CONTRIBUTING.md#clone) 进行厂商化，从源代码编译并产生高效的 ESM 输出。

最终结果非常有希望。我们设法让 Shikiji 在任何运行时环境中工作，甚至可以 [从 CDN 导入并在浏览器中运行](https://shiki.style/guide/install#cdn-usage)，只需一行代码。

我们还抓住机会改进了 Shiki 的 API 和内部架构。我们从简单的字符串连接转换为使用 [`hast`](https://github.com/syntax-tree/hast)，为生成 HTML 输出创建了抽象语法树 (AST)。这为公开一个 [转换器 API](https://shiki.style/guide/transformers) 打开了可能性，允许用户修改中间 HAST 并进行许多以前很难实现的很酷的集成。

深色/浅色模式支持 [是一个经常被请求的功能](https://github.com/shikijs/shiki/issues/33)。由于 Shiki 采用静态方法，因此无法在渲染时动态更改主题。过去的解决方案是生成两次高亮的 HTML，并根据用户的偏好切换它们的可见性——这效率不高，因为这会重复负载，或者使用了 [CSS 变量主题](https://github.com/shikijs/shiki/pull/212)，这失去了 Shiki 在细粒度高亮方面的优势。随着 Shikiji 的新架构，我退后一步重新思考了这个问题，并 [提出了思路](https://github.com/shikijs/shiki/issues/33#issuecomment-1676362336)，将常见的标记拆分并将多个主题合并为内联 CSS 变量，这样既提供了高效的输出，又与 Shiki 的哲学保持一致。您可以在 [Shiki 的文档](https://shiki.style/guide/dual-themes) 中了解更多信息。

为了使迁移更容易，我们还创建了 [`shikiji-compat` 兼容性层](https://shikiji.netlify.app/guide/compat)，它使用 Shikiji 的新基础并提供向后兼容的 API。

为了让 Shikiji 在 Cloudflare Workers 上工作，我们面临最后一个挑战，它们不支持 [从内联二进制数据启动 WASM 实例](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiate_static)。相反，出于安全原因，它需要导入静态的 `.wasm` 资源。这意味着我们的 "All-in-ESM" 方法在 CloudFlare 上并不适用。这将需要用户为不同的 WASM 来源提供额外的工作，这使得体验比我们预期的更加困难。目前，[Pooya Parsa](https://github.com/pi0) 介入并制作了通用层 [`unjs/unwasm`](https://github.com/unjs/unwasm)，它支持即将到来的 [WebAssembly/ES Module Integration](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration) 提案。它已经被集成到 [Nitro 以拥有自动化的 WASM 目标](https://github.com/unjs/nitro/pull/2037) 中。我们希望 `unwasm` 将帮助开发人员在处理 WASM 时获得更好的体验。

总的来说，Shikiji 的重写效果很好。[Nuxt Content](https://github.com/nuxt/content)、[VitePress](https://vitepress.dev/) 和 [Astro](https://astro.build/) 已经迁移到它。我们收到的反馈也非常积极。

## 合并回

我是 Shiki 的团队成员，并且不时帮助发布。虽然 [Pine](https://github.com/octref) 是 Shiki 的负责人，他忙于其他事情，Shiki 的迭代放慢了。在 Shikiji 的实验期间，我 [提出了一些改进](https://github.com/shikijs/shiki/issues/510)，这可能有助于 Shiki 获得现代结构。虽然大家普遍同意这个方向，但会有很多工作要做，没有人开始工作。

虽然我们很高兴使用 Shikiji 解决了我们的问题，但我们当然不想看到社区被两个版本的 Shiki 分割。在与 Pine 的一次通话中，我们达成了将两个项目合并为一个的共识：

<read-more color="purple" icon="i-octicon-git-merge-24" to="https://github.com/shikijs/shiki/pull/557">

feat!: 将 Shikiji 合并回 Shiki 用于 v1.0 <span className="opacity-50">

#557

</span>
</read-more>

我们非常高兴看到我们在 Shikiji 中的工作已经合并回 Shiki，这不仅对我们自己有用，也使整个社区受益。有了这次合并，它 **解决了我们多年来在 Shiki 中约 95% 的开放问题**：

![Shikiji 合并回 Shiki](https://nuxt.zhcndoc.com/assets/blog/shiki-merge-pr.png)

Shiki 现在也有了 [一个全新的文档站点](https://shiki.style/)，你还可以在你的浏览器中直接使用它（感谢不可知的方法！）。许多框架现在与 Shiki 内置集成，也许你已经在某个地方使用了它！

## Twoslash

[Twoslash](https://github.com/twoslashes/twoslash) 是一个集成工具，用于从 [TypeScript 语言服务](https://github.com/microsoft/TypeScript/wiki/Using-the-Language-Service-API) 获取类型信息并生成到你的代码片段中。它本质上使你的静态代码片段具有类似于 VS Code 编辑器的悬停类型信息。它是由 [Orta Therox](https://github.com/orta) 为 [TypeScript 文档网站](https://github.com/microsoft/TypeScript-Website) 制作的，你可以在这里找到 [原始源码](https://github.com/microsoft/TypeScript-Website/tree/v2/packages/ts-twoslasher)。Orta 还为 Shiki v0.x 版本创建了 [Twoslash 集成](https://github.com/shikijs/twoslash)。那时，Shiki [没有合适的插件系统](https://github.com/shikijs/shiki/issues/380)，这使得 `shiki-twoslash` 必须构建为 Shiki 的一个包装器，这使得设置起来有点困难，因为现有的 Shiki 集成无法直接与 Twoslash 配合使用。

我们还抓住了在重写 Shikiji 时修订 Twoslash 集成的机会，也是一种 [自食其果](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) 的方式，验证了可扩展性。有了新的 HAST 内部，我们能够 [将 Twoslash 作为转换器插件集成](https://shiki.style/packages/twoslash)，使其在 Shiki 工作的所有地方工作，并且以可组合的方式与其他转换器一起使用。

有了这个，我们开始考虑我们可能可以让 Twoslash 在 [nuxt.com](https://nuxt.zhcndoc.com/) 上工作，你现在正在看的这个网站。nuxt.com 在幕后使用 [Nuxt Content](https://github.com/nuxt/content)，与其他文档工具如 VitePress 不同，Nuxt Content 提供的好处之一是它能够处理动态内容并在边缘运行。由于 Twoslash 依赖于 TypeScript 以及来自你的依赖项的巨大类型模块图，将所有这些东西运送到边缘或浏览器并不理想。听起来很棘手，但挑战接受！

我们首先想到的是从 CDN 按需获取类型，使用你在 [TypeScript playground](https://www.typescriptlang.org/play) 上看到的 [自动类型获取](https://github.com/microsoft/TypeScript-Website/tree/v2/packages/ata) 技术。我们制作了 [`twoslash-cdn`](https://github.com/antfu/twoslash-cdn)，允许 Twoslash 在任何运行时中运行。然而，这听起来仍然不是最理想的解决方案，因为它仍然需要进行许多网络请求，这可能会破坏在边缘运行的目的。

在底层工具的几次迭代之后（例如在 [`@nuxtjs/mdc`](https://github.com/nuxt-modules/mdc/pull/129)，Nuxt Content 使用的 markdown 编译器），我们设法采取了混合方法，并制作了 [`nuxt-content-twoslash`](https://github.com/antfu/nuxt-content-twoslash)，它在构建时运行 Twoslash 并将结果缓存以供边缘渲染。这样，我们可以避免将任何额外的依赖项运送到最终捆绑包中，但仍然在网站上拥有丰富的交互式代码片段：

```vuetwoslash
<script setup>
// 尝试悬停在下面的标识符上以查看类型
const count = useState('counter', () => 0)
const double = computed(() => count.value * 2)
</script>

<template>
  <button>Count is: {{ count }}</button>
  <div>Double is: {{ double }}</div>
</template>
```

在此期间，我们还抓住机会与 Orta 一起重构 [Twoslash](https://github.com/twoslashes/twoslash)，使其具有更高效和现代的结构。它还允许我们拥有 [`twoslash-vue`](https://github.com/twoslashes/twoslash/tree/main/packages/twoslash-vue)，它提供了你在上面玩的 [Vue SFC](https://vue.zhcndoc.com/guide/scaling-up/sfc.html) 支持。它由 [Volar.js](https://github.com/volarjs/volar.js) 和 [`vuejs/language-tools`](https://github.com/vuejs/language-tools) 提供动力。随着 Volar 变得框架不可知，并且框架共同工作，我们期待着看到这样的集成在未来扩展到更多的语法，如 Astro 和 Svelte 组件文件。

## 集成

如果你想在自己的网站上尝试 Shiki，你可以在这里找到我们已经制作的一些集成：

- [Nuxt](https://shiki.style/packages/nuxt)
  - 如果使用 [Nuxt Content](https://content.nuxt.com/)，Shiki 是 [内置的](https://content.nuxt.com/get-started/configuration#highlight)。对于 Twoslash，你可以在此基础上添加 [`nuxt-content-twoslash`](https://github.com/antfu/nuxt-content-twoslash)。
  - 如果不是，你可以使用 [`nuxt-shiki`](https://github.com/pi0/nuxt-shiki) 将 Shiki 作为 Vue 组件或组合式函数使用。
- [VitePress](https://shiki.style/packages/vitepress)
  - Shiki 是 [内置的](https://vitepress.dev/guide/markdown#syntax-highlighting-in-code-blocks)。对于 Twoslash，你可以使用 [`vitepress-twoslash`](https://shiki.style/packages/vitepress#twoslash)。
- 低级集成 - Shiki 为 markdown 编译器提供官方集成：

  - [`markdown-it`](https://shiki.style/packages/markdown-it) - [`markdown-it`](https://github.com/markdown-it/markdown-it) 的插件
  - [`rehype`](https://shiki.style/packages/rehype) - [`rehype`](https://github.com/rehypejs/rehype) 的插件

在 [Shiki 的文档](https://shiki.style/) 上查看更多集成

## 结论

**我们在 Nuxt 的使命不仅是为开发者打造一个更好的框架，而且还要让整个前端和 Web 生态系统变得更好。** 我们不断突破边界，并支持现代 Web 标准和最佳实践。我们希望你喜欢新的 [Shiki](https://github.com/shikijs/shiki)、[unwasm](https://github.com/unjs/unwasm)、[Twoslash](https://github.com/twoslashes/twoslash) 以及我们在让 Nuxt 和 Web 变得更好的过程中制作的许多其他工具。


## Sitemap

See the full [sitemap](https://nuxt.zhcndoc.com/sitemap.md) for all pages.
