跳到主要内容

核心功能

模块热替换(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 需要:

  1. 确定哪些模块受到影响(直接修改的模块 + 依赖它的模块)
  2. 判断这些模块是否支持 HMR(是否有 import.meta.hot.accept 调用)
  3. 如果不支持 HMR,则需要完整重载页面
变更文件 → 分析依赖图 → 确定更新范围 → 判断 HMR 支持

阶段三:WebSocket 通知

服务器通过 WebSocket 连接向浏览器推送更新消息。消息格式大致如下:

{
type: 'update',
updates: [
{
type: 'js-update',
path: '/src/components/Button.vue',
acceptedPath: '/src/components/Button.vue',
timestamp: 1634567890123
}
]
}

阶段四:浏览器处理更新

浏览器接收到更新消息后:

  1. 请求更新后的模块(带有时间戳参数防止缓存)
  2. 执行模块的 HMR 回调(如果有)
  3. 递归处理依赖模块的更新
  4. 触发 vite:afterUpdate 事件
WebSocket 消息 → 模块请求 → 代码替换 → 回调执行 → 状态保留

关键点:为什么 Vite 的 HMR 更快?

传统打包器的 HMR 需要重新打包受影响的 chunk,而 chunk 往往包含多个模块。Vite 直接替换单个模块文件,更新粒度更细,速度自然更快。

框架 HMR 支持

Vite 为流行框架提供官方 HMR 集成,开箱即用:

框架官方插件特点
Vue@vitejs/plugin-vue单文件组件完整支持,保留组件状态
React@vitejs/plugin-reactFast Refresh,保留组件状态和 hooks
React SWC@vitejs/plugin-react-swc基于 SWC 编译,更快
Preact@prefresh/vite轻量级 HMR
Sveltevite-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.tsconfigPathstrue 来启用:

// 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 不是 ESNextES2022 及更新版本,或者没有 tsconfig.json 文件,useDefineForClassFields 将默认为 false,这可能与默认的 esbuild.target: esnext 产生问题,因为它可能会转译为静态初始化块,而某些浏览器可能不支持。

因此,建议在 tsconfig.json 中将 target 设置为 ESNextES2022 及更新版本,或者显式设置 useDefineForClassFieldstrue

其他影响构建结果的编译器选项

以下 tsconfig.json 选项会影响 Vite 的构建结果:

选项说明
extends继承其他配置文件
importsNotUsedAsValues控制未使用的导入如何处理
preserveValueImports保留未使用的值导入
verbatimModuleSyntax精确控制模块语法(TS 5.0+)
jsxjsxFactoryjsxFragmentFactoryJSX 相关配置
jsxImportSourceJSX 导入源
experimentalDecorators实验性装饰器支持
alwaysStrict始终使用严格模式

关于 verbatimModuleSyntax

TypeScript 5.0 引入了 verbatimModuleSyntax 选项,它替代了 importsNotUsedAsValuespreserveValueImports。启用后,TypeScript 会精确保留导入语句的写法,避免自动移除或转换。这对于确保构建结果与源代码一致很有帮助。

{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}

客户端类型支持

为了获得 Vite 特定功能的类型提示,需要添加 vite/client 类型:

{
"compilerOptions": {
"types": ["vite/client"]
}
}

或者在文件开头使用三斜线指令:

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

vite/client 提供以下类型定义:

  • 资源导入的类型(如 .svg.png 文件)
  • import.meta.env 的类型
  • import.meta.hot HMR 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`)

动态导入规则

为安全起见,动态导入必须满足以下规则:

  1. 必须以 ./../ 开头
// ✅ 有效
import(`./dir/${file}.js`)

// ❌ 无效 - 不允许绝对路径或裸导入
import(`/dir/${file}.js`)
import(`some-package/${file}.js`)
  1. 必须包含文件扩展名
// ✅ 有效
import(`./dir/${file}.js`)

// ❌ 无效 - 缺少扩展名
import(`./dir/${file}`)
  1. 同一目录的导入必须指定文件名模式
// ✅ 有效 - 有前缀或后缀模式
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 模块。然而,随着项目规模增长,这种模式面临挑战:

  1. 开发/生产行为不一致:开发时的非打包 JavaScript 与生产环境的打包构建产生不同的运行时行为,可能导致只在生产环境出现的问题
  2. 大型项目性能下降:每个模块被单独获取,产生大量网络请求。在大型应用中可能需要处理数百甚至数千个独立请求

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-srcfont-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 支持复杂场景,构建优化确保生产环境性能。

这些功能大多无需配置即可使用,让你能专注于业务开发。当需要定制行为时,可以查阅后续章节了解详细的配置选项。