vite只言片语(一)
第一部分,让我们从构建工具的角度,逐步深入了解
Vite
,重点关注它的核心特性——依赖预构建。
理解 vite
Vite
是一个思维比较前卫而且先进的构建工具,它解决了一些Webpack
解决不了的问题,同时降低了一些心智负担。Vite
基于自己得天独厚的优势,他已经占有一席之地,Vite
是 vue 团队的官方出品,背靠强大的生态,vue-cli
已经将Vite
作为预设构建工具。使用vue-cli
去构建 vue 项目的时候你要写的vue.config.js
不再是webpack
的配置而是vite
的配置(目前只基于浏览器项目)。Vite
支持直接构建react
、angular
,和svelte
项目。
而这只是Vite
优势的一角。
什么是构建工具
浏览器只认识 html、css、js。
企业级项目里除了基本的原生 Api,为了提高团队生产力必定会加入其他Library
,比如TypeScript
、Vue
、sass
、babel
、build tool
等等。
在前后端没分离的多页应用时期,只要有任何代码、文件改动,都需要重新发布或者部署到服务器,才能看见最新的页面,开发起来相当麻烦。
而构建工具能够帮我们把tsc
、react-compliler
、sass
、babel
,uglifyjs
集成到一起,这样我们能把更多精力放在业务代码。
构建工具做了什么
- 模块化支持:支持从
node_modules
引入代码 + 多种模块化支持(vite
默认是 ESM 规范) - 代码兼容性与转译:比如
babel
语法降级,less
、ts
语法兼容等等 - 提高项目性能:
- 压缩文件:压缩 JavaScript、CSS 等代码,减小文件大小,提高加载速度。
- 文件指纹:为文件生成唯一的指纹或哈希值,以便于缓存管理和版本控制。
- 代码分割:将代码拆分成更小的块,使得页面加载时只需下载当前视图所需的代码,提高加载速度。
- 优化开发体验:
- 错误检查和修复: 使用工具(如
ESLint
、TSLint
)检查代码中的错误和潜在问题,并在可能的情况下自动修复。 - 服务启动:提供本地开发服务器,支持热更新(
Hot Module Replacement
),使得开发过程更加高效。
- 错误检查和修复: 使用工具(如
如果没有构建工具,我们将要手动执行这些任务,这会导致效率低下、易出错,并且难以保持项目的可维护性。
为什么选 vite?:官方首当其冲的指引
由于Webpack
底层实现和兼容性考虑各方面因素,导致编译和打包时间劣于Vite
。
依赖预加载
官方解释:当你首次启动 vite 时,Vite 在本地加载你的站点之前预构建了项目依赖。默认情况下,它是自动且透明地完成的。
可以理解为,vite 首先会找到对应的依赖,然后调用esbuild
(对 js 语法进行处理的一个库),将其他规范的代码转换成esmodule
规范,然后放到当前目录下的node modules/.vite/deps/
,对esmodule
规范的各个模块统一集成。
官方也给出了相关说明npm-dependency-resolving-and-pre-bundling
为什么需要
- 模块兼容性:不同的第三方包会有不同的导入导出格式(比如非 ESM 规范的 react,在开发环境,需要借助预构建将非 ESM 转成 ESM,从而在浏览器 import 模块时成功识别)
- 路径便捷处理:对路径的处理上可以直接使用 .vite/deps,方便路径重写
- 性能提升:网络多包传输的性能问题(也是原生 ESM 规范不敢支持
node_modules
的原因之一),有了依赖预构建以后无论依赖有多少 export 和 import,vite
都会尽可能的集成为一个或几个模块。
根据上述三点,我们可以得出结论:依赖预构建是为了保障模块的兼容性,也是为了提升性能。
思维导图
预构建究竟是怎么样的过程?我们先来看一幅关于依赖预构建的思维导图
启动开发服务器: 当执行
npm run dev
启动 Vite 的开发服务器时,Vite 默认会检索项目目录下的所有 .html 文件(忽略了node_modules
、build.outDir
、__tests__
和coverage
)以确定需要预构建的依赖项。这一过程在没有明确指定 build.rollupOptions.input 或 optimizeDeps.entries 的情况下自动进行。分析入口文件: Vite 会分析项目的入口 HTML 文件,检测其中的
<script>
标签,以识别引入的JavaScript
或TypeScript
资源。例如,通过分析/src/main.ts
,Vite 能够识别项目的主要入口。分析模块依赖: 进一步分析
/src/main.ts
中的模块依赖,包括第三方依赖和项目内源代码。对于第三方依赖,Vite 记录其入口文件地址,并将其标记为外部模块,不进行深度递归扫描。对于项目内源代码,Vite 递归扫描所有的导入语句,建立模块之间的依赖关系。递归分析非第三方模块: 在分析主模块后,Vite 进一步递归分析非第三方模块中的依赖引用,例如 /src/App.tsx。这个过程重复第三步骤,构建完整的依赖关系图。
生产依赖预构建: 基于建立的依赖关系图,Vite 利用 EsBuild 对扫描出的所有第三方依赖入口文件进行打包。生成的产物存放在
node_modules/.vite/deps
文件夹中,例如,源码中导入的 antd 最终会被构建为一个单独的antd.js
文件。
这个过程的优势在于提高开发服务器的启动速度,同时通过按需预构建依赖,加速了整体的开发体验。
源代码
由于 Vite 的项目结构为
monorepo
结构,这里我们仅仅关心 vite 目录即可。
首先,Vite 目录下的 /pakcages/vite/bin/vite.js 文件是作为项目 cli 入口文件。
实际当运行 vite 命令时会执行该文件,执行该文件会经过以下调用链:
执行 /vite/src/node/cli.ts
文件处理一系列命令行参数。
处理完毕后再次调用 /vite/src/node/server/index.ts
创建开发服务器。
创建服务关键函数便是 createServer 函数,其中不乏一些配置函数比如 resolveConfig
、resolveChokidarOptions
等等。
依赖预构建
在 _createServer 下半部分我们能发现以下内容:
1 | if (!middlewareMode && httpServer) { |
在 initServer
执行之前保证 WebSocket 服务器成功启动和连接,这与 HMR 相关。
Hot Module Replacement,HMR 允许在运行时更新应用程序的一部分而无需进行完全刷新。为了实现 HMR,通常会使用 WebSocket 来在开发服务器和客户端之间建立实时的双向通信通道。这个通道用于传递模块变更的信息,以便在应用程序运行时进行相应的更新。
接下来看核心函数 initServer
1 | // httpServer.listen can be called multiple times |
buildStart
首先是两个 if 是为了防止 buildStart
多次调用产生性能问题,container.buildStart({})
主要负责触发并行执行构建过程中插件的 buildStart
钩子,确保插件在构建开始阶段得到调用。这对于插件执行一些在构建开始前的准备工作非常重要,例如一些初始化或预处理工作。
initDepsOptimizer
initDepsOptimizer 函数下的 createDepsOptimizer 便是 pre-bundled
的主要实现。
初始
1 | async function createDepsOptimizer( |
在初始化阶段,该函数接收两个参数:config 是已解析的 Vite 配置,而 server 是 ViteDevServer 的实例。它还定义了一系列变量,包括日志记录器、是否是 SSR 模式、时间戳等。
加载缓存和设置状态
1 | const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr); |
这一部分主要用于加载缓存的优化元数据,初始化一些状态变量,并设置一些用于处理优化过程的工具函数(比如生成_metadata
文件,创建 depsOptimizer
对象等等)。其中,depsOptimizer
是一个关键的对象,它包含了优化元数据、处理新依赖的函数等。
启动优化器
1 | async function onCrawlEnd() { |
在静态导入结束后,触发 onCrawlEnd
函数。根据是否有优化结果,决定是使用之前的结果进行优化,还是执行新的优化操作。这部分逻辑保证了 Vite
在启动阶段能够正确处理静态导入和优化依赖。
注册缺失的导入
1 | function registerMissingImport(id: string, resolved: string): OptimizedDepInfo { |
registerMissingImport
函数用于注册缺失的导入,它负责将缺失的依赖添加到优化元数据中,以及触发后续的优化操作。这个函数是整个优化过程中一个关键的环节。
优化器运行
1 | // 优化运行成功后,将创建所有当前和发现的依赖项的新捆绑版本 |
runOptimizer
函数是整个优化过程的核心。它包含了处理新依赖的逻辑、触发优化运行、处理优化结果的流程。这里通过比对哈希值等判断是否需要执行全页面刷新,以达到
提交优化
1 | async function commitProcessing() { |
在 commitProcessing
函数中:
- 如果不需要页面刷新 (needsReload 为 false),则会执行 await commitProcessing(),并记录日志或启动 debounced 日志记录。
- 如果需要页面刷新 (needsReload 为 true),也会执行 await commitProcessing(),然后记录日志或启动 debounced 日志记录,并执行 fullReload 函数,发起全页面刷新。
其他功能
此外,还有一些其他功能,如处理延迟启动优化器(delayDepsOptimizerUntil
)、注册新依赖(registerMissingImport
)、触发重新运行(rerun
)等等。这些功能在整个优化过程中起到了辅助作用。
最后
createDepsOptimizer
函数是 Vite
中实现依赖优化的核心。通过对其代码的深入解析,我们理解了其初始化过程、静态导入处理、优化运行和结果处理等关键步骤。这个函数的设计巧妙,保证了 Vite
在开发过程中对依赖的快速响应和高效优化。
The End
本文简单聊了聊构建工具和vite
的核心之一依赖预构建 Dependency Pre-Bundling
,后续我会继续更新 vite 其他进阶内容,比如 HMR、 build过程等等。
$The\,End$