Vite 預構建的核心原理
1. 兼容性與性能的雙重目標
Vite 的預構建旨在解決兩個主要問題:兼容性和性能。對於兼容性,由於 Vite 在開發階段將所有代碼視爲原生 ES 模塊,因此需要將 CommonJS 或 UMD 格式的依賴轉換爲 ESM 格式。對於性能,Vite 通過預構建將多個內部模塊的 ESM 依賴關係轉換爲單個模塊,減少了網絡請求的數量,從而提高了頁面加載速度。
2. 自動依賴搜尋
Vite 通過掃描項目源碼自動尋找引入的依賴項,並將這些依賴項作爲預構建包的入口點。這一過程通過 esbuild
執行,因此非常快速。如果在服務器啓動後遇到新的依賴關係導入,Vite 將重新運行依賴構建進程並重新加載頁面。
2. 工作過程
當聲明一個script
標籤類型爲module
時,如
<script type="module" src="/src/main.js"></script>
GET
請求main.js
文件
// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
main.js
文件,會檢測到內部含有import
引入的包,又會import
引用發起HTTP
請求獲取模塊的內容文件,如App.vue
、vue
文件
Vite
其核心原理是利用瀏覽器現在已經支持ES6
的import
,碰見import
就會發送一個HTTP
請求去加載文件,Vite
啓動一個koa
服務器攔截這些請求,並在後端進行相應的處理將項目中使用的文件通過簡單的分解與整合,然後再以ESM
格式返回返回給瀏覽器。Vite
整個過程中沒有對文件進行打包編譯,做到了真正的按需加載,所以其運行速度比原始的webpack
開發編譯速度快出許多。
預構建的實現細節
1.依賴預構建的觸發
當首次啓動 Vite 開發服務器時,Vite 會檢查是否存在預構建的依賴。如果沒有找到相應的緩存,Vite 將抓取源碼並自動尋找引入的依賴項。這個過程是通過 Vite 的內部插件 esbuildScanPlugin
實現的,它會遍歷所有的入口文件,解析出依賴列表,並進行預構建。
2.預構建過程
預構建過程是通過 Vite 的 optimizeDeps
函數觸發的。該函數首先會檢查是否存在一個名爲 _metadata.json
的文件,該文件記錄了預構建模塊的信息。如果文件存在且哈希值與當前依賴的哈希值一致,Vite 將跳過預構建過程。如果哈希值不一致或文件不存在,Vite 將執行預構建,並更新 _metadata.json
文件。
3.緩存策略
Vite 的預構建依賴會緩存在 node_modules/.vite
目錄下。這個目錄中的文件會根據 package.json
、lockfile 以及 vite.config.js
中的配置來決定是否需要重新構建。這種緩存策略大大減少了重複構建的開銷,提高了開發效率。
模擬實踐
vite會攔截import,對於相對地址的文件,瀏覽器可以直接加載,但是對於像import { createApp } from 'vue'
這種加載一個裸模塊,vite就會通過一次預打包,將第三方模塊放在node_modules/.vite
,然後將裸模塊地址替換成相對地址。以及加載的是vue文件瀏覽器無法解析,vite也是需要將vue文件轉化成js文件。
所以我們第一步創建一個服務器,將裸模塊替換相對地址讓瀏覽器可以加載文件,第二步解析vue成js文件,讓瀏覽器可以識別
1、js加載和裸模塊路徑重寫
直接加載vue會瀏覽器會報錯
對裸模塊路徑重寫
const Koa = require('koa')
const fs=require('fs')
const path=require('path')
const app=new Koa();
app.use(async (ctx)=>{
const {url}=ctx.request;
if(url==='/'){
//返回主頁
ctx.type='text/html'
ctx.body=fs.readFileSync('./index.html','utf-8')
}else if(url.endsWith('.js')){
// js文件加載路徑處理
const p=path.join(__dirname,url);
ctx.type='application/javascript'
ctx.body=rewriteImport(fs.readFileSync(p,'utf-8'))
}
})
//裸模塊路徑重寫
//將import xxx from './xx' 替換成 import xxx from '/@moudle/xxx'
//將裸模塊進行替換和重寫,官方的處理方式是先使用esbuild打包後緩存在node_modules中
function rewriteImport(content){
return content.replace(/ from ['"](.*)['"]/g,function(s1,s2){
if(s2.startsWith('./')||s2.startsWith('/')||s2.startsWith('../')){
return s1
}else{
//裸模塊,需要替換
return ` from '/@moudles/${s2}'`
}
})
}
app.listen(3000,()=>{
console.log('kvite start')
})
重寫後
但是又有新的問題,裸模塊無法加載
2、對裸模塊加載進行處理
app.use(async (ctx)=>{
...
else if(url.startsWith('/@moudles/')){
const moudleName=url.replace('/@moudles/','');
// node_moudle中找
const prefix=path.join(__dirname,'../node_modules',moudleName)
//package中匹配
const moudle=require(prefix+'/package.json').moudle
const filePath=path.join(prefix,moudle)
const ret=fs.readFileSync(filePath,'utf-8');
ctx.type='application/javascript'
ctx.body=rewriteImport(ret)
}
...
})
處理後可以加載vue模塊了
對main.js文件進行豐富
<template>
<div>
{{ title }}
</div>
</template>
<script>
import { reactive } from "@vue/composition-api";
export default {
setup() {
const state = reactive({
title: "hello,kvite!!!",
});
},
};
</script>
3、開始解析SFC
app.use(async (ctx)=>{
...
else if(url.indexOf('.vue')>-1){
const p=path.join(__dirname,url.split('?')[0])
const ast=compilerSFC.parse(fs.readFileSync(p,'utf-8'))
if(!query.type){
//SFC請求
//讀取vue文件,解析爲js文件
//獲取腳本內容
const scriptContent=ast.descriptor.script.content;
const script=scriptContent.replace('export defalut ','const __script=')
ctx.type='application/javascript'
ctx.body=`
${rewriteImport(script)}
//解析tpl
import {render as __render} from '${url}?type=template'
__sciprt.render=__render
export defalut __sctipt
`
}else if(query.type==='template'){
const tpl=ast.descriptor.template.content;
const render=compilerDOM.compiler(tpl,{mode:module}).code
ctx.type='application/javascript'
ctx.body=rewriteImport(render)
}
}
})
成功輸出
完整代碼
const Koa = require('koa')
const fs=require('fs')
const path=require('path')
const compilerSFC =require('vue/compiler-sfc')
const compilerDOM=require('vue/compiler-dom')
const app=new Koa();
app.use(async (ctx)=>{
const {url}=ctx.request;
if(url==='/'){
ctx.type='text/html'
ctx.body=fs.readFileSync('./index.html','utf-8')
}else if(url.endsWith('.js')){
// js文件加載路徑處理
const __filenameNew = fileURLToPath(import.meta.url)
const __dirnameNew = path.dirname(__filenameNew)
const p=path.join(__dirnameNew,url);
ctx.type='application/javascript'
// ctx.body=fs.readFileSync(p,'utf-8')
ctx.body=rewriteImport(fs.readFileSync(p,'utf-8'))
}else if(url.startsWith('/@moudles/')){
const moudleName=url.replace('/@moudles/','');
// node_moudle中找
const __filenameNew = fileURLToPath(import.meta.url)
const __dirnameNew = path.dirname(__filenameNew)
const prefix=path.join(__dirnameNew,'../node_modules',moudleName)
//package中匹配
const moudle=require(prefix+'/package.json').moudle
const filePath=path.join(prefix,moudle)
const ret=fs.readFileSync(filePath,'utf-8');
ctx.type='application/javascript'
ctx.body=rewriteImport(ret)
}else if(url.indexOf('.vue')>-1){
const p=path.join(__dirname,url.split('?')[0])
const ast=compilerSFC.parse(fs.readFileSync(p,'utf-8'))
if(!query.type){
//SFC請求
//讀取vue文件,解析爲js文件
//獲取腳本內容
const scriptContent=ast.descriptor.script.content;
const script=scriptContent.replace('export defalut ','const __script=')
ctx.type='application/javascript'
ctx.body=`
${rewriteImport(script)}
//解析tpl
import {render as __render} from '${url}?type=template'
__sciprt.render=__render
export defalut __sctipt
`
}else if(query.type==='template'){
const tpl=ast.descriptor.template.content;
const render=compilerDOM.compiler(tpl,{mode:module}).code
ctx.type='application/javascript'
ctx.body=rewriteImport(render)
}
}
})
//裸模塊重寫
//將import xxx from './xx' 替換成 import xxx from '/@moudle/xxx'
//將裸模塊進行替換和重寫,官方的處理方式是先使用esbuild打包依賴在地址上
function rewriteImport(content){
return content.replace(/ from ['"](.*)['"]/g,function(s1,s2){
if(s2.startsWith('./')||s2.startsWith('/')||s2.startsWith('../')){
return s1
}else{
//裸模塊,需要替換
return ` from '/@moudles/${s2}'`
}
})
}
app.listen(3000,()=>{
console.log('dvite start')
})
作者:京東物流 段欣欣
來源:京東雲開發者社區