模式切换
自定义组件
为什么需要自定义组件?
复用
- 问题:在多个页面中需要重复使用相同的 UI 结构和逻辑
- 示例:商品卡片、用户头像、底部按钮栏、加载状态等
- 好处:一次开发,多处使用,提高开发效率
html
<!-- 没有组件时,每个页面都要重复写 -->
<!-- page1.wxml -->
<view class="product-card">
<image src="{{product1.image}}"></image>
<text>{{product1.name}}</text>
<text>¥{{product1.price}}</text>
<button>购买</button>
</view>
<!-- page2.wxml -->
<view class="product-card">
<image src="{{product2.image}}"></image>
<text>{{product2.name}}</text>
<text>¥{{product2.price}}</text>
<button>购买</button>
</view>
<!-- 使用组件后 -->
<!-- 所有页面都可以这样使用 -->
<product-card
product="{{product}}"
bind:buy="onBuy"
/>
解耦
- 问题:复杂页面的逻辑和模板混杂在一起,难以维护
- 好处:将页面拆分为独立的、职责单一的组件,便于开发和测试
html
<!-- 没有组件:一个庞大的页面 -->
<view class="user-profile-page">
<!-- 用户信息区域 -->
<view class="user-header">...</view>
<!-- 订单列表 -->
<view class="order-list">...</view>
<!-- 商品推荐 -->
<view class="product-recommend">...</view>
<!-- 底部导航 -->
<view class="tab-bar">...</view>
</view>
<!-- 使用组件:结构清晰 -->
<view class="user-profile-page">
<user-header user="{{userInfo}}"></user-header>
<order-list orders="{{orders}}"></order-list>
<product-recommend></product-recommend>
<tab-bar current="profile"></tab-bar>
</view>
创建和使用自定义组件
创建自定义组件
步骤1:创建组件目录和文件
components/
└── product-card/
├── product-card.js # 组件逻辑
├── product-card.wxml # 组件模板
├── product-card.wxss # 组件样式
└── product-card.json # 组件配置
步骤2:声明组件
json
// components/product-card/product-card.json
{
"component": true,
"usingComponents": {}
}
步骤3:编写组件模板和样式
html
<!-- components/product-card/product-card.wxml -->
<view class="product-card">
<image class="product-image" src="{{image}}"></image>
<view class="product-info">
<text class="product-name">{{name}}</text>
<text class="product-price">¥{{price}}</text>
</view>
<button class="buy-btn" bindtap="onBuyTap">购买</button>
</view>
css
/* components/product-card/product-card.wxss */
.product-card {
border: 1rpx solid #eee;
border-radius: 10rpx;
padding: 20rpx;
margin: 20rpx;
}
.product-image {
width: 100%;
height: 300rpx;
}
.product-name {
font-size: 32rpx;
color: #333;
}
.product-price {
font-size: 36rpx;
color: #e4393c;
}
.buy-btn {
background-color: #07c160;
color: white;
}
步骤4:编写组件逻辑
javascript
// components/product-card/product-card.js
Component({
// 组件的属性列表
properties: {
image: {
type: String,
value: ''
},
name: {
type: String,
value: ''
},
price: {
type: Number,
value: 0
},
productId: {
type: String,
value: ''
}
},
// 组件的初始数据
data: {
// 组件内部数据
isFavorite: false
},
// 组件的方法列表
methods: {
onBuyTap: function() {
// 触发自定义事件,向父组件传递数据
this.triggerEvent('buy', {
productId: this.properties.productId,
price: this.properties.price
})
},
onFavoriteTap: function() {
this.setData({
isFavorite: !this.data.isFavorite
})
}
}
})
使用自定义组件
步骤1:在页面中引用组件
json
// pages/index/index.json
{
"usingComponents": {
"product-card": "/components/product-card/product-card"
}
}
步骤2:在页面模板中使用组件
html
<!-- pages/index/index.wxml -->
<view>
<text>商品列表</text>
<product-card
image="{{products[0].image}}"
name="{{products[0].name}}"
price="{{products[0].price}}"
product-id="{{products[0].id}}"
bind:buy="onProductBuy"
/>
<product-card
image="{{products[1].image}}"
name="{{products[1].name}}"
price="{{products[1].price}}"
product-id="{{products[1].id}}"
bind:buy="onProductBuy"
/>
</view>
步骤3:在页面中处理组件事件
javascript
// pages/index/index.js
Page({
data: {
products: [
{
id: '1',
name: 'iPhone 14',
price: 5999,
image: '/images/iphone14.jpg'
},
{
id: '2',
name: 'MacBook Pro',
price: 12999,
image: '/images/macbook.jpg'
}
]
},
onProductBuy: function(event) {
const { productId, price } = event.detail;
console.log('购买商品:', productId, '价格:', price);
// 处理购买逻辑
wx.navigateTo({
url: `/pages/order/order?productId=${productId}`
});
}
})
组件间的通信
properties - 父组件向子组件传递数据
javascript
// 子组件:components/my-component/my-component.js
Component({
properties: {
// 简化的定义方式
title: String,
// 完整的定义方式
count: {
type: Number, // 类型
value: 0, // 默认值
observer: function(newVal, oldVal) {
// 属性值变化时的监听函数
console.log('count changed:', newVal, oldVal);
}
},
// 复杂的对象类型
userInfo: {
type: Object,
value: {}
}
}
})
html
<!-- 父组件使用 -->
<my-component
title="Hello World"
count="{{currentCount}}"
user-info="{{userData}}"
/>
triggerEvent - 子组件向父组件传递数据
javascript
// 子组件触发事件
// components/my-button/my-button.js
Component({
methods: {
onTap: function() {
const myEventDetail = { // detail 对象,提供给事件监听函数
message: '按钮被点击了',
timestamp: new Date().getTime()
};
const myEventOption = { // 触发事件的选项
bubbles: true, // 事件是否冒泡
composed: true, // 事件是否可以穿越组件边界
capturePhase: false // 事件是否拥有捕获阶段
};
this.triggerEvent('myevent', myEventDetail, myEventOption);
},
onCustomTap: function() {
this.triggerEvent('customevent', {
value: '自定义数据'
});
}
}
})
html
<!-- 父组件监听事件 -->
<my-button
bind:myevent="onMyEvent"
bind:customevent="onCustomEvent"
/>
<!-- 也可以使用 bindmyevent 的写法 -->
<my-button
bindmyevent="onMyEvent"
bindcustomevent="onCustomEvent"
/>
javascript
// 父组件处理事件
// pages/index/index.js
Page({
onMyEvent: function(event) {
console.log('收到子组件事件:', event.detail.message);
console.log('时间戳:', event.detail.timestamp);
},
onCustomEvent: function(event) {
console.log('自定义事件数据:', event.detail.value);
}
})
组件生命周期
javascript
// components/my-component/my-component.js
Component({
lifetimes: {
// 组件实例刚刚被创建时执行
created: function() {
console.log('组件 created - 还不能调用 setData');
},
// 组件实例进入页面节点树时执行
attached: function() {
console.log('组件 attached - 可以调用 setData');
// 适合进行初始化工作
this.setData({ isLoading: true });
this.fetchData();
},
// 组件在视图层布局完成后执行
ready: function() {
console.log('组件 ready - 视图层布局完成');
// 适合操作节点、开始动画
},
// 组件实例被从页面节点树移除时执行
detached: function() {
console.log('组件 detached - 被移除');
// 适合清理工作,清除定时器等
if (this.timer) {
clearInterval(this.timer);
}
},
// 组件发生错误时执行
error: function(err) {
console.error('组件 error:', err);
}
},
// 组件所在页面的生命周期
pageLifetimes: {
// 页面显示时触发
show: function() {
console.log('页面显示了 - 组件可以执行相应操作');
},
// 页面隐藏时触发
hide: function() {
console.log('页面隐藏了 - 组件可以暂停操作');
},
// 页面尺寸变化时触发
resize: function(size) {
console.log('页面尺寸变化:', size);
}
},
methods: {
fetchData: function() {
// 模拟数据加载
setTimeout(() => {
this.setData({
isLoading: false,
data: '加载完成的数据'
});
}, 1000);
}
}
})
插槽 slot 的使用
插槽用于让组件可以接收外部传入的任意内容,增加组件的灵活性。
单个插槽
html
<!-- 组件模板:components/container/container.wxml -->
<view class="container">
<view class="header">
<text>{{title}}</text>
</view>
<view class="content">
<!-- 插槽,用于放置外部传入的内容 -->
<slot></slot>
</view>
<view class="footer">
<slot name="footer"></slot>
</view>
</view>
html
<!-- 使用组件 -->
<container title="我的容器">
<!-- 默认插槽的内容 -->
<view>这是主体内容</view>
<text>可以是任意内容</text>
<!-- 具名插槽的内容 -->
<view slot="footer">
<button>底部按钮</button>
</view>
</container>
多个插槽(需要在配置中启用)
javascript
// components/container/container.js
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
properties: {
title: String
}
})
html
<!-- 组件模板支持多个具名插槽 -->
<view class="card">
<view class="card-header">
<slot name="header"></slot>
</view>
<view class="card-body">
<slot name="body"></slot>
</view>
<view class="card-footer">
<slot name="footer"></slot>
</view>
</view>
html
<!-- 使用多插槽组件 -->
<card>
<view slot="header">
<text>自定义头部</text>
<image src="/images/icon.png"></image>
</view>
<view slot="body">
<text>自定义主体内容</text>
</view>
<view slot="footer">
<button>自定义底部按钮</button>
</view>
</card>
插槽作用域
html
<!-- 组件内部数据不会暴露给插槽内容 -->
<view class="component">
<text>组件内部数据: {{internalData}}</text>
<view class="slot-area">
<!-- 插槽内容只能访问使用组件页面的数据,不能访问组件内部数据 -->
<slot></slot>
</view>
</view>