首页/文章/vite工具

Vite 的实现原理,确实很巧妙

2024-05-20
364713 分钟
...

vite 是新兴的构建工具,它相比 webpack 最大的特点就是快。

那它是如何做到这么快的呢?

因为 vite 在开发环境并不做打包。

我们创建个 vite 项目:

npx create-vite

安装依赖,然后把服务跑起来:

npm install
npm run dev

浏览器访问下:

本地是 main.tsx 引入了 App.tsx,并且还有 react 和 react-dom/client 的依赖:

用 devtools 看下:

可以看到,main.tsx、App.tsx 还有 react 和 react-dom/client 的依赖都是直接引入的,做了编译,但是并没有打包。

这是基于浏览器的 type 为 module 的 script 实现的:

我们加一个 index2.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="aaa.js"></script>
  </body>
</html>

然后添加 aaa.js

import { add } from './bbb.js'

console.log(add(1, 2));

bbb.js

export function add(a, b) {
    return a + b;
}

起个静态服务访问下:

npx http-server

浏览器访问下 http://localhost:8080/index2.html

可以看到,aaa 和 bbb 模块都被下载并执行了。

当然,我们没有做编译,如果有 ts 或者 jsx 的语法,需要做一次编译。

那我们是不是可以起个服务器,请求的时候根据 url 找到对应的文件,编译之后返回呢?

没错,如果你这样想了,那你也可以写一个 vite。

vite 在开发环境下就是起了一个做编译的服务器,根据请求的 URL 找到对应的模块做编译之后返回。

当你执行 npm run dev 的时候:

vite 会跑一个开发服务:

这个开发服务是基于 connect 实现的,vite 给它加了很多中间件来处理请求:

当你请求 index.html 的时候,它会通过 ast 遍历,找到其中所有的 script:

然后提前对这些文件做编译:

编译是通过不同插件完成的:

插件就是一个对象,它导出了 transform 方法的话,就会在 transform 的时候被调用。

比如图中有 css 插件来编译 css、esbuild 插件来编译 ts/js 等。

每个插件都会判断下,只处理对应的资源:

比如 vite:esbuild 插件,就是对 js/ts 做编译,然后返回编译后的 code 和 sourcemap:

还有个 import-anlysis 插件,在 esbuild 完成编译之后,分析模块依赖,继续处理其它模块的 transform:

这样,浏览器只要访问了 index.html,那么你依赖的所有的 js 模块,就都给你编译了。

这就是 vite 为什么叫 no bundle 方案,它只是基于浏览器的 module import,在请求的时候对模块做下编译。

但不知道大家有没有想过一个问题:

浏览器支持 es module 的 import,那如果 node_modules 下的依赖有用 commonjs 模块规范的代码呢?

是不是就不行了。

这种就需要提前做一些转换,把 commonjs 转成 esm。

还有一个问题,如果每个模块都是请求时编译,那向 lodash-es 这种包,它可是有几百个模块的 import 呢:

这样跑起来,一个 node_modules 下的包就有几百个请求,依赖多了以后,很容易就几千个请求。

这谁受的了?

所以我们要提前处理下,不但要把 node_modules 下代码的 commonjs 提前转成 es module,还有提前对这些包做一次打包,变成一个 es module 模块。

所以,vite 加了一个预构建功能 pre bundle。

在启动完开发服务器的时候,就马上对 node_modules 下的代码做打包,这个也叫 deps optimize,依赖优化。

如何优化呢?

首先,扫描出所有的依赖来:

这一步是用 esbuild 做的:

esbuild.context 和 esbuild.build 差不多的功能。

可以看到,用 esbuild 对入口 index.html 开始做打包,输出格式为 esm,但是 write 为 false,不写入磁盘。

有同学说,esbuild 支持 import html 么?

这里用到了一个 esbuild scan plugin:

vite 实现的,用来记录依赖的:

它会在每种模块路径解析的时候做处理,其中支持了 html 的处理。

这样用 esbuild 处理完一遍,是不是就知道预打包哪些包了?

我们在项目里引入下 dayjs 和 lodash-es 再试下:

依然给你一个不少的给分析了出来:

接下来调用 esbuild 打包就行。

但打包之前呢,还会对路径做扁平化,比如 react-dom/client 变成 react-dom_client

效果就是打包之后文件是平级的。

从每个依赖包作为入口打包,输出 esm 格式的模块到 node_modules/.vite 下。

之后还会生成一个 _metadata.json 文件写入 node_modules/.vite 下:

这样的:

这个 metadata.json 是干啥的呢?

看到这几个 hash 了么

vite 会在这些预打包的模块后加一个 query 字符串带上 hash,然后用 max-age 强缓存:

因为这些依赖一般不会变,不用每次都请求,强缓存就行。

但是在 lock 文件变化或者 config 有一些变化的时候也需要重新 build:

重新预编译,然后在资源请求时带上新的 query,这样就让强缓存失效了。

这里强缓存的用法很典型,面试官们可以记一下作为考点。

这样,vite 的开发服务的请求时编译,再就是预构建就都完成了。

有的同学可能会问,为啥预构建要用 esbuild 呢?

原因就是快:

vite 在 dev 时的核心原理我们理清了,但是在 build 的时候总要打包的吧。

那肯定,在 build 的时候 vite 会用 rollup 做打包。

那不会导致开发时的代码和生产环境不一致么?

不会。

能做到这一点也很巧妙。

看下 build 时的 rollup 插件:

是不是似曾相识?

对比下 dev 时跑的 vite 插件:

没错,vite 插件时兼容 rollup 插件的,这样在开发的时候,在生产环境打包的时候,都可以用同样的插件对代码做 transform 等处理。

处理用的插件都一样,又怎么会开发和生产不一致呢?

这也是 vite 的巧妙之处。

在 dev 的时候,它实现了一个 PluginContainer,用和 rollup 插件同样的参数来调用 vite 插件:

然后 build 的时候,可以把这些插件直接作为 rollup 插件用。

对了,vite 在 dev 的时候还支持热更新,也就是本地改了代码能够自动同步到浏览器。

这个就是基于 chokidar 监听了本地文件变动:

然后在模块变动的时候通过 websocket 通知浏览器端:

浏览器端接受之后做相应处理就好了:

我们改下 Aaa.tsx,可以看到浏览器端收到了 update 的 ws 消息:

收到消息之后,把模块换成这个新的,加上 timestamp 重新请求就好了:

总结

今天我们分析了下 vite 的实现原理。

它是基于浏览器的 type 为 module 的 script 可以直接下载 es module 模块实现的。

做了一个开发服务,根据请求的 url 来对模块做编译,调用 vite 插件来做不同模块的 transform。

但是 node_modules 下的文件有的包是 commonjs 的,并且可能有很多个模块,这时 vite 做了预构建也叫 deps optimize。

它用 esbuild 分析依赖,然后用 esbuild 打包成 esm 的包之后输出到 node_modules/.vite 下,并生成了一个 metadata.json 来记录 hash。

浏览器里用 max-age 强缓存这些预打包的模块,但是带了 hash 的query。这样当重新 build 的时候,可以通过修改 query 来触发更新。

在开发时通过 connect 起了一个服务器,调用 vite 插件来做 transform,并且对 node_modules 下的模块做了预构建,用 esbuild 打包。

在生产环境用 rollup 来打包,因为 vite 插件兼容了 rollup 插件,所以也是用同样的插件来处理,这样能保证开发和生产环境代码一致。

此外,vite 还基于 chokidar 和 websocket 来实现了模块热更新。

这就是 vite 的实现原理。

回想下,不管是基于浏览器 es module import 实现的编译服务,基于 esbuild 做的依赖预构建,基于 hash query 做的强缓存和缓存更新,还是兼容 rollup 的 vite 插件可以在开发服务和 rollup 里同时跑,这些功能实现都挺巧妙的。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
vite工具2026-03-17
深度解析:NODE_ENV 与 Mode (模式)
在现代前端开发中,尤其是使用 Vite 、 Qwik 、 Next.js 等基于 Node.js 的构建工具时,开发者经常会被两个相似的概念绕晕: 和 Mode (模式) 。 它们看起来都在做同一件事:“区分开发环境和生产环境”。但实际上,它们在架构设计中扮演着截然不同却又紧密协作的角色。混淆这两者可能导致构建配置错误、环境变量加载失败,甚至生产环境泄露敏感信息。 一、核心定义:它们到底是什么? 1\. :行业通用的“运行时开关” 起源...
学习
vite工具2025-10-01
vite常用配置
目录 1\. 基础配置 1.1 项目初始化 1.2 基础 vite.config.js 配置 配置说明与最佳实践 插件配置 (plugins) 好处 : 插件系统是Vite的核心特性,提供了丰富的功能扩展能力 坏处 : 过多插件会增加构建时间,插件冲突可能导致构建失败 建议 : 只引入必要的插件,定期检查插件更新和兼容性 适用场景 : 所有Vite项目都需要根据技术栈选择合适的插件 开发服务器配置 (server) host: ‘0.0...
学习
vite工具2025-06-04
Vite 项目中使用 vite-plugin-dts 插件的详细指南
在现代前端开发中,TypeScript 的类型声明文件( )对于提供良好的开发体验和代码提示至关重要。如果你正在使用 Vite 构建一个库项目, 插件是一个非常有用的工具,它可以帮助你自动生成 文件。以下是如何在你的 Vite 项目中安装、配置和使用 插件的详细指南。 一、插件介绍 是一个用于在 Vite 的库模式下,从 或 源文件生成类型文件( )的插件。它能够帮助开发者在构建库时,自动生成类型声明文件,从而提高开发效率。 二、安装插...
学习
vite工具2025-03-11
vite项目打包build后提示说超过500k了
说可以通过配置build.rollupOptions.output.manualChunks来提升一下,好可以,我来配置一下,把那些可能大的包单独分配一下: 然后再看一下,是变小了,但是还是500警告啊: 那就开始分析是哪个包比较大吧,使用rollup plugin visualizer这个插件来分析一下,rollup plugin visualizer是一个开源项目,地址: 安装: 配置插件 在 文件中添加 插件的配置,如下: 构建完...
学习
vite工具2024-10-10
一次低端机 WebView 白屏的兼容之路
问题 项目:Vite4 + Vue3,APP WebView 项目 页面在 OPPO A5 手机上打不开,页面空白。 最开始是客户端在看,然后发现一个警告,大概也因为没看出什么问题,给到 Web 前端。 相关背景 为了方便描述过程的行为,先做一些相关背景的介绍。知道这些背景才能更好的了解问题的复杂。这些在解决问题的过程中始终是干扰因素,在反复调试试错的过程中才梳理总结出来,这里把它们列出来。 使用测试 App,其中有两个入口,一个是本地...
学习
vite工具2024-09-09
推荐工具vite-plugin-fake-server,可线上部署使用mockmock线上部署
最近遇到了个麻烦,后端迟迟给不了接口,测试那边又要求数据不能写死,必须可以提供mock调试。这不最近找到了一个mock工具,能解决传统mock工具无法线上部署的问题。 工具名字叫做 ,兼容vite,从官网示例上来看是同时支持 和 来模拟数据的。使用起来也非常简单,分三步走就行。 1. 安装 2. 引入 这里贴出最简单的配置,一个是制定文件夹,一个是设置在生产环境下模拟数据。 3. 使用 这里我根据个人习惯,在 文件夹下创建一个 和数个模...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录