Nuxt 自动导入不该靠自觉:一次组件和 Vue API import 的 ESLint 护栏

Nuxt 的自动导入很好用,但它有一个隐蔽问题:只要团队里有人继续手写 import,代码仍然能跑,review 也未必会注意到。久而久之,同一套组件既有自动导入名,又有相对路径 import;同一批 Vue API 既能直接用,又到处写 import { computed, ref } from 'vue'

这类问题单看一行不大。真正麻烦的是它会破坏项目的可理解性:组件路径一改,显式 import 跟着坏;目录层级重构后,旧的 deep import 会把子组件暴露面撑大;业务组件里堆满框架 API import,也会让读者误以为这些 API 不是项目约定的一部分。

这次给一个 Nuxt 项目补了两条 ESLint 护栏:

  • app/components 里的 Vue 组件,不再相对路径显式 import 另一个 .vue 组件。
  • app/pages 里的页面,不再显式 import app/components 下的 .vue 组件。
  • Nuxt 已经自动导入的常用 Vue 运行时 API,例如 computedrefwatchtoRefs,不再从 vue 做 value import。
  • import type { Ref, ComputedRef } from 'vue' 仍然允许,因为类型 import 不属于运行时自动导入。

这两条规则都先用 warn,不做 autofix。规则在 lint 阶段把「这里应该交给 Nuxt 自动导入」说清楚,具体修改仍交给开发者结合上下文判断。

Nuxt 自动导入把组件路径和 Vue API 变成框架约定,业务代码不需要再维护第二套 import 图

为什么要从 warn 开始

这种规则很适合先 warn,不适合一上来 error。

规则很重要,存量代码也通常不会一开始就干净。一次扫描可能扫出几十个、上百个 warning,其中有些属于当前模块,有些属于别人的模块,有些甚至是历史迁移中临时留下的边界写法。直接升成 error,会把所有人卡在同一个门口,最后很可能变成「为了过 lint 大批量机械修改」。

warn 更像一个探照灯。它先回答三个问题:

  1. 规则会扫出多少问题。
  2. 问题集中在哪些模块。
  3. 哪些可以本轮顺手改,哪些应该交给对应 owner。

这次第一次扫描后,新的两条规则一共留下 184 个 warning,其中 Vue API import 129 个、组件 import 55 个。分布最高的是账号、页面头部、个人身份、首页卡片和展示名这些区域。这个结果说明,规则确实能抓到问题,也说明它不适合在当前阶段直接变成 error。

所以策略是:先建立规则,再按风险拆开处理。Vue API import 属于机械清理,保留类型 import 后可以批量改;组件 import 要看导入名是否只在模板里使用,再把模板标签改成 Nuxt 自动导入名。chatroommessage 这类不是当前 owner 的模块先通过路径配置放过。

组件 import 规则怎么定边界

组件规则没有试图拦截所有组件 import。它只抓两个明确场景。

第一个场景是 app/components 内部相对路径 import .vue 文件:

// 不推荐
import Avatar from './Avatar/index.vue'

如果这个文件位于 app/components/common/Account/Overview/Contacts/Item/index.vue,Nuxt 本来会根据路径生成组件名。调用方可以直接在模板里写:

<CommonAccountOverviewContactsItemAvatar />

这样组件名来自路径语义,重构时也更容易看出公开使用面。

第二个场景是 app/pages 显式 import app/components 下的组件:

// 不推荐
import FeedSection from '~/components/home/FeedSection/index.vue'

页面如果要用全局组件,同样应该直接使用 Nuxt 自动导入名:

<HomeFeedSection />

规则会根据 import 的真实路径推导建议组件名,并把它写进 warning message。它不做 autofix,因为删除 import 后还要确认模板里是否已经使用正确组件名、测试环境是否有 Nuxt 组件自动导入 mock、以及这个 import 是否被 TypeScript 类型推导借用了。这些都需要人或 Agent 做一次语义 review。

规则只拦截应该交给 Nuxt 的 import;类型 import、普通工具 import 和非 owner 模块不会被误伤

Vue API import 规则只拦 value import

Vue API 规则也没有把 from 'vue' 一刀切掉。它只看 value import,并且只看配置名单里的 API。

需要提醒的是这种写法:

import { computed, ref, watch } from 'vue'

在 Nuxt app 代码里,computedrefwatch 这些运行时 API 已经自动导入,组件可以直接用:

const keyword = ref('')
const trimmedKeyword = computed(() => keyword.value.trim())

但类型 import 仍然应该保留:

import type { Ref } from 'vue'

这是一个很重要的边界。运行时代码可以由 Nuxt 自动导入,类型系统仍然需要明确来源。规则如果把类型 import 也拦掉,就会让开发者在 RefComputedRefMaybeRef 这类类型上绕路,最后反而降低可读性。

规则内部也保留了 names 配置。默认名单覆盖常用 Vue API;如果某个项目只希望约束一部分,或者后续 Nuxt 自动导入能力变化,可以通过 ESLint 配置调整,不需要改规则源码。

配置要留出 owner 边界

项目里经常会同时有多个模块 owner。静态规则如果没有 owner 边界,很容易把「这个模块现在不归我改」变成「我顺手改了别人代码」。这在多人分支、rebase 频繁、业务正在迁移时尤其危险。

这次配置里把 chatroommessage 先放过:

const AUTO_IMPORT_RULE_IGNORED_PATH_PATTERNS = [
  '^app/components/(chatroom|message)/',
  '^app/pages/(chatroom|message)(/|\\\\.)',
]

这些模块后续仍然可以整理,只是当前规则先服务当前 owner。等对应模块开始整理,再把它们从忽略名单里拿出来。这样规则可以先进入主干,后续治理也有抓手,不必等到全项目一次性清零。

这里还有一个小经验:这类名单最好配置化,不要硬编码到规则里。规则负责识别问题,项目配置负责决定当前扫描范围。两层分开后,规则可以复用,项目也能按 owner、路径、迁移阶段逐步推进。

为什么不做 autofix

很多 ESLint 规则可以 autofix,比如排序、空格、简单语法替换。但自动导入这类规则不太适合第一版就做 autofix。

原因有几个。

首先,删除组件 import 之后,模板里的组件名不一定已经对。比如原来 import 叫 Avatar,模板里写 <Avatar />;自动导入名可能是 <CommonAccountOverviewContactsItemAvatar />。这不是简单删一行能解决的。

其次,测试环境不一定有 Nuxt 自动导入上下文。Vue Test Utils 单测里,真实 Nuxt 会自动注册的组件,可能需要在 global.components 里手动补 stub 或真实组件。删除 import 后,测试也要一起看。

再次,有些 import 可能被用于类型推导,例如 InstanceType<typeof ChildComponent>。这类写法应该改成显式 expose interface,还是保留特殊 import,要结合组件契约判断,不能机械替换。

所以第一版 warning message 只给建议写法,不直接改代码。它告诉人和 Agent:「这行应该删,模板里应该用哪个 Nuxt 自动导入组件名」。真正改的时候仍然要看上下文。

存量代码怎么改

这类规则落地时,最好不要追求第一天清零。更稳的流程是:

  1. 规则先以 warn 进入。
  2. 全量扫一遍,按模块、owner 和风险分桶。
  3. 当前正在改的模块先顺手修。
  4. 每修一块,都补规则测试、组件测试或 source 级断言。
  5. 存量 warning 少到可以控制时,再考虑把核心范围升成 error。

自动导入护栏的落地流程:先 warn 建规则,再扫描、分桶、按 owner 小步治理

这次实际改法也是这样:先补规则和 RuleTester 测试,再扫全量 warning;随后先批量处理 Vue API value import,再逐个 review 组件 import 是否只用于模板。最终在当前配置范围内把新规则 warning 清到 0,非当前 owner 的 chatroommessage 仍然通过配置忽略,后续等 owner 明确后再推进。

这一步看起来慢,但它有两个好处:

  • 不会因为一条新规则把大量不相关业务文件卷进当前 diff。
  • 每个被修改的模块都能跟着跑自己的单测、类型检查和 lint,而不是只做一次大范围字符串替换。

规则本身也要有测试

ESLint 规则是代码,也会退化。尤其这种规则既要识别路径,又要识别 import 类型,还要给出建议文案,不能只靠手工扫一遍。

RuleTester 至少要覆盖这些情况:

  • app/components 里的相对 .vue import 会 warning。
  • app/pages 里从 ~/components/... import .vue 会 warning。
  • 普通 helper import 不 warning。
  • chatroommessage 这类配置忽略路径不 warning。
  • computedreftoRefs 等 Vue API value import 会 warning。
  • import type 不 warning。

这里有一个现实细节:如果测试环境没有 TypeScript parser,import type 的测试可能需要额外接入 parser,或者先把类型 import 的行为放在规则代码里处理、在后续规则测试环境完善时补上专门用例。测试要保护规则边界的长期可维护性,覆盖率数字只是附带结果。

总结

Nuxt 自动导入不是「少写几行 import」这么简单,它也是项目公开面的一部分。

如果团队决定按路径自动生成组件名、按 Nuxt 自动导入使用 Vue API,那就不要只靠口头约定。口头约定挡不住历史代码、复制粘贴和多 Agent 并行改动。更稳定的做法是:

  • 用 ESLint 把规则变成可见 warning。
  • warning message 里给出建议写法,而不是只说错了。
  • 不急着 autofix,先让人 review 真实上下文。
  • 通过配置保留 owner 边界,避免改到当前不负责的模块。
  • 给规则自己补测试,让护栏也被护栏保护。

等存量逐步清掉之后,再把核心目录从 warn 升到 error。到那时,这条规则就不只是“提醒”,而是项目结构长期稳定的一部分。