在之前的文章中介紹了借助 TypeScript AST 語(yǔ)法樹(shù)解析,對(duì) React 組件 Props 類(lèi)型定義及注釋提取,自動(dòng)生成組件對(duì)應(yīng) 截圖、用法、參數(shù)說(shuō)明、README、Demo 等。在社區(qū)中取得了比較好的反響,同時(shí)應(yīng)用在團(tuán)隊(duì)中也取得了較為不錯(cuò)的結(jié)果,現(xiàn)在內(nèi)部組件系統(tǒng)中已經(jīng)累計(jì)使用該方案沉淀 1000+ 的 React 組件。
之前我們是借助了 webpack + TypeScript 做了一套用于開(kāi)發(fā) React 組件的腳手架套件,當(dāng)開(kāi)發(fā)者要組件開(kāi)發(fā)時(shí),即可直接使用腳手架初始化對(duì)應(yīng)項(xiàng)目結(jié)構(gòu)進(jìn)行開(kāi)發(fā)。
雖然主路徑上確實(shí)解決了組件開(kāi)發(fā)中所遇到的組件無(wú)圖無(wú)真相、組件參數(shù)文檔缺失、組件用法文檔缺失、組件 Demo 缺失、組件無(wú)法索引、組件產(chǎn)物不規(guī)范等內(nèi)部組件管理和沉淀上的問(wèn)題,但 Webpack 的方案始終還是會(huì)讓組件開(kāi)發(fā)多一層編譯,當(dāng)一個(gè)組件庫(kù)沉淀超過(guò) 300+ 時(shí),引入依賴(lài)不斷增長(zhǎng),還是會(huì)帶來(lái)組件編譯上的負(fù)荷導(dǎo)致開(kāi)發(fā)者開(kāi)發(fā)體驗(yàn)下降。
一 Vite 帶來(lái)的曙光
Vite 給前端帶來(lái)的絕對(duì)是一次革命性的變化,這么說(shuō)毫不夸張。
或許應(yīng)該說(shuō)是 Vite 背后整合的 esbuild 、 Browser es modules、HMR、Pre-Bundling 等這些社區(qū)中關(guān)于 JS 編譯發(fā)展的先進(jìn)工具和思路,在 Vite 這樣的整合推動(dòng)下,給前端開(kāi)發(fā)帶來(lái)了革命性變化。
我很早就說(shuō)過(guò),任何一個(gè)框架或者庫(kù)的出現(xiàn)最有價(jià)值的一定不是它的代碼本身,而是這些代碼背后所帶來(lái)的新思路、新啟發(fā)。所以我在寫(xiě)文章的時(shí)候,也很注重能把我思考最后執(zhí)行的整個(gè)過(guò)程講清楚。
Vite 為什么快,主要是 esbuild 進(jìn)行 pre-bundles dependencies + 瀏覽器 native ESM 動(dòng)態(tài)編譯,這里我不做過(guò)多贅述,詳細(xì)參考:Vite: The Problems
在這個(gè)思路的背景下,回到我們組件開(kāi)發(fā)的場(chǎng)景再看會(huì)發(fā)現(xiàn)以下幾個(gè)問(wèn)題高度吻合:
-
組件庫(kù)開(kāi)發(fā),實(shí)際上不需要編譯全部組件。
-
組件開(kāi)發(fā),編譯預(yù)覽頁(yè)面主要給開(kāi)發(fā)者使用,瀏覽器兼容可控。
-
HMR(熱更新)能力在 Vite 加持下更加顯得立竿見(jiàn)影,是以往組件開(kāi)發(fā)和調(diào)試花費(fèi)時(shí)間最多的地方。
-
Vite 中一切源碼模塊動(dòng)態(tài)編譯,也就是 TypeScript 類(lèi)型定義和 JS 注釋也可以做到動(dòng)態(tài)編譯,大大縮小編譯范圍。
那么,以往像 StoryBook 和之前我們用于提取 tsx 組件類(lèi)型定義的思路將可以做一個(gè)比較大的改變。
之前為了獲取組件入?yún)⒌念?lèi)型數(shù)據(jù)會(huì)在 Wwebpack 層面做插件用于動(dòng)態(tài)分析 export 的 tsx 組件,在該組件下動(dòng)態(tài)加入一段 __docgenInfo 的靜態(tài)屬性變量,將從 AST 分析得到的類(lèi)型數(shù)據(jù)和注釋信息注入進(jìn)組件 JS Bundle,從而進(jìn)一步處理為動(dòng)態(tài)參數(shù)設(shè)置:
TypeScript 對(duì)組件 Props 的定義
分析注入到 JS Bundle 中的內(nèi)容
分析轉(zhuǎn)換后實(shí)現(xiàn)的參數(shù)交互設(shè)置
所以對(duì)于組件來(lái)說(shuō),實(shí)際上獲取這一份類(lèi)型定義的元數(shù)據(jù)對(duì)于組件本身來(lái)說(shuō)是冗余的,不論這個(gè)組件中的這部分元數(shù)據(jù)有沒(méi)有被用到,都會(huì)在 Webpack 編譯過(guò)程中解析提取并注入到組件 Bundle 中,這顯然是很低效的。
在 Vite 的思路中,完全可以在使用到組件元數(shù)據(jù)時(shí),再獲取其元數(shù)據(jù)信息,比如加載一個(gè) React 組件為:
import ReactComponent from './component1.tsx'
那么加載其元數(shù)據(jù)即:
import ComponentTypeInfo from './component1.tsx.type.json'; // or const ComponentTypeInfoPromise = import('./component1.tsx.type.json');
通過(guò) Vite 中 Rollup 的插件能力加載 .type.json 文件類(lèi)型,從而做到對(duì)應(yīng)組件元數(shù)據(jù)的解析。同時(shí)借助 Rollup 本身對(duì)于編譯依賴(lài)收集和 HMR 的能力,做到組件類(lèi)型變化的熱更新。
二 設(shè)計(jì)思路
以上是看到 Vite 的模塊加載思路,得到的一些靈感和啟發(fā),從而做出的一個(gè)初步設(shè)想。
但如果真的要做這樣一個(gè)基于 Vite 的 React 、 Rax 組件開(kāi)發(fā)套件,除了組件入?yún)⒃獢?shù)據(jù)的獲取以外,當(dāng)然還有其他需要解決的問(wèn)題,首當(dāng)其沖的就是對(duì)于 .md 的文件解析。
1 組件 Usage
參照 dumi 及 Icework 所提供的組件開(kāi)發(fā)思路,組件 Usage 完全可以以 Markdown 寫(xiě)文檔的形式寫(xiě)到任何一個(gè) .md 文件中,由編譯器動(dòng)態(tài)解析其中關(guān)于 jsx、tsx、css、scss、less 的代碼區(qū)塊,并且把它當(dāng)做一段可執(zhí)行的 script 編譯后,運(yùn)行在頁(yè)面中。
這樣既是在寫(xiě)文檔,又可以運(yùn)行調(diào)試組件不同入?yún)⑾陆M件表現(xiàn)情況,組件有多少中Case,可以寫(xiě)在不同的區(qū)塊中交由用戶(hù)自己選擇查看,這個(gè)設(shè)計(jì)思路真是讓人拍案叫絕!
最后,如果能結(jié)合上述提到 Vite 的 esbuild 動(dòng)態(tài)加載和 HMR 能力,那么整個(gè)組件開(kāi)發(fā)體驗(yàn)將會(huì)再一次得到質(zhì)的飛躍。
所以針對(duì) Markdown 文件需要做一個(gè) Vite 插件來(lái)執(zhí)行對(duì) .md 的文件解析和加載,預(yù)期要實(shí)現(xiàn)的能力如下:
import { content, modules } from "./component1/README.md"; // content README.md 的原文內(nèi)容 // modules 通過(guò)解析獲得的`jsx`,`tsx`,`css`,`scss`,`less` 運(yùn)行模塊
預(yù)期設(shè)想效果,請(qǐng)點(diǎn)擊放大查看:
2 組件 Runtime
一個(gè)常規(guī)的組件庫(kù)目錄應(yīng)該是什么樣的?不論是在一個(gè)單獨(dú)的組件倉(cāng)庫(kù),還是在一個(gè)已有的業(yè)務(wù)項(xiàng)目中,其實(shí)組件的目錄結(jié)構(gòu)大同小異,大致如下:
components ├── component1 │ ├── README.md │ ├── index.scss │ └── index.tsx ├── component2 │ ├── README.md │ ├── index.scss │ └── index.tsx
在我們的設(shè)想中你可以在任意一個(gè)項(xiàng)目中啟動(dòng)組件開(kāi)發(fā)模式,在運(yùn)行 vite-comp 之后就可以看到一個(gè)專(zhuān)門(mén)針對(duì)組件開(kāi)發(fā)的界面,在上面已經(jīng)幫你解析并渲染出來(lái)了在 README.md 中編寫(xiě)的組件 Usage,以及在 index.tsx 定義的 interface,只需要訪(fǎng)問(wèn)不同的文件路徑,即可查看對(duì)應(yīng)組件的表現(xiàn)形態(tài)。
同時(shí),最后可以幫你可以將這個(gè)界面上的全部?jī)?nèi)容編譯打包,截圖發(fā)布到 NPM 上,別人看到這個(gè)組件將會(huì)清晰看到其組件入?yún)ⅲ梅?,截圖等,甚至可以打開(kāi) Demo 地址,修改組件參數(shù)來(lái)查看組件不同狀態(tài)下的表現(xiàn)形態(tài)。
如果要實(shí)現(xiàn)這樣的效果,則需要一套組件運(yùn)行的 Runtime 進(jìn)行支持,這樣才可以協(xié)調(diào) React 組件、README.md、TypeScript 類(lèi)型定義串聯(lián)成我們所需要的組件調(diào)試+文檔一體的組件開(kāi)發(fā)頁(yè)面。
在這樣的 Runtime 中,同樣需要借助 Vite 的模塊解析能力,將其 URL 為 **/*/(README|*).html 的請(qǐng)求,轉(zhuǎn)換為一段可訪(fǎng)問(wèn)的組件 Runtime Html 返回給瀏覽器,從而讓瀏覽器運(yùn)行真正的組件開(kāi)發(fā)頁(yè)面。
http://localhost:7000/components/component1/README.html -> /components/component1/README.html -> /components/component1/README.md -> Runtime Html
3 組件 Props Interface
正如我上述內(nèi)容中講到的,如果利用 Vite 添加一個(gè)對(duì) tsx 的組件 props interface 類(lèi)型解析的能力,也可以做成獨(dú)立插件用于解析 .tsx.type.json 結(jié)尾的文件類(lèi)型,通過(guò) import 這種類(lèi)型的文件,從而讓編譯器動(dòng)態(tài)解析其 tsx 文件中所定義的 TypeScript 類(lèi)型,并作為模塊返回給前端消費(fèi)。
其加載過(guò)程就可以當(dāng)做是一個(gè)虛擬的模塊,可以理解為你可以通過(guò)直接 import 一個(gè)虛擬的文件地址,獲取到對(duì)應(yīng)的 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 進(jìn)行,所以在轉(zhuǎn)換性能上無(wú)法和組件主流程編譯同步進(jìn)行。
在請(qǐng)求到該文件類(lèi)型時(shí),需要考慮在 Vite 的 Serve 模式下,新開(kāi)線(xiàn)程進(jìn)行這部分內(nèi)容編譯,由于整個(gè)過(guò)程是異步行為,不會(huì)影響組件主流程渲染進(jìn)度。當(dāng)請(qǐng)求返回響應(yīng)后,再用于渲染組件 Props 定義及側(cè)邊欄面板部分。
在熱更新過(guò)程中,同樣需要考慮到 tsx 文件修改范圍是否涉及到 TypeScript 類(lèi)型的更改,如果發(fā)現(xiàn)修改導(dǎo)致類(lèi)型變化時(shí),再觸發(fā) HMR 事件進(jìn)行模塊更新。
三 組件 Build
以上都是在討論組件在 Vite 的 Serve 態(tài)(也就是開(kāi)發(fā)態(tài))下的情況,我們上文中大量借助 Vite 利用瀏覽器 es module 的加載能力,從而做的一些開(kāi)發(fā)態(tài)的動(dòng)態(tài)加載能力的擴(kuò)展。
但是 Vite 在組件最終 Build 過(guò)程中是沒(méi)有 Server 服務(wù)啟動(dòng),當(dāng)然也不會(huì)有瀏覽器動(dòng)態(tài)加載,所以為了讓別人也可以看到我們開(kāi)發(fā)的組件,能夠體驗(yàn)我們開(kāi)發(fā)時(shí)調(diào)試組件的樣子,就需要考慮為該組件編譯產(chǎn)出一份可以被瀏覽器運(yùn)行的 html。
所以在 Vite 插件開(kāi)發(fā)過(guò)程中,是需要考慮在 Build 狀態(tài)下的編譯路徑的,如果是在 Build 狀態(tài)下,Vite 將使用 Rollup 的編譯能力,那么就需要考慮手動(dòng)提供所有組件的 rollup.input(entries)。
在插件編寫(xiě)過(guò)程中,一定需要遵循 Rollup 所提供的插件加載生命周期,才能保證 Build 過(guò)程和 Serve 過(guò)程的模塊加載邏輯和編譯邏輯保持一致。
我一開(kāi)始在實(shí)現(xiàn)的過(guò)程中,就是沒(méi)有了解透徹 Vite 和 Rollup 的關(guān)系,在模塊解析過(guò)程中依賴(lài)了大量 Vite 的 Server 提供的服務(wù)端中間件能力。導(dǎo)致在考慮到 Build 態(tài)時(shí),才意識(shí)到其中的問(wèn)題,最后幾乎重新寫(xiě)了之前的加載邏輯。
四 總結(jié)
我姑且把這個(gè)方案(套件)稱(chēng)之為 vite-comp,其大致的構(gòu)成就是由 Vite + 3 Vite Pugins 構(gòu)成,每個(gè)插件相互不耦合,相互職責(zé)也不相同,也就是說(shuō)你可以拿到任意一個(gè) Vite 插件去做別的用途,后續(xù)會(huì)考慮單獨(dú)開(kāi)源,分別是:
-
Markdown,用于解析 .md 文件,加載后可獲取原文及 jsx、tsx 等可運(yùn)行區(qū)塊。
-
TypeScript Interface,用于解析 .tsx 文件中對(duì)于 export 組件的 props 類(lèi)型定義。
-
Vite Comp Runtime,用于運(yùn)行組件開(kāi)發(fā)態(tài),編譯最終組件文檔。
結(jié)合 Vite,已經(jīng)實(shí)現(xiàn)了 Vite 模式下的 React、Rax 組件開(kāi)發(fā),它相比于之前使用 Webpack 做的組件開(kāi)發(fā),已經(jīng)體現(xiàn)出了以下幾個(gè)大優(yōu)勢(shì):
-
無(wú)懼大型組件庫(kù),即使有 2000 個(gè)組件在同一個(gè)項(xiàng)目中,啟動(dòng)依舊是 <1000ms。
-
高效的組件元數(shù)據(jù)加載流,項(xiàng)目一切依賴(lài)編譯按需進(jìn)行。
-
毫秒級(jí)熱更新響應(yīng),借助 esbuild 幾乎是按下保存的一瞬間,就可以看到改動(dòng)效果。
預(yù)覽體驗(yàn):
啟動(dòng)
Markdown 組件文檔毫秒級(jí)響應(yīng)
TypeScript 類(lèi)型識(shí)別
Vite 現(xiàn)在還是只是剛剛起步,這種全新的編譯模式,已經(jīng)給我?guī)?lái)了非常多的開(kāi)發(fā)態(tài)收益,結(jié)合 Vite 的玩法未來(lái)一定還會(huì)層出不窮,比如 Midway + lambda + Vite 的前端一體化方案也是看得讓人拍案叫絕,在這個(gè)欣欣向榮的前端大時(shí)代,相信不同前端產(chǎn)物都會(huì)和 Vite 結(jié)合出下一段傳奇故事。
我是一個(gè)熱愛(ài)生活的前端工程師!Yooh!
【相關(guān)教程推薦:React視頻教程】