Vite源码解析一

166 阅读5分钟

前言

在学习源码之前,要知道我们是为什么要去学习Vite源码,Vite区别于webpack的特点是什么。

webpack原理是把一切需要处理的东西都处理好之后打包在一个文件里运行,这也是无法冷启动导致首次运行速度慢的原因。而vite最大的特点就是no-bundle(不打包),这样就会实现冷启动,提升本地开发速度。

什么是no-bundle

在一个项目里,需要处理的有两类代码:

  • 第一类是你自己写的业务代码
  • 第二类是node_modules里需要用到的第三方依赖包代码 vite所说的no-bundle仅仅是指第一类业务代码,对于第二类第三方依赖的代码还是采用的bundle(打包) 的方式来引入,为了提升打包的速度使用Esbuild进行秒打包编译。

针对第一类代码,利用浏览器对script标签的type=module的(也就是所说的ES Module规范)实现,每一个import语句都是一个网络请求来实现no-boundle。因为ESM规范是浏览器的通用实现,所以不需要像以前的CommonJS/AMD之类的去制定单独的规范。

关于前端规范角度这篇文章从前端模块化发展史去思考Vite讲得比较详细,可以解释清楚ESM的优点。

生产环境

虽然Vite是no-bundle,开发环境下开发速度很快,但是生产环境下总是要打包的。所以Vite又利用另一个神级打包神器——rollup,很多生产环境下rollup插件是可以和vite兼容的。

总结

所以vite基本是站在巨人肩膀上了,开发环境需要Esbuild,生产环境需要rollup。而Vite自己实现的部分可以看作是一个Dev Server,主要用来处理开发环境下的开发体验。

有了这个认知学习源码会非常清晰地认知到我们到底要学习什么。

准备

在看源码之前,我们先要找到vite的关键函数是什么,也就是vite的运行流程是怎样的。进而明白什么叫作no-bundle

首先,先使用npm create vite@latest创建一个项目。然后下面根据vite默认的配置来分析vite的结构,搞清楚vite运行的过程。

分析

vite初始代码

看一下新建项目package.json:

  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "node server",
    "preview": "vite preview"
  },

这里着重看scripts里的命令,可以看出npm run dev参数的命令是vite,我们去node_modules里的.bin里去找到vite命令。

.bin里的vite命令来自node_modules里的vite文件夹里的bin文件夹里的vite.js

function start() {
  require('../dist/node/cli')
}

总之这一大堆代码,运行到最终就是为了运行start函数,这里其他代码不多说,感兴趣的可以仔细看。

所以此时需要去../dist/node/cli路径找到cli.js文件:

前面一大堆都不看,找到cli:

cli
    .option('-c, --config <file>', `[string] use specified config file`)
    .option('--base <path>', `[string] public base path (default: /)`)
    .option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
    .option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
    .option('-d, --debug [feat]', `[string | boolean] show debug logs`)
    .option('-f, --filter <filter>', `[string] filter debug logs`)
    .option('-m, --mode <mode>', `[string] set env mode`);

这就是package.json的script里,我们使用npm run dev会运行的vite命令,如果我们需要参数去实现功能,参数就是在这里处理的。

然后仔细看这一段代码会发现一些注释在提示:

比如//dev的注释提示我们,接下来的处理是关于执行vite dev的处理。dev的参数有--host/port/https.....等等。

但是主要的处理还是在action函数里:从一个压缩打包的./chunks/dep-59dc6e00.jsJS文件里导出一个createServer函数,然后使用createServer新建一个Node Server,这个就是用来承载更新的服务器。(Ps:chunk一般表示JS文件的打包)

// dev
cli
    .command('[root]', 'start dev server') // default command
    .alias('serve') // the command is called 'serve' in Vite's API
    .alias('dev') // alias to align with the script name
    .option('--host [host]', `[string] specify hostname`)
    .option('--port <port>', `[number] specify port`)
    .option('--https', `[boolean] use TLS + HTTP/2`)
    .option('--open [path]', `[boolean | string] open browser on startup`)
    .option('--cors', `[boolean] enable CORS`)
    .option('--strictPort', `[boolean] exit if specified port is already in use`)
    .option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`)
    .action(async (root, options) => {
    // output structure is preserved even after bundling so require()
    // is ok here
    const { createServer } = await Promise.resolve().then(function () { return require('./chunks/dep-59dc6e00.js'); }).then(function (n) { return n.index$1; });
    try {
        const server = await createServer({
            root,
            base: options.base,
            mode: options.mode,
            configFile: options.config,
            logLevel: options.logLevel,
            clearScreen: options.clearScreen,
            server: cleanOptions(options)
        });
        if (!server.httpServer) {
            throw new Error('HTTP server not available');
        }
        await server.listen();
        const info = server.config.logger.info;
        info(index.colors.cyan(`\n  vite v${require('vite/package.json').version}`) +
            index.colors.green(` dev server running at:\n`), {
            clear: !server.config.logger.hasWarned
        });
        server.printUrls();
        // @ts-ignore
        if (global.__vite_start_time) {
            // @ts-ignore
            const startupDuration = perf_hooks.performance.now() - global.__vite_start_time;
            info(`\n  ${index.colors.cyan(`ready in ${Math.ceil(startupDuration)}ms.`)}\n`);
        }
    }
    catch (e) {
        index.createLogger(options.logLevel).error(index.colors.red(`error when starting dev server:\n${e.stack}`), { error: e });
        process.exit(1);
    }
});

由此可以看出createServer肯定是vite源码实现本地开发着重要实现的功能。

按着这个思路继续看//build下的action,可以看到build函数也是vite工具实现打包功能需要着重实现的函数。

// build
cli
    .command('build [root]', 'build for production')
    .option('--target <target>', `[string] transpile target (default: 'modules')`)
    .option('--outDir <dir>', `[string] output directory (default: dist)`)
    .option('--assetsDir <dir>', `[string] directory under outDir to place assets in (default: assets)`)
    .option('--assetsInlineLimit <number>', `[number] static asset base64 inline threshold in bytes (default: 4096)`)
    .option('--ssr [entry]', `[string] build specified entry for server-side rendering`)
    .option('--sourcemap', `[boolean] output source maps for build (default: false)`)
    .option('--minify [minifier]', `[boolean | "terser" | "esbuild"] enable/disable minification, ` +
    `or specify minifier to use (default: esbuild)`)
    .option('--manifest [name]', `[boolean | string] emit build manifest json`)
    .option('--ssrManifest [name]', `[boolean | string] emit ssr manifest json`)
    .option('--emptyOutDir', `[boolean] force empty outDir when it's outside of root`)
    .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`)
    .action(async (root, options) => {
    const { build } = await Promise.resolve().then(function () { return require('./chunks/dep-59dc6e00.js'); }).then(function (n) { return n.build$1; });
    const buildOptions = cleanOptions(options);
    try {
        await build({
            root,
            base: options.base,
            mode: options.mode,
            configFile: options.config,
            logLevel: options.logLevel,
            clearScreen: options.clearScreen,
            build: buildOptions
        });
    }
    catch (e) {
        index.createLogger(options.logLevel).error(index.colors.red(`error during build:\n${e.stack}`), { error: e });
        process.exit(1);
    }
});

本次源码解析主要是看dev情况下的源码,所以在接下来源码里我们首先从createServer入手。

网络请求

我们运行成功后打开页面去查看网络请求,会发现每一个import语句都对应一个网络请求。

比如main.ts文件,返回给浏览器的响应是经过Vite的Dev Server处理后的代码。createApp来自Esbuild已经打包好的vue源代码,App.vue可以看到后续继续网络请求了。

image.png 这就是Vite的核心:通过网络请求实现按需加载,而不是webpack去打包所有文件。

结束

此时可以去github clone到vite的源码,准备开始真正的源码解析。因为vite采用的是当下比较流行的monorepo的形式,所以vite的源码是放在一个仓库vitejs下的,vitejs下有很多别的项目,可以只clone vite,也可以把整个vitejsclone下来。