Skip to content

第12章 静态资源处理

Web 应用不仅仅由 JavaScript 和 CSS 构成。图片、字体、音视频、JSON 文件、纯文本等静态资源构成了应用的血肉。在原始的开发流程中,开发者需要手动管理这些资源的路径、大小优化和缓存策略——复制文件到正确的目录、为文件名添加 hash、配置 CDN 路径、决定哪些小图片应该内联为 Data URL 以减少 HTTP 请求。这些繁琐但重要的工作在 Vite 中被自动化了。

Vite 的静态资源处理系统让这一切变得透明而高效:你只需 import 一个图片文件,Vite 就会在开发时返回正确的 URL,在构建时自动决定是内联为 Data URL 还是生成带 hash 的独立文件。这种 "import 即使用" 的体验消除了资源管理的心智负担,让开发者专注于业务逻辑。

本章将深入 plugins/asset.ts,剖析 Vite 如何识别资源文件、处理不同的导入模式、基于大小阈值决定内联策略、生成 hash 文件名、管理资源清单,以及处理 public 目录中的静态文件。

本章要点

  • 理解 Vite 资源插件的注册与工作原理
  • 掌握 URL、raw、inline 三种资源导入模式的实现细节
  • 深入 assetsInlineLimit 的判定逻辑与回调扩展
  • 了解 hash 文件名生成与 _​_VITE_ASSET_​_ 占位符机制
  • 掌握 manifest 资源清单的生成与使用
  • 理解 public 目录与项目资源的处理差异

12.1 资源类型识别

默认资源类型

Vite 维护了一个详尽的默认资源文件扩展名列表,定义在 constants.ts 中。这个列表涵盖了 Web 开发中最常见的静态资源类型,从图片格式(包括现代的 AVIF 和 WebP)到音视频格式,再到字体和其他二进制文件:

typescript
// constants.ts
export const DEFAULT_ASSETS_RE: RegExp = new RegExp(
  `\\.(` +
    // 图片:涵盖了从传统 PNG/JPEG 到现代 AVIF/WebP 的所有常见格式
    'apng|bmp|gif|ico|cur|jpg|jpeg|jfif|pjpeg|pjp|png|svg|tif|tiff|webp|avif|' +
    // 媒体:音视频格式,包括字幕文件 VTT
    'mp4|webm|ogg|mp3|wav|flac|aac|opus|mov|m4a|vtt|' +
    // 字体:所有主流 Web 字体格式
    'woff2?|eot|ttf|otf|' +
    // 其他:Web 应用清单、PDF、纯文本
    'webmanifest|pdf|txt' +
  `)(\\?.*)?$`
)

export const DEFAULT_ASSETS_INLINE_LIMIT = 4096 // 4 KiB

这个正则表达式末尾的 (\\?.*)?$ 部分确保了带有查询参数的资源引用也能被正确识别。例如 logo.png?v=2icon.svg?inline 都会被匹配。

除了内置的资源类型列表,Vite 还提供了 assetsInclude 配置选项,允许用户扩展资源类型的识别范围。这对于使用非标准文件格式的项目(如 3D 模型文件 .glb、地理信息文件 .geojson 等)非常有用。

MIME 类型注册

正确的 MIME 类型对于浏览器正确渲染资源至关重要。Vite 使用 mrmime 库来查询文件的 MIME 类型,但这个库对某些常见类型的注册存在偏差。Vite 通过 registerCustomMime 函数进行修正,确保在开发服务器返回资源和构建时生成 Data URL 时使用最佳的 Content-Type:

typescript
export function registerCustomMime(): void {
  // ico 文件应使用 image/x-icon 而非 IANA 注册的 image/vnd.microsoft.icon
  // 这是因为 image/x-icon 有更好的浏览器兼容性
  mrmime.mimes['ico'] = 'image/x-icon'
  // cur 是光标文件,与 ico 共享相同的文件格式
  mrmime.mimes['cur'] = 'image/x-icon'
  // flac 无损音频格式
  mrmime.mimes['flac'] = 'audio/flac'
  // eot 是一种旧的嵌入式 OpenType 字体格式
  mrmime.mimes['eot'] = 'application/vnd.ms-fontobject'
}

关于 .ico 文件的 MIME 类型选择值得说明:虽然 IANA 正式注册的类型是 image/vnd.microsoft.icon,但 image/x-icon 在实践中有更广泛的浏览器支持,HTML5 Boilerplate 等知名项目也推荐使用后者。

12.2 资源插件架构

assetPlugin 是 Vite 资源处理的核心插件。它通过 resolveIdloadrenderChunkgenerateBundle 四个钩子覆盖了资源处理的完整生命周期。从开发者写下 import logo from './logo.png' 的那一刻起,到最终产物中出现正确的资源路径或内联 Data URL,每一步都由这个插件精心编排。

resolveId:资源识别门户

resolveId 钩子是资源处理管线的第一道关卡。它利用 Rolldown 的 filter 机制进行高效的预筛选——只有匹配资源模式的模块 ID 才会进入处理逻辑,其他模块在正则匹配阶段就被快速排除。这种过滤器设计对于大型项目非常重要,因为它避免了对每个模块 ID 调用 JavaScript 函数的开销:

typescript
resolveId: {
  filter: {
    id: [urlRE, DEFAULT_ASSETS_RE, .../* 用户自定义资源模式 */],
  },
  handler(id) {
    if (!config.assetsInclude(cleanUrl(id)) && !urlRE.test(id)) {
      return
    }
    // 处理 public 目录中的资源引用
    const publicFile = checkPublicFile(id, config)
    if (publicFile) {
      return id
    }
  },
},

对于 public 目录中的文件引用,resolveId 直接返回原始 ID,让后续的 load 钩子来处理路径转换。这是因为 public 目录中的文件不参与模块解析——它们的路径在最终产物中保持不变。

load:资源加载的核心逻辑

load 钩子是资源处理的灵魂。根据查询参数和运行环境的不同,它采取完全不同的处理策略。所有的资源最终都被转换为一个 JavaScript 模块,导出一个字符串——要么是 URL,要么是文件内容。这种统一的抽象使得资源可以像普通模块一样被 import、被 tree-shaking 和被代码分割:

typescript
load: {
  filter: {
    id: {
      include: [rawRE, urlRE, DEFAULT_ASSETS_RE, .../* 用户自定义 */],
      exclude: /^\0/, // 排除虚拟模块(以 \0 开头的 ID 是 Rollup 约定的虚拟模块标识)
    },
  },
  async handler(id) {
    // raw 模式:返回文件内容字符串
    if (rawRE.test(id)) {
      const file = checkPublicFile(id, config) || cleanUrl(id)
      this.addWatchFile(file)
      return {
        code: `export default ${JSON.stringify(await fsp.readFile(file, 'utf-8'))}`,
        moduleType: 'js',
      }
    }

    // URL 或默认模式
    if (!urlRE.test(id) && !config.assetsInclude(cleanUrl(id))) return

    id = removeUrlQuery(id)
    let url = await fileToUrl(this, id)

    // 开发模式下继承 HMR 时间戳,确保文件变更时浏览器重新请求
    if (!url.startsWith('data:') && this.environment.mode === 'dev') {
      const mod = this.environment.moduleGraph.getModuleById(id)
      if (mod && mod.lastHMRTimestamp > 0) {
        url = injectQuery(url, `t=${mod.lastHMRTimestamp}`)
      }
    }

    return {
      code: `export default ${JSON.stringify(encodeURIPath(url))}`,
      moduleSideEffects: config.command === 'build' && this.getModuleInfo(id)?.isEntry
        ? 'no-treeshake' : false,
      moduleType: 'js',
    }
  },
},

12.3 三种导入模式

Vite 为静态资源提供了三种导入模式,每种模式适用于不同的使用场景。开发者通过在导入路径上附加查询参数来选择模式。这种基于查询参数的模式选择是一个优雅的设计——它不需要额外的配置文件,意图直接表达在代码中,一目了然:

URL 模式(默认)

这是最常用的导入模式。导入一个资源文件时,Vite 返回该资源的 URL。在开发模式下这是开发服务器的路径(如 /src/assets/logo.png),在构建模式下则根据 assetsInlineLimit 阈值决定是返回 Data URL 还是输出文件的路径(如 /assets/logo-a1b2c3.png)。这种模式适用于需要在 JavaScript 中引用资源路径的场景,最典型的就是图片的 src 属性。

基于 VitePress 构建