写在前面
很多人学 Element Plus 从背 API 开始,这样学到的只是组件的"怎么用"。本篇想回答更深层的问题:UI 组件库的本质价值是什么?"全量引入"和"按需引入"的差距有多大、原理是什么?Form 的校验系统是如何工作的?Table 处理大数据量时为什么会卡,怎么解决?主题定制应该用 CSS 变量还是 SCSS?什么时候值得二次封装,什么时候不值得?和 Vite 的关系又是怎样的?
本文主要参考 Element Plus 官方文档 及相关技术实践。
目录
- [1. Element Plus 的来历与设计理念](#1. Element Plus 的来历与设计理念)
- [2. 组件库的本质价值:不是省代码,是约束](#2. 组件库的本质价值:不是省代码,是约束)
- [3. 三种引入方式:全量 vs 按需 vs 手动](#3. 三种引入方式:全量 vs 按需 vs 手动)
- [4. 表单系统深度解析:Form + FormItem + 校验](#4. 表单系统深度解析:Form + FormItem + 校验)
- [5. 表格系统:从基础表格到大数据虚拟滚动](#5. 表格系统:从基础表格到大数据虚拟滚动)
- [6. 弹窗与反馈组件体系](#6. 弹窗与反馈组件体系)
- [7. 主题定制:CSS 变量 vs SCSS 变量](#7. 主题定制:CSS 变量 vs SCSS 变量)
- [8. 国际化与全局配置](#8. 国际化与全局配置)
- [9. 与 Vite 的关系:构建工具层面的深度协作](#9. 与 Vite 的关系:构建工具层面的深度协作)
- [10. 二次封装:时机、原则与陷阱](#10. 二次封装:时机、原则与陷阱)
- [11. 同类库选型对比](#11. 同类库选型对比)
- [12. 常见坑深度解析](#12. 常见坑深度解析)
- 小结
1. Element Plus 的来历与设计理念
1.1 从 Element UI 到 Element Plus
Element UI 是饿了么前端团队在 2016 年为 Vue 2 开发的组件库,在国内企业后台系统中占据了极高的市场份额。随着 Vue 3 在 2020 年正式发布,Vue 2 的生态逐步迁移,Element UI 也随之升级为 Element Plus,专为 Vue 3 重写。
主要变化:
- 全面支持 Vue 3:使用 Composition API 重写组件内部逻辑,更好地支持 Tree Shaking
- TypeScript 重写:全面使用 TypeScript,提供完善的类型定义
- Vite 友好:基于 ES Module,原生支持 Vite 的按需编译
- CSS 变量主题系统:主题定制从 SCSS 变量迁移到 CSS 变量,支持运行时动态切换
- 新增组件:Table V2(虚拟滚动表格)、ConfigProvider(全局配置)等
1.2 Element Plus 的四条设计原则
这是理解整个组件库设计哲学的基础,官方文档明确列出:
一致(Consistency):与现实生活的流程和逻辑保持一致,遵循用户习惯的语言和概念;界面中所有元素和结构保持一致,包括设计样式、图标、文本、元素位置。
反馈(Feedback):通过界面样式和交互动效让用户可以清晰感知自己的操作;操作后,通过页面元素的变化清晰展现当前状态。
效率(Efficiency):简化流程,设计简洁直观的操作流程;语言表达清晰且表意明确,让用户快速理解进而作出决策。
可控(Controllability):根据场景给予用户操作建议或安全提示,但不能代替用户进行决策;用户可以自由地进行操作,包括撤销、回退和终止当前操作。
这四条原则不只是理念宣言,它们直接体现在每个组件的设计决策中:为什么 Dialog 默认点击遮罩可以关闭(可控)?为什么 Form 的错误提示在输入框下方而不是弹窗(反馈)?为什么 Button 有 loading 状态(反馈)?理解这些,你就能判断什么时候覆盖默认行为是合理的,什么时候是在破坏设计一致性。
2. 组件库的本质价值:不是省代码,是约束
很多人把组件库理解为"省代码的工具",这严重低估了它的价值。一个 <el-dialog> 替代 100 行 div + CSS,确实省了代码;但如果只是为了省代码,你完全可以自己封装一个 Dialog 工具函数。
组件库真正的价值在于这几个层面:
2.1 交互一致性:降低用户认知成本
如果每个弹窗的关闭按钮位置不一样,有的在左上角,有的在右上角;每个表单的错误提示有的是红色文字,有的是弹出 alert------用户在使用你的系统时,每次都要重新"学习"操作规则,认知成本极高。
Element Plus 定义了一套统一的交互规范:确认按钮永远是主色,弹窗关闭按钮在右上角,表单错误提示在输入框下方......使用 Element Plus 的整个系统,用户建立起一套认知模型后,在任何页面都能复用这个模型。
2.2 无障碍访问(Accessibility):被忽视的工程质量
WAI-ARIA 规范要求交互组件要有正确的 role、aria-label、aria-expanded、tabindex 等属性,以支持屏幕阅读器和键盘导航。
自己写组件很容易完全忽略这些。Element Plus 的组件默认支持:键盘 Tab 切换焦点、Enter/Space 触发按钮、Esc 关闭弹窗、屏幕阅读器正确朗读状态......这些是专业工程质量的体现,也是某些行业(政府、金融)的合规要求。
2.3 设计令牌(Design Token):系统级的设计一致性
颜色、间距、字体大小、圆角、阴影......这些视觉参数如果散落在各个组件的 CSS 里,改一个主题色需要全局搜索替换,极易遗漏。
Element Plus 通过 CSS 变量(--el-color-primary、--el-border-radius-base......)统一管理所有视觉参数。修改主题时,只改变量值,所有组件自动更新。
2.4 浏览器兼容性:已经踩过的坑
position: sticky 在某些 Safari 版本的表现不一致;CSS Grid 在 iOS 15 以下有 bug;某些 focus-visible 行为在不同浏览器里不一样......Element Plus 团队已经测试并修复了这些问题。自己写组件就要自己踩这些坑。
结论:组件库不是偷懒,是用别人已经解决的工程问题,把你的时间留给你的业务问题。
3. 三种引入方式:全量 vs 按需 vs 手动
不同的引入方式在开发体验 和包体积之间有明确的权衡,了解原理才能做出合理选择。
3.1 全量引入:最简单,但包最大
typescript
// 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, { size: 'small', zIndex: 3000 })
app.mount('#app')
全量引入会把 Element Plus 的所有组件和样式都打进 Bundle,即使你只用了 10 个组件。
打包体积参考(gzip 前):
- JS:约 1.1 MB
- CSS:约 280 KB
- gzip 后:JS 约 280 KB,CSS 约 65 KB
对于内网系统、原型开发、概念验证,全量引入完全可以接受,开发最简单,不需要额外配置。
3.2 自动按需引入(官方推荐)
这是 Element Plus 官方推荐的方式,通过 unplugin-auto-import 和 unplugin-vue-components 两个 Vite/Webpack 插件,在构建时自动分析代码,只打包实际使用到的组件。
bash
npm install -D unplugin-vue-components unplugin-auto-import
typescript
// vite.config.ts
import { defineConfig } from 'vite'
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: [
AutoImport({
resolvers: [ElementPlusResolver()],
// 自动导入 Vue、VueRouter 等 API(可选,但推荐)
imports: ['vue', 'vue-router'],
dts: 'src/auto-imports.d.ts',
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts',
}),
],
})
配置完成后,在组件里完全不需要手动 import:
vue
<script setup>
// 不需要:import { ElButton, ElForm } from 'element-plus'
// 不需要:import { ElMessage } from 'element-plus'
const visible = ref(false)
const handleSuccess = () => {
ElMessage.success('操作成功') // ElMessage 自动可用
}
</script>
<template>
<!-- ElButton、ElForm、ElInput 等组件自动注册,无需手动导入 -->
<el-button @click="visible = true">打开弹窗</el-button>
<el-dialog v-model="visible" title="提示">内容</el-dialog>
</template>
体积效果:只打包用到的组件,实际项目中通常能减少 40%~60% 的 Element Plus 相关体积。
两个插件的分工:
unplugin-auto-import:处理 JS/TS 中的函数调用 ,如ElMessage.success()、ElMessageBox.confirm()unplugin-vue-components:处理模板中的组件标签 ,如<el-button>、<el-form>
3.3 手动引入:最精细,成本最高
vue
<script>
import { ElButton, ElInput } from 'element-plus'
export default {
components: { ElButton, ElInput }
}
</script>
手动引入在 Element Plus 的基础上已经支持 Tree Shaking(ES Module 格式),但样式需要额外处理(使用 unplugin-element-plus 插件)。
适用场景:需要精确控制引入哪些组件的特殊情况,日常项目推荐用自动按需引入。
4. 表单系统深度解析:Form + FormItem + 校验
表单是后台系统的核心交互,深入理解 Element Plus 的表单系统能避免大量问题。
4.1 表单系统的三层结构
el-form(数据模型层)
└── el-form-item(字段层:绑定 prop、包含校验规则)
└── 实际控件(el-input、el-select、el-date-picker......)
这个结构不是随意的设计,而是有明确分工:
el-form持有整个表单的数据模型(:model)和全局校验规则(:rules)el-form-item的prop属性是连接数据字段和校验规则的桥梁 ,必须和:model里的字段名一致- 控件通过
v-model双向绑定到form对象的对应字段
vue
<script setup>
const formRef = ref(null)
// form 对象的字段名,必须与 el-form-item 的 prop 一致
const form = reactive({
username: '',
email: '',
role: '',
})
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
],
}
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<!-- prop="username" 必须和 form.username 的字段名一致 -->
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
<el-button @click="formRef.resetFields()">重置</el-button>
</el-form-item>
</el-form>
</template>
4.2 校验规则的工作原理
Element Plus 的表单校验底层使用 async-validator 库,这解释了为什么校验可以是异步的。
trigger 的两个值:
'blur':失去焦点时触发,适合大多数输入字段'change':值变化时实时触发,适合 Select、DatePicker 等选择类组件
自定义校验函数:
javascript
const rules = {
username: [
{
validator: async (rule, value, callback) => {
if (!value) {
callback(new Error('请输入用户名'))
return
}
// 异步校验:检查用户名是否已被占用
try {
const { exists } = await api.checkUsername(value)
if (exists) {
callback(new Error('用户名已被占用'))
} else {
callback() // 校验通过,必须调用 callback()
}
} catch (e) {
callback() // 网络错误时不阻塞提交
}
},
trigger: 'blur',
}
]
}
手动触发校验:
javascript
const handleSubmit = async () => {
try {
// validate() 返回 Promise,校验失败会 reject
await formRef.value.validate()
// 走到这里说明校验全部通过
await api.createUser(form)
ElMessage.success('创建成功')
} catch (errors) {
// errors 是校验失败的字段列表,通常不需要处理
console.log('校验失败', errors)
}
}
// 只校验指定字段
await formRef.value.validateField(['username', 'email'])
// 清除指定字段的校验状态
formRef.value.clearValidate(['username'])
// 重置表单(清空值 + 清除校验状态)
formRef.value.resetFields()
4.3 动态表单:校验规则的动态增删
后台系统里常见"动态添加行"的表单(如批量添加用户、商品规格配置),此时 prop 需要动态生成:
vue
<script setup>
const form = reactive({
items: [
{ name: '', price: '' }
]
})
const addItem = () => {
form.items.push({ name: '', price: '' })
}
const removeItem = (index) => {
form.items.splice(index, 1)
}
</script>
<template>
<el-form :model="form">
<el-form-item
v-for="(item, index) in form.items"
:key="index"
:prop="`items.${index}.name`"
:rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
:label="`商品 ${index + 1}`"
>
<el-input v-model="item.name" />
<el-button @click="removeItem(index)" type="danger">删除</el-button>
</el-form-item>
<el-button @click="addItem">+ 添加商品</el-button>
</el-form>
</template>
注意 prop 的写法:items.0.name、items.1.name......这是 async-validator 支持的嵌套路径语法。
5. 表格系统:从基础表格到大数据虚拟滚动
5.1 基础表格
vue
<template>
<el-table
:data="tableData"
@selection-change="handleSelectionChange"
border
stripe
>
<!-- 多选列 -->
<el-table-column type="selection" width="55" />
<!-- 序号列 -->
<el-table-column type="index" label="序号" width="60" />
<!-- 数据列 -->
<el-table-column prop="name" label="姓名" width="120" sortable />
<el-table-column prop="email" label="邮箱" min-width="200" />
<!-- 自定义渲染列 -->
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '在职' : '离职' }}
</el-tag>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="fetchList"
@size-change="fetchList"
/>
</template>
几个重要属性说明:
fixed="right":列固定到右侧,横向滚动时不随之移动,操作列必备min-widthvswidth:width是固定宽度,min-width是最小宽度,剩余空间会分配给min-width的列sortable:启用排序(前端排序),加:sort-method="customSort"可自定义排序逻辑
5.2 大数据量时的性能问题
当表格需要展示 1000 条以上数据 时,普通 el-table 会出现明显卡顿。原因是:
DOM 渲染是昂贵的。1000 行 × 10 列 = 10000 个 DOM 节点,每次滚动触发回流(reflow)的成本极高。浏览器一帧只有 16.6ms(60fps),一次大量 DOM 操作就可能超过这个预算,导致卡顿。
普通表格的渲染模型:
所有数据 → 全部渲染为 DOM → 用户只看到可视区域的几十行
↑
1000 个 <tr> 节点全部存在于内存中
5.3 虚拟滚动:Table V2
Element Plus 提供了 el-table-v2(Table V2)组件,基于虚拟滚动技术解决大数据量问题:
虚拟滚动的核心思想:只渲染当前可见区域(viewport)内的行,滚动时动态替换 DOM 内容,始终只保持少量 DOM 节点。
虚拟滚动渲染模型:
视口高度 600px,每行 40px → 可见行数约 15 行
缓冲区:上下各多渲染 5 行(防止滚动时白屏)
实际渲染 DOM:约 25 行(无论数据有多少)
用户感知到的是"滚动了 1000 行",但实际上只有 25 行 DOM 存在
vue
<script setup>
import { ref } from 'vue'
const columns = [
{ key: 'id', dataKey: 'id', title: 'ID', width: 80 },
{ key: 'name', dataKey: 'name', title: '姓名', width: 120 },
{ key: 'email', dataKey: 'email', title: '邮箱', width: 200 },
]
// 可以轻松处理 10000 条数据
const tableData = ref(
Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
name: `用户${i + 1}`,
email: `user${i + 1}@example.com`,
}))
)
</script>
<template>
<el-table-v2
:columns="columns"
:data="tableData"
:width="700"
:height="400"
fixed
/>
</template>
Table V2 vs 普通 Table 的性能对比:
| 数据量 | 普通 el-table | el-table-v2 |
|---|---|---|
| 100 行 | 流畅 | 流畅 |
| 500 行 | 轻微卡顿 | 流畅 |
| 1000 行 | 明显卡顿 | 流畅 |
| 5000 行 | 严重卡顿/崩溃 | 流畅 |
| 10000+ 行 | 不可用 | 流畅 |
什么时候用 Table V2?
- 数据量确实超过 1000 行
- 需要前端加载全量数据(如离线应用、Excel 导出预览)
- 有性能要求的场景
大多数后台系统不需要 Table V2 ,因为有分页机制,每页 20~100 条,普通 el-table 完全够用。在设计表格时,优先考虑分页而不是虚拟滚动。
6. 弹窗与反馈组件体系
Element Plus 的弹窗和反馈组件分为两类:声明式 (在模板里声明)和命令式(在 JS 里调用)。
6.1 声明式:Dialog、Drawer
vue
<script setup>
const visible = ref(false)
const handleClose = (done) => {
ElMessageBox.confirm('确定关闭吗?').then(done).catch(() => {})
}
</script>
<template>
<!-- v-model 控制显示/隐藏 -->
<!-- :before-close 拦截关闭行为(用于"有未保存更改时询问" -->
<el-dialog
v-model="visible"
title="用户详情"
width="600px"
:before-close="handleClose"
draggable
>
<!-- 对话框内容 -->
<UserForm :user-id="currentUserId" />
<!-- 底部按钮区域 -->
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">确认</el-button>
</template>
</el-dialog>
</template>
Drawer 抽屉组件与 Dialog API 几乎一致,区别是从侧边滑出而非居中弹出,适合表单填写场景:
vue
<el-drawer v-model="drawerVisible" title="新建用户" size="40%">
<UserCreateForm @success="handleSuccess" />
</el-drawer>
6.2 命令式:Message、MessageBox、Notification
命令式 API 适合在异步操作回调里调用,不适合在模板里声明:
javascript
// Message:轻量级提示(左上角或顶部,3秒自动消失)
ElMessage.success('保存成功')
ElMessage.error('保存失败:网络超时')
ElMessage.warning('数据即将过期,请及时更新')
ElMessage.info('后台同步中,请稍候')
// 可配置项
ElMessage({
message: '保存成功',
type: 'success',
duration: 5000, // 显示时长,0 表示不自动关闭
showClose: true, // 显示关闭按钮
})
// MessageBox:需要用户确认的对话框
const handleDelete = async (id) => {
try {
// confirm 返回 Promise,点击"确定"resolve,点击"取消"reject
await ElMessageBox.confirm('确定删除该用户吗?此操作不可撤销', '警告', {
type: 'warning',
confirmButtonText: '确定删除',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--danger', // 删除操作用危险色
})
await api.deleteUser(id)
ElMessage.success('删除成功')
fetchList()
} catch {
// 用户点取消,不做任何处理
}
}
// Notification:需要持久显示的通知(右上角)
ElNotification({
title: '新消息',
message: '你有 3 条待处理审批',
type: 'info',
duration: 0, // 0 表示不自动关闭
onClick: () => router.push('/approve'),
})
三者的选择场景:
| 场景 | 使用 |
|---|---|
| 操作成功/失败的简短提示 | ElMessage |
| 需要用户确认的危险操作 | ElMessageBox.confirm |
| 需要用户输入内容 | ElMessageBox.prompt |
| 需要长时间显示、用户主动关闭的通知 | ElNotification |
| 页面内的持久提示(如权限不足) | el-alert(声明式) |
7. 主题定制:CSS 变量 vs SCSS 变量
Element Plus 提供了两套主题定制方案,各有适用场景。
7.1 方案一:CSS 变量(推荐,简单灵活)
CSS 变量(Custom Properties)是现代浏览器原生支持的特性,Element Plus 2.x 已全面用 CSS 变量重构了组件样式系统。
css
/* src/styles/element-theme.css */
:root {
/* 主色:影响按钮、链接、选中状态等 */
--el-color-primary: #1890ff;
/* 主色的派生色(Element Plus 会根据主色自动生成这些,但也可以手动覆盖) */
--el-color-primary-light-3: #6cb8ff; /* 悬停色 */
--el-color-primary-light-5: #95ccff; /* 浅色背景 */
--el-color-primary-light-7: #c2e0ff; /* 更浅 */
--el-color-primary-light-8: #d6ebff;
--el-color-primary-light-9: #ecf5ff; /* 极浅背景 */
--el-color-primary-dark-2: #1060cc; /* 加深色 */
/* 尺寸 */
--el-border-radius-base: 4px;
--el-font-size-base: 14px;
/* 边框 */
--el-border-color: #dcdfe6;
}
typescript
// main.ts
import { createApp } from 'vue'
import 'element-plus/dist/index.css'
import './styles/element-theme.css' // 在 Element Plus CSS 之后引入,才能覆盖
import App from './App.vue'
运行时动态修改(实现暗色模式切换):
typescript
// 通过 JS 动态修改 CSS 变量
const toggleDarkMode = (isDark: boolean) => {
const el = document.documentElement
if (isDark) {
el.style.setProperty('--el-color-primary', '#409eff')
el.style.setProperty('--el-bg-color', '#141414')
el.style.setProperty('--el-text-color-primary', '#e5eaf3')
el.classList.add('dark')
} else {
el.style.removeProperty('--el-color-primary')
el.style.removeProperty('--el-bg-color')
el.classList.remove('dark')
}
}
// 搭配 VueUse 的 useDark 更优雅
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleDark = useToggle(isDark)
7.2 方案二:SCSS 变量(深度定制,更复杂)
如果需要比 CSS 变量更深层的定制(如修改 Sass mixin 的计算逻辑),需要用 SCSS 方案:
scss
// src/styles/element-variables.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #1890ff,
),
'success': (
'base': #52c41a,
),
)
);
typescript
// vite.config.ts
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
// 每个 scss 文件编译前都注入这个变量文件
additionalData: `@use "~/styles/element-variables.scss" as *;`,
},
},
},
})
CSS 变量 vs SCSS 变量的选择:
| 维度 | CSS 变量 | SCSS 变量 |
|---|---|---|
| 配置复杂度 | 低,直接写 CSS | 高,需要配置 Vite preprocessor |
| 运行时切换 | ✅ 支持(动态修改 CSS 变量) | ❌ 不支持(编译时确定) |
| 定制深度 | 中(样式层面) | 深(可修改 Sass 计算逻辑) |
| 构建速度影响 | 无 | 有影响(每个文件都注入变量) |
| 推荐场景 | 主题色定制、暗色模式 | 需要修改组件尺寸计算规则 |
大多数后台系统用 CSS 变量方案就够了,SCSS 方案留给有深度定制需求的项目。
8. 国际化与全局配置
8.1 国际化配置
Element Plus 组件内置的文字(如日期选择器的"今天"、分页的"共X条")默认是英文。中文项目必须配置国际化:
typescript
// main.ts
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'dayjs/locale/zh-cn' // 日期组件(DatePicker)依赖 Day.js,也需要设置
const app = createApp(App)
app.use(ElementPlus, {
locale: zhCn,
})
如果用的是按需引入,需要用 el-config-provider:
vue
<!-- App.vue -->
<template>
<el-config-provider :locale="zhCn" :size="size" :z-index="zIndex">
<router-view />
</el-config-provider>
</template>
<script setup>
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'dayjs/locale/zh-cn'
const size = ref('default') // 全局组件尺寸
const zIndex = ref(2000) // 全局弹出组件 z-index
</script>
8.2 全局尺寸配置
Element Plus 支持三种尺寸:large、default、small。后台系统通常设置 small 来展示更多信息:
typescript
app.use(ElementPlus, { size: 'small' })
也可以在运行时根据用户偏好动态切换:
vue
<el-config-provider :size="currentSize">
<app-content />
</el-config-provider>
<script setup>
// 用 localStorage 持久化用户的尺寸偏好
const currentSize = useStorage('app-size', 'default')
</script>
9. 与 Vite 的关系:构建工具层面的深度协作
Element Plus 本身是框架无关的组件库,但它与 Vite 的配合有几个值得深入理解的地方。
9.1 ES Module 与 Tree Shaking
Element Plus 提供了 ES Module 格式的包(element-plus/es),这使得 Vite(底层 Rolldown/Rollup)可以进行 Tree Shaking,只打包实际导入的组件。
// 这是 element-plus/es 的结构(简化版)
element-plus/es/
├── components/
│ ├── button/index.mjs
│ ├── input/index.mjs
│ ├── table/index.mjs
│ └── ...(每个组件独立)
└── index.mjs(入口,导出所有组件)
当你使用自动按需引入时,Vite 只会打包你真正用到的组件文件,未用到的组件文件被 Tree Shaking 排除在外。
9.2 样式的按需处理
组件 JS 被按需引入了,但对应的 CSS 也需要按需引入,否则要么漏样式,要么全量引入 CSS。
unplugin-vue-components 的 ElementPlusResolver 会自动处理这个问题:识别到你用了 <el-button>,在注入组件 JS 的同时,也注入对应的 button.css。
样式注入的两种模式 (通过 ElementPlusResolver 的 importStyle 参数控制):
javascript
Components({
resolvers: [
ElementPlusResolver({
importStyle: 'css', // 默认:引入编译好的 CSS 文件
// importStyle: 'sass', // 引入 SCSS 源文件(配合 SCSS 主题定制时使用)
})
]
})
9.3 Vite dev server 的 HMR 与 Element Plus
修改 Element Plus 相关的代码时,Vite 的 HMR 表现如下:
- 修改组件的
<template>或<script setup>:精准 HMR,只更新该组件 - 修改CSS 变量文件(如 element-theme.css):CSS HMR 即时生效,无需刷新页面,主题色实时更新
- 修改 SCSS 主题变量 (
element-variables.scss):触发 SCSS 重新编译,稍慢,但仍是 HMR 不是整页刷新
9.4 Vite 的 optimizeDeps 与预构建
Element Plus 是一个大型 npm 包,Vite 在首次启动时会对其进行依赖预构建(用 Rolldown/esbuild 处理),将多个内部 ES Module 文件合并,减少开发阶段的 HTTP 请求数。
如果你遇到 Element Plus 相关的奇怪问题(如组件样式不对、部分功能失效),可以尝试清除预构建缓存:
bash
rm -rf node_modules/.vite
npm run dev
10. 二次封装:时机、原则与陷阱
10.1 什么时候值得封装
场景一:注入业务逻辑
vue
<!-- ❌ 每个页面重复写权限判断 -->
<el-button
v-if="userStore.permissions.includes('user:delete')"
type="danger"
@click="handleDelete"
>
删除
</el-button>
<!-- ✅ 封装成 AuthButton,权限逻辑收归一处 -->
<AuthButton permission="user:delete" type="danger" @click="handleDelete">
删除
</AuthButton>
vue
<!-- components/AuthButton.vue -->
<script setup lang="ts">
interface Props {
permission: string
}
const props = defineProps<Props>()
const userStore = useUserStore()
const hasAuth = computed(() => userStore.permissions.includes(props.permission))
</script>
<template>
<!-- $attrs 透传所有其他属性(type、size、loading 等) -->
<el-button v-if="hasAuth" v-bind="$attrs">
<slot />
</el-button>
</template>
场景二:统一默认配置
项目中所有 Table 都需要 border、stripe,分页都用固定的 page-sizes,每次都手写很繁琐:
vue
<!-- components/AppTable.vue:统一 Table 默认配置 -->
<script setup lang="ts">
// 透传所有 props 给 el-table,同时提供默认值
defineOptions({ inheritAttrs: false })
</script>
<template>
<div>
<el-table
v-bind="$attrs"
border
stripe
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
>
<slot />
</el-table>
<el-pagination
v-if="total"
:current-page="currentPage"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="$emit('page-change', $event)"
@size-change="$emit('size-change', $event)"
/>
</div>
</template>
场景三:封装复杂交互逻辑
带搜索的 el-select(远程搜索 + 防抖 + loading 状态):
vue
<!-- components/RemoteSelect.vue -->
<script setup lang="ts">
interface Props {
fetchOptions: (keyword: string) => Promise<Option[]>
modelValue: string | number
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const loading = ref(false)
const options = ref<Option[]>([])
const handleSearch = useDebounceFn(async (keyword: string) => {
if (!keyword) return
loading.value = true
options.value = await props.fetchOptions(keyword)
loading.value = false
}, 300)
</script>
<template>
<el-select
:model-value="modelValue"
filterable
remote
:remote-method="handleSearch"
:loading="loading"
@update:model-value="emit('update:modelValue', $event)"
v-bind="$attrs"
>
<el-option
v-for="opt in options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</template>
10.2 封装原则
原则一:透传属性(v-bind="$attrs")
封装组件时,用 v-bind="$attrs" 把未在 defineProps 里声明的属性全部透传给底层的 Element Plus 组件。这样你的封装组件可以接受原始组件的所有配置项,不需要逐一转发。
原则二:封装层越薄越好
封装的目的是解决某个具体问题(注入权限、统一默认值、封装复杂交互),而不是重新实现原始组件。如果你的封装组件需要把 el-table 的所有 props 一个个重新声明,说明封装粒度不对。
原则三:命名加业务前缀
封装的组件用 App 或项目名前缀,如 AppTable、AppSelect、AuthButton,与 Element Plus 的 el- 前缀区分,避免命名混淆。
10.3 不值得封装的情况
vue
<!-- ❌ 只用了一两次,直接写 props 更清晰 -->
<el-input v-model="value" placeholder="请输入" clearable :maxlength="50" show-word-limit />
<!-- ❌ 不要为了"统一"而强行封装,反而增加理解成本 -->
<AppInput v-model="value" :max-length="50" :show-count="true" />
<!-- 用的人需要同时查 AppInput 和 el-input 两套文档 -->
11. 同类库选型对比
后台系统不只有 Element Plus 可选,了解各库的边界有助于做出合理选择:
| 库 | Vue 版本 | 定位 | 优势 | 劣势 | 选型建议 |
|---|---|---|---|---|---|
| Element Plus | Vue 3 | 通用后台 | 组件最全、文档好、社区最大、中文友好 | 风格中性,深度定制需覆盖样式 | 新建 Vue 3 后台系统首选 |
| Element UI | Vue 2 | 通用后台 | 生态成熟,很多老项目在用 | Vue 2 维护模式,不再新增功能 | Vue 2 老项目维护 |
| Ant Design Vue | Vue 3/2 | 企业级内网 | 设计规范严谨,Ant Design 生态 | 包体积较大,学习曲线稍陡 | 有设计体系要求的项目 |
| Naive UI | Vue 3 | TypeScript 优先 | 完全 TypeScript,API 设计现代,主题系统灵活 | 社区较小,部分组件不如 EP 完善 | 追求 TS 体验的新项目 |
| Vant | Vue 3/2 | 移动端 H5 | 专为移动端优化,触控体验好,轻量 | 不适合 PC 后台 | 移动端 H5 |
| Tailwind CSS | 框架无关 | 原子化 CSS | 极致灵活,无预设组件束缚,定制容易 | 没有现成的复杂组件(表单、表格需自建) | 强定制需求的官网/产品页 |
选型决策原则:
- 团队熟悉度第一:团队都熟悉 Element Plus,就用 Element Plus,不要为了"新"而换库
- 组件完整度第二:后台系统需要 Table、Form、Tree、DatePicker 等,优先选组件完整的库
- 包体积最后考虑:通过按需引入可以大幅减少体积,这不应是选型的主要依据
12. 常见坑深度解析
12.1 表单重置的正确姿势
vue
<script setup>
const formRef = ref(null)
// ❌ 错误:手动清空值,但不会重置校验状态
const form = reactive({ username: '', email: '' })
const handleReset = () => {
form.username = ''
form.email = ''
// 结果:表单值清了,但红色错误提示还残留
}
// ✅ 正确:用 resetFields() 同时清空值和校验状态
const handleReset = () => {
formRef.value?.resetFields()
}
</script>
重要细节 :resetFields() 会把字段重置到组件初始化时 的值,而不是空值。如果你的"新建"弹窗复用了"编辑"弹窗,弹窗打开前必须先把 form 数据重置到初始状态,否则 resetFields() 会重置到上一次编辑的数据。
javascript
// 打开新建弹窗的正确流程
const handleAdd = () => {
// 先把数据重置为初始状态
Object.assign(form, { username: '', email: '', role: '' })
// 再打开弹窗
nextTick(() => {
formRef.value?.clearValidate() // 清除校验状态
visible.value = true
})
}
12.2 Dialog 关闭时未清理表单状态
vue
<!-- ❌ 常见问题:关闭弹窗后重新打开,上次的校验错误还在 -->
<el-dialog v-model="visible" @close="handleClose">
<el-form ref="formRef" :model="form">
...
</el-form>
</el-dialog>
<script setup>
// ✅ 在弹窗关闭时清理
const handleClose = () => {
nextTick(() => {
formRef.value?.resetFields()
})
}
</script>
12.3 Table 的 row-key 必须唯一
vue
<!-- ❌ 没有设置 row-key,Table 选择、展开等功能会出错 -->
<el-table :data="list">
<!-- ✅ 必须设置唯一键 -->
<el-table :data="list" row-key="id">
row-key 在以下场景是必须的:树形表格、可展开行、保持选中状态(数据更新后选中不丢失)。最佳实践是所有 Table 都设置 row-key。
12.4 Select 选项列表为空时的 loading 状态
vue
<script setup>
const loading = ref(false)
const options = ref([])
// ❌ 忘记处理 loading 状态,用户看不到数据在加载
onMounted(async () => {
options.value = await api.getOptions()
})
// ✅ 显示加载状态
onMounted(async () => {
loading.value = true
options.value = await api.getOptions()
loading.value = false
})
</script>
<template>
<el-select v-loading="loading" v-model="value">
<el-option v-for="opt in options" :key="opt.value" v-bind="opt" />
</el-select>
</template>
12.5 el-table-column 的 prop 和 formatter 混用
vue
<!-- ❌ 同时设置 prop 和 formatter,formatter 不生效 -->
<el-table-column prop="status" label="状态" :formatter="statusFormatter" />
<!-- ✅ 用 formatter 时不需要 prop -->
<el-table-column label="状态" :formatter="statusFormatter" />
<!-- ✅ 或者用 slot,更灵活 -->
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag>{{ statusMap[row.status] }}</el-tag>
</template>
</el-table-column>
小结
读完这篇,希望你对 Element Plus 的理解从"查文档找 API"升级到"理解设计意图":
-
Element Plus 的四条设计原则(一致、反馈、效率、可控)不是宣传语,是每个组件行为背后的决策依据。理解这些,你才知道什么时候应该遵从默认行为,什么时候改变它是合理的。
-
组件库的本质价值是交互一致性、无障碍访问、设计令牌和浏览器兼容性,而不只是省代码。
-
三种引入方式 各有权衡:全量引入简单但体积大,自动按需引入是官方推荐,两个 Vite 插件分工明确------
unplugin-auto-import处理 JS 函数,unplugin-vue-components处理模板组件。 -
表单系统的三层结构 (Form → FormItem → 控件)不是随意设计,
prop属性是字段名和校验规则的桥梁。resetFields()重置的是"初始值"而不是"空值",这是很多 bug 的根源。 -
大数据量时用 Table V2(虚拟滚动),普通表格用分页机制。虚拟滚动通过只渲染可视区域内的行,将 DOM 节点数量从"全量"降为"常数"。
-
主题定制优先用 CSS 变量,配置简单且支持运行时切换(暗色模式)。只有需要修改 Sass 计算逻辑时才用 SCSS 变量方案。
-
与 Vite 的关系 :Element Plus 基于 ES Module,Vite/Rolldown 可以对其 Tree Shaking。
unplugin-vue-components同时处理组件注册和样式注入,CSS 变量的修改通过 Vite HMR 实时生效。 -
二次封装的核心原则 :用
v-bind="$attrs"透传属性,封装层越薄越好,只解决一个具体问题,不要重新发明 Element Plus。