十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

iView Table+Page分页全解析:从组件联调到后端接口避坑指南

iView Table+Page分页全解析:从组件联调到后端接口避坑指南 做后台管理系统的时候表格加翻页基本是绕不开的组合。ivew现在叫 View UI里的 Table 和 Page 两个组件单拿出来都不复杂但真正用到项目里特别是数据量上来、需要接后端接口的时候各种小问题就冒出来了。我最初用的时候也踩过不少坑比如分页一翻数据不刷新、total 算错、表格 loading 状态没处理导致重复请求。这篇文章就把我从零开始接入到后来在多个项目里复用总结出来的经验整理一遍从分页思路、组件接法、后端联调到常见坑位排查都过一遍给还在折腾这块的朋友做个参考。说下适用范围如果你用的是 Vue 2 iView或 View UI这套技术栈文章里的代码可以直接抄如果你是 Vue 3 或其他组件库思路一样就是 API 名需要替换一下。我会把前端假分页和真分页两种写法都讲清楚并说明该选哪个。1. 先想清楚你需要的到底是前端分页还是后端分页很多新手拿到表格分页需求第一反应就是“给 Table 加个分页器”然后打开组件文档开始复制粘贴。但这是最容易出问题的地方——分页的方案取决于你的数据来源而不是 UI 长什么样。1.1 前端分页适合什么场景前端分页就是把后端一次返回的所有数据存在前端在本地做切片。比如接口一次性返回了 1000 条数据前端按照每页 10 条自己把数据切开丢给 Table 展示。适用条件是数据总量可控我一般以 500 条以内为参考线极限容忍到 2000 条左右、数据结构简单、不需要服务端做复杂聚合筛选。好处很明显不依赖后端接口格式交互快翻页零延迟。坏处也直接数据量一大首屏请求就慢浏览器渲染几千行也会卡而且一旦超过后端接口限制很多公司网关会限制单次返回条数方案就直接不可行。1.2 后端分页才是生产环境的标配后端分页就是接口支持page和pageSize参数每次只把当前页的数据返回给前端。前端拿到数据后更新表格同时把接口返回的total总数同步给 Page 组件。这才是真正面向生产环境的方案。你不需要关心数据总量有多少后端返回多少前端就展示多少内存和渲染压力都小。代价是前端状态管理变复杂了涉及页码、每页条数、查询条件、请求时序之间的联动。我项目里的原则是列表数据预计超过 500 条或者需求明确说未来数据会持续增长直接走后端分页别犹豫。前端分页看起来很省事后期改造成本反而最高。1.3 为什么用 iView 的 Table Page 组合iView 生态里其实有Table自带的分页配置#!js :page可以把分页器直接挂在表格底部。但我个人更推荐用独立的Page组件来分页原因有三点Table 自带的:page配置项比较弱能控制的粒度有限比如没有独立的事件回调、不好定制插槽。Page 组件是独立的数据驱动组件可以放到表格上方、下方、甚至弹窗里单独用布局灵活。在复杂表格场景里比如有工具栏、批量操作按钮、统计信息分页器位置通常需要单独控制独立 Page 更直观。你在很多开源后台管理系统源码里看到的也是 Table 独立 Page 的组合这种写法可读性高团队接手也好维护。2. 从零开始Table 和 Page 组件的最小可用实现这一节先讲前端分页的写法因为它不依赖后端最适合用来理解组件的联动逻辑。理解了数据是怎么切片的后面接后端接口就是做几行替换的事。2.1 数据状态应该怎么设计用一个data存放全量数据等于把后端返回的结果原样存下来。然后定义currentPage和pageSize两个状态来驱动展示。但展示给 Table 的数据不要手动去算建议用一个 computed 属性来处理。export default { data() { return { // 后端一次返回的完整数据集 tableData: [], // 当前页码 currentPage: 1, // 每页条数默认给 10 pageSize: 10, }; }, computed: { // 展示给表格的数据根据页码和每页条数切片 paginatedData() { const start (this.currentPage - 1) * this.pageSize; const end start this.pageSize; return this.tableData.slice(start, end); }, }, };为什么用 computed 而不是直接在 data 里维护一个pageData因为直接维护意味着你要在currentPage、pageSize、tableData三个变量变化时手动去同步很容易漏掉某个场景。computed 依赖追踪任何相关状态变化都会自动重算这是最省心的做法。2.2 表格列的定义要点Table 的列配置直接写在模板里反而更直观但用 columns 数据来配置更灵活尤其是需要动态控制某列的显示隐藏时。template Table :columnscolumns :datapaginatedData stripe border / /template script export default { data() { return { columns: [ { title: ID, key: id, width: 100 }, { title: 姓名, key: name, minWidth: 120 }, { title: 邮箱, key: email, minWidth: 200 }, { title: 创建时间, key: createdAt, minWidth: 180 }, ], }; }, }; /script有一点提醒一下key字段必须和后台返回的字段名一致否则表格单元格会渲染空白。如果你遇到的字段名是user_name这种下划线风格前端列配置里可以用key: user_name或者让后端用驼峰再封装一层。2.3 Page 组件的参数和事件解析Page 组件是 iView 里封装的翻页器常用的属性和事件如下。属性 / 事件类型说明currentNumber当前页码必填支持v-model双向绑定totalNumber数据总条数必填分页器根据它计算页码数量page-sizeNumber每页条数可选配合page-size-opts使用show-totalBoolean是否显示总条数文本建议开启show-elevatorBoolean是否显示跳页输入框数据量多时建议开启show-sizerBoolean是否显示每页条数切换器page-size-optsArray可切换的每页条数集合比如[10, 20, 50, 100]on-changeFunction页码变化时触发参数是新的页码on-page-size-changeFunction每页条数变化时触发参数是新的每页条数筛选日常用的配置我一般在项目里的写法是这样template Page :currentcurrentPage :totaltableData.length :page-sizepageSize show-total show-elevator show-sizer :page-size-opts[10, 20, 30, 50] on-changehandlePageChange on-page-size-changehandlePageSizeChange / /template script export default { methods: { handlePageChange(page) { this.currentPage page; }, handlePageSizeChange(pageSize) { this.pageSize pageSize; // 每页条数变化时最好把页码重置回第一页避免停留在不存在的页上 this.currentPage 1; }, }, }; /script2.4 完整的表格分页模板把上面的部分拼起来一个最小可用的前端分页表格就出来了。template div classtable-wrapper Table :columnscolumns :datapaginatedData stripe border / div classpage-wrapper Page :totaltableData.length :currentcurrentPage :page-sizepageSize show-total show-sizer show-elevator :page-size-opts[10, 20, 50, 100] on-changehandlePageChange on-page-size-changehandlePageSizeChange / /div /div /template script export default { data() { return { tableData: [], columns: [ { title: ID, key: id, width: 100 }, { title: 姓名, key: name, minWidth: 120 }, { title: 邮箱, key: email, minWidth: 200 }, ], currentPage: 1, pageSize: 10, }; }, computed: { paginatedData() { const start (this.currentPage - 1) * this.pageSize; return this.tableData.slice(start, start this.pageSize); }, }, methods: { handlePageChange(page) { this.currentPage page; }, handlePageSizeChange(pageSize) { this.pageSize pageSize; this.currentPage 1; }, }, }; /script style scoped .page-wrapper { display: flex; justify-content: flex-end; margin-top: 16px; } /style到这里最基本的分页功能已经能跑了。但如果你直接拿这个代码上生产环境会遇到几个问题搜索筛选时忘记重置页码、数据删除到某页空了没回声、翻页后没有滚动到顶部。这些细节我在第 4 节逐个说。3. 接后端接口的真分页请求、参数、状态同步前端假分页在数据量小的时候没问题但真实项目里基本都是真分页。真分页的复杂度在于请求参数、返回结果、状态同步这三者不能乱。3.1 后端接口参数怎么约定前端传参习惯一般是page页码从 1 开始和pageSize每页条数也有团队喜欢用pageNum和limit。这个没有统一标准但前端必须有一个人和后端对齐。后端返回的数据结构我在项目里见到的规范一般是{ code: 0, message: success, data: { list: [ { id: 1, name: 张三 } ], total: 100, page: 1, pageSize: 10 } }前端拿到list丢给 Table拿到total丢给 Page 的total属性。这里容易踩的第一个坑是很多后端返回的是data.records而不是data.list字段名不同你需要和后端确认或者在api层做个统一转换。3.2 请求时序和 loading 状态翻页时发请求最常见的问题就是请求竞态。用户快速连续点下一页第一次请求还没回来第二次请求又发出去了最后返回的数据顺序可能是乱序的表格显示的内容和页码对不上。解决办法有两个层面。一个是加 loading 状态在每次请求前把 loading 置为 true表格显示加载中状态同时可以用if (this.loading) return拦截重复点击。另一个是维护一个请求序号或者用取消请求的方法但成本较高一般用 loading 拦截就够了。iView 的 Table 自带loading属性布尔值控制即可template Table :loadingloading :columnscolumns :datalistData / /template请求逻辑封装成一个方法只要查询条件或页码变化就调用它methods: { async fetchTableData() { if (this.loading) return; this.loading true; try { const params { page: this.currentPage, pageSize: this.pageSize, // 其他查询条件... keyword: this.keyword, }; const res await getTableList(params); this.listData res.data.list; this.total res.data.total; // 如果当前页数据为空且不是第一页说明删光了或数据被过滤了回退一页 if (!this.listData.length this.currentPage 1) { this.currentPage - 1; this.fetchTableData(); return; } } catch (error) { // 错误处理可以配合 Message 组件提示 this.$Message.error(列表加载失败); } finally { this.loading false; } }, handlePageChange(page) { if (page this.currentPage) return; this.currentPage page; this.fetchTableData(); }, handlePageSizeChange(pageSize) { this.pageSize pageSize; this.currentPage 1; this.fetchTableData(); }, },3.3 搜索、重置、筛选和分页的联动表格页面上几乎一定会搭配一个搜索区这地方最容易出现的逻辑 bug 是用户在第三页搜索了一个关键词结果列表都刷新成新数据了页码还在第 3 页有时候后端不校验页码超范围返回空数据页面就一片空白。正确的做法是所有查询条件变化时页码强制回第一页methods: { handleSearch() { this.currentPage 1; this.fetchTableData(); }, handleReset() { this.keyword ; this.dateRange []; // 重置其他查询条件... this.currentPage 1; this.fetchTableData(); }, },3.4 和后端接口常见的分页联调问题后端分页失效这个问题在很多团队里都遇到过。比如我用 MyBatis Plus 做后端的时候分页拦截器没有生效最常见的原因是配置类没加MapperScan注解的位置不对或者分页插件的DbType写成了 MySQL但实际数据库是 OracleSQL 语法就不兼容了。遇到分页失效快速排查的思路是打印后端打印出的 SQL看有没有LIMIT或ROWNUM没有就说明分页拦截器没生效。检查分页插件是否配置在 MyBatis 的 configuration 之前顺序颠倒会导致拦截器不执行。看传入的page和pageSize是不是被后续的查询条件覆盖了比如直接new QueryWrapper()之后又 set 了last(LIMIT 10)这种手写 SQL 会干扰分页。前端这边要做的是确认请求 URL 里的参数拼接是否正确。打开浏览器 DevTools 的 Network 面板点一次翻页看请求参数有没有带上page2pageSize10。不带的话检查是不是在getTableList里漏传参数了。4. 进阶玩法排序、筛选、保留选中态分页功能本身不难但实际项目里不会只让你做个空表格。经常是表格要支持多选、支持点击表头排序、支持某些列筛选这些功能一旦和分页叠加事情就开始复杂了。4.1 翻页后保留勾选状态iView Table 的多选功能默认是on-selection-change事件回调给你一个当前页选中的数据数组。问题是你翻到第 2 页勾选了几条再翻回第 1 页之前勾选的数据还在但表格不会自动勾上而且selection数组已经被新页面的选择覆盖了。我以前踩过这个坑用户在 5 页里勾了十几条数据点批量删除发现只删了当前页的。后来用了_checked字段方案在表格数据源里维护一个映射。data() { return { checkedMap: {}, // 用 id 作为 key记录勾选状态 }; }, computed: { paginatedData() { return this.tableData.slice(start, end).map(item ({ ...item, _checked: !!this.checkedMap[item.id], })); }, }, methods: { handleSelectionChange(selection) { // 先把当前页全部标记为未勾选 this.paginatedData.forEach(item { this.checkedMap[item.id] false; }); // 再把选中的标记为 true selection.forEach(item { this.checkedMap[item.id] true; }); }, getCheckedRows() { // 从全局映射里筛出所有勾选的项 return this.tableData.filter(item this.checkedMap[item.id]); }, },其实 iView 的on-selection-change返回的selection已经是包含当前页选中项的数组如果你的需求不跨页直接存下来即可。跨页场景用上面方案性能最差的情况也就是全量遍历一次tableData对于常规列表完全可以接受。4.2 表头排序和分页的联动iView Table 的排序是前端基于当前页数据来做的也就是说如果你不分页它排序挺好用的但真分页模式下后端每次只返回当前页的数据此时表头排序只能排这一页结果就很诡异了。正确姿势是排序状态由后端处理。Table 的on-sort-change回调里能拿到key排序字段和orderasc/desc把这个参数传给后端请求时带上。template Table :columnscolumns :datalistData on-sort-changehandleSortChange / /template script export default { methods: { handleSortChange({ key, order }) { // order 为 normal 时说明取消了排序 this.sortKey order normal ? : key; this.sortOrder order normal ? : order; this.currentPage 1; this.fetchTableData(); }, }, }; /script这里的坑在于列配置里必须给对应列加上sortable: true否则点击表头不会触发排序事件。有些人不加这个属性然后在事件里排了半天发现回调不执行排错方向都找错了。4.3 自定义分页器的显示样式Page 组件默认右对齐还是左对齐完全看父容器的布局。我一般把分页器单独放到表格下方右侧加一点边距再调一下show-total的文案。iView 的show-total默认是显示共 xxx 条如果想自定义文案可以写成具名插槽template Page :totaltotal show-total template slottotal 共 {{ total }} 条数据每页 {{ pageSize }} 条 /template /Page /template这里提一句插槽只在需要自定义显示内容时才用日常场景用默认的共 xxx 条就够了别画蛇添足。4.4 页容量切换后保持合理页码当用户把每页条数从 10 条切换成 20 条时如果当前在第 5 页总数据 45 条原来第 5 页对应第 41~45 条切到 20 条每页后总共只有 3 页当前页码 5 就超出了范围。后端接口如果没做兜底会返回空数组。前端一行代码就能解决切页容量时重置页码为 1。我在前面已经写过了这里再强调一次这是最省心的策略。严谨的团队会在重置页码之后再发一次请求刷新数据避免表格里的数据和total不一致。5. 常见问题排查与避坑速查表这一节把我在实际部署和同事请教中遇到的高频问题整理成速查表你遇到类似情况可以直接翻到这里对照。5.1 表格不显示数据却显示空行这个通常不是分页器的问题而是data和columns的 key 没对上。用 iView 的 Table如果 columns 里的key写的字段在data里不存在表格单元格就是空的。排查看两点接口返回的字段名是不是和key完全一致大小写、下划线都要注意。数据是不是真的有值可以在模板里临时加上{{ listData }}打个调试文本或者打开 Vue Devtools 看listData的值。5.2 total 对不上、页码丢失total对不上最经典的原因是没有用接口返回的总数而是用了listData.length。你以为每个接口都返回了全量数据其实后端只返回了 10 条当前页数据listData.length一直是 10。然后 Page 组件的 total 就一直是 10分页器只会出现一页。这个坑很多人都会踩一次排查思路就是打印接口返回的数据结构看total字段到底在哪一层是不是被 axios 拦截器包了一层导致取值错误。5.3 点击翻页后页面被顶到顶部或其他页面填充内容混乱分页器只负责切数据它并不负责把表格滚动到可视区域。如果你的表格在页面底部翻页后用户视线停留在底部新数据渲染出来后用户要往上滑很久才能看到表格顶部体验很差。解决方案是翻页后调一个 scroll 方法handlePageChange(page) { this.currentPage page; this.fetchTableData(); // 滚动到表格区域 const tableEl this.$refs.tableWrapper; if (tableEl) { tableEl.scrollIntoView({ behavior: smooth }); } },在table-wrapper容器上加上reftableWrapper翻页后平滑滚动回表格顶部这个小细节很加分。5.4 删除或操作后列表数据不刷新如果页面支持删除删除后最好根据自己的业务做判断如果当前页已经空了且不是第一页往前退一页否则直接重新请求当前页。这个我在前面的fetchTableData里写过了这里的判断逻辑建议大家都写上不然会出现“最后一页删完最后一条表格空着但分页器还停留在 3/3 页”的尴尬情况。5.5 分页插件失效以 MyBatis Plus 为例虽然这是后端问题但前端联调时需要知道怎么定位。MyBatis Plus 的PaginationInnerInterceptor配置后依旧分页失效经常是以下原因原因快速判断方法拦截器没注册到 MyBatis 配置看启动日志搜PaginationInnerInterceptor数据库类型写错MySQL 库但配了 OracleSQL 语句完全对不上手写 SQL 里有自定义分页自己写了LIMIT会和拦截器拼接冲突前端配合排查的方式就是看 Network 面板请求参数的pageSize传的是不是字符串有些后端框架对字符串类型的pageSize解析有兼容问题建议前端统一用 Number 类型传递。5.6 和其他组件库的分页差异用惯了 Ant Design Vue 的Table自带pagination属性的人切到 iView 会有点不习惯。antd 的表格 pagination 是内置的只要传一个配置对象就行iView 则倾向于把 Table 和 Page 拆开自行组合。两种方式各有侧重antd 开箱即用但定制深度不如独立 PageiView 组合灵活但需要你自己管理状态联动。没有谁更好关键是团队技术栈统一。如果你要迁移记住 antd 的pagination.current对应 iView Page 的currentpagination.total对应total事件上 antd 是onChangeiView 是on-change其他逻辑都是一样的。6. 聊一些经验沉淀做表格分页这几年我最大的体会是分页本身不难难的是想清楚数据的生命周期。数据从接口到页面经历了参数构造、请求发送、响应解析、状态存储、切片展示、用户交互这一整条链路任何一个环节状态不同步页面上就会出幺蛾子。我个人习惯在项目里单独封装一个usePagination混入或自定义 hook把currentPage、pageSize、total、loading、fetchData这些逻辑统一收敛起来。这样每个列表页面只关心自己的columns和查询条件不用复制粘贴一大堆状态和方法也不容易出现某个页面忘了重置页码之类的低级 bug。最后再分享一个小技巧联调的时候多保持 Network 面板开着。前端一大半分页 bug 都能从请求参数上看出端倪——参数带没带、类型对不对、返回结构是否稳定。与其在代码里反复 debugger不如先瞄一眼接口请求往往一眼就能锁定问题方向。希望这篇文章能帮你少走点弯路。
返回列表