透過 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 正在努力將 網頁推向邊緣,透過更低的延遲和更好的效能使網路更易於存取。與 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),以便可以被建置工具理解和捆綁。這樣做的目的是為了建立 CloudFlare Workers 使用的程式碼 bundle,而無需使用檔案系統或發出網路請求。

之前 - 從檔案系統讀取 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 中運作的 Node.js 特定 API。靜態地使用 import() 也會讓 Rollupwebpack 等打包工具能夠建構模組關係圖,並將捆綁的程式碼以區塊的形式發出

然後,我意識到,要使其在邊緣執行時環境中運作,實際上需要更多步驟。由於打包工具期望導入在建置時可解析(這表示為了支援所有語言和主題),我們需要在程式碼庫中每個文法和主題檔案中列出所有導入語句。這最終會導致一個龐大的 bundle 大小,其中包含大量您可能實際上不會使用的文法和主題。這個問題在邊緣環境中尤其重要,因為 bundle 大小對於效能至關重要。

因此,我們需要找出更好的中間點,使其更好地運作。

分支 - Shikiji

知道這可能會從根本上改變 Shiki 的運作方式,而且由於我們不想讓我們的實驗冒著破壞現有 Shiki 使用者的風險,所以我建立了一個名為 Shikiji 的 Shiki 分支。我從頭開始重寫了程式碼,同時牢記先前的 API 設計決策。目標是使 Shiki 與執行時環境無關、高效且高效,就像我們在 UnJS 中的理念一樣。

為了實現這一點,我們需要讓 Shikiji 完全與 ESM 相容、純粹且 可 tree-shake。這一直延伸到 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 上看到的 Auto-Type-Acquisition 技術。我們建立了 twoslash-cdn,允許 Twoslash 在任何執行階段運行。但是,這聽起來仍然不是最優的解決方案,因為它仍然需要進行許多網路請求,這可能會違背在邊緣運行的目的。

在對底層工具進行幾次迭代後(例如在 @nuxtjs/mdc,Nuxt Content 使用的 markdown 編譯器),我們設法採用了混合方法,並建立了 nuxt-content-twoslash,它在建置時運行 Twoslash,並快取結果以進行邊緣渲染。這樣我們就可以避免將任何額外的依賴項運送到最終的捆綁包中,同時仍然在網站上擁有豐富的互動式程式碼片段。

<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 的使命不僅是為開發人員提供更好的框架,也是為了讓整個前端和網路生態系統變得更好。 我們不斷突破界限,並支持現代網路標準和最佳實踐。我們希望您喜歡新的 ShikiunwasmTwoslash 以及我們在使 Nuxt 和網路變得更好的過程中開發的許多其他工具。