普通视图

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

Vue3 + Element Plus 输入框省略号插件:零侵入式全局解决方案

2025年9月16日 15:49

🚀 Vue3 + Element Plus 输入框省略号插件:零侵入式全局解决方案

📖 前言

在日常开发中,我们经常会遇到输入框内容过长需要显示省略号的需求。传统的做法是在每个组件中手动添加样式和逻辑,但这种方式存在以下问题:

  • 重复代码:每个输入框都要写一遍相同的逻辑
  • 维护困难:样式分散在各个组件中,难以统一管理
  • 容易遗漏:新增输入框时容易忘记添加省略号功能
  • 性能问题:每个组件都要单独处理,没有统一的优化

今天我将分享一个零侵入式的全局解决方案,通过 Vue3 插件的方式,自动为所有 `el-input` 输入框添加省略号显示和悬浮提示功能。

🎯 功能特性

  • 完全自动化:无需在任何组件中手动添加代码
  • 智能监听:自动处理动态添加的输入框
  • 性能优化:使用 WeakSet 避免重复处理
  • 类型安全:完整的 TypeScript 支持
  • 内存友好:完善的事件监听器清理机制
  • 响应式:支持窗口大小变化时重新计算

🛠️ 技术实现

核心思路

我们的解决方案基于以下几个核心技术:

  1. MutationObserver:监听 DOM 变化,自动处理动态添加的输入框
  2. WeakSet:记录已处理的元素,避免重复处理
  3. Vue3 插件系统:通过插件方式全局注册功能
  4. 事件委托:统一管理事件监听器

完整代码实现

/**
 * el-input 省略号全局插件
 * 自动为所有 el-input 输入框添加省略号显示和悬浮提示功能
 * 不包含 textarea 类型
 */

class InputEllipsisManager {
  private observer: MutationObserver | null = null
  private processedElements = new WeakSet<HTMLElement>()

  constructor() {
    this.init()
  }

  init() {
    // 等待 DOM 加载完成后开始处理
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', () => this.startObserving())
    } else {
      this.startObserving()
    }
  }

  private startObserving() {
    // 处理已存在的元素
    this.processExistingElements()

    // 创建 MutationObserver 监听 DOM 变化
    this.observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList') {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              this.processElement(node as HTMLElement)
            }
          })
        }
      })
    })

    // 开始观察
    this.observer.observe(document.body, {
      childList: true,
      subtree: true
    })
  }

  private processExistingElements() {
    // 处理页面中已存在的所有 el-input
    const inputs = document.querySelectorAll('.el-input:not(.el-textarea)')
    inputs.forEach(input => this.processElement(input as HTMLElement))
  }

  private processElement(element: HTMLElement) {
    // 如果已经处理过,跳过
    if (this.processedElements.has(element)) {
      return
    }

    // 查找 el-input 元素
    const inputs = element.classList?.contains('el-input') && !element.classList?.contains('el-textarea')
      ? [element]
      : Array.from(element.querySelectorAll?.('.el-input:not(.el-textarea)') || [])

    inputs.forEach(inputEl => {
      if (this.processedElements.has(inputEl)) {
        return
      }

      this.processedElements.add(inputEl)
      this.addEllipsisToInput(inputEl)
    })
  }

  private addEllipsisToInput(inputEl: HTMLElement) {
    const inputInner = inputEl.querySelector('.el-input__inner') as HTMLInputElement
    
    if (!inputInner || inputInner.tagName.toLowerCase() === 'textarea') {
      return
    }

    // 添加省略号样式
    inputInner.style.textOverflow = 'ellipsis'
    inputInner.style.whiteSpace = 'nowrap'
    inputInner.style.overflow = 'hidden'

    // 创建更新提示的函数
    const updateTooltip = () => {
      const text = inputInner.value || inputInner.placeholder || ''
      if (text && inputInner.scrollWidth > inputInner.clientWidth) {
        inputInner.title = text
      } else {
        inputInner.removeAttribute('title')
      }
    }

    // 添加事件监听器
    const events = ['input', 'focus', 'blur', 'change']
    events.forEach(eventType => {
      inputInner.addEventListener(eventType, updateTooltip)
    })

    // 初始检查
    updateTooltip()

    // 监听窗口大小变化
    const resizeHandler = () => {
      setTimeout(updateTooltip, 100)
    }
    window.addEventListener('resize', resizeHandler)

    // 保存清理函数
    ;(inputEl as any)._ellipsisCleanup = () => {
      events.forEach(eventType => {
        inputInner.removeEventListener(eventType, updateTooltip)
      })
      window.removeEventListener('resize', resizeHandler)
    }
  }

  // 公共方法:手动刷新所有输入框的省略号状态
  public refreshInputEllipsis() {
    this.processExistingElements()
  }

  // 销毁方法
  public destroy() {
    if (this.observer) {
      this.observer.disconnect()
    }
    
    // 清理所有已处理元素的事件监听器
    document.querySelectorAll('.el-input').forEach(inputEl => {
      if ((inputEl as any)._ellipsisCleanup) {
        ;(inputEl as any)._ellipsisCleanup()
        delete (inputEl as any)._ellipsisCleanup
      }
    })
  }
}

// 创建全局实例
let ellipsisManager: InputEllipsisManager | null = null

// Vue 插件定义
export default {
  install(app: any) {
    // 在应用挂载后启动
    app.mixin({
      mounted() {
        if (!ellipsisManager) {
          ellipsisManager = new InputEllipsisManager()
        }
      }
    })

    // 提供全局方法
    app.config.globalProperties.\$refreshInputEllipsis = () => {
      if (ellipsisManager) {
        ellipsisManager.refreshInputEllipsis()
      }
    }
  }
}

// 导出管理器类(可选,用于高级用法)
export { InputEllipsisManager }

关键代码解析

1. MutationObserver 监听 DOM 变化

this.observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'childList') {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          this.processElement(node as HTMLElement)
        }
      })
    }
  })
})

作用:自动监听页面中新增的 DOM 元素,确保动态添加的输入框也能被处理。

2. WeakSet 避免重复处理

private processedElements = new WeakSet<HTMLElement>()

if (this.processedElements.has(element)) {
  return
}
this.processedElements.add(element)

作用:使用 WeakSet 记录已处理的元素,避免重复处理同一个输入框,提高性能。

3. 智能省略号检测

const updateTooltip = () => {
  const text = inputInner.value || inputInner.placeholder || ''
  if (text && inputInner.scrollWidth > inputInner.clientWidth) {
    inputInner.title = text
  } else {
    inputInner.removeAttribute('title')
  }
}

作用:通过比较 `scrollWidth` 和 `clientWidth` 来判断内容是否超出,只有超出时才显示悬浮提示。

📦 安装使用

1. 创建插件文件

将上述代码保存为 `src/plugins/inputEllipsis.ts`

2. 在 main.js 中注册插件

import { createApp } from 'vue'
import App from '@/App.vue'
import inputEllipsisPlugin from '@/plugins/inputEllipsis'

const app = createApp(App)

app
  .use(inputEllipsisPlugin) // 注册输入框省略号插件
  .mount('#app')

3. 添加全局样式(可选)

// src/styles/element-plus.scss

// el-input 省略号全局样式
.el-input:not(.el-textarea) {
  .el-input__inner {
    // 确保省略号正确显示
    &[style*=\"text-overflow: ellipsis\"] {
      display: block;
      width: 100%;
      box-sizing: border-box;
    }
  }

  // 为只读状态的输入框也支持省略号
  &.is-disabled .el-input__inner {
    &[style*=\"text-overflow: ellipsis\"] {
      cursor: default;
    }
  }
}

// 确保输入框容器支持省略号
.el-input__wrapper {
  overflow: hidden;
}

🎨 使用效果

安装插件后,所有的 `el-input` 都会自动添加省略号功能:

<template>
  <!-- 这些输入框会自动添加省略号功能 -->
  <el-input v-model=\"value1\" placeholder=\"自动添加省略号\" />
  <el-input v-model=\"value2\" placeholder=\"这个也会自动处理\" />
  
  <!-- textarea 不会被影响 -->
  <el-input type=\"textarea\" v-model=\"value3\" placeholder=\"这是文本域,不会被处理\" />
  
  <!-- 动态添加的输入框也会被自动处理 -->
  <el-input v-if=\"showInput\" v-model=\"value4\" placeholder=\"动态输入框也会被处理\" />
</template>

功能演示

  • 内容超出时显示省略号
  • 鼠标悬浮时显示完整内容
  • 支持输入内容变化时动态更新
  • 支持窗口大小变化时重新计算
  • 自动排除 textarea 类型

🔧 高级用法

手动刷新省略号状态

// 在任何组件中
this.\$refreshInputEllipsis()

获取管理器实例

import { InputEllipsisManager } from '@/plugins/inputEllipsis'

// 创建自定义实例
const customManager = new InputEllipsisManager()

🚀 性能优化

1. 防抖处理

const resizeHandler = () => {
  setTimeout(updateTooltip, 100)
}

窗口大小变化时使用防抖,避免频繁计算。

2. 事件监听器清理

;(inputEl as any)._ellipsisCleanup = () => {
  events.forEach(eventType => {
    inputInner.removeEventListener(eventType, updateTooltip)
  })
  window.removeEventListener('resize', resizeHandler)
}

每个输入框都保存清理函数,避免内存泄漏。

3. WeakSet 优化

使用 WeakSet 而不是 Set,让垃圾回收器自动清理不再使用的元素引用。

🎯 适用场景

  • 管理系统:大量表单输入框
  • 数据展示:表格中的输入框
  • 动态表单:根据条件动态生成的输入框
  • 组件库:需要统一处理输入框样式的项目

🔍 技术亮点

  1. 零侵入式:无需修改任何现有组件代码
  2. 自动化:完全自动处理,无需手动干预
  3. 高性能:使用现代浏览器 API 优化性能
  4. 类型安全:完整的 TypeScript 支持
  5. 内存友好:完善的内存管理机制

📝 总结

这个输入框省略号插件通过 Vue3 插件系统、MutationObserver 和 WeakSet 等技术,实现了一个完全自动化的解决方案。它不仅解决了传统方案的痛点,还提供了更好的性能和用户体验。

核心优势

  • 🚀 零侵入:安装即用,无需修改现有代码
  • 🎯 自动化:智能处理所有输入框
  • 高性能:优化的算法和内存管理
  • 🛡️ 类型安全:完整的 TypeScript 支持

如果您觉得这个方案有用,欢迎点赞收藏!也欢迎在评论区分享您的使用心得和改进建议。


作者简介:专注于前端技术分享,Vue3 + TypeScript 实践者

技术栈:Vue3, TypeScript, Element Plus, 前端工程化

样式工程化:如何实现Design System

作者 ygria
2025年9月16日 15:38

对于样式的处理,我经历了好几个阶段,也对其中的琐碎、麻烦、相互干扰、不够工程化深恶痛绝,可以说这玩意就是我从后端开发转向全栈的一个拦路虎。好在我现在踩了很多坑,也算积累了一点经验了。

从原生CSS到TailwindCSS

 原生CSS

大家应该都知道最初的“三剑客”,也就是Html、JavaScript、CSS。如果只要做一个简单的demo,也就是单网页,我们可以这么做:

  1. html标签上加上CSS类名,比如.my-container
  2. 在CSS文件中,写CSS样式,例如:
.my-container {
    width: 200px;
    height: 200px
}
  1. 在HTML中使用stylesheet标签引入CSS文件。

这也就是最初始的写法,也是我们在网页上F12,去浏览别人的网页看到的样子。这么写有一些缺点:

  1. 对于命名能力要求高,心智负担重,往往需要再学习一套命名的规范。 写函数容易,给函数命名难,所以有了匿名函数,而样式也是一样:写一个标题,CSS类名叫heading, 再写一个标题呢?写一个描述,叫description, 再写一个呢?如果有各种不同样式布局的标题、容器、文本,逐渐就变得越来越搞不清楚哪个对哪个。如果是多人合作的项目,还需要沟通好规范。

  2. 样式、模板位置分离,难改。我们现在需要改某个页面元素的样式,需要:

 ①找到HTML元素的类名(可能有多个应用优先级不同的样式,所以需要判断是哪个决定了它最终呈现出的样式)  ②跳到CSS文件中,修改样式  ③查看网页上样式有没有修改成功  ④检查其他页面使用相同类名的地方是否正常

在多个文件和窗口中跳来跳去,很容易打断思路,也会越来越难以维护。 3. CSS原生写法不支持嵌套,所有伪类(::before, ::after) 样式、状态样式(:hover, :focus)等,都需要独立闭合的大括号括起来,写起来相当麻烦。

  1. 样式是否真的生效相当难以判断,很多时候都是看页面上样式是不是正常了。这种非强制要求可能导致”技术债“越积越深,直到有一天要求调整样式的时候,发现已经极为混乱和难以维护。

 工程化、框架和PostCSS

在原生Html和Javascript有了框架:Vue、React等等后,CSS也有了更好的处理方式。

  1. SCSS/ LESS 等CSS后处理器出现,支持嵌套等各种语法糖,让CSS写起来更为简便。

  2. 支持组件内局部应用的样式。例如:Vue的  标签支持加上 scoped 关键字,让这个组件内的样式只在本组件内有效,避免样式污染,也有效降低了心智负担。

原理:通过打包时的插件配置,将开发时所写的样式还原成浏览器可以识别的CSS文件,将嵌套的扁平化,局部或者不同适用场景的统一添加前缀。

TailwindCSS: 快速入门+设计思想

一旦接触了TailwindCSS 很难不被它的直接简洁迷上。

它相当于提供了一整套速写词,再也不用去记臃肿的写法了!再也不用三四行来写一个属性了!也不用再到处找样式文件,只需要在html标签里写上一串原子类的className,样式就直接显示到了屏幕上。

原先设置字号需要设置13px14px 等,现在只需要写text-xs 或者text-sm。原先需要设置圆角尺寸,都是需要传入特定的值,现在只需要传入 rounded-mdrounded-lg。 阴影原先需要自己写,现在只需要写 shadow-mdshadow-sm。 换言之,只需要给出语义化的类名,就可以实现想要的效果,再也不需要关心琐碎的细节。

快速的可见即可得: 从html元素的className中读到bg-red,直接就能看到页面元素的背景是红色的。而原来则需要多一次跳转:先看到类名,再找到样式实现。当整个系统都如此,对于降低心智负担是极为重要的。

最棒的就是它内置了一套设计系统,包括尺寸、状态和色彩系统。

1. 尺寸

直接使用符合响应式标准的单位rem,和一套语义化的封装: smmdlg……换言之,我们不需要再考虑这个按钮到底应该占多大,而是应该从它实际的功能和使用场景出发,考虑它应该是大、中等还是小。

2. 色彩

内置了多种标准颜色的色彩系统,也支持自己定义色调梯度,使用颜色+渐进数值来提供原始色值和类名的对应关系,更符合直觉:需要颜色更深一些,数字就更大,需要颜色更小一些,数字就更浅。

举例:主要按钮使用 bg-blue-600, 背景色使用 bg-blue-100 ,而不需要再去调色盘查看哪个是主要的蓝色,哪个是浅蓝色。透明度也不需要自己写了,而是直接使用形如bg-blue-600/80的格式,就是透明度80%的意思。

3. 状态

在写了一套样式后,我们需要考虑它处于不同上下文的种种状态,例如:在不同屏幕尺寸下/明暗或者不同主题下/用户不同行为下/数据不同状态下等,我将其统一称之为“状态”。

举例: ① 在电脑上可以排列成每行四个的卡片,在手机屏幕上只能放得下每行一个,不然内容放不下。间距也应该随之缩小。

② 按钮在悬停时应该有特殊的样式,给用户以“我正在交互热区”的正反馈,当按钮不能点击时,应该有disabled 的状态。当正在从接口请求数据时,页面和按钮应该是loading状态。

③ 在黑暗模式下,所有的色彩都需要调整成适配的样子。例如原本是白底黑字,暗黑模式就应该是黑底白字。

TailwindCSS支持使用冒号来响应状态查询、容器查询和媒体查询。例如:bg-red-500 hover:bg-red-600 ,意思就是在hover状态下背景颜色加深。

可读性非常强。对于响应式的尺寸设计,也可以通过md: sm: 的格式来实现。

提供了一整套内置的设计系统外,也支持传入特定值来支持特殊的样式,既保留了系统的优雅简洁,也保留了开放和灵活性。目前很多UI框架都使用TailwindCSS实现。

大量使用了TailwindCSS编程后,也会觉得抽象粒度不够,写起来费劲。我们终究是需要抽象出来一套标准的UI组件复用在系统中。那么怎么去设计这一套UI组件呢?

Radix UI Themes:完善的样式系统都有哪些东西

在研究样式系统时,我看到了Radix UI , 精致漂亮色彩丰富,完完全全是我喜欢的样子。阅读它Theme 的源码,让我体悟到了样式系统的设计原则。

语义化:Design Token的运用

我需要一个创建的按钮,希望它足够醒目、重要。它是蓝色的(蓝色是我们的主色调!)

别再写background-color: blue了!

对,用tailwindcss写起来简洁一些……bg-blue-600……不,也别这么写了!

应该写的是: bg-accent-6,意思是第六梯度的主色调,这样主色调变了,也不需要去改动这一部分的代码。

这就是我们应该做的:在写样式时,我们写语义化的语句(给它重要的颜色,重要的字重,重要的阴影,hover时颜色变深),然后再通过给变量赋值的方式,来使其真正生效(重要的颜色是什么颜色?hover时具体颜色变深多少?),这样我们只需要写一次,就可以实现各种不同的主题。

业务层逻辑的剥离:样式系统只负责样式

对于一个重要的按钮,我们写出来的可能是这样:

bg-accent-6 hover:bg-accent-7 shadow-md text-md ……

把这些封装在工具类中,最终给这个重要的按钮的类名就是 ——

btn-primary

不。这样就是样式系统与业务逻辑再一次重叠和混淆了。primary 的意思是主要的,包含着一种业务逻辑层面的判断。正确的命名应该是只说明样式,这样上层调用时不会造成任何歧义和混淆。

Radix UI的做法是给出了六种变体(variant):classicsolidsoftsurfaceghostoutline。 在使用的时候,当然可以根据业务逻辑去判断出,classicsolid一般用于一级按钮,softsurface用于二级按钮……但那是业务层关心的事。

Pasted image 20250916152917

足够低的优先度,给扩展提供的可能性

Radix Theme中所提供出的所有组件,都支持外部传入的样式进行覆盖和改写,保留了足够的灵活度。我们尽可以继续使用TailwindCSS来加上自己喜欢的样式。

总结

样式系统必不可少!

将复杂琐碎的样式细节隐藏起来,写业务代码时只需要关注交互逻辑、数据流向等,而无需再关注样式……这也就是样式系统的意义所在。

而对于跨端开发的交互方面的抽象,又是另一个问题了……等我踩完坑,再写一篇。

❌
❌