跳到主要内容

认识

2023年11月20日
柏拉文
越努力,越幸运

一、认识


Vite 是一个前端开发与构建工具。采用双引擎架构, 开发阶段使用 Esbuild + no-bundle 服务,生产环境用 Rollup 编译构建。

Vite在开发阶段, Vite 项目的启动可以分为两步。第一步是依赖预构建,借助 Esbuild 超快的编译速度来做第三方库构建和 TS/JSX 语法编译, 第二步是 Dev Server 的启动, 基于浏览器原生 ESModule 的支持实现了 no-bundle 服务,实现开发阶段的 Dev Server, 进行模块的按需加载, 可以直接在浏览器中运行源码,无需事先打包。每一个文件请求进来都会经历一系列的编译流程,然后 Vite 会将编译结果响应给浏览器。

Vite 生产环境借助 Rollup, 从 AST 解析的功能开始,完成代码的词法分析(tokenize)和语义分析(parse),实现模块依赖图和作用域链的搭建,并完成 Tree Shaking、循环依赖检测及 Bundle 代码生成

Preview

二、工作


  1. 搭建开发环境: 安装必要的依赖,并搭建项目的构建脚本,同时完成 cli 工具的初始化代码

  2. 依赖预构建: 通过 Esbuild 实现依赖扫描和依赖构建的功能

    1. 确定预构建入口

    2. 从入口开始扫描出用到的依赖

    3. 对依赖进行预构建

  3. 搭建 Vite 的插件机制: 也就是开发 PluginContainerPluginContext 两个主要的对象

  4. 实现 no-bundle 服务的编译构建能力: 包括入口 HTML 处理、 TS/TSX/JS/TSX 编译、CSS 编译和静态资源处理

  5. 实现 HMR 热更新: 从搭建模块依赖图开始,逐步实现 HMR 服务端和客户端的开发

三、问题


3.1 Vite 是如何使用 EsBuild 的呢?

ViteEsbuild 作为自己的性能利器,将 Esbuild 各个垂直方向的能力(BundlerTransformerMinifier)利用的淋漓尽致,给 Vite 的高性能提供了有利的保证。

  1. 作为 Bundler: 首先是开发阶段的依赖预构建阶段, 为了解决 ESM 格式的兼容性问题和海量请求的问题, 使用 EsBuild 对于第三方依赖,需要在应用启动前进行打包并且转换为 ESM 格式。

  2. 作为 Transformer: 在 TS(X)/JS(X) 单文件编译上面,Vite 也使用 Esbuild 进行语法转译,也就是将 Esbuild 作为 Transformer 来用。也就是说,Esbuild 转译 TS 或者 JSX 的能力通过 Vite 插件提供,这个 Vite 插件在开发环境和生产环境都会执行, 当 Vite 使用 Esbuild 做单文件编译之后,提升可以说相当大了

  3. 作为 Minifier: 那为什么 Vite 要将 Esbuild 作为生产环境下默认的压缩工具呢?因为压缩效率实在太高了!传统的方式都是使用 Terser 这种 JS 开发的压缩器来实现,在 Webpack 或者 Rollup 中作为一个 Plugin 来完成代码打包后的压缩混淆的工作。但 Terser 其实很慢,主要有 2 个原因: 首先压缩这项工作涉及大量 AST 操作,并且在传统的构建流程中,AST 在各个工具之间无法共享,比如 Terser 就无法与 Babel 共享同一个 AST,造成了很多重复解析的过程。然后 JS 本身属于解释性 + JIT(即时编译) 的语言,对于压缩这种 CPU 密集型的工作,其性能远远比不上 Golang 这种原生语言。因此,Esbuild 这种从头到尾共享 AST 以及原生语言编写的 Minifier 在性能上能够甩开传统工具的好几十倍。

3.2 Vite 是如何使用 Rollup 的呢?

ViteVite 用作生产环境打包的核心工具, Vite 默认选择在生产环境中利用 Rollup 打包,并基于 Rollup 本身成熟的打包能力进行扩展和优化,主要包含 3 个方面:

  1. CSS 代码分割: 如果某个异步模块中引入了一些 CSS 代码,Vite 就会自动将这些 CSS 抽取出来生成单独的文件,提高线上产物的缓存复用率。

  2. 自动预加载: Vite 会自动为入口 chunk 的依赖自动生成预加载标签 <link rel="modulepreload"> ,如:

    <head>
    <!-- 省略其它内容 -->
    <!-- 入口 chunk -->
    <script type="module" crossorigin src="/assets/index.250e0340.js"></script>
    <!-- 自动预加载入口 chunk 所依赖的 chunk-->
    <link rel="modulepreload" href="/assets/vendor.293dca09.js">
    </head>

    这种适当预加载的做法会让浏览器提前下载好资源,优化页面性能。

  3. 异步 Chunk 加载优化: 在异步引入的 Chunk 中,通常会有一些公用的模块,如现有两个异步引入的 Chunk: AB,而且两者有一个公共依赖 C,如下图:

    Preview

    一般情况下,Rollup 打包之后,会先请求 A,然后浏览器在加载 A 的过程中才决定请求和加载 C,但 Vite 进行优化之后,请求 A 的同时会自动预加载 C,通过优化 Rollup 产物依赖加载方式节省了不必要的网络开销。

3.3 Vite 开发环境与生产环境的 Plugin 是如何兼容呢?

无论是开发阶段还是生产环境,Vite 都根植于 Rollup 的插件机制和生态,如下面的架构图所示:

Preview

在开发阶段,Vite 借鉴了 WMR 的思路,自己实现了一个 Plugin Container,用来模拟 Rollup 调度各个 Vite 插件的执行逻辑,而 Vite 的插件写法完全兼容 Rollup,因此在生产环境中将所有的 Vite 插件传入 Rollup 也没有问题。

反过来说,Rollup 插件却不一定能完全兼容 Vite(这部分我们会在插件开发小节展开来说)。不过,目前仍然有不少 Rollup 插件可以直接复用到 Vite

Vite 的做法是从头到尾根植于的 Rollup 的生态,设计了和 Rollup 非常吻合的插件机制

参考资料