Vite 扫盲

7,400 阅读9分钟

当我们的浏览器不支持ES Module的时候,我们会通过使用Webpack、Rollup、Parcel等工具来提取,处理、连接我们的源代码,但是当我们的项目变得越来越复杂,模块数量越来越多的时候,我们启动一个Dev Server所需要的时间也会变得越来越长,当我们在编辑代码,保存,即使有HRM功能,可能也要花费几秒钟,才能反映到页面中。这种开发体验是非常不好的,而Vite就是来解决这种开发体验上的问题。总的来说Vite有如下优点:

  • 去掉打包步骤,快速的冷启动
  • 及时的模块热更新,不会随着模块变多而使得热更新变慢
  • 真正的按需编译

Bundle vs Bundleless

要学习了解Vite 首先谈论Bundle和Bundleless,这是两种不同的开发方式,在过去我们一般都会使用Webpack这个打包构建工具来打包构建我们的代码,那为什么过去需要打包,简单来说就是一下几点:

  • HTTP/1.1 各浏览器有并行连接限制
  • 浏览器不支持模块系统(如 CommonJS 包不能直接在浏览器运行)
  • 代码依赖关系与顺序管理

为什么开始尝试不打包

  • 随着项目不断变大,启动和热更新所等待的时间越来越长
  • HTTP/2.0 多路并用
  • 各大浏览器逐一支持 ESM
  • 原生的解决了代码依赖和复用的问题
  • 越来越多的 npm 包拥抱 ESM(尽管很多包的依赖并不是)
Bundle(Webpack)Bundleless(Vite/Snowpack)
启动时间长,完成打包项目短,只启动Server 按需加载
构建时间随项目体积线性增长构建时间复杂度O(1)
加载性能打包后加载对应Bundle请求映射至本地文件
缓存能力缓存利用率一般,受split方式影响缓存利用率近乎完美
文件更新重新打包重新请求单个文件
调试体验通常需要SourceMap进行调试不强依赖SourceMap,可单文件调试
生态非常完善目前先对不成熟,但是发展很快
  • Bundle vs Bundleless Bundle vs Bundleless

  • Bundle Refresh Bundle Refresh

  • Bundleless Refresh Bundleless Refresh

Vite原理

Vite分为开发模式和生产模式

开发模式:,Vite提供了一个开发服务器,然后结合原生的ESM,当代码中出现import的时候,发送一个资源请求,Vite开发服务器拦截请求,根据不同文件类型,在服务端完成模块的改写(比如单文件的解析编译等)和请求处理,实现真正的按需编译,然后返回给浏览器。请求的资源在服务器端按需编译返回,完全跳过了打包这个概念,不需要生成一个大的bundle。服务器随起随用,所以开发环境下的初次启动是非常快的。而且热更新的速度不会随着模块增多而变慢,因为代码改动后,并不会有bundle的过程。

Vite Server 所有逻辑基本都依赖中间件实现。这些中间件,拦截请求之后,完成了如下内容:

  • 处理 ESM 语法,比如将业务代码中的 import 第三方依赖路径转为浏览器可识别的依赖路径;

  • 对 .ts、.vue 等文件进行即时编译;

  • 对 Sass/Less 的需要预编译的模块进行编译;

  • 和浏览器端建立 socket 连接,实现 HMR。

Vite拦截资源请求

生产模式: 利用Rollup来构建源代码。

Vite将需要处理的代码分为了两大类

第三方依赖:这类代码大部分都是纯JavaScript,而且不会怎么经常变化,Vite会通过pre-bundle的方式来处理这部分代码,至于为什么要pre-bundle,这个后面会讲。Vite2使用esbulid来构建这部分代码,esbuild是基于go的,处理速度会比用JavaScript写的打包器要快10-100倍,这也是Vite为什么在开发阶段很快的一个原因。

业务代码:通常这部分代码,都不是纯的JavaScript(例如:JSX,Vue等),经常会被修改,而且也不需要一次性全部加载(可以根据路由,做代码分割加载)

由于Vite使用了原生的ESM,Vite本身只需要按需编译代码,启动静态服务器就可以。只有当浏览器请求这些模块,这些模块才会被编译,动态加载到当前页面中。

特征

  • NPM Dependency Resolving

原生的ESM不支持 bare module imports,换句话说原生的ESM只支持通过一个相对或者绝对路径来引用资源,不支持通过一个包名来引用资源

import React from 'react'

Uncaught TypeError: Failed to resolve module specifier "test2.js". Relative references must start with either "/", "./", or "../".

我们之前在使用Webpack的项目进行构建的时候,我们都会使用这种引入方式去引入一个我们安装的第三方依赖的包,背后其实也是Webpack帮我们做了这样的一层转换,同样如果想在ESM中这样去引入一个包,那么也得做这样一层路径转换。

  • Dependency pre-bundling

在开发阶段,Vite将所有的代码作为native ESM来处理,因此Vite必须将CommonJS或者UMD的依赖转换为native ESM 减少模块(减少请求数量)。Vite将包含许多内部模块的ESM依赖转换为单个模块,以提高后续页面加载性能。 预打包只有在依赖变动时才需要执行,但在有大量依赖的项目中,每次执行还是可能会需要很长时间。Vite之前是使用 Rollup 来执行这个过程,在 2.0 中我们切换到了 esbuild,使这个过程加快了几十倍 同时Vite帮我们把这些第三方依赖的包都设置了http缓存,这样当我们在加载相同资源的时候可以直接读取缓存

  • Hot Module Replacement
  • TypeScript
  • Vue
  • JSX
  • CSS
  • 导入.css文件将通过具有HMR支持的标记将其内容注入页面
  • 强化路径解析:CSS 中的 @import 和 url() 路径都通过 Vite的路径解析器来解析,从而支持 alias 和 npm 依赖。 自动URL改写:所有 url() 路径都会被自动改写从而确保在开发和构建中都指向正确的文件路径。
  • CSS代码分割:当异步加载一个模块时,Vite会自动将模块中的css提取成一个单独的文件,当这个模块被加载时,会通过style标签加载进来。
  • css moudle支持
  • CSS Pre-processors
  • Static Assets
  • JSON
  • Web Assembly
  • Web Workers
  • Dynamic Import Polyfill
  • Async Chunk Loading Optimization

在未经优化时,当我们去加载一个异步模块时,只有请求并解析当前模块之后,才会发现该模块还import了其他模块,然后再去发送网络请求,这个需要额外的网络往返,但是Vite可以解析代码,知道当前异步模块会依赖哪些直接依赖的模块,从而可以并行的发送网络请求。

优化前

Entry ---> A ---> C

优化后

Entry ---> (A + C)

Caching

文件系统缓存 Vite会将pre-bundle的依赖存放在node_modules/.vite下。只有当以下情况发生时,才会重新pre-bundle来更新缓存。

  • The dependencies list in your package.json
  • Package manager lockfiles, e.g. package-lock.json, yarn.lock, or pnpm-lock.yaml.
  • Relevant fields in your vite.config.js, if present.

浏览器缓存 Vite Dev Server 会将这些第三方依赖设置HTTP强缓存来提升性能,只有当这些依赖发生变化时,才会去更新query id使之前的的缓存失效。

Automatic Dependency Discovery

如果当依赖在缓存中没有找到的话,Vite会自动到node_modules中去找,并且把它提取到缓存中。Vite会将pre-bundle的依赖存放在node_modules/.vite下

为什么生产环境还是需要bundle

尽管现在已广泛支持native ESM,但由于嵌套import 会导致发送大量的网络请求,即使使用HTTP2,在生产中使用未捆绑的ESM仍然效率低下。 为了在生产中获得最佳的加载性能,最好将代码bundle一遍(结合Tree Shaking,lazy-loading和common chunk splitting等技术手段)

为什么不在生产环境使用ESbuild

esbuild的速度非常快,已经是一个非常强大的Bundler,但一些应用程序绑定所需的重要特性仍在开发中——特别是代码拆分和CSS处理。目前,Rollup在这些方面更加成熟和灵活。也就是说,当esbuild在将来稳定这些特性时,我们不排除在生产版本中使用esbuild的可能性

与Snowpack的比较

Snowpack 也是一个基于native ESM的本地开发服务器,有一些相似的地方,但是也有一些不同。

Production Build

Snowpack 默认build之后的文件是没有捆绑的,它将每个文件转换为单独的构建模块,然后将这些模块提供给不同的'optimizers'来执行实际的捆绑操作。这样做的好处是,你可以在不同的end-bundlers(eg: webpack,rollup,esbuild),以满足特定的需求。

由于Vite集成了构建步骤,所以Vite支持更多的特性(这些特性在Snowpack中还不支持)

Faster Dependency Pre-Bundling 由于在开发阶段,Vite采用了esbuild,来进行pre-bundling,相比Rollup会快很多

Monorepo Support CSS Pre-Processor Support First Class Vue Support

浏览器支持

  • 在开发环境,需要原生支持ESM的浏览器
  • 在生产环境,默认也是需要支持原生ESM的浏览器,但是可以通过@vitejs/plugin-legacy来构建支持低版本浏览器的包

在Webpack 构建的项目中,我们的入口文件通常是一个js, index.html仅仅作为一个模板,而在Vite构建的项目中,index.html作为入口文件。

参考文章

深入 vite 原理:尤大最新力作到底是如何实现的?

Vite 依赖预编译,缩短数倍的冷启动时间

聊聊 ESM、Bundle 、Bundleless 、Vite 、Snowpack

从 Bundleless 看前端构建

Demo地址

Vite-Study