后台选组件库,选的不是好不好看,而是"它替你做掉了哪些决定,又把哪些硬塞给你"。Element Plus 的强项是表单、表格、弹窗这些刚需;弱项是自由度——默认值很强预设,改起来要么顺它的变量体系,要么跟它的 DOM 结构打架。
这篇讲六件事:版本对应 → 引入方式 → 主题 → 国际化 → 表单与表格 → 封装边界,末尾列坑。
1. 版本对应:Vue 3 只有 Element Plus
| 包 | Vue 版本 | 状态 | 说明 |
|---|---|---|---|
element-ui |
Vue 2 | 维护模式 | 不跟进 Vue 3,babel-plugin-component 也停在 Vue 2 |
element-plus |
Vue 3.2+ | 活跃 | TS + 组合式 API 重写,API 有大量改动 |
迁移易漏三处:el-icon 换成 @element-plus/icons-vue 组件;size="medium" 已废弃;el-table 的 slot-scope 全换 #default="scope"。按 Element Plus 2.x 文档核对再动手,能省一半返工。
2. 引入方式:全量还是按需
// 全量:两行搞定,代价全进包
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
// 按需:vite.config.ts,样式自动带上
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()], dts: 'src/auto-imports.d.ts' }),
Components({ resolvers: [ElementPlusResolver({ importStyle: 'css' })], dts: 'src/components.d.ts' }),
],
})
| 维度 | 全量 | 按需 |
|---|---|---|
| 配置成本 | 两行 | 两个插件 + d.ts |
| 产物 | 组件与样式全进包 | 只含模板里用到的 |
| 函数式组件 | 自动可用 | 靠 AutoImport 的 resolver 兜 |
<component :is> 动态用法 |
可用 | 失效,解析器看不到运行时字符串 |
| SCSS 变量定制 | 需重写入口 | 需 importStyle: 'sass' |
两条硬边界:
- 按需靠静态分析。
<component :is="'ElButton'">、h(ElButton)不显式import就会运行时找不到组件。 importStyle决定 SCSS 变量能否覆盖:'css'是预编译产物,改$colors无效;要 SCSS 定制就改'sass',编译更慢。
别凭感觉判断按需一定更小。vite build 后看 dist/assets 各 chunk 体积,或挂 rollup-plugin-visualizer 核对。
3. 主题:CSS 变量优先,SCSS 兜底
Element Plus 的样式分两层:SCSS 变量(编译期)生成 CSS 变量(运行期)。运行时能改的都走 CSS 变量。
/* 换主色必须同时补齐派生色阶 */
:root {
--el-color-primary: #2563eb;
--el-color-primary-light-3: #5b8def;
--el-color-primary-light-5: #8fb2f5;
--el-color-primary-light-9: #eef4fe;
--el-color-primary-dark-2: #1d4ed8;
--el-border-radius-base: 6px;
}
html.dark { /* 暗色要单独写一份 */
--el-bg-color: #1a1a1a;
--el-text-color-primary: #e5e7eb;
}
只改 --el-color-primary 是最常见的半成品:light-3/light-5/light-9 是独立定义的,不跟着变,于是按钮 hover 还是旧色、el-tag 浅底还是旧色阶。
// 想一次性生成整套色阶,走 SCSS 变量
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: ('primary': ('base': #2563eb), 'success': ('base': #16a34a))
);
@use 'element-plus/theme-chalk/src/index.scss' as *;
| 定制层次 | 手段 | 生效时机 | 适用 |
|---|---|---|---|
| 单点微调 | CSS 变量 | 运行时 | 品牌色、圆角、字号 |
| 整族色阶 | SCSS $colors |
编译期 | 换主色且要全部派生色 |
| 组件结构 | :deep() 覆盖 |
运行时 | 布局微调,但脆弱 |
| 深度定制 | 自建组件 | — | 库已不适用 |
html.dark 只是把 --el-bg-color、--el-text-color-primary 重新赋值一遍,自定义的"品牌色浅底"也得给暗色写一份,否则会亮得刺眼。
4. 国际化
<!-- App.vue:组件库文案跟随 vue-i18n 的 locale -->
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
const { locale } = useI18n()
const elLocale = computed(() => (locale.value === 'zh-CN' ? zhCn : en))
</script>
<template>
<el-config-provider :locale="elLocale" :z-index="3000">
<router-view />
</el-config-provider>
</template>
el-config-provider 是全局配置唯一入口:语言包、默认尺寸、弹层 z-index 起始值。弹窗套弹窗 z-index 打架,多半是没设或设得太低。语言包按需 import,别 import * as lang。
5. 表单校验
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
const formRef = ref<FormInstance>()
const form = reactive({ name: '', email: '', age: 0 })
const rules: FormRules = {
name: [{ required: true, message: '必填', trigger: 'blur' }],
email: [{ type: 'email', message: '格式不正确', trigger: ['blur', 'change'] }],
age: [{
validator: (_r, v, cb) => (v < 18 ? cb(new Error('需年满 18 岁')) : cb()),
trigger: 'change',
}],
}
async function submit() {
const ok = await formRef.value?.validate().catch(() => false)
if (!ok) return
await fetch('/api/users', { method: 'POST', body: JSON.stringify(form) })
}
</script>
| 用法 | 返回 | 何时用 |
|---|---|---|
validate() 无参 |
Promise<boolean>,失败 reject |
await 流程 |
validate(cb) |
undefined,回调拿 (valid, fields) |
要知道哪几个字段失败 |
validateField(['a']) |
Promise |
分步表单只校验当前步 |
clearValidate() |
— | 弹窗重开清红字 |
三条规则:await validate() 失败是 reject 不是返回 false,不接 .catch 就是未捕获拒绝;自定义 validator 必须调 callback;trigger: 'change' 在 el-input 上是每次输入都触发,要"失焦校验 + 输入清红字"就用 blur + clearValidate。
6. el-table 的大数据量问题
el-table 每个单元格一个节点,一万行即几万个 DOM,卡在渲染不在数据。
| 方案 | 适用量级 | 代价 |
|---|---|---|
服务端分页 + el-pagination |
任意 | 需后端支持,不能一屏看全 |
前端分页(拿全量再 slice) |
数千行 | 首屏传输大 |
el-table-v2(虚拟滚动) |
十万行级 | API 不同,插槽能力弱 |
换 vxe-table 等 |
十万行 + 复杂表头 | 换库成本 |
<script setup lang="ts">
const page = ref(1), size = ref(50)
const all = ref<Row[]>([])
const pageData = computed(() =>
all.value.slice((page.value - 1) * size.value, page.value * size.value))
</script>
<template>
<el-table :data="pageData" height="600" border>
<el-table-column prop="id" label="ID" width="80" fixed />
<el-table-column prop="name" label="名称" show-overflow-tooltip />
</el-table>
<el-pagination v-model:current-page="page" :page-size="size"
:total="all.length" layout="total, prev, pager, next" />
</template>
el-table-v2 与 el-table 不是同一套 API:列走 columns 数组、单元格走 cellRenderer。选它前先确认多级表头、行合并、fixed 列——这三项在 v2 上要么不支持,要么写法完全不同。
两条细节:show-overflow-tooltip 比自定义 tooltip 轻得多;fixed 列会复制 DOM,列多翻倍。
7. 二次封装的边界
| 场景 | 该封装 | 直接改样式 |
|---|---|---|
| 同一业务规则反复出现(金额右对齐 + 千分位) | ✅ | — |
| 统一交互(删除二次确认) | ✅ | — |
| 只想让按钮圆角不同 | — | ✅ CSS 变量 |
| 要改组件 DOM 结构 | ✅ 自建 | ❌ :deep() 会随版本碎掉 |
| 要透传全部 props/事件 | ✅ v-bind="$attrs" |
— |
判断标准只有一条:调用方少写的代码是否大于封装本身的维护成本。只包 type 和 size 的 BaseButton 是纯负债。要封就封到业务语义层:
<script setup lang="ts">
defineOptions({ inheritAttrs: false })
withDefaults(defineProps<{ label?: string; loading?: boolean }>(), { label: '保存' })
</script>
<template>
<el-button v-bind="$attrs" type="primary" :loading="loading">{{ label }}</el-button>
</template>
inheritAttrs: false 是关键:不关掉自动继承,class 和 style 会同时落在根节点和内部 el-button 上。
8. 组件库与设计系统
组件库是实现,设计系统是规范。性价比最高的做法是加一层 token:颜色、圆角、字号收进 --app-*,再映射到 --el-*,换库时改动面只剩映射层。直接对着组件库画设计稿,做出来的后台会长得像别人的。
9. 排错:六个高频现象
| 现象 | 原因 | 处理 |
|---|---|---|
弹窗内 formRef 为 null |
el-dialog 默认不渲染内容 |
在 @open 或 nextTick 后取值 |
el-select 绑对象选不中 |
未指定 value-key |
设 value-key="id",或改绑 id |
| 暗色下自定义区域仍是亮色 | 只覆盖了 :root |
补 html.dark 一套 |
| 主色改了 hover 还是旧色 | 派生色阶未同步 | 补齐 light-3/light-5,或用 SCSS $colors |
ElMessage 样式丢失 |
按需引入但没走 resolver | import 'element-plus/es/components/message/style/css' |
| 表格滚动表头错位 | height 用百分比或祖先有 transform |
给固定像素高度,去掉 transform |
第一条最高频:el-dialog 未打开前内容不存在,onMounted 里 resetFields() 必然报错。
10. 边界:什么时候别用 Element Plus
| 场景 | 更合适 | 理由 |
|---|---|---|
| C 端展示页 | 自建 / Tailwind | 默认样式是"后台味" |
| 强设计驱动产品 | 无头组件(Radix Vue / Headless UI) | 结构自由,样式自控 |
| 只要三五个组件 | 手写或用 VueUse | 引入成本大于收益 |
| 极致包体 | 按需 + 自建轻组件 | 运行时依赖无法完全 tree-shake |
| 复杂数据网格 | AG Grid / TanStack Table | el-table 的表头与列能力有天花板 |
Element Plus 的最佳射程是中后台管理系统:表单多、表格多、弹窗多、交互固定、用户是内部人员。离得越远,每小时都在跟默认值谈判。
11. 自测
element-ui能配 Vue 3 吗?迁移时哪三处最容易漏?- 按需引入在哪种写法下会失效,为什么?
- 只改
--el-color-primary,为什么按钮 hover 色没变? validate()失败时返回什么,不接.catch会怎样?el-table与el-table-v2的 API 差异具体在哪三点?- 弹窗里的表单为什么
formRef一开始是null?