Vue 2中按钮与表单组件的合理解耦方案
Vue 2 按钮与表单组件通信的解耦方案
我在Vue 2项目中实现按钮与表单组件的交互,按钮包含**初始、加载、完成(成功/失败)**三种状态,不同状态对应不同的显示文本与图标:
- 加载状态:显示旋转图标
- 成功完成:显示对勾图标
- 失败完成:显示叉号图标
目前表单组件通过按钮的点击事件接收startLoading和concludedSuccessfully回调函数,以此切换按钮状态,但这种方式导致组件耦合度过高。我也考虑过通过ref调用按钮方法的方式,但希望找到更优雅的解耦方案。
原按钮组件代码
<template> <button class="ui-button" @click="clicked" :data-status-type="status_type" :disabled="is_disabled" :type="type" > <i :class="icon" v-if="is_disabled || concluded"></i> {{ title }} </button> </template> <script> export default { props: { title: { type: String, }, type: { default: "button", type: String, }, }, data() { return { concluded: false, icon: "fa fa-spin ", is_disabled: false, status_type: "success", }; }, methods: { clicked() { if (!this.is_disabled) { this.$emit( "clicked", () => { this.is_disabled = true; this.icon = "fa fa-spin fas fa-spinner"; }, (succeeded) => { this.is_disabled = false; this.concluded = true; this.icon = succeeded ? "fas fa-check" : "fas fa-xmark"; this.status_type = succeeded ? "success" : "error"; setTimeout(() => { this.concluded = false; this.icon = ""; this.status_type = ""; }, 1500); } ); } }, }, }; </script>
原表单组件代码
<template> <div> <ThePages :parents="accompaniments"> <!-- ... 其他响应式内容 --> <template #extra_button> <TheButton @clicked="sendItemToCart" :title="button_text" :disabled="button_disabled" /> </template> </ThePages> </div> </template> <script> import axios from 'axios' import FormatHelper from '../helpers/FormatHelper' import SwalHelper from '../helpers/SwalHelper' import TheButton from './TheButton.vue' import ThePages from './ThePages.vue' import TheQuantityPicker from './TheQuantityPicker.vue' export default { props: ['product'], components: { TheButton, ThePages, TheQuantityPicker, }, data() { return { accompaniments: this.product.accompaniment_categories, button_text: '', button_disabled: false, format_helper: FormatHelper.toBRCurrency, observation: '', quantity: 1, success: false, } }, created() { this.addQuantityPropToAccompaniments() this.availability() }, methods: { // ... 其他方法 async sendItemToCart(startLoading, concludedSuccessfully) { startLoading() // 切换按钮状态为加载中 this.button_text = 'Adicionando...' await axios .post(route('cart.add'), { accompaniments: this.buildAccompanimentsArray(), id: this.product.id, quantity: this.quantity, observation: this.observation, }) .then(() => { concludedSuccessfully(true) // 切换按钮状态为成功 this.button_text = 'Adicionado' SwalHelper.productAddedSuccessfully() }) .catch((error) => { concludedSuccessfully(false) // 切换按钮状态为失败 if ( error?.response?.data?.message == 'Este produto atingiu a quantidade máxima para este pedido.' ) { SwalHelper.genericError(error?.response?.data?.message) } else { SwalHelper.genericError() } this.button_text = 'Adicionar ao carrinho' }) }, }, } </script>
优化方案一:通过Props传递状态,按钮内部处理UI变化
让按钮组件成为受控组件,完全依赖外部传入的状态值渲染UI,表单组件只需要维护状态逻辑,实现彻底解耦。
优化后的按钮组件
<template> <button class="ui-button" @click="$emit('click')" :data-status-type="statusType" :disabled="isDisabled" :type="type" > <i :class="iconClass" v-if="isDisabled || status === 'success' || status === 'error'"></i> {{ title }} </button> </template> <script> export default { props: { title: { type: String, required: true }, type: { default: "button", type: String, }, // 外部传入按钮状态:idle / loading / success / error status: { type: String, default: 'idle', validator: val => ['idle', 'loading', 'success', 'error'].includes(val) } }, computed: { isDisabled() { return this.status === 'loading' }, statusType() { return this.status === 'success' ? 'success' : this.status === 'error' ? 'error' : '' }, iconClass() { switch(this.status) { case 'loading': return 'fa fa-spin fas fa-spinner' case 'success': return 'fas fa-check' case 'error': return 'fas fa-xmark' default: return '' } } }, watch: { status(newVal) { // 成功/失败状态1.5秒后通知外部重置 if (newVal === 'success' || newVal === 'error') { setTimeout(() => { this.$emit('reset-status') }, 1500) } } } }; </script>
优化后的表单组件
<template> <div> <ThePages :parents="accompaniments"> <!-- ... 其他响应式内容 --> <template #extra_button> <TheButton @click="sendItemToCart" :title="buttonText" :status="buttonStatus" @reset-status="resetButtonStatus" /> </template> </ThePages> </div> </template> <script> import axios from 'axios' import FormatHelper from '../helpers/FormatHelper' import SwalHelper from '../helpers/SwalHelper' import TheButton from './TheButton.vue' import ThePages from './ThePages.vue' import TheQuantityPicker from './TheQuantityPicker.vue' export default { props: ['product'], components: { TheButton, ThePages, TheQuantityPicker, }, data() { return { accompaniments: this.product.accompaniment_categories, buttonText: 'Adicionar ao carrinho', buttonStatus: 'idle', format_helper: FormatHelper.toBRCurrency, observation: '', quantity: 1, } }, created() { this.addQuantityPropToAccompaniments() this.availability() }, methods: { // ... 其他方法 async sendItemToCart() { this.buttonStatus = 'loading' this.buttonText = 'Adicionando...' try { await axios.post(route('cart.add'), { accompaniments: this.buildAccompanimentsArray(), id: this.product.id, quantity: this.quantity, observation: this.observation, }) this.buttonStatus = 'success' this.buttonText = 'Adicionado' SwalHelper.productAddedSuccessfully() } catch(error) { this.buttonStatus = 'error' this.buttonText = 'Adicionar ao carrinho' if (error?.response?.data?.message === 'Este produto atingiu a quantidade máxima para este pedido.') { SwalHelper.genericError(error?.response?.data?.message) } else { SwalHelper.genericError() } } }, resetButtonStatus() { this.buttonStatus = 'idle' this.buttonText = 'Adicionar ao carrinho' } }, } </script>
其他可选方案
- 全局状态管理(Vuex/Pinia):如果项目中有多个组件需要复用按钮状态逻辑,可以将状态抽离到全局仓库,按钮从仓库获取状态,表单通过提交更新状态的动作实现交互。
- 事件总线:创建全局事件总线,表单组件发送状态变更事件,按钮组件监听事件更新自身状态。但该方式在大型项目中难以维护,仅推荐小型项目使用。
内容的提问来源于stack exchange,提问作者Bernardo Benini Fantin
相关产品推荐
相关产品推荐

