☰
Vite+Vue3自动引入:ESLint与TS报错全解决指南
2026/10/1 4:41:23 网站建设 项目流程

上周帮同事排查一个新搭建的 Vite + Vue3 + TS 项目,跑得好好的,npm run lint却爆了几十个红色报错:'ref' is not defined、'useRouter' is not defined。代码里明明没写过import啊,自动引入不是应该自动吗?这东西确实自动,但“自动“发生在构建链路里,ESLint 并不知道。这个问题几乎每个从零开始用 unplugin-auto-import 的人都会撞上,区别只是有人卡了半天,有人三十秒定位到根因。

这篇文章就把整个配置过程和报错解决链路完整展开,覆盖 unplugin-auto-import 的编译期原理、Vite 配置写法、ESLint no-undef 误报的修复、TS2304 类型报错的解法,再补上自定义 API 和 UI 库 resolver 的进阶玩法。适合刚接触这套组合拳、或者配置完被“两边报错”折磨的朋友直接参考。

1. 先搞清 unplugin-auto-import 的“自动”发生在哪一步

1.1 编译期注入不是运行时魔法

很多人第一次看到 unplugin-auto-import 的最大困惑是:它到底在哪里“变”出了 import?

它不是一个运行时库,不向浏览器注入任何全局变量。它的工作位置在 Vite 的 transform 阶段——也就是源码被编译成可运行代码之前。插件会拿到你写的模块源码,做一次 AST 分析,找出所有“被使用但没有导入”的标识符,然后判断这些标识符是否命中配置里的 API 映射表。如果命中了,就在源码顶部注入对应的import语句。

举个例子,你在.vue文件里写:

<script setup lang="ts"> const count = ref(0) const plusOne = computed(() => count.value + 1) </script>

经过 unplugin-auto-import 处理之后,实际的编译产物相当于变成了:

<script setup lang="ts"> import { ref, computed } from 'vue' const count = ref(0) const plusOne = computed(() => count.value + 1) </script>

这个转换发生在编译期,所以运行时完全没有额外的依赖开销,也不会影响 tree-shaking。用个不严谨但好懂的类比:就像你写作文的时候老师帮你把脚注自动补上了,但批改老师(ESLint)看的是你交上来的原文,没看到补脚注这一步,于是觉得你引用来源不明。

这也是为什么自动引入能帮项目省掉大量重复的 import 代码,尤其是组件多、页面多的中后台项目,一个文件顶部少则十几行、多则几十行的 import 清理掉之后,代码干净程度是肉眼可见的。

1.2 它和 unplugin-vue-components 是怎么分工的

这套体系里容易混的还有另一个插件:unplugin-vue-components。很多教程把它俩放在一起讲,但它们的职责完全不同。

unplugin-auto-import 处理的是 JS/TS 层面的标识符,比如ref、computed、watch、useRouter、useStore这类来自 Vue 生态或自定义工具库的函数。而 unplugin-vue-components 处理的是模板里的组件标签,比如你直接写<el-button>或<BaseCard>,它会在模板编译阶段帮你把对应组件 import 进来。

简单记:auto-import 管函数,components 管组件标签。大多数项目里这两个插件是成对出现的,但职责区分清楚之后,排查报错时就不至于一头雾水。

2. Vite + Vue3 + TS 下的基础配置:装依赖、写配置、生成声明文件

2.1 安装依赖与 vite.config.ts 插件注册

假设你已经用官方脚手架创建好项目(npm create vite@latest选择 Vue + TypeScript 模板),先把依赖装上:

npm install -D unplugin-auto-import # 如果你还需要组件自动按需引入,一起装 npm install -D unplugin-vue-components

然后到项目根目录的vite.config.ts里注册插件:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' export default defineConfig({ plugins: [ vue(), AutoImport({ imports: ['vue', 'vue-router', 'pinia'], dts: 'src/auto-imports.d.ts', eslintrc: { enabled: true, filepath: './.eslintrc-auto-import.json', }, }), Components({ dts: 'src/components.d.ts', }), ], })

这里先解释几个关键配置项,后面报错解决全靠它们:

  • imports:预设的自动引入来源。'vue'代表所有 Vue 核心 API,'vue-router'代表useRouter、useRoute这些,'pinia'代表defineStore、storeToRefs。如果你的项目用了 VueUse、vue-i18n 等,也可以继续往数组里加。

  • dts:生成类型声明文件的路径。TS 项目必须配,否则 TypeScript 语言服务不认识这些“凭空出现”的标识符。

  • eslintrc.enabled:这个开关打开后,插件会在编译时同步生成一个.eslintrc-auto-import.json文件,专门给 ESLint 用。后面第三节重点讲它。

  • filepath:生成文件的位置。默认生成在项目根目录,我的习惯是放在根目录,因为 ESLint 配置文件在根目录时引用路径最省事。

注册完之后先npm run dev启动一次,让插件把src/auto-imports.d.ts和.eslintrc-auto-import.json两个文件生成出来。如果文件没出现,先别急着往下配,大概率是配置没生效或路径写错了。

2.2 dts 类型声明文件的取舍与提交策略

src/auto-imports.d.ts生成之后,你的 IDE 会立刻认识ref、computed这些标识符,不再画红色波浪线。但这里有个容易被忽略的坑:这个文件只是被生成出来了,TypeScript 到底认不认它,取决于 tsconfig.json 的 include 范围。

拿默认脚手架生成的tsconfig.app.json举例,它通常长这样:

{ "compilerOptions": { // ... }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] }

只要 dts 文件生成在src目录内(比如src/auto-imports.d.ts),它天然就在 include 范围里。但如果你图省事,没配dts路径,插件默认会把文件生成在项目根目录,这时如果 tsconfig 的 include 只覆盖了src,类型声明就不会生效,TS 照样报“找不到名称 ref”。

我的建议是:dts路径显式写成src/auto-imports.d.ts,然后把生成的文件提交到 git 仓库。很多人不习惯提交自动生成的文件,但 auto-imports.d.ts 值得提交,原因很实际:新同事把仓库 clone 下来,还没跑 dev 的时候,类型提示和 CI 检查都依赖这个文件,如果没提交,第一遍 npm run typecheck 大概率挂。

3. ESLint 一片红的“未定义”报错:从根因到解决

3.1 为什么自动引入了 ESLint 还不认识

这是文章标题里点名要解决的问题。现象是:代码运行正常、IDE 的类型提示也正常,但一执行 lint 命令,满屏都是'ref' is not defined、'computed' is not defined。

根因其实很清晰:ESLint 的no-undef规则做的是静态检查,它只认当前文件里显式声明的变量和 import 语句。unplugin-auto-import 在编译期注入的 import,ESLint 根本看不到。它不知道ref是从哪来的,自然就当成“未定义变量”处理。

可以理解成 ESLint 是个极其较真的语文老师,作文里每个引用的出处都必须明明白白写在纸上,而自动引入恰好是那个“老师看不到的批注”。

不过这里有个细节要先说清楚:如果你用的是 Vue 3 的<script setup>,并且 ESLint 配置里有vue插件,可能会觉得 ref 应该被允许才对。但实际上no-undef还是会被触发,因为脚本核心规则并不区分“这是不是模板语法”“这是不是组合式 API 上下文”,它只识别当前文件里有没有定义来源。

3.2 eslintrc 选项:给 ESLint 生成一张“全局变量登记表”

unplugin-auto-import 官方显然知道这个痛点,所以早就做了配套能力:在插件配置里开启eslintrc.enabled之后,它会自动生成一个文件,本质内容就是一张“全局变量登记表”。

生成的.eslintrc-auto-import.json大致长这样(具体内容会根据你的 imports 配置动态变化):

{ "globals": { "computed": true, "ref": true, "watch": true, "useRouter": true, "useRoute": true, "defineStore": true } }

这些globals配置告诉 ESLint:这些标识符是全局可用的,不用显式导入也不会报no-undef。

但光有生成的配置文件还不够,你必须在 ESLint 的配置文件里把它“引”进来。如果你用的是传统.eslintrc.cjs(ESLint 8 及之前的主流写法),在extends数组里加一行即可:

module.exports = { extends: ['./.eslintrc-auto-import.json'], // 其他配置... }

如果你用的是 ESLint 9 的 flat config(eslint.config.js),则需要手动读取生成的 JSON 文件,把globals合入对应配置块:

import { readFileSync } from 'node:fs' const autoImportGlobals = JSON.parse( readFileSync(new URL('./.eslintrc-auto-import.json', import.meta.url), 'utf-8') ) export default [ { languageOptions: { globals: autoImportGlobals.globals, }, }, // 其他 flat config 配置... ]

这里用readFileSync而不是import ... with { type: 'json' },是为了兼容不同 Node 版本的模块解析行为,实测最稳。

配完之后重新执行 lint,之前一整片红基本就会消掉。

3.3 排查链路:配置完还报错时,按顺序检查这五步

如果你照着上面的方案配完,lint 还是报is not defined,别急着怀疑插件坏了。按下面这个顺序排查,绝大多数情况都能定位到问题。

第一,检查eslintrc.enabled和filepath是否正确。有人会在配置里写:

eslintrc: { enabled: true }

然后去项目根目录找.eslintrc-auto-import.json,找不到——因为你不指定filepath时,插件会生成到node_modules/.cache或者插件默认目录(不同版本行为有差异),不是你想当然的根目录。所以最好显式指定filepath: './.eslintrc-auto-import.json'。

第二,检查生成文件里的globals是否真的包含你报错的标识符。比如你只配了imports: ['vue'],却在代码里用了useRouter,那生成的 globals 里永远不会出现useRouter。Vue Router 的 API 需要单独预设'vue-router'。

第三,检查 ESLint 配置文件的加载顺序。.eslintrc.cjs里同时存在extends: ['plugin:vue/vue3-recommended', './.eslintrc-auto-import.json']时,生成的配置文件最好放在最后面,避免被后面的规则覆盖掉。

第四,检查是不是有.eslintignore或 ignorePatterns 把自动生成的 json 文件给忽略了。ESLint 不会 lint JSON 文件,但如果你把它加进 ignore,它的内容就不会被加载。

第五,检查 IDE 的 ESLint 服务是否缓存了旧的全局变量列表。VS Code 里按下Ctrl+Shift+P执行 “ESLint: Restart ESLint Server”,通常这一步能解决“配置明明对了还飘红”的玄学问题。这个细节我放在后面专门讲,因为实际开发中遇到最多。

4. TS 侧的第二道关卡:auto-imports.d.ts 与 TS2304

4.1 找到名字却过不了类型检查的原因

把 ESLint 解决掉之后,你可能会遇到另一类报错,来自 TypeScript 而不是 ESLint:Cannot find name 'ref',错误码是 TS2304。这个报错的语义和 ESLint 的 no-undef 类似,都是“这个标识符在当前作用域里找不到声明”。

区别在于,TS 不知道ref是什么,通常是因为类型声明文件没有被正确加载。unplugin-auto-import 的dts配置项生成的auto-imports.d.ts文件,就是专门用来解决这个问题的。文件内容大致长这样:

// 由 unplugin-auto-import 自动生成,不要手动修改 export {} declare global { const computed: typeof import('vue')['computed'] const ref: typeof import('vue')['ref'] const watch: typeof import('vue')['watch'] const useRoute: typeof import('vue-router')['useRoute'] const useRouter: typeof import('vue-router')['useRouter'] }

它通过declare global把这些标识符声明成全局变量,并且从对应模块里推导出精确的 TypeScript 类型。生成之后,TS 类型检查、IDE 自动补全、类型跳转就都正常了。

但前提是 tsconfig.json 的include里包含这个文件。如果你把dts写成了'auto-imports.d.ts'(默认根目录),而 tsconfig 的 include 只覆盖src,那么这个文件不会参与编译,TS2304 依旧会出现。

4.2 “dts 文件生成了但仍报错”的常见原因

我自己踩过几次坑,总结下来这类问题有几种典型原因。

第一种,文件路径和 tsconfig 的 include 不匹配。上面已经说过,不再重复。处理方式统一:把dts固定到src/auto-imports.d.ts,一劳永逸。

第二种,TypeScript Server 没重启。编辑器和 tsc 的进程会缓存模块图和文件列表,如果你改完 tsconfig 或新增了 d.ts 文件但没有触发文件监听,界面上的红色波浪线会一直残留。VS Code 里执行 “TypeScript: Restart TS Server” 可以强制重新加载。

第三种,版本兼容问题。如果你的 TypeScript 版本比较老,而 Vite 和插件的版本很新,生成的 d.ts 里可能用到了它不认识的语法。这种情况在升级项目依赖时比较常见,方案是统一把 TypeScript 升到与 Vite 5/6 匹配的版本,或者按插件 README 里标注的 peerDependencies 调整。

第四种,手动改动了生成的 d.ts 文件。记住:这个文件是编译时自动覆盖的,你手动加的任何代码都会在下次 dev/build 时被冲掉。如果发现文件内容和你预期不一样,不要改它,应该改配置文件里的 imports 或 resolvers。

5. 进阶配置:自定义 API、VueUse 与 UI 库 resolver

5.1 自定义 API:imports 不是只能写字符串预设

前文提到的imports预设是最省事的写法,但实际项目的工具函数往往藏在项目的utils目录里,比如formatDate、debounce这类工具函数。如果它们也能自动引入,代码会清爽很多。

unplugin-auto-import 的imports支持数组和对象两种形态,自定义 API 的写法是这样的:

AutoImport({ imports: [ 'vue', 'vue-router', 'pinia', { '@/utils/format': ['formatNumber', 'formatDate'], 'lodash-es': ['debounce', 'throttle'], '@/hooks/usePermission': [['usePermission', 'usePermission']], }, ], })

对象形式里,key是模块路径,value是需要自动引入的具名导出的名字列表。配置完成并重启 dev 后,你在任何文件里直接写formatNumber(...)或debounce(...),插件会自动补上对应 import,不需要手动维护。

但要注意路径别名问题。上面的@/utils/format依赖 vite 配置里的resolve.alias。如果你配了@指向src目录,插件内部会基于你给的目标路径去注入 import,编译器阶段拿到的是别名,构建阶段会正确解析。实测下来,避免相对路径地狱的效果很好。

还有一点提醒:自动引入的标识符尽量不要跟文件里手动声明的局部变量重名。插件会尝试做判断,如果源码里已经显式导入了同名标识符或存在同名局部声明,一般会跳过注入,但这种“隐形规则”不该成为你依赖的对象,命名时多留个心眼。

5.2 配合 unplugin-vue-components 实现组件和 API 全自动

中后台项目最常见的自动引入诉求,其实是 UI 库。Element Plus、Naive UI 这类组件库如果全量导入,包体积会比较难看;手动按需导入又很啰嗦。社区的标准方案是把 unplugin-auto-import 和 unplugin-vue-components 配合起来。

先安装组件库和 components 插件:

npm install element-plus npm install -D unplugin-vue-components

然后修改 vite.config.ts:

import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig({ plugins: [ vue(), AutoImport({ imports: ['vue', 'vue-router', 'pinia'], resolvers: [ElementPlusResolver()], dts: 'src/auto-imports.d.ts', eslintrc: { enabled: true, filepath: './.eslintrc-auto-import.json', }, }), Components({ resolvers: [ElementPlusResolver()], dts: 'src/components.d.ts', }), ], })

这里有两个容易混淆的点。

第一,Components插件的 resolver 管的是模板里的组件标签,比如<el-button>、<el-table>。你在模板里直接写el-button,它会自动 import 并只打包对应组件及其样式。

第二,AutoImport里的resolvers管的是命令式 API,比如ElMessage、ElNotification、ElMessageBox这几个经常在 JS 逻辑里用的东西。如果不配置 AutoImport 的 resolver,你在事件处理里直接写ElMessage.success(...),ESLint 就会报ElMessage is not defined。这个坑很多人踩过,因为模板里的组件已经自动引入了,下意识觉得 API 也应该自动存在。

如果你用 Naive UI,对应的 resolver 是NaiveUiResolver,配置套路完全一致。VueUse 更简单,直接把'@vueuse/core'加进imports数组,就能自动引入useDark、useStorage、useWindowSize这些函数。

6. 实践中的避坑清单和三个检查顺序

6.1 先查生成产物:没有文件等于没启动过

自动生成的.eslintrc-auto-import.json和src/auto-imports.d.ts,是这套配置的“体检报告”。如果 lint 或类型检查报错,第一件事永远是检查这两个文件是否存在、内容是否最新。

我见过不少新手配置完AutoImport后没有跑 dev,直接去执行 typecheck,然后一脸蒙:“配置明明写了,为什么还报错?”因为插件是在 Vite 启动时执行转换逻辑并生成文件的,没启动过就什么都没有。先跑一次 dev,让文件生成出来,再谈后续排查。

这两个文件还有一个值得养成的习惯:提交到仓库。前面说过 d.ts 要提交,.eslintrc-auto-import.json同样建议提交。否则团队其他成员 clone 后第一次跑 lint,可能因为生成文件的时机不对而误报。提交进去后,大家拿到的就是同一份“全局变量列表”。

6.2 ESLint 和 TypeScript 的“服务缓存”才是隐形杀手

配置看着全对,生成文件也都在,界面还是飘红——这种时候,90% 是编辑器服务缓存的问题。

VS Code 里同时装了 ESLint 和 Volar(Vue 官方插件)时,两个服务都可能缓存之前的诊断状态。我习惯的排查顺序是:

先执行Ctrl+Shift+P里的 “ESLint: Restart ESLint Server”,再执行 “TypeScript: Restart TS Server”。如果还不行,直接 “Developer: Reload Window” 重载窗口。这三个操作基本能解决九成以上的“配置正确但编辑器不认”问题。

另一类缓存问题出现在 CI 流程里。如果项目在 CI 上跑 lint 或 typecheck,要确保生成动作先于检查动作。最稳妥的做法是在package.json的lint和typecheck脚本之前加一个vite build或vite dev的产物生成步骤,或者干脆依赖已提交的生成文件,避免 CI 从零开始排查。

6.3 这个配置怎么带给团队其他成员

最后聊聊团队层面。自动引入在个人项目里很爽,但放到多人协作时,生成的配置文件如果不解释清楚,很容易被当成“乱七八糟的项目残留”。

我的做法是在项目根目录放一个.env.example风格的说明文档,或者在 README 里加一小节,写清楚这几件事:

  • .eslintrc-auto-import.json和src/auto-imports.d.ts是自动生成的,不要手动编辑。
  • 配置文件源头是vite.config.ts里的AutoImport和Components插件。
  • 新增全局 API 时,改imports或resolvers,然后重启 dev,让生成文件更新。
  • lint 报错先按第三节的排查链路走,不要强行写// eslint-disable-next-line。

这套配置在 Vite + Vue3 + TS 技术栈里已经相当成熟,只要理解了它的编译期机制和 ESLint/TS 的静态分析边界,剩下的就是照着实测过的模板抄一遍。遇到报错时按着“看生成文件、查路径配置、重启服务”的思路走,基本都能在几分钟内解决。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询