关于二次封装elementUi
前言
为什么要做这个事呢,作为一个前端后台开发工程师,最经常做的工作就是后台表格开发,由于业务比较简单,就自己单独封装了一个table表格,来提升开发效率。但是觉得没什么难点亮点,重新整理封装思路,研究简单业务可以高速提效,复杂业务也可以自定义完成的公共组件,并发布到npm上包可以拉取。
搭建基础
首先需要搭建一个vue3 + ts + vite + elementPlus的一个架子;
- 创建项目
pnpm create vite my-vue-app --template vue-ts
cd my-vue-app
- 安装依赖
pnpm install
pnpm add element-plus
pnpm add -D @types/node
- 一些配置
- package.json
{
"name": "z-dynamic",
"private": false,
"version": "0.0.6",
"type": "module",
"types": "dist/types/index.d.ts",
"import": "dist/z-dynamic.es.js",
"require": "dist/z-dynamic.umd.js",
"files": [
"dist",
"README.md"
],
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/z-dynamic.es.js",
"require": "./dist/z-dynamic.umd.js"
}
},
"scripts": {
"dev": "vite",
"build": "vite build && vue-tsc",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@types/node": "^22.15.3",
"element-plus": "^2.9.9",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"@vue/tsconfig": "^0.7.0",
"typescript": "~5.8.3",
"vite": "^6.3.5",
"vue-tsc": "^2.2.8"
},
"peerDependencies": {
"vue": "^3.5.13",
"element-plus": "^2.9.9"
},
"author": "zhoume",
"description": "A dynamic table component based on Element Plus",
"keywords": [
"vue3",
"element-plus",
"table",
"component"
]
}
- 配置vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
// 指定库的入口文件路径
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'ZDynamic',
fileName: (format) => `z-dynamic.${format}.js`,
formats:['es', 'umd']
},
rollupOptions: {
// 指定外部依赖,这些依赖不会被打包进库中
external: ['vue', 'element-plus'],
output: {
globals: {
vue: 'Vue',
'element-plus': 'ElementPlus'
},
exports: 'named'
}
},
cssCodeSplit: true,
sourcemap: true
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'~': path.resolve(__dirname, './')
}
}
})
- tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": false,
"noEmit": false,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"declaration": true,
"declarationDir": "dist/types",
"emitDeclarationOnly": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
}
- 修改main.ts
import {createApp} from 'vue'
import 'element-plus/dist/index.css'
import ElementPlus from 'element-plus'
import App from './App.vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
// 注册Element Plus
app.use(ElementPlus)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
- 写前准备(写组件时测试的位置)
<script setup lang="ts">
</script>
<template>
<div>
HelloWorld
</div>
</template>
<style scoped>
</style>
- 删除src/components下的HelloWorld.vue 和 src下的style.css
- 在src下创建types/index.ts用于存放类型
- 运行项目
pnpm dev
这样一个基础vue3 + vite + ts + elementPlus的基础结构就搭好了。
正式开写
首先分析需求
我们要做的是一个动态表格,传入表格的列的配置和表格的数据,来展示一个表格;其中可以内置一些展示配置如展示图片、标签等;还需要提供一些插槽或者自定义渲染的方式来给使用者自己去渲染自己需要的组件。
创建文件
在src/components下创建d-table.vue文件 先去写一个基础的elementplus的表格出来
<script lang="ts" setup>
const tableData = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
]
</script>
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
</el-table>
</template>
app中引入d-table组件
<script setup lang="ts">
import DTable from '@/components/d-table.vue'
</script>
<template>
<div>
<DTable/>
</div>
</template>
<style scoped>
</style>
页面
列数据与表格数据抽离
将d-table组件中的列数据与表格数据抽离,提取到父组件中通过父传子的方式去传递。
- d-table文件
<script lang="ts" setup>
import {Props} from "@/types";
const props = withDefaults(defineProps<Props>(), {
columns: () => [],
data: () =>[],
})
</script>
<template>
<el-table :data="props.data" style="width: 100%">
<template v-for="column in props.columns" :key="`${column.prop}-${column.label}`">
<el-table-column v-bind="column"/>
</template>
</el-table>
</template>
- app.vue
<script setup lang="ts">
import DTable from '@/components/d-table.vue'
import {ref} from "vue";
const columns = ref([
{
prop: 'name',
label: 'Name',
width: 180,
},
{
prop: 'date',
label: 'Date',
width: 180,
},
{
prop: 'address',
label: 'Address',
width: 180,
},
])
const data = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
]
</script>
<template>
<div>
<DTable :columns="columns" :data="data"/>
</div>
</template>
<style scoped>
</style>
- types 申明的类型
export interface TableColumn<T = any> {
prop: string | keyof T
label: string
width?: number | string
}
export interface Props<T = any> {
columns: TableColumn<T>[]
data?: T[]
}
再看页面和之前一样
这样已经完成了一个基础表格的封装,但是用这个去做业务需求是远远不够的,对比element的表格还有很多功能需要拿过来;接下来就是做这部分工作。
属性和事件的透传
<script lang="ts" setup>
import {Props} from "@/types";
import type {TableInstance} from 'element-plus'
import {ref, useAttrs} from "vue";
defineOptions({
name: 'DTable'
})
const props = withDefaults(defineProps<Props>(), {
columns: () => [],
data: () => [],
})
const attrs = useAttrs()
const tableRef = ref<TableInstance>()
console.log(attrs, props)
defineExpose({tableRef})
</script>
<template>
<el-table
ref="tableRef"
v-bind="{ ...props, ...attrs }"
>
<template v-for="column in props.columns" :key="`${column.prop}-${column.label}`">
<el-table-column v-bind="column">
</el-table-column>
</template>
</el-table>
</template>
这时候再去测试比如说我要给表格加一个边框但是我的组件的props中并没有定义border属性他会通过attrs去传递
<DTable :columns="columns" :data="data" :border="true"/>
console.log(attrs, props)打印结果
再测试一下事件的透传点击某一行会打印对应的行的内容
const rowClick = (row: any) => {
console.log(row)
}
</script>
<template>
<div>
<DTable @row-click="rowClick" :columns="columns" :data="data" :border="true"/>
</div>
</template>
打印结果
接下来进行插槽的透传 element官方提供了三个default append empty
这边用empty做示例 用useSlots()拿到传递的插槽 再通过循环放进模板中
<script lang="ts" setup>
import {Props} from "@/types";
import type {TableInstance} from 'element-plus'
import {ref, useAttrs, useSlots} from "vue";
defineOptions({
name: 'DTable'
})
const props = withDefaults(defineProps<Props>(), {
columns: () => [],
data: () => [],
})
const attrs = useAttrs()
const slots = useSlots()
const tableRef = ref<TableInstance>()
defineExpose({tableRef})
</script>
<template>
<el-table
ref="tableRef"
v-bind="{ ...props, ...attrs }"
>
<template v-for="column in props.columns" :key="`${column.prop}-${column.label}`">
<el-table-column v-bind="column">
</el-table-column>
</template>
<template v-for="(_, name) in slots" #[name]="scope" :key="name">
<slot :name="name" v-bind="scope" ></slot>
</template>
</el-table>
</template>
将数据清空测试
<template>
<div>
<DTable @row-click="rowClick" :columns="columns" :data="data" :border="true">
<template #empty>
<el-tag>空</el-tag>
</template>
</DTable>
</div>
</template>
<style scoped>
</style>
结果
表格列的差异化
我们要知道有什么需要做差异化的,在我们业务需求里有一些表格除了展示文字以外还要展示图片、按钮、输入框、或者其他组件等更奇葩的内容;目前的组件是远远不够的。怎样去做呢,element官方提供了default插槽可以去实现。 首先我们要去先完成官方提供的其他插槽的内容。比如HeaderSlot这边直接给示例
<template>
<el-table
ref="tableRef"
v-bind="{ ...props, ...attrs }"
>
<template v-for="column in props.columns" :key="`${column.prop}-${column.label}`">
<el-table-column v-bind="column">
<!-- Header Slot -->
<template #header v-if="column.headerSlot">
<slot :name="column.headerSlot" :column="column"/>
</template>
<!-- Default Slot -->
</el-table-column>
</template>
<template v-for="(_, name) in slots" #[name]="scope" :key="name">
<slot :name="name" v-bind="scope" ></slot>
</template>
</el-table>
</template>
<script setup lang="ts">
const columns = ref([
{
prop: 'name',
label: 'Name',
width: 180,
},
{
prop: 'date',
label: 'Date',
width: 180,
headerSlot: 'header'
},
{
prop: 'address',
label: 'Address',
width: 180,
},
])
</script>
<template>
<div>
<DTable :columns="columns" :data="data" :border="true">
<!--名字和columns中的对应-->
<template #header="scope">
<div>
{{ scope.column.label }}
<el-tooltip content="这是一个提示">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
</DTable>
</div>
</template>
接下来就是正式做差异化的内容了
- 提供具名插槽来给用户做差异化的功能
<!-- 2. 具名插槽 -->
<slot
v-else-if="column.slot"
:name="column.slot"
:row="scope.row"
:column="column"
>
- 通过自定义的渲染函数和component组件的配合实现
<!-- 1. 自定义渲染函数 -->
<component
:is="column.customRender"
v-if="column.customRender"
:row="scope.row"
:column="column"
/>
- 展示过滤文本的位置
<!-- 3. 格式化函数 -->
<template v-else-if="column.formatter">
{{ column.formatter(scope.row) }}
</template>
- 和默认位置
<template v-else>
{{ scope.row[column.prop] }}
</template>
直接上示例
<script lang="ts" setup>
import {Props} from "@/types";
import type {TableInstance} from 'element-plus'
import {ref, useAttrs, useSlots} from "vue";
defineOptions({
name: 'DTable'
})
const props = withDefaults(defineProps<Props>(), {
columns: () => [],
data: () => [],
})
const attrs = useAttrs()
const slots = useSlots()
const tableRef = ref<TableInstance>()
defineExpose({tableRef})
</script>
<template>
<el-table
ref="tableRef"
v-bind="{ ...props, ...attrs }"
>
<template v-for="column in props.columns" :key="`${column.prop}-${column.label}`">
<el-table-column v-bind="column">
<!-- Header Slot -->
<template #header v-if="column.headerSlot">
<slot :name="column.headerSlot" :column="column"/>
</template>
<!-- Default Slot -->
<template #default="scope" v-if="!column.type || column.type === 'default'">
<!-- 1. 自定义渲染函数 -->
<component
:is="column.customRender"
v-if="column.customRender"
:row="scope.row"
:column="column"
/>
<!-- 2. 具名插槽 -->
<slot
v-else-if="column.slot"
:name="column.slot"
:row="scope.row"
:column="column"
>
{{ scope.row[column.prop] }}
</slot>
<!-- 3. 格式化函数 -->
<template v-else-if="column.formatter">
{{ column.formatter(scope.row) }}
</template>
<!-- 4. 默认渲染 -->
<template v-else>
{{ scope.row[column.prop] }}
</template>
</template>
</el-table-column>
</template>
<template v-for="(_, name) in slots" #[name]="scope" :key="name">
<slot :name="name" v-bind="scope" ></slot>
</template>
</el-table>
</template>
使用组件 可以用h方法去渲染自定义组件也可以用具名插槽
<script setup lang="ts">
import {h, ref} from 'vue'
import DTable from '@/components/d-table.vue'
import {ElButton} from 'element-plus'
import test from '@/components/test.vue'
import type {TableInstance} from 'element-plus'
// 表格实例引用
const tableRef = ref<TableInstance>()
// 表格数据
const tableData = ref([
{
id: 1,
date: '2024-01-01',
name: 'Tom',
address: '北京市朝阳区',
age: 18
},
{
id: 2,
date: '2024-01-02',
name: 'Jerry',
address: '上海市浦东新区',
age: 20
},
{
id: 3,
date: '2024-01-02',
name: 'Jerry',
address: '上海市浦东新区',
age: 20,
children: [
{
id: 31,
date: '2016-05-01',
name: 'wangxiaohu',
address: 'No. 189, Grove St, Los Angeles',
},
{
id: 32,
date: '2016-05-01',
name: 'wangxiaohu',
address: 'No. 189, Grove St, Los Angeles',
},
],
}
])
// 表格列配置
const columns = ref([
{
type: 'selection',
width: 55
},
{
label: '序号',
type: 'index',
width: 55
},
{
prop: 'date',
label: '日期',
width: 180,
// 使用自定义表头插槽
headerSlot: 'dateHeader'
},
{
prop: 'name',
label: '姓名',
width: 180,
// 使用自定义列插槽
slot: 'name'
},
{
prop: 'address',
label: '地址',
// 使用格式化函数
formatter: (row: any) => `地址: ${row.address}`
},
{
prop: 'age',
label: '年龄',
// 使用自定义渲染函数
customRender: ({row}: any) => {
return h('span', {
style: {
color: row.age >= 20 ? 'red' : 'green'
}
}, row.age)
}
},
{
prop: 'operation',
label: '操作',
width: 200,
customRender: (scope: any) => {
const row = scope.row
return h('div', [
h(ElButton, {type: 'primary', onClick: () => handleEdit(row)}, () => '编辑'),
h(test, {row, onClick: () => handleDelete(row)}, () => [])
])
}
}
])
// 处理编辑
const handleEdit = (row: any) => {
console.log('编辑', row)
}
// 处理删除
const handleDelete = (row: any) => {
console.log('删除', row)
}
// 获取表格实例方法示例
const getTableInstance = () => {
console.log('表格实例:', tableRef.value)
}
const handleSelectionChange = (val: any) => {
console.log('选中的数据:', val)
}
</script>
<template>
<div>
<DTable
ref="tableRef"
:columns="columns"
:data="tableData"
border
stripe
row-key="id"
@selection-change="handleSelectionChange"
>
<!-- 日期表头自定义插槽 -->
<template #dateHeader="{ column }">
<el-icon>
<calendar/>
</el-icon>
{{ column.label }}
</template>
<!-- 姓名列自定义插槽 -->
<template #name="{ row }">
<el-tag>{{ row.name }}</el-tag>
</template>
</DTable>
<el-button @click="getTableInstance">获取表格实例</el-button>
</div>
</template>
除了多级表头这还需要单独处理以外 element官方提供的大部分功能已经实现,后续再去完善
打包上传
首先需要做一个打包的入口;在src下创建一个index.ts文件
import {App} from 'vue'
import DTable from '@/components/d-table.vue'
import type { TableColumn } from '@/types'
const components = [DTable]
export type { TableColumn }
export default {
install(app: App) {
components.forEach((component: any) => {
console.log('components', component.name)
app.component(component.name, component)
})
}
}
导出组件和类型文件
接下来就是做打包和上传npm
我这里已经注册过npm账号了自己私下去注册就好
pnpm build 打包
npm login 登录 输入验证码即可
npm publish 上传
这里就可以看到自己刚刚发的包啦