前端工具鏈十年盤點:https://mp.weixin.qq.com/s/FBxVpcdVobgJ9rGxRC2zfg
Webpack、Rollup 、Esbuild、Vite ?
webpack: 基于 JavaScript 開發的前端打包構建框架,通過依賴收集,模塊解析,生成 chunk,最終輸出生成的打包產物,是一個BundleBased的框架,優點是大而全,缺點是配置繁瑣。
Rollup: Rollup 是專門針對類庫進行打包,它的優點是小巧而專注,現在很多我們熟知的庫都都使用它進行打包,比如:Vue、React 和 three.js 等。
Esbuild: 一個基于 Go 編寫的高性能構建工具,和其他構建工具相比,速度快到 ?10-100x,其內置了一些 Loader 能解析編譯常見的 JS(X)、TS(X)等文件,同時支持通過插件的形式處理其他類型的文件。
Vite: Vite 基于_ESMBased_devServer 在開發環境實現了快速啟動、按需編譯、即時模塊熱更新等能力,同時針對同一份代碼,在生產環境通過 Rollup 進行打包,生成線上產物。
Vite 簡介
背景驅動
目前比較成熟的前端開發構建工具,如 webpack 等,基本是通過“打包”的方式來進行源碼構建,即通過對源碼進行依賴收集、構建處理,最終生成可在瀏覽器運行的 JS 文件,然而隨著項目增長,他們存在以下問題
打包構建時間也會隨著增長,項目本地啟動緩慢
更新緩慢,即使使用 HMR 開發,也需要幾秒的時間代碼變更才能反映到頁面上,嚴重影響開發體驗
得益于現在前端生態系統的快速發展,Vite 基于下面兩個新特性去解決上述存在的問題
瀏覽器開始支持 原生 ES 模塊
越來越多 JavaScript 工具使用編譯型語言如 Go 等進行編寫,加快了構建速度
核心功能
本地開發環境,利用瀏覽器支持原生 ESM 文件的特性,不對源代碼進行打包操作,瀏覽器直接動態引入資源,并在 devServer 對請求的資源進行處理,最終返回瀏覽器可運行的內容
依賴預構建,首次啟動的時候,通過 Esbuild 對項目的依賴進行預構建,并緩存在本地,后續瀏覽器請求的時候可以直接返回
更高效的HMR模塊,利用瀏覽器的緩存特性,優化資源的請求,使得無論應用大小如何,HMR 始終能保持快速更新
在生產產物構建時,基于 Rollup 進行打包,并提供了一套 ?構建優化? 的 ?構建命令,開箱即用。
Vite 核心模塊原理
本次分享主要介紹最核心的兩個功能的實現原理
依賴預構建
瀏覽器模塊加載流程
源碼初識
源碼版本:v2.8.2
之前有簡單了解過 Webpack 的源碼,看的一頭霧水,這一層層的 callback 都是些啥?然而 vite 框架的源碼看起來就很簡潔明了,非常易懂
./src
├── client # 客戶端運行時WEB SOCKET以及HMR相關的代碼
│ ├── client.ts
│ ├── env.ts
│ ├── overlay.ts
│ └── tsconfig.json
└── node # 本地服務器相關代碼├── __tests__├── build.ts # 生產環境rollup build代碼├── certificate.ts├── cli.ts # cli,入口├── config.ts├── constants.ts├── http.ts├── importGlob.ts├── index.ts # 導出出口├── logger.ts├── optimizer # 依賴預構建├── packages.ts├── plugin.ts├── plugins # 插件├── preview.ts # build構建后,在預覽模式下啟動Vite Server,以模擬生產部署├── server # server文件夾,dev環境主要代碼├── ssr├── tsconfig.json└── utils.ts7 directories, 18 files
我們主要關注server
目錄下的代碼,框架通過在本地啟動一個 http+connect 的服務器,然后在啟動之前做一些優化操作主入口在src/server/index.ts
的createServer
函數中,這個函數里做了以下幾件事情
流程初始化
1)調用resolveConfig
函數,解析合并各種配置
2)初始化一個http+connect
服務器
3)創建插件容器 ,createPluginContainer
方法,把插件的各個鉤子函數串聯起來,后續在請求處理的過程中直接執行掛載好的鉤子函數
4)生成一個server
對象,包含配置信息、服務器信息、一些輔助函數等
5)配置一系列內置中間件,各個中間件做的事情,可以參考文章https://www.modb.pro/db/966326)返回 server 對象
調用 server 的 listen 方法
1)運行插件container
的buildStart
鉤子,進而運行所有插件的buildStart
鉤子
2)進行依賴預構建,運行runOptimize
函數。
3)開啟服務,監聽端口
請求處理流程
1)主要處理流程在tansformMiddleware中間件處理,這部分后面的內容會詳細介紹
依賴預構建
進行依賴預構建有兩個目的:
CommonJS 和 UMD 兼容性: 開發階段中,Vite 的開發服務器將所有代碼視為原生 ES 模塊。因此,Vite 必須先將作為 CommonJS 或 UMD 發布的依賴項轉換為 ESM。
性能:Vite 將有許多內部模塊的 ESM 依賴關系轉換為單個模塊,以提高后續頁面加載性能。例如將 lodash 中的小模塊打包成一個大的文件
參數配置
首先看一下,vite 配置中關于 optimizeDeps 的入參
export interface DepOptimizationOptions {/*** 入口文件,默認從html文件進行解析收集依賴,如果配置了的話,就從配置文件開始進行解析*/entries?: string | string[]/*** 需要進行預構建的文件*/include?: string[]/*** 不需要進行預構建的依賴*/exclude?: string[]/*** 預構建是通過esbuild進行的,所以可以自定義配置esbuild參數*/esbuildOptions?: Omit<EsbuildBuildOptions,| 'bundle'| 'entryPoints'| 'external'| 'write'| 'watch'| 'outdir'| 'outfile'| 'outbase'| 'outExtension'| 'metafile'>
}
預構建結果
預構建的結果默認保存在node_modules/.vite
中,具體預構建的依賴列表在_metadata.json 文件中,其中_metadata.json 的內容為一個 json 結構
{// 配置的hash值hash : afcda65e ,/*** 主要用于瀏覽器獲取預構建的 npm 依賴時,添加的查詢字符串* 在依賴變化時,瀏覽器能更新緩存*/browserHash : c369dd06 ,optimized : { // 預構建的優化列表react : {// 構建后的文件地址file : /Users/zhachunliu/Desktop/own/demo/vite-demo/vite-react-project/node_modules/.vite/react.js ,// 原始文件地址src : /Users/zhachunliu/Desktop/own/demo/vite-demo/vite-react-project/node_modules/react/index.js ,// 記錄那些在依賴預構建時,使用了commonjs語法的依賴// 如果使用了commonjs語法,那么 needsInterop 為 trueneedsInterop : true},react-dom : {file : /Users/zhachunliu/Desktop/own/demo/vite-demo/vite-react-project/node_modules/.vite/react-dom.js ,src : /Users/zhachunliu/Desktop/own/demo/vite-demo/vite-react-project/node_modules/react-dom/index.js ,needsInterop : true},lodash : {file : /Users/zhachunliu/Desktop/own/demo/vite-demo/vite-react-project/node_modules/.vite/lodash.js ,src : /Users/zhachunliu/Desktop/own/demo/vite-demo/vite-react-project/node_modules/lodash/lodash.js ,needsInterop : true},react/jsx-dev-runtime : {file : /Users/zhachunliu/Desktop/own/demo/vite-demo/vite-react-project/node_modules/.vite/react_jsx-dev-runtime.js ,src : /Users/zhachunliu/Desktop/own/demo/vite-demo/vite-react-project/node_modules/react/jsx-dev-runtime.js ,needsInterop : true}}
}
預構建過程
入口文件:src/node/optimizer/index.ts
,入口函數:optimizeDeps,構建過程如下
調用
getDepHash()
函數去計算當前依賴相關的的 hash 值,影響依賴預構建 hash 值的內容有包管理器的 lockfile,例如 package-lock.json,yarn.lock,或者 pnpm-lock.yaml
vite.config.js 中的部分相關配置,如 plugins、optimizeDeps 的 include 和 exclude 等
讀取本地_metadata.json 中的 hash 值,判斷和計算出來的是否一致,一致且未設置強制構建的話,則直接結束預構建過程,否則需要進入預構建過程
通過
({ deps, missing } = await ``scanImports``(config));
進行依賴掃描,得到需要處理的依賴,deps 是一個對象,是依賴的包名和文件系統中的路徑的映射,如下圖所示
scanImports
方法會掃描根目錄下的所有 .html 文件或者用戶配置對 optimizeDeps.entries 文件,然后找到文件中所有的 script 標簽,這樣就找到了入口 js 文件,之后調用 esbuild,通過配置的插件,就可以一層層的找到對應的依賴項了使用
es-module-lexer
的parse
處理所有的 deps,獲得其中exportsData
內容 ,并得到依賴 id 到exportsData
的映射,用于之后esbuild
構建時進行依賴圖分析并打包到一個文件里面,parse 解析后的結構如下圖所示
調用
esbuild
進行依賴的預構建,并將構建之后的文件寫入緩存目錄node_modules/.vite
,得益于 esbuild 比傳統構建工具快 10-100 倍的速度,所以依賴預構建也是非常快的
將 metadata 信息寫進本地緩存目錄下,后續可以直接使用緩存的依賴
依賴訪問過程
在進行了依賴預構建之后,如何訪問這些已經構建的依賴呢
1)在加載資源文件的時候,會通過vite:import-analysis
插件進行依賴解析,碰到已經進行預構建的依賴,直接替換,將import React from 'react'
替換成import __vite__cjsImport2_react from /node_modules/.vite/react.js?v=0f16c3f0
這樣的形式
2)在瀏覽器去請求資源的時候,通過resolvePlugin
插件去解析,獲得真正的本地文件,匹配到對應的本地緩存資源
模塊加載
對于瀏覽器請求,針對一個文件的訪問,vite 會如何進行處理呢?
主要由以下兩個中間件來統一處理請求的內容,并在中間件處理的流程中調用 vite 插件容器的相關鉤子函數
transformMiddleware
:核心中間件處理代碼
indexHtmlMiddleware
:html 相關請求處理中間件
vite 插件體系
在這里,我們先了解一下 vite 的插件體系,Vite 插件擴展了設計出色的 Rollup 接口,帶有一些 Vite 獨有的配置項。
因此,你只需要編寫一個 Vite 插件,就可以同時為開發環境和生產環境工作。vite 的插件其實就是定義一個對象,該對象包含了一系列的 hook 函數配置
export default function myPlugin() {const virtualModuleId = '@my-virtual-module'const resolvedVirtualModuleId = '\0' + virtualModuleIdreturn {name: 'my-plugin', // 必須的,將會在 warning 和 error 中顯示resolveId(id) {if (id === virtualModuleId) {return resolvedVirtualModuleId}},load(id) {if (id === resolvedVirtualModuleId) {return `export const msg = from virtual module `}}}
}
Rollup 插件兼容性
相當數量的 Rollup 插件將直接作為 Vite 插件工作,但并不是所有的,因為有些插件鉤子在非構建式的開發服務器上下文中沒有意義。
一般來說,只要 Rollup 插件符合以下標準,它就應該像 Vite 插件一樣工作:
沒有使用
moduleParsed
鉤子。
它在打包鉤子和輸出鉤子之間沒有很強的耦合。
和rollup保持一致的通用鉤子以下鉤子在服務器啟動時被調用:
options
buildStart
以下鉤子會在每個傳入模塊請求時被調用:
resolveId
load
transform
以下鉤子在服務器關閉時被調用:
buildEnd
closeBundle
Vite 獨有鉤子
Vite 插件也可以提供鉤子來服務于特定的 Vite 目標。這些鉤子會被 Rollup 忽略。
config
configResolved
transformIndexHtml
handleHotUpdate
具體插件執行過程
1)在 dev 環境模擬了一套和 rollup 保持一致的插件運行環境,確保在開發環境和生產環節的核心環節執行同樣的流程
2)vite 通過createPluginContainer創建了一個插件容器,將每個插件中對應的 hook 收集起來
3)最終在各個生命周期階段,執行對應的已經收集好的鉤子
模塊請求加載過程
GET /
當訪問頁面的時候,實際是有一個 GET / => /index.html 的重定向進入 indexHtmlMiddleware 這個過程,主要做了一件事情,注入 dev 環境需要的一些依賴,@vite/client 主要用來和服務器進行 ws 通信并處理一些 hmr 相關的工作,@react/refresh
這段代碼,是 vite-plugin-react 插件注入的代碼,用來處理 dev 環境的一些能力
GET /@vite/client
前面講到,@vite/client 里面的代碼主要用于與服務器進行 ws 通信來進行 hmr 熱更新、以及重載頁面等操作。
這個請求會直接進入 transformMiddleware 中間件中,進入中間件的處理過程:中間件會調用transformRequest(url, server, options = {})
函數
@vite/client 是如何映射到對應的內容呢,在調用
pluginContainer.resolveId
的過程中會遇到 aliasPlugin 插件的鉤子,執行名稱替換,最終替換成vite/dist/client/client.mjs
繼續將改寫過的路徑傳給下一個插件,最終進入
resolvePlugin
插件的tryNodeResolve
函數,最終解析獲得該文件的 id 為/Users/zhachunliu/.nvm/versions/node/v14.17.0/lib/node_modules/vite/dist/client/client.mjs
最終通過
pluginContainer.load
獲取加載本地文件,然后通過pluginContainer.transform
進行代碼轉換,將轉換后的代碼通過send
方法發送給瀏覽器
GET /src/main.tsx
針對普通的 tsx 文件的請求,流程基本上和上面介紹的GET /@vite/client
一致,不同點在于使用的插件鉤子內容不一樣,因為需要對 tsx 文件進行處理成 js
通過 resolveId 鉤子函數,將/src/main.tsx 映射到本地文件系統
調用 load 鉤子函數,加載本地文件到內存中
通過 vite:react-babel 插件,將 jsx 語法進行轉換,轉換成 js 代碼
通過 vite:esbuild 插件,進行代碼格式化
通過 vite:import-analysis 插件,將代碼中所有的 import 內容,轉換成對應的本地文件,方便后續直接請求
返回結果
其他的所有請求,都是經過類似的插件處理流程,最終返回給瀏覽器一段可執行的 JS 代碼,就不一一介紹了。
vite 調試工具
vite-plugin-inspect(插件調試工具,強推)
在學習、調試或創作插件時,建議在你的項目中引入vite-plugin-inspect。它可以幫助你檢查 Vite 插件的中間狀態。安裝后,你可以訪問localhost:3000/__inspect/
來檢查你項目的模塊和棧信息。請查閱vite-plugin-inspect 文檔中的安裝說明。
Vite debug 模式
通過vite --force --debug
命令,可以明確的了解到,啟動過程和請求過程,經歷了什么插件,具體的執行流程等,方便調試
參考資料
前端工具鏈十年盤點:https://mp.weixin.qq.com/s/FBxVpcdVobgJ9rGxRC2zfg
如何調試 vite 源碼:https://maximomussini.com/posts/debugging-javascript-libraries
源碼理解:https://jishuin.proginn.com/p/763bfbd5f00e
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
掃碼加我微信 ruochuan02、拉你進源碼共讀群
今日話題
目前建有江西|湖南|湖北?籍 前端群,想進群的可以加我微信 ruochuan12?進群。分享、收藏、點贊、在看我的文章就是對我最大的支持~