vue开发中的常见败招
1 父子组件都是表单,父组件如何获取子组件中的值
<parent-form>
<child-form :initFormData="xxx" ref="childForm">
</child-form>
<button @click="save">提交</button>
</parent-form>
假设有这样一个组件,父组件有自己的表单项,同时嵌套一个子组件也是表单,当父组件点击提交时候 需要获取子组件的数据,很多人都会直接用ref 直接获取子组件中的data属性,其实这是非常差劲的写法,
使用 ref 直接获取子组件的 data 虽然可行,但在 Vue 的开发模式中并不是最优解。
最优方案建议:使用 v-model 双向绑定
为什么 v-model 更好?
- 数据流清晰 :符合 Vue “Props down, Events up” 的核心原则。父组件不需要知道子组件内部变量叫什么(比如是叫 tableFormData 还是 ruleFormParams ),只需要关心绑定的数据。
- 解耦 :父组件不需要通过 ref 去探测子组件的内部状态。如果以后子组件内部重构了变量名,只要 v-model 协议不变,父组件代码无需任何改动。
- 实时同步 :父组件的 form 中始终保持着最新的表格数据,点击“保存”时直接提交 this.form 即可,不需要在提交瞬间去额外“收集”一次数据。
实现步骤
我已经为你完成了相关的代码重构,具体逻辑如下:
- 修改子组件支持 v-model 在 BodyTableForm.vue 和 ParamsTableForm.vue 中:
- 将接收数据的 prop 从 params 改为 value (Vue 2 v-model 的默认 prop)。
- 添加 watch 监听:
- 监听 value :当父组件传入新数据时,同步给内部的 tableFormData 。
- 监听 tableFormData :当表格内容发生变化(新增、删除、修改)时,通过 $emit('input', newVal) 通知父组件。
// 子组件核心逻辑
props: {
value: { type: Array, default: () => [] }
},
watch: {
value: {
handler(val) { this.tableFormData = val; },
immediate: true, deep: true
},
tableFormData: {
handler(val) { this.$emit('input', val); },
deep: true
}
}
- 父组件使用 v-model 绑定 在 ApiRequest/index.vue 中:
- 移除了冗余的 computed 属性,改为在 data.form 中直接维护 params 和 requestBodyFields 。
- 模板中直接使用 v-model 绑定。
<!-- 父组件模板 -->
<BodyTableForm v-model="form.requestBodyFields" />
<ParamsTableForm v-model="form.params" />
- 完善初始化逻辑 在父组件的 initForm 中增加了 深拷贝 逻辑。这样可以确保子组件对数据的修改不会意外污染到 Vuex 原始数据,只有在点击“确定”提交后才会生效。
总结:其他可选方案对比
通过现在的 v-model 实现,你的父组件在点击“确定”时, this.form 已经包含了所有子表格的最新数据,逻辑非常自然。
2 “默认值覆盖”问题。
核心问题分析
当你使用 this.form = { ...this.form, ...this.apiInfo } 时,JavaScript 的对象展开运算符(Spread Operator)会遵循“后者覆盖前者”的原则。即便 this.apiInfo 中的字段值是 null 、 undefined 或 '' (空字符串),它依然会无情地覆盖掉 this.form 中预设的系统默认值(如 GET 或 FALSE )。
最优解:在组件初始化时使用“非空合并”策略
虽然可以在 Store 中处理,但我更建议在 组件的 initForm 方法中 解决。
原因 :
-
UI 表现与数据分离 :Store 应该尽可能真实地反映后端返回的数据(哪怕是空的)。而“默认值”往往是 UI 层的表现需求。
-
灵活性 :不同的表单可能对同一个数据对象有不同的默认值需求。
-
可维护性 :在组件 data() 中定义的默认值一眼可见,维护起来最直观。 修改后的代码实现 我已经更新了 ApiRequest/index.vue 的逻辑:
initForm () { if (this.apiInfo) { // 1. 先进行深拷贝,断开引用 const apiData = JSON.parse(JSON.stringify(this.apiInfo))
// 2. 关键逻辑:过滤掉 apiInfo 中的“空值” // 只有当 apiInfo 中的字段真正有内容时,才去覆盖 form 的默认值 Object.keys(apiData).forEach(key => { const val = apiData[key] if (val === null || val === undefined || val === '') { delete apiData[key] // 如果是空值,从待合并对象中删除 } }) // 3. 执行合并 this.form = { ...this.form, ...apiData }} },
为什么不建议在 Store 中处理?
- Store 污染 :如果在 setOperationCurrentResource 突变中强行填入默认值,那么当你后续需要判断“这个字段是用户填的还是系统默认的”时,就会变得非常困难,因为原始的空状态已经丢失了。
- 不可预测性 :全局 Store 的改动会影响所有引用该数据的组件。如果你在 Store 里把 null 变成了 GET ,某些只需要展示“暂无数据”的只读页面可能就会错误地显示出 GET 。
总结
通过在组件内采用 “非空合并” 策略:
- 保留了默认值 :如果后端没传,保留 data() 里的 GET 。
- 尊重了有效值 :如果后端传了 POST ,它会正确覆盖 GET 。
- 保护了 Store :Vuex 里的数据依然是纯净的。 这种方案既解决了你的覆盖问题,又保证了代码的健壮性和可维护性。
tab切换子组件获取其他子组件的接口数据
采用了**“Vuex Action 集中管理 + 组件内按需触发”**的重构方案。这种方式在保证数据即时性的同时,也最大程度地降低了组件间的耦合。
修改内容 1. 在 Store 中封装 fetchAuthList Action
在 store/index.js 中新增了一个异步 Action。
-
智能判断 :增加了一个 force 参数。默认情况下,如果 Store 中已经有数据,它不会重复请求接口,从而节省性能。
-
统一入口 :所有需要获取认证列表的地方都统一走这个 Action。 2. 重构认证列表页逻辑 修改了 Authentication/index.vue ,将其原本分散的接口调用改为了 dispatch('fetchAuthList', { force: true }) 。
-
强制刷新 :在认证主页面,我们显式传入 force: true ,确保用户看到的列表始终是最新的。 3. 在操作弹窗中增加“保底”加载逻辑 在 Operation/Resource/AddDialog/index.vue 中增加了对 visible 的监听:
-
按需加载 :当“添加资源”的弹窗打开时,组件会检查 Store 中的 authList 是否为空。如果是空的(说明用户直接跳过了认证 Tab),则会自动触发一次数据加载。
重构后的优势
- 数据安全 :彻底解决了你担心的“直接点击操作 Tab 导致 authList 为空”的问题。
- 逻辑复用 :获取列表的逻辑从组件中抽离,变得可插拔。如果以后还有第三个、第四个 Tab 需要用到认证数据,只需要在相应位置执行一行 dispatch 即可。
- 架构专业 :符合 Vuex “Actions 处理异步,Mutations 处理同步”的规范,代码可读性和可测试性显著提升。
子组件上报,父组件编排” 的经典设计模式
父子组件通过props传递数据到子组件,而不是在组件中直接调用vuex, 如果不是直接子组件,是子组件的子组件,可以考虑将中间的组件设计成容器组件, 通过 v-bind="listeners"完全透传子组件信息
- 子组件:职责纯净化 所有 Tab 子组件( BasicInfo.vue , Authentication/index.vue , 等)不再直接操作 Store 里的 stepsStatus 。
-
它们现在只负责在关键时刻(如保存成功、数据加载完成)派发一个通用的 @status-change 事件。
-
代码示例 : this.$emit('status-change', true) 。 2. 父组件:流程编排者 Details.vue 承担起“管家”的角色:
-
在模板中通过 @status-change="handleStatusChange(DETAIL_TAB.XXX.value, $event)" 显式监听每个 Tab 的状态。
-
由父组件统一调用 updateStepStatus 来更新全局 Store。
-
优势 :你现在只需要在 Details.vue 里的 handleStatusChange 方法打一个断点,就能监控到全流程中任何一步的状态切换,调试效率极大提升。 3. Store:数据契约层 store/index.js 只负责接收指令并持久化状态,不再被多个组件直接修改,数据流向变得更加清晰(单向数据流)。
重构收益总结
-
解耦 :子组件不再需要导入 DETAIL_TAB 常量或映射 updateStepStatus mutation,变得更加轻量和独立。
-
透明 :父组件的模板里清晰地展示了哪些组件会触发流程变化,逻辑不再“隐身”。
-
健壮 :避免了子组件因为拼写错误或引用了错误的 Store Key 而导致的流程卡死。
典型的 UI 布局与组件解耦之间的矛盾
---逻辑属于子组件,但 UI 必须呈现在父组件外框 ”的场景
如图所示,新建配置在ui上和tab bar在一行,但是实际上他的功能操作只是针对于认证表单内部的操作, 认证配置组件名称 detail 包含tabs和以下子组件 基本信息 baseinfo 认证表单 auth 认证开发 dev
要如何实现这个功能,常见做法
1 最简单的做法, 新建配置按钮建在detail组件下,当点击的时候通过ref调用auth组建的定义的方法,类似于el-form对外提供的valited方法, 优点是调用简单,缺点是,detail组件需要知道auth组件定义的方法,有一天子组件删除了这个方法,则父组件会报错, vue最佳时间原则 props down, eventsup 原则。此种方式不符合这个原则
2 新建配置按钮建在auth组件下 + css样式
通过给按钮加css样式 定位的方式移动到 tab bar位置, 这种方式不好,因为严重依赖父组件样式,比如父组件需要增加相对定位等,有一天detail组件改变样式,则auth样式需要跟随修改,且auth组件需要增加overflow:visible,否则新建配置按钮超出自己的边界后就看不到了
3 slot插槽能用吗, 不能
Vue 中 slot 的标准用法确实是「子组件定义插槽(挖坑),父组件使用时填充内容(填坑)
正好和现在需求相反,现在想要的类似于父组件挖坑,子组件填坑,所以适合用teleport方式
4 teleport 使用注意事项 再看图片上有两个ui特点 1 ,只有tab选中auth的时候才展示 新建配置, 需要detail组件将当前激活的是否是auth组件通过props传递给auth组件,auth组件通过v-if判断是否渲染 teleport
2 当有多个detail组件可以互相切换,类似于手风琴效果,那么每次切换当前deatil都会销毁重建,那么就会引发两个问题
1 teleport挂载时机,当detail先销毁未创建时候,开始初始化并尝试执行 Teleport 。如果此时 .auth-tabs-action-portal (假设teleport挂在的节点名称是.auth-tabs-action-portal )节点还在 Vue 的渲染队列中,尚未真正插入物理 DOM, Teleport 就会因为找不到目标而失败。
选择器冲突 :由于你在 v-for 循环中使用类名选择器 .auth-tabs-action-portal ,在 transition 动画期间,可能会短暂存在 两个 具有相同类名的节点(旧的还没销毁完,新的已经创建),导致按钮挂载到了错误的节点上。
最优解决方案:唯一标识 + 挂载延迟
为了彻底解决这个稳定性问题,我们需要做两件事: 确保目标唯一 和 确保目标已就绪 。 第一步:在父组件中提供唯一的 Portal 目标 修改 AuthDetailView/index.vue ,为每个行分配一个唯一的类名或 ID。
<!-- AuthDetailView/index.vue -->
<div class="content-body" v-show="isContentExpanded">
<!-- 【优化点 1】:使用 row.code 确保选择器的全局唯一性 -->
<div :class="['auth-tabs-action-portal-' + row.code, 'auth-tabs-action-portal']"></div>
<el-tabs v-model="activeTab">
<el-tab-pane label="认证表单" :name="TAB_KEYS.FORM">
<auth-form-config
v-if="activeTab === TAB_KEYS.FORM"
:portalTarget="'.auth-tabs-action-portal-' + row.code"
...
/>
</el-tab-pane>
</el-tabs>
</div>
第二步:在子组件中控制挂载时机 修改 AuthFormConfig.vue ,通过 mounted 生命周期钩子来确保 DOM 已经渲染完成。
<!-- AuthFormConfig.vue -->
<template>
<div class="auth-form-config">
<!-- 【优化点 2】:增加 isMounted 判断,确保组件挂载后才执行 Teleport -->
<Teleport v-if="isMounted" :to="portalTarget">
<div class="auth-form-config-btn-group" @click="handleClickAddField">
<span>新建配置</span>
</div>
</Teleport>
...
</div>
</template>
<script>
export default {
props: ['portalTarget', ...],
data() {
return {
isMounted: false // 初始设为 false
}
},
mounted() {
// 【关键】:在 mounted 之后,DOM 节点肯定已经存在于页面中了
this.$nextTick(() => {
this.isMounted = true;
});
}
}
</script>