Nuxt `app/composables/` 目录详解:Composable 自动导入、类型生成与文件扫描机制
本篇指南基于 Nuxt 官方文档 composables 目录说明 及其底层实现,讲清 app/composables/ 目录的完整工作方式:如何通过命名导出或默认导出定义可被自动导入的 composable、.nuxt/imports.d.ts 类型文件如何生成、Nuxt 只扫描目录顶层文件的原因,以及如何用 re-export 或 imports.dirs 配置扩展扫描范围。读完后,你能在 Nuxt 应用中正确组织 composable 代码,理解自动导入在构建管线中的真实调用链(扫描 → 收集 → 模板生成 → 转换注入),并在 TypeScript 报 Cannot find name 等错误时快速定位原因。
一、app/composables/ 目录的作用
Nuxt 约定 app/composables/ 目录用于存放 Vue composables(组合式函数)。得益于其约定式目录结构,Nuxt 会自动扫描该目录下的顶层文件,将其中导出的函数注册为自动导入(auto-imports) 候选:你在 .js、.ts、.vue 文件里可以直接调用这些函数,无需显式 import。
<script setup lang="ts">
const count = ref(1) // ref 由 Nuxt 自动导入
</script>
从源码结构看,自动导入由独立的 nuxt:imports 模块驱动(见 imports 模块入口),它基于 unimport 库完成「目录扫描 → 收集导出 → 生成类型模板 → 编译期注入 import」的完整管线。
与经典的全局声明不同,Nuxt 的自动导入只在实际被使用的生产代码中包含对应符号,同时保留完整类型、IDE 补全与提示能力(参见 Auto-imports 概念文档)。
二、两种定义 composable 的方式
方式 1:命名导出(推荐)
文件中的命名导出会以其导出名被自动导入:
export const useFoo = () => {
return useState('foo', () => 'bar')
}
方式 2:默认导出
默认导出会以「文件名去扩展名后的 camelCase」形式暴露:
// 将可用名暴露为 useFoo()(文件名去扩展名后的 camelCase)
export default function () {
return useState('foo', () => 'bar')
}
也就是说,use-foo.ts 的默认导出会以 useFoo() 的名称被自动导入。
使用自动导入的 composable
定义完成后,即可在 .js、.ts 和 .vue 文件里直接使用,无需任何 import 语句:
<script setup lang="ts">
const foo = useFoo()
</script>
<template>
<div>
{{ foo }}
</div>
</template>
重要说明:目录本身不提供额外响应式能力
app/composables/ 目录不会给你的代码提供任何额外的响应式能力。composable 内部的响应式完全依赖 Vue Composition API 的机制,如 ref 和 reactive。此外,响应式代码也不受该目录边界限制——你可以在应用中任何需要响应式的地方自由使用这些特性。
三、类型生成:.nuxt/imports.d.ts
在底层,Nuxt 会自动生成文件 .nuxt/imports.d.ts 来声明自动导入的类型。这一点可以从源码中得到直接印证:imports 模块 通过 addTypeTemplate 注册了三个类型模板:
imports.d.ts:由 unimport 的toExports生成,导出所有已收集的自动导入;types/imports.d.ts:生成declare global全局类型声明,使 IDE 能识别「未导入即可用」的符号;当imports.autoImport为false时,该模板只会写入一行注释,提示你改用显式导入;types/shared-imports.d.ts:仅收录 Nuxt 与 Nitro(服务端)两侧来源相同的共享导入,避免把某一侧独有的类型污染到shared/上下文。
注意:必须运行以下命令之一,Nuxt 才会生成这些类型文件:
如果你在没有运行 dev server 的情况下新建了一个 composable,TypeScript 会抛出诸如 Cannot find name 'useBar'. 的错误——因为类型模板尚未重新生成。从 imports 模块 的源码看,dev 模式下 Nuxt 注册了 builder:watch 钩子监听 composables 目录变化,一旦检测到目录下文件变动,就会调用 regenerateImports() 并刷新类型模板,因此 dev server 运行时错误会自动消失。
四、进阶用法示例
1. 嵌套 Composables
一个 composable 可以通过自动导入调用另一个 composable(它们之间互相 import 完全不需要手写):
export const useFoo = () => {
const nuxtApp = useNuxtApp()
const bar = useBar()
}
其中 useNuxtApp、useBar 等均为自动导入符号。useNuxtApp 等框架内建 composables 并不是来自你的 app/composables/ 目录,而是由 Nuxt 内建的 preset 注入的:从 presets 定义 可以看到,useNuxtApp、useState、useFetch、useAsyncData 等符号都来自 #app/nuxt、#app/composables/state、#app/composables/fetch 等框架内部模块路径,例如 #app/composables/asyncData 导出了 useAsyncData、useLazyAsyncData、useNuxtData、refreshNuxtData、clearNuxtData、createUseAsyncData 等。这些 preset 在 module 默认配置 中通过 presets: defaultPresets 注入,defaultPresets 还包含完整的 Vue API preset(ref、computed、生命周期钩子等,见 vuePreset)。
2. 访问插件注入
你可以在 composable 中访问 插件注入(plugin 通过 provide 挂载到 nuxtApp 上的辅助函数):
export const useHello = () => {
const nuxtApp = useNuxtApp()
return nuxtApp.$hello
}
五、文件扫描机制:只扫描顶层
Nuxt 只扫描 app/composables/ 目录的顶层文件。例如以下结构:
composables/
├── index.ts // 会被扫描
├── useFoo.ts // 会被扫描
└── nested/
└── utils.ts // 不会被扫描
只有 app/composables/index.ts 和 app/composables/useFoo.ts 会被查找导入;nested/utils.ts 会被忽略。
要让嵌套模块也支持自动导入,有两种方式:
方式一(推荐):从 app/composables/index.ts 重新导出你需要的 composable
// 使该导出启用自动导入
export { utils } from './nested/utils.ts'
方式二:配置扫描器包含嵌套目录,通过 nuxt.config.ts 的 imports.dirs 选项:
export default defineNuxtConfig({
imports: {
dirs: [
// 扫描顶层 composables
'~/composables',
// ... 或按特定名称和文件扩展名扫描嵌套一层的 composables
'~/composables/*/index.{ts,js,mjs,mts}',
// ... 或扫描给定目录下的所有 composables
'~/composables/**',
],
},
})
六、源码级深潜:自动导入管线是如何运转的
结合 imports 模块源码,可以把 app/composables/ 目录的自动导入过程拆成以下几步。
1. 扫描目录的组装(含 Layer 支持)
当 scan: true(默认值)时,模块会遍历所有 layer,为每个 layer 组装待扫描目录(见 源码 L55-L76):
composablesDirs.push(
resolve(layer.config.srcDir, 'composables'),
resolve(layer.config.srcDir, 'utils'),
resolve(layer.config.srcDir, 'types'),
resolve(layer.config.rootDir, layer.config.dir?.shared ?? 'shared', 'utils'),
resolve(layer.config.rootDir, layer.config.dir?.shared ?? 'shared', 'types'),
)
也就是说,composables/、utils/、types/、shared/utils/、shared/types/ 五类目录都会参与扫描,并且配置中 imports.dirs 里额外声明的 glob 目录也会被解析别名后加入。若某个 layer 通过自身配置设置了 imports.scan: false,则会跳过该 layer 的扫描。扫描结果随后经 imports:dirs 钩子交给各模块追加自定义目录,最终交给 unimport 的 scanDirExports() 提取每个文件的导出(见 regenerateImports)。
这里也解释了为什么「只扫描顶层」是合理设计:scanDirExports 按目录 + glob 工作,嵌套文件需要显式的 glob(如 ~/composables/**)才会被纳入,从而避免把深层工具函数意外暴露为全局符号。
2. 模块扩展点:imports:extend 钩子
每次重新收集导入时,Nuxt 都会调用 imports:extend 钩子,把当前导入列表交给所有已注册模块修改(见 源码 L169)。对于模块作者,Nuxt Kit 提供了三个便捷 API(见 kit/imports):
addImports(imports):追加自动导入项;addImportsDir(dirs, { prepend }):追加(或前置)扫描目录;addImportsSources(presets):追加 preset 来源。
此外源码中还有冲突检测:如果你的自定义导入与 Nuxt 内建 preset 符号同名且优先级不足,Nuxt 会发出 NUXT_B6002 诊断警告(见 源码 L168-L178),这解释了为什么自定义 composable 不应与 useFetch 等内建命名重名。
3. 热更新与 dev server 重启
dev 模式下有两层监听机制(见 源码 L84-L92 与 L196-L202):
- composables 相关目录内文件级变化:触发
regenerateImports(),增量刷新imports.mjs、imports.d.ts等模板; - composables 目录本身被创建或删除(
addDir/unlinkDir事件):直接触发 dev server 重启,因为目录列表结构发生了变化。
4. 编译期注入与 #imports 别名
自动导入最终通过构建插件 TransformPlugin 完成:对匹配 transform.include(默认为 buildDir 下的代码)的模块,用 ctx.injectImports(code, id, options) 把「用到但未导入」的符号改写为真实 import 语句(见 源码 L130-L137)。因此生产 bundle 中只会包含真正被使用的导入——这正是「只包含使用到的部分」的实现方式。
同时,Nuxt 生成了 imports.mjs 模板并注册 #imports 别名,你可以在需要时显式导入任意自动导入符号:
<script setup lang="ts">
import { computed, ref } from '#imports'
const count = ref(1)
const double = computed(() => count.value * 2)
</script>
5. 关闭或局部关闭自动导入
- 完全关闭:
imports.autoImport: false。关闭后不再自动注入 import,types/imports.d.ts只写入禁用注释,但仍可通过#imports显式导入(见 源码 L279-L291)。 - 仅关闭目录扫描:
imports.scan: false。框架函数(ref、computed等 preset 导入)仍然可用,但你在app/composables/、app/utils/中的自定义代码需要手动导入。注意:这会破坏 layer 体系的覆盖能力,跨 layer 使用时需显式导入。
七、关键配置项速查
结合 nuxt.config 配置参考 与 模块默认值:
| 配置项 | 默认值 | 说明 |
|---|---|---|
imports.autoImport |
true |
是否自动注入 import 语句;false 时仍需 #imports 显式导入 |
imports.scan |
true |
是否扫描 composables/、utils/ 等目录;false 时自定义 composable 需手动导入 |
imports.dirs |
[] |
额外扫描的目录,支持 glob(如 ~/composables/**) |
imports.presets |
内建 preset 集 | 自动导入的来源 preset(Vue API、Nuxt 内建 composables 等) |
imports.global |
false |
是否以全局方式声明 |
imports.transform.include |
buildDir 正则 | 自动导入转换插件作用的文件范围 |
八、小结
app/composables/目录是 Nuxt 自动导入体系的核心目录之一:顶层文件的命名导出按导出名、默认导出按文件名 camelCase 暴露;- 该目录只提供「自动导入」能力,不提供任何额外响应式能力,响应式仍由 Vue Composition API 承担;
- 类型声明依赖
.nuxt/imports.d.ts,需运行nuxt prepare、nuxt dev或nuxt build生成; - 扫描仅限顶层,嵌套 composable 优先通过
app/composables/index.ts重新导出,或使用imports.dirs配置 glob 扫描; - 自动导入管线(layer 扫描 →
scanDirExports→imports:extend钩子 → 类型模板 →TransformPlugin注入)全部实现在 packages/nuxt/src/imports/module.ts,preset 定义在 packages/nuxt/src/imports/presets.ts,模块作者可借助 packages/kit/src/imports.ts 中的addImports/addImportsDir/addImportsSources扩展它。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00