哈嘍,很高興你能點開這篇部落格,本部落格是針對 Vite
原始碼的解讀系列文章,認真看完後相信你能對 Vite
的工作流程及原理有一個簡單的瞭解。
Vite
是一種新型的前端構建工具,能夠顯著提升前端開發體驗。
我將會使用圖文結合的方式,儘量讓本篇文章顯得不那麼枯燥(顯然對於原始碼解讀類文章來說,這不是個簡單的事情)。
如果你還沒有使用過 Vite
,那麼你可以看看我的前兩篇文章,我也是剛體驗沒兩天呢。(如下)
本篇文章解讀的主要是 vite
原始碼本體,vite
通過 connect
庫提供開發伺服器,通過中介軟體機制實現多項開發伺服器配置。而 vite
在本地開發時沒有藉助 webpack
或是 rollup
這樣的打包工具,而是通過排程內部 plugin
實現了檔案的轉譯,從而達到小而快的效果。
好了,話不多說,我們開始吧!
vite dev
專案目錄
本文閱讀的 Vite
原始碼版本是 2.8.0-beta.3
,如果你想要和我一起閱讀的話,你可以在這個地址下載 Vite 原始碼。
我們先來看看 Vite
這個包的專案目錄吧。(如下圖)
這是一個整合管理的專案,其核心就是在 packages
裡面的幾個包,我們來分別看看這幾個包是做什麼的吧。(如下)
包名 | 作用 |
---|---|
vite | Vite 主庫,負責 Vite 專案的本地開發(外掛排程)和生產產物構建(Rollup 排程) |
create-vite | 用於建立新的 Vite 專案,內部存放了多個框架(如 react、vue )的初始化模板 |
plugin-vue | Vite 官方外掛,用於提供 Vue 3 單檔案元件支援 |
plugin-vue-jsx | Vite 官方外掛,用於提供 Vue 3 JSX 支援(通過 專用的 Babel 轉換外掛)。 |
plugin-react | Vite 官方外掛,用於提供完整的 React 支援 |
plugin-legacy | Vite 官方外掛,用於為打包後的檔案提供傳統瀏覽器相容性支援 |
playground | Vite 內建的一些測試用例及 Demo |
這幾個原始碼倉庫其實有閱讀的價值,但是我們這次還是先專注一下我們本期的主線 —— Vite
,從 Vite
開始吧。
接下來我們重點解讀 vite
本地開發服務命令 —— vite / vite dev / vite serve
。
vite dev
我們來了解一下 vite dev
命令,也就是本地開發服務的內部工作流程。
vite dev
呼叫了內部的 createServer
方法建立了一個服務,這個服務利用中介軟體(第三方)支援了多種能力(如 跨域
、靜態檔案伺服器
等),並且內部建立了 watcher
持續監聽著檔案的變更,進行實時編譯和熱過載。
而 createServer
做的事情就是我們需要關注的核心邏輯。
在 createServer
方法中,首先進行了對配置的收集工作 —— resolveConfig
。
vite 支援的配置
我們正好可以通過原始碼看看 vite
專案支援的配置,你也可以直接參照 Vite 官方文件。(如下)
配置名稱 | 配置說明 |
---|---|
configFile | 配置檔案,預設讀取根目錄下的 vite.config.js 配置檔案 |
envFile | 環境變數配置檔案,預設讀取根目錄下的 .env 環境變數配置檔案 |
root | 專案的根目錄,預設值是執行命令的目錄 —— process.cwd() |
base | 類似於 webpack 中的 publicPath ,也就是資源的公共基礎路徑 |
server | 本地執行時的服務設定,比如設定 host(主機地址)、port(執行埠)...詳細配置可以參考 vite 文件 |
build | 構建生產產物時的選項,可以參考 vite 文件 |
preview | 預覽選項,在使用了 build 命令後,可以執行 vite preview 對產物進行預覽,具體配置可以參考 vite 文件 |
publicDir | 靜態資源目錄,用於放置不需要編譯的靜態資源,預設值是 public 目錄 |
cacheDir | 快取資料夾,用於放置 vite 預編譯好的一些快取依賴,加速 vite 編譯速度 |
mode | 編譯模式,本地執行時預設值是 development ,構建生產產物時預設是 production |
define | 定義全域性變數,其中開發環境每一項會被定義在全域性,而生產環境將會被靜態替換 |
plugins | 配置 vite 專案的外掛 |
resolve | resolve 支援的配置較多,可以參考 vite 文件 |
css | 關於 css 檔案的編譯選項,可以參考 vite 文件 |
json | 關於 json 檔案的編譯選項,可以參考 vite 文件 |
esbuild | 看官方文件是用於轉換檔案的,但是不太清楚具體的工作是做什麼的,有了解的麻煩在評論區留言解惑一下 |
assetsInclude | 設定需要被 picomatch 模式(一種檔案匹配模式)獨立處理的檔案型 |
optimizeDeps | 依賴優化選項,具體可以參考 vite 文件 |
ssr | ssr 的相關選項,具體可以參考 vite 文件 |
logLevel | 調整控制檯輸出的級別,預設為 info |
customLogger | 自定義 logger ,該選項沒有暴露,是一個內部選項 |
clearScreen | 預設為 true ,配置為 false 後,每次重新編譯不會清空之前的內容 |
envDir | 用於載入環境變數配置檔案 .env 的目錄,預設為當前根目錄 |
envPrefix | 環境變數的字首,帶字首的環境變數將會被注入到專案中 |
worker | 配置 bundle 輸出型別、plugins 以及 Rollup 配置項 |
在上面這些配置中,有一部分可以在啟動時,通過命令列引數新增,比如通過 vite --base / --mode development
的形式進行設定。
如果你希望該配置可以通過配置讀取,也可以全部通過 vite.config.js
來進行配置。
配置斷點除錯
在粗略看過一遍 vite
支援的配置後,我們回到 createServer
函式,準備開始閱讀。
在此之前,如果我們能夠直接執行 vite dev
命令並打上斷點,能夠更好地幫助我們更好的閱讀原始碼,所以我們先來配置一下。
我們需要先進入 vite/packages/vite
,安裝依賴,然後在 scripts
中執行 npm run build
,將 vite
構建到 dist
目錄中。
然後,我們使用 vscode
的除錯功能,建立一個 launch.json
(如下),執行我們的一個 vite
專案。
// launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "packages/vite/bin/vite.js",
"args": ["/Users/Macxdouble/Desktop/ttt/vite-try"]
}
]
}
除錯配置完成後,我們可以在 resolveConfig
函式中打一個斷點,檢視效果(檔案位置在 dist
目錄中,大家需要根據自己的引用找到對應檔案)。
載入配置檔案
resolveConfig
的第一步就是載入專案目錄的配置檔案,如果沒有指定配置檔案位置,會自動在根目錄下尋找 vite.config.js
、vite.config.mjs
、vite.config.ts
、vite.config.cjs
。
如果沒有找到配置檔案,則直接會中止程式。
vite
專案初始化時,會在專案根目錄下自動生成vite.config.js
配置檔案。
在讀取配置檔案後,會將配置檔案和初始化配置(優先順序更高,有部分配置來自於命令列引數)進行合併,然後得到一份配置。(如下圖)
配置收集 - resolveConfig
在 createServer
的開頭,呼叫了 resolveConfig
函式,進行配置收集。
我們先來看看 resolveConfig
都做了哪些事情吧。
處理外掛執行順序
首先,resolveConfig
內部處理了外掛排序規則,對應下面的排序規則。
在後續處理的過程中,外掛將按照對應的排序規則先後執行,這樣能夠讓外掛更好地工作在各個生命週期節點。
合併外掛配置
在外掛排序完成後,vite
的 外掛
暴露了一個配置 config
欄位,可以通過設定該屬性,使外掛能夠新增或改寫 vite
的一些配置。(如下圖)
處理 alias
然後,resolveConfig
內部處理了 alias
的邏輯,將指定的 alias
替換成對應的路徑。
讀取環境變數配置
接下來,resolveConfig
內部找到 env
的配置目錄(預設為根目錄),然後在目錄中讀取對應的 env
環境變數配置檔案。我們可以看看內部的讀取規則優先順序(如下圖)
可以看出,讀取的優先順序分別是 .env.[mode].local
、.env.[mode]
。如果不存在對應 mode
的配置檔案,則會嘗試去尋找 .env.local
、.env
配置檔案,讀取到配置檔案後,使用 doteenv
將環境變數寫入到專案中;如果這些環境變數配置檔案都不存在的話,則會返回一個空物件。
該環境變數配置檔案並不影響專案執行,所以不配置也沒有什麼影響。
匯出配置
接下來,vite
初始化了構建配置,也就是文件中的 build
屬性,詳情可以參照 構建選項文件
最後,resolveConfig
處理了一些 publicDir
、cacheDir
目錄後,匯出了下面這份配置。
const resolved: ResolvedConfig = {
...config,
configFile: configFile ? normalizePath(configFile) : undefined,
configFileDependencies,
inlineConfig,
root: resolvedRoot,
base: BASE_URL,
resolve: resolveOptions,
publicDir: resolvedPublicDir,
cacheDir,
command,
mode,
isProduction,
plugins: userPlugins,
server,
build: resolvedBuildOptions,
preview: resolvePreviewOptions(config.preview, server),
env: {
...userEnv,
BASE_URL,
MODE: mode,
DEV: !isProduction,
PROD: isProduction
},
assetsInclude(file: string) {
return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
},
logger,
packageCache: new Map(),
createResolver,
optimizeDeps: {
...config.optimizeDeps,
esbuildOptions: {
keepNames: config.optimizeDeps?.keepNames,
preserveSymlinks: config.resolve?.preserveSymlinks,
...config.optimizeDeps?.esbuildOptions
}
},
worker: resolvedWorkerOptions
}
resolveConfig
內部還有一些額外的工作處理,主要是收集內部外掛集合(如下圖),還有配置一些廢棄選項警告資訊。
本地開發服務 - createServer
回到 createServer
方法,該方法通過 resolveConfig
拿到配置後,第一時間處理了 ssr
(服務端渲染)的邏輯。
如果使用了服務端渲染,則會通過別的方式進行本地開發除錯。
如果不是服務端渲染,則會建立一個 http server
用於本地開發除錯,同時建立一個 websocket
服務用於熱過載。(如下圖)
檔案監聽 + 熱過載
然後,vite
建立了一個 FSWatcher
物件,用於監聽本地專案檔案的變動。(這裡使用的是 chokidar
庫)
const watcher = chokidar.watch(path.resolve(root), {
ignored: [
// 忽略 node_modules 目錄的檔案變更
'**/node_modules/**',
// 忽略 .git 目錄的檔案變更
'**/.git/**',
// 忽略使用者傳入的 `ignore` 目錄檔案的變更
...(Array.isArray(ignored) ? ignored : [ignored])
],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true,
...watchOptions
}) as FSWatcher
然後,vite
將多個屬性和方法組織成了一個 server
物件,該物件負責啟動本地開發服務,也負責服務後續的開發熱過載。
接下來,我們看看 watcher
是如何做頁面熱過載的吧,原理就是監聽到檔案變更後,重新觸發外掛編譯,然後將更新訊息傳送給客戶端。(如下圖)
外掛容器
接下來,vite
建立了外掛容器(pluginContainer
),用於在構建的各個階段呼叫外掛的鉤子。(如下圖)
實際上外掛容器是在熱過載之前建立的,為了方便閱讀,文章將熱過載的內容都放在了一起。
中介軟體機制
接下來是一些內部中介軟體的處理,當配置開發伺服器選項時,vite
內部通過 connect
框架的中介軟體能力來提供支援。(如下圖)
其中,對 public
目錄、公共路徑等多項配置都是通過 connect
+ 中介軟體實現的,充分地利用了第三方庫的能力,而沒有重複造輪子。
預構建依賴
接下來,vite
內部對專案中使用到的依賴進行的預構建,一來是為了相容不同的 ES 模組規範,二來是為了提升載入效能。(如下圖)
準備工作就緒後,vite
內部呼叫 startServer
啟動本地開發伺服器。(如下)
// ...
httpServer.listen(port, host, () => {
httpServer.removeListener('error', onError)
resolve(port)
})
小結
至此,vite
本身的原始碼部分就解析完了。
可以看出,在本地開發時,vite
主要依賴 外掛 + 中介軟體體系
來提供能力支援。因為本地開發時只涉及到少量編譯工作,所以非常的快。只有在構建生產產物時,vite
才用到了 rollup
進行構建。
我們用一張流程圖來最後梳理一遍 vite 本地開發服務
內部的工作流程吧。
那麼本期文章就到此結束,在下一篇文章,我會挑選 1 - 2 個比較典型的外掛或是 build
篇(生產產物構建)來進行原始碼解析。
最後一件事
如果您已經看到這裡了,希望您還是點個贊再走吧~
您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!
如果覺得本文對您有幫助,請幫忙在 github 上點亮 star
鼓勵一下吧!