普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月9日首页

Element UI 表格 show-overflow-tooltip 长文本导致闪烁的根本原因与解法

2026年1月9日 13:55

问题复现

在 Element UI (Vue 2) 项目中,el-table-column 开启 show-overflow-tooltip 展示超长文本(500字+)。 现象: 鼠标悬停单元格,Tooltip 疯狂闪烁(显示-消失-显示循环),侧边滚动条也跟随闪烁(副作用)。 关键环境信息: 页面原本就有滚动条(无布局重排),但依然闪烁。

排查与验证

初步排查: 曾怀疑是 Tooltip 撑开页面导致滚动条出现进而挤压布局。但经过验证,页面滚动条一直存在,布局并未发生位移,因此排除“重排(Reflow)”导致的坐标变化。

核心对照实验:

  1. 自动模式:使用 show-overflow-tooltip -> 闪烁
  2. 手动模式:在 template 中使用 <el-tooltip> 包裹内容,不限制宽高 -> 不闪烁

根本原因分析

既然布局没动,为什么会自动关闭?答案是 Tooltip 自身的遮挡与事件逻辑缺陷

1. 遮挡触发 (Occlusion)

由于文本极长,Tooltip 渲染尺寸巨大。在特定分辨率下,Popper.js 计算出的定位会导致 Tooltip 弹出的一瞬间,其 DOM 元素直接覆盖(Overlap)在了鼠标光标之上

2. 机制差异

  • show-overflow-tooltip (Table 内置逻辑) :Element UI 的 Table 组件使用单例模式维护一个全局 Tooltip。它主要监听单元格(Cell)的 mouseleave 事件。Bug 流程: Tooltip 弹出盖住鼠标 -> 浏览器判定鼠标离开单元格(进入 Tooltip) -> 触发 Cell 的 mouseleave -> Table 的处理逻辑较为脆弱,在判定“鼠标是否进入 Tooltip”时出现时序问题或逻辑漏洞 -> 直接关闭 Tooltip。 Tooltip 关闭 -> 鼠标重新落回单元格 -> 触发 mouseenter -> 死循环
  • 手动 <el-tooltip> (独立组件) :手动模式下,每个单元格拥有独立的 Tooltip 实例。该组件内部对 enterable(鼠标进入浮层)有完善的处理机制。 正常流程: Tooltip 弹出盖住鼠标 -> 组件检测到鼠标虽然离开了 Reference(触发源),但进入了 Popper(浮层) -> 保持显示状态

结论与解决方案

show-overflow-tooltip 是一个为了性能牺牲了部分交互稳定性的“阉割版”实现,无法完美处理“弹出层直接遮挡触发源”的极端情况。

最佳解法: 放弃 show-overflow-tooltip,使用 Slot 手动接管。

<el-table-column label="详情" width="300">
  <template slot-scope="scope">
    <el-tooltip 
      effect="dark" 
      :content="scope.row.detail" 
      placement="top"
      popper-class="my-popper"
    >
      <div class="ellipsis-cell">{{ scope.row.detail }}</div>
    </el-tooltip>
  </template>
</el-table-column>

建议优化: 虽然手动挡不限制宽高也不会闪烁,但为了阅读体验,建议通过 CSS 限制最大高度。

/* 全局样式 */
.my-popper {
  max-width: 400px;
  max-height: 300px;
  overflow-y: auto;
}

总结

当排查“幽灵闪烁”问题时,如果页面布局未动,请重点关注层级遮挡导致的鼠标事件丢失。对于复杂场景,手动控制的组件永远比自动的语法糖更可靠。

昨天以前首页

记一次Vue 2主应用集成Vue 3子项目的Monorepo迁移踩坑指南

2026年1月7日 17:35

前言

最近在进行Monorepo架构调整,需要将一个现有的Vue 3(Vite)项目作为一个子应用 (apps/wj) 迁移到由Vue 2(Webpack)主导的大仓中。本以为只是简单的“文件夹移动”,结果在依赖管理、网络代理和端口映射上踩了一圈坑。

本文记录了从迁移到跑通全流程遇到的4个典型问题及解决方案。

坑点一:pnpm 严格模式下的“幽灵依赖”

💥 现象

将项目移入大仓后,执行 dev 脚本报错:

'vite' 不是内部或外部命令,也不是可运行的程序

或者启动后报错找不到 unplugin-auto-importvue-request 等插件。

🔍 原因

原项目可能使用 npm/yarn,存在依赖提升 (Hoisting) ,即 devDependencies 即使没写在 package.json 里,依靠根目录 node_modules 也能跑。 但迁移到 pnpm Monorepo 后,pnpm 的严格机制要求所有使用的包必须显式声明

✅ 解决

在根目录通过 --filter 为子应用补全依赖:

# 补全构建工具
pnpm add vite @vitejs/plugin-vue vue-tsc -D --filter wj

# 补全缺失的业务/构建插件
pnpm add unplugin-auto-import unplugin-vue-components -D --filter wj
pnpm add vue-request --filter wj

坑点二:Workspace 内部包的正确引用

💥 现象

Vite 启动报错:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'configs' imported from ...

子应用试图引用大仓共享的配置包(packages/configs),但找不到模块。

🔍 原因

子应用虽然物理上在 monorepo 里,但 package.json 里没有声明对内部包的依赖,导致软链接未建立。

✅ 解决

使用 --workspace 协议建立软链:

# 将内部包链接给子应用
pnpm add configs --workspace --filter wj

注意:如果共享包内部也用了某些插件(如 @vitejs/plugin-vue),共享包自己也必须安装该插件,否则会报“父级依赖缺失”。


坑点三:Node 高版本 localhost 解析陷阱 (IPv6)

💥 现象

主应用配置了代理转发到子应用,但在浏览器访问时报 HTTP 500,终端报错:

Error: connect EACCES ::1:5192

🔍 原因

  • 环境: Node.js v17+
  • 机制: 主应用代理配置写了 target: 'http://localhost:5192'。Node 默认将 localhost 解析为 IPv6 地址 ::1
  • 冲突: 子应用 (Vite) 默认只监听 IPv4 (127.0.0.1)。主应用去 IPv6 端口找人,自然连不上。

✅ 解决

方案A(推荐): 修改主应用代理配置,强制使用 IPv4 IP。

// 主应用 vite.config.ts / vue.config.js
proxy: {
  '/wj': {
    target: 'http://127.0.0.1:5192', // 👈 不要写 localhost
    changeOrigin: true
  }
}

方案B: 让子应用监听所有地址。启动命令改为 vite --host


坑点四:主应用代理“漏气” (接口返回 HTML)

💥 现象

页面加载成功,但业务接口(如 /cmisp/api/xxx)报 304200,查看 Response 内容竟然是 index.html 的代码,导致 JSON 解析失败。

🔍 原因

主应用只代理了页面路由 /wj,但子应用发出的 API 请求是 /cmisp 开头的。 主应用不认识 /cmisp,将其当成了前端路由处理,直接返回了 index.html

✅ 解决

在主应用中补全 API 的代理转发规则:

// 主应用 vite.config.ts
server: {
  proxy: {
    // 1. 子应用页面资源
    '/wj': {
      target: 'http://127.0.0.1:5192',
      changeOrigin: true
    },
    // 2. 子应用 API 请求 (新增)
    '/cmisp': {
      target: 'http://127.0.0.1:5192', // 如果是 mock 数据走这里;如果是真实后端填后端 IP
      changeOrigin: true
    }
  }
}

总结

Monorepo 迁移不仅仅是文件搬运,核心在于:

  1. 依赖边界:pnpm 下必须“谁用谁装”。
  2. 网络互通:Node 高版本下 localhost 的 IPv6 坑需要格外注意。
  3. 路由接管:主应用作为网关,必须接管子应用的所有请求(包括静态资源和 API)。
❌
❌