Vite 笔记
第一章:走进 Vite 世界
一、是什么
Vite 是一个由 Vue.js 的作者 Evan You 开发的现代前端构建工具。它利用了浏览器原生的 ES modules 支持来实现快速的冷启动和即时热更新,提供了一个更快速、更轻量的开发环境。
### 二、原理前端构建工具是什么?
前端构建工具是用于自动化前端开发流程的工具,包括但不限于代码转换(例如,将 TypeScript 转换为 JavaScript、将 ES6+ 代码转换为 ES5 代码)、文件合并、文件压缩、代码检查、测试等。
1. 依赖预构建
1)是什么
no-bundle 只是对于我们自己写的代码而言,对于第三方依赖,Vite 还是选择 bundle (打包) ,并且使用速度极快的打包器 Rolldown 来完成这一过程,达到秒级的依赖编译速度。
所以当首次启动 Vite 时,Vite 在本地加载你的站点之前预构建了项目依赖。默认情况下,它是自动且透明完成的。
这就是 Vite 执行时所做的“依赖预构建”。
2)为什么要依赖预构建
CommonJS 和 UMD 兼容性:开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将以 CommonJS 或 UMD 形式提供的依赖项转换为 ES 模块。
打包第三方库的代码,将各个第三方库分散的文件合并到一起,减少 HTTP 请求数量,避免页面加载性能劣化。
方便对路径重写:对路径的处理上可以直接使用
.vite/deps(这也是原生 esmodule 规范不敢支持 node_modules 的原因之一)。javascriptimport _ from "/node_modules/.vite/lodash"; // lodash可能也import了其他的东西 import __vite__cjsImport0_lodash from "/node_modules/.vite/deps/lodash.js?v=ebe57916";
注意:
依赖预构建仅适用于开发模式,并使用 Rolldown 将依赖项转换为 ES 模块。
3)构建好的依赖放哪
[1] 文件系统缓存
Vite 将预构建的依赖项缓存到 node_modules/.vite 中。它会基于以下几个来源来决定是否需要重新运行预构建步骤:
- 包管理器的锁文件内容,例如
package-lock.json、yarn.lock、pnpm-lock.yaml,或者bun.lockb; - 补丁文件夹的修改时间;
vite.config.js中的相关字段;NODE_ENV的值。
只有在上述其中一项发生更改时,才需要重新运行预构建。
如果出于某些原因想要强制 Vite 重新构建依赖项,可以在启动开发服务器时指定 --force 选项,或手动删除 node_modules/.vite 缓存目录。
[2] 浏览器缓存
已预构建的依赖请求使用 HTTP 头 max-age=31536000, immutable 进行强缓存,以提高开发期间页面重新加载的性能。一旦被缓存,这些请求将永远不会再次访问开发服务器。如果安装了不同版本的依赖项(这反映在包管理器的 lockfile 中),则会通过附加版本查询自动失效。如果想通过本地编辑来调试依赖项,可以:
- 通过浏览器开发工具的 Network 选项卡暂时禁用缓存;
- 重启 Vite 开发服务器指定
--force选项,来重新构建依赖项; - 重新载入页面。
2. 架构
1)新版
2025 年 12 月 3 号,Vite 8 Beta 发布了,底层换成了 Rolldown。
Rolldown 是什么?
简单说:一个 Rust 写的打包器,目标是同时替代 Esbuild 和 Rollup。
尤雨溪的 VoidZero 团队搞的,拿了 1700 多万美金融资。整个工具链是这样的:
三个项目同一个团队维护,行为一致性有保障。
Rolldown 和 Esbuild 速度差不多,比 Rollup 快 10-30 倍。尤雨溪自己测 Vue 核心代码的打包,Rolldown 比 Rollup 快 7 倍,比 Esbuild 还快将近 2 倍。
CSS 压缩
默认从 Esbuild 换成了 Lightning CSS。
JS 压缩
从 Esbuild 换成了 Oxc Minifier。
2)旧版 - 双引擎架构

Vite 在预构建与生产打包中使用的工具不一样。预构建使用 Esbuild,生产打包使用传统的 Rollup。且对 TS 和 JSX 编译、代码压缩都使用 Esbuild。
Vite 兼容 Rollup 的生态,且对 Rollup 打包好的产物进行了优化。如:
- CSS 代码分割。如果某个异步模块中引入了一些 CSS 代码,Vite 就会自动将这些 CSS 抽取出来生成单独的文件,提高线上产物的缓存复用率。
- 自动预加载。Vite 会自动为入口 chunk 的依赖自动生成预加载标签
<link rel="modulepreload">。 - 异步 Chunk 加载优化。
三、快速入门
1. 创建 Vite 项目
npm create vite@latest
# or
pnpm create vite然后按照提示操作即可!
2. index.html 与项目根目录
项目根目录:
- 默认为执行
vite xxx命令所在目录。 - Vite 会在项目根目录中寻找自己的配置文件。
- 源码中的绝对 URL 路径将以项目的根作为基础来解析。
入口文件:index.html 放在项目根目录下,Vite 会把这个作为入口,解析项目依赖。
一个 import 语句对于 Vite 来说就是一个网络请求。当请求
http://localhost:5000地址时,Vite 的 Dev Server 会接受到这个请求,然后读取对应的文件内容,进行一定的中间处理,最后将处理的结果返回给浏览器。
第二章:功能
一、配置文件
官方文档:配置 Vite
1. 是什么
当以命令行方式运行 Vite 时,Vite 会自动解析项目根目录下名为 vite.config.js 的配置文件(也支持其他 ts、mjs 扩展名)。
export default {
// 配置选项
}注意:即使项目没有在
package.json中开启type: "module",Vite 也支持在配置文件中使用 ESM 语法。
可以显式地通过 --config 命令行选项指定一个配置文件(相对于 cwd (current working directory) 路径进行解析)。
vite --config my-config.js2. 配置智能提示
因为 Vite 本身附带 TypeScript 类型,所以可以通过 IDE 和 jsdoc 的配合来实现智能提示:
/** @type {import('vite').UserConfig} */
export default {
// ...
}另外可以使用 defineConfig 工具函数,这样不用 jsdoc 注解也可以获取类型提示:
import { defineConfig } from 'vite'
export default defineConfig({
// ...
})Vite 也直接支持 TypeScript 配置文件。可以在 vite.config.ts 中使用上述的 defineConfig 工具函数,或者 satisfies 运算符:
import type { UserConfig } from 'vite'
export default {
// ...
} satisfies UserConfig3. 情景配置
如果配置文件需要基于(dev / serve 或 build)命令或者不同的模式来决定选项,则可以选择导出这样一个函数:
export default defineConfig(({ command }) => {
if (command === 'serve') {
return {
// dev 独有配置
}
} else {
// command === 'build'
return {
// build 独有配置
}
}
})注意:Vite 的 API 中,在开发环境下 command 的值为 serve(vite dev 和 vite serve 是 vite 的别名),而在生产环境下为 build(vite build)。
二、环境变量和模式
官方文档:环境变量和模式
1. 环境变量
[1] 读取
Vite 使用 dotenv 从环境目录(envDir)中的下列文件加载额外的环境变量。
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略上面的环境变量配置文件这么多,以哪个为准?环境加载优先级
用于指定模式的文件(例如
.env.production)会比通用形式的优先级更高(例如.env)。命令行参数优先级最高。例如
VITE_SOME_KEY=123 vite build的时候。
注意:改动任何环境变量文件,要生效需要重启。
[2] 代码中使用
js 中使用
加载的环境变量会通过 import.meta.env 以字符串形式暴露给客户端源码。且只有以 VITE_ 为前缀的变量才会暴露。例如:
VITE_SOME_KEY=123
DB_PASSWORD=foobar只有 VITE_SOME_KEY 会被暴露为 import.meta.env.VITE_SOME_KEY 提供给客户端源码,而 DB_PASSWORD 则不会。
console.log(import.meta.env.VITE_SOME_KEY) // "123"
console.log(import.meta.env.DB_PASSWORD) // undefined如果想自定义 env 变量的前缀,请参阅 envPrefix。
HTML 中使用
import.meta.env 中的任何属性都可以通过 %ENV_NAME% 语法在 HTML 文件中使用:
<h1>Vite is running in %MODE%</h1>
<p>Using data from %VITE_API_URL%</p>如果环境变量在 import.meta.env 中不存在,比如不存在的 %NON_EXISTENT%,则会将被忽略而不被替换,这与在 JS 中使用 import.meta.env.NON_EXISTENT 不同,JS 中会被替换为 undefined。
2. 模式
默认情况下,开发服务器 (dev 命令) 运行在 development (开发) 模式,而 build 命令则运行在 production (生产) 模式。
这意味着当执行 vite build 时,它会自动加载 .env.production 中可能存在的环境变量:
# .env.production
VITE_APP_TITLE=My App若想在 vite build 时来使用不同的环境变量配置文件,可以通过传递 --mode 选项标志来覆盖命令使用的默认模式。例如,如果想在 staging (预发布)模式下构建应用:
vite build --mode staging还需要新建一个 .env.staging 文件:
# .env.staging
VITE_APP_TITLE=My App (staging)3. 内置常量
一些内置常量在所有情况下都可用:
import.meta.env.MODE: {string} 应用运行的模式。import.meta.env.BASE_URL: {string} 部署应用时的基本 URL。由base配置项决定。import.meta.env.PROD: {boolean} 应用是否运行在生产环境(使用NODE_ENV='production'运行开发服务器或构建应用时使用NODE_ENV='production')。import.meta.env.DEV: {boolean} 应用是否运行在开发环境。import.meta.env.SSR: {boolean} 应用是否运行在 server 上。
4. TS 智能提示
在 src 目录下创建一个 vite-env.d.ts 文件:
interface ViteTypeOptions {
// 添加这行代码,你就可以将 ImportMetaEnv 的类型设为严格模式,
// 这样就不允许有未知的键值了。
// strictImportMetaEnv: unknown
}
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}三、Glob 导入
1. 使用
1)单个匹配模式
从文件系统导入多个模块:
const modules = import.meta.glob('./dir/*.js')以上将会被转译为下面的样子:
// vite 生成的代码
const modules = {
'./dir/bar.js': () => import('./dir/bar.js'),
'./dir/foo.js': () => import('./dir/foo.js')
}可以遍历 modules 对象的 key 值来访问相应的模块:
for (const path in modules) {
modules[path]().then((mod) => {
console.log(path, mod)
})
}匹配到的文件默认是懒加载的,通过动态导入实现,并会在构建时分离为独立的 chunk。如果倾向于直接引入所有的模块(例如依赖于这些模块中的副作用首先被应用),可以传入 { eager: true } 作为第二个参数:
const modules = import.meta.glob('./dir/*.js', { eager: true })以上会被转译为下面的样子:
// vite 生成的代码
import * as __vite_glob_0_0 from './dir/bar.js'
import * as __vite_glob_0_1 from './dir/foo.js'
const modules = {
'./dir/bar.js': __vite_glob_0_0,
'./dir/foo.js': __vite_glob_0_1
}2)多个匹配模式
第一个参数可以是一个 glob 数组,例如:
const modules = import.meta.glob(['./dir/*.js', './another/*.js'])3)反面匹配模式
同样也支持反面 glob 匹配模式(以 ! 作为前缀)。若要忽略结果中的一些文件,你可以添加“排除匹配模式”作为第一个参数:
const modules = import.meta.glob(['./dir/*.js', '!**/bar.js'])// vite 生成的代码
const modules = {
'./dir/foo.js': () => import('./dir/foo.js')
}4)其他使用方式
[1] 具名导入
也可能只想要导入模块中的部分内容,那么可以利用 import 选项。
const modules = import.meta.glob('./dir/*.js', { import: 'setup' })// vite 生成的代码
const modules = {
'./dir/bar.js': () => import('./dir/bar.js').then((m) => m.setup),
'./dir/foo.js': () => import('./dir/foo.js').then((m) => m.setup)
}当与 eager 一同存在时,甚至可以对这些模块进行 tree-shaking。
const modules = import.meta.glob('./dir/*.js', {
import: 'setup',
eager: true
})// vite 生成的代码
import { setup as __vite_glob_0_0 } from './dir/bar.js'
import { setup as __vite_glob_0_1 } from './dir/foo.js'
const modules = {
'./dir/bar.js': __vite_glob_0_0,
'./dir/foo.js': __vite_glob_0_1
}设置 import 为 default 可以加载默认导出。
const modules = import.meta.glob('./dir/*.js', {
import: 'default',
eager: true
})// vite 生成的代码
import { default as __vite_glob_0_0 } from './dir/bar.js'
import { default as __vite_glob_0_1 } from './dir/foo.js'
const modules = {
'./dir/bar.js': __vite_glob_0_0,
'./dir/foo.js': __vite_glob_0_1
}[2] 自定义查询
也可以使用 query 选项来提供对导入的自定义查询,比如,可以将资源 作为字符串引入 或者 作为 URL 引入 :
const moduleStrings = import.meta.glob('./dir/*.svg', {
query: '?raw',
import: 'default'
})
const moduleUrls = import.meta.glob('./dir/*.svg', {
query: '?url',
import: 'default'
})// vite 生成的代码
const moduleStrings = {
'./dir/bar.svg': () => import('./dir/bar.svg?raw').then((m) => m['default']),
'./dir/foo.svg': () => import('./dir/foo.svg?raw').then((m) => m['default'])
}
const moduleUrls = {
'./dir/bar.svg': () => import('./dir/bar.svg?url').then((m) => m['default']),
'./dir/foo.svg': () => import('./dir/foo.svg?url').then((m) => m['default'])
}你还可以为其他插件提供定制化的查询参数:
const modules = import.meta.glob('./dir/*.js', {
query: { foo: 'bar', bar: true }
})[3] 基础路径
还可以使用 base 选项为导入提供基础路径:
const modulesWithBase = import.meta.glob('./**/*.js', {
base: './base'
})// vite 生成的代码:
const modulesWithBase = {
'./dir/foo.js': () => import('./base/dir/foo.js'),
'./dir/bar.js': () => import('./base/dir/bar.js')
}base 选项只能是相对于导入文件的目录路径,或者相对于项目根目录的绝对路径。不支持别名和虚拟模块。
只有相对路径的 glob 模式会被解释为相对于解析后的基础路径。
如果提供了基础路径,所有生成的模块键值都会被修改为相对于该基础路径。
2. 注意事项
- 这只是一个 Vite 独有的功能。
- 该 Glob 模式会被当成导入标识符:必须是相对路径(以
./开头)或绝对路径(以/开头,相对于项目根目录解析)或一个别名路径。 - 所有
import.meta.glob的参数都必须以字面量传入。不可以在其中使用变量或表达式。
四、动态导入
和 glob 导入 类似,Vite 也支持带变量的动态导入。
const module = await import(`./dir/${file}.js`)注意:变量仅代表一层深的文件名。如果 file 是 foo/bar,导入将会失败。
另请注意,动态导入必须符合以下规则才能被打包:
- 导入语句必须以
./或../开头。 - 导入语句必须以文件扩展名结尾。
- 导入到自身目录时,必须指定文件名模式。
第三章:配置文件
一、共享选项
1. 基础
root:设置项目根目录。可以是一个绝对路径,或者一个相对于该配置文件本身的相对路径。
base:开发或生产环境服务的公共基础路径。
// vite.config.ts
// 是否为生产环境,在生产环境一般会注入 NODE_ENV 这个环境变量,见下面的环境变量文件配置
const isProduction = process.env.NODE_ENV === 'production';
// 填入项目的 CDN 域名地址
const CDN_URL = 'xxxxxx';
// 具体配置
{
base: isProduction ? CDN_URL: '/'
}
// -------------------------------------------------
// .env.development
NODE_ENV=development
// .env.production
NODE_ENV=productionmode:'development' 用于开发、'production' 用于构建。
define:定义全局变量,在程序里都可以使用。它和 Webpack 里的 DefinePlugin 是一模一样的功能。构建时被静态替换。
plugins:使用到的插件。
publicDir:作为静态资源服务的文件夹。该目录中的文件在开发期间在 / 处提供,并在构建期间复制到 outDir 的根目录。该值可以是文件系统的绝对路径,也可以是相对于项目根目录的相对路径。将 publicDir 设定为 false 可以关闭此项功能。
cacheDir:存储缓存文件的目录。值为相对路径(以项目根目录为基准)或文件的绝对路径。
2. resolve
import { defineConfig } from 'vite'
export default defineConfig({
resolve: {
alias: {
'utils': path.resolve(__dirname, 'src/utils'), // 遇到 import 'utils' 就指引到绝对路径
'vue': 'vue/dist/vue.esm-bundler.js' // 连第三方包的名字都能劫持替换
},
mainFields: ['browser', 'module', 'jsnext:main', 'jsnext'],
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json']
}
})3. 静态资源处理
assetsInclude:告诉 Vite 把哪些文件后缀名看成静态资源。import 和 require 返回路径。
4. CSS
1)CSS Modules
css.modules:CSS modules 行为可以通过 css.modules 选项 进行配置。
localsConvention
用于配置 CSS 模块的类名转换规则。
localsConvention?:
| 'camelCase'
| 'camelCaseOnly'
| 'dashes'
| 'dashesOnly'
| ((
originalClassName: string,
generatedClassName: string,
inputFile: string,
) => string)CSS 模块允许在 JavaScript 中以对象的形式导入 CSS 类。localsConvention 选项决定了这些类名在导入时应该如何转换。
localsConvention 选项有四个可能的值:
'camelCase':在这种模式下,类名会被转换为驼峰式(camelCase)和短横线式(dashes)两种形式。例如,.my-class会被转换为{ 'my-class': '...', myClass: '...' }。'camelCaseOnly':在这种模式下,类名只会被转换为驼峰式。例如,.my-class会被转换为{ myClass: '...' }。'dashes':在这种模式下,类名会被转换为驼峰式和短横线式两种形式,但是驼峰式的类名会被转换为短横线式。例如,.myClass会被转换为{ 'my-class': '...', myClass: '...' }。'dashesOnly':在这种模式下,类名只会被转换为短横线式。例如,.myClass会被转换为{ 'my-class': '...' }。
如果 css.modules.localsConvention 设置开启了 camelCase 格式变量名转换(例如 localsConvention: 'camelCaseOnly'),可以使用按名导入。
// .apply-color -> applyColor
import { applyColor } from './example.module.css'
document.getElementById('foo').className = applyColorscopeBehaviour
scopeBehaviour?: 'global' | 'local'local 表示在 CSS 选择器后面加上 hash 来防止类名冲突;global 表示不会在 CSS 选择器后面加上 hash,原来的选择器名字是什么,就是什么。
generateScopedName
用于自定义 CSS 模块的作用域名生成规则。
generateScopedName?:
| string
| ((name: string, filename: string, css: string) => string)默认情况下,Vite 会为 CSS 模块生成唯一的作用域名,以避免样式冲突。generateScopedName 选项允许你自定义这个生成规则。
generateScopedName 选项可以是一个字符串或一个函数:
如果它是一个字符串,那么这个字符串应该是一个包含
[name]、[filename]、[hash]等占位符的模板。例如,你可以设置generateScopedName: '[name]__[hash]',那么生成的作用域名可能是myClass__3a4b2c。如果它是一个函数,那么这个函数应该接受三个参数:
name(原始的类名)、filename(CSS 文件的路径)、css(CSS 文件的内容),并返回一个字符串作为作用域名。例如,你可以设置generateScopedName: (name, filename, css) => name + '__' + hash(css),那么生成的作用域名可能是myClass__3a4b2c。在这个例子中,
myClass是一个 CSS 类,3a4b2c是这个 CSS 文件内容的哈希值。这样,每个 CSS 类都会有一个唯一的作用域名,避免了样式冲突。
hashPrefix
用于自定义 CSS 模块的哈希前缀。
hashPrefix?: string在 CSS 模块中,为了避免样式冲突,Vite 会为每个类名生成一个唯一的哈希值。hashPrefix 选项允许你为这个哈希值添加一个前缀。
例如,如果你设置 hashPrefix: 'my-prefix',那么生成的类名可能是 my-prefix__myClass__3a4b2c。
在这个例子中,myClass 是一个 CSS 类,3a4b2c 是这个 CSS 类的哈希值,my-prefix 是你自定义的前缀。
这个选项可以帮助你在查看生成的 CSS 代码时更容易识别出来自哪个模块的样式。
globalModulePaths
用于指定哪些 CSS 模块应该被视为全局模块。
globalModulePaths?: RegExp[]默认情况下,Vite 会将 .module.css 文件视为 CSS 模块,而将其他 CSS 文件视为全局 CSS 文件。全局 CSS 文件中的样式会应用到全局,而不是仅限于特定的模块。
globalModulePaths 选项允许你改变这个行为。这个选项应该是一个正则表达式数组,这些正则表达式用于匹配文件路径。匹配的文件会被视为全局 CSS 文件,即使它们的扩展名是 .module.css。
例如,如果设置 globalModulePaths: [/global\.css$/],那么所有以 global.css 结尾的文件都会被视为全局 CSS 文件,即使它们的扩展名是 .module.css。
这个选项可以帮助你更灵活地管理你的 CSS 模块和全局样式。
2)预处理器
css.preprocessorOptions:指定传递给 CSS 预处理器的选项。文件扩展名用作选项的键。每个预处理器支持的选项可以在它们各自的文档中找到:
sass/scss:可以安装 sass-embedded 或 sass 包。为了获得最佳性能,建议安装 sass-embedded 包。less:选项styl/stylus:仅支持define,可以作为对象传递。
css.preprocessorOptions[extension].additionalData:用来为每一段样式内容添加额外的代码。但是要注意,如果你添加的是实际的样式而不仅仅是变量,那这些样式在最终的产物中会重复。
css.preprocessorMaxWorkers:指定 CSS 预处理器可以使用的最大线程数。默认 CPU 数量减 1。
3)用于处理 CSS 的引擎
css.transformer:用于指定用于处理 CSS 的引擎。默认值 'postcss',还可使用 'lightningcss'。
4)给 CSS 引擎传递参数
css.postcss:内联的 PostCSS 配置(格式同 postcss.config.js),或者一个(默认基于项目根目录的)自定义的 PostCSS 配置路径。
css.lightningcss:用于配置 Lightning CSS。
5)生成 CSS 的 Sourcemap
css.devSourcemap:在开发过程中是否启用 sourcemap。
5. JSON
json.namedExports:是否支持从 .json 文件中进行按名导入。
json.stringify:若设置为 true,导入的 JSON 会被转换为 export default JSON.parse("..."),这样会比转译成对象字面量性能更好,尤其是当 JSON 文件较大的时候。如果设置为 'auto',只有当数据大于 10kB 时,才会对数据进行字符串化处理。
6. 环境变量
envDir:用于加载 .env 文件的目录。可以是一个绝对路径,也可以是相对于项目根的路径。设置为 false 将禁用 .env 文件的加载。
envPrefix:以 envPrefix 开头的环境变量会通过 import.meta.env 暴露在你的客户端源码中。默认值 VITE_。
7. 日志
logLevel:调整控制台输出的级别,默认为
'info'。clearScreen:设为 false 可以避免 Vite 清屏而错过在终端中打印某些关键信息。
customLogger:自定义日志记录器。
typescriptinterface Logger { info(msg: string, options?: LogOptions): void // 负责输出普通信息 warn(msg: string, options?: LogOptions): void // 负责输出警告 warnOnce(msg: string, options?: LogOptions): void // 负责只输出一次的警告(防止刷屏) error(msg: string, options?: LogErrorOptions): void // 负责输出报错 clearScreen(type: LogType): void // 负责清屏(比如你每次保存文件,Vite 会刷一下屏) hasErrorLogged(error: Error | RollupError): boolean // 用来判断有没有报错记录 hasWarned: boolean // 标记有没有记录过警告 }如果要替换 Vite 的日志系统,必须提供一个包含上面这些方法的对象。这样 Vite 底层想打日志时,调用你的这些方法才不会奔溃。
怎么既不破坏 Vite 自己的酷炫排版和颜色,又能在它的上面动手脚呢?官方给了下面这个示例代码:
typescriptimport { createLogger, defineConfig } from 'vite' // 第一步:向 Vite 伸手要把原装的默认日志系统拿出来 const logger = createLogger() // 第二步:悄悄把原装系统里的“打警告(warn)”这个功能单独存下来备用 const loggerWarn = logger.warn // 第三步:魔改被拿出来的那个 logger 的 warn 方法(狸猫换太子) logger.warn = (msg, options) => { // 【拦截逻辑】:如果 Vite 想打印的消息是关于 CSS 的,而且说文件是空的 if (msg.includes('vite:css') && msg.includes(' is empty')) { return // 直接 return(闭嘴),啥也不打印!这个警告就被你屏蔽了。 } // 【放行逻辑】:如果是别的警告,原汁原味地交还给原装警告功能去打印 loggerWarn(msg, options) } // 最后:把这个外表还是原来的 logger,但肚子里警告功能已经被你魔改过的对象,交还给配置。 export default defineConfig({ customLogger: logger, })
8. 其他
oxc:Oxc 工具的配置。
二、开发服务器选项
server.host:指定服务器应该监听哪个 IP 地址。 如果将此设置为 0.0.0.0 或者 true 将监听所有地址,包括局域网和公网地址。
server.allowedHosts:Vite 允许响应的主机名。 默认情况下,允许 localhost 及其下的所有 .localhost 域名和所有 IP 地址。 使用 HTTPS 时,将跳过此检查。如果设置为 true,服务器将被允许响应任何主机的请求。
server.port:指定开发服务器端口。若指定端口被占用,Vite 会自动切换其他端口。
server.strictPort:设为 true 时,若端口已被占用则会直接退出,而不是尝试下一个可用端口。
server.open:设为 true 时,开发服务器启动时,自动在浏览器中打开应用程序。当该值为字符串时,它将被用作 URL 的路径名。
server.proxy:设置代理服务器。
三、构建选项
1. 输出产物
build.target:项目要兼容的浏览器版本。
build.outDir:指定输出路径,相对项目根目录。
build.assetsDir:指定生成静态资源的存放路径(相对于 build.outDir)。
build.assetsInlineLimit:小于此阈值的导入或引用资源将内联为 base64 编码,以避免额外的 http 请求。设置为 0 可以完全禁用此项。
build.sourcemap:构建后是否生成 source map 文件。如果为 true,将会创建一个独立的 source map 文件。如果为 'inline',source map 将作为一个 data URI 附加在输出文件中。'hidden' 的工作原理与 true 相似,只是 bundle 文件中相应的注释将不被保留。
build.rolldownOptions:配置 Rolldown,并与 Vite 的内部 Rolldown 选项合并。
build.dynamicImportVarsOptions:用来控制打包工具在遇到 import(./pages/${A}.js) 这种带变量的语法时,去哪些目录“捞文件”的范围限制开关。
build.write:false 来禁用将构建后的文件写入磁盘。
build.copyPublicDir:默认情况下,Vite 会在构建阶段将 publicDir 目录中的所有文件复制到 outDir 目录中。可以通过设置该选项为 false 来禁用该行为。
build.lib:以库的形式构建。entry 是必需的,因为库不能使用 HTML 作为入口。name 是暴露的全局变量。
build.watch:一直监听代码改变后编译。
2. CSS 相关
build.cssCodeSplit:启用或禁用 CSS 代码拆分。当启用时,在异步 chunk 中导入的 CSS 将内联到异步 chunk 本身,并在其被加载时一并获取。如果禁用,整个项目中的所有 CSS 将被提取到一个 CSS 文件中。
build.cssTarget:CSS 代码要兼容的浏览器。
build.cssMinify:覆盖压缩 CSS 的配置,而不是使用默认的 build.minify。
3. 压缩
build.minify:禁用或更换压缩器,默认使用 Oxc Minifier。
build.terserOptions:传递给 Terser 的更多 minify 选项。此外,还可以传递一个 maxWorkers: number 选项来指定最大的工作线程数。默认为 CPU 核心数减 1。
4. 报告
build.license:生成许可证信息。
build.manifest:是否生成一个 manifest 文件,包含了没有被 hash 过的资源文件名和 hash 后版本的映射。
build.emptyOutDir:默认情况下,若 outDir 在根目录下,则 Vite 会在构建时清空该目录。若 outDir 在根目录之外则会抛出一个警告避免意外删除掉重要的文件。可以设置该选项来关闭这个警告。
build.reportCompressedSize:启用 / 禁用 gzip 压缩大小报告。
build.chunkSizeWarningLimit:规定触发警告的 chunk 大小。(以 kB 为单位)。
七、依赖优化选项
官方文档:依赖优化选项
1. 依赖预构建
1)optimizeDeps.entries
告诉 Vite 应该去扫描哪些文件里面的依赖。
2)optimizeDeps.include
明确告诉 Vite 把哪些依赖加入到预构建中。
3)optimizeDeps.exclude
明确告诉 Vite 把哪些依赖排除掉,在预构建中。
4)optimizeDeps.force
设置为 true 每次都强制依赖预构建,而忽略之前已经缓存过的、已经优化过的依赖。
5)optimizeDeps.noDiscovery
关闭默认的依赖预构建。只扫描 optimizeDeps.include 中列出的依赖项。
2. 其他
1)optimizeDeps.holdUntilCrawlEnd
等待入口爬取完成后再运行优化器,减少二次优化的概率。
2)optimizeDeps.needsInterop
Vite 大多数时候都能正确分析出某个包是否需要转换为 ESM,但在某些复杂场景下不行,就需要添加到这个选项里。
第四章:资源处理
一、接入 CSS 工程化方案
1. 原生 CSS
Vite 天生就支持对 css 文件的直接处理。导入 .css 文件后 vite 会干如下事情。
- Vite 在读取到 main.js 中引用到了 index.css。
- 直接去使用 fs 模块去读取 index.css 中文件内容。
- 直接创建一个 style 标签,将 index.css 中文件内容直接 copy 进 style 标签里。
- 将 style 标签插入到 index.html 的 head 中。
- 将该 css 文件中的内容直接替换为 js 脚本(方便 HMR 热更新或者 css 模块化)。同时设置 Content-Type 为 js,从而让浏览器以 JS 脚本的形式来执行该 css 后缀的文件。
2. CSS 预处理器
Vite 本身对 CSS 各种预处理器语言 (Sass/Scss、Less 和 Stylus) 做了内置支持。
但由于 Vite 底层会调用 CSS 预处理器的官方库进行编译,而 Vite 为了实现按需加载,并没有内置这些工具库,而是让用户根据需要安装。
# .scss 和 .sass
npm add -D sass-embedded # 或 sass
# .less
npm add -D less
# .styl 和 .stylus
npm add -D stylus之后在项目中使用 import 引入文件即可。
总结:Vite 同时提供了对
.scss,.sass,.less,.styl和.stylus文件的内置支持。没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖。
注意:如果使用的是单文件组件,可以通过<style lang="sass">(或其他预处理器)自动开启对代码的处理。
若有 variable.scss 全局变量,在每次使用时都要 @import "../../variable";,怎么可以省略引入?
// vite.config.ts
import { normalizePath } from 'vite';
// 如果类型报错,需要安装 @types/node: pnpm i @types/node -D
import path from 'path';
// 全局 scss 文件的路径
// 用 normalizePath 解决 window 下的路径问题
const variablePath = normalizePath(path.resolve('./src/variable.scss'));
export default defineConfig({
// css 相关的配置
css: {
preprocessorOptions: {
scss: {
// additionalData 的内容会在每个 scss 文件的开头自动注入
additionalData: `@import "${variablePath}";`
}
}
}
})同样的,可以对 less 和 stylus 进行一些能力的配置,可以去官方文档中查阅更多的配置项:
3. CSS Modules
任何以 .module.css 为后缀名的 CSS 文件都被认为是一个 CSS modules 文件。导入这样的文件会返回一个相应的模块对象:
/* example.module.css */
.red {
color: red;
}import classes from './example.module.css'
document.getElementById('foo').className = classes.red原理:全部都是基于 node。
- module.css (module 是一种约定,表示需要开启 css 模块化)
- 他会将你的所有类名进行一定规则的替换(将 footer 替换成 _footer_i22st_1)
- 同时创建一个映射对象
- 将替换过后的内容塞进 style 标签里,然后放入到 head 标签中 (能够读到 index.html 的文件内容)
- 将 componentA.module.css 内容进行全部抹除,替换成 JS 脚本
- 将创建的映射对象在脚本中进行默认导出
4. PostCSS
天生就支持。配置方式:
单独配置文件:
postcss.config.jsvite 配置文件中配置
ts// vite.config.ts 增加如下的配置 import autoprefixer from 'autoprefixer'; export default { css: { // 进行 PostCSS 配置 postcss: { plugins: [ autoprefixer({ // 需要提前安装 `pnpm i autoprefixer -D` 的 postcss 插件 // 指定目标浏览器 overrideBrowserslist: ['Chrome > 40', 'ff > 31', 'ie 11'] }) ] } } }
由于有 CSS 代码的 AST (抽象语法树) 解析能力,PostCSS 可以做的事情非常多,甚至能实现 CSS 预处理器语法和 CSS Modules,社区当中也有不少的 PostCSS 插件,除了刚刚提到的 autoprefixer 插件,常见的插件还包括:
- postcss-pxtorem:用来将 px 转换为 rem 单位,在适配移动端的场景下很常用。
- postcss-preset-env:通过它,你可以编写最新的 CSS 语法,不用担心兼容性问题。
- cssnano:主要用来压缩 CSS 代码,跟常规的代码压缩工具不一样,它能做得更加智能,比如提取一些公共样式进行复用、缩短一些常见的属性值等等。
5. CSS In JS
6. CSS 原子化框架
二、处理静态资源
1. 图片
1)常规图片
在开发中,使用图片的地方常见在 JS 、CSS 、HTML 中。
//----------------- JS 和 HTML
import logoSrc from '@assets/imgs/vite.png';
// HTML 中
<img id="logo" src={logoSrc} />
// JS 中
const img = document.getElementById('logo') as HTMLImageElement;
img.src = logoSrc;
// CSS 中
background: url('@assets/imgs/background.png') no-repeat;注意:
- 常见的图像、媒体和字体文件类型被自动检测为资源。你可以使用
assetsInclude选项 扩展内部列表。 - 较小的资源体积小于
assetsInlineLimit选项值 则会被内联为 base64 data URL。
2)SVG 组件方式加载
SVG 组件加载在不同的前端框架中的实现不太相同,社区中也已经了有了对应的插件支持:
- Vue3 项目中可以引入 vite-svg-loader。
- React 项目使用 vite-plugin-svgr 插件。
以 React 项目为例:
pnpm i vite-plugin-svgr -D然后需要在 vite 配置文件添加这个插件:
// vite.config.ts
import svgr from 'vite-plugin-svgr';
{
plugins: [
// 其它插件省略
svgr()
]
}随后注意要在 tsconfig.json 添加如下配置,否则会有类型错误:
{
"compilerOptions": {
// 省略其它配置
"types": ["vite-plugin-svgr/client"]
}
}接下来让我们在项目中使用 svg 组件:
import { ReactComponent as ReactLogo } from '@assets/icons/logo.svg';
export function Header() {
return (
// 其他组件内容省略
<ReactLogo />
)
}2. JSON
import { version } from '../../../package.json'; // 导入 JSON 文件, 会把 JSON 解析为对象不过也可以在配置文件禁用按名导入的方式:
// vite.config.ts
{
json: {
stringify: true
}
}这样会将 JSON 的内容解析为 export default JSON.parse("xxx"),这样会失去按名导出的能力,不过在 JSON 数据量比较大的时候,可以优化解析性能。
3. 其他资源
1)HTML
位于项目根目录下的 HTML 都可以通过 URL 来访问。且 Vite 会自动处理 HTML 引用的资源,会作为应用的一部分进行处理和打包。
要退出对某些元素的 HTML 处理,可以在元素上添加 vite-ignore 属性,这在引用外部 assets 或 CDN 时非常有用。
2)TypeScript
天然支持引入。使用 Oxc 转换器 仅作转换,不支持类型检查。因此这部分需要交给 IDE 或构建过程中处理。
3)JSX
天然支持引入。JSX 的转译也是通过 Oxc 转换器 处理的。
可以使用 jsxInject(这是一个仅在 Vite 中使用的选项)为 JSX 注入 helper,以避免手动导入:
import { defineConfig } from 'vite'
export default defineConfig({
oxc: {
jsxInject: `import React from 'react'`,
},
})4. 特殊资源后缀
Vite 中引入静态资源时,也支持在路径最后加上一些特殊的 query 后缀,包括:
?url:表示获取资源的路径。?raw:表示拿到资源的原始内容。?inline:表示资源强制内联,而不是打包成单独的文件。
第五章:使用插件
一、理论知识
1. 步骤
1)npm 安装:将插件添加到项目的 devDependencies。
2)在 vite.config.js 配置文件中的 plugins 数组中引入它。
查找 Vite 现有哪些插件,可以去 Vite Plugin Registry 或 awesome-vite 网址查找。
2. 停用或禁用插件
早期这样:
const myPlugins = [basePlugin()];
// 必须要写一大坨 if 逻辑来判断
if (isProd) {
myPlugins.push(compressPlugin());
}
export default defineConfig({
plugins: myPlugins
})现在,Vite 说了,Falsy 虚值的插件将被忽略。
export default defineConfig({
plugins: [
basePlugin(),
// 写法 1:如果是生产环境就调用插件,否则返回 false
isProd ? compressPlugin() : false,
// 写法 2:更酷的简写。如果 isDev 为真则启用插件,为假则这里变成 false
isDev && debugPlugin()
]
})3. 强制插件排序
使用 enforce 修饰符来强制插件的位置。
pre:在 Vite 核心插件之前调用该插件。- 默认:在 Vite 核心插件之后调用该插件。
post:在 Vite 构建插件之后调用该插件。
import image from '@rollup/plugin-image'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
...image(),
enforce: 'pre',
},
],
})Vite 中插件执行顺序
- Alias
- 带有
enforce: 'pre'的用户插件 - Vite 核心插件
- 没有 enforce 值的用户插件
- Vite 构建用的插件
- 带有
enforce: 'post'的用户插件 - Vite 后置构建插件(最小化,manifest,报告)
二、vite-aliases
在 Vite 6 中才需要这个插件。
1. 基本使用
vite-aliases 是一个用于自动生成 Vite 别名的插件。这些别名可以让你更方便地引用项目中的文件。
首先,需要安装 vite-aliases 插件。可以通过 npm 或 yarn 来安装:
npm install vite-aliases --save-dev
# 或者
yarn add vite-aliases --dev然后,在 Vite 配置文件(默认是 vite.config.js 或 vite.config.ts)中引入并使用这个插件:
import { defineConfig } from 'vite'
import viteAliases from 'vite-aliases'
export default defineConfig({
plugins: [viteAliases()]
})现在,vite-aliases 插件会自动为你的 src 目录生成别名。例如,如果有一个位于 src/components/MyComponent.vue 的文件,可以通过以下方式来引用它:
import MyComponent from '@/components/MyComponent.vue'在这个例子中,@ 是一个别名,代表 src 目录。
也可以通过传递一个选项对象来自定义 vite-aliases 插件的行为。例如,可以更改别名或添加额外的路径:
export default defineConfig({
plugins: [viteAliases({
dir: 'src', // 设置要为其生成别名的目录
prefix: '@', // 设置别名的前缀
deep: true, // 如果为 true,则为所有子目录生成别名。如果为 false,则只为顶级目录生成别名
allowGlobalAlias: false, // 如果为 true,则允许全局别名。全局别名是指不以任何前缀开头的别名
useTypescript: false, // 如果为 true,则从 TypeScript 配置文件(tsconfig.json)中读取 paths 选项,并将其用作别名
useConfig: false, // 如果为 true,则从 Vite 配置文件中读取 resolve.alias 选项,并将其用作别名
useFindConfig: false, // 如果为 true,则自动查找并使用 Vite 和 TypeScript 配置文件中的别名
extensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'vue', 'mjs'] // 设置要为其生成别名的文件类型
})]
})更多详细的配置选项,可以查看 vite-aliases 的 GitHub 仓库。
三、vite-plugin-mock
1. 基本使用
vite-plugin-mock 是一个用于 Vite 的模拟数据插件,可以帮助在开发过程中快速创建模拟 API。
首先,需要安装 vite-plugin-mock 插件。可以通过 npm 或 yarn 来安装:
npm install vite-plugin-mock --save-dev
# 或者
yarn add vite-plugin-mock --dev然后,在 Vite 配置文件(默认是 vite.config.js 或 vite.config.ts)中引入并使用这个插件:
import { defineConfig } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig({
plugins: [viteMockServe()]
})现在,可以在项目中创建模拟数据文件。默认情况下,vite-plugin-mock 会加载 mock 目录下的所有 .ts 文件。例如,可以在项目根目录下创建一个 mock/user.ts 文件:
export default [
{
url: '/api/user',
method: 'get',
response: () => {
return {
code: 0,
data: {
name: 'vben',
},
}
},
},
]在这个例子中,当访问 /api/user 时,服务器会返回一个 JSON 对象,这个对象包含一个 code 属性和一个 data 属性。
也可以通过传递一个选项对象来自定义 vite-plugin-mock 插件的行为。例如,可以更改模拟数据文件的目录或启用插件的热更新功能:
export default defineConfig({
plugins: [viteMockServe({
supportTs: false,
mockPath: 'mock',
localEnabled: process.env.NODE_ENV === 'development',
prodEnabled: process.env.NODE_ENV === 'production',
injectCode: `
import { setupProdMockServer } from '../mock/_createProductionServer';
setupProdMockServer();
`
})]
})更多详细的配置选项,你可以查看 vite-plugin-mock 的 GitHub 仓库。
2. 生成数据
vite-plugin-mock 插件可以配合数据生成库(如 faker.js 或 mockjs)来模拟生成数据。
以下是一个使用 mockjs 的例子:
首先,你需要安装 mockjs:
npm install mockjs --save-dev
# 或者
yarn add mockjs --dev然后,在你的模拟数据文件中,你可以使用 mockjs 来生成随机数据:
import Mock from 'mockjs'
export default [
{
url: '/api/user',
method: 'get',
response: () => {
return Mock.mock({
'code': 0,
'data|1-10': [{
'name': '@NAME',
'id|+1': 1,
'age|18-32': 1
}]
})
},
},
]在这个例子中,当你访问 /api/user 时,服务器会返回一个包含 1 到 10 个用户的数组,每个用户都有一个随机的名字、一个递增的 ID 和一个 18 到 32 之间的随机年龄。
同样的,你也可以使用 faker.js 来生成随机数据。首先安装 faker.js:
npm install faker --save-dev
# 或者
yarn add faker --dev然后在你的模拟数据文件中使用 faker.js:
import faker from 'faker'
export default [
{
url: '/api/user',
method: 'get',
response: () => {
return {
code: 0,
data: {
name: faker.name.findName(),
email: faker.internet.email(),
},
}
},
},
]在这个例子中,当你访问 /api/user 时,服务器会返回一个包含随机生成的名字和电子邮件的用户。
第六章:性能优化
一、构建性能
1. HMR
1)简介
HMR 的全称叫做 Hot Module Replacement,即模块热替换或者模块热更新。在计算机领域当中也有一个类似的概念叫热插拔,HMR 的作用其实一样,就是在页面模块更新的时候,直接把页面中发生变化的模块替换为新的模块,同时不会影响其它模块的正常运作。
HMR = 局部刷新 + 状态保存
2. HMR API
Vite 作为完整的构建工具,基于原生 ESM 模块规范实现了 HMR 系统,能在文件变更时侦测并局部更新。其 HMR API 遵循由 Vite、Snowpack、WMR 共同制定的通用 ESM HMR 规范。
直观地来看一看 HMR API 的类型定义:
interface ImportMeta {
readonly hot?: {
readonly data: any
accept(): void
accept(cb: (mod: any) => void): void
accept(dep: string, cb: (mod: any) => void): void
accept(deps: string[], cb: (mods: any[]) => void): void
prune(cb: () => void): void
dispose(cb: (data: any) => void): void
decline(): void
invalidate(): void
on(event: string, cb: (...args: any[]) => void): void
}
}import.meta 对象是现代浏览器原生的一个内置对象,Vite 所做的事情就是在这个对象上的 hot 属性中定义了一套完整的属性和方法。因此,在 Vite 当中,你就可以通过 import.meta.hot 来访问关于 HMR 的这些属性和方法,比如 import.meta.hot.accept()。
1)模块更新时逻辑
import.meta.hot 的 accept 方法用于确定 Vite 热更新的边界,即接受模块更新。它有三种用法:接受自身、某个子模块或多个子模块的更新。
接受自身更新
当模块接受自身的更新时,则当前模块会被认为 HMR 的边界。也就是说,除了当前模块,其他的模块均未受到任何影响。

// render.ts
if (import.meta.hot) {
import.meta.hot.accept((mod) => mod.render())
}接受依赖模块的更新

// main.ts
import { render } from './render';
import './state';
render();
if (import.meta.hot) {
import.meta.hot.accept('./render.ts', (newModule) => {
newModule.render();
})
}接受多个子模块的更新

// main.ts
import { render } from './render';
import { initState } from './state';
render();
initState();
if (import.meta.hot) {
import.meta.hot.accept(['./render.ts', './state.ts'], (modules) => {
console.log(modules);
})
}发现打印的 modules 值是数组。

// main.ts
import { render } from './render';
import { initState } from './state';
render();
initState();
if (import.meta.hot) {
import.meta.hot.accept(['./render.ts', './state.ts'], (modules) => {
// 自定义更新
const [renderModule, stateModule] = modules;
if (renderModule) {
renderModule.render();
}
if (stateModule) {
stateModule.initState();
}
})
}2)模块销毁时逻辑
import.meta.hot.dispose:在模块更新时,旧模块需要销毁时需要做的一些事情。
// state.ts
let timer: number | undefined;
if (import.meta.hot) {
import.meta.hot.dispose(() => {
if (timer) {
clearInterval(timer);
}
})
}
export function initState() {
let count = 0;
timer = setInterval(() => {
let countEle = document.getElementById('count');
countEle!.innerText = ++count + '';
}, 1000);
}3)共享数据
hot.data 属性:在不同的模块实例间共享一些数据。
let timer: number | undefined;
if (import.meta.hot) {
// 初始化 count
if (!import.meta.hot.data.count) {
import.meta.hot.data.count = 0;
}
import.meta.hot.dispose(() => {
if (timer) {
clearInterval(timer);
}
})
}
export function initState() {
const getAndIncCount = () => {
const data = import.meta.hot?.data || {
count: 0
};
data.count = data.count + 1;
return data.count;
};
timer = setInterval(() => {
let countEle = document.getElementById('count');
countEle!.innerText = getAndIncCount() + '';
}, 1000);
}4)其它方法
1. import.meta.hot.decline()
这个方法调用之后,相当于表示此模块不可热更新,当模块更新时会强制进行页面刷新。
2. import.meta.hot.invalidate()
这个方法就更简单了,只是用来强制刷新页面。
3. 自定义事件
你还可以通过 import.meta.hot.on 来监听 HMR 的自定义事件,内部有这么几个事件会自动触发:
vite:beforeUpdate当模块更新时触发;vite:beforeFullReload当即将重新刷新页面时触发;vite:beforePrune当不再需要的模块即将被剔除时触发;vite:error当发生错误时(例如,语法错误)触发。
如果你想自定义事件可以通过上节中提到的 handleHotUpdate 这个插件 Hook 来进行触发:
// 插件 Hook
handleHotUpdate({ server }) {
server.ws.send({
type: 'custom',
event: 'custom-update',
data: {}
})
return []
}
// 前端代码
import.meta.hot.on('custom-update', (data) => {
// 自定义更新逻辑
})第七章:分包
Vite 底层使用 Rolldown 打包,因此分包配置在 Rolldown 中进行的。
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rolldownOptions: {
output: {
codeSplitting: {}
}
}
}
})官方文档:
一、groups
用于代码拆分的组。
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
name: 'vendor',
test: /node_modules/,
},
],
}
}
}
}
})一些名词:
- bundle 指的是整体的打包产物,包含 JS 和各种静态资源。
- chunk 指的是打包后的 JS 文件,是 bundle 的子集。
- vendor 是指第三方包的打包产物,是一种特殊的 chunk。
1. Vite 默认拆包策略
在生产环境下 Vite 完全利用 Rollup 进行构建,因此拆包也是基于 Rollup 来完成的,但 Rollup 本身是一个专注 JS 库打包的工具,对应用构建的能力还尚为欠缺,Vite 正好是补足了 Rollup 应用构建的能力,在拆包能力这一块的扩展就是很好的体现。
.
├── assets
│ ├── Dynamic.3df51f7a.js // Async Chunk
│ ├── Dynamic.f2cbf023.css // Async Chunk (CSS)
│ ├── favicon.17e50649.svg // 静态资源
│ ├── index.1e236845.css // Initial Chunk (CSS)
│ ├── index.6773c114.js // Initial Chunk
│ └── vendor.ab4b9e1f.js // 第三方包产物 Chunk
└── index.html // 入口 HTML一方面 Vite 实现了自动 CSS 代码分割的能力。而另一方面, Vite 基于 Rollup 的 manualChunks API 实现了应用拆包的策略:
- 对于
Initital Chunk而言,业务代码和第三方包代码分别打包为单独的 chunk,在上述的例子中分别对应index.js和vendor.js。需要说明的是,这是 Vite 2.9 版本之前的做法,而在 Vite 2.9 及以后的版本,默认打包策略更加简单粗暴,将所有的 js 代码全部打包到index.js中。 - 对于
Async Chunk而言 ,动态 import 的代码会被拆分成单独的 chunk,如上述的 Dynacmic 组件。
Vite 默认拆包的优势在于实现了 CSS 代码分割与业务代码、第三方库代码、动态 import 模块代码三者的分离,但缺点也比较直观,第三方库的打包产物容易变得比较臃肿,上述例子中的 vendor.js 的大小已经达到 500 KB 以上,显然是有进一步拆包的优化空间的,这个时候我们就需要用到 Rollup 中的拆包 API —— manualChunks 了。
2. 自定义拆包策略
针对更细粒度的拆包,Vite 的底层打包引擎 Rollup 提供了 manualChunks,让我们能自定义拆包策略。
// vite.config.ts
export default {
build: {
rollupOptions: {
output: {
// manualChunks 配置
manualChunks: {},
},
}
},
}manualChunks 配置为对象
key 代表 chunk 的名称, value 为一个字符串数组,每一项为第三方包的包名。
// vite.config.ts
{
build: {
rollupOptions: {
output: {
// manualChunks 配置
manualChunks: {
// 将 React 相关库打包成单独的 chunk 中
'react-vendor': ['react', 'react-dom'],
// 将 Lodash 库的代码单独打包
'lodash': ['lodash-es'],
// 将组件库的代码打包
'library': ['antd', '@arco-design/web-react'],
},
},
}
},
}Vite 中的默认拆包策略是通过函数的方式来进行配置的。
ts// Vite 部分源码 function createMoveToVendorChunkFn(config: ResolvedConfig): GetManualChunk { const cache = new Map<string, boolean>() // 返回值为 manualChunks 的配置 return (id, { getModuleInfo }) => { // Vite 默认的配置逻辑其实很简单 // 主要是为了把 Initial Chunk 中的第三方包代码单独打包成`vendor.[hash].js` if ( id.includes('node_modules') && !isCSSRequest(id) && // 判断是否为 Initial Chunk staticImportedByEntry(id, getModuleInfo, cache) ) { return 'vendor' } } }
manualChunk 配置为函数
Rollup 会对每一个模块调用 manualChunks 函数,在 manualChunks 的函数入参中你可以拿到模块 id 及模块详情信息 ,经过一定的处理后返回 chunk 文件的名称 ,这样当前 id 代表的模块便会打包到你所指定的 chunk 文件中。
// vite.config.ts
manualChunks(id) {
if (id.includes('antd') || id.includes('@arco-design/web-react')) {
return 'library';
}
if (id.includes('lodash')) {
return 'lodash';
}
if (id.includes('react')) {
return 'react';
}
}3. 终极解决方案
安装插件
pnpm i vite-plugin-chunk-split -D然后在项目中引入并使用。
// vite.config.ts
import { chunkSplitPlugin } from 'vite-plugin-chunk-split';
export default {
chunkSplitPlugin({
// 指定拆包策略
customSplitting: {
// 1. 支持填包名。`react` 和 `react-dom` 会被打包到一个名为`render-vendor`的 chunk 里面 (包括它们的依赖,如 object-assign)
'react-vendor': ['react', 'react-dom'],
// 2. 支持填正则表达式。src 中 components 和 utils 下的所有文件被会被打包为`component-util`的 chunk 中
'components-util': [/src\/components/, /src\/utils/]
}
})
}具体用法参见:vite-plugin-chunk-split README
此外还有 vite-plugin-dynamic-chunk 插件也是如此。
第八章:插件开发
一、基础知识
1. 约定
如果插件不使用 Vite 特定的钩子,可以遵守 Rolldown 插件约定。
- Rolldown 插件应该有一个带
rolldown-plugin-前缀、语义清晰的名称。 - 在 package.json 的
keywords字段中包含rolldown-plugin和vite-plugin关键字。
对于使用了 Vite 特定的钩子:
- Vite 插件应该有一个带
vite-plugin-前缀、语义清晰的名称。 - 在 package.json 中包含
vite-plugin关键字。
如果插件只适用于特定的框架:
vite-plugin-vue-前缀作为 Vue 插件vite-plugin-react-前缀作为 React 插件
2. 钩子
Vite 插件扩展了 Rolldown 的插件接口,添加了一些 Vite 特有的选项。
1)通用钩子
在开发中,Vite 开发服务器会创建一个插件容器来调用 Rolldown 构建钩子,因此,Vite 插件就是 Rolldown 插件。
在服务器启动时被调用:
在每个传入模块请求时被调用:
在服务器关闭时被调用:
构建钩子中的 moduleParsed 在开发模式下不会调用,输出钩子除了 closeBundle 外,也不会调用。因为 Vite 是 No-bundle。
2)独有钩子
[1] config
在解析 Vite 配置前调用。可以动态地读取或修改 vite.config.js 里的配置。
注意:这里不能往配置文件里在添加插件,会无效。应该在编写的插件里直接返回一个插件数组。
export default function myPluginFactory() {
return [PluginA(), PluginB()]
}[2] configResolved
在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置。
[3] configureServer
用于配置开发服务器的钩子。
[4] configurePreviewServer
用于预览服务器。
[5] transformIndexHtml
因为 Vite 是以 index.html 为入口,所以 Vite 专门设计了这个钩子,让插件能够轻易地“篡改” index.html。
这个钩子会收到两个参数:
- html:当前 HTML 文件的纯文字字符串。
- 上下文上下文(Context):让你知道现在的情况。如果你在
npm run dev阶段,这里会提供整个 Vite 开发服务器的实例;如果在打包(build)阶段,它会提供所有打包出来的 JS/CSS 产物信息。
Vite 提供了非常灵活的修改方式,可以返回三种格式的数据:
- 第一种:纯文本硬替换。直接返回一个被你修改过的全新 HTML 字符串(比如你用正则把里面的
<!-- TILE -->替换成了我的网站)。 - 第二种(最常用):返回“标签描述对象”。你不必自己痛苦地用正则去查找
<head>或<body>的位置来拼接字符串。你可以直接告诉 Vite:“帮我注入一个特定标签”。javascript// Vite 会自动帮你把这段脚本完美地插入到 HTML 中(默认插在 <head> 内容的最底部) return [ { tag: 'script', // 什么标签? attrs: { src: 'xxx.js' },// 有什么属性? injectTo: 'body' // 插入到哪里?(head, body, head-prepend 等) } ] - 第三种:两者的结合。既局部修改原始 HTML 文字,又动态注入一些新标签,返回
{ html: newHtml, tags: myTags }即可。
顺序
Vite 自身也会对 index.html 进行大量的底层转换工作(比如识别你写的 <script type="module" src="...">,并建立热更新连接等)。 order 属性决定了你的插件要在 Vite 自身对 HTML 动刀的“前面”还是“后面”去发挥作用:
order: 'pre'(前置)在 Vite 处理 HTML 之前执行。如果你的插件通过这个钩子向 HTML 中动态塞入了一段
<script src="my-code.ts">源码级别的引用,你应该用pre。因为这样注入后,接下来的 Vite 解析工作就会看到这段脚本,并把它当做正常源码去打包、编译。不写 order(默认的中间状态)
等 Vite 刚刚完成对 HTML 的基础解析处理后生效,适合常规的修改。
order: 'post'(后置)在所有其他插件全部完工,HTML 临门一脚准备输出给浏览器(或保存为压缩构建产物)的最后关头应用。此时的 HTML 已经是完全体,适合做最终的扫尾工作(例如生成并向 body 底部挂载打包耗时信息、资源指纹校验码预加载等)。
[6] handleHotUpdate
HMR 有关的钩子。
二、开发插件
1. config - 目录别名
原理:config 钩子会给我们传递 vite.config.js 里的数据,刚好可以在此钩子里生成目录别名的配置来跟此配置合并,接下来交给 Vite 解析配置就行。
const fs = require("fs");
const path = require("path");
// ---------------------------------- 工具函数 ----------------------------------
function diffDirAndFile(dirFilesArr = [], basePath = "") {
const result = {
dirs: [],
files: []
}
dirFilesArr.forEach(name => {
const currentFileStat = fs.statSync(path.resolve(__dirname, basePath + "/" + name));
console.log("current file stat", name, currentFileStat.isDirectory());
const isDirectory = currentFileStat.isDirectory();
if (isDirectory) {
result.dirs.push(name);
} else {
result.files.push(name);
}
})
return result;
}
function getTotalSrcDir(keyName) {
const result = fs.readdirSync(path.resolve(__dirname, "../src"));
const diffResult = diffDirAndFile(result, "../src");
console.log("diffResult", diffResult);
const resolveAliasesObj = {}; // 放的就是一个一个的别名配置 @assets: xxx
diffResult.dirs.forEach(dirName => {
const key = `${keyName}${dirName}`;
const absPath = path.resolve(__dirname, "../src" + "/" + dirName);
resolveAliasesObj[key] = absPath;
})
return resolveAliasesObj;
}
// ---------------------------------- 插件 ----------------------------------
module.exports = ({
keyName = "@"
} = {}) => {
return {
config(config, env) {
// 只是传给你, 没有执行配置文件
console.log("config", config, env);
// config: 目前的一个配置对象
// production development serve build yarn dev yarn build
// env: mode: string, command: string
// config函数可以返回一个对象, 这个对象是部分的viteconfig配置【其实就是你想改的那一部分】
const resolveAliasesObj = getTotalSrcDir(keyName);
console.log("resolve", resolveAliasesObj);
return {
// 在这我们要返回一个resolve出去, 将src目录下的所有文件夹进行别名控制
// 读目录
resolve: {
alias: resolveAliasesObj
}
};
}
}
}2. transformIndexHtml - 处理 HTML
原理:使用 transformIndexHtml 钩子来处理 index.html 文件。transformIndexHtml 在此例子中必须在最前面执行,因为有模板语法,其他插件先执行会报错。
module.exports = (options) => {
return {
// 转换html的
transformIndexHtml: {
enforce: "pre", // 将插件的执行时机提前, 提前到 Vite 的内置插件之前执行
transform: (html, ctx) => {
return html.replace(/<%= title %>/g, options.inject.data.title);
},
},
};
};3. configureServer - fake 数据
原理:发起网络请求时,若使用 fetch / xhr 不要写 http:// 完整路径,只写请求路径,浏览器就会拼接上地址栏的路径发起网络请求。刚好到达 Vite 开发服务器,因此可以在 configureServer 钩子中对 Vite 开发服务器做增强(编写中间件)。
@/mock/index.js
const mockJS = require("mockjs");
const userList = mockJS.mock({
"data|100": [{ // 生成一个对象, 里面有一个data属性, 值是数组, 数组里面有100个对象, 每个对象包含的属性如下
name: "@cname", // 生成不同的中文名
"id|+1": 1, // id从1开始, 每次加一
time: "@time", // 时间
date: "@date" // 日期
}]
})
module.exports = [
{
method: "post",
url: "/api/users",
response: ({ body }) => {
return {
code: 200,
msg: "success",
data: userList
};
}
},
]编写插件:
const fs = require("fs");
const path = require("path");
export default (options) => {
// 做的最主要的事情就是拦截http请求
// 当我们使用fetch或者axios去请求的
// axios baseUrl // 请求地址
// 当打给本地的开发服务器的时候 viteserver服务器接管
return {
configureServer(server) {
// 服务器的相关配置
// req, 请求对象 --> 用户发过来的请求, 请求头请求体 url cookie
// res: 响应对象, - res.header
// next: 是否交给下一个中间件, 调用next方法会将处理结果交给下一个中间件
const mockStat = fs.statSync("mock");
const isDirectory = mockStat.isDirectory();
let mockResult = [];
if (isDirectory) {
// process.cwd() ---> 获取你当前的执行根目录
mockResult = require(path.resolve(process.cwd(), "mock/index.js"));
console.log("result", mockResult);
}
server.middlewares.use((req, res, next) => {
console.log("req", req.url);
// 看我们请求的地址在mockResult里有没有
const matchItem = mockResult.find(mockDescriptor => mockDescriptor.url === req.url);
console.log("matchItem", matchItem);
if (matchItem) {
console.log("进来了", );
const responseData = matchItem.response(req);
console.log("responseData", responseData);
// 强制设置一下他的请求头的格式为json
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(responseData)); // 设置请求头 异步的
} else {
next(); // 你不调用next 你又不响应 也会响应东西
}
}) // 插件 === middlewares
}
}
}
