- API 路由统一: 移除 /api/h5/,统一 /api/pms/ 入口 - 二进制改名: api-server -> pms-api(与服务名一致) - nginx: 合并 /api/ + /pms/api/ 为 /api/pms/ - 新增 docs/API.md 接口文档 - 后端: CabinetDetail 增加 rssi/pow_fail/address/soc 等字段 - Redis 管道批量读取仓体实时数据(SOC/电压/电流/功率) - DB: 所有 TIMESTAMP 列改为 DATETIME(修复类型不匹配) - 用户管理: 新增创建时间/修改时间列,状态改用 Switch - 角色管理/用户管理: 操作按钮改为图标 - PC 设备详情: 基本信息补全 RSSI/ICCID/电源/地址 - PC 卡片: 通道统计改用 Lucide 图标,颜色统一 - PC 二维码: canvas 渲染,含编号文字,支持右键下载 - H5: 按 uni-app 规范修正导航(navigateTo 替代 reLaunch) - H5: 详情页加返回导航条 - H5: 扫码支持摄像头(jsQR)+ ?id= 参数 - H5: 底部 tabBar 在所有页面常显 - H5: 首页统计改为 6 项(设备/通道/空闲/充电/充满/故障) - H5: 下拉刷新 / 上拉加载 - 产品首页: 添加业务联系 - 文档: 清理过时文件,补充缺失规范章节
5.5 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Component Events Don't Bubble | MEDIUM | Vue component events only reach direct parent - sibling and grandparent communication requires alternative patterns | gotcha |
|
Component Events Don't Bubble
Impact: MEDIUM - Unlike native DOM events, Vue component events do NOT bubble up the component tree. When a child emits an event, only its direct parent can listen for it. Grandparent components and siblings never receive the event.
This is a common source of confusion for developers coming from vanilla JavaScript where events naturally bubble up the DOM.
Task Checklist
- Only expect events from direct child components
- Use provide/inject for deeply nested communication
- Use state management (Pinia) for complex cross-component communication
- Re-emit events at each level if manual bubbling is needed
- Consider whether your component hierarchy is too deep
The Problem
<!-- GrandParent.vue -->
<template>
<!-- This handler NEVER fires for grandchild events -->
<Parent @item-selected="handleSelect" />
</template>
<!-- Parent.vue -->
<template>
<Child />
</template>
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['item-selected'])
function selectItem(item) {
// This event reaches Parent, but NOT GrandParent
emit('item-selected', item)
}
</script>
Solution 1: Re-emit at Each Level (Simple Cases)
Manually forward events through each component.
Correct:
<!-- GrandParent.vue -->
<template>
<Parent @item-selected="handleSelect" />
</template>
<!-- Parent.vue -->
<script setup>
const emit = defineEmits(['item-selected'])
</script>
<template>
<!-- Re-emit to grandparent -->
<Child @item-selected="(item) => emit('item-selected', item)" />
</template>
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['item-selected'])
function selectItem(item) {
emit('item-selected', item)
}
</script>
Drawback: Becomes tedious with deeply nested components.
Solution 2: Provide/Inject (Ancestor Communication)
For deeply nested components, provide a callback from the ancestor.
Correct:
<!-- GrandParent.vue -->
<script setup>
import { provide } from 'vue'
function handleItemSelected(item) {
console.log('Item selected:', item)
}
// Provide the callback to all descendants
provide('onItemSelected', handleItemSelected)
</script>
<template>
<Parent />
</template>
<!-- Parent.vue - No changes needed -->
<template>
<Child />
</template>
<!-- Child.vue -->
<script setup>
import { inject } from 'vue'
// Inject the callback from any ancestor
const onItemSelected = inject('onItemSelected', () => {})
function selectItem(item) {
onItemSelected(item)
}
</script>
Advantages:
- Skips intermediate components
- No prop drilling or re-emitting
- Works at any nesting depth
Solution 3: State Management (Complex Applications)
For cross-component communication, especially between siblings or unrelated components, use Pinia.
Correct:
// stores/selection.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useSelectionStore = defineStore('selection', () => {
const selectedItem = ref(null)
function selectItem(item) {
selectedItem.value = item
}
return { selectedItem, selectItem }
})
<!-- DeepChild.vue - Updates state -->
<script setup>
import { useSelectionStore } from '@/stores/selection'
const store = useSelectionStore()
function handleSelect(item) {
store.selectItem(item)
}
</script>
<!-- SiblingComponent.vue - Reacts to state -->
<script setup>
import { useSelectionStore } from '@/stores/selection'
const store = useSelectionStore()
</script>
<template>
<div v-if="store.selectedItem">
Selected: {{ store.selectedItem.name }}
</div>
</template>
Solution 4: Event Bus (Use Sparingly)
For truly decoupled components, a simple event bus can work:
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
<!-- ComponentA.vue -->
<script setup>
import { emitter } from './eventBus'
function notify() {
emitter.emit('custom-event', { data: 'value' })
}
</script>
<!-- ComponentB.vue -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { emitter } from './eventBus'
function handleEvent(data) {
console.log('Received:', data)
}
onMounted(() => emitter.on('custom-event', handleEvent))
onUnmounted(() => emitter.off('custom-event', handleEvent))
</script>
Warning: Event buses make data flow hard to trace. Prefer provide/inject or state management.
Comparison Table
| Method | Best For | Complexity |
|---|---|---|
| Re-emit | 1-2 levels deep | Low |
| Provide/Inject | Deep nesting, ancestor communication | Medium |
| Pinia/State | Complex apps, sibling communication | Medium |
| Event Bus | Truly decoupled, rare cases | Low (but risky) |
Native Events DO Bubble
Note that native DOM events attached to elements still bubble normally:
<!-- GrandParent.vue -->
<template>
<!-- Native click bubbles up from button inside Child -->
<div @click="handleClick">
<Parent />
</div>
</template>
Only Vue component events (those emitted with emit()) don't bubble.