1. 程式人生 > 其它 >Vite原理與外掛實戰

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',
    })],
})