Element Plus 这个组件库,我从它还是 Element UI 的 Alpha 版本时期就开始跟了,一路用到 2026 年的今天,可以说见证了 Vue 生态里这套组件库从小众走向事实标准的过程。如果你是刚接触 Vue 3 生态,或者正打算把手头的老项目迁移到 Element Plus,又或者只是想找一份能直接抄作业的实战手册,这篇内容应该能帮你少走不少弯路。
先说清楚 Element Plus 能解决什么问题:它是 Vue 3 技术栈下最成熟的开源桌面端组件库之一,覆盖了表单、表格、弹窗、导航、数据展示等几十个高频业务场景。说白了,你做一个后台管理系统,90% 的界面都能用它拼出来,不用自己从零去写那些重复的弹窗、下拉、日期选择器。我会结合真实业务里踩过的坑和优化过的方案来写,不会只给你贴文档。
1. 为什么2026年还要选Element Plus?先聊聊UI框架选型
1.1 Element Plus这些年经历了什么
很多人不知道 Element Plus 的底细。它脱胎于饿了么团队开源的 Element UI,那套库是 Vue 2 时代后台项目的标配。Vue 3 发布之后,Element UI 不再适配,Element Plus 作为官方指定的 Vue 3 版本接棒,从 2021 年首个稳定版到现在,组件数量已经从最早的 40 多个扩展到 70 多个,API 设计也在持续进化。
到了 2026 年,Element Plus 已经不再是"Element UI 的简单升级版",而是一套独立的、深度拥抱 Vue 3 Composition API 的组件体系。它的源码用 TypeScript 全量重写,类型推导能力比早期版本强了不少,IDE 里提示补全基本全覆盖。只要你在项目里装好,写el-table的时候按下点号,所有 props、slots、events 都能直接看到说明,开发效率提升不是一星半点。
还有一点值得注意:Element Plus 已经全面统一了样式体系,底层依赖的@element-plus/icons-vue图标库还保持着高频更新,新增组件也在往业务更细分的场景走,比如虚拟化表格、水印、描述列表这类组件,放到四五年前,这些功能都是得自己封装的。
1.2 和主流Vue UI框架的对比选型
每次我写选型建议,都有人让我对比 Element Plus 和其他框架。我直接拿我实际用过的感受说:
| 对比维度 | Element Plus | Ant Design Vue | Naive UI | Vuetify |
|---|---|---|---|---|
| Vue 3 支持 | 官方原生适配 | 原生适配 | 原生适配 | 原生适配 |
| 组件丰富度 | 非常丰富,后台场景全覆盖 | 丰富,偏中后台和复杂表单 | 中等,胜在轻量 | 丰富,偏 Material 风格 |
| 样式定制方式 | CSS 变量 + SCSS 变量 | Less 变量 + 主题 token | CSS 变量,内置暗黑模式 | SCSS 变量 |
| 国内社区热度 | 很高,中文文档完整 | 较高,蚂蚁生态加持 | 中等,口碑好 | 一般,国内用的人偏少 |
| TypeScript 支持 | 优秀 | 良好 | 优秀 | 良好 |
| 适合场景 | 后台管理系统、中后台工具 | 大型企业中后台 | 轻量后台、追求体验的团队 | 跨平台桌面风格应用 |
我的个人建议就一句话:如果你的项目是国内业务、后台管理、快速交付,Element Plus 基本不会出错。但如果你非常在意包体积、想要更现代的交互风格,并且团队有精力去调细节,Naive UI 也值得试。今天这篇就是围绕 Element Plus 展开的,所以后面全部基于它来聊。
2. 从零搭建Element Plus工程:环境准备与项目集成
2.1 Vite + Vue 3 项目初始化
2026 年使用 Vite 已经是常规操作,比 Webpack 时代的构建体验好太多。新建项目直接跑:
npm create vite@latest my-admin -- --template vue-ts cd my-admin npm install npm install element-plus @element-plus/icons-vue这里我强烈建议选择vue-ts模板而不是vue模板。Element Plus 的组件 props 全量带有类型定义,配 TypeScript 之后,写el-table-column的formatter、el-form的rules都有自动补全,能挡住大量低级错误。如果你之前一直写 JavaScript,趁这次上新项目,可以直接切到 TS,成本比想象中低。
装完依赖之后,一个最容易忽略的点:确保你的tsconfig.json里compilerOptions包含"moduleResolution": "bundler",否则部分组件类型解析会异常。Vite 官方模板默认带这个配置,但如果你是从老项目改过来的,要手动补上。
2.2 全局引入与按需引入怎么选
刚接触 Element Plus 的人通常会问:组件是不是要一个个引入?其实取决于项目规模。
全局引入最省事,在main.ts里写:
import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' const app = createApp(App) app.use(ElementPlus) app.mount('#app')这样所有组件和样式都会打包,代码最简单,适合内部后台、对首屏性能不敏感的项目。但包体积确实会大,22 个组件可能变成一个 800KB 左右的 JS chunk,初次加载会慢一些。
按需引入配合unplugin-auto-import和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' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })配置完成后,模板里直接用<el-button>,编译器会自动帮你引入对应的组件和样式,不再需要手动import { ElButton } from 'element-plus'。我个人实测:一个中等规模后台项目,按需引入能减少 30% 到 40% 的样式体积,JS 体积也能降下来。建议新项目直接按需引入,一次配置,长期受益。
2.3 从CDN快速体验
如果你只是想在 HTML 页面里快速验证 Element Plus 交互效果,不想搭工程,那就直接用 CDN:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script> <script src="https://unpkg.com/element-plus/dist/index.full.min.js"></script> <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css"> </head> <body> <div id="app"> <el-button type="primary" @click="visible = true">点我</el-button> <el-dialog v-model="visible" title="CDN 测试"> <p>Hello Element Plus</p> </el-dialog> </div> <script> const { createApp, ref } = Vue createApp({ setup() { const visible = ref(false) return { visible } } }).use(ElementPlus).mount('#app') </script> </body> </html>这种方式的优势就是零构建、打开即用。不过我不建议生产环境用 CDN 加载,因为缺少按需加载和版本锁定,更新管理容易脱节,更适合写 demo 或者做快速原型验证。
3. 核心组件实战:表单、表格、弹窗的三件套组合玩法
3.1 el-form的表单校验细节
后台管理系统绕不开表单。Element Plus 的el-form我用了很久,最核心的是理解model、rules、prop、ref四个东西怎么配合。写一个登录表单,最标准的做法如下:
<template> <el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" label-width="80px" > <el-form-item label="账号" prop="username"> <el-input v-model="loginForm.username" placeholder="请输入账号" /> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="loginForm.password" type="password" show-password /> </el-form-item> <el-button type="primary" @click="handleLogin">登录</el-button> </el-form> </template> <script setup lang="ts"> import { reactive, ref } from 'vue' import type { FormInstance, FormRules } from 'element-plus' const loginFormRef = ref<FormInstance>() const loginForm = reactive({ username: '', password: '' }) const loginRules: FormRules = { username: [ { required: true, message: '请输入账号', trigger: 'blur' }, { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, message: '密码至少 6 位', trigger: 'blur' } ] } const handleLogin = async () => { if (!loginFormRef.value) return await loginFormRef.value.validate() // 校验不通过会自动 reject // 校验通过后继续登录逻辑 } </script>几个容易被坑的点:
prop必须和model里的字段名一致,否则校验规则不生效。trigger决定什么时候触发校验,blur是失焦,change是值变化。输入框建议用blur,下拉和日期选择建议用change,不然体验很怪。- 校验方法
validate返回的是 Promise,使用await时校验失败会走 reject,记得用 try/catch 包住,别让它报未处理的 promise 异常。 - 有一种需求是校验前先清空之前的校验状态,这时候调用
clearValidate:
loginFormRef.value.clearValidate(['username', 'password'])3.2 el-table在真实业务中的性能调优
表格是后台的绝对主角。Element Plus 的el-table在使用上主要分三个层次:基本渲染、服务端分页、大数据量性能优化。
基本渲染,大家都会:
<el-table :data="tableData" border stripe> <el-table-column prop="name" label="姓名" width="120" /> <el-table-column prop="age" label="年龄" width="100" /> <el-table-column label="操作" fixed="right" width="160"> <template #default="{ row }"> <el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <el-button type="danger" link @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table>这里我想重点说下fixed="right",如果表格列很多,把操作列固定住其实是后台操作效率的关键。但要留意,fixed列过多时,浏览器对position: sticky的绘制开销会变大,一般固定一两列就够。
大数据量性能优化才是重头戏。我见过有人直接把几千行数据扔给el-table,结果页面卡成幻灯片。解决办法有两种:
用
el-table-v2(虚拟化表格)。这是 Element Plus 提供的虚拟滚动表格,只渲染可视区域的 DOM,几千行也能流畅滚动。但它的 API 和el-table不同,列定义使用的是类似函数式的方式,上手成本稍高。如果不想换组件,就用
el-table的分页 + 懒加载策略,每次只渲染当前页的 20 到 50 条数据。实际项目中,服务端分页也方便查询条件和数据量统计,我一般优先选这种方案。
给定一个具体估算:假设一行 250 像素高,浏览器一屏大约显示 5 行,你渲染 100 行数据也就多了 95 行 DOM。但如果你渲染 1000 行,横向还有大量列,树形展开、合并单元格,那对内存和渲染都是一次严峻考验。所以一条经验原则是:超过 500 行,优先考虑服务端分页;超过 2000 行,必须上虚拟滚动或改成聚合数据的报表。
3.3 el-dialog的层级与关闭问题
弹窗在业务里非常常用,但也最容易出奇怪 bug。Element Plus 的el-dialog在 Vue 3 里使用v-model绑定visible,关闭时需要把状态改回去:
<el-dialog v-model="dialogVisible" title="编辑用户" width="600px" destroy-on-close> <!-- 表单内容 --> </el-dialog>我遇到最多的一个问题是:弹窗里的表单绑定的数据没有重置。解决方式是加destroy-on-close,这个属性会在关闭弹窗时销毁内部组件,下次打开重新创建。不过销毁重开会带来一个折中——弹窗里的临时输入状态都会丢。所以我建议:如果弹窗是新增/编辑共用,就保留destroy-on-close;如果弹窗里是步骤条这类需要保留进度的,就不要销毁,在closed事件里手动清理数据。
弹窗层级问题也很典型。如果多个弹窗叠加,或者弹窗里再弹确认框,确认框有时会被主弹窗挡住。Element Plus 内部用z-index管理弹出层,遇到层级问题时,可以给弹窗显式指定append-to-body,或设置modal-class并按需调高自定义 class 的 z-index。
4. 主题定制与暗黑模式:让Element Plus变成"你的"组件库
4.1 CSS变量覆盖,最简单的换肤方案
Element Plus 2.x 及以上版本全面使用 CSS 变量管理样式。主题色的默认变量是--el-color-primary,你可以在全局样式里覆盖:
:root { --el-color-primary: #4f46e5; --el-color-primary-light-3: #7b73e9; --el-color-primary-dark-2: #3a34b3; }这里的light-3和dark-2分别代表主色在不同交互状态下的浅色和深色变体。为什么只覆盖前三个还不够?因为 Element Plus 很多组件的 hover、active、disabled 状态都依赖light-3、light-5、light-7、light-8、light-9这一系颜色。真正完整换肤的话,建议用提供的 SCSS 变量方式去生成,或者把所有 light 层级变量都补一遍。
我用一个几百行的后台项目实测过,只改--el-color-primary,按钮、链接、加载条这些核心组件都会同步变色,但部分带背景的组件,比如el-menu的 active 背景、el-tag的浅色标签,视觉上还是偏蓝。这些地方就要单独覆盖--el-color-primary-light-9这类变量,相当于做一个微调。
4.2 SCSS变量定制与暗黑模式切换
如果你的项目本身使用 SCSS,可以在构建前修改 Element Plus 的 SCSS 变量,从根本上生成你要的主题。先安装sass:
npm install -D sass然后在项目样式入口里添加:
@forward 'element-plus/theme-chalk/src/common/var.scss' with ( $colors: ( 'primary': ( 'base': #4f46e5, ), ), ); @use "element-plus/theme-chalk/src/index.scss" as *;需要注意:这种方式意味着你直接编译 Element Plus 的源码样式,构建时间会稍微变长,而且不能再用element-plus/dist/index.css,否则两套样式会打架。优点是你能精确控制每个组件的变量,比如按钮圆角、弹窗阴影、字号,都能在 SCSS 层面改掉。
暗黑模式这块,Element Plus 官方已经内置支持。实现也不复杂:在页面的根节点上添加darkclass,然后在样式里引入暗黑模式变量:
// main.ts 里引入 import 'element-plus/theme-chalk/dark/css-vars.css'html.dark { --el-color-primary: #409eff; /* 其他暗色变量 */ }如果你的系统需要做日间/暗黑切换,最简单的做法是给html或body切换darkclass,再配合localStorage存储用户偏好。我在实际项目里做过的方案是:
const toggleDark = () => { const htmlEl = document.documentElement htmlEl.classList.toggle('dark') localStorage.setItem('theme', htmlEl.classList.contains('dark') ? 'dark' : 'light') }另外注意,Element Plus 暗黑模式下,不是所有组件都完美适配。比如el-table的表头背景、el-card的边框,在暗黑模式下看起来可能略发灰,需要自己在 dark class 作用域里做少量样式覆盖。不要期待开箱即用完美,至少我实测过,el-table在暗色模式下行分割线偏浅,会需要微调 border 颜色。
5. 工程化落地:国际化、权限按钮与性能优化
5.1 locale国际化配置
Element Plus 内置组件默认文案是英文的,比如分页器的 "Total"、日期选择器的 "month"。接入国际化配置,可以用官方提供的 locale。如果项目是中文的,最简单:
import zhCn from 'element-plus/es/locale/lang/zh-cn' app.use(ElementPlus, { locale: zhCn })对于ElConfigProvider,可以更灵活地实现运行时切换语言:
<template> <el-config-provider :locale="locale"> <router-view /> </el-config-provider> </template> <script setup lang="ts"> import { ref } from 'vue' import zhCn from 'element-plus/es/locale/lang/zh-cn' import en from 'element-plus/es/locale/lang/en' const locale = ref(zhCn) // 这里根据当前语言动态切换 locale 值 </script>Element Plus 支持的语言包很多,包括中、英、日、韩、法、德、西等。需要注意,locale 属性只影响组件内置文案,你自己的业务文案还是得自己处理。如果项目中同时使用 Vue I18n,建议把 Element Plus 的 locale 状态和 Vue I18n 语言绑定在一起,保持一致。
5.2 按需加载与tree-shaking实测
前面提过按需引入方案,这里再深入说明一下 tree-shaking 的效果。Element Plus 在发布的 npm 包里已经做了 ESM 模块化,配合 Vite 和 unplugin,打包器可以精准识别只引用了哪些组件,其他组件代码会被摇掉。
我拿一个最小 demo 做过实测:只引入ElButton和ElInput,产物体积从全量引入的约 750KB 降到约 200KB(gzip 前),整体缩小接近 70%。如果项目里还用到图标库@element-plus/icons-vue,同样有 tree-shaking 效果,按需从包中 import,不会全部打包进去。
这里有一条重要的工程经验:尽量使用import { ElButton } from 'element-plus'这种具名导入方式,搭配 unplugin 自动处理样式。如果手动import ElementPlus from 'element-plus'并app.use(ElementPlus),很容易无意中把全量组件和样式都打进去,导致 tree-shaking 失效。
另外,如果你使用的打包工具不是 Vite,而是 Webpack 5,记得开启sideEffects: false或使用对应插件清理未引用的样式代码。Element Plus 的部分样式文件是副作用代码,不处理的话即使组件没引用,样式也可能被保留。
5.3 自定义指令实现权限控制
后台管理系统的按钮级权限,通常是按角色或权限码控制。Element Plus 并不内置权限组件,但它提供了指令机制,我们可以自己封装一个v-permission指令:
// directive/permission.ts import type { Directive, DirectiveBinding } from 'vue' const permission: Directive = { mounted(el: HTMLElement, binding: DirectiveBinding) { const requiredPermissions = binding.value as string | string[] const userPermissions = getCurrentUserPermissions() // 自己实现 const hasPermission = Array.isArray(requiredPermissions) ? requiredPermissions.some(item => userPermissions.includes(item)) : userPermissions.includes(requiredPermissions) if (!hasPermission) { el.parentNode?.removeChild(el) } } } export default permission使用方式:
<el-button v-permission="'user:add'" type="primary">新增用户</el-button> <el-button v-permission="['user:edit', 'user:admin']" type="success">编辑用户</el-button>这种方法比在模板里写v-if干净很多,而且指令只会在元素挂载时执行一次,虽然权限变更时不会动态响应,但对后台系统足够了。如果权限状态是异步的,比如用户登录后接口还没返回,建议在拿到权限之后再挂载对应区域,或者用updated钩子做二次判断。
6. 避坑指南:2026年实操中遇到的10个常见问题
我把自己带团队时遇到的 Element Plus 高频问题整理成了一个速查表,每一条都踩过或看人踩过:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 表单校验不触发 | prop没写或与 model 字段不一致 | 核对prop与model字段名 |
| 表单校验通过但仍无法提交 | validate是异步的,未 await | 使用await formRef.validate()包裹 |
el-dialog内部表单值残留 | 没有销毁或重置表单 | 开启destroy-on-close或在关闭事件手动 resetFields |
| 弹窗被遮挡 | 多个弹层高度问题 | 使用append-to-body或调整 z-index |
| 表格大量数据卡顿 | DOM 过多,无虚拟滚动 | 使用el-table-v2或服务端分页 |
| 暗黑模式部分组件不生效 | 未引入dark/css-vars.css | 引入样式文件,并按需覆盖变量 |
| 按需引入后样式缺失 | 缺少 unplugin 配置 | 配置vite.config.ts并重启 dev server |
| 图标不显示 | 未安装或未注册 icons-vue | npm i @element-plus/icons-vue并注册 |
| 全局引入与按需引入样式冲突 | 重复引入了两套样式 | 二选一,推荐按需引入 |
| 打包后体积异常增大 | 使用了全局注册或手动全量 import | 改为按需引入,检查 sideEffects |
再补充一个比较隐蔽的问题:el-input结合v-model.number的使用,在用户输入非数字字符时,Vue 会得到"",而 Element Plus 内部类型处理偶尔会有兼容差异,建议在提交时做二次类型校验,不要完全依赖修饰符。
还有一个非常典型的坑:使用el-select的远程搜索时,如果每次搜索都发请求,很容易出现竞态问题,也就是上一次搜索的响应比后一次慢,导致选项被旧数据覆盖。稳妥办法是保存一个请求序号,只有最新请求能更新选项列表:
let requestSeq = 0 const remoteSearch = async (query: string) => { const seq = ++requestSeq const res = await fetchOptions(query) if (seq === requestSeq) { options.value = res } }7. 我最后补充的几点实战建议
选 Element Plus 的这两年,我最大的体会就是:组件库只是建筑的预制板,真正决定项目好坏的是你把它组合成什么样。该用el-form的时候别自己写正则校验,该用el-table的时候别手动拼接表格字符串,组件库的价值就是让你把精力从轮子制造里解放出来,放到业务逻辑和用户体验上。
另外我强烈建议,新项目里把主题变量和暗黑模式从第一天就设计进去。不要等界面全部写完了,再想着换肤。CSS 变量这个东西最大的好处就是后改成本也低,但如果你自己写死了一堆十六进制色值,后面升级主题的时候就会非常痛苦,满屏找色号,实在酸爽。
最后再分享一个小习惯:升级 Element Plus 版本的时候,别直接npm update一把梭,要先去官方 Changelog 看一眼 breaking changes。我记得从 2.5 升级到 2.6 那会儿,有几个组件的事件参数做了调整,我的老代码直接跑出undefined,排查花了不少时间。组件库升级,稳字当头。