Vite 特性和部分源碼解析


Vite 的特性
Vite 的主要特性就是 Bundleless。基於瀏覽器開始原生的支持 JavaScript 模塊功能,JavaScript 模塊依賴於 import 和 export 的特性,目前主流瀏覽器基本都支持;

那這有什么優勢呢?

去掉打包步驟
打包是開發者利用打包工具將應用各個模塊集合在一起形成 bundle,以一定規則讀取模塊的代碼,以便在不支持模塊化的瀏覽器里使用,並且可以減少 http 請求的數量。但其實在本地開發過程中打包反而增加了我們排查問題的難度,增加了響應時長,Vite 在本地開發命令中去除了打包步驟,從而縮短構建時長。

按需加載
為了減少 bundle 大小,一般會想要按需加載,主要有兩種方式:

使用動態引入 import() 的方式異步的加載模塊,被引入模塊依然需要提前編譯打包;
使用 tree shaking 等方式盡力的去掉未引用的模塊;
而 Vite 的方式更為直接,它只在某個模塊被 import 的時候動態的加載它,實現了真正的按需加載,減少了加載文件的體積,縮短了時長;

Vite開發環境主體流程
下圖是 Vite 在開發環境運行時加載文件的主體流程。

Vite 部分源碼解析
總體目錄結構
|-CHANGELOG.md
|-LICENSE.md
|-README.md
|-bin
| |-openChrome.applescript
| |-vite.js
|-client.d.ts
|-package.json
|-rollup.config.js #打包配置文件
|-scripts
| |-patchTypes.js
|-src
| |-client #客戶端
| | |-client.ts
| | |-env.ts
| | |-overlay.ts
| | |-tsconfig.json
| |-node #服務端
| | |-build.ts
| | |-cli.ts #命令入口文件
| | |-config.ts
| | |-constants.ts #常量
| | |-importGlob.ts
| | |-index.ts
| | |-logger.ts
| | |-optimizer
| | | |-esbuildDepPlugin.ts
| | | |-index.ts
| | | |-registerMissing.ts
| | | |-scan.ts
| | |-plugin.ts #rollup 插件
| | |-plugins #插件相關文件
| | | |-asset.ts
| | | |-clientInjections.ts
| | | |-css.ts
| | | |-esbuild.ts
| | | |-html.ts
| | | |-index.ts
| | | |-...
| | |-preview.ts
| | |-server
| | | |-hmr.ts #熱更新
| | | |-http.ts
| | | |-index.ts
| | | |-middlewares #中間件
| | | | |-...
| | | |-moduleGraph.ts #模塊間關系組裝(樹形)
| | | |-openBrowser.ts #打開瀏覽器
| | | |-pluginContainer.ts
| | | |-send.ts
| | | |-sourcemap.ts
| | | |-transformRequest.ts
| | | |-ws.ts
| | |-ssr
| | | |-tests
| | | | |-ssrTransform.spec.ts
| | | |-ssrExternal.ts
| | | |-ssrManifestPlugin.ts
| | | |-ssrModuleLoader.ts
| | | |-ssrStacktrace.ts
| | | |-ssrTransform.ts
| | |-tsconfig.json
| | |-utils.ts
|-tsconfig.base.json
|-types
| |-...
復制代碼
server 核心方法
從入口文件 cli.ts,可以看到三個命令對應了 3 個核心的文件&方法;

dev 命令
文件路徑:./server/index.ts;

主要方法:createServer;

主要功能:項目的本地開發命令,基於 httpServer 啟動服務,Vite 通過對請求路徑的劫持獲取資源的內容返回給瀏覽器,服務端將文件路徑進行了重寫。例如:

項目源碼如下:

import { createApp } from 'vue';
import App from './index.vue';
復制代碼
經服務端重寫后,node_modules 文件夾下的三方包代碼路徑也會被拼接完整。

import __vite__cjsImport0_vue from "/node_modules/.vite/vue.js?v=ed69bae0";
const createApp = __vite__cjsImport0_vue["createApp"];
import App from '/src/pages/back-sky/index.vue';
復制代碼
2.build 命令 文件路徑:./build.ts ;

主要方法:build;

主要功能:使用 rollup 打包編譯

3.optimize 命令

文件路徑:./optimizer/index.ts;

主要方法:optimizeDeps;

主要功能:主要針對第三方包,Vite 在執行 runOptimize 的時候中會使用 rollup 對三方包重新編譯,將編譯成符合 esm 模塊規范的新的包放入 node_modules 下的 .vite 中,然后配合 resolver 對三方包的導入進行處理:使用編譯后的包內容代替原來包的內容,這樣就解決了 Vite 中不能使用 cjs 包的問題。

下面是 .vite 文件夾中的 _metadata.json 文件,它在預編譯的過程中生成,羅列了所有被預編譯完成的文件及其路徑。例如:

{
"hash": "31d458ff",
"browserHash": "ed69bae0",
"optimized": {
"element-plus/lib/utils/dom": {
"file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/element-plus_lib_utils_dom.js",
"src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/utils/dom.js",
"needsInterop": true
},
"element-plus": {
"file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/element-plus.js",
"src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/index.esm.js",
"needsInterop": false
},
"vue": {
"file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js",
"src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/vue/dist/vue.runtime.esm-bundler.js",
"needsInterop": true
},
......
}
}
}
復制代碼
模塊解析
預構建是用來提升頁面重載速度,它將 CommonJS、UMD 等轉換為 ESM 格式。預構建這一步由 esbuild 執行,這使得 Vite 的冷啟動時間比任何基於 JavaScript 的打包程序都要快得多。

為什么 ESbuild 會更快?

使用 Go 語言
重度並行,使用 CPU
高效使用內存
Scratch 編寫,減少使用三方庫,避免導致性能不可控
重寫導入為合法的 URL,例如 /node_modules/.vite/my-dep.js?v=f3sf2ebd 以便瀏覽器能夠正確導入它們

熱更新
熱更新主體流程如下:

服務端基於 watcher 監聽文件改動,根據類型判斷更新方式,並編譯資源
客戶端通過 WebSocket 監聽到一些更新的消息類型
客戶端收到資源信息,根據消息類型執行熱更新邏輯

下面是服務端熱更新的核心 hmr.ts 中的部分判斷邏輯;

如果配置文件或者環境文件發生修改時,會觸發服務重啟,才能讓配置生效。

if (file === config.configFile || file.endsWith('.env')) {
// auto restart server 配置&環境文件修改則自動重啟服務
debugHmr([config change] ${chalk.dim(shortFile)})
config.logger.info(
chalk.green('config or .env file changed, restarting server...'),
{ clear: true, timestamp: true }
)
await restartServer(server)
return
}
復制代碼
html 文件更新時,將會觸發頁面的重新加載。

if (file.endsWith('.html')) { // html 文件更新
config.logger.info(chalk.green(page reload ) + chalk.dim(shortFile), {
clear: true,
timestamp: true
})
ws.send({
type: 'full-reload',
path: config.server.middlewareMode
? '*'
: '/' + normalizePath(path.relative(config.root, file))
})
} else {
// loaded but not in the module graph, probably not js
debugHmr([no modules matched] ${chalk.dim(shortFile)})
}
復制代碼
Vue 等文件更新時,都會進入 updateModules 方法,正常情況下只會觸發 update,實現熱更新,熱替換;

function updateModules(
file: string,
modules: ModuleNode[],
timestamp: number,
{ config, ws }: ViteDevServer
) {
const updates: Update[] = []
const invalidatedModules = new Set ()
// 遍歷插件數組,關聯下面的片段
for (const mod of modules) {
const boundaries = new Set<{
boundary: ModuleNode
acceptedVia: ModuleNode
}>()
// 設置時間戳
invalidate(mod, timestamp, invalidatedModules)
// 查找引用模塊,判斷是否需要重載頁面
const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries)
// 找不到引用者則會發起刷新
if (hasDeadEnd) {
config.logger.info(chalk.green( page reload ) + chalk.dim(file), {
clear: true,
timestamp: true
})
ws.send({
type: 'full-reload'
})
return
}
updates.push(
...[...boundaries].map(({ boundary, acceptedVia }) => {
type: ${boundary.type}-update as Update['type'],
timestamp,
path: boundary.url,
acceptedPath: acceptedVia.url
}))
)
}
// 日志輸出
config.logger.info(
updates
.map(({ path }) => chalk.green( hmr update ) + chalk.dim(path))
.join('\n'),
{ clear: true, timestamp: true }
)
// 向客戶端發送消息,進行熱更新操作
ws.send({
type: 'update',
updates
})
}
復制代碼
上面代碼中的 modules 是熱更新時需要執行的各個插件

for (const plugin of config.plugins) {
if (plugin.handleHotUpdate) {
const filteredModules = await plugin.handleHotUpdate(hmrContext)
if (filteredModules) {
hmrContext.modules = filteredModules
}
}
}
復制代碼
Vite 會把模塊的依賴關系組合成 moduleGraph,它的結構類似樹形,熱更新中判斷哪些文件需要更新也會依賴 moduleGraph;它的文件內容大致如下:

// moduleGraph 返回的 ModuleNode 大致結構
ModuleNode {
id: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.js',
file: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.js',
importers: Set {},
importedModules: Set {
ModuleNode {
id: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js?v=32cfd30c',
file: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js',
......
lastHMRTimestamp: 0,
url: '/node_modules/.vite/vue.js?v=32cfd30c',
type: 'js'
},
ModuleNode {
id: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.vue',
file: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.vue',
......
url: '/src/pages/back-sky/index.vue',
type: 'js'
},
ModuleNode {
id: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/theme-chalk/index.css',
file: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/theme-chalk/index.css',
importers: [Set],
importedModules: Set {},
acceptedHmrDeps: Set {},
isSelfAccepting: true,
transformResult: [Object],
ssrTransformResult: null,
ssrModule: null,
lastHMRTimestamp: 0,
url: '/node_modules/element-plus/lib/theme-chalk/index.css',
type: 'js'
},
......
},
acceptedHmrDeps: Set {},
isSelfAccepting: false,
transformResult: {
code: 'import __vite__cjsImport0_vue from ' +
'"/node_modules/.vite/vue.js?v=32cfd30c"; const createApp = ' +
'__vite__cjsImport0_vue["createApp"];\nimport App from ' +
"'/src/pages/back-sky/index.vue';\nimport " +
"'/node_modules/element-plus/lib/theme-chalk/index.css';\n\nconst app = " +
'createApp(App);\n\nimport { addHistoryMethod } from ' +
"'/src/pages/back-sky/api/index.js';\nimport {\n ElButton,\n ElDropdown,\n " +
'ElDropdownMenu,\n ElDropdownItem,\n ElMenu,\n ElSubmenu,\n ElMenuItem,\n ' +
'ElMenuItemGroup,\n ElPopover,\n ElDialog,\n ElRow,\n ElInput,\n ' +
"ElLoading,\n} from '/node_modules/.vite/element-plus.js?v=32cfd30c';\n\n" +
'app.use(ElButton);\napp.use(ElLoading);\napp.use(ElDropdown);\n' +
'app.use(ElDropdownMenu);\napp.use(ElDropdownItem);\napp.use(ElMenu);\n' +
'app.use(ElSubmenu);\napp.use(ElMenuItem);\napp.use(ElMenuItemGroup);\n' +
'app.use(ElPopover);\napp.use(ElDialog);\napp.use(ElRow);\napp.use(ElInput);\n' +
"\nconst f = ()=>{\n return app.mount('#app');\n};\n\nconst $backsky = " +
"document.getElementById('back-sky');\nif($backsky) {\n $backsky.innerHTML " +
"= '';\n $backsky.appendChild(f().$el);\n} else {\n window.onload = " +
"function(){\n document.getElementById('back-sky') && " +
"document.getElementById('back-sky').appendChild(f().$el);\n };\n}\n\n" +
"window.addHistoryListener = addHistoryMethod('historychange');\n" +
"history.pushState = addHistoryMethod('pushState');\nhistory.replaceState " +
"= addHistoryMethod('replaceState');\n\n// 監聽hash路由變化,不與onhashchange互相覆蓋\n" +
'addHashChange(()=>{\n setTimeout(() => {\n const $backsky = ' +
"document.getElementById('back-sky');\n if($backsky && " +
"$backsky.innerHTML === '') {\n $backsky.appendChild(f().$el);\n }\n " +
" },0);\n});\n\nfunction addHashChange(callback) {\n if('onhashchange' in " +
'window === false){//瀏覽器不支持\n return false;\n }\n ' +
'if(window.addEventListener) {\n ' +
"window.addEventListener('hashchange',function(e) {\n callback && " +
'callback(e);\n },false);\n }else if(window.attachEvent) {//IE 8 及更早 IE ' +
"版本瀏覽器\n window.attachEvent('onhashchange',function(e) {\n callback " +
'&& callback(e);\n });\n }\n ' +
"window.addHistoryListener('history',function(e){\n callback && " +
'callback(e);\n });\n}\n\n\n',
map: null,
etag: 'W/"846-Qa424gJKl3YCqHDWXXsM1mFHRqg"'
},
ssrTransformResult: null,
ssrModule: null,
lastHMRTimestamp: 0,
url: '/src/pages/back-sky/index.js',
type: 'js'
}
復制代碼
原有項目切換
最后我們來看下如何使用 Vite 去打包一個舊的 Vue 項目;

首先我們需要升級 Vue3

npm install vue@next
復制代碼
並為項目添加 vite 配置文件,在根目錄下創建 vite.config.js,並為它添加一些基礎的配置。

// vite.config.js
// vite2.1.5
const path = require('path');
import vue from '@vitejs/plugin-vue';

export default {
// 配置選項
resolve: {
alias: {
'@utils': path.resolve(__dirname, './src/utils')
},
},
plugins: [vue()],
};
復制代碼
引用的第三方組件庫可能也會需要升級,例如:升 element-ui 至 element-plus

npm install element-plus
復制代碼
Vue3 在 import 時,需使用 createApp 方法進行初始化

import { createApp } from 'vue';
import App from './index.vue';
const app = createApp(App);
import {
ElInput,
ElLoading,
} from 'element-plus';

app.use(ElButton);
app.use(ElLoading);
......
復制代碼
到這里就可以將項目運行起來了。 注意:Vite 官方不允許省略 .vue 后綴,否則就會報錯;

[plugin:vite:import-analysis] Failed to resolve import "./todoList" from "src/pages/back-sky/components/header/index.vue". Does the file exist?
/components/header/index.vue:2:23
1 |
2 | import todoList from './todoList';
import todoList from './todoList.vue';
復制代碼
最后我們來對比一下該項目兩種構建方式時間的對比;

Webpack 冷啟動,耗時 7513ms:

⚠ 「wdm」: Hash: 1ad1dd54289cfad8ecbe
Version: webpack 4.46.0
Time: 7513ms
Built at: 2021-05-24 13:59:35
復制代碼
相同項目 Vite 冷啟動,耗時 924ms:

vite
Pre-bundling dependencies:
vue
element-plus
@zcy/zcy-request
element-plus/lib/utils/dom
(this will be run only when your dependencies or config have changed)
vite v2.3.3 dev server running at:
Local: http://localhost:3000/
Network: use --host to expose
ready in 924ms.
復制代碼
二次啟動(預編譯的依賴已存在),耗時 407ms;

vite
vite v2.3.3 dev server running at:
Local: http://localhost:3000/
Network: use --host to expose
ready in 407ms.
復制代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM