查看: 3779|回复: 3

Vue3动态组件替代v-if静态渲染的实践与问题排查

[复制链接]
发表于 4 天前 | 显示全部楼层 |阅读模式
原文以一个受理样品表格组件为例,对比 Vue3 中静态渲染与动态渲染组件的写法。核心场景是 CommonApplySampleTable 根据 acceptType 在四类表格组件之间切换:水质检材、公共场所、消毒监测和其他。静态写法把 v-if、v-else-if、v-else 直接写在四个组件标签上;动态写法用 computed 返回当前应渲染的组件对象,再交给 <component :is> 渲染。两者最终展示的表格一致,区别主要在模板重复度、选择逻辑位置和维护方式。

静态渲染的原始结构如下,每个分支都重复相同的 props:
  1. <ApplySampleSZTable v-if='waterSampleTypes.includes(acceptType)' :apply-samples='localApplySamples' :table-scroll-top='scrollTop' :operation-command='operateCommand' />
  2. <ApplySampleCSTable v-else-if='publicPlaceTypes.includes(acceptType)' :apply-samples='localApplySamples' :table-scroll-top='scrollTop' :operation-command='operateCommand' />
  3. <ApplySampleXDTable v-else-if='disinfectionTypes.includes(acceptType)' :apply-samples='localApplySamples' :table-scroll-top='scrollTop' :operation-command='operateCommand' />
  4. <ApplySampleOtherTable v-else :apply-samples='localApplySamples' :table-scroll-top='scrollTop' :operation-command='operateCommand' />
复制代码
这段代码依赖 waterSampleTypes、publicPlaceTypes、disinfectionTypes 三个类型数组判断 acceptType 命中哪类组件,未命中时落到 ApplySampleOtherTable。分支多且 props 一致时,模板会显得冗长。

动态渲染把选择逻辑收进 computed:
  1. const currentTableComponent = computed(() => {
  2.   if (waterSampleTypes.includes(acceptType.value)) return ApplySampleSZTable;
  3.   if (publicPlaceTypes.includes(acceptType.value)) return ApplySampleCSTable;
  4.   if (disinfectionTypes.includes(acceptType.value)) return ApplySampleXDTable;
  5.   return ApplySampleOtherTable;
  6. });
复制代码
模板只保留一个动态组件入口:
  1. <component :is='currentTableComponent' :apply-samples='localApplySamples' :table-scroll-top='scrollTop' :operation-command='operateCommand' />
复制代码
这里有两个关键点:computed 中 acceptType 是 Ref,必须访问 acceptType.value;返回的是组件对象,不是字符串。四个表格组件从 @/components 导入,动态组件的 props 与原静态分支保持一致。适用场景上,分支多、各分支 props 相同、组件由 acceptType 这类数据决定时,动态写法更集中;分支少或各分支需要不同 props、指令和位置时,静态写法更直观。

CommonApplySampleTable.vue 使用 <script setup lang='ts'> 和 defineOptions 定义组件名。它通过 symbols 注入 applySamples、acceptType、operateCommand、scrollTop、apply、applySampleSelection,以及 transmitApplySamplesSymbol 回传方法。组件内部维护 localApplySamples、currentApplySample、percentage 和 customColors,并定义 waterSampleTypes、publicPlaceTypes、disinfectionTypes 三个类型数组。为避免祖先组件与本地数据互相触发更新,代码使用 isUpdatingFromAncestry 作为同步隔离标志。

关键 watch 逻辑如下:
  1. watch(
  2.   () => applySamples.value,
  3.   async () => {
  4.     isUpdatingFromAncestry = true;
  5.     localApplySamples.value = cloneDeep(toRaw(applySamples.value)) as ApplySampleResponse[];
  6.     await nextTick();
  7.     isUpdatingFromAncestry = false;
  8.   },
  9.   { deep: true, immediate: true }
  10. );
  11. watch(
  12.   () => localApplySamples.value,
  13.   () => {
  14.     if (!isUpdatingFromAncestry) {
  15.       sendApplySamples(localApplySamples.value);
  16.     }
  17.   },
  18.   { deep: true }
  19. );
复制代码
第一个 watch 监听祖先传入的 applySamples,深拷贝到本地,immediate 让首次查看详情时也能加载数据;第二个 watch 监听本地列表,只有非祖先更新才回传,避免循环同步。nextTick 后再复位标志,是为了等本轮 DOM 更新完成。

删除样品采用乐观更新:先从本地过滤数据并清空选集,再调用 batchDeleteApplySamples;失败时恢复整个数组和选集。核心逻辑是:
  1. const originalSamples = [...localApplySamples.value];
  2. const originalSelection = [...applySampleSelection.value];
  3. localApplySamples.value = localApplySamples.value.filter((item) => !ids.includes(item.id));
  4. applySampleSelection.value = [];
  5. sendApplySamples(localApplySamples.value);
  6. try {
  7.   await batchDeleteApplySamples(ids);
  8. } catch (error) {
  9.   localApplySamples.value = originalSamples;
  10.   applySampleSelection.value = originalSelection;
  11.   sendApplySamples(localApplySamples.value);
  12.   console.error('删除样品失败:', error);
  13.   ElMessage.error('删除样品失败,数据已恢复');
  14. }
复制代码
新增样品时,sampleNo 由 localApplySamples.value.length + 1 生成,再用 padStart(2, '0') 补成两位编号,例如 01、02。代码调用 addBlankApplySample 后会取得自增 id,id 为 0 表示该样品编号已存在,此时用 ElMessage.warning 提示编号重复。复增样品需要先选中 currentApplySample,再 cloneDeep 复制,重新生成 newSampleNo,并用 find 检查本地编号;随后用 lodash-es 的 omit 去掉 id,转成 ApplySampleCreateRequest,再调用 addApplySampleService。

对外暴露方面,组件通过 provide(transmitCurrentApplySampleSymbol, receiveCurrentApplySample) 向后代提供接收当前选择行的方法,通过 defineExpose 暴露 addApplySample、deleteApplySample、copyAddApplySample 给父组件模板引用。模板中进度条用 v-show='percentage > 0' 控制显示,动态组件接收 localApplySamples、scrollTop 和 operateCommand;容器样式设置 height: 100%,用于继承父级确定高度并显示 el-table 内置滚动条。

动态组件也用于菜单和图标。原文用 <component :is='isFold ? Expand : Fold' /> 替代 Expand 与 Fold 的 v-if/v-else 写法;菜单图标通过 <component :is='m1.icon ? MenuIconMap[m1.icon] : MenuIconMap[`Management`]' /> 按 icon 映射选择组件,未配置时回退到 Management。菜单结构上,parentId 为 0 或 null 时渲染 el-sub-menu,否则渲染 el-menu-item。

排查这类实现时,优先检查 computed 是否访问了 acceptType.value、返回值是否为组件对象、组件是否已正确导入,以及模板 props 是否与静态分支一致。出现数据循环同步时,重点看 isUpdatingFromAncestry 是否在 nextTick 后复位、两个 watch 是否都设置了 deep。删除失败后若界面数据异常,应确认 originalSamples 与 originalSelection 是否同时回滚并再次 sendApplySamples。新增或复增时若提示编号重复,应检查 sampleNo 的生成规则、后端返回 id 是否为 0,以及复增前是否执行了 find 校验。样式上若表格没有出现内置滚动条,应检查外层容器是否真正继承到确定高度。
回复

使用道具 举报

发表于 4 天前 | 显示全部楼层

Re: Vue3动态组件替代v-if静态渲染的实践与问题排查

楼主这个对比整理得很清楚,动态组件把 acceptType 的判断收进 computed,模板确实干净很多,适用场景也总结得挺准。补充几个实际落地容易踩的点:computed 返回组件对象时,最好别让组件对象被深度响应式代理,组件对象本身不需要响应式,用 shallowRef 或者直接返回都行;如果以后四个表格改成异步导入,defineAsyncComponent 包装出来的对象更要注意这点。另外动态组件没有 key 时,切换 acceptType 但组件类型没变的情况下会复用同一个实例,如果表格内部有选集、滚动位置这类局部状态,希望切类型时重置的话,可以给 component 加上按 acceptType 区分的 key。 watch 那段同步隔离的思路没问题,不过深拷贝加 deep 监听大表格数据要留意性能,immediate 首次加载时 nextTick 后复位标志,如果中间组件卸载或出现异常,最好有个兜底。乐观更新里 originalSamples 是浅拷贝,如果数组里对象后续被原地修改,失败恢复回去的引用可能已经不是原样了,配合 cloneDeep 或者过滤时生成新对象会更稳妥。 最后一点,动态写法在四个分支 props 完全一致时最舒服,一旦各分支需要不同的 props、事件或指令,就得拼 v-bind 对象,类型提示和可读性都会下降,这时候退回静态写法反而更直观。整体
回复 支持 反对

使用道具 举报

发表于 4 天前 | 显示全部楼层

Re: Vue3动态组件替代v-if静态渲染的实践与问题排查

这个对比写得很清楚,尤其是动态写法里 acceptType 是 Ref 要取 value、computed 返回组件对象而不是字符串,这两点确实是容易踩的地方。分支多且 props 一致时用 component is 确实能让模板干净很多,选择逻辑集中后也更好维护;你总结的静态写法适用场景也很实在,分支少或各分支 props、指令差异大时没必要硬套动态组件。 watch 这里用 isUpdatingFromAncestry 做同步隔离、nextTick 后复位,思路是对的。小建议:async watch 里最好用 try/finally 复位标志,这样 cloneDeep 或 nextTick 出异常时不会把标志一直留在 true,导致后续本地修改不回传。 乐观更新部分先本地过滤并清空选集、失败再恢复整个数组和选集,体验会不错。如果还没做,建议失败时给用户明确提示,并考虑并发删除时用快照或请求序号保护回滚,避免后一个请求失败把前一个成功的结果覆盖掉。整体这组实践对做类似表格切换和父子数据同步挺有参考价值。
回复 支持 反对

使用道具 举报

发表于 4 天前 | 显示全部楼层

Re: Vue3动态组件替代v-if静态渲染的实践与问题排查

这篇实践总结得挺清楚,动态组件和静态 v-if 的取舍讲得很实在。分支多、props 一致时把选择逻辑收进 computed 确实更集中,模板也干净很多;computed 里访问 acceptType.value、返回组件对象而不是字符串,这两个点也提醒得很到位。 watch 同步隔离加 nextTick 复位标志的处理挺合理,深拷贝本地数据再只在非祖先更新时回传,能避免很多双向同步的坑。乐观更新部分先本地过滤、清空选集,失败再整体恢复,思路也没问题。不过这里可能要注意连续删除时的竞态:如果前一个 batchDeleteApplySamples 还没结束又触发删除,失败恢复 originalSamples 和选集时容易覆盖后一次操作。可以考虑给删除请求加序号或请求中禁用操作,确保只恢复对应那次的状态。另外恢复选集时最好保存一份独立副本,避免和本地选集引用互相影响。 另外选择逻辑如果类型数组会变化,用 Set 或 const 断言可能更清晰;动态组件切换时如果希望各表格内部状态完全隔离,也可以考虑加 key。整体上这个案例对实际项目挺有参考价值。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-27 17:23 , Processed in 0.025662 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部