Vite原理與外掛實戰
Vite 是什麼?
Vite 是一種新型前端構建工具,能夠顯著提升前端開發體驗。
它主要由兩部分組成:
- 一個開發伺服器,它基於原生 ES 模組提供了豐富的內建功能。
- 一套構建指令,它使用 Rollup 打包你的程式碼,並且它是預配置的,可輸出用於生產環境的高度優化過的靜態資源。
Vite 的特點
-
在開發過程中,vite 是一個開發伺服器,根據瀏覽器的請求編譯原始檔。
無需捆綁,編譯後真正做到按需使用。
未修改的檔案會返回 304,所以瀏覽器根本就不會請求。
這就是它啟動快、保持快的原因。 -
Vite 支援熱模組替換,這和 "簡單的過載頁面 "有本質的區別。
Vue 元件和 CSS HMR 是開箱即用的支援,第三方框架可以利用 HMR API。 -
Vite 通過
esbuild
支援.(t|j)sx?
檔案,開箱即用,速度快得驚人。 -
Vite 支援
.css
,.less
,.sass
等
Vite 在開發中如何做到按需載入?
Vite 有一個開發伺服器,它根據瀏覽器的請求編譯原始檔,不會載入無關的檔案。
來看 Vite 是如何載入執行的一段簡單的 Vue3 程式碼
App.vue
<template> <div> <h1>{{ count }} * 2 = {{ double }}</h1> <button @click="add">click</button> </div> </template> <script> import { ref, computed } from 'vue' export default { setup() { const count = ref(1) function add() { count.value++ } const double = computed(() => count.value * 2) return { count, double, add } }, } </script> <style>h1 { color: red }</style>
在瀏覽器中開啟 localhost:3000/,會返回包含處理過的 index.html 中的內容
原始碼:
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Vite 處理過後返回的程式碼:
後臺中處理 HTML 中的程式碼
讀取 HTML 檔案,在字串中插入 script 標籤,定義 process 變數
if (url == '/') {
let content = fs.readFileSync('./index.html', 'utf-8')
content = content.replace(
'<script',
`
<script>
window.process = {env:{NODE_ENV:'DEV'}}
</script>
<script`,
)
ctx.type = 'text/html'
ctx.body = content
}
html 中的 script 標籤會向後臺請求/src/main.js 檔案
原 main.js 程式碼:
import { createApp } from 'vue'import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
經過 Vite 處理後,main.js 程式碼:
import { createApp } from '/@modules/vue'import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
import { createApp } from 'vue'
被重新成 import { createApp } from '/@modules/vue'
了
接著瀏覽器向後臺請求/@modules/vue,./App.vue, ./index.css。 後臺收到/@modules/vue這樣的請求,會去讀取 node_modules/vue/package.json 的 module 欄位,
拿到 "dist/vue.runtime.esm-bundler.js",接著去請求這個檔案
if (url.startsWith('/@modules/')) {
const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
const module = require(prefix + '/package.json').module
const p = path.resolve(prefix, module)
const ret = fs.readFileSync(p, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(ret)
}
main.js 裡面還 import 了 App.vue,瀏覽器會向後臺請求/src/App.vue
App.vue 原始碼:
<template>
<div>
<h1>{{ count }} * 2 = {{ double }}</h1>
<button @click="add">click</button>
</div>
</template>
<script>
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(1)
function add() { count.value++ }
const double = computed(() => count.value * 2)
return { count, double, add }
},
}
</script>
<style>h1 { color: red }</style>
後臺處理的程式碼:
if (url.indexOf('.vue') > -1) {
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
const { descriptor } = compilerSfc.parse(fs.readFileSync(p, 'utf-8'))
// ?type=template
if (!query.type) {
ctx.type = 'application/javascript'
ctx.body = `
${rewriteImport(descriptor.script.content).replace(
'export default',
'const __script =',
)}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script
`
} else if (query.type == 'template') {
const template = descriptor.template
const render = compileDom.compile(template.content, { mode: 'module' }).code
ctx.type = 'application/javascript'
ctx.body = rewriteImport(render)
}
}
Vite 處理過的 App.vue:
import { ref, computed } from '/@modules/vue'
const __script = {
setup() {
const count = ref(1)
function add() {
count.value++
}
const double = computed(() => count.value * 2)
return { count, double, add }
},
}
import { render as __render } from '/src/App.vue?type=template'
__script.render = __render
export default __script
這裡主要是請求 template 裡面的內容,傳送一個/src/App.vue?type=template 請求
/src/App.vue?type=template 請求返回的內容:
可以看到 compileDom.compile 函式把 App.vue 的 template 編譯成一個 render 函數了
export function render(_ctx, _cache) {
return (
_openBlock(),
_createElementBlock('div', null, [
_createElementVNode(
'h1',
null,
_toDisplayString(_ctx.count) + ' * 2 = ' + _toDisplayString(_ctx.double),
1 /* TEXT */,
),
_createElementVNode('button', { onClick: _ctx.add }, 'click', 8 /* PROPS */, [
'onClick',
]),
])
)
}
後臺處理style的程式碼:
if (url.endsWith('.css')) {
const p = path.resolve(__dirname, url.slice(1))
const file = fs.readFileSync(p, 'utf-8')
const content = `
const css = '${file.replace(/\n/g, '')}'
let link = document.createElement('style')
link.setAttribute('type','text/css')
document.head.appendChild(link)
link.innerHTML = css
export default css
`
ctx.type = 'application/javascript'
ctx.body = content
}
./index.css請求返回的內容:
const css = 'h1 { color: red;}'
let link = document.createElement('style')
link.setAttribute('type','text/css')
document.head.appendChild(link)
link.innerHTML = css
export default css
因為請求/@modules/vue返回的內容中import了/@modules/@vue/runtime-dom,所以瀏覽器會向後臺請求/@modules/@vue/runtime-dom,
直到把所有依賴請求載入完畢,然後頁面才會渲染。
外掛程式碼:
export function filemanager(userOptions: UserOptions = {
source: './dist',
destination: './dist.zip',
}): PluginOption {
const { source, destination } = userOptions;
return {
name: 'vite-plugin-file-manager',
apply: 'build',
closeBundle() {
const output = fs.createWriteStream(destination as string)
const archive = archiver('zip')
output.on('close', function () {
console.log('archiver done')
})
archive.on('error', function (err) {
throw err
})
archive.pipe(output)
archive.glob('**/*', {
cwd: source,
})
archive.finalize()
}
}
}
vite.config.ts中的配置:
import { defineConfig } from 'vite'
import { filemanager } from 'vite-plugin-filemanager'
export default defineConfig({
plugins: [filemanager({
source: './dist/',
destination: './dist.zip',
})],
})