跳到正文

Vue script setup 中组件静默消失的原因:一次命名冲突排查(3):检测组件与指令的绑定冲突

Vue script setup 中组件静默消失的原因:一次命名冲突排查 · 1 · 2 · 第 3 篇(当前)

同一个 script setup 可以合法声明 PopupStatus 和 popupStatus,但模板中的 popup-status 可能先命中普通状态。编译器按照既定候选顺序生成代码,普通的未定义组件、未使用变量和作用域遮蔽检查因此未必报告。

这里集中讨论怎样检测组件与自定义指令的名字冲突。复现是组件与状态只差大小写,修复是让它们具有不同语义名称;编译器绑定分类和候选名顺序见顶层绑定怎样参与模板名字解析。

静态检查能不能发现

静态检查可以发现这类冲突,但现成规则和专门模拟编译器解析的规则,覆盖范围不同。

eslint-plugin-vue 可以检查 .vue 文件里的 <template> 和 <script>,具备同时看模板和脚本的基础能力,并不是只能查普通 TypeScript 文件。

与组件定义、使用和变量遮蔽相关的规则包括:

vue/no-undef-components 会检查模板里使用了但没有在 <script setup> 或 Options API components 里定义的组件。

vue/no-unused-components 会检查注册后没有在模板里使用的组件。

vue/no-template-shadow 会检查模板局部变量遮蔽外层变量,例如 v-for 变量名和外层作用域冲突。

但这些规则并不能直接抓出本文这个问题。

可以用最小样例验证。这个例子里,PopupStatus 是组件,popupStatus 是状态:

<template>
  <popup-status :message="popupStatus.message" :type="popupStatus.type" />
</template>

<script setup lang="ts">
import PopupStatus from './popup-status.vue';

const popupStatus = {
  message: 'ok',
  type: 'success'
};
</script>

即使打开这些规则:

rules: {
  'vue/no-undef-components': 'error',
  'vue/no-unused-components': 'error',
  'vue/no-template-shadow': 'error',
  'vue/no-undef-properties': 'error',
  'vue/no-unused-properties': ['error', { groups: ['setup'] }],
  'vue/no-dupe-keys': 'error'
}

实际结果也不会报 PopupStatus 和 popupStatus 冲突。

这几条规则各自检查的条件都没有被触发:

  • <popup-status> 能找到一个叫 PopupStatus 的导入组件,所以它不是未定义组件。
  • 从模板使用情况看,PopupStatus 不是未使用组件。
  • popupStatus 被 :message 和 :type 用到了,所以它也不是未使用变量。
  • popupStatus 和 PopupStatus 是两个不同的 JavaScript 标识符,所以普通重复 key 或 shadow 规则也不会认为它们冲突。

vue/no-undef-components 的源码更能说明这个边界。

vue/no-undef-components 的 GitHub 源码 里有一个 DefinedInSetupComponents。它会收集 <script setup> 里的变量名,再用接近 Vue 的 rawName -> camelName -> pascalName 逻辑判断组件是否已定义。

这条规则回答的是「有没有定义」,不是「解析结果是不是开发者想要的那个定义」。因此只要 <script setup> 里存在 popupStatus 或 PopupStatus 这种可匹配名字,它就倾向于认为 <popup-status> 已定义;至于这个名字最后指向组件 import 还是普通状态对象,不在它的检查范围内。

vue/no-template-shadow 也包不住。它检查的是模板局部变量,例如 v-for="item in list" 里的 item 是否遮蔽了外层变量;本文这个冲突发生在 <script setup> 顶层绑定和组件 tag 解析之间,不是模板局部作用域声明。

坑就藏在 Vue 编译器更细的解析优先级里:<popup-status> 会依次尝试 popupStatus 和 PopupStatus,而 setup-const 又早于 setup-maybe-ref 命中。现成规则没有模拟 resolveSetupReference 这一层顺序,所以漏掉了。

vue-tsc 也不能作为这类问题的保险。用同样的最小复现跑过当前项目同代版本的 vue-tsc --noEmit,结果没有报错。这个现象也合理:类型检查看到的是 Vue 模板生成后的类型上下文,而不是开发者「原本想让 <popup-status> 命中哪个组件」的意图。

其他知名工具目前也不适合作为主要兜底:

Oxlint 的插件兼容说明 里,vue 标记为不完整支持,说明它只覆盖能作用在 script tag 上的部分 eslint-plugin-vue 规则。这个问题需要同时理解 template tag、<script setup> 顶层绑定和 Vue 编译器解析优先级,不能只靠 script 规则完成。

Biome 的语言支持说明 提到 Vue / Svelte / Astro 这类文件存在 HTML 侧支持边界。即使工具能解析 .vue 文件,也不等于已经实现了 Vue 编译器级别的组件解析冲突检查。

当前能确认的是:官方 Vue ESLint 规则、vue-tsc、Oxlint、Biome 都不应该被当成这次问题的现成防线。

静态检查仍然有空间,只是通用 ESLint + 官方 Vue 推荐规则暂时抓不到;需要补一条更贴近 Vue 编译器解析顺序的规则,专门模拟这个命名冲突。

规则的判断口径可以先收窄到这几类场景:

  • 只检查 <script setup> 的 SFC。
  • 对组件 tag,收集顶层 import 进来的 PascalCase 名字,例如 PopupStatus,再收集顶层普通绑定,例如 popupStatus,包括变量、解构变量、函数和类。
  • 对自定义指令,收集 <script setup> 顶层绑定,并把 v-popup-status 映射成 v-popup-status -> vPopupStatus -> VPopupStatus 这组候选名。
  • 遍历 template 里的组件 tag 和非内置自定义指令名,模拟 Vue compiler-core 的解析顺序。
  • 如果同一个模板名字同时命中多个候选,而且当前写法会稳定落到更可疑的绑定,就报错。
  • 静态事件名、props / attrs、slot 名和 template ref 先不检查;它们不走同一条 resolveSetupReference() 候选解析链路。

这条检查现在已经沉淀到 @sheng/eslint-plugin 里的 @sheng/vue-no-component-name-conflict。项目接入时优先看规则文档和插件的使用指南,不要再从这篇文章里手动复制旧源码。

规则源码的核心在 src/rules/vue-no-component-name-conflict/index.ts。script visitor 先收集三类顶层名字,分别是 import 进来的 PascalCase 组件、普通绑定和局部指令;template visitor 再处理 tag,把组件 tag 归一化成 raw -> camel -> Pascal 候选,把自定义指令归一化成 v-raw -> vCamel -> VPascal 候选。规则只在模板真正出现歧义名字且候选同时命中时报告。

这类冲突可以用专门的 Lint 规则持续检查,组件渲染测试则适合补充本次回归,不必给每个组件都写一条「它真的渲染出来了」的测试。

Vue + TypeScript + 多组件协作的项目仍然值得引入 ESLint。它能提前拦下未定义组件、未使用组件、模板变量遮蔽、废弃写法和错误的 Vue API 用法;这次问题提醒的是工具链分层:现成 ESLint 规则覆盖不了所有编译器边界。

规则页里保留了可编辑示例、接入配置、维护入口和测试位置。业务项目确认 vue-eslint-parser 配置正常后,可以考虑把这条规则从默认的 warn 覆写成 error,因为它命中时已经是稳定解析歧义,不只是代码风格偏好。

本地护栏之外

@sheng/eslint-plugin 先处理项目内的命名冲突。上游可以分别讨论 ESLint 规则、官方文档提醒和 Vue 编译诊断,它们解决的问题不同。

第一条线是 eslint-plugin-vue。这是最合适的落点,因为这个问题是「静态可发现的高风险命名冲突」,而且 eslint-plugin-vue 已经能同时读取 template 和 <script setup> 作用域。

更合适的步骤是先准备最小复现和规则设计,再开 issue 说明现有 vue/no-undef-components、vue/no-unused-components、vue/no-template-shadow 的边界。vue/no-undef-components 关注 undefined component;当前问题关注 ambiguous / shadowing。新增一条 vue/no-setup-component-name-conflict 会更贴近问题。

这条规则的边界先压在最小复现能证明的范围里:

  1. 只检查 <script setup>。
  2. 收集顶层 import,识别组件候选:
    • .vue default import。
    • PascalCase import。
    • defineAsyncComponent 返回值。
    • 其他可通过配置补充的组件工厂。
  3. 收集普通顶层绑定:
    • const / let / function / class。
    • 解构变量。
    • composable 返回值解构。
  4. 遍历 template 里的组件 tag,生成 raw / camel / Pascal 候选名;如果要覆盖自定义指令,对 v-xxx 生成 v-xxx / vXxx / VXxx 候选名。
  5. 模拟 Vue compiler-core 的解析顺序:
    • 按源码顺序逐种检查 BindingTypes,包括同一组里不同类型的先后顺序。
    • 每种类型内部依次检查 raw、camel、Pascal,命中就停止。
  6. 如果同一个 tag 同时命中组件候选和非组件候选,并且非组件候选会先命中,就 report。
  7. 默认不 autofix,只给 suggest:
    • 重命名状态变量。
    • 或给组件 import 起别名。
  8. 对命名空间组件、显式组件变量、动态组件、无法判断类型的复杂表达式保守跳过,降低误报。

第二条线是 Vue 官方文档。文档可以在 <script setup> 的 Using Components 附近补一个 warning,把心智模型讲清楚:

Imported components and local bindings share the same template scope.
Avoid declaring local bindings whose camelCase / PascalCase names overlap with component tags.

第三条线是 Vue core。2026-08-15 核对到的 vuejs/core@a2b40db9 还没有解决这个问题;resolveSetupReference() 仍然按既有顺序返回第一个命中的 setup 绑定,也没有多候选诊断。

如果改 Vue core,不应该改解析优先级,也不应该让编译器猜「哪个 import 更像组件」。更稳的做法是只补诊断:同一个模板名字归一化后命中多个 setup 绑定时,继续按旧规则生成代码,但在开发场景把它作为 compiler error 抛给调用方,在构建场景保留 warning。Vite、Nuxt、Webpack 这类开发服务器本来就会把 compiler error 通过自己的 overlay 显示到浏览器前台;Vue core 不需要直接耦合某个 bundler 的 overlay 实现。

运行时 warning 可以作为后续单独优化,但它只能做兜底。当前运行时看到的是「一个普通对象被当成组件,但对象没有 render/template」,所以提示容易停在组件格式不对。更有帮助的文案可以提醒读者检查 <script setup> 里的组件 tag 是否解析到了同名普通绑定。不过,普通函数被误当成 functional component 时不一定会进入这个分支,因此上游诊断更适合优先放在 compiler-core。

自定义指令如果也进入 Vue core 诊断,resolveSetupReference() 的错误文案就不能写死成 Component tag ...,需要让调用点传入 component / directive 这类语义,或者改成更泛化的 template name 文案。事件、props、slot、ref 这些场景不走同一条解析链路,PR 文档里说明边界即可。

总结

这类检查需要同时理解模板名字与 script setup 顶层绑定,并模拟编译器真正使用的类型组和候选顺序。现成规则能判断有没有定义,却不一定判断最终选中了开发者意图中的那个定义;专门护栏补的是这个职责缺口。

项目规则、官方文档提醒与编译诊断可以分别推进,复杂表达式和无法确认的组件形态保守跳过。组件 tag 与自定义指令共用解析机制,事件、props、slot 和 ref 不应因为也涉及命名就一并纳入。

上一篇:顶层绑定怎样参与模板名字解析。