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 和改進網路過程中製作的許多其他工具。