在日常中后台开发中,我们经常需要实现各种数据列表页,包括查询条件表单、数据表格、分页等功能。然而,不同业务的列表页虽然样式和交互相似,但背后的后端接口格式可能千差万别。一旦后端接口格式发生变化,前端列表页的代码就可能需要大幅修改,给维护带来不少麻烦。本文将介绍一套通用的可复用列表页组件方案,通过灵活的配置和技巧来应对各种“奇葩”后端接口。
为什么需要通用的列表页组件?
在中后台项目中,列表页通常由查询条件表单和数据列表展示组成,是重复率很高的功能模块。如果每个列表页都各自实现,会产生大量重复代码,也不利于统一维护。构建一个通用的列表页组件可以带来诸多好处:
- 减少重复代码:将列表页的通用逻辑(如分页处理、表格渲染、查询表单)封装起来,在不同页面复用,避免每次从零编写。
- 统一交互与样式:统一列表页的查询交互(如“展开/收起”高级查询)、表格样式和空数据提示等,提升系统的一致性。
- 应对需求变更:当需要对列表页功能做修改(比如新增导出按钮、调整分页逻辑)时,只需在组件内部修改一次,所有使用该组件的页面都会同步更新。
- 屏蔽后端差异:通过配置来适配不同后端接口的请求和响应格式,列表组件内部消化这些差异,页面使用方无需感知接口的特殊性。
综上,封装通用列表页组件既是工程复用的需要,也是提高开发效率和代码健壮性的有效手段。
requestConfig.buildPayload:适配不同后端接口格式
后端的列表接口往往有不同的请求入参规范。例如,有的接口期望请求体直接提供查询条件,有的则要求将查询条件嵌套在 model
或 pageBean.model
下,还有的分页参数字段名各不相同。如果我们在每个页面手动拼装不同格式的请求,无疑增加了重复劳动和出错概率。
解决方案:在通用列表页组件中引入 requestConfig.buildPayload()
钩子,用于根据统一的查询参数对象构建不同格式的请求载荷。组件对外暴露 requestConfig
配置,使用方可以传入自定义的 buildPayload
函数来自定义请求格式:
// 使用通用列表组件时传入配置
<CommonList
:requestConfig="{
url: '/api/getListData',
method: 'POST',
// 自定义请求载荷构建逻辑
buildPayload: (queryParams, pagination) => {
// 例:将查询参数包裹在 pageBean.model 中,并添加分页信息
return {
pageBean: {
page: pagination.currentPage,
size: pagination.pageSize,
model: queryParams
}
}
}
}"
... />
在组件内部,每当需要发起列表请求时,会调用 requestConfig.buildPayload(formData, pagination)
来获取最终的请求体。例如:
- 直接使用
model
包装:某些接口希望所有查询条件都放在model
字段下,那么buildPayload
可以返回{ pageNum, pageSize, model: { ...查询条件 } }
。 - 嵌套在
pageBean.model
:对于要求分页信息和查询条件一起封装的接口,则返回{ pageBean: { page, size, model: { ...查询条件 } } }
。 - 无特殊包装:如果后端直接接受平铺的查询参数,那么
buildPayload
直接返回{ page, size, ...查询条件 }
即可。
通过这种钩子机制,我们实现了请求格式的适配层。无论后端接口多么“奇葩”,我们都能在不改动组件核心代码的前提下,通过定制 buildPayload
轻松应对。这极大提升了组件的适应性,也让接口变更对前端的影响降到最低。
优雅实现查询表单的展开/收起
高级查询条件往往很多,我们通常提供“展开/收起”按钮来在界面上隐藏部分条件。当用户点击“展开”时显示所有字段,“收起”则只显示基础字段。如何实现这个功能,同时保证表单字段的状态不丢失,是我们要解决的关键问题。
常规做法的问题
一些常见但不理想的实现方式包括:
- 替换表单规则:通过切换不同的表单字段列表(form rule)来控制显示哪些字段。例如收起时使用一套精简字段数组,展开时替换成完整字段数组。然而这样做会导致组件的销毁和重建,已填写的数据会丢失。尤其是在使用表单生成器如 form-create 时,动态增删字段会重置部分已选值。频繁切换规则也增加了实现复杂度。
- v-if 条件渲染:对每个可收起字段加上
v-if="showAll"
来决定渲染与否。这种方式同样会在收起时移除 DOM 元素,字段状态可能会丢,而且需要在展开时重新挂载组件。类似地,表单验证状态也会被重置。
用隐藏属性控制显示
推荐做法是利用隐藏属性来控制字段显隐,而非移除节点或替换整个规则。在 form-create 中,我们有两种隐藏方式:
- 隐藏字段(无 DOM) :通过调用
fApi.hidden(true, fieldName)
可以隐藏指定字段,隐藏后完全不渲染对应的组件,DOM 节点将移除。这样做适合初始就不需要渲染大量高级字段的场景,减少 DOM 开销。但要注意,字段隐藏后表单验证也不会触发。 - 隐藏组件(保留 DOM) :调用
fApi.display(false, fieldName)
则会将组件通过 CSS 隐藏(display:none
),但组件实例仍然保留在 DOM 中。优点是字段的绑定值和验证状态都不会丢失,再次显示时能保持原有状态。
在实际实现中,我们可以结合两种方式。例如初始进入页面时将高级字段使用 hidden
隐藏以减轻渲染负担;当用户点击“展开”按钮时,再将这些字段用 display
显示出来。收起时,则仅隐藏(display:none)而不销毁组件,以便保留用户可能已输入的内容。
具体代码逻辑示例:
data() {
return {
showAll: false, // 控制展开/收起的状态
advancedFields: ['age', 'address', 'company'] // 需要隐藏的高级查询字段name列表
}
},
methods: {
toggleFields() {
this.showAll = !this.showAll;
if (this.showAll) {
// 展开:显示所有字段
this.fApi.display(true, this.advancedFields);
} else {
// 收起:隐藏高级字段(保留其值和状态)
this.fApi.display(false, this.advancedFields);
}
}
}
通过这种方式,“展开/收起”查询表单非常流畅:组件状态不重建不重置,用户在高级字段中已输入的值在收起后虽然不可见,但再次展开时还能看到,避免了反复输入。同时,隐藏的字段也不会影响布局,表单其余部分不会因为移除节点而闪烁。
隐藏字段的查询与重置处理
实现字段隐藏后,还需要处理两个细节问题:查询时隐藏字段不参与、重置时隐藏字段也要处理。否则可能出现隐藏字段的值误参与查询,或重置操作无法清空隐藏字段等情况。
跳过隐藏字段参与查询
当用户收起高级查询后,再点击查询按钮时,我们不应将隐藏字段的值提交给后端,否则会造成意外的筛选。即使之前用户在高级字段填过值,收起状态下也应视为暂不使用。为此,可以在构造请求参数时过滤掉所有当前隐藏的字段:
const formData = this.fApi.formData(); // 获取表单所有字段的当前值
for (const field of this.advancedFields) {
if (!this.showAll) {
// 收起状态下,直接移除高级字段的参数
delete formData[field];
}
}
const payload = this.requestConfig.buildPayload(formData, this.pagination);
如上,我们利用 fApi.formData()
获取所有字段的值,然后根据 showAll
状态剔除 advancedFields
列表中的字段。这样生成的查询参数就只包含可见的查询条件,保证后台只按用户期望的条件筛选数据。
当然,更严谨的做法是利用 form-create 提供的 hiddenStatus
接口动态判断字段是否隐藏:
Object.keys(formData).forEach(field => {
if (this.fApi.hiddenStatus(field)) {
delete formData[field]; // 隐藏状态则剔除
}
});
这在多处使用隐藏字段的场景下更加通用。
确保重置清空所有字段
点击“重置”按钮或执行表单清空时,我们期望所有查询条件都被清空,包括那些当前隐藏的高级字段。然而,如果直接使用 Element UI 提供的 this.$refs.form.resetFields()
或 form-create 的 fApi.resetFields()
,需要注意默认行为是否覆盖隐藏字段。
在 form-create 中,fApi.resetFields()
会重置表单的所有字段值(也可以选择特定字段)。但是这里的“重置”往往是指恢复初始值:如果某些字段设置了初始值,reset 后会回到初始值而非空。因此,为实现“彻底清空”,我们可能需要做额外处理:
- 未设置初始值的字段:reset 后本来就是空的,可直接使用
resetFields()
清空。 - 有默认初始值的字段:reset 会回到默认值,而我们希望清空为“无”。对于这类字段,可以在重置后调用
setValue
将其设为空字符串或空数组等。 - 隐藏字段:确保在重置时也包含隐藏字段。一种简单方式是直接调用
fApi.resetFields()
不传参,让它重置所有字段。如果我们之前对隐藏字段做了剔除查询等操作,resetFields 仍会把它们复位到初始值。若想完全清除其值,可以在 resetFields 之后再主动将高级字段对应的值设置为''
或undefined
。
综合考虑,最佳实践是封装一个清空方法,既使用 resetFields 恢复默认,又针对有默认值或特殊需求的字段做定制处理。例如:
resetAllFields() {
// 重置所有字段到初始值
this.fApi.resetFields();
// 清空隐藏字段的值(覆盖默认初始值的情况)
this.advancedFields.forEach(field => {
this.fApi.setValue(field, ''); // 将值设为空(或相应类型的初始空值)
});
}
这样,无论字段当前是否显示,我们都能确保查询条件彻底被清空,不会遗留上次的状态。
internalRule:避免直接改动组件属性
在使用 form-create 构建动态表单时,我们经常需要更新字段的属性或状态,例如切换字段的 disabled
、修改占位提示文字等。如果不借助正确的方法,直接修改 form-create 内部生成的组件属性,可能导致不可预期的结果,甚至丢失字段状态。我们通过一个内部规则(internalRule)机制来避免这些问题。
避免直接修改表单项属性
直接操作表单项的 props 可能遇到以下坑:
- 修改不生效:form-create 对传入的规则进行了封装,直接更改
rule.props.x
有时不会触发视图更新,因为内部可能没有观测这些深层变化。 - 状态丢失或重置:粗暴地替换整个规则对象会导致对应字段被重新创建,之前填写的值或校验状态丢失。这和前文提到的增删规则类似,会清空已有输入。
一个典型案例是,动态增删字段或修改其属性后,需要保留用户已填写的数据。form-create 官方建议使用其提供的 API 方法来操作,例如使用 fApi.setValue()
给字段赋值,以及避免直接操作规则数组。正如前述,当我们调用 this.rule.push()
新增规则时,其他字段可能重置;而使用 fApi.append()
或 prepend()
等方法就能避免这种情况。
internalRule 思路
所谓 internalRule,指的是在组件内部维护一份表单规则的源数据副本或额外的配置,用于记录和控制字段属性变化。要点如下:
- 初始规则克隆:在创建表单时,将传入的
formRule
深拷贝一份保存在组件内部(例如this.internalRule = deepClone(props.formRule)
)。后续所有对表单结构的调整,都基于 internalRule 来进行。 - 统一通过 API 更新:当需要修改字段属性(比如隐藏、禁用某字段)时,不直接操作
props.formRule
,而是通过fApi
提供的方法或更新 internalRule 来实现。例如,要禁用名为 "status" 的字段,我们优先选择this.fApi.disabled(true, 'status')
,这会由 form-create 内部去处理 DOM 和状态同步,而不是手动改rule.props.disabled
。 - 确保状态不丢失:由于我们保留了 internalRule,哪怕外部传入的规则在父组件因条件变化而重新计算,我们仍可以根据 internalRule 判断哪些字段之前的状态需要恢复。比如在展开高级查询时,我们知道哪些字段应该处于什么状态,而不依赖于外部重新给我们的规则(因为外部可能不知道用户中途对字段的改动)。
举个例子,假设我们的组件接受一个 formRule
列表作为 Prop。我们在内部保存 internalRule
,并使用它生成 form-create 表单。当父组件可能出于某些原因重新传入一个新 formRule
时,我们可以智能对比 newProps 和 internalRule,仅对差异部分更新,而用户在界面上交互产生的状态(选中的值、隐藏显示状态等)在 internalRule 中有记录,不会无故被覆盖。
通过 internalRule,我们相当于构建了一个单一数据源来管理表单结构和状态的变化,避免了外部频繁调整导致的冲突。这种模式下,组件内部对 form-create 有完全的掌控力,确保了字段状态的稳定与延续。
彻底清空 vs. resetFields:清理查询条件的最佳实践
在实际项目中,用户有时希望“一键清空”所有查询条件,恢复到一个完全空白的初始状态(可能与默认初始值不同)。结合我们前面的讨论,总结出几条最佳实践建议:
- 使用组件方法优于手动操作 DOM:无论是隐藏字段还是清空表单,都应优先使用 form-create 提供的 API(如
hidden/display
、resetFields
、setValue
等)来操作。避免通过操作 DOM 或组件实例属性的方式清理数据,这样更稳健也更易维护。 - 区分重置和清空:resetFields倾向于恢复初始值,而“清空”通常指把用户填写的数据全部清除。根据需求选择合适的方法,必要时组合多种手段。例如先 reset 恢复默认,再二次清理默认值字段。
- 隐藏字段特殊处理:对于暂时隐藏的字段,查询时过滤、清空时仍要重置。这保证了隐藏即忽略,但一旦显现又是干净的新状态,不会把收起时残留的数据误用到下一次查询。
- 保存用户输入体验:在展开/收起切换中,不轻易销毁用户已经输入的内容,而通过隐藏来软控制。这给了用户更好的体验(展开后还能找回之前输入的条件),同时对开发来说也减少了状态管理的复杂度。
最后,经过这套方案改造后的列表页组件,无论面对怎样的后端接口格式变更或需求调整,都能从容应对——后端奇葩接口再也无法威胁我们的前端代码稳定。
总结:构建通用列表页组件时,既要考虑适配各种接口格式(通过 buildPayload
等钩子灵活封装),又要注重前端交互细节(如查询表单的展开收起实现)。利用 form-create 等工具的特性,我们可以优雅地隐藏和显示表单项,避免直接改 DOM 或规则造成的数据丢失。同时,要善用其 API 进行状态管理和表单重置,确保每次查询和重置都符合预期。按照以上最佳实践,就能封死各种后端奇葩接口对前端的影响,稳健地提升列表页的可维护性和用户体验。