一、 为什么你的Vue组件总在“拆家”?
很多新手学Vue时最懵的时刻:明明跟着教程写了组件,结果数据像迷路的小孩在父子组件间乱窜。比如电影购票页面,用户选座时座位颜色要变,购票按钮要实时计算价格,这些功能如果全塞在一个.vue文件里,代码会臃肿得像国庆景区的厕所排队——根本动不了。
组件的本质是分而治之:
- 把页面拆成头部导航、电影海报轮播、座位网格、购票面板四个独立组件
- 组件间用Props/Events通信,就像快递员送包裹一样规范
- 状态管理用Vuex?不!小项目用EventBus更轻量(文末会教你避坑)
举个栗子:当用户点击座位时,座位组件需要告诉父组件“这个位置被选了”,父组件再通知购票面板更新价格。如果直接操作DOM,代码会变成意大利面条——扯不断理还乱。
二、 购票APP组件设计:把界面变成乐高手册
1. 项目结构(用文件树秒懂)
movie-ticket/
├─ src/
│ ├─ components/
│ │ ├─ Header.vue # 顶栏:返回键+标题
│ │ ├─ PosterSwiper.vue # 海报轮播
│ │ ├─ SeatMap.vue # 座位网格(核心难点!)
│ │ └─ TicketPanel.vue # 购票信息面板
│ └─ App.vue # 组件组装平台
2. 组件拆解心法
Header组件:简单到笑出声
<template>
<div class="header">
<button @click="$router.back()">←</button>
<h3>{{ title }}</h3>
</div>
</template>
<script>
export default {
props: ['title'] // 父组件传影城名称
}
</script>
SeatMap组件:内含二维数组魔法
座位数据用二维数组模拟影厅布局,0=空位 1=已售 2=选中:
seats: [
[0,0,1,0,2],
[1,0,0,0,0],
// ... 更多行
]
每个座位绑定动态class,点击时触发事件:
<template>
<div class="seat-map">
<div v-for="(row, i) in seats" :key="i" class="row">
<div
v-for="(seat, j) in row"
:key="j"
:class="['seat', {
sold: seat === 1,
selected: seat === 2
}]"
@click="selectSeat(i, j)"
>💺</div>
</div>
</div>
</template>
三、 组件通信:用“快递系统”代替“吼叫传话”
父子组件通信最怕变成“洋葱模型”——数据穿过多层组件就像剥洋葱,边写边流泪。
正确姿势:
- 父传子用Props(像寄明信片)
在App.vue中:
<SeatMap :seats="cinemaData" @seat-select="onSeatSelect"/>
- 子传父用Events(像收快递)
在SeatMap.vue中点击座位时:
methods: {
selectSeat(i, j) {
if (this.seats[i][j] === 1) return // 已售座位不可选
this.$emit('seat-select', { row: i, col: j }) // 发货!
}
}
- 兄弟组件用EventBus(像小区广播)
创建event-bus.js:
import Vue from 'vue'
export const EventBus = new Vue()
在TicketPanel中监听:
mounted() {
EventBus.$on('seat-selected', (seat) => {
this.selectedSeats.push(seat)
})
}
四、 状态管理的平替方案
小项目没必要上Vuex,教你用2行代码实现状态共享:
// state.js
export const store = {
selectedSeats: [],
ticketPrice: 42
}
在组件中直接引入,注意响应式问题可用Vue.set解决。这招就像用保鲜盒代替冰箱——轻量又够用!
五、 完整可跑示例(复制粘贴即用)
App.vue(组装所有组件)
<template>
<div id="app">
<Header title="IMAX影厅 - 《流浪地球3》"/>
<PosterSwiper :images="['poster1.jpg','poster2.jpg']"/>
<SeatMap :seats="seatsData" @seat-select="handleSeatSelect"/>
<TicketPanel :selected="selectedSeats" :price="42"/>
</div>
</template>
<script>
import Header from './components/Header.vue'
import SeatMap from './components/SeatMap.vue'
import TicketPanel from './components/TicketPanel.vue'
export default {
components: { Header, SeatMap, TicketPanel },
data() {
return {
seatsData: [[0,0,1], [1,0,0], [0,0,0]],
selectedSeats: []
}
},
methods: {
handleSeatSelect(seat) {
const {row, col} = seat
// 切换选中状态
this.seatsData[row][col] = this.seatsData[row][col] === 2 ? 0 : 2
this.selectedSeats = this.seatsData.flatMap((r, i) =>
r.map((s, j) => s === 2 ? {row: i, col: j} : null).filter(Boolean)
)
}
}
}
</script>
SeatMap.vue(核心交互组件)
<template>
<div class="seat-map">
<div v-for="(row, i) in seats" :key="i" class="row">
<div
v-for="(seat, j) in row"
:key="j"
:class="['seat',
seat === 1 ? 'sold' : '',
seat === 2 ? 'selected' : '']"
@click="selectSeat(i, j)">
{{ seat === 1 ? '❌' : '💺' }}
</div>
</div>
</div>
</template>
<script>
export default {
props: ['seats'],
methods: {
selectSeat(i, j) {
if (this.seats[i][j] === 1) {
alert('这个座位已售出啦!')
return
}
this.$emit('seat-select', { row: i, col: j })
}
}
}
</script>
<style scoped>
.seat-map { margin: 20px; }
.row { display: flex; justify-content: center; }
.seat {
width: 30px; height: 30px; margin: 5px;
cursor: pointer; border-radius: 5px;
}
.sold { background: #ccc; cursor: not-allowed; }
.selected { background: #4CAF50; }
</style>
(因篇幅限制,TicketPanel等组件代码已省略,完整源码可私信获取)
六、 踩坑预警:这三个雷区千万别踩!
- 动态修改Prop:直接改props会报警告,应该用events让父组件改
- 事件名大小写:HTML中事件名会自动转小写,建议用kebab-case命名
- SCSS作用域:style scoped有时无法覆盖子组件,可用/deep/穿透
七、 总结:组件化是Vue的灵魂乐高
通过电影购票案例,你会发现组件化开发就像拼乐高:
- 每个组件都是独立的积木块
- Props和Events是积木的凸起和凹槽
- 状态管理是拼装说明书
当你能把页面拆成可复用的组件时,就完成了从“切图仔”到“工程师”的蜕变。现在就去GitHub克隆源码,边啃爆米花边敲代码吧——毕竟,没有实战的理论,就像没有电影的影厅,空虚得很!
(注:本文代码适用于Vue 2.x,Vue 3版本需调整组合式API)

被折叠的 条评论
为什么被折叠?



