前言
接上篇文章,我們瞭解到vite的本地構建原理主要是:啓動一個 connect 服務器攔截由瀏覽器請求 ESM的請求。通過請求的路徑找到目錄下對應的文件做一下編譯最終以 ESM的格式返回給瀏覽器。
基於這個核心思想,我們可以嘗試來動手實現一下。
搭建靜態服務器
基於koa搭建一個項目:
項目結構如上,服務使用koa搭建,bin指定cli可執行文件的位置
#!/usr/bin/env node
// 代表該腳本使用node執行
const koa = require('koa');
const send = require('koa-send');
const App = new koa()
App.listen(3000, () => {
console.log('Server is running at http://localhost:3000');
});
這樣一個服務就搭建好了,為了方便調試,我們在該工作目錄下執行npm link,這樣可以將該項目鏈接支全局的npm,相當於全局安裝了這個npm包。
接着我們在任意項目下執行my-vite就能夠啓動該服務了!
處理根目錄html文件
由於上面服務我們沒有對任何路由進行處理,當訪問http://localhost:3000會發現什麼也沒有,我門首先需要將項目的模版文件index.html返回給瀏覽器
const root = process.cwd(); // 獲取當前工作目錄
console.log('當前工作目錄:', process.cwd());
// 靜態文件服務區
App.use(async (ctx, next) => {
// 處理根路徑,返回index.html
await send(ctx, ctx.path, { root: process.cwd() ,index: 'index.html'});
await next();
});
index.html模版文件如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script>
window.process = { env: { NODE_ENV: 'development' } };
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
就是以ESM的方式加載了vue的入口文件main.ts
加完這段代碼,我們在vue3項目下執行一下my-vite
來到瀏覽器看一下此時的情況:
此時瀏覽器加載了main.ts,該文件如下:它通過import引入了兩個模塊
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
按理來説,瀏覽器此時應該會接着發起請求,去獲取這兩個模塊,但現在卻並沒有🤔
此時控制枱有個錯誤:
意思就是加載模塊,必須以相對路徑才可以(/、./、../)
所以我們現在需要來處理這些模塊的加載路徑問題
處理模塊加載路徑
由於三方模塊都是直接以模塊名來加載的,所以這裏我們需要將這些模塊的引用路徑轉換成相對路徑。
// 處理模塊導入
const importAction = (content) => {
return content.replace(/(from\s+['"])(?!\.\/)/g, '$1/@modules/')
}
// 修改第三方模塊的路徑
App.use(async (ctx, next) => {
// console.log('ctx.path', ctx.type, ctx.path);
// 處理ts或者js文件
if (ctx.path.endsWith('.ts') || ctx.path.endsWith('.js')){
const content = await fileToString(ctx.body); // 獲取文件內容
ctx.type = 'application/javascript'; // 設置響應類型為js
ctx.body = importAction(content); // 處理import加載路徑
}
await next();
});
在這個中間件中,我們使用正則表達式將模塊的引用路徑替換成了/@modules開頭,這樣就符合瀏覽器的引用規則了。
接着再到瀏覽器中來觀察此時的情況:
此時瀏覽器已經可以發出另外兩個請求,分別去加載vue模塊以及App.vue組件了。
可以看到vue模塊的加載路徑已經變成了/@modules開頭了,雖然現在該路徑還是404,但最起碼比起之前我們又往前走一步了。
其實404也很好理解,因為我們的服務現在壓根就還沒處理這類路徑,所以接下來就該處理/@modules這類path並加載模塊內容
加載第三方模塊
這裏我們只需要去攔截剛剛/@modules開頭的路徑,並找到該路徑下的模塊的真正位置,最後返回給瀏覽器就可以了。
// 加載第三方模塊
App.use(async (ctx, next) => {
if (ctx.path.startsWith('/@modules/')) {
const moduleName = ctx.path.substr(10); // 獲取模塊名稱
const modulePath = path.join(root, 'node_modules', moduleName); // 獲取模塊路徑
const package = require(modulePath + '/package.json'); // 獲取模塊的package.json
// console.log('modulePath', modulePath);
ctx.path = path.join('/node_modules', moduleName, package.module); // 重寫路徑
}
await next();
});
我們可以通過讀取package.json文件中的module字段,來找到第三方模塊的入口文件。
該中間件需要在處理模塊加載路徑的中間件之前執行
此時再來到瀏覽器中查看:
可以看到,此時的vue模塊已經能夠重新加載了,但下面又多加載了四個模塊,它們又是從哪來的呢?
可以看到vue模塊中又引入了runtime-dom模塊,並且它們的加載路徑也被轉成了/@modules開頭,這就是上面提到的加載模塊的中間件需要在處理模塊加載路徑的中間件之前執行,模塊加載回來之後又經過了處理加載路徑的中間件,所以就相當於遞歸把模塊的路徑全都轉換成相對路徑了
runtime-dom模塊又引入了runtime-core與shared模塊,而runtime-core模塊又引入了reactivity模塊,所以會看到上圖中這樣的一種加載順序。
模塊的加載引入都正確了,但頁面還是沒又任何渲染內容出現
這是因為此時的App.vue還沒經過任何編譯處理,瀏覽器並不能直接識別並執行該文件
所以接下來的重點是需要將App.vue文件編譯成瀏覽器能夠執行的javascript內容(render函數)
處理Vue單文件組件
這裏我們需要使用Vue的編譯模塊@vue/compiler-sfc與@vue/compiler-dom來對vue文件進行編譯處理。
處理script
const compileScript = importAction(
compilerSfc.compileScript(
descriptor,
{
id: descriptor.filename
}
).content); // 編譯script
處理template
const compileRender =importAction(compilerDom.compile(descriptor.template.content,
// 編譯template, render函數中變量從setup中獲取
{ mode: 'module',
sourceMap: true,
filename: path.basename(ctx.path),
__isScriptSetup: true, // 標記是否是setup
compatConfig: { MODE: 3 }, // 兼容vue3
}).code); // 編譯template
處理style
let styles = '';
if(descriptor.styles.length){
console.log('descriptor.styles', descriptor.styles);
// 處理樣式
styles = descriptor.styles.map((style,index) => {
return `
import '${ctx.path}?type=style&index=${index}';
`
}).join('\n');
} // 處理樣式
這裏是通過讓它另外發起一次請求來對style進行處理,這樣隔離開邏輯能夠更清晰
處理樣式的請求
在中間件中通過攔截type為style的請求來進行處理
if (ctx.query.type === 'style') {
// 處理樣式
const styleBlock = descriptor.styles[ctx.query.index];
console.log('styleBlock', styleBlock);
ctx.type = 'application/javascript';
ctx.body = `
const _style = (css) => {
const __style = document.createElement('style');
__style.type = 'text/css';
__style.innerHTML = css;
document.head.appendChild(__style);
}
_style(${JSON.stringify(styleBlock.content)});
export default _style;
`;
}
最後驗證
總結
在深入探索了vite的工作流程之後,你可能會發現,儘管從概念上看似簡單,但vite背後的實現卻相當複雜且精妙。我們剛剛通過走一遍其核心流程,對vite如何加載模塊、解析和編譯文件有了初步的認識。然而,這僅僅是冰山一角。
總的來説,vite的工作原理雖然可以通過一個簡化的示例來理解,但其真正的強大和複雜性遠不止於此。如果對vite的深入工作原理感興趣,可以去深入閲讀它的源碼,在那裏我們能夠學習到更多知識。