注:本篇是組內PPT分享內容,說的部分會比較多。弄成文章后還未整理,后續整理一下。
0. 源碼目錄結構
https://github.com/vitejs/vite
1. 源碼入口
1.1 運行npm run dev后發生了什么?
先準備一個vite腳手架生成的項目vite-vue3,當運行npm run dev
后發生了什么?在package.json中看到運行的是vite命令。
vite命令是在哪里注冊的呢,在node_modules/vite/package.json
中查看bin字段。
"bin"
字段的作用是能讓我們在命令窗口全局輸入命令執行??梢钥吹?vite 命令:
"bin": {
"vite": "bin/vite.js"
}
打開vite.js看到,其中主要運行的是
function start() {
require('../dist/node/cli')
}
這里的/dist/node/cli.js是打包后的文件,可能有點長,可以配合vite打包前的源碼一起閱讀。
在/dist/node/cli.js中,首先使用 cac 命令工具處理用戶的輸入。
然后命令執行的是createServer
,這個createServer
從 ./chunks/dep-55830a1a.js
引入,這里也是打包后的代碼,比較長。
1.2 createServer
我們找到了前面大概執行的順序后,這里回到源碼,在 packages/vite/src/node/server/index.ts
里面找到createServer
:
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
// Vite 配置整合
const config = await resolveConfig(inlineConfig, 'serve', 'development')
const root = config.root
const serverConfig = config.server
// 創建http服務
const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)
// 創建ws服務
const ws = createWebSocketServer(httpServer, config, httpsOptions)
// 創建watcher,設置代碼文件監聽
const watcher = chokidar.watch(path.resolve(root), {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(Array.isArray(ignored) ? ignored : [ignored])
],
...watchOptions
}) as FSWatcher
// 創建server對象
const server: ViteDevServer = {
config,
middlewares,
httpServer,
watcher,
ws,
moduleGraph,
listen,
...
}
// 文件監聽變動,websocket向前端通信
watcher.on('change', async (file) => {
...
handleHMRUpdate()
})
// 服務 middleware
middlewares.use(...)
// optimize: 預構建
await initDepsOptimizer(config, server)
// 監聽端口,啟動服務
httpServer.listen = (async (port, ...args) => { ... })
return server
}
可以看到 createServer
做了很多事情,上面列舉主要的幾個:
resolveConfig:整合配置(resolvePlugins)
注冊各種中間件(indexHtml、transformMiddleware、static)
HMR:使用chokidar監聽文件的修改
optimizeDeps:預構建
創建httpServer,啟動服務
...
2. 預構建
2.1 no-bundle vs bundle
怎么理解no-bundle?不打包?
2.2 為什么要預構建?
1、Vite是基于瀏覽器原生支持ESM的能力實現的,要求用戶的代碼模塊必須是ESM模塊,因此必須將commonJs的文件提前處理,轉化成 ESM 模塊并緩存入 node_modules/.vite
2、減少模塊和請求數量。例如,lodash-es 有超過 600 個內置模塊。當我們執行 import { debounce } from 'lodash-es'
時,瀏覽器同時發出 600 多個 HTTP 請求!盡管服務器在處理這些請求時沒有問題,但大量的請求會在瀏覽器端造成網絡擁塞,導致頁面的加載速度相當慢。
通過預構建 lodash-es 成為一個模塊,我們就只需要一個 HTTP 請求了!
2.3 預構建的核心流程
預構建的核心流程,包括緩存判斷、依賴掃描、依賴打包和元信息保存這四個主要的步驟。
關于預構建的實現代碼都在optimizeDeps
函數當中,在倉庫源碼的 packages/vite/src/node/optimizer/index.ts
查看 optimizeDeps
:
optimizeDeps
首先調用loadCachedDepOptimizationMetadata
獲取node_modules/.vite/_metadata.json
中的元信息。
然后調用getDepHash
,這個函數是讀取目錄下的package-lock.json的內容,然后將文件內容進行hash得到一個hash值。
然后兩個hash進行對比是否相等。
也就是說,預編譯就是看node_modules的包有沒有變化,如果不相等。會調用scanImports
去掃,在scanImports
中,得到入口文件后,對入口文件進行了解析,當然,具體的解析過程在依賴掃描階段的 Esbuild 插件(esbuildScanPlugin
)中得以實現。
這里就會使用esbuild.build
去編譯文件,其中esbuildDepPlugin
就是打包的插件:
生成出來保存到.vite下。最后,執行writeFile
,再將相關信息保存到_metadata.json
。
3. 核心編譯流程
webpack中plugin和loader的區別?
3.1 Rollup插件機制
Rollup 的打包過程中,會定義一套完整的構建生命周期,從開始打包到產物輸出,中途會經歷一些標志性的階段,并且在不同階段會自動執行對應的插件鉤子函數(Hook)。
Vite 的插件機制是基于 Rollup 來設計的。Vite 模擬了 Rollup 的插件機制,設計了一個 PluginContainer
對象來調度各個插件。
PluginContainer
的 實現 基于借鑒于 WMR 中的rollup-plugin-container.js,主要分為 2 個部分:
1、實現 Rollup 插件鉤子的調度
2、實現插件鉤子內部的 Context 上下文對象
PluginContainer的定義了一系列執行plugin的方法。如buildStart、resolveId、load、transform。
3.2 vite插件的接口定義
packages/vite/src/node/plugin.ts
:
export interface Plugin extends RollupPlugin {
enforce?: 'pre' | 'post'
apply?: 'serve' | 'build' | ((config: UserConfig, env: ConfigEnv) => boolean)
config?: (
config: UserConfig,
env: ConfigEnv
) => UserConfig | null | void | Promise<UserConfig | null | void>
configResolved?: (config: ResolvedConfig) => void | Promise<void>
configureServer?: ServerHook
configurePreviewServer?: PreviewServerHook
transformIndexHtml?: IndexHtmlTransform
handleHotUpdate?(
ctx: HmrContext
): Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>
resolveId?(
this: PluginContext,
source: string,
importer: string | undefined,
options: {
custom?: CustomPluginOptions
ssr?: boolean
/**
* @internal
*/
scan?: boolean
}
): Promise<ResolveIdResult> | ResolveIdResult
load?(
this: PluginContext,
id: string,
options?: { ssr?: boolean }
): Promise<LoadResult> | LoadResult
transform?(
this: TransformPluginContext,
code: string,
id: string,
options?: { ssr?: boolean }
): Promise<TransformResult> | TransformResult
}
3.3 當瀏覽器一個JS請求到vite服務時,發生了什么?
例如:
<script type="module" src="/src/main.js"></script>
或者 import { get } from './utils';
// main transform middleware
middlewares.use(transformMiddleware(server))
可以看到pluginContainer會執行插件中的鉤子。對于不同的資源會有不同的插件去處理。
vite的內置插件:
路徑解析插件(packages/vite/src/node/plugins/resolve.ts)
路徑解析插件(即vite:resolve)是 Vite 中比較核心的插件,幾乎所有重要的 Vite 特性都離不開這個插件的實現,諸如依賴預構建、HMR、SSR 等等。
CSS 編譯插件(packages/vite/src/node/plugins/css.ts)
import分析插件(packages/vite/src/node/plugins/importAnalysis.ts)
重寫import語句,如import Vue from 'vue';
導入路徑會重寫為預構建文件夾的路徑;
注入HMR客戶端腳本。
Esbuild 轉譯插件(packages/vite/src/node/plugins/esbuild.ts)
用來進行 .js、.ts、.jsx和tsx,代替了傳統的 Babel 或者 TSC 的功能,這也是 Vite 開發階段性能強悍的一個原因。
4. HMR流程
打包工具實現熱更新的思路都大同小異:主要是通過WebSocket創建瀏覽器和服務器的通信監聽文件的改變,當文件被修改時,服務端發送消息通知客戶端修改相應的代碼,客戶端對應不同的文件進行不同的操作的更新。
瀏覽器文件是幾時被注入的?在importAnalysis插件中:
if (hasHMR && !ssr) {
debugHmr(
`${
isSelfAccepting
? `[self-accepts]`
: acceptedUrls.size
? `[accepts-deps]`
: `[detected api usage]`
} ${prettyImporter}`
)
// inject hot context
str().prepend(
`import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` +
`import.meta.hot = __vite__createHotContext(${JSON.stringify(
importerModule.url
)});`
)
}