核心功能
模块热替换(HMR)
模块热替换(Hot Module Replacement,HMR)是现代前端开发工具的核心特性之一。它允许在应用程序运行时替换、添加或删除模块,而无需完全刷新页面。
为什么 HMR 很重要?
在传统的开发流程中,每次修改代码都需要刷新页面来查看效果。这会导致:
- 应用状态丢失(如已填写的表单、当前的路由位置)
- 需要重新执行初始化逻辑
- 开发效率下降
HMR 解决了这些问题:修改代码后,只更新变化的模块,保留应用的其他状态。
Vite HMR 的优势
Vite 的 HMR 基于原生 ESM 实现,相比传统打包工具有显著优势:
传统打包器:HMR 更新需要重新打包受影响的 chunk,然后整个 chunk 被替换。当项目规模增大时,chunk 体积也会增大,更新速度变慢。
Vite:HMR 更新精确到单个模块。只需要编译变化的文件,然后精确替换对应的模块。更新速度不受项目大小影响,始终保持在毫秒级别。
HMR 工作原理
理解 HMR 的工作原理有助于你更好地使用和调试它。整个流程可以分为以下几个阶段:
阶段一:文件监听与变更检测
Vite 开发服务器启动时会通过 chokidar 库监听项目目录下的文件变化。当文件被修改、添加或删除时,chokidar 会触发相应的事件。
文件系统事件 → chokidar 监听 → Vite 服务器接收事件
阶段二:模块图更新
Vite 维护着一个模块依赖图,记录了每个模块的导入关系。当文件变化时,Vite 需要:
- 确定哪些模块受到影响(直接修改的模块 + 依赖它的模块)
- 判断这些模块是否支持 HMR(是否有
import.meta.hot.accept调用) - 如果不支持 HMR,则需要完整重载页面
变更文件 → 分析依赖图 → 确定更新范围 → 判断 HMR 支持
阶段三:WebSocket 通知
服务器通过 WebSocket 连接向浏览器推送更新消息。消息格式大致如下:
{
type: 'update',
updates: [
{
type: 'js-update',
path: '/src/components/Button.vue',
acceptedPath: '/src/components/Button.vue',
timestamp: 1634567890123
}
]
}
阶段四:浏览器处理更新
浏览器接收到更新消息后:
- 请求更新后的模块(带有时间戳参数防止缓存)
- 执行模块的 HMR 回调(如果有)
- 递归处理依赖模块的更新
- 触发
vite:afterUpdate事件
WebSocket 消息 → 模块请求 → 代码替换 → 回调执行 → 状态保留
关键点:为什么 Vite 的 HMR 更快?
传统打包器的 HMR 需要重新打包受影响的 chunk,而 chunk 往往包含多个模块。Vite 直接替换单个模块文件,更新粒度更细,速度自然更快。
框架 HMR 支持
Vite 为流行框架提供官方 HMR 集成,开箱即用:
| 框架 | 官方插件 | 特点 |
|---|---|---|
| Vue | @vitejs/plugin-vue | 单文件组件完整支持,保留组件状态 |
| React | @vitejs/plugin-react | Fast Refresh,保留组件状态和 hooks |
| React SWC | @vitejs/plugin-react-swc | 基于 SWC 编译,更快 |
| Preact | @prefresh/vite | 轻量级 HMR |
| Svelte | vite-plugin-svelte | 官方支持,热更新组件 |
对于这些框架,你不需要手动处理 HMR,插件会自动处理组件的更新和状态保留。
手动处理 HMR
在某些场景下,你可能需要手动控制 HMR 行为,比如开发库或处理特殊逻辑时。Vite 通过 import.meta.hot 对象暴露 HMR API。
基本条件判断
所有 HMR 代码都应该用条件判断包裹,确保生产环境时被 tree-shaking 移除:
if (import.meta.hot) {
// HMR 代码只在开发环境执行
import.meta.hot.accept()
}
接受自身更新
模块可以接受自身的热更新:
// counter.js
export let count = 0
export function increment() {
count++
}
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
if (newModule) {
// newModule 是更新后的模块
// 注意:count 的值不会自动更新,需要手动处理
console.log('count 模块已更新')
}
})
}
接受依赖更新
模块可以监听其依赖的更新,而不必重新加载自身:
import { render } from './render.js'
render()
if (import.meta.hot) {
import.meta.hot.accept('./render.js', (newRenderModule) => {
// render.js 更新时调用
// 使用新的渲染函数重新渲染
newRenderModule.render()
})
}
清理副作用
当模块被更新或移除时,可能需要清理之前创建的副作用:
let timer = setInterval(() => {
console.log('tick')
}, 1000)
if (import.meta.hot) {
import.meta.hot.dispose(() => {
// 模块更新前清理定时器
clearInterval(timer)
})
}
数据持久化
import.meta.hot.data 对象在模块的不同版本之间持久存在,可用于传递数据:
if (import.meta.hot) {
// 保存当前状态
if (!import.meta.hot.data.count) {
import.meta.hot.data.count = 0
}
// 更新后可以读取之前保存的状态
console.log('之前的计数:', import.meta.hot.data.count)
import.meta.hot.data.count++
}
强制失效
有时模块无法处理更新,需要强制让导入者重新加载:
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// 判断是否可以处理更新
if (cannotHandleUpdate(newModule)) {
// 强制让导入者重新加载
import.meta.hot.invalidate('无法处理此更新')
}
})
}
监听 HMR 事件
Vite 会自动派发以下 HMR 事件:
if (import.meta.hot) {
// 更新即将应用
import.meta.hot.on('vite:beforeUpdate', (data) => {
console.log('即将更新:', data)
})
// 更新已完成
import.meta.hot.on('vite:afterUpdate', (data) => {
console.log('更新完成:', data)
})
// 即将完整重载
import.meta.hot.on('vite:beforeFullReload', () => {
console.log('即将完整重载页面')
})
// 发生错误
import.meta.hot.on('vite:error', (error) => {
console.error('HMR 错误:', error)
})
// WebSocket 连接断开
import.meta.hot.on('vite:ws:disconnect', () => {
console.log('开发服务器连接断开')
})
// WebSocket 重新连接
import.meta.hot.on('vite:ws:connect', () => {
console.log('开发服务器已重新连接')
})
}
TypeScript 支持
Vite 原生支持 TypeScript,无需额外配置即可使用 .ts 文件。这是 Vite 开箱即用理念的重要体现。
仅转译,不类型检查
理解 Vite 处理 TypeScript 的方式很重要:Vite 使用 Oxc 转译 TypeScript,但不进行类型检查。
为什么要这样设计?
转译可以按文件进行。当浏览器请求一个 .ts 文件时,Vite 只需要将该文件转译为 JavaScript 即可。这与 Vite 的按需编译模型完美契合。
类型检查需要了解整个模块图。要检查一个文件的类型是否正确,编译器需要知道所有相关类型定义,这需要分析整个项目的依赖关系。这会显著拖慢开发速度。
Oxc 的 TypeScript 转译速度比 tsc 快 20-30 倍,这是 Vite 极速开发体验的重要来源。
推荐的类型检查方案
虽然 Vite 不在开发时进行类型检查,但你仍然需要类型检查来确保代码质量。以下是推荐的方案:
方案一:IDE 实时检查(推荐)
现代 IDE(如 VS Code、WebStorm)会在编辑时实时显示类型错误。这是最便捷的方式,不需要任何额外配置。
VS Code 会使用项目中的 TypeScript 版本进行类型检查,错误会直接显示在编辑器中。
方案二:构建时检查
在构建命令中添加类型检查:
{
"scripts": {
"build": "tsc --noEmit && vite build",
"type-check": "tsc --noEmit"
}
}
--noEmit 标志告诉 TypeScript 只进行类型检查,不生成输出文件。
方案三:开发时后台检查(使用插件)
使用 vite-plugin-checker 在开发时后台运行类型检查,错误会显示在页面上:
npm install -D vite-plugin-checker
// vite.config.js
import { defineConfig } from 'vite'
import checker from 'vite-plugin-checker'
export default defineConfig({
plugins: [
checker({
typescript: true,
// 也可以同时运行 ESLint
eslint: {
lintCommand: 'eslint "./src/**/*.{ts,tsx}"',
},
}),
],
})
这样可以在开发时获得实时的类型错误提示,而不影响 Vite 的启动速度。
内置 tsconfig paths 支持(Vite 8 新特性)
Vite 8 提供了内置的 TypeScript 路径别名解析支持。开发者可以通过设置 resolve.tsconfigPaths 为 true 来启用:
// vite.config.js
export default defineConfig({
resolve: {
tsconfigPaths: true,
},
})
启用后,Vite 会自动读取 tsconfig.json 中的 paths 配置,无需手动在 resolve.alias 中重复配置。这有一定的性能开销,因此默认不启用。
emitDecoratorMetadata 支持(Vite 8 新特性)
Vite 8 内置支持 emitDecoratorMetadata 选项,不再需要外部插件:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
TypeScript 编译器选项
Vite 有一些推荐的 TypeScript 编译器选项:
{
"compilerOptions": {
// 必须设置为 true
// esbuild 只执行转译,不支持需要类型信息的功能
// 这个选项确保每个文件可以独立转译
"isolatedModules": true,
// 对于 ES2022+ 默认为 true
// 与 TypeScript 4.3.2+ 的行为一致
"useDefineForClassFields": true,
// 跳过库类型检查
// 避免依赖包中的类型定义问题
"skipLibCheck": true,
// Vite 默认使用 esnext,允许使用最新的 JS 特性
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler"
}
}
target 配置说明
Vite 会忽略 tsconfig.json 中的 target 值,遵循与 esbuild 相同的行为。
- 开发环境:使用
esbuild.target选项指定,默认为esnext以实现最小化转译 - 生产构建:
build.target选项优先级高于esbuild.target
重要提示:如果 tsconfig.json 中的 target 不是 ESNext 或 ES2022 及更新版本,或者没有 tsconfig.json 文件,useDefineForClassFields 将默认为 false,这可能与默认的 esbuild.target: esnext 产生问题,因为它可能会转译为静态初始化块,而某些浏览器可能不支持。
因此,建议在 tsconfig.json 中将 target 设置为 ESNext 或 ES2022 及更新版本,或者显式设置 useDefineForClassFields 为 true。
其他影响构建结果的编译器选项
以下 tsconfig.json 选项会影响 Vite 的构建结果:
| 选项 | 说明 |
|---|---|
extends | 继承其他配置文件 |
importsNotUsedAsValues | 控制未使用的导入如何处理 |
preserveValueImports | 保留未使用的值导入 |
verbatimModuleSyntax | 精确控制模块语法(TS 5.0+) |
jsx、jsxFactory、jsxFragmentFactory | JSX 相关配置 |
jsxImportSource | JSX 导入源 |
experimentalDecorators | 实验性装饰器支持 |
alwaysStrict | 始终使用严格模式 |
关于 verbatimModuleSyntax:
TypeScript 5.0 引入了 verbatimModuleSyntax 选项,它替代了 importsNotUsedAsValues 和 preserveValueImports。启用后,TypeScript 会精确保留导入语句的写法,避免自动移除或转换。这对于确保构建结果与源代码一致很有帮助。
{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}
客户端类型支持
为了获得 Vite 特定功能的类型提示,需要添加 vite/client 类型:
{
"compilerOptions": {
"types": ["vite/client"]
}
}
或者在文件开头使用三斜线指令:
/// <reference types="vite/client" />
vite/client 提供以下类型定义:
- 资源导入的类型(如
.svg、.png文件) import.meta.env的类型import.meta.hotHMR API 的类型
自定义环境变量类型
为你的环境变量添加类型提示:
// vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_API_URL: string
readonly VITE_API_TIMEOUT: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
注意:不要在 vite-env.d.ts 中使用 import 语句,否则会破坏类型扩展:
// ❌ 错误:import 语句会导致文件被视为模块
import { SomeType } from './types'
interface ImportMetaEnv {
readonly VITE_DATA: SomeType
}
// ✅ 正确:直接使用内置类型或三斜线指令
interface ImportMetaEnv {
readonly VITE_DATA: string
}
CSS 处理
Vite 对 CSS 提供开箱即用的支持,并提供了灵活的配置选项。
基础 CSS
导入 .css 文件会自动将内容注入页面:
import './style.css'
Vite 会自动处理 CSS 中的路径引用。例如,url() 中的相对路径会被正确解析:
/* 开发时:/src/assets/logo.png */
/* 生产时:/assets/logo.[hash].png */
.container {
background: url('./assets/logo.png');
}
@import 内联与 URL 重定位
Vite 预配置了 postcss-import 来处理 CSS @import 内联。这意味着:
路径别名支持:Vite 的路径别名(如 @)在 CSS @import 中同样有效:
/* 使用别名导入 */
@import '@/styles/variables.css';
URL 自动重定位:所有 CSS url() 引用(即使导入的文件在不同目录)都会自动重定位以确保正确性:
/* src/components/button.css */
/* 即使此文件被其他目录的文件导入 */
/* 背景图片路径也会正确解析 */
.button {
background: url('../assets/icon.png');
}
对于 Sass 和 Less 文件,同样支持 @import 别名和 url() 重定位。但 Stylus 由于 API 限制不支持此功能。
现代 CSS:推荐原生特性
由于 Vite 只针对现代浏览器,推荐使用原生 CSS 特性配合 PostCSS 插件(如 postcss-nesting)来编写符合未来标准的 CSS,而不是依赖预处理器。
现代 CSS 原生支持嵌套:
/* 原生 CSS 嵌套(现代浏览器支持) */
.container {
padding: 20px;
&:hover {
background: blue;
}
.title {
font-size: 24px;
@media (min-width: 768px) {
font-size: 32px;
}
}
}
使用 PostCSS 插件实现更好的兼容性:
// postcss.config.js
export default {
plugins: {
'postcss-nesting': {}, // CSS 嵌套
'autoprefixer': {}, // 自动前缀
},
}
这种方式的优势:编写的 CSS 符合 CSS 工作组的标准草案,未来浏览器原生支持后可以直接移除 PostCSS 插件。
CSS 预处理器
如果需要更高级的特性,Vite 内置支持 Sass、Less、Stylus,只需安装对应的依赖即可:
# Sass(推荐 sass-embedded,性能更好)
npm install -D sass-embedded
# 或使用纯 JS 版本
npm install -D sass
# Less
npm install -D less
# Stylus
npm install -D stylus
在 Vue 单文件组件中使用:
<template>
<div class="container">
<h1 class="title">Hello</h1>
</div>
</template>
<style lang="scss">
$primary-color: #42b983;
.container {
padding: 20px;
.title {
color: $primary-color;
font-size: 24px;
}
}
</style>
在独立文件中使用:
// styles/main.scss
@import './variables.scss';
.container {
background: $bg-color;
}
import './styles/main.scss'
CSS Modules
CSS Modules 是一种 CSS 作用域隔离方案。以 .module.css 结尾的文件会被自动处理:
/* Button.module.css */
.button {
padding: 10px 20px;
border: none;
border-radius: 4px;
}
.primary {
background: #42b983;
color: white;
}
.secondary {
background: #f5f5f5;
color: #333;
}
import styles from './Button.module.css'
// 使用生成的类名
const button = document.createElement('button')
button.className = `${styles.button} ${styles.primary}`
CSS Modules 的类名会被转换为唯一的标识符,避免全局冲突。例如:
/* 编译前 */
.button { }
/* 编译后 */
.Button_button__xyz123 { }
配置 CSS Modules
// vite.config.js
export default defineConfig({
css: {
modules: {
// 类名格式:[name]__[local]__[hash:base64:5]
generateScopedName: '[name]__[local]__[hash:base64:5]',
// 启用 camelCase,可以使用 styles.primaryButton 而非 styles['primary-button']
localsConvention: 'camelCaseOnly',
// 行为模式:'local'(默认)或 'global'
scopeBehaviour: 'local',
},
},
})
Lightning CSS(实验性)
从 Vite 4.4 开始,Vite 实验性支持使用 Lightning CSS 处理 CSS。Lightning CSS 是一个用 Rust 编写的极快 CSS 处理器。
要启用 Lightning CSS,需要安装依赖并配置:
npm install -D lightningcss
// vite.config.js
export default defineConfig({
css: {
transformer: 'lightningcss',
lightningcss: {
targets: {
chrome: 80,
},
},
},
build: {
// 可选:使用 Lightning CSS 压缩 CSS
cssMinify: 'lightningcss',
},
})
启用后,CSS 文件将由 Lightning CSS 而不是 PostCSS 处理。注意:
- CSS Modules 配置需要使用
css.lightningcss.cssModules而不是css.modules - Lightning CSS 的目标浏览器配置独立于 JavaScript 的
build.target
CSS 内联与提取
使用 ?inline 查询参数阻止 CSS 自动注入页面:
// 普通 CSS 会被注入页面
import './style.css'
// 使用 ?inline 返回 CSS 字符串,不注入页面
import cssString from './style.css?inline'
console.log(cssString) // CSS 内容字符串
使用 ?no-inline 强制不内联(通常用于动态导入):
// 动态导入 CSS,不会阻塞渲染
const loadStyles = async () => {
await import('./heavy-styles.css?no-inline')
}
PostCSS
Vite 自动应用项目中的 PostCSS 配置。只需创建配置文件:
// postcss.config.js
export default {
plugins: {
// 自动添加浏览器前缀
'autoprefixer': {},
// 使用现代 CSS 特性
'postcss-preset-env': {
stage: 3,
features: {
'nesting-rules': true,
},
},
// 其他插件...
'cssnano': {}, // 压缩 CSS
},
}
Vite 也支持在 vite.config.js 中内联 PostCSS 配置:
export default defineConfig({
css: {
postcss: {
plugins: [
require('autoprefixer')(),
],
},
},
})
CSS 预处理器全局变量
在多个文件中共享变量是一个常见需求。Vite 提供了便捷的配置方式:
// vite.config.js
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
// 自动在每个 SCSS 文件前注入
additionalData: `@use "@/styles/variables.scss" as *;`,
},
less: {
// Less 配置
additionalData: `@import "@/styles/variables.less";`,
},
},
},
})
这样你可以在任何组件中直接使用这些变量,无需手动导入:
<style lang="scss">
/* variables.scss 中定义的变量直接可用 */
.container {
color: $primary-color;
}
</style>
CSS Source Maps
开发时启用 CSS source maps:
export default defineConfig({
css: {
devSourcemap: true,
},
})
静态资源处理
Vite 提供了智能的静态资源处理机制,让你可以轻松管理项目中的图片、字体等资源。
资源导入基础
导入静态资源会返回解析后的 URL:
import imgUrl from './img.png'
document.getElementById('hero-img').src = imgUrl
URL 的值取决于环境:
- 开发环境:
/src/img.png(原始路径) - 生产环境:
/assets/img.2d8efhg.png(带哈希的路径)
这与 Webpack 的 file-loader 行为类似,但更简洁。
自动检测的资源类型
Vite 自动将以下文件类型识别为静态资源:
- 图片:
.png、.jpg、.jpeg、.gif、.svg、.webp、.ico - 字体:
.woff、.woff2、.eot、.ttf、.otf - 媒体:
.mp4、.webm、.ogg、.mp3、.wav、.flac - 其他:
.pdf、.txt
可以通过 assetsInclude 配置扩展这个列表:
// vite.config.js
export default defineConfig({
assetsInclude: ['**/*.md'], // 将 .md 文件也视为静态资源
})
资源内联
小于 build.assetsInlineLimit(默认 4KB)的资源会被内联为 base64 data URL:
// 小于 4KB 的图片会被内联
import smallIcon from './icon.png'
console.log(smallIcon) // "data:image/png;base64,iVBORw0KGgo..."
// 大于 4KB 的图片返回 URL
import largeImage from './photo.png'
console.log(largeImage) // "/assets/photo.abc123.png"
这可以减少 HTTP 请求数量,提升页面加载性能。
调整内联阈值:
export default defineConfig({
build: {
assetsInlineLimit: 8192, // 提高到 8KB
},
})
查询参数修饰符
Vite 提供了几个查询参数来控制资源处理方式:
?url:显式作为 URL 导入
// 无论文件大小,都返回 URL
import workletUrl from 'extra-scalloped-border/worklet.js?url'
CSS.paintWorklet.addModule(workletUrl)
?raw:作为字符串导入
// 读取文件的原始内容
import shaderCode from './shader.glsl?raw'
console.log(shaderCode) // 文件的字符串内容
这对于导入着色器代码、markdown 文本等场景很有用。
?inline 和 ?no-inline:控制内联行为
// 强制内联为 base64
import inlineIcon from './icon.png?inline'
// 强制不内联,总是返回 URL
import urlImage from './large.png?no-inline'
?worker:导入为 Web Worker
// 作为独立 chunk
import Worker from './heavy-computation.js?worker'
const worker = new Worker()
// 内联为 base64
import InlineWorker from './worker.js?worker&inline'
const worker = new InlineWorker()
// Shared Worker
import SharedWorker from './worker.js?sharedworker'
const sharedWorker = new SharedWorker()
public 目录
public 目录用于存放不需要被构建处理的静态资源:
public/
├── favicon.ico
├── robots.txt
└── images/
└── logo.png
这些文件会:
- 开发时直接服务于根路径
/ - 构建时原样复制到输出目录
引用 public 目录的资源时使用绝对路径:
<img src="/images/logo.png" />
<link rel="icon" href="/favicon.ico" />
何时使用 public 目录?
robots.txt:搜索引擎爬虫配置favicon.ico:网站图标- 不需要哈希的静态资源
- 第三方库提供的静态文件
何时使用 src/assets 目录?
- 需要被构建处理的资源
- 需要哈希命名的资源
- 需要被代码动态导入的资源
使用 new URL()
Vite 支持使用 new URL(url, import.meta.url) 动态获取资源 URL:
// 静态路径
const imgUrl = new URL('./img.png', import.meta.url).href
document.getElementById('img').src = imgUrl
// 动态路径(Vite 会分析并生成对应的导入)
function getImageUrl(name) {
return new URL(`./images/${name}.png`, import.meta.url).href
}
// 使用
const url = getImageUrl('logo')
这种方式在开发环境无需 Vite 处理,因为浏览器原生支持 import.meta.url。
在生产构建时,Vite 会转换动态 URL 为具体的导入:
// 构建后
import __img0png from './images/img0.png'
import __img1png from './images/img1.png'
function getImageUrl(name) {
const modules = {
'./images/img0.png': __img0png,
'./images/img1png': __img1png,
}
return new URL(modules[`./images/${name}.png`], import.meta.url).href
}
注意:这种方式不支持 SSR,因为 import.meta.url 在浏览器和 Node.js 中语义不同。
CSS 中的资源引用
CSS 文件中的 url() 引用会被 Vite 处理:
.container {
background: url('./assets/bg.png'); /* 相对路径 */
font-family: url('./fonts/custom.woff'); /* 字体文件 */
}
路径解析规则与 JavaScript 导入相同:
- 相对路径:相对于当前 CSS 文件
- 绝对路径:相对于项目根目录
JSON 导入
JSON 文件可以直接导入,支持具名导入以实现 tree-shaking:
// 导入整个对象
import packageData from './package.json'
console.log(packageData.version)
// 具名导入(有助于 tree-shaking)
import { name, version } from './package.json'
console.log(name, version)
动态 JSON 导入
// 动态导入 JSON
async function loadConfig(lang) {
const config = await import(`./locales/${lang}.json`)
return config.default
}
Glob 导入
import.meta.glob 是 Vite 提供的强大功能,可以批量导入匹配模式的多个模块。这在处理大量文件时特别有用,比如自动注册组件、动态加载路由等。
基础用法
// 懒加载模式(返回 Promise)
const modules = import.meta.glob('./dir/*.js')
// 结果:
// {
// './dir/foo.js': () => import('./dir/foo.js'),
// './dir/bar.js': () => import('./dir/bar.js'),
// }
// 遍历并加载
for (const path in modules) {
modules[path]().then((module) => {
console.log(path, module)
})
}
懒加载模式下,模块只有在需要时才会被加载,适合实现按需加载。
立即加载
使用 { eager: true } 立即加载所有匹配的模块:
const modules = import.meta.glob('./dir/*.js', { eager: true })
// 结果:
// {
// './dir/foo.js': { default: ..., named: ... },
// './dir/bar.js': { default: ..., named: ... },
// }
// 直接使用,无需 await
for (const path in modules) {
console.log(path, modules[path].default)
}
具名导入
只导入特定的导出,减少打包体积:
const modules = import.meta.glob('./dir/*.js', {
import: 'setup', // 只导入 setup 导出
eager: true,
})
// 结果:
// {
// './dir/foo.js': fooSetup,
// './dir/bar.js': barSetup,
// }
多个匹配模式
// 匹配多个模式
const modules = import.meta.glob([
'./components/*.vue',
'./views/*.vue',
])
排除文件
使用 ! 前缀排除特定文件:
const modules = import.meta.glob([
'./dir/**/*.js',
'!**/test.js', // 排除测试文件
'!**/node_modules/**',
])
Glob 导入选项
interface ImportMetaGlobOptions {
// 是否立即加载,默认 false
eager?: boolean
// 只导入特定导出
import?: string
// 导入类型:'default' | 'url' | 'raw' | 'worker'
query?: Record<string, string> | string
// 基础路径
base?: string
// 是否生成类型声明文件(实验性)
exhaustive?: boolean
}
exhaustive 选项(实验性)
exhaustive 选项用于控制 TypeScript 类型生成的严格程度:
const modules = import.meta.glob('./dir/*.js', {
exhaustive: true,
})
当设置为 true 时,Vite 会根据 glob 模式生成精确的类型声明。这对于需要严格类型检查的项目非常有用。
注意:此功能目前是实验性的,可能会有变化。
### 自定义查询
你可以使用 `query` 选项为导入提供查询参数:
```javascript
// 导入 SVG 作为字符串
const moduleStrings = import.meta.glob('./dir/*.svg', {
query: '?raw',
eager: true,
})
// 导入 SVG 作为 URL
const moduleUrls = import.meta.glob('./dir/*.svg', {
query: '?url',
eager: true,
})
也可以传递自定义查询对象供其他插件使用:
const modules = import.meta.glob('./dir/*.js', {
query: { foo: 'bar', bar: true },
})
基础路径选项
使用 base 选项可以为导入指定基础路径:
const modules = import.meta.glob('./**/*.js', {
base: './base',
})
// 结果:
// {
// './dir/foo.js': () => import('./base/dir/foo.js'),
// './dir/bar.js': () => import('./base/dir/bar.js'),
// }
base 选项只能是相对于导入文件的目录路径,或相对于项目根目录的绝对路径。不支持别名和虚拟模块。
只有相对路径的 glob 才会被解释为相对于解析后的基础路径。所有生成的模块键都会被修改为相对于基础路径。
实际应用示例
自动注册组件
// 自动导入所有组件
const componentModules = import.meta.glob('./components/*.vue', { eager: true })
const components = {}
for (const path in componentModules) {
const name = path.match(/\/([^/]+)\.vue$/)[1]
components[name] = componentModules[path].default
}
// 注册到 Vue
app.use(components)
动态路由
// 懒加载所有路由组件
const routes = Object.keys(import.meta.glob('./views/**/*.vue')).map(path => {
const name = path.match(/\/views\/(.+)\.vue$/)[1]
return {
path: `/${name.toLowerCase()}`,
component: () => import.meta.glob('./views/**/*.vue')[path](),
}
})
Glob 导入的限制
- 模式必须是字面量字符串,不能使用变量
- 仅支持相对于项目根目录的路径
- 在 SSR 中,
import.meta.glob的行为可能不同
动态导入
Vite 支持动态导入变量路径:
const module = await import(`./dir/${name}.js`)
动态导入规则
为安全起见,动态导入必须满足以下规则:
- 必须以
./或../开头
// ✅ 有效
import(`./dir/${file}.js`)
// ❌ 无效 - 不允许绝对路径或裸导入
import(`/dir/${file}.js`)
import(`some-package/${file}.js`)
- 必须包含文件扩展名
// ✅ 有效
import(`./dir/${file}.js`)
// ❌ 无效 - 缺少扩展名
import(`./dir/${file}`)
- 同一目录的导入必须指定文件名模式
// ✅ 有效 - 有前缀或后缀模式
import(`./dir/prefix-${file}.js`)
import(`./dir/${file}-suffix.js`)
// ❌ 无效 - 同一目录无模式
import(`./${file}.js`)
动态导入的实际应用
按需加载模块
async function loadModule(type) {
switch (type) {
case 'editor':
return await import('./modules/editor.js')
case 'chart':
return await import('./modules/chart.js')
default:
return await import('./modules/default.js')
}
}
国际化
async function loadLocale(lang) {
const messages = await import(`./locales/${lang}.json`)
i18n.setLocaleMessage(lang, messages.default)
}
WebAssembly
Vite 支持导入预编译的 .wasm 文件。使用 ?init 查询参数导入时,默认导出会返回一个初始化函数,该函数返回一个解析为 WebAssembly.Instance 的 Promise。
基本用法
import init from './example.wasm?init'
init().then((instance) => {
// 使用导出的方法
instance.exports.main()
})
初始化函数也可以接收一个导入对象,传递给 WebAssembly.instantiate 作为第二个参数:
init({
env: {
memory: new WebAssembly.Memory({ initial: 256 }),
table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
},
}).then((instance) => {
instance.exports.main()
})
访问 WebAssembly Module 对象
如果需要访问 Module 对象(例如多次实例化),可以使用显式的 URL 导入,然后手动执行实例化:
import wasmUrl from 'foo.wasm?url'
const loadModule = async () => {
const response = fetch(wasmUrl)
const { instance, module } = await WebAssembly.instantiateStreaming(response)
// 可以使用 module 对象多次实例化
const anotherInstance = await WebAssembly.instantiate(module)
}
生产构建行为
在生产构建中:
- 小于
assetInlineLimit的.wasm文件会被内联为 base64 字符串 - 较大的文件会被视为静态资源按需获取
SSR 中的注意事项
在 SSR 环境中,?init 导入中的 fetch() 可能会失败并抛出 TypeError: Invalid URL。这是因为在 Node.js 环境中 URL 解析方式不同。
Vite 8 新特性:.wasm?init 导入现在在 SSR 环境中原生支持,无需手动处理。这是 Vite 8 的重要改进之一。
对于 Vite 7 及更早版本,需要使用以下替代方案:
import wasmUrl from 'foo.wasm?url'
import { readFile } from 'node:fs/promises'
const loadModuleInSSR = async () => {
const buffer = await readFile('.' + wasmUrl)
const { instance } = await WebAssembly.instantiate(buffer, {
/* 导入对象 */
})
}
插件支持
ES Module Integration Proposal for WebAssembly 目前尚未原生支持。如果需要更高级的 WebAssembly 功能,可以使用 vite-plugin-wasm 等社区插件。
Web Workers
Web Workers 允许你在后台线程运行 JavaScript,不会阻塞主线程。这对于执行计算密集型任务非常有用。
构造函数方式(推荐)
使用 new URL() 语法创建 Worker:
// 标准 Worker
const worker = new Worker(new URL('./worker.js', import.meta.url))
// 模块 Worker(支持 import)
const moduleWorker = new Worker(
new URL('./worker.js', import.meta.url),
{ type: 'module' }
)
这种方式的优势:
- 语法标准,与浏览器原生 API 一致
- TypeScript 类型支持良好
- 不需要特殊的构建配置
查询后缀方式
使用 ?worker 后缀导入:
import MyWorker from './worker.js?worker'
const worker = new MyWorker()
// 发送消息
worker.postMessage({ type: 'start', data: someData })
// 接收消息
worker.onmessage = (e) => {
console.log('Worker 返回:', e.data)
}
Worker 选项
// 内联为 base64(不生成单独文件)
import InlineWorker from './worker.js?worker&inline'
// 只获取 URL,不实例化
import WorkerUrl from './worker.js?worker&url'
const worker = new Worker(WorkerUrl)
Shared Worker
共享 Worker 可以被多个浏览上下文(标签页、iframe)共享:
import MySharedWorker from './worker.js?sharedworker'
const sharedWorker = new MySharedWorker()
// 通过 port 通信
sharedWorker.port.onmessage = (e) => {
console.log('Shared Worker 消息:', e.data)
}
sharedWorker.port.postMessage('Hello')
Worker 内部访问 Vite 功能
在 Worker 中可以导入其他模块:
// worker.js
import { calculate } from './utils.js'
self.onmessage = (e) => {
const result = calculate(e.data)
self.postMessage(result)
}
实际应用示例
图片处理
// imageWorker.js
self.onmessage = async (e) => {
const { imageData, filter } = e.data
// 应用滤镜处理
const processed = applyFilter(imageData, filter)
self.postMessage(processed)
}
// main.js
const worker = new Worker(new URL('./imageWorker.js', import.meta.url))
function processImage(imageData, filter) {
return new Promise((resolve) => {
worker.onmessage = (e) => resolve(e.data)
worker.postMessage({ imageData, filter })
})
}
数据计算
// computationWorker.js
self.onmessage = (e) => {
const { numbers } = e.data
// 复杂计算
const result = heavyCalculation(numbers)
self.postMessage(result)
}
构建优化
Vite 在构建时自动应用多种优化策略,同时提供配置选项让你精细控制。
Full Bundle Mode(实验性)
Vite 8 引入了实验性的 Full Bundle Mode,在开发时也进行模块打包,类似于生产构建。这是 Vite 架构的一次重要演进。
为什么需要 Full Bundle Mode?
Vite 以非打包开发服务器(unbundled dev server)闻名,让浏览器直接加载 ES 模块。然而,随着项目规模增长,这种模式面临挑战:
- 开发/生产行为不一致:开发时的非打包 JavaScript 与生产环境的打包构建产生不同的运行时行为,可能导致只在生产环境出现的问题
- 大型项目性能下降:每个模块被单独获取,产生大量网络请求。在大型应用中可能需要处理数百甚至数千个独立请求
Full Bundle Mode 的优势:
- 开发服务器启动快 3 倍
- 完整重载快 40%
- 网络请求减少 10 倍
- 开发和生产行为一致,减少调试难度
如何启用:
// vite.config.js
export default defineConfig({
experimental: {
enableFullBundleMode: true,
},
})
此功能目前处于实验阶段,Vite 团队计划在收集反馈后将其设为默认选项。对于大型项目,这是一个值得尝试的性能优化方案。
自动优化
Vite 默认应用的优化:
CSS 代码分割:异步 chunk 中的 CSS 会被提取为单独文件,实现按需加载。
// 异步加载的组件,其 CSS 会单独提取
const AsyncComponent = () => import('./HeavyComponent.vue')
预加载指令:自动生成 <link rel="modulepreload">,让浏览器提前加载关键资源。
<!-- Vite 自动生成 -->
<link rel="modulepreload" href="/assets/vendor.js">
<link rel="modulepreload" href="/assets/main.js">
异步 chunk 加载优化:预加载共享的 chunk,减少网络往返。
// 入口 A 和 B 都依赖 C
// 当加载 A 时,Vite 会同时预加载 C
// 然后加载 B 时,C 已经在缓存中
手动代码分割
通过 manualChunks 控制代码分割策略:
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// 将特定库打包在一起
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus'],
'utils': ['lodash-es', 'dayjs'],
},
},
},
},
})
使用函数形式实现更灵活的分割:
manualChunks(id) {
// 将 node_modules 中的依赖单独打包
if (id.includes('node_modules')) {
// 按包名分组
const match = id.match(/node_modules\/(?:@([^/]+)\/([^/]+)|([^/]+))/)
if (match) {
const scope = match[1]
const name = match[2] || match[3]
return `vendor/${scope ? `${scope}-` : ''}${name}`
}
}
// 将组件单独打包
if (id.includes('/src/components/')) {
return 'components'
}
}
动态导入分割
动态导入会自动创建独立的 chunk:
// 路由懒加载
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'), // 独立 chunk
},
{
path: '/settings',
component: () => import('./views/Settings.vue'), // 独立 chunk
},
]
// 组件懒加载
const AsyncModal = defineAsyncComponent(() =>
import('./components/Modal.vue')
)
// 条件加载
if (featureEnabled) {
const module = await import('./feature.js')
module.init()
}
Chunk 命名控制
export default defineConfig({
build: {
rollupOptions: {
output: {
// 入口文件命名
entryFileNames: 'js/[name]-[hash].js',
// chunk 文件命名
chunkFileNames: 'js/[name]-[hash].js',
// 资源文件命名
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.')
const ext = info[info.length - 1]
if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(assetInfo.name)) {
return 'img/[name]-[hash][extname]'
}
if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
return 'fonts/[name]-[hash][extname]'
}
if (ext === 'css') {
return 'css/[name]-[hash][extname]'
}
return 'assets/[name]-[hash][extname]'
},
},
},
},
})
构建分析
使用插件分析构建产物:
npm install -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true, // 构建后自动打开报告
gzipSize: true, // 显示 gzip 大小
brotliSize: true, // 显示 brotli 大小
filename: 'stats.html',
}),
],
})
构建性能优化
提高构建速度的配置:
export default defineConfig({
build: {
// 禁用 source map 加速构建
sourcemap: false,
// 禁用压缩大小报告
reportCompressedSize: false,
// 使用 esbuild 压缩(比 terser 快)
minify: 'esbuild',
// 提高 chunk 大小警告阈值
chunkSizeWarningLimit: 1000,
},
})
内容安全策略(CSP)
部署内容安全策略(Content Security Policy)时,由于 Vite 的内部机制,需要设置某些指令或配置。
使用 Nonce
当设置 html.cspNonce 时,Vite 会为所有 <script> 和 <style> 标签以及样式表和模块预加载的 <link> 标签添加 nonce 属性。此外,Vite 还会注入一个 meta 标签:
<meta property="csp-nonce" nonce="PLACEHOLDER" />
// vite.config.js
export default defineConfig({
html: {
cspNonce: 'YOUR-NONCE-VALUE',
},
})
带有 property="csp-nonce" 的 meta 标签的 nonce 值会在开发和构建后被 Vite 使用。
安全警告:确保为每个请求替换占位符为唯一值。这对于防止绕过资源策略非常重要。
处理 data: URI
默认情况下,在构建过程中 Vite 会将小资源内联为 data URI。需要在相关指令(如 img-src、font-src)中允许 data:,或者通过设置 build.assetsInlineLimit: 0 来禁用内联。
安全警告:不要为 script-src 允许 data:,这将允许注入任意脚本。
依赖许可证生成
Vite 可以在构建时生成一个包含所有依赖许可证的文件,使用 build.license 选项配置:
// vite.config.js
export default defineConfig({
build: {
license: true,
},
})
这会在 .vite/license.md 生成一个许可证文件,内容类似:
# Licenses
The app bundles dependencies which contain the following licenses:
## dep-1 - 1.2.3 (MIT)
MIT License
...
## dep-2 - 4.5.6 (Apache-2.0)
Apache License 2.0
...
如果需要在不同的路径提供该文件,可以传递 { fileName: 'license.md' } 参数,这样文件会在 https://example.com/license.md 提供。
export default defineConfig({
build: {
license: {
fileName: 'license.md',
},
},
})
这对于需要在应用中展示和致谢所用依赖的应用非常有用。
写在最后
Vite 的核心功能覆盖了现代 Web 开发的主要需求:HMR 让开发更高效,TypeScript 和 CSS 支持开箱即用,静态资源处理智能便捷,WebAssembly 和 Web Workers 支持复杂场景,构建优化确保生产环境性能。
这些功能大多无需配置即可使用,让你能专注于业务开发。当需要定制行为时,可以查阅后续章节了解详细的配置选项。