本文基于「灶台导航」小程序实际开发经验,详解组件化开发的核心要点,涵盖全局组件注册、page-header 封装、props 与事件传递等关键技术。
一、为什么要组件化?
在开发复杂小程序时,如果不进行组件化,会面临以下问题:
| 问题 | 表现 | 后果 |
|---|---|---|
| 代码重复 | 多个页面复制相同代码 | 维护困难、bug 多 |
| 样式不一致 | 各页面 UI 风格差异 | 用户体验割裂 |
| 逻辑分散 | 相同功能多处实现 | 改一处漏多处 |
| 协作困难 | 多人修改同一文件 | 冲突频繁 |
组件化的核心价值:一次封装,多处复用,统一维护。
二、组件基础
2.1 创建组件
小程序组件位于 components/ 目录:

2.2 组件基本结构
javascript
// components/page-header/page-header.js
Component({
// 组件属性
properties: {
title: {
type: String,
value: ''
},
showBack: {
type: Boolean,
value: true
}
},
// 组件数据
data: {
statusBarHeight: 0
},
// 组件生命周期
lifetimes: {
attached() {
const systemInfo = wx.getSystemInfoSync()
this.setData({
statusBarHeight: systemInfo.statusBarHeight
})
}
},
// 组件方法
methods: {
onBack() {
this.triggerEvent('back')
}
}
})
2.3 组件配置
json
// components/page-header/page-header.json
{
"component": true,
"usingComponents": {}
}
三、全局组件注册
3.1 在 app.json 中注册
json
// app.json
{
"pages": [
"pages/index/index",
"pages/cook/cook"
],
"usingComponents": {
"page-header": "/components/page-header/page-header",
"loading": "/components/loading/loading",
"empty-state": "/components/empty-state/empty-state",
"modal": "/components/modal/modal"
}
}
优势:
- 所有页面可直接使用,无需重复声明
- 统一管理,修改方便
- 减少页面 JSON 配置量
3.2 页面级组件注册
如果组件只在特定页面使用,可在页面 JSON 中注册:
json
// pages/cook/cook.json
{
"usingComponents": {
"timer": "/components/timer/timer"
}
}
四、Props 属性传递
4.1 属性定义
javascript
Component({
properties: {
// 简写
title: String,
// 完整写法
showBack: {
type: Boolean,
value: true, // 默认值
observer: function(newVal, oldVal) {
// 属性变化时的回调
console.log('showBack changed:', newVal)
}
},
// 复杂类型
userInfo: {
type: Object,
value: {}
},
// 数组类型
menuList: {
type: Array,
value: []
}
}
})
4.2 页面中传递属性
xml
<!-- 页面中使用组件 -->
<page-header
title="烹饪中"
showBack="{{true}}"
user-info="{{userInfo}}"
bind:back="onBack"
/>
注意事项:
- 字符串可以直接传递:
title="标题" - 布尔值、数字、对象、数组需要使用双花括号:
showBack="{``{true}}" - 属性名建议使用 kebab-case:
user-info而非userInfo
五、事件传递
5.1 子组件触发事件
javascript
// components/page-header/page-header.js
Component({
methods: {
onBack() {
// 触发事件,可携带数据
this.triggerEvent('back', {
from: 'header',
timestamp: Date.now()
})
},
onRightTap() {
// 触发带冒号的事件名
this.triggerEvent('righttap', {
action: 'save'
})
}
}
})
5.2 父页面监听事件
xml
<!-- pages/cook/cook.wxml -->
<page-header
title="烹饪中"
bind:back="onBack"
bind:righttap="onRightAction"
/>
javascript
// pages/cook/cook.js
Page({
onBack(e) {
// 获取事件携带的数据
console.log(e.detail.from) // 'header'
console.log(e.detail.timestamp)
// 业务逻辑
wx.navigateBack()
},
onRightAction(e) {
if (e.detail.action === 'save') {
this.saveProgress()
}
}
})
5.3 事件命名规范
| 事件名 | 使用场景 |
|---|---|
bind:back |
返回按钮点击 |
bind:confirm |
确认操作 |
bind:cancel |
取消操作 |
bind:change |
值变化 |
bind:tap |
点击 |
六、插槽(Slot)使用
6.1 单插槽
javascript
// 组件定义
Component({
options: {
multipleSlots: false // 默认单插槽
}
})
xml
<!-- 组件模板 -->
<view class="card">
<view class="header">{{title}}</view>
<slot></slot> <!-- 插槽位置 -->
</view>
xml
<!-- 页面使用 -->
<card title="标题">
<view>这里是插入的内容</view>
</card>
6.2 多插槽
javascript
// components/modal/modal.js
Component({
options: {
multipleSlots: true
},
properties: {
title: String
}
})
xml
<!-- components/modal/modal.wxml -->
<view class="modal">
<view class="modal-header">
<slot name="header"></slot>
</view>
<view class="modal-body">
<slot name="body"></slot>
</view>
<view class="modal-footer">
<slot name="footer"></slot>
</view>
</view>
xml
<!-- 页面使用 -->
<modal>
<view slot="header">自定义标题</view>
<view slot="body">内容区域</view>
<view slot="footer">
<button>确定</button>
</view>
</modal>
七、组件通信进阶
7.1 父组件调用子组件方法
javascript
// 页面中
Page({
onReady() {
// 获取组件实例
this.modal = this.selectComponent('#modal')
},
showModal() {
// 调用组件方法
this.modal.show()
},
hideModal() {
this.modal.hide()
}
})
javascript
// 组件中
Component({
methods: {
show() {
this.setData({ visible: true })
},
hide() {
this.setData({ visible: false })
}
}
})
7.2 兄弟组件通信
通过父组件中转:
javascript
// 页面
Page({
data: {
sharedData: null
},
onChildAChange(e) {
this.setData({ sharedData: e.detail.value })
}
})
xml
<!-- 页面模板 -->
<child-a bind:change="onChildAChange" />
<child-b data="{{sharedData}}" />
7.3 全局事件总线
javascript
// utils/eventBus.js
class EventBus {
constructor() {
this.events = {}
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(cb => cb(data))
}
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
}
}
module.exports = new EventBus()
八、组件最佳实践
8.1 命名规范
| 类型 | 命名规范 | 示例 |
|---|---|---|
| 组件目录 | kebab-case | page-header/ |
| 组件文件 | kebab-case | page-header.js |
| 属性名 | kebab-case | show-back |
| 事件名 | 小写 | bind:back |
| 方法名 | camelCase | onBackTap() |
8.2 组件职责单一
javascript
// ❌ 不推荐:一个组件做太多事
Component({
properties: {
type: String, // 'header' | 'footer' | 'sidebar'
// ...
}
})
// ✅ 推荐:职责分离
// page-header 组件只负责头部
// page-footer 组件只负责底部
9.3 避免 setData 过度使用
javascript
// ❌ 不推荐
this.setData({ a: 1 })
this.setData({ b: 2 })
this.setData({ c: 3 })
// ✅ 推荐
this.setData({
a: 1,
b: 2,
c: 3
})
十、总结
组件化开发核心要点:
| 要点 | 说明 |
|---|---|
| 全局注册 | app.json 中声明,所有页面可用 |
| Props 传递 | 属性定义、类型校验、默认值 |
| 事件通信 | triggerEvent 触发,bind 监听 |
| 插槽使用 | 单插槽、具名插槽扩展组件 |
| 职责单一 | 一个组件只做一件事 |
通过组件化开发,可以显著提升代码复用性和可维护性。
作者:「倒灶了队」
项目:灶台导航 - 微信小程序
更新时间:2026-03-29