1. 什么是Element Plus?
Element Plus是一套基于Vue 3的桌面端组件库,由饿了么前端团队开源维护。作为Element UI的升级版本,它完全重写了底层代码以适应Vue 3的Composition API特性。我在多个企业级项目中实际使用过Element Plus,发现它的组件丰富度和API设计确实能显著提升开发效率。
与同类UI库相比,Element Plus有几个突出优势:
- 完整的TypeScript支持
- 主题定制系统更灵活
- 国际化方案更成熟
- 性能优化更到位(特别是大数据量表格场景)
注意:虽然Element UI仍可继续使用,但官方已明确表示将主要维护Element Plus,新项目建议直接采用Plus版本。
2. 环境准备与基础配置
2.1 创建Vue 3项目
首先确保已安装最新版Node.js(建议16.x以上),然后通过Vite快速初始化项目:
bash复制npm create vite@latest my-element-project --template vue-ts
cd my-element-project
2.2 安装Element Plus
推荐使用自动导入方案(unplugin-vue-components),这能显著减少打包体积:
bash复制npm install element-plus @element-plus/icons-vue
npm install -D unplugin-vue-components unplugin-auto-import
修改vite.config.ts:
typescript复制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()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
]
})
2.3 全局样式配置
在src/styles目录下创建element-variables.scss:
scss复制@forward "element-plus/theme-chalk/src/common/var.scss" with (
$colors: (
"primary": (
"base": #1890ff,
),
)
);
然后在main.ts中引入:
typescript复制import 'element-plus/theme-chalk/src/index.scss'
import './styles/element-variables.scss'
3. 核心组件实战指南
3.1 表格组件深度使用
Element Plus的el-table在处理大数据量时表现优异。这里分享几个实战技巧:
强制更新表格数据(对应热词问题):
typescript复制// 方法1:使用key强制重新渲染
const tableKey = ref(0)
const reloadTable = () => tableKey.value++
// 方法2:使用doLayout方法
const tableRef = ref()
const handleUpdate = async () => {
await nextTick()
tableRef.value?.doLayout()
}
性能优化配置:
html复制<el-table
:data="tableData"
:row-key="row => row.id"
:height="600"
:virtual-scroll="true"
:loading="loading"
@sort-change="handleSort"
>
<el-table-column prop="date" label="日期" sortable />
<!-- 其他列 -->
</el-table>
3.2 表单验证最佳实践
Element Plus的表单验证基于async-validator,推荐这样组织代码:
typescript复制const formRules = reactive({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 6, max: 16, message: '长度在6到16个字符', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] }
]
})
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
try {
await formEl.validate()
// 验证通过后的逻辑
} catch (err) {
console.error('验证失败:', err)
}
}
3.3 树形控件与配置编辑
针对热词中的"select configuration element in the tree"场景:
html复制<el-tree
:data="configTree"
node-key="id"
:props="defaultProps"
@node-click="handleNodeClick"
:highlight-current="true"
>
<template #default="{ node }">
<span>{{ node.label }}</span>
<el-button
v-if="node.level === 2"
size="small"
@click.stop="editConfig(node)"
>
编辑配置
</el-button>
</template>
</el-tree>
对应的编辑面板实现:
typescript复制const currentConfig = ref(null)
const editConfig = (node) => {
currentConfig.value = node.data
// 打开右侧编辑面板...
}
4. 常见问题解决方案
4.1 样式冲突处理
当与其他UI库混用时,可能出现样式污染。解决方案:
- 使用scoped样式
vue复制<style scoped>
/* 组件内样式 */
</style>
- 自定义命名空间
修改element-variables.scss:
scss复制$namespace: 'ep';
- 深度选择器
scss复制:deep(.el-input__inner) {
background-color: #f5f7fa;
}
4.2 国际化配置
完整的多语言支持方案:
typescript复制import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import en from 'element-plus/dist/locale/en.mjs'
const app = createApp(App)
app.use(ElementPlus, {
locale: currentLang === 'zh' ? zhCn : en
})
4.3 按需加载优化
即使使用自动导入,仍需注意:
- 图标单独引入
typescript复制import { Delete, Search } from '@element-plus/icons-vue'
- 手动排除不常用组件
typescript复制Components({
resolvers: [
ElementPlusResolver({
exclude: /^ElBreadcrumb/ // 排除面包屑组件
})
]
})
5. 进阶开发技巧
5.1 自定义主题开发
推荐使用官方主题生成工具:
- 安装主题工具
bash复制npm i element-plus-theme-generator -D
- 创建主题配置文件
javascript复制// theme-generator.js
const { generateTheme } = require('element-plus-theme-generator')
generateTheme({
config: {
theme: 'custom',
output: 'src/assets/theme',
variables: {
'--el-color-primary': '#409EFF',
'--el-border-radius-base': '4px'
}
}
})
- 在package.json中添加脚本
json复制"scripts": {
"gen-theme": "node theme-generator.js"
}
5.2 组件二次封装实践
以封装增强型DatePicker为例:
vue复制<template>
<el-date-picker
v-model="innerValue"
:type="type"
:disabled-date="disabledDate"
@change="handleChange"
/>
</template>
<script setup>
const props = defineProps({
modelValue: [Date, String, Array],
type: {
type: String,
default: 'date'
},
disabledBefore: {
type: Date,
default: null
}
})
const emit = defineEmits(['update:modelValue'])
const innerValue = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val)
})
const disabledDate = time => {
return props.disabledBefore && time < props.disabledBefore
}
const handleChange = val => {
console.log('选择日期:', val)
}
</script>
5.3 与状态管理库集成
Pinia中使用Element Plus的Loading服务:
typescript复制// stores/loading.ts
import { defineStore } from 'pinia'
import { ElLoading } from 'element-plus'
export const useLoadingStore = defineStore('loading', {
actions: {
showLoading(text = '加载中...') {
return ElLoading.service({
lock: true,
text,
background: 'rgba(0, 0, 0, 0.7)'
})
}
}
})
在组件中使用:
typescript复制const loadingStore = useLoadingStore()
const fetchData = async () => {
const loading = loadingStore.showLoading()
try {
await api.getData()
} finally {
loading.close()
}
}
6. 项目实战经验
6.1 权限管理系统案例
基于Element Plus构建权限系统的关键点:
- 动态菜单实现
vue复制<el-menu
:default-active="activeMenu"
:router="true"
:unique-opened="true"
>
<template v-for="item in permissionStore.routes">
<sidebar-item
v-if="!item.hidden"
:key="item.path"
:item="item"
/>
</template>
</el-menu>
- 按钮权限控制
vue复制<el-button
v-if="hasPermission('user:add')"
type="primary"
@click="handleAdd"
>
新增用户
</el-button>
6.2 复杂表单生成器
利用Element Plus实现动态表单:
typescript复制const formSchema = ref([
{
type: 'input',
label: '用户名',
prop: 'username',
rules: [{ required: true }]
},
{
type: 'select',
label: '角色',
prop: 'role',
options: [
{ label: '管理员', value: 1 },
{ label: '编辑', value: 2 }
]
}
])
const renderFormItem = (item) => {
switch(item.type) {
case 'input':
return <el-input v-model="formData[item.prop]" />
case 'select':
return (
<el-select v-model="formData[item.prop]">
{item.options.map(opt => (
<el-option
key={opt.value}
label={opt.label}
value={opt.value}
/>
))}
</el-select>
)
// 其他类型处理...
}
}
6.3 可视化配置平台
结合Element Plus和JSON Schema:
typescript复制const generateForm = (schema) => {
return h(ElForm, {
model: formData,
rules: formRules
}, () =>
schema.properties.map(prop => (
h(ElFormItem, {
label: prop.title,
prop: prop.name
}, () =>
renderField(prop)
)
))
)
}
const renderField = (field) => {
switch(field.type) {
case 'string':
return h(ElInput, {
modelValue: formData[field.name],
'onUpdate:modelValue': val => formData[field.name] = val
})
case 'number':
return h(ElInputNumber, {
modelValue: formData[field.name],
'onUpdate:modelValue': val => formData[field.name] = val
})
// 其他字段类型...
}
}
7. 性能优化专项
7.1 表格渲染优化
大数据量场景下的实践:
- 虚拟滚动配置
html复制<el-table
:data="bigData"
height="500"
:virtual-scroll="true"
:row-height="50"
>
<!-- 列定义 -->
</el-table>
- 分页策略
typescript复制const pagination = reactive({
pageSize: 50,
currentPage: 1,
total: 0
})
const showData = computed(() => {
const start = (pagination.currentPage - 1) * pagination.pageSize
return bigData.slice(start, start + pagination.pageSize)
})
7.2 按需加载策略
- 组件级动态导入
vue复制<template>
<component :is="dynamicComp" />
</template>
<script setup>
const dynamicComp = shallowRef(null)
onMounted(async () => {
if (needRichEditor) {
const module = await import('./RichEditor.vue')
dynamicComp.value = module.default
}
})
</script>
- 第三方库懒加载
typescript复制const loadBMap = () => {
return new Promise((resolve) => {
if ((window as any).BMap) {
resolve(true)
return
}
const script = document.createElement('script')
script.src = 'https://api.map.baidu.com/api?v=3.0&ak=your_ak&callback=initBMap'
document.head.appendChild(script)
window.initBMap = () => {
resolve(true)
}
})
}
7.3 构建配置优化
vite.config.ts关键配置:
typescript复制export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('element-plus')) {
return 'element-plus'
}
if (id.includes('node_modules')) {
return 'vendor'
}
}
}
},
chunkSizeWarningLimit: 1000
}
})
8. 生态工具推荐
8.1 开发辅助工具
-
Element Plus Helper(VSCode扩展)
- 提供组件代码片段
- 显示组件文档提示
- 快速跳转到官方文档
-
unplugin-icons
bash复制
npm i -D @element-plus/icons-vue unplugin-icons配置vite:
typescript复制import Icons from 'unplugin-icons/vite' export default defineConfig({ plugins: [ Icons({ compiler: 'vue3', autoInstall: true }) ] })
8.2 测试工具链
-
Vitest + Testing Library
bash复制
npm i -D vitest @testing-library/vue @vue/test-utils happy-dom测试示例:
typescript复制import { render, fireEvent } from '@testing-library/vue' import ElButtonDemo from './ElButtonDemo.vue' test('按钮点击事件', async () => { const { getByText, emitted } = render(ElButtonDemo) const btn = getByText('点击我') await fireEvent.click(btn) expect(emitted()).toHaveProperty('click') }) -
Cypress组件测试
bash复制
npm i -D cypress @cypress/vue @cypress/webpack-dev-server配置cypress/plugins/index.js:
javascript复制const { startDevServer } = require('@cypress/webpack-dev-server') const webpackConfig = require('../../vite.config') module.exports = (on, config) => { on('dev-server:start', options => startDevServer({ options, webpackConfig }) ) return config }
8.3 文档工具
-
Storybook集成
bash复制
npx storybook init npm i -D storybook-builder-vite配置.storybook/main.js:
javascript复制module.exports = { stories: ['../src/**/*.stories.@(js|ts|mdx)'], addons: ['@storybook/addon-essentials'], core: { builder: 'storybook-builder-vite' }, async viteFinal(config) { config.plugins.push(require('unplugin-vue-components').vite({ resolvers: [require('unplugin-vue-components/resolvers').ElementPlusResolver()] })) return config } } -
Vitepress文档站点
bash复制
npm i -D vitepress配置docs/.vitepress/config.ts:
typescript复制import { defineConfig } from 'vitepress' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import Components from 'unplugin-vue-components/vite' export default defineConfig({ vite: { plugins: [ Components({ resolvers: [ElementPlusResolver()] }) ] } })
9. 升级与迁移策略
9.1 从Element UI迁移
-
主要变更点:
- 所有组件名称的
el-前缀保持不变 - 图标系统完全重构(需单独安装@element-plus/icons-vue)
- 表单验证的
validate方法返回Promise - 部分组件API调整(如Pagination的current-page改为current)
- 所有组件名称的
-
自动化迁移工具:
bash复制
npm i -D element-migration-helper运行迁移检查:
bash复制
npx emh analyze ./src
9.2 版本升级指南
-
安全升级步骤:
bash复制# 先升级到最新补丁版本 npm install element-plus@2.2.x # 测试通过后再升级小版本 npm install element-plus@2.3.x # 最后考虑大版本升级 npm install element-plus@3.x -
破坏性变更处理:
- 查阅官方Release Notes
- 使用
npm ls element-plus确认依赖关系 - 建立隔离测试环境验证
9.3 多版本共存方案
在微前端场景下的解决方案:
html复制<!-- 主应用 -->
<script>
window.ElementPlus = { locale: zhCn }
</script>
<script src="https://unpkg.com/element-plus@2.3.8/dist/index.full.js"></script>
<!-- 子应用 -->
<script>
const myElement = {
install(app) {
app.config.globalProperties.$ELEMENT = { size: 'small' }
// 手动注册所需组件...
}
}
</script>
10. 移动端适配方案
10.1 响应式布局实践
- 断点处理策略:
scss复制// 覆盖Element Plus默认断点
$--sm: 768px;
$--md: 992px;
$--lg: 1200px;
$--xl: 1920px;
- 组件级适配:
vue复制<el-form :inline="isMobile ? false : true">
<!-- 表单内容 -->
</el-form>
<script setup>
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
</script>
10.2 触摸交互优化
- 增大点击区域:
scss复制.el-button {
min-height: 44px; // 苹果推荐的最小触摸尺寸
padding: 12px 20px;
}
- 手势支持增强:
vue复制<template>
<el-carousel
:autoplay="false"
@swipe-left="next"
@swipe-right="prev"
>
<!-- 轮播项 -->
</el-carousel>
</template>
<script setup>
import { useSwipe } from '@vueuse/core'
const carouselRef = ref(null)
const { isSwiping, direction } = useSwipe(carouselRef)
watch(direction, (dir) => {
if (dir === 'left') carouselRef.value.next()
if (dir === 'right') carouselRef.value.prev()
})
</script>
10.3 移动端专属组件
- 下拉刷新实现:
vue复制<template>
<el-scrollbar
ref="scrollbar"
@scroll="handleScroll"
>
<div
v-show="pullDownState !== 'waiting'"
class="pull-down-tip"
>
{{ pullDownText }}
</div>
<!-- 内容区域 -->
</el-scrollbar>
</template>
<script setup>
const pullDownState = ref('waiting') // waiting | pulling | loading
const startY = ref(0)
const pullDownText = computed(() => {
return {
waiting: '下拉刷新',
pulling: '释放刷新',
loading: '加载中...'
}[pullDownState.value]
})
const handleScroll = (e) => {
if (e.scrollTop <= -50 && pullDownState.value === 'waiting') {
pullDownState.value = 'pulling'
}
}
const handleTouchStart = (e) => {
startY.value = e.touches[0].clientY
}
const handleTouchEnd = () => {
if (pullDownState.value === 'pulling') {
pullDownState.value = 'loading'
loadData().finally(() => {
pullDownState.value = 'waiting'
})
}
}
</script>
11. 主题定制高级技巧
11.1 动态主题切换
- CSS变量方案:
typescript复制const setTheme = (theme) => {
const root = document.documentElement
root.style.setProperty('--el-color-primary', theme.primary)
root.style.setProperty('--el-border-radius', theme.radius)
// 其他变量...
}
// 使用示例
setTheme({
primary: '#ff4500',
radius: '8px'
})
- 多主题编译方案:
javascript复制// build-themes.js
const fs = require('fs')
const sass = require('sass')
const themes = [
{ name: 'light', primary: '#409EFF' },
{ name: 'dark', primary: '#FF4500' }
]
themes.forEach(theme => {
const result = sass.compileString(`
@use "element-plus/theme-chalk/src/index" as *;
@forward "element-plus/theme-chalk/src/common/var.scss" with (
$colors: (
"primary": (
"base": ${theme.primary},
),
)
);
`)
fs.writeFileSync(
`public/theme/${theme.name}/index.css`,
result.css
)
})
11.2 组件级样式覆盖
- BEM规范覆盖:
scss复制.el-button--custom {
@include b(button) {
@include e(content) {
font-weight: bold;
}
}
}
- 插槽样式注入:
vue复制<el-table :data="tableData">
<template #header>
<style>
.el-table__header th {
background: var(--el-color-primary-light-9);
}
</style>
</template>
<!-- 表格列 -->
</el-table>
11.3 主题插件开发
创建主题插件示例:
typescript复制// theme-plugin.ts
import type { App } from 'vue'
export interface ThemeOptions {
primary?: string
dark?: boolean
}
export default {
install(app: App, options: ThemeOptions = {}) {
const setTheme = () => {
const style = document.documentElement.style
style.setProperty(
'--el-color-primary',
options.primary || '#409EFF'
)
if (options.dark) {
style.setProperty('--el-bg-color', '#1a1a1a')
// 其他暗色变量...
}
}
app.provide('$theme', { setTheme })
setTheme()
}
}
使用插件:
typescript复制import { createApp } from 'vue'
import themePlugin from './theme-plugin'
const app = createApp(App)
app.use(themePlugin, {
primary: '#FF4500',
dark: true
})
12. 无障碍访问支持
12.1 ARIA属性增强
- 表单组件优化:
vue复制<el-form-item
prop="email"
label="邮箱"
aria-required="true"
:aria-describedby="emailError ? 'email-error' : null"
>
<el-input
v-model="form.email"
aria-invalid="emailError ? 'true' : 'false'"
/>
<span
v-if="emailError"
id="email-error"
class="error-message"
>
请输入有效的邮箱地址
</span>
</el-form-item>
- 键盘导航支持:
typescript复制const handleKeyDown = (e: KeyboardEvent) => {
switch(e.key) {
case 'ArrowDown':
// 向下导航逻辑
break
case 'ArrowUp':
// 向上导航逻辑
break
case 'Enter':
// 确认选择
break
}
}
12.2 屏幕阅读器适配
- 动态内容通知:
typescript复制import { useAnnouncer } from '@vueuse/core'
const announce = useAnnouncer()
const handleSubmit = () => {
try {
await submitForm()
announce('表单提交成功')
} catch (err) {
announce('提交失败,请检查表单错误')
}
}
- 加载状态提示:
vue复制<template>
<div
v-if="loading"
aria-live="polite"
aria-busy="true"
class="sr-only"
>
数据加载中,请稍候
</div>
</template>
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
</style>
12.3 高对比度模式
- 主题变量调整:
scss复制$--color-primary: #0056b3 !default;
$--color-text-regular: #000 !default;
$--border-color-base: #000 !default;
$--background-color-base: #fff !default;
- 系统偏好检测:
typescript复制const isHighContrast = window.matchMedia('(prefers-contrast: more)').matches
watchEffect(() => {
if (isHighContrast) {
document.documentElement.classList.add('high-contrast')
} else {
document.documentElement.classList.remove('high-contrast')
}
})
13. 服务端渲染方案
13.1 Nuxt.js集成
- 模块配置:
typescript复制// nuxt.config.ts
export default defineNuxtConfig({
modules: [
[
'@element-plus/nuxt',
{
importStyle: 'scss',
themes: ['dark']
}
]
],
css: [
'element-plus/dist/index.css',
'element-plus/theme-chalk/dark/css-vars.css'
]
})
- 自动导入配置:
typescript复制// nuxt.config.ts
export default defineNuxtConfig({
imports: {
autoImport: true
},
components: {
global: true,
dirs: ['~/components']
}
})
13.2 SSR兼容处理
- 客户端特定代码处理:
typescript复制onMounted(() => {
if (process.client) {
import('element-plus').then(module => {
// 客户端特定逻辑
})
}
})
- 样式表处理:
typescript复制// plugins/element.client.ts
export default defineNuxtPlugin(() => {
if (process.client) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = 'https://unpkg.com/element-plus/dist/index.css'
document.head.appendChild(link)
}
})
13.3 静态站点生成
- Nuxt内容缓存:
typescript复制// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ['/'],
crawlLinks: true
}
}
})
- 关键CSS提取:
typescript复制// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
inlineSSRStyles: false
},
build: {
extractCSS: true
}
})
14. 微前端集成方案
14.1 qiankun子应用配置
- 样式隔离处理:
javascript复制// 主应用配置
import { start } from 'qiankun'
start({
sandbox: {
experimentalStyleIsolation: true
}
})
// 子应用生命周期
export async function mount(props) {
const { container } = props
const instance = createApp(App)
instance.use(ElementPlus)
instance.mount(container.querySelector('#app'))
}
- 动态主题同步:
typescript复制// 主应用
const setGlobalTheme = (theme) => {
window.__MAIN_THEME__ = theme
}
// 子应用
watchEffect(() => {
if (window.__MAIN_THEME__) {
applyTheme(window.__MAIN_THEME__)
}
})
14.2 Module Federation方案
- Webpack配置:
javascript复制// host.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
remotes: {
elementApp: 'elementApp@http://localhost:3001/remoteEntry.js'
}
})
]
}
// remote.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'elementApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/ElButtonWrapper.vue'
}
})
]
}
- 动态组件加载:
vue复制<template>
<component :is="remoteComponent" />
</template>
<script setup>
const remoteComponent = ref(null)
onMounted(async () => {
const module = await import('elementApp/Button')
remoteComponent.value = module.default
})
</script>
14.3 样式隔离策略
- Shadow DOM方案:
typescript复制const shadowRoot = element.attachShadow({ mode: 'open' })
const style = document.createElement('style')
style.textContent = `
@import url('element-plus/dist/index.css');
`
shadowRoot.appendChild(style)
- CSS前缀方案:
scss复制// webpack配置中使用postcss
module.exports = {
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
require('postcss-prefix-selector')({
prefix: '#micro-app',
transform(prefix, selector) {
if (selector.includes('el-')) {
return `${prefix} ${selector}`
}
return selector
}
})
]
}
}
}
15. 安全加固指南
15.1 XSS防护措施
- 富文本内容处理:
typescript复制import { sanitize } from 'dompurify'
const safeHtml = computed(() => {
return sanitize(richText.value, {
ALLOWED_TAGS: ['p', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href', 'title']
})
})
- 动态内容渲染:
vue复制<el-tooltip
:content="sanitize(userContent)"
raw-content
/>
15.2 CSRF防护集成
- 请求拦截器配置:
typescript复制import axios from 'axios'
import { ElMessage } from 'element-plus'
const service = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
service.interceptors.request.use(config => {
const token = getCookie('csrfToken')
if (token) {
config.headers['X-CSRF-TOKEN'] = token
}
return config
})
service.interceptors.response.use(
response => response,
error => {
if (error.response.status === 403) {
ElMessage.error('CSRF验证失败')
}
return Promise.reject(error)
}
)
15.3 内容安全策略
- CSP元标签配置:
html复制<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline' https://unpkg.com;
style-src 'self' 'unsafe-inline' https://unpkg.com;
img-src 'self' data: https://*;
">
- 非内联脚本处理:
typescript复制// 动态加载Element Plus
const loadElementPlus = () => {
return new Promise((resolve) => {
const script = document.createElement('script')
script.src = 'https://unpkg.com/element-plus'
script.onload = resolve
document.head.appendChild(script)
})
}
16. 测试覆盖率提升
16.1 组件单元测试
- 基础组件测试:
typescript复制import { mount } from '@vue/test-utils'
import ElButton from '../ElButton.vue'
test('按钮点击事件', async () => {
const wrapper = mount(ElButton, {
props: {
onClick: vi.fn()
}
})
await wrapper.trigger('click')
expect(wrapper.props('onClick')).toHaveBeenCalled()
})
- 表单验证测试:
typescript复制test('表单验证规则', async () => {
const wrapper = mount(ElForm, {
props: {
model: { username: '' },
rules: {
username: [{ required: true, message: '必填项' }]
}
}
})
const valid = await wrapper.vm.validate()
expect(valid).toBe(false)
expect(wrapper.find('.el-form-item__error').text()).toBe('必填项')
})
16.2 E2E测试实践
- Cypress测试示例:
typescript复制describe('Element Plus组件测试', () => {
it('表格分页功能', () => {
cy.visit('/table-demo')
cy.get('.el-pagination__next').click()
cy.get('.el-table__row').should('have.length', 10)
})
})
- 视觉回归测试:
javascript复制// cypress/plugins/index.js
const { initPlugin } = require('cypress-plugin-snapshots/plugin')
module.exports = (on, config) => {
initPlugin(on, config)
return config
}
// cypress/integration/visual.spec.js
describe('视觉测试', () => {
it('按钮样式匹配', () => {
cy.visit('/button-demo')
cy.get('.el-button').first().toMatchImageSnapshot()
})
})
16.3 性能基准测试
- 渲染性能测试:
typescript复制import { render, screen } from '@testing-library/vue'
import { performance } from 'perf_hooks'
test('大数据表格渲染性能', async () => {
const start = performance.now()
await render(BigTableDemo, {
props: { data: generateData(1000) }
})
const duration = performance.now() - start
expect(duration).toBeLessThan(500)
})
- 内存泄漏检测:
typescript复制test('组件卸载内存释放', async () => {
const wrapper = mount(ModalDemo)
const before = process.memoryUsage().heapUsed
await wrapper.unmount()
await new Promise(resolve => setTimeout(resolve, 1000))
