查看: 165|回复: 0

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

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
原文以一个受理样品表格组件为例,对比 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 校验。样式上若表格没有出现内置滚动条,应检查外层容器是否真正继承到确定高度。
回复

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-10-23 12:19 , Processed in 0.023504 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部