首页
/ Nuxt `app/composables/` 目录详解:Composable 自动导入、类型生成与文件扫描机制

Nuxt `app/composables/` 目录详解:Composable 自动导入、类型生成与文件扫描机制

2026-09-04 19:35:45作者:殷蕙予

本篇指南基于 Nuxt 官方文档 composables 目录说明 及其底层实现,讲清 app/composables/ 目录的完整工作方式:如何通过命名导出或默认导出定义可被自动导入的 composable、.nuxt/imports.d.ts 类型文件如何生成、Nuxt 只扫描目录顶层文件的原因,以及如何用 re-exportimports.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 的机制,如 refreactive。此外,响应式代码也不受该目录边界限制——你可以在应用中任何需要响应式的地方自由使用这些特性。

三、类型生成:.nuxt/imports.d.ts

在底层,Nuxt 会自动生成文件 .nuxt/imports.d.ts 来声明自动导入的类型。这一点可以从源码中得到直接印证:imports 模块 通过 addTypeTemplate 注册了三个类型模板:

  • imports.d.ts:由 unimport 的 toExports 生成,导出所有已收集的自动导入;
  • types/imports.d.ts:生成 declare global 全局类型声明,使 IDE 能识别「未导入即可用」的符号;当 imports.autoImportfalse 时,该模板只会写入一行注释,提示你改用显式导入;
  • 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()
}

其中 useNuxtAppuseBar 等均为自动导入符号。useNuxtApp 等框架内建 composables 并不是来自你的 app/composables/ 目录,而是由 Nuxt 内建的 preset 注入的:从 presets 定义 可以看到,useNuxtAppuseStateuseFetchuseAsyncData 等符号都来自 #app/nuxt#app/composables/state#app/composables/fetch 等框架内部模块路径,例如 #app/composables/asyncData 导出了 useAsyncDatauseLazyAsyncDatauseNuxtDatarefreshNuxtDataclearNuxtDatacreateUseAsyncData 等。这些 preset 在 module 默认配置 中通过 presets: defaultPresets 注入,defaultPresets 还包含完整的 Vue API preset(refcomputed、生命周期钩子等,见 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.tsapp/composables/useFoo.ts 会被查找导入;nested/utils.ts 会被忽略。

要让嵌套模块也支持自动导入,有两种方式:

方式一(推荐):从 app/composables/index.ts 重新导出你需要的 composable

// 使该导出启用自动导入
export { utils } from './nested/utils.ts'

方式二:配置扫描器包含嵌套目录,通过 nuxt.config.tsimports.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.mjsimports.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。框架函数(refcomputed 等 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 preparenuxt devnuxt build 生成;
  • 扫描仅限顶层,嵌套 composable 优先通过 app/composables/index.ts 重新导出,或使用 imports.dirs 配置 glob 扫描;
  • 自动导入管线(layer 扫描 → scanDirExportsimports:extend 钩子 → 类型模板 → TransformPlugin 注入)全部实现在 packages/nuxt/src/imports/module.ts,preset 定义在 packages/nuxt/src/imports/presets.ts,模块作者可借助 packages/kit/src/imports.ts 中的 addImports / addImportsDir / addImportsSources 扩展它。
登录后查看全文
热门项目推荐
相关项目推荐