Skip to content

6.3 Vite 功能介绍

Vite 的特点是使用原生 ESM 来实现模块化构建,这样核心的模块化逻辑交给浏览器实现,提升了打包速度。但同时原生 ESM 也有不足之处,Vite 则在此基础上提供了许多增强功能,使打包构建的整体能力更加完善。

裸模块解析

原生 ES 导入不支持下面这样的裸模块导入:

js
import { someMethod } from "app";

裸模块(bare module)是指没有指定相对或者绝对路径的模块。如上面代码中的 “app”,浏览器不认识这是什么。可能你想导入的模块是当前目录下的 app.js,那么必须这样写:

js
import { someMethod } from "./app.js";

但我们在 Node.js 或者打包工具(Webpack)中经常使用没有任何路径的裸模块,这是因为它们有自己的查找模块路径的方法。为此 Vite 也实现了对裸模块的使用,其转换逻辑如下:

  1. 预构建:将 CommonJS/UMD 转换为 ESM 格式。
  2. 修改裸模块路径,比如将路径指向 node_modules 文件夹下。

为什么要使用预构建呢?因为我们安装的第三方模块并不都是 ESM,有可能是 CommonJS 或者 UMD,这类模块 Vite 无法解析,因此需要先将其转换为 ESM。

既然裸模块 ESM 不认识,那么 Vite 便按照一定的规则修改裸模块路径。假设导入模块 dayjs,修改后的模块路径如下:

js
// 源码中使用时
import dayjs from "dayjs";
// 浏览器解析时
import dayjs from "/node_modules/dayjs/dayjs.min.js";

依赖预构建

在解析模块时,如果模块是非原生 ES 模块,则要使用预构建将其转换为 ESM,然后才能交给浏览器运行。那么预构建是如果实现的呢?

Vite 使用 esbuild 来执行预构建,这使我们在开发中几乎感受不到打包过程,因为它的速度非常快。Vite 中的预构建与 Webpack 中的编译概念可以类比,区别是 Vite 只需要将模块构建成 ESM,构建内容比 Webpack 更少且速度更快。

注意:在生产环境下不会使用依赖预构建,而是借助 rollup 的 rollup/plugin-commonjs 插件来打包代码。

为什么在生产环境不使用 esbuild?这一点尤雨溪在直播中介绍过,主要是因为当前 esbuild 对打包功能支持不够完善,如生成哈希值、处理资源文件、分析包文件等无法用 esbuild 实现。如果在未来 esbuild 的打包功能更完善稳定,Vite 则会考虑在生产环境使用 esbuild。

预构建在性能和提速方面几乎做到了极致。除了构建工具本身速度快,Vite 还做了许多优化方案来提高构建效率,代表性的有自动缓存和依赖优化。

  1. 自动缓存

预构建会将依赖自动缓存以避免重复构建,提升加速度。当你导入一个模块时,预构建会自动分析模块的依赖关系,构建后并将其缓存起来,下一次使用模块时便可绕过构建直接从缓存读取。如果在开发过程中导入新的依赖(重新导入模块或模块更新),预构建会重新执行并缓存。

预构建缓存有浏览器和文件系统两层缓存,以此来最大程度地提升构建效率和页面重载性能,它们的区别如下:

  • 文件系统缓存:将依赖缓存到 node_modules/.vite 文件夹下。
  • 浏览器缓存:以 HTTP 头 “max-age=31536000,immutable” 强缓存。

有时候我们需要强制清除缓存来重新构建代码,那么这两层缓存都需要清除。首先在浏览器调试工具的 Network 选项卡中禁用缓存,然后启动开发服务器时使用 --force 命令选项,此时进入页面所有依赖会被重新构建。

  1. 依赖优化

默认情况下,Vite 会从 index.html 开始抓取项目的依赖项并执行预构建,这个过程是根据模块间的导入关系自动实现的。Vite 只会从 node_modules 目录下抓取依赖,有时候我们可能需要修改依赖路径,比如将 src 目录下的某个模块添加为依赖,或者排除某个 node_modules 目录下的 ESM 模块,这时就要用到依赖优化。

依赖优化通过 optimizeDeps 配置项来实现,具体用法我们会在后面的配置部分展开介绍。

模块热替换

模块热替换(HRM)在 Webpack 中大家都体验过。当在开发模式下修改代码后,构建工具会自动检测到文件变化,并将变化的部分重新构建并在页面中更新,而无需刷新浏览器,这种方式大大提高了前端的开发效率。

Vite 提供了一套使用原生 ESM 实现的 “HMR API”,主流框架可以使用该 API 来实现更快更精准的模块热替换。Vite 提供了官方插件分别实现了 Vue、React、Preact 等常用框架的模块热替换,我们可以拿来就用。

框架与实现模块热替换插件对应关系如下:

  1. Vue:@vitejs/plugin-vue。
  2. React:@vitejs/plugin-react。
  3. Preact:@prefresh/vite。

使用脚手架创建项目时默认会启动模块热替换功能,并不需要手动处理。但我们应该知道如何在一个全新的 Vite 项目中集成模块热替换。比如在 Vue 项目中,实现模块热替换只需要在 vite.config.ts 中添加入下代码:

js
import vue from "@vitejs/plugin-vue";
export default defineConfig({
  plugins: [vue()],
});

TypeScript

Vite 天然支持 TypeScript,它支持直接导入 .ts 文件,不需要其他任意插件的协助。我们谈到的支持 TypeScript 主要是对以下两部分的支持:

  • 类型转译:将 TypeScript 代码转译为 JavaScript 代码。
  • 类型检查:检查 TypeScript 代码中是否有类型错误。

TypeScript 本身提供了一个 tsc 命令来实现这两部分的功能。不过 Vite 没有借助该命令,而是使用 esbuild 来实现类型转译,速度比 tsc 快了 20~30 倍,这使得 HMR 的更新速度小于 50ms(官方数据)。

Vite 专注于构建,仅执行 .ts 文件的转译工作,却并不执行任何类型检查,通常类型检查的工作还是交给编辑器执行(VSCode 中的 TypeScript 插件)。因此 Vite 为 TypeScript 提供了更快的类型转译,而类型检查却要自己实现。

在 Vite 中使用 TypeScript 还有以下两个注意事项。

  1. 编译器选项配置。

在 Vite 中使用 TypeScript,需要注意在 tsconfig.json 配置文件中的 compilerOptions 选项下,有几个需要注意的配置选项。

(1)isolatedModules。

TypeScript 将没有 import/export 的文件视为旧脚本文件。这样的文件不是 ESM,它们的任何定义都会合并到全局名称空间中。请设置该选项的值为 true,以保证 esbuild 在转译 TypeScript 时,转译对象都是标准的 ESM。当用户在编写一个没有 import/export 关键字的文件时,编辑器会提示错误,这样会强制规范用户编写 ESM 代码。

(2)useDefineForClassFields。

在一个类中,假设要定义和修改一个 tag 属性,我们会通过 this.tag = xxx 这种方式实现。然而这种方式在有继承关系的类中,可能会同时修改子类和父类的 tag 属性。当我们只想修改当前类而不想影响到父类时,就要设置 "useDefineForClassFields": true。

该属性设置为 true 后,TypeScript 会将 this.tag = xxx 编译为 Object.defineProperty(this, 'tag', {}),这会保证属性设置只对当前类生效,Vite 推荐使用这种方式,我们只需要了解该选项的意义并设置为 true 即可。

  1. 客户端类型

Vite 默认的类型定义是面向 Node.js API 的,因为大多时候 Vite 是在 Node.js 环境下使用。但有一部分类型需要在客户端中使用,比较有代表性的就是环境变量。在 Vite 中,定义的环境变量在浏览器环境下可以通过 import.meta.env 对象访问,那么该对象就要设置为环境变量的类型。

为此,Vite 单独为客户端提供了类型,我们在客户端的根目录添加一个 d.ts 声明文件,引入这个类型:

js
/// <reference types="vite/client" />

现在,在源码中使用 import.meta.env 就可以看到环境变量的类型了。Vite 的客户端类型还提供了以下类型定义补充:

  • 资源导入(例如:导入一个 .svg 文件)
  • import.meta.hot:HMR API 类型定义。

JSX/TSX 转译

在 Vite 中 .jsx 和 .tsx 文件同样开箱即用,它们的转译同样是通过 esbuild 实现。

在 Vue3 中,JSX 转译通过官方插件 @vitejs/plugin-vue-jsx 实现,该插件同时支持 JSX & TSX 语法。而在 React 中插件 @vitejs/plugin-react 同时支持了 HRM 和 JSX/TSX 转译,不需要单独的插件实现。

如果在非 Vue/React 项目中使用 JSX,则需要自定义 esbuild 配置。比如在 Preact 中使用 JSX 配置方法如下:

js
// vite.config.js
import { defineConfig } from "vite";
export default defineConfig({
  esbuild: {
    jsxFactory: "h",
    jsxFragment: "Fragment",
  },
});

CSS 系列支持

在 Vite 中,不管是 .vue 文件中的 CSS 代码,还是在 JS 文件中单独导入一个 CSS 文件,最终样式都会被编译到 html 文件的 <style> 标签中。

Vite 对 CSS 提供了多项优化支持,主要包含三个方面。

  1. @import

@import 关键字的作用是在一个 CSS 文件中导入另一个 CSS 文件,这样可以让 CSS 像 JS 模块一样互相引用。比如在项目的 CSS 文件中引入 UI 框架的样式,就可以这样写:

css
@import "element-plus/dist/index.css";
#app {
  font-size: 15px;
}

值得庆祝的是,@import 还支持使用别名。假设我们在 vite.config.ts 中配置了别名 “@” 指向 src 目录,那么该别名不光可以在 JS 文件中使用,在 @import 中也可以,如下:

js
// app.js
import '@/style/app.css'

// app.css
@import '@/style/base.css';
#app {
  font-size: 15px;
}

Vite 通过 postcss-import 实现了 @import 关键字,因此在项目中 PostCSS 也是开箱即用的。你可以指定一个 postcss.config.js 文件,该配置将对所有的 CSS 文件生效。

  1. CSS Modules

任何以 .module.css 为后缀名的 CSS 文件都被认为是一个 CSS modules 文件,使用 CSS Modules 的作用是该文件在被 JavaScript 导入时,会自动导出一个 JavaScript 对象。该对象的属性是 CSS 中定义的类名,属性值则是一个生成的唯一类名。

也就是说,CSS Modules 中定义的样式在导入时不会直接生效,而是生成一个唯一的类名并将样式放入其中。若要使用这个样式,需要为元素添加这个类名,如下。

js
// test.module.css
.title {
  color: red;
  font-size: 16px;
}

// test.js
import styles from './test.module.css'
console.log(styles.title) // _red_hyqy7_1
document.querySelector('p').className = styles.title

CSS Modules 编译后会生成一个唯一的类名,并在该类名下放入样式。比如上面的 test.module.css,编译后生成了类名 “red_hyqy7_1”,最终样式如下:

css
._red_hyqy7_1 {
  color: red;
  font-size: 16px;
}
  1. CSS 预处理器

由于 Vite 的目标仅为现代浏览器,所以更推荐使用 CSS 变量、或者 PostCSS 插件提供的面向未来的 CSS 语法来实现高级的样式功能。比如要在项目中实现主题色切换,就可以将主题色定义为一个 CSS 变量,切换时修改这个变量即可。

即便如此,Vite 依然内置了对 Less、Sass 等预处理器的支持,无需安装特定的插件,只需要安装相应的预处理器依赖即可:

sh
# sass
yarn add -D sass

# less
yarn add -D less

在 Vue 单文件组件中,开启预处理器只需要一个 lang 属性,例如 <style lang="sass"></style> 表示该部分样式使用 sass 预处理器,当然 lang 属性你也可以替换成 less 等其他预处理器名称。

Sass 和 Less 中使用 @import 时同样支持使用路径别名,这非常方便。除此之外,也可以使用预处理器版的 CSS Modules,比如:test.module.less。

静态资源导入

静态资源主要是图片,字体等非代码资源,导入静态资源的方式和导入 ESM 一样,区别是 Vite 会识别导入的静态资源,并对其进行特定的处理。

当导入图片或字体时,会返回解析后的 URL:

js
import imgUrl from "./img.png";
console.log(imgUrl); // /src/assets/img.png

当导入 JSON 时,会直接返回 JSON 对象,甚至可以直接将其解构,如下:

js
import json from "./test.json";
console.log(json); // { name: 'test' }

import { name } from "./test.json";
console.log(name); // test

Vite 还支持我们修改资源被引入的方式。比如设置导入资源返回的是资源路径还是字符串,通过以下方式实现:

js
import json from "./test.json?url";
console.log(json); // /src/assets/img.png

import json2 from "./test.json?raw";
console.log(json2); // "{ name: 'test' }"
  1. 动态导入

在 ESM 中标准的导入方式是使用 import 关键字,它是同步导入,只能在模块顶层使用。而有时候我们需要使用动态导入,也就是常说的懒加载、异步加载,比如动态导入页面路由、动态加载某个资源等,Vite 也支持我们这样做。

动态导入通过 import() 函数实现,该函数可全局使用。函数参数是一个要导入的资源路径,函数执行后返回一个 Promise,因此有以下两种方式获取导入后的资源。

js
// Promise
import("xxx.js").then((res) => {
  console.log(res);
});

// async/await
const fun = async () => {
  let res = await import("xxx.js");
};

在 Vite 中使用动态导入的模块,在构建时会拆分为单独的 chunk,这样可以避免生成一个过于庞大的文件,影响页面加载性能。

  1. Glob 导入

Vite 支持通过模糊匹配批量导入资源,这种方式被称为 “Glob 导入”。Glob 导入是用特殊的 import.meta.glob 函数实现,参数是一个模糊路径。假设有现在有一个 dir 目录,下面有 a.js 和 b.js 两个文件,那么 Glob 导入方式如下:

js
var modules = import.meta.glob("./dir/*.js");

上面代码返回一个对象,属性是匹配到的文件路径,值是一个 import() 动态导入函数。上面的 Glob 导入的效果等同于下面的代码:

js
var modules = {
  "./dir/a.js": () => import("./dir/a.js"),
  "./dir/b.js": () => import("./dir/a.js"),
};

由上述代码可见,Glob 导入模块默认使用动态导入。如果想直接导入所有模块,你可以将 { eager: true } 作为第二个参数传入。

js
var modules = import.meta.glob("./dir/*.js", { eager: true });