静态资源

Nuxt 提供两种方式管理静态资源。

Nuxt 使用两个目录来处理样式表、字体、图片等静态资源。

  • public/ 目录的内容会原样挂载在服务器根路径下。
  • app/assets/ 目录按约定存放需要构建工具(Vite 或 webpack)处理的资源。

Public 目录

public/ 目录用作静态资源的公共服务器,文件通过应用的指定 URL 公开访问。

你可以在应用代码或浏览器中通过根路径 / 访问 public/ 目录下的文件。

示例

比如,引用 public/img/ 目录下的图片文件,它的静态 URL 是 /img/nuxt.png:

<template>
  <img
    src="/img/nuxt.png"
    alt="发现 Nuxt"
  >
</template>

Assets 目录

Nuxt 使用 Vite(默认)或 webpack 来构建和打包应用。这些构建工具的主要功能是处理 JavaScript 文件,但可以通过 Vite 插件或 webpack loader扩展来处理其他类型的资源,比如样式表、字体或 SVG。这一步会转换原始文件,主要是为了优化性能或缓存(比如样式表压缩或浏览器缓存失效)。

按约定,Nuxt 使用 app/assets/ 目录存放这些文件,但这个目录不会被自动扫描,你可以用任何其他名字。

在应用代码中,你可以通过 ~/assets/ 路径引用 app/assets/ 目录下的文件。

示例

比如,引用一个会被构建工具处理的图片文件(前提是配置了对应的文件扩展名处理规则):

<template>
  <img
    src="~/assets/img/nuxt.png"
    alt="发现 Nuxt"
  >
</template>

Nuxt 不会把 app/assets/ 目录下的文件通过 /assets/my-file.png 这样的静态 URL 提供。如果你需要静态 URL,请使用 public/ 目录。

静态 vs. 动态 src

当 src 在模板中是静态字符串字面量时,构建工具会将其重写为运行时辅助函数来解析最终 URL。像 /img/nuxt.png 这样的公共路径会被包裹,使得页面渲染时应用你的 app.baseURL;而像 ~/assets/img/nuxt.png 这样的打包路径则会变成一个 import,解析到带 hash 的输出文件。

<template>
  <!-- 静态路径会被重写:运行时应用 app.baseURL,打包文件带 hash。 -->
  <img src="/img/nuxt.png">
  <img src="~/assets/img/nuxt.png">
</template>

因为 app.baseURL 是在运行时应用的,所以即使 base URL 只在部署时才知道(比如通过 NUXT_APP_BASE_URL 设置),静态公共路径也能正常工作,无论文件是否被构建工具处理。这种解析只发生在构建工具能看到的字面路径上。

如果绑定的 :src 值是在运行时拼接的,构建工具对此是不可见的,所以不会有任何重写。字符串会原样使用:

<template>
  <!-- 这样不行:路径是运行时构建的,Vite 永远不会把它识别为 import。 -->
  <img :src="`~/assets/img/${name}.png`">
</template>

因此,运行时构建的公共路径如 /img/${name}.png 不会自动加上 app.baseURL 前缀。如果你的应用部署在域名子路径下,需要自己用 useRuntimeConfig().app.baseURL 加上前缀(比如通过 joinURL)。

下面介绍路径只在运行时知道的情况下如何处理。

公共资源

如果文件不需要处理或 hash,放在 public/ 目录下,通过 URL 引用:

<script setup lang="ts">
const props = defineProps<{
  name: string
}>()

const imageUrl = computed(() => `/img/${props.name}.png`)
</script>

<template>
  <img
    :src="imageUrl"
    :alt="props.name"
  >
</template>

public/ 目录下的文件保持原始文件名。

用 Vite 打包资源

以下方法是 Vite(Nuxt 默认构建工具)特有的。

当可能的文件已知时,显式列出它们的 import:

<script setup lang="ts">
const props = defineProps<{
  theme: 'light' | 'dark'
}>()

const logos = {
  light: () => import('./assets/img/logo-light.png?url'),
  dark: () => import('./assets/img/logo-dark.png?url'),
}

const logoUrl = (await logos[props.theme]()).default
</script>

<template>
  <img
    :src="logoUrl"
    alt="Nuxt"
  >
</template>

每个 import 都有字面路径,所以 Vite 在构建时能找到这两个文件,运行时只加载选中的模块。

当很多文件共享同一目录和扩展名时,使用变量动态 import,而不是逐个列出:

async function getImageUrl (name: string) {
  const image = await import(`./assets/img/${name}.png?url`)
  return image.default
}

这个例子中只有文件名可以是动态的。目录和扩展名保持在 import 中,Vite 才能在构建时找到可能的文件。

对于更广泛的模式或可用文件的显式映射,使用 import.meta.glob:

const images = import.meta.glob<string>('./assets/img/*.{png,jpg,svg}', {
  query: '?url',
  import: 'default',
})

async function getImageUrl (name: string) {
  const load = images[`./assets/img/${name}.png`]

  if (!load) {
    throw new Error(`未知图片: ${name}`)
  }

  return await load()
}

Glob import 默认是懒加载的。如果 URL 需要同步可用,加上 eager: true:

const images = import.meta.glob<string>('./assets/img/*.{png,jpg,svg}', {
  query: '?url',
  import: 'default',
  eager: true,
})

每个匹配的资源仍然会包含在构建产物中。懒加载 import 按需加载每个匹配项,而 eager glob 会预先加载所有匹配项,可能增加初始 JavaScript 体积或将小资源内联。

在 SSR 标记中使用懒加载 import 的 URL 之前,一定要 await。Vite 的 new URL(..., import.meta.url) 模式在 SSR 下不工作。