Nuxt 静态资源处理指南:public/ 与 app/assets/ 目录选型及运行时路径解析
【免费下载链接】nuxtthe full-stack Vue framework项目地址: https://gitcode.com/GitHub_Trending/nu/nuxt
本篇基于 Nuxt 官方文档 assets 展开,讲清 Nuxt 中两套资产目录(public/与app/assets/)的职责边界、静态src与动态:src在构建期/运行期的不同解析行为,以及当资源路径只在运行时才确定时,如何用 Vite 的动态 import 与import.meta.glob正确拿到资源 URL。读完后你能直接做出资源目录选型,并掌握 SSR 环境下安全的动态资源引用写法。
两种资产目录总览
Nuxt 用两个目录处理样式表、字体、图片等资产:
| 目录 | 是否经过构建工具处理 | 访问方式 | 文件名 |
|---|---|---|---|
public/ | 否,原样拷贝 | 根 URL/下的静态 URL,如/img/nuxt.png | 保留原始文件名 |
app/assets/ | 是,由 Vite(默认)或 webpack 处理 | 代码中通过~/assets/路径引用,构建后解析为带哈希的输出文件 | 哈希化 |
二者的核心区别在于:public/是"静态服务器",app/assets/是"构建管线入口"。
Public 目录:原样提供的静态资源
public/目录被用作静态资源服务器,其中的文件在应用的可定义 URL 下按原始路径对外可用——无论是从浏览器访问还是从应用代码引用,都以根 URL/为起点。
例如,public/img/目录下的一张图片,可通过静态 URL/img/nuxt.png访问:
<template> <img src="/img/nuxt.png" alt="Discover Nuxt" > </template>从源码看,目录名的默认值来自层级配置:Nuxt 核心在构建缓存中按layer.config.dir?.public || 'public'定位每个层的 public 目录(见 cache.ts),因此public/只是约定名,可通过dir.public配置修改。需要特别说明的是:在 Vite 方案下,直接配置vite.publicDir是不受支持的,类型定义中该字段被标记为@deprecated且类型为never,并要求改设dir.public(见 config.ts 与 diagnostics.ts)。
另外,Nitro 在构建后还会检测 public 资产与静态页面路由的"撞名"问题:public-assets.ts 中的getAssetPathsForRoute会把路由映射为 Nitro 可服务的资产路径(如/docs/intro对应docs/intro与docs/intro/index.html),用于判断路由是否被 public 资产遮蔽。其行为(包括baseURL前缀的匹配规则)由 public-assets.test.ts 中的用例完整覆盖。
Assets 目录:交给构建工具处理
Nuxt 默认使用 Vite(亦可选 webpack)构建和打包应用。这些构建工具的主职是处理 JavaScript,但可通过插件(Vite)或 loader(webpack)扩展来处理样式表、字体、SVG 等其他资产。这一步会转换原始文件,主要目的是性能或缓存(如样式表压缩、通过内容哈希使浏览器缓存失效)。
按约定,这类文件存放在app/assets/目录,但要注意两点:
- 该目录没有自动扫描(auto-scan)功能——只有被代码显式引用的文件才会进入构建产物;
- 目录名本身可以随意取,
app/assets/只是约定。
在应用代码中,用~/assets/路径引用该目录下的文件:
<template> <img src="~/assets/img/nuxt.png" alt="Discover Nuxt" > </template>注意:Nuxt 不会把
app/assets/中的文件以静态 URL(如/assets/my-file.png)对外提供。如果你需要一个稳定的静态 URL,请把文件放到public/目录。
从源码结构看,客户端构建产物最终会写入 Nitro 的 public 输出目录:Vite 的环境插件 environments.ts 把客户端输出目录计算为join(useServerBuild(nuxt).output.publicDir(), nuxt.options.app.buildAssetsDir)——这正是~/assets/引用经过构建后"从文件系统路径变成带哈希的 URL"的落点。
静态 src 与动态 src:构建工具只能看到字面量
这是本文最关键的一个行为分界:只有模板里写死的字符串字面量src,才会被构建工具改写。
静态src:构建期改写,运行期补全 baseURL
当src是模板中的静态字符串字面量时,构建工具会把它改写为运行期辅助函数:
- public 路径(如
/img/nuxt.png)会被包裹,页面渲染时应用app.baseURL; - 打包路径(如
~/assets/img/nuxt.png)会被改写为 import,解析为带哈希的输出文件。
<template> <!-- 静态路径会被改写:app.baseURL 在运行期应用,打包文件会带哈希 --> <img src="/img/nuxt.png"> <img src="~/assets/img/nuxt.png"> </template>因为app.baseURL是在运行期应用的,所以静态 public 路径即使 baseURL 只在部署时才确定(例如通过NUXT_APP_BASE_URL注入)也能正确工作,且无论该文件是否经过构建处理。
动态:src:构建工具看不见,字符串原样使用
绑定值在运行时拼装的:src,对构建工具而言是"不透明"的,上面所有改写都不会发生,字符串按原样使用:
<template> <!-- 不工作:路径在运行时拼装,Vite 永远不会把它识别为 import --> <img :src="`~/assets/img/${name}.png`"> </template>因此,像/img/${name}.png这样在运行时拼装的 public 路径不会被自动加上app.baseURL前缀。如果应用部署在源站根路径之下,需要自己用useRuntimeConfig().app.baseURL手动拼接(例如借助 ufo 的joinURL帮助函数)。
路径只在运行时确定时怎么办
以下分两条路线给出文档中的完整解法。
路线一:public 资产 + 运行时 URL
文件无需处理或哈希时,放进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>注意部署在子路径下时需自行拼接 baseURL(见上一节)。
路线二:Vite 打包资产(Nuxt 默认构建器)
以下写法是Vite 特有的,前提是所有 import 表达式中都保留字面量路径,让 Vite 在构建期能"看见"这些文件。
候选文件已知时,显式列出每个 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(`Unknown image: ${name}`) } return await load() }glob import 默认是惰性的(lazy)。如果 URL 必须同步可用,加上eager: true:
const images = import.meta.glob<string>('./assets/img/*.{png,jpg,svg}', { query: '?url', import: 'default', eager: true, })两种模式的取舍:所有匹配到的资产都会进入构建产物;lazy import 按需加载每个匹配项,而 eager glob 会预先加载全部匹配项,可能增大初始 JavaScript 体积或内联小资产。
SSR 警告
警告:在把 lazy import 的 URL 用于服务端渲染的标记之前,先
await它。Vite 文档中的new URL(..., import.meta.url)模式不兼容 SSR,不要在服务端代码中使用。
选型小结
把文档的完整脉络浓缩为四条决策规则:
- 需要稳定静态 URL / 不经过构建→ 放
public/,按根 URL 引用;目录名可用dir.public调整,不要直接配置vite.publicDir(已被类型标记为不支持,见 config.ts)。 - 需要哈希缓存 / 压缩 / 打包处理→ 放
app/assets/(或任意自定义目录),通过~/assets/引用;该目录不会被自动扫描,也不对外暴露静态 URL。 src是字面量→ 交给构建工具自动改写,baseURL 在运行期自动生效,无需操心。- 路径运行时拼装→ public 资产自己拼 URL(含 baseURL 前缀);打包资产则必须让 import 表达式保留字面量目录与扩展名,按需选用显式 import 列表、变量动态 import 或
import.meta.glob,并保证在 SSR 渲染前完成await。
【免费下载链接】nuxtthe full-stack Vue framework项目地址: https://gitcode.com/GitHub_Trending/nu/nuxt
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考