與在瀏覽器中運行的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過程中開發的其他許多工具。