Vite原理实现

723 阅读3分钟

image.png

Vite简介

引用原作者的话:Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,

完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。

针对生产环境则可以把同一份代码用 rollup 打包。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,

做得好可以彻底解决改一行代码等半天热更新的问题。

正如尤大说的在开发环境下,跳过了打包,直接通过ESM方式加载文件。

基于webpack 开发环境加载逻辑

image.png 这样的工程可能会遇到这样的问题

image.png

vite基于ESM 加载逻辑

image.png

10个相同文件 vue-cli VS vite

image.png 很明显,vite在开发启动时间远远领先与vue-cli。

那么vite做了什么呢?

ESModule

ES 2015在语言层面上实现了模块功能,且实现简单,可以替代CommonJS和AMD规范,成为在服务器和浏览器通用的解决方案。

<script type="module"> // 标识为ESM
	import {test} from './test.js'
 </script>

浏览器会自动发起请求,请求test.js文件。

但ESM不支持“裸”模块引入,会直接报错。例如

import {defineComponent} from 'san'

Vite会检测到当前服务中所以的.js文件,并重写他们的路径例如/@modules/san

Vite使用了es-module-lexer 用来解析import语法,通过替换的方式将 'san'替换为'/@modules/san'。

Vite1.0浅析(暂无热更新,编译模块)

启动KOA服务

koa-server

const Koa = require('koa');
const {staticPlugin} = require('./plugins/serverPluginStatic');
const {moduleRewritePlugin} = require('./plugins/serverPluginModuleRewrite');
const {moduleResolvePlugin} = require('./plugins/serverPluginModuleResolve');
const {htmlRewritePlugin} = require('./plugins/serverPluginHtml');
const {vueComplitePlugin} =  require('./plugins/serverPluginCompliteVue');
function createServer() {
    const app = new Koa(); // 创建一个koa实例
    const root = process.cwd();
    const context = {
        app,
        root // 当前根位置
    };
    // 插件集合 依此进行安装
    const resolvePlugins = [
        htmlRewritePlugin,
        // 4) 解析import 重写路径
        moduleRewritePlugin,
        // 3) 解析以/@modules文件
        moduleResolvePlugin,
        // 2) vue文件编译
        vueComplitePlugin,
        // 1) 实现静态服务的功能。 最后
        staticPlugin
    ];
    resolvePlugins.forEach(plugin => plugin(context));
    return app; // 返回app 中有listen方法
}
module.exports = createServer;

Vite 通过 Koa 启动了一个 http 服务,并且加载了一些插件。通过添加插件来对不同类型的文件做不同的逻辑处理。 模块的解析机制的相关插件是 serverRewritePlugin 和 serverResolvePlugin,静态文件处理用了koa-static

模块路径重写

需要先解析import 语法,用来替换引入的包路径,上面我们也提到,Vite使用的是es-module-lexer用来解析import语法

中间件serverPluginModuleRewrite

moduleRewritePlugin

const {readBody} = require('./utils');
const {parse} = require('es-module-lexer'); // 解析 import 语法
const MagicString = require('magic-string'); // 字符串具有不变性
function rewriteImports(source) {
    // let imports = parse(source); // [[],[],boolean]
    let imports = parse(source)[0];
    let magicString = new MagicString(source); // overwrite();
	// n 匹配到的 from 'vue', s=>start e=>end ss=>整条语句的开始 se=>整条语句结束, d => dynamic >-1 是否是动态引入
    // [ 
    //     [
    //         { n: 'vue', s: 27, e: 30, ss: 0, se: 31, d: -1 },
    //         { n: './App.vue', s: 49, e: 58, ss: 32, se: 59, d: -1 },
    //         { n: './index.css', s: 68, e: 79, ss: 60, se: 80, d: -1 }
    //     ],
    //     []
    // ]
    if (imports.length) {
        imports.forEach(i => {
            let {s: start, e: end} = i;
            let id = source.substring(start, end);
            // 当前开头是 / 或者 . 的不需要重写
            if (/^[^/.]/.test(id)) {
                id = `/@modules/${id}`;
                magicString.overwrite(start, end, id);
            }
        });
    }
    return magicString.toString();
}
function moduleRewritePlugin({app, root}) {
    app.use(async (ctx, next) => {
        await next(); // 静态服务
        if (ctx.body && ctx.response.is('js')) {
            let content = await readBody(ctx.body);
            // 重写内容 将重写后的结果返回回去
            const result = rewriteImports(content);
            ctx.body = result;
        }
    });
}
exports.moduleRewritePlugin = moduleRewritePlugin;

模块路径引入

处理/@modules/xxx/路径,返回正确的

moduleResolvePlugin

const moduleReg = /^/@modules//;
const fs = require('fs').promises;
const path = require('path');
function resolveVue(root) {
    // vue3 有几部分组成
	// 运行时相关代码 runtime-dom runtime-core
	// compiler compiler-sfc(单文件编译部分) reactivity(:Vue3 响应式部分) shared(Vue3 工具库)
    const resolvePath = (name) => path.resolve(root, 'node_modules', `@vue/${name}/dist/${name}.esm-bundler.js`);
    const runtimeDomPath = resolvePath('runtime-dom');
    const runtimeCorePath = resolvePath('runtime-core');
    const reactivityPath = resolvePath('reactivity');
    const sharedPath = resolvePath('shared');
    return {
        '@vue/runtime-dom': runtimeDomPath,
        '@vue/runtime-core': runtimeCorePath,
        '@vue/reactivity': reactivityPath,
        '@vue/shared': sharedPath,
        'vue': runtimeDomPath
    };
}
function moduleResolvePlugin({app, root}) {
    const vueResolved = resolveVue(root);
    app.use(async (ctx, next) => {
        if (!moduleReg.test(ctx.path)) { // 处理当前请求的路径是否以 /@modules/开头的
            return next();
        }
        // 将/@modules/替换掉 指向到当前项目中
        const id = ctx.path.replace(moduleReg, '');
        ctx.type = 'js'; // 设置响应js类型
        // 去当前项目下查找 vue对应的真实文件
        const content = await fs.readFile(vueResolved[id], 'utf8');
        ctx.body = content;
    });
}
exports.moduleResolvePlugin = moduleResolvePlugin;

vue文件处理

vueCompilePlugin

const path = require('path');
const fs = require('fs').promises;
function vueCompilePlugin ({root, app}) {
    app.use(async (ctx, next) => {
        if (!ctx.path.endsWith('.vue')) {
            return next();
        }
        const filePath = path.join(root, ctx.path);
        const content = await fs.readFile(filePath, 'utf8');
        // 引入.vue文件解析模板
        const {compileTemplate, parse} = require(path.resolve(root, 'node_modules', '@vue/compiler-sfc/dist/compiler-sfc.cjs'));
        let {descriptor} = parse(content);
        if (!ctx.query.type) {
            // App.vue
            let code = '';
            if (descriptor.script) {
                let content = descriptor.script.content;
                code += content.replace(/((?:^|\n|;)\s*)export default/, '$1const __script=');
            }
            if (descriptor.template) {
                const requestPath = ctx.path + `?type=template`;
                code += `\nimport { render as __render } from "${requestPath}"`;
                code += `\n__script.render = __render`;
            }
            code += `\nexport default __script`;
            ctx.type = 'js';
            ctx.body = code;
        }
        if (ctx.query.type === 'template') {
            ctx.type = 'js';
            let content = descriptor.template.content;
            const {code} = compileTemplate({source: content}); // 将app.vue中的模板 转换成render函数
            ctx.body = code;
        }
    })
};
exports.vueCompilePlugin = vueCompilePlugin;

image.png 参考资料:

Vite中文文档

Vite2.0升级内容

koa2-中间件原理

npm init 解读

es-module-lexer

Vite Github

Koa

ESM 工作原理

ESM 兼容性