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原代码




在浏览器中打开 localhost:3000/,会返回包含处理过的 index.html 中的内容

index原代码:



    
        

Vite 处理过后返回的代码:



    
        
                
    

后台中处理 HTML 中的代码

读取 HTML 文件,在字符串中插入 script 标签,定义 process 变量

if (url == '/') {
    let content = fs.readFileSync('./index.html', 'utf-8')
    content = content.replace(
        '
            window.process = {env:{NODE_ENV:'DEV'}}
        
        

原 main.js 代码:

html 中的 script 标签会向后台请求/src/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'

去请求node_modules中的文件

接着浏览器向后台请求/@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)
}

App.vue 原代码:

main.js 里面还 import 了 App.vue,浏览器会向后台请求/src/App.vue





后台处理.vue的代码:

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 请求

经过后台处理过的App.vue的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',
    })],
})