
作者 | 風水
來源 | 阿裡技術公衆号
去年發表的《一個好的元件應該是什麼樣的?》 一文介紹了借助 TypeScript AST 文法樹解析,對 React 元件 Props 類型定義及注釋提取,自動生成元件對應 截圖、用法、參數說明、README、Demo 等。在社群中取得了比較好的反響,同時應用在團隊中也取得了較為不錯的結果,現在内部元件系統中已經累計使用該方案沉澱 1000+ 的 React 元件。
之前我們是借助了 webpack + TypeScript 做了一套用于開發 React 元件的腳手架套件,當開發者要元件開發時,即可直接使用腳手架初始化對應項目結構進行開發。
雖然主路徑上确實解決了元件開發中所遇到的元件無圖無真相、元件參數文檔缺失、元件用法文檔缺失、元件 Demo 缺失、元件無法索引、元件産物不規範等内部元件管理和沉澱上的問題,但 Webpack 的方案始終還是會讓元件開發多一層編譯,當一個元件庫沉澱超過 300+ 時,引入依賴不斷增長,還是會帶來元件編譯上的負荷導緻開發者開發體驗下降。
一 Vite 帶來的曙光
Vite 給前端帶來的絕對是一次革命性的變化,這麼說毫不誇張。
或許應該說是 Vite 背後整合的 esbuild 、 Browser es modules、HMR、Pre-Bundling 等這些社群中關于 JS 編譯發展的先進工具和思路,在 Vite 這樣的整合推動下,給前端開發帶來了革命性變化。
我很早就說過,任何一個架構或者庫的出現最有價值的一定不是它的代碼本身,而是這些代碼背後所帶來的新思路、新啟發。是以我在寫文章的時候,也很注重能把我思考最後執行的整個過程講清楚。
Vite 為什麼快,主要是 esbuild 進行 pre-bundles dependencies + 浏覽器 native ESM 動态編譯,這裡我不做過多贅述,詳細參考:Vite: The Problems
在這個思路的背景下,回到我們元件開發的場景再看會發現以下幾個問題高度吻合:
- 元件庫開發,實際上不需要編譯全部元件。
- 元件開發,編譯預覽頁面主要給開發者使用,浏覽器相容可控。
- HMR(熱更新)能力在 Vite 加持下更加顯得立竿見影,是以往元件開發和調試花費時間最多的地方。
- Vite 中一切源碼子產品動态編譯,也就是 TypeScript 類型定義和 JS 注釋也可以做到動态編譯,大大縮小編譯範圍。
那麼,以往像 StoryBook 和之前我們用于提取 tsx 元件類型定義的思路将可以做一個比較大的改變。
之前為了擷取元件入參的類型資料會在 Wwebpack 層面做插件用于動态分析 export 的 tsx 元件,在該元件下動态加入一段 __docgenInfo 的靜态屬性變量,将從 AST 分析得到的類型資料和注釋資訊注入進元件 JS Bundle,進而進一步處理為動态參數設定:
TypeScript 對元件 Props 的定義
分析注入到 JS Bundle 中的内容
分析轉換後實作的參數互動設定
是以對于元件來說,實際上擷取這一份類型定義的中繼資料對于元件本身來說是備援的,不論這個元件中的這部分中繼資料有沒有被用到,都會在 Webpack 編譯過程中解析提取并注入到元件 Bundle 中,這顯然是很低效的。
在 Vite 的思路中,完全可以在使用到元件中繼資料時,再擷取其中繼資料資訊,比如加載一個 React 元件為:
import ReactComponent from './component1.tsx'
那麼加載其中繼資料即:
import ComponentTypeInfo from './component1.tsx.type.json';
// or
const ComponentTypeInfoPromise = import('./component1.tsx.type.json');
通過 Vite 中 Rollup 的插件能力加載 .type.json 檔案類型,進而做到對應元件中繼資料的解析。同時借助 Rollup 本身對于編譯依賴收集和 HMR 的能力,做到元件類型變化的熱更新。
二 設計思路
以上是看到 Vite 的子產品加載思路,得到的一些靈感和啟發,進而做出的一個初步設想。
但如果真的要做這樣一個基于 Vite 的 React 、 Rax 元件開發套件,除了元件入參中繼資料的擷取以外,當然還有其他需要解決的問題,首當其沖的就是對于 .md 的檔案解析。
1 元件 Usage
參照 dumi 及 Icework 所提供的元件開發思路,元件 Usage 完全可以以 Markdown 寫文檔的形式寫到任何一個 .md 檔案中,由編譯器動态解析其中關于 jsx、tsx、css、scss、less 的代碼區塊,并且把它當做一段可執行的 script 編譯後,運作在頁面中。
這樣既是在寫文檔,又可以運作調試元件不同入參下元件表現情況,元件有多少中Case,可以寫在不同的區塊中交由使用者自己選擇檢視,這個設計思路真是讓人拍案叫絕!
最後,如果能結合上述提到 Vite 的 esbuild 動态加載和 HMR 能力,那麼整個元件開發體驗将會再一次得到質的飛躍。
是以針對 Markdown 檔案需要做一個 Vite 插件來執行對 .md 的檔案解析和加載,預期要實作的能力如下:
import { content, modules } from "./component1/README.md";
// content README.md 的原文内容
// modules 通過解析獲得的`jsx`,`tsx`,`css`,`scss`,`less` 運作子產品
預期設想效果,請點選放大檢視:
2 元件 Runtime
一個正常的元件庫目錄應該是什麼樣的?不論是在一個單獨的元件倉庫,還是在一個已有的業務項目中,其實元件的目錄結構大同小異,大緻如下:
components
├── component1
│ ├── README.md
│ ├── index.scss
│ └── index.tsx
├── component2
│ ├── README.md
│ ├── index.scss
│ └── index.tsx
在我們的設想中你可以在任意一個項目中啟動元件開發模式,在運作 vite-comp 之後就可以看到一個專門針對元件開發的界面,在上面已經幫你解析并渲染出來了在 README.md 中編寫的元件 Usage,以及在 index.tsx 定義的 interface,隻需要通路不同的檔案路徑,即可檢視對應元件的表現形态。
同時,最後可以幫你可以将這個界面上的全部内容編譯打包,截圖釋出到 NPM 上,别人看到這個元件将會清晰看到其元件入參,用法,截圖等,甚至可以打開 Demo 位址,修改元件參數來檢視元件不同狀态下的表現形态。
如果要實作這樣的效果,則需要一套元件運作的 Runtime 進行支援,這樣才可以協調 React 元件、README.md、TypeScript 類型定義串聯成我們所需要的元件調試+文檔一體的元件開發頁面。
在這樣的 Runtime 中,同樣需要借助 Vite 的子產品解析能力,将其 URL 為 *//(README|*).html 的請求,轉換為一段可通路的元件 Runtime Html 傳回給浏覽器,進而讓浏覽器運作真正的元件開發頁面。
http://localhost:7000/components/component1/README.html
->
/components/component1/README.html
->
/components/component1/README.md
->
Runtime Html
3 元件 Props Interface
正如我上述内容中講到的,如果利用 Vite 添加一個對 tsx 的元件 props interface 類型解析的能力,也可以做成獨立插件用于解析 .tsx.type.json 結尾的檔案類型,通過 import 這種類型的檔案,進而讓編譯器動态解析其 tsx 檔案中所定義的 TypeScript 類型,并作為子產品傳回給前端消費。
其加載過程就可以當做是一個虛拟的子產品,可以了解為你可以通過直接 import 一個虛拟的檔案位址,擷取到對應的 React 元件元資訊:
// React Component
import Component from './component1.tsx';
// React Component Props Interface
import ComponentTypeInfo from './component1.tsx.type.json';
// or
const ComponentTypeInfoPromise = import('./component1.tsx.type.json');
由于這種解析能力并不是借助于 esbuild 進行,是以在轉換性能上無法群組件主流程編譯同步進行。
在請求到該檔案類型時,需要考慮在 Vite 的 Serve 模式下,新開線程進行這部分内容編譯,由于整個過程是異步行為,不會影響元件主流程渲染進度。當請求傳回響應後,再用于渲染元件 Props 定義及側邊欄面闆部分。
在熱更新過程中,同樣需要考慮到 tsx 檔案修改範圍是否涉及到 TypeScript 類型的更改,如果發現修改導緻類型變化時,再觸發 HMR 事件進行子產品更新。
三 元件 Build
以上都是在讨論元件在 Vite 的 Serve 态(也就是開發态)下的情況,我們上文中大量借助 Vite 利用浏覽器 es module 的加載能力,進而做的一些開發态的動态加載能力的擴充。
但是 Vite 在元件最終 Build 過程中是沒有 Server 服務啟動,當然也不會有浏覽器動态加載,是以為了讓别人也可以看到我們開發的元件,能夠體驗我們開發時調試元件的樣子,就需要考慮為該元件編譯産出一份可以被浏覽器運作的 html。
是以在 Vite 插件開發過程中,是需要考慮在 Build 狀态下的編譯路徑的,如果是在 Build 狀态下,Vite 将使用 Rollup 的編譯能力,那麼就需要考慮手動提供所有元件的 rollup.input(entries)。
在插件編寫過程中,一定需要遵循 Rollup 所提供的插件加載生命周期,才能保證 Build 過程和 Serve 過程的子產品加載邏輯和編譯邏輯保持一緻。
我一開始在實作的過程中,就是沒有了解透徹 Vite 和 Rollup 的關系,在子產品解析過程中依賴了大量 Vite 的 Server 提供的服務端中間件能力。導緻在考慮到 Build 态時,才意識到其中的問題,最後幾乎重新寫了之前的加載邏輯。
四 總結
我姑且把這個方案(套件)稱之為 vite-comp,其大緻的構成就是由 Vite + 3 Vite Pugins 構成,每個插件互相不耦合,互相職責也不相同,也就是說你可以拿到任意一個 Vite 插件去做别的用途,後續會考慮單獨開源,分别是:
- Markdown,用于解析 .md 檔案,加載後可擷取原文及 jsx、tsx 等可運作區塊。
- TypeScript Interface,用于解析 .tsx 檔案中對于 export 元件的 props 類型定義。
- Vite Comp Runtime,用于運作元件開發态,編譯最終元件文檔。
結合 Vite,已經實作了 Vite 模式下的 React、Rax 元件開發,它相比于之前使用 Webpack 做的元件開發,已經展現出了以下幾個大優勢:
- 無懼大型元件庫,即使有 2000 個元件在同一個項目中,啟動依舊是 <1000ms。
- 高效的元件中繼資料加載流,項目一切依賴編譯按需進行。
- 毫秒級熱更新響應,借助 esbuild 幾乎是按下儲存的一瞬間,就可以看到改動效果。
預覽體驗:
啟動
Markdown 元件文檔毫秒級響應
TypeScript 類型識别
Vite 現在還是隻是剛剛起步,這種全新的編譯模式,已經給我帶來了非常多的開發态收益,結合 Vite 的玩法未來一定還會層出不窮,比如 Midway + lambda + Vite 的前端一體化方案也是看得讓人拍案叫絕,在這個欣欣向榮的前端大時代,相信不同前端産物都會和 Vite 結合出下一段傳奇故事。
我是一個熱愛生活的前端工程師!Yooh!
相關連結 https://vitejs.dev/guide/why.html#the-problems https://d.umijs.org/ https://ice.work/
前端開發技術圖譜
6 大知識點,14 個課程,680 個課時,将前端開發知識和實戰經驗融入圖譜,包含 HTML 、CSS、JavaScript 、jQuery 、Vue 、React 、Angular 、NodeJS 等前端開發必備技能,幫你迅速提升。
點選這裡,開始學習吧~