Shiki是一个使用TextMate 语法和主题的语法高亮工具,它与为 VS Code 提供支持的引擎相同。它为您的代码片段提供了最准确、最美观的语法高亮显示。它由Pine Wu于2018年创建,当时他是 VS Code 团队的一员。最初是一个尝试使用Oniguruma进行语法高亮的实验。
与现有为浏览器设计的语法高亮工具(如Prism等等和)不同,Shiki 采取了不同的方法,即提前高亮。它将高亮后的 HTML 发送到客户端,从而在零 JavaScript 的情况下生成准确美观的语法高亮。它很快流行起来,成为一个非常受欢迎的选择,尤其适用于静态站点生成器和文档站点。
例如,对于下面的代码片段
export default defineNuxtConfig({
modules: [
'@nuxt/content',
],
})
Shiki 将生成以下 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 的高亮工具难以实现的精确结果。
虽然 Shiki 非常棒,但它仍然是一个设计为在 Node.js 上运行的库。这意味着它仅限于高亮静态代码,并且在处理动态代码时会遇到困难,因为 Shiki 不在浏览器中工作。此外,Shiki 依赖于 Oniguruma 的 WASM 二进制文件,以及一堆沉重的 JSON 格式的语法和主题文件。它使用 Node.js 文件系统和路径解析来加载这些文件,这在浏览器中是无法访问的。
为了改善这种情况,我发起了这个 RFC,后来通过这个 PR被引入,并随 Shiki v0.9 发布。虽然它抽象了文件加载层,根据环境使用 fetch 或文件系统,但使用起来仍然相当复杂,因为您需要手动将语法和主题文件部署到您的 bundle 或 CDN 中的某个位置,然后调用 setCDN
方法来告诉 Shiki 从哪里加载这些文件。
这个解决方案并不完美,但至少它使得 Shiki 可以在浏览器中运行以高亮动态内容。从那时起,我们一直使用这种方法——直到本文的故事开始。
开端
Nuxt 正在努力推动 Web 到边缘,通过更低的延迟和更好的性能使 Web 更易访问。像 CDN 服务器一样,CloudFlare Workers等边缘托管服务部署在全球各地。用户从最近的边缘服务器获取内容,而无需往返于可能远在千里之外的源服务器。虽然它提供了诸多优势,但也带来了一些权衡。例如,边缘服务器使用受限的运行时环境。CloudFlare Workers 也不支持文件系统访问,并且通常不保留请求之间的状态。虽然 Shiki 的主要开销是预先加载语法和主题,但这在边缘环境中效果不佳。
这一切都始于Sébastien和我的一次聊天。我们当时正试图让Nuxt Content使用 Shiki 高亮代码块在边缘环境中工作。
我开始通过本地修补shiki-es
(一个由Pooya Parsa构建的 Shiki 的 ESM 版本)进行实验,将语法和主题文件转换为ECMAScript 模块(ESM),以便构建工具能够理解和打包。这是为了创建 CloudFlare Workers 能够使用的代码包,而无需使用文件系统或进行网络请求。
import fs from 'fs/promises'
const cssGrammar = JSON.parse(await fs.readFile('../langs/css.json', 'utf-8'))
const cssGrammar = await import('../langs/css.mjs').then(m => m.default)
我们需要将 JSON 文件包装成内联字面量的 ESM 格式,这样我们就可以使用import()
来动态导入它们。区别在于 import()
是一个在任何地方都可用的标准 JavaScript 功能,而 fs.readFile
是 Node.js 特有的 API,只在 Node.js 中有效。静态使用 import()
还可以让像Rollup等等webpack这样的打包工具能够构建模块关系图并以分块的形式输出打包代码.
然后,我意识到要让它在边缘运行时环境中工作,还需要做更多的工作。由于打包工具期望导入在构建时是可解析的(这意味着为了支持所有语言和主题),我们需要在代码库中的每个语法和主题文件中列出所有的导入语句。这将导致一个巨大的打包体积,其中包含大量您可能实际不需要的语法和主题。这个问题在边缘环境中尤为重要,因为打包体积对性能至关重要。
所以,我们需要找到一个更好的折衷方案来使其更好地工作。
分叉 - Shikiji
深知这可能会从根本上改变 Shiki 的工作方式,并且我们不想用我们的实验冒着破坏现有 Shiki 用户的风险,我创建了一个 Shiki 的分支,名为Shikiji。我从头开始重写了代码,同时牢记了以前的 API 设计决策。目标是使 Shiki 运行时无关、高性能和高效,就像我们在UnJS.
为了实现这一点,我们需要让 Shikiji 完全 ESM 友好、纯粹且可进行摇树优化。这甚至涉及到 Shiki 的依赖项,例如vscode-oniguruma
等等和
vscode-textmate,它们都是以 Common JS (CJS) 格式提供的。vscode-oniguruma
还包含一个由emscripten生成的 WASM 绑定,其中包含
悬空 Promise,这将导致 CloudFlare Workers 无法完成请求。我们最终通过将 WASM 二进制文件嵌入到base64 字符串中并作为 ES 模块发布,手动重写 WASM 绑定以避免悬空 Promise,以及vendored 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 Module 集成提案。它已集成到Nitro 中,以实现自动化 WASM 目标。我们希望 unwasm
将帮助开发者在使用 WASM 时获得更好的体验。
总的来说,Shikiji 的重写工作进展顺利。Nuxt Content, VitePress等等和都已迁移到它。我们收到的反馈也一直非常积极。
合并回去
我是 Shiki 的团队成员,并时不时地协助发布。虽然Pine是 Shiki 的负责人,但他忙于其他事务,Shiki 的迭代速度放慢了。在 Shikiji 的实验期间,我提出了一些改进建议,可以帮助 Shiki 获得现代结构。虽然大家普遍同意这个方向,但需要做的工作相当多,没有人开始着手处理。
虽然我们乐于使用 Shikiji 来解决我们遇到的问题,但我们当然不希望看到社区因两个不同版本的 Shiki 而分裂。在与 Pine 通话后,我们达成共识,将两个项目合并为一个
我们非常高兴看到我们在 Shikiji 中的工作被合并回 Shiki,这不仅对我们自己有益,也造福了整个社区。通过这次合并,它解决了 Shiki 多年来约 95% 的未决问题。
Shiki 现在还拥有一个全新的文档站点,你甚至可以在浏览器中直接体验它(多亏了与平台无关的方法!)。许多框架现在都内置了对 Shiki 的集成,也许你已经在某个地方使用它了!
Twoslash
Twoslash是一个集成工具,用于从TypeScript 语言服务中检索类型信息并生成到您的代码片段中。它本质上使您的静态代码片段拥有类似于 VS Code 编辑器的悬停类型信息。它由Orta Therox为TypeScript 文档站点创建,您可以在此处找到原始源代码。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上看到的自动类型获取(Auto-Type-Acquisition)技术。我们制作了twoslash-cdn
,它允许 Twoslash 在任何运行时环境中运行。然而,这听起来仍然不是最优的解决方案,因为它仍然需要进行许多网络请求,这可能会违背在边缘运行的目的。
在底层工具(例如 Nuxt Content 使用的 Markdown 编译器)进行了一些迭代后,我们成功地采用了混合方法并开发了@nuxtjs/mdc
nuxt-content-twoslash,它在构建时运行 Twoslash 并缓存结果以供边缘渲染。这样,我们可以避免将任何额外的依赖项打包到最终的 bundle 中,但仍然在网站上拥有丰富的交互式代码片段。
在此期间,我们还借此机会与 Orta 重构了
<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>
,以实现更高效和现代的结构。它还允许我们拥有Twoslashtwoslash-vue,它提供了您上面正在体验的
Vue SFC支持。它由Volar.js和等等vuejs/language-tools
提供支持。随着 Volar 发展为与框架无关,以及框架之间的协作,我们期待未来这种集成能够扩展到更多语法,例如 Astro 和 Svelte 组件文件。
集成
如果你想在自己的网站上尝试 Shiki,这里有一些我们已经完成的集成:
- Nuxt
- 如果使用Nuxt Content,Shiki 是内置的。对于 Twoslash,你可以在其上添加
,它在构建时运行 Twoslash 并缓存结果以供边缘渲染。这样,我们可以避免将任何额外的依赖项打包到最终的 bundle 中,但仍然在网站上拥有丰富的交互式代码片段。
。 - 如果不是,你可以使用
nuxt-shiki
将 Shiki 用作 Vue 组件或可组合函数。
- 如果使用Nuxt Content,Shiki 是内置的。对于 Twoslash,你可以在其上添加
- VitePress
- Shiki 是内置的。对于 Twoslash,你可以使用
vitepress-twoslash
.
- Shiki 是内置的。对于 Twoslash,你可以使用
- 低层级集成 - Shiki 为 Markdown 编译器提供官方集成:
结论
Nuxt 的使命不仅是为开发者构建更好的框架,更是让整个前端和网络生态系统变得更好。 我们不断突破界限,支持现代网络标准和最佳实践。我们希望您喜欢新的Shiki, unwasm, Twoslash以及我们在构建 Nuxt 和改进网络过程中制作的许多其他工具。