Nuxt Nation 大会即将到来。加入我们,时间为 11 月 12 日至 13 日。
文章·  

Shiki v1.0 的演进

Shiki v1.0 带来许多改进和功能 - 看看 Nuxt 如何推动 Shiki 的演进!

Shiki 是一款语法高亮库,它使用 TextMate 语法和主题,与 VS Code 使用的引擎相同。它为代码片段提供了最准确和美观的语法高亮显示之一。它由 Pine Wu 于 2018 年创建,当时他还是 VS Code 团队的一员。它最初是一个实验,旨在使用 Oniguruma 进行语法高亮。

与现有的语法高亮库(如 PrismHighlight.js)旨在在浏览器中运行不同,Shiki 采取了一种不同的方法,即 **提前高亮**。它将高亮后的 HTML 发送到客户端,使用 **零 JavaScript** 生成准确且美观的语法高亮。它很快流行起来,成为非常受欢迎的选择,尤其是在静态网站生成器和文档网站中。

虽然 Shiki 很棒,但它仍然是一个设计在 Node.js 上运行的库。这意味着它仅限于高亮静态代码,并且在处理动态代码时会遇到问题,因为 Shiki 不在浏览器中工作。此外,Shiki 依赖于 Oniguruma 的 WASM 二进制文件,以及一堆以 JSON 格式存储的繁重的语法和主题文件。它使用 Node.js 文件系统和路径解析来加载这些文件,这在浏览器中是无法访问的。

为了改善这种情况,我 启动了这份 RFC,后来通过 这份 PR 合并到 Shiki v0.9 中。虽然它将文件加载层抽象化,以便根据环境使用 fetch 或文件系统,但使用起来仍然相当复杂,因为您需要在捆绑包或 CDN 中手动提供语法和主题文件,然后调用 setCDN 方法来告诉 Shiki 在哪里加载这些文件。

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

开始

Nuxt 正在投入大量精力将 网络推向边缘**,通过降低延迟和提高性能使网络更易访问。与 CDN 服务器类似,CloudFlare Workers** 等边缘托管服务部署在世界各地。用户从最近的边缘服务器获取内容,无需往返数千英里外的源服务器。除了它提供的强大优势外,它也有一些权衡。例如,边缘服务器使用受限的运行时环境。CloudFlare Workers 也不支持文件系统访问,通常不会在请求之间保留状态。虽然 Shiki 的主要开销是在启动时加载语法和主题,但这在边缘环境中效果不佳。

这一切都始于我和 Sébastien 的一次聊天。我们试图让 Nuxt Content(它使用 Shiki 来高亮代码块)在边缘运行。

Chat History Between Sébastien and Anthony

我开始通过在本地修补 shiki-es(由 Pooya Parsa 编写的 Shiki 的 ESM 构建版本)来进行实验,将语法和主题文件转换为 ECMAScript 模块 (ESM)**,以便构建工具可以理解和捆绑它们。这样做是为了创建 CloudFlare Workers 可以使用的代码捆绑包,而无需使用文件系统或发出网络请求。

我们需要将 JSON 文件包装到 ESM 中作为内联字面量,以便我们可以使用 import() 动态导入它们。区别在于 import() 是一个在任何地方都适用的标准 JavaScript 特性,而 fs.readFile 是一个仅在 Node.js 中有效的 Node.js 特定 API。静态使用 import() 还可以使像 Rollupwebpack 这样的捆绑器能够构建模块关系图并 将捆绑后的代码作为块发出**。

然后,我意识到要使其在边缘运行时工作,还需要做更多工作。由于捆绑器期望在构建时能够解析导入(这意味着为了支持所有语言和主题),我们需要在代码库中的每个语法和主题文件中列出所有导入语句。这最终会导致一个巨大的捆绑包大小,其中包含许多您可能实际上不会使用的语法和主题。这个问题在边缘环境中尤其重要,因为捆绑包大小对性能至关重要。

因此,我们需要找到一个更好的中间地带,使其更好地工作。

分支 - Shikiji

知道这可能会从根本上改变 Shiki 的工作方式,并且由于我们不想让我们的实验破坏现有的 Shiki 用户,所以我创建了一个名为 Shikiji 的 Shiki 分支。我从头重写了代码,同时牢记之前的 API 设计决策。目标是使 Shiki 与运行时无关,并且像我们在 UnJS 中的理念一样,具有高性能和效率。

为了实现这一点,我们需要使 Shikiji 完全与 ESM 兼容,纯净且 可摇树**。这涉及到 Shiki 的所有依赖项,例如 vscode-onigurumavscode-textmate,它们以 Common JS (CJS)** 格式提供。vscode-oniguruma 还包含由 emscripten 生成的 WASM 绑定,其中包含 悬空 Promise**,这会导致 CloudFlare Workers 无法完成请求。我们最终将 WASM 二进制文件嵌入到 base64 字符串**中并将其作为 ES 模块发送,手动重写 WASM 绑定以避免悬空 Promise,并 vscode-textmate 作为供应商**,以从其源代码编译并生成高效的 ESM 输出。

最终结果非常有希望。我们设法让 Shikiji 在任何运行时环境中工作,甚至可以通过一行代码从 CDN 导入并在浏览器中运行 导入并在浏览器中运行**。

我们还借此机会改进了 Shiki 的 API 和内部架构。我们从简单的字符串连接切换到使用 hast,为生成 HTML 输出创建抽象语法树 (AST)。这为公开 Transformers API** 创造了可能性,允许用户修改中间 HAST 并进行许多很酷的集成,这些集成在以前很难实现。

黑暗/浅色模式支持 是一个经常被请求的功能**。由于 Shiki 采用静态方法,因此无法在渲染时动态更改主题。过去解决方法是生成两次高亮 HTML,并根据用户的偏好切换它们的可见性 - 这效率不高,因为它会复制有效负载,或者使用 CSS 变量主题**,这会丢失 Shiki 擅长的细粒度高亮。凭借 Shikiji 新的架构,我退一步重新思考了这个问题,并 提出了一个想法**,即将常用标记分解并合并多个主题作为内联 CSS 变量,这可以提供高效的输出,同时符合 Shiki 的理念。您可以在 Shiki 的文档** 中了解更多信息。

为了简化迁移,我们还创建了 shikiji-compat 兼容层**,它使用 Shikiji 的新基础并提供向后兼容 API。

为了让 Shikiji 在 Cloudflare Workers 上工作,我们还有一个最后的挑战,因为它们不支持从内联二进制数据 初始化 WASM 实例**。相反,出于安全原因,它需要导入静态 .wasm 资源。这意味着我们的“全 ESM”方法在 CloudFlare 上效果不佳。这需要用户额外的工作来提供不同的 WASM 源,这使得体验比我们预期的更困难。此时,Pooya Parsa 介入并创建了通用层 unjs/unwasm,它支持即将推出的 WebAssembly/ES 模块集成** 提案。它已集成到 Nitro 中以实现自动化的 WASM 目标**。我们希望 unwasm 能够帮助开发人员在使用 WASM 时获得更好的体验。

总的来说,Shikiji 重写效果很好。Nuxt ContentVitePressAstro 已经迁移到它。我们收到的反馈也非常好。

合并回主分支

我是 Shiki 团队的一员,并且不定期地帮助发布版本。虽然 Pine 是 Shiki 的负责人,但他一直在忙于其他事情,Shiki 的迭代速度也慢了下来。在 Shikiji 的实验过程中,我 提出了一些改进建议**,这些建议可以帮助 Shiki 获得现代化的结构。虽然大家普遍同意这个方向,但需要做很多工作,而且还没有人开始着手。

虽然我们很高兴使用 Shikiji 来解决我们遇到的问题,但我们当然不希望看到社区因 Shiki 的两个不同版本而分裂。在与 Pine 通话后,我们达成共识,将这两个项目合并为一个。

feat!: 将 Shikiji 合并回 Shiki v1.0 #557

我们非常高兴地看到我们在 Shikiji 中的工作已合并回 Shiki,这不仅对我们自己有益,而且也惠及整个社区。通过这次合并,它 **解决了 Shiki 多年来 95% 的未解决问题**。

Shikiji Merged Back to Shiki

Shiki 现在也有了一个 全新的文档网站**,您也可以在浏览器中直接使用它(这要归功于其与运行时无关的方法!)。许多框架现在都内置了与 Shiki 的集成,也许您已经在某个地方使用了它!

Twoslash

Twoslash 是一款集成工具,用于从 TypeScript 语言服务 中检索类型信息并将其生成到您的代码片段中。它本质上使您的静态代码片段具有类似于 VS Code 编辑器的悬停类型信息。它由 Orta TheroxTypeScript 文档网站 创建,您可以在 此处找到原始源代码。Orta 还为 Shiki v0.x 版本创建了 Twoslash 集成。当时,Shiki 没有合适的插件系统,这使得 shiki-twoslash 必须作为 Shiki 的包装器构建,使其设置起来有点困难,因为现有的 Shiki 集成将无法直接与 Twoslash 协同工作。

在我们重写 Shikiji 时,我们也借此机会修改了 Twoslash 集成,这同时也是一种 自我测试 和验证可扩展性的方法。借助新的 HAST 内部机制,我们能够 将 Twoslash 集成作为一个转换器插件,使其在 Shiki 可用的任何地方都能工作,并且还可以以可组合的方式与其他转换器一起使用。

有了这个,我们开始考虑是否可以使 Twoslash 在您正在查看的网站 nuxt.com 上工作。nuxt.com 在后台使用 Nuxt Content,与 VitePress 等其他文档工具不同,Nuxt Content 提供的一个好处是它能够处理动态内容并在边缘运行。由于 Twoslash 依赖于 TypeScript 以及来自您的依赖项的庞大的类型模块图,因此将所有这些内容发送到边缘或浏览器并不是理想的选择。听起来很棘手,但我们接受挑战!

我们首先想到的是根据需要从 CDN 获取类型,使用您将在 TypeScript Playground 上看到的 自动类型获取 技术。我们制作了 twoslash-cdn,它允许 Twoslash 在任何运行时环境中运行。但是,这仍然不是最佳解决方案,因为它仍然需要发出许多网络请求,这可能会抵消在边缘运行的目的。

在对底层工具进行了几次迭代(例如,在 @nuxtjs/mdc,Nuxt Content 使用的 Markdown 编译器上)之后,我们成功地采用了混合方法并制作了 nuxt-content-twoslash,它在构建时运行 Twoslash 并缓存结果以进行边缘渲染。这样,我们就可以避免将任何额外的依赖项发送到最终的 bundle 中,但仍然可以在网站上拥有丰富的交互式代码片段。

<script setup>
// Try hover on identifiers below to see the types
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,使其具有更高效和现代的结构。它还允许我们拥有 twoslash-vue,它提供了您上面所使用的 Vue SFC 支持。它由 Volar.jsvuejs/language-tools 提供支持。随着 Volar 逐渐发展成为框架无关的,以及框架之间能够协同工作,我们期待看到此类集成扩展到未来更多语法,例如 Astro 和 Svelte 组件文件。

集成

如果您想在自己的网站上尝试 Shiki,以下是一些我们已经制作的集成。

Shiki 的文档 中查看更多集成。

结论

Nuxt 的使命不仅是为开发者打造更好的框架,还包括让整个前端和 Web 生态系统变得更好。 我们不断突破界限,并支持现代 Web 标准和最佳实践。我们希望您喜欢新的 ShikiunwasmTwoslash 以及我们在使 Nuxt 和 Web 变得更好的过程中创建的许多其他工具。