Vue 3 + Vite项目里,用Element Plus封装分页组件Pagination踩坑实录(附完整代码)
Vue 3 + Vite工程化实践:Element Plus分页组件深度封装指南
在现代化前端工程中,组件封装的艺术往往决定了项目的可维护性和开发效率。当Vue 3的Composition API遇上Vite的闪电般构建速度,再结合Element Plus这一UI库的强大能力,我们该如何打造一个既优雅又实用的分页组件?本文将带你从工程化角度,完整走过组件设计、自动导入优化、样式隔离和业务集成的全流程。
1. 工程化组件设计基础
优秀的组件封装始于合理的项目结构规划。在Vite+Vue 3的项目中,我们推荐采用以下目录组织方式:
src/
├── components/
│ ├── base/ # 基础通用组件
│ │ └── Pagination/
│ │ ├── index.ts # 组件入口文件
│ │ ├── src/
│ │ │ ├── Pagination.vue # 组件实现
│ │ │ └── types.ts # 类型定义
│ │ └── style/ # 组件样式
├── composables/ # 组合式函数
└── utils/ # 工具函数
这种结构将组件的逻辑、样式和类型定义分离,同时保持完整的封装性。对于分页组件,我们需要先明确其核心职责:
- 接收分页参数(currentPage/pageSize/total等)
- 渲染分页UI并与Element Plus的el-pagination交互
- 处理分页变化事件并通知父组件
- 提供自定义样式插槽的能力
在Vue 3的Composition API下,组件的props设计应该充分考虑TypeScript支持。以下是基础props的类型定义示例:
// src/components/base/Pagination/src/types.ts
export interface PaginationProps {
currentPage: number
pageSize: number
total: number
pageSizes?: number[]
layout?: string
background?: boolean
small?: boolean
disabled?: boolean
hideOnSinglePage?: boolean
}
2. Composition API下的组件实现
基于上述类型定义,我们可以开始构建组件主体。使用Composition API的最大优势是能够将相关逻辑组织在一起,而不是按照选项API的范式强制分离。
<!-- src/components/base/Pagination/src/Pagination.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import type { PaginationProps } from './types'
const props = withDefaults(defineProps<PaginationProps>(), {
pageSizes: () => [10, 20, 50, 100],
layout: 'total, sizes, prev, pager, next, jumper',
background: true,
small: false,
disabled: false,
hideOnSinglePage: false
})
const emit = defineEmits<{
(e: 'update:currentPage', val: number): void
(e: 'update:pageSize', val: number): void
(e: 'change', val: { currentPage: number, pageSize: number }): void
}>()
const internalPageSize = computed({
get: () => props.pageSize,
set: (val) => emit('update:pageSize', val)
})
const internalCurrentPage = computed({
get: () => props.currentPage,
set: (val) => emit('update:currentPage', val)
})
const handleSizeChange = (val: number) => {
internalPageSize.value = val
emit('change', {
currentPage: internalCurrentPage.value,
pageSize: val
})
}
const handleCurrentChange = (val: number) => {
internalCurrentPage.value = val
emit('change', {
currentPage: val,
pageSize: internalPageSize.value
})
}
</script>
这段代码展示了几个关键实践:
- 使用
withDefaults为props提供默认值 - 定义严格的emit类型约束
- 通过计算属性实现v-model双向绑定
- 统一封装分页变化事件
模板部分则需要特别注意Element Plus组件的正确使用:
<template>
<div class="pagination-container">
<el-pagination
v-model:current-page="internalCurrentPage"
v-model:page-size="internalPageSize"
:page-sizes="pageSizes"
:layout="layout"
:background="background"
:small="small"
:disabled="disabled"
:hide-on-single-page="hideOnSinglePage"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
3. Vite环境下的自动导入优化
在Vite项目中,我们可以利用unplugin-vue-components实现Element Plus组件的自动导入,避免手动注册的麻烦。首先确保vite.config.ts配置正确:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [
ElementPlusResolver({
importStyle: 'sass',
}),
],
dts: 'src/components.d.ts',
directoryAsNamespace: true,
}),
],
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "~/styles/element/index.scss" as *;`,
},
},
},
})
对于我们封装的分页组件,还需要在入口文件中进行导出:
// src/components/base/Pagination/index.ts
import Pagination from './src/Pagination.vue'
export { Pagination }
export default Pagination
然后在项目的任何地方,都可以直接使用组件而无需显式导入:
<script setup>
// 无需import,自动导入机制会处理
const pagination = ref({
currentPage: 1,
pageSize: 10,
total: 100
})
</script>
<template>
<Pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
/>
</template>
4. 样式隔离与主题定制
在组件封装中,样式管理常常被忽视,但却至关重要。我们推荐采用以下策略:
1. 作用域样式 :使用scoped确保组件样式不会泄漏
<style scoped lang="scss">
.pagination-container {
margin-top: 20px;
text-align: right;
:deep(.el-pagination) {
// 覆盖Element Plus默认样式
.btn-prev,
.btn-next {
border-radius: 4px;
}
}
}
</style>
2. 主题变量覆盖 :通过SCSS变量统一修改
// styles/element/var.scss
$--color-primary: #1890ff;
$--pagination-button-width: 32px;
3. 提供样式插槽 :允许外部自定义部分样式
<template>
<div class="pagination-container">
<slot name="prefix"></slot>
<el-pagination ... />
<slot name="suffix"></slot>
</div>
</template>
5. 高级功能扩展
基础分页组件完成后,我们可以考虑添加一些增强功能:
快速跳转优化 :
<script setup>
const quickJump = (page) => {
if (page > 0 && page <= Math.ceil(props.total / props.pageSize)) {
internalCurrentPage.value = page
}
}
</script>
<template>
<div class="quick-jumper">
跳至 <input
type="number"
:min="1"
:max="Math.ceil(total / pageSize)"
@keyup.enter="quickJump($event.target.value)"
> 页
</div>
</template>
分页大小记忆功能 :
import { useLocalStorage } from '@vueuse/core'
const pageSize = useLocalStorage('pagination-page-size', 10)
与表格组件的联动 :
<script setup>
const tableData = ref([])
const loading = ref(false)
const pagination = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const fetchData = async () => {
loading.value = true
try {
const res = await api.getList({
page: pagination.value.currentPage,
size: pagination.value.pageSize
})
tableData.value = res.data
pagination.value.total = res.total
} finally {
loading.value = false
}
}
watch(() => [
pagination.value.currentPage,
pagination.value.pageSize
], fetchData, { immediate: true })
</script>
6. 单元测试与类型安全
完善的组件应该包含单元测试。使用Vitest可以方便地测试我们的分页组件:
// tests/components/Pagination.spec.ts
import { mount } from '@vue/test-utils'
import Pagination from '@/components/base/Pagination'
describe('Pagination.vue', () => {
it('emits change event when page changes', async () => {
const wrapper = mount(Pagination, {
props: {
currentPage: 1,
pageSize: 10,
total: 100
}
})
await wrapper.find('.el-pager li:nth-child(2)').trigger('click')
expect(wrapper.emitted('change')).toBeTruthy()
expect(wrapper.emitted('change')[0]).toEqual([{
currentPage: 2,
pageSize: 10
}])
})
})
对于类型安全,我们可以利用Volar和Vue 3的TS支持来获得完美的开发体验。确保在tsconfig.json中配置:
{
"compilerOptions": {
"types": ["vite/client", "unplugin-vue-components/types"]
}
}
7. 性能优化与最佳实践
在大型项目中,分页组件的性能优化不容忽视:
- 防抖处理 :快速点击分页按钮时
import { debounce } from 'lodash-es'
const handleCurrentChange = debounce((val: number) => {
// 处理逻辑
}, 300)
- 虚拟滚动集成 :大数据量时
<template>
<el-table-v2
:columns="columns"
:data="data"
:pagination="{
currentPage,
pageSize,
total,
onChange: handlePageChange
}"
/>
</template>
- 服务端分页与前端分页的智能切换
const isServerPagination = computed(() => total.value > 10000)
- 分页器响应式布局
<script setup>
const layout = computed(() => {
return window.innerWidth < 768
? 'prev, pager, next'
: 'total, sizes, prev, pager, next, jumper'
})
</script>
8. 业务场景深度集成
最后,我们来看几个实际业务中的集成案例:
案例一:带筛选条件的分页
const filters = ref({})
const fetchData = () => {
api.getList({
...filters.value,
page: pagination.value.currentPage,
size: pagination.value.pageSize
})
}
案例二:多标签页分页记忆
const tabPagination = ref({
tab1: { currentPage: 1, pageSize: 10 },
tab2: { currentPage: 1, pageSize: 20 }
})
const handleTabChange = (tab) => {
pagination.value = { ...tabPagination.value[tab] }
}
案例三:分页与路由同步
const route = useRoute()
const router = useRouter()
watch(() => route.query.page, (page) => {
if (page) pagination.value.currentPage = Number(page)
})
watch(() => pagination.value.currentPage, (page) => {
router.push({ query: { ...route.query, page } })
})
在真实项目中使用时,我发现将分页逻辑提取为组合式函数可以极大提高复用性:
// composables/usePagination.ts
export function usePagination(initial = { currentPage: 1, pageSize: 10 }) {
const pagination = reactive({ ...initial })
const reset = () => {
pagination.currentPage = 1
}
return { pagination, reset }
}
更多推荐
所有评论(0)