Shiki 是一个使用 TextMate 语法和主题的语法高亮器,与 VS Code 使用相同的引擎。它为您的代码片段提供了最准确、最美观的语法高亮。Pine Wu 于 2018 年在 VS Code 团队期间创建了它,最初是为了使用 Oniguruma 进行语法高亮。
与在浏览器中运行的 Prism 和 Highlight.js 等现有语法高亮器不同,Shiki 采用了预高亮的方式。它将高亮后的 HTML 发送到客户端,无需 JavaScript 即可产生准确、美观的语法高亮。很快,它便成为了一个非常受欢迎的选择,尤其是用于静态网站生成器和文档网站。
尽管 Shiki 很出色,但它仍然是一个设计在 Node.js 上运行的库。这意味着它仅限于高亮静态代码,对于动态代码则会有问题,因为 Shiki 不能在浏览器中运行。此外,Shiki 依赖于 Oniguruma 的 WASM 二进制文件以及 JSON 格式的多个重型语法和主题文件。它使用 Node.js 的文件系统和路径解析来加载这些文件,这在浏览器中是无法访问的。
为了改善这种情况,Anthony Fu 开始了这项 RFC,后来以 PR 的形式发布并应用于 Shiki v0.9。虽然它将文件加载层抽象为根据环境使用 fetch 或文件系统,但使用起来仍相当复杂,因为需要在包或 CDN 中手动提供语法和主题文件,然后调用 setCDN 方法告诉 Shiki 从哪里加载这些文件。
虽然这个解决方案并不完美,但至少使 Shiki 能够在浏览器中运行以高亮动态内容。从那时起,大家一直在使用这种方法,直到本文故事的开始。
Shiki 的开始
Nuxt 正在积极推动 Web 发展,以降低延迟、提高性能,使 Web 更加便捷。与 CDN 服务器类似,Cloudflare Workers 等边缘托管服务遍布全球。用户可以从最近的边缘服务器获取内容,无需往返数千英里外的原始服务器。虽然它带来了诸多好处,但也存在一些权衡。例如,边缘服务器使用受限的运行时环境。Cloudflare Workers 不支持文件系统访问,并且通常不会在请求之间保留状态。而 Shiki 的主要开销是预先加载语法和主题,这在边缘环境中可能无法很好地工作。
这一切始于 Sébastien 和 Anthony Fu 之间的聊天。他们试图让使用 Shiki 突出显示代码块的 Nuxt Content 在边缘运行。
他开始通过本地修补 shiki-es
(Pooya Parsa 构建的 Shiki 的 ESM 版本)进行实验,将语法和主题文件转换为 ECMAScript 模块(ESM),以便构建工具能够理解和打包。这是为了创建供 Cloudflare Workers 使用的代码捆绑包,而无需使用文件系统或发出网络请求。
他需要将 JSON 文件作为内联文本封装成 ESM,以便使用 import()
动态导入。import()
是标准的 JavaScript 特性,通用性强,而 fs.readFile
是 Node.js 特有的 API,仅适用于 Node.js。使用静态的 import()
可以使 Rollup 和 webpack 等打包工具构建模块关系图,并将打包后的代码作为块输出。
然而,他意识到在边缘运行时上实现这一点需要更多工作。由于打包工具期望在构建时能够解析导入(即支持所有语言和主题),需要在代码库中的每个语法和主题文件中列出所有导入语句。这会导致打包文件变得非常大,包含大量可能并不使用的语法和主题。在边缘环境中,打包文件的大小对性能至关重要,因此这个问题尤为重要。
因此,他们需要找到一个更好的平衡点,以便更好地实现这一功能。
Shiki 的分支 - Shikiji
了解到这可能会从根本上改变 Shiki 的工作方式,而且他们不想用实验破坏现有 Shiki 用户的体验,因此 Anthony Fu 创建了 Shiki 的一个分支版本,名为 Shikiji。在保留之前 API 设计决策的同时,从头开始重写了代码。他们的目标是让 Shiki 与 JavaScript 运行时无关,性能出色且高效,就像在 UnJS 秉持的理念一样。
为了实现这一目标,需要让 Shikiji 完全支持 ESM,纯净且支持 Tree shaking。这涉及到 Shiki 的依赖项,如 vscode-oniguruma 和 vscode-textmate,它们目前以 Common JS(CJS)格式提供。vscode-oniguruma 还包含一个由 emscripten 生成的 WASM 绑定,其中包含悬空的 Promise,这会导致 CloudFlare Workers 无法完成请求。他们最终将 WASM 二进制文件嵌入到 base64 字符串中,并将其作为 ES 模块发布,手动重写 WASM 绑定以避免悬空的 Promise,并将 vscode-textmate 供应商化,从其源代码编译并生成高效的 ESM 输出。
最终的结果非常令人兴奋,他们成功地在任何运行时环境中运行 Shikiji,甚至可以通过 CDN 导入并在浏览器中通过一行代码运行。
他们还借此机会改进了 Shiki 的 API 和内部架构。不再使用简单的字符串连接,而是使用 hast,创建抽象语法树(AST)来生成 HTML 输出。这为用户提供了修改中间 HAST 并进行许多之前难以实现的有趣集成的可能性,打开了暴露 Transformers API 的大门。
用户经常要求支持 Dark / Light 模式。由于 Shiki 采用静态方式,无法在渲染时即时更改主题。过去的解决方案是生成两次高亮 HTML,根据用户偏好切换可见性,但这效率低下且重复了数据负载。另一种方法使用 CSS 变量主题,但失去了 Shiki 的精细高亮功能。Shikiji 的新架构重新审视了这个问题,并提出将通用标记分解为多个主题,并将它们合并为内联 CSS 变量的想法,这既高效又符合 Shiki 的理念。更多详情请查阅 Shiki 文档。
为方便迁移,他们还创建了 shikiji-compact 兼容性层,它使用 Shikiji 的新基础并提供向后兼容API。
在 Cloudflare Workers 上运行 Shikiji 时,他们面临了一个挑战:它们不支持从内联二进制数据初始化 WASM 实例,而是出于安全原因需要导入静态 .wasm
资产。这意味着「All-in-ESM」方法不适用于 Cloudflare,用户需要提供不同的 WASM 源,这增加了操作的复杂性。此时,Pooya Parsa 介入了进来,创建了通用层 unjs / unwasm,它支持即将推出的 WebAssembly / ES 模块集成提案。它已集成到 Nitro 中,可自动生成 WASM 目标。他们希望 unwasm 能帮助开发人员在使用 WASM 时获得更好的体验。
总的来说,Shikiji 的重写效果良好。Nuxt Content、VitePress 和 Astro 已迁移到 Shikiji,他们收到的反馈也非常积极。
Shiki 合并 Shikiji
Anthony Fu 是 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 工作的任何地方工作,并与其他转换器以可组合的方式使用。
因此,他们开始思考是否可以在 Nuxt 官网上使用 Twoslash。Nuxt 官网底层使用 Nuxt Content 来渲染文档,与其他文档工具(如 VitePress)不同,Nuxt Content 的一个优点是能够处理动态内容并在边缘运行。由于 Twoslash 依赖于 TypeScript 以及来自你依赖项的巨量类型模块图,将所有这些内容发送到边缘或浏览器并不理想。听起来很棘手,但挑战已接受!
他们首先考虑使用 CDN 按需获取类型,使用你在 TypeScript 游乐场中看到的自动类型获取技术。他们创建了 twoslash-cdn,允许 Twoslash 在任何运行时中运行。然而,这听起来并不是最优的解决方案,因为它仍然需要发出许多网络请求,这可能会削弱在边缘运行的目的。
经过几次对底层工具(如 Nuxt Content 使用的 markdown 编译器 @nuxtjs/mdc)的迭代后,他们成功采用混合方法,开发了 nuxt-content-twoslash,它能在构建时运行 Twoslash 并缓存结果,以便进行边缘渲染。这样避免了向最终包发送任何额外依赖项,但仍能在网站上展示丰富的交互式代码片段。
同时,他们还借此机会与 Orta 一起重构了 Twoslash,使其结构更加高效和现代。这也让其拥有了 twoslash-vue,它在你之前玩过的 Vue SFC 中提供支持。它基于 Volar.js 和 vuejs/language-tools。随着 Volar 变得越来越无框架化,各种框架也开始协同工作,期待未来这样的集成能够扩展到更多语法,如 Astro 和 Svelte 组件文件。
尝试使用 Shiki
如果您想在自己的网站上试用 Shiki,这里有一些集成选项:
Nuxt :如果使用 Nuxt Content,则 Shiki 内置其中。如果没有使用 Nuxt Content,您可以使用 nuxt-shiki 将 Shiki 用作 Vue 组件或 Composibles。
VitePress:Shiki 内置其中。对于 Twoslash,您可以使用 vitepress-twoslash。
底层支持:Shiki 为 Markdown 编译器提供官方集成「markdown-it 插件、rehype 插件」。
最后
Nuxt 的使命不仅是为开发人员提供更好的框架,还要使整个前端和 Web 生态系统变得更好。Nuxt 一直在突破界限,支持现代 Web 标准和最佳实践。希望你喜欢新的 Shiki、unwasm、Twoslash 以及 Nuxt 核心开发团队在改进 Nuxt 和 Web 过程中开发的其他许多工具。
参考链接:https://nuxt.com/blog/shiki-v1