透過超過 100 個技巧的精選集學習 Nuxt!
文章·  

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 或檔案系統,但它仍然相當複雜,因為您需要手動將文法和主題檔案放在您的 bundle 或 CDN 中的某個位置,然後呼叫 setCDN 方法來告訴 Shiki 從哪裡載入這些檔案。

這個解決方案並不完美,但至少它讓 Shiki 可以在瀏覽器中運行以突顯動態內容。從那時起,我們一直使用這種方法 - 直到本文的故事開始。

開始

Nuxt 正在努力將 web 推向邊緣,透過更低的延遲和更好的效能,使 web 更易於存取。與 CDN 伺服器一樣,邊緣託管服務(如 CloudFlare Workers)部署在全球各地。使用者從最近的邊緣伺服器取得內容,而無需往返可能在數千英里之外的原始伺服器。儘管它提供了很棒的優點,但也帶來了一些權衡。例如,邊緣伺服器使用受限的運行時環境。CloudFlare Workers 也不支援檔案系統存取,並且通常不會在請求之間保留狀態。雖然 Shiki 的主要開銷是預先載入文法和主題,但這在邊緣環境中無法很好地運作。

這一切都始於 Sébastien 和我之間的一次聊天。我們試圖讓使用 Shiki 來突顯程式碼區塊的 Nuxt Content 在邊緣上運作。

Chat History Between Sébastien and Anthony

我開始實驗,在本地修補 shiki-es(由 Pooya Parsa 構建的 Shiki 的 ESM 版本),將文法和主題檔案轉換為 ECMAScript 模組 (ESM),以便構建工具可以理解和捆綁它。這樣做的目的是為了建立程式碼 bundle,供 CloudFlare Workers 使用,而無需使用檔案系統或發出網路請求。

之前 - 從檔案系統讀取 JSON 資源
import fs from 'fs/promises'

const cssGrammar = JSON.parse(await fs.readFile('../langs/css.json', 'utf-8'))
之後 - 使用 ESM 導入
const cssGrammar = await import('../langs/css.mjs').then(m => m.default)

我們需要將 JSON 檔案包裝到 ESM 中作為內聯文字,以便我們可以使用 import() 來動態導入它們。不同之處在於,import() 是一個標準的 JavaScript 功能,可以在任何地方運作,而 fs.readFile 是一個 Node.js 特有的 API,僅在 Node.js 中運作。靜態地使用 import() 也會使捆綁器(如 Rollupwebpack)能夠建構模組關係圖,並 將捆綁的程式碼作為 chunks 發出

然後,我意識到要使其在邊緣運行時環境中運作,實際上需要更多的工作。由於捆綁器期望導入在構建時可解析(意味著為了支援所有語言和主題),我們需要在程式碼庫中每個文法和主題檔案中列出所有導入語句。這將導致一個巨大的 bundle 大小,其中包含一堆您可能實際上不會使用的文法和主題。這個問題在邊緣環境中尤其重要,在邊緣環境中,bundle 大小對於效能至關重要。

因此,我們需要找到一個更好的中間地帶,使其更好地運作。

分支 - Shikiji

了解到這可能會從根本上改變 Shiki 的運作方式,並且由於我們不想冒著透過我們的實驗破壞現有 Shiki 使用者的風險,我啟動了 Shiki 的一個分支,名為 Shikiji。我從頭開始重寫程式碼,同時牢記之前的 API 設計決策。目標是使 Shikiji 成為運行時環境不可知、高效能和高效的,就像我們在 UnJS 的理念一樣。

為了實現這一目標,我們需要使 Shikiji 完全與 ESM 友好、純粹且 tree-shakable。這一直向上追溯到 Shiki 的依賴項,例如 vscode-onigurumavscode-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,並根據使用者的偏好切換其可見性 - 這效率不高,因為它會重複 payload,或者使用 CSS 變數主題,這會失去 Shiki 擅長的細緻突顯效果。憑藉 Shikiji 擁有的新架構,我退後一步並重新思考了這個問題,並 提出了 分解常見 token 並將多個主題合併為內聯 CSS 變數的想法,這提供了高效的輸出,同時符合 Shiki 的理念。您可以在 Shiki 的文件中了解更多相關資訊。

為了使遷移更容易,我們還建立了 shikiji-compat 相容性層,它使用 Shikiji 的新基礎並提供向後相容性 API。

為了使 Shikiji 在 Cloudflare Workers 上運作,我們遇到了最後一個挑戰,因為它們不支援從內聯二進制數據 啟動 WASM 實例。相反,由於安全原因,它需要導入靜態 .wasm 資源。這意味著我們的「All-in-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 編輯器的 hover 類型資訊。它由 Orta TheroxTypeScript 文件網站製作,您可以在那裡找到 此處的原始程式碼。Orta 也為 Shiki v0.x 版本建立了 Twoslash 整合。在那時,Shiki 沒有適當的插件系統,這使得 shiki-twoslash 必須建構為 Shiki 的包裝器,使其有點難以設定,因為現有的 Shiki 整合無法直接與 Twoslash 協同運作。

當我們重寫 Shikiji 時,我們也藉此機會修改了 Twoslash 整合,這也是一種 dog-fooding 並驗證可擴展性的方法。憑藉新的 HAST 內部結構,我們能夠 將 Twoslash 作為轉換器插件整合,使其可以在 Shiki 運作的任何地方運作,並且以可組合的方式與其他轉換器一起使用。

有了這個,我們開始考慮我們是否可以讓 Twoslash 在 nuxt.com(您正在瀏覽的網站)上運作。nuxt.com 在底層使用 Nuxt Content,與其他文件工具(如 VitePress)不同,Nuxt Content 提供的好處之一是它能夠處理動態內容並在邊緣上運行。由於 Twoslash 依賴 TypeScript 以及來自您的依賴項的龐大類型模組圖,因此將所有這些東西運送到邊緣或瀏覽器並不是理想的選擇。聽起來很棘手,但我們接受挑戰!

我們首先想到從 CDN 按需獲取類型,使用您在 TypeScript playground 上看到的 Auto-Type-Acquisition 技術。我們製作了 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 的過程中製作的許多其他工具。