本文将手把手教你开发一个可复用的微信小程序自定义组件,涵盖组件封装、属性定义、事件通信、插槽使用,以及发布到npm供团队复用的完整流程。
一、为什么要封装自定义组件?
在实际项目开发中,我们经常会遇到重复的UI场景:
- 列表页的加载状态(骨架屏、加载中、加载失败、空数据)
- 表单中的输入框带清除按钮
- 商品卡片、用户头像等业务组件
如果每次都复制粘贴代码,不仅维护成本高,还容易产生不一致的bug。封装成自定义组件后:
- 一次开发,多处复用
- 统一修改,自动生效
- 降低页面代码复杂度
二、实战案例:封装一个带状态反馈的列表容器
假设我们需要一个智能列表容器组件,自动处理加载状态、空数据提示、错误重试。
2.1 创建组件目录结构
components/
└── smart-list/
├── smart-list.js
├── smart-list.json
├── smart-list.wxml
└── smart-list.wxss
2.2 组件配置文件
// smart-list.json
{
"component": true,
"usingComponents": {}
}
2.3 组件逻辑层
// smart-list.js
Component({
/**
* 组件的属性列表(对外暴露的API)
*/
properties: {
// 加载状态:loading | success | error | empty
status: {
type: String,
value: 'loading'
},
// 空数据时的提示文案
emptyText: {
type: String,
value: '暂无数据'
},
// 错误时的提示文案
errorText: {
type: String,
value: '加载失败,点击重试'
},
// 是否显示骨架屏
showSkeleton: {
type: Boolean,
value: true
}
},
/**
* 组件的初始数据
*/
data: {
skeletonCount: 3 // 骨架屏条数
},
/**
* 组件的方法列表
*/
methods: {
// 点击错误状态的重试按钮
handleRetry() {
this.triggerEvent('retry', {})
},
// 点击空数据的操作按钮
handleEmptyAction() {
this.triggerEvent('emptyaction', {})
}
}
})
2.4 组件视图层
<!-- smart-list.wxml -->
<view class="smart-list">
<!-- 骨架屏状态 -->
<block wx:if="{{status === 'loading' && showSkeleton}}">
<view class="skeleton-item" wx:for="{{skeletonCount}}" wx:key="index">
<view class="skeleton-avatar"></view>
<view class="skeleton-content">
<view class="skeleton-title"></view>
<view class="skeleton-text"></view>
</view>
</view>
</block>
<!-- 加载中状态(无骨架屏) -->
<view wx:elif="{{status === 'loading' && !showSkeleton}}" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 成功状态:显示插槽内容 -->
<block wx:elif="{{status === 'success'}}">
<slot></slot>
</block>
<!-- 空数据状态 -->
<view wx:elif="{{status === 'empty'}}" class="empty-container">
<view class="empty-icon">📭</view>
<text class="empty-text">{{emptyText}}</text>
<view class="empty-action" bindtap="handleEmptyAction">
<slot name="empty-action"></slot>
</view>
</view>
<!-- 错误状态 -->
<view wx:elif="{{status === 'error'}}" class="error-container">
<view class="error-icon">❌</view>
<text class="error-text">{{errorText}}</text>
<button class="retry-btn" bindtap="handleRetry">重新加载</button>
</view>
</view>
2.5 组件样式
/* smart-list.wxss */
.smart-list {
min-height: 300rpx;
}
/* 骨架屏样式 */
.skeleton-item {
display: flex;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.skeleton-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
.skeleton-content {
flex: 1;
margin-left: 20rpx;
}
.skeleton-title,
.skeleton-text {
height: 30rpx;
margin-bottom: 20rpx;
border-radius: 6rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
.skeleton-text {
width: 60%;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* 加载中样式 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #e0e0e0;
border-top-color: #07c160;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 空数据样式 */
.empty-container,
.error-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
}
.empty-icon,
.error-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}
.empty-text,
.error-text {
color: #999;
font-size: 28rpx;
margin-bottom: 30rpx;
}
.retry-btn {
background: #07c160;
color: #fff;
font-size: 28rpx;
padding: 16rpx 40rpx;
border-radius: 8rpx;
}
三、在页面中使用组件
3.1 页面配置
// pages/order-list/order-list.json
{
"usingComponents": {
"smart-list": "/components/smart-list/smart-list"
}
}
3.2 页面使用示例
<!-- pages/order-list/order-list.wxml -->
<smart-list
status="{{listStatus}}"
emptyText="您还没有订单"
bind:retry="loadData"
bind:emptyaction="goShopping"
>
<!-- 列表内容插槽 -->
<view class="order-item" wx:for="{{orderList}}" wx:key="id">
<text>订单号:{{item.orderNo}}</text>
<text>金额:¥{{item.amount}}</text>
</view>
<!-- 空数据操作插槽 -->
<view slot="empty-action">去逛逛</view>
</smart-list>
3.3 页面逻辑
// pages/order-list/order-list.js
Page({
data: {
listStatus: 'loading',
orderList: []
},
onLoad() {
this.loadData()
},
async loadData() {
this.setData({ listStatus: 'loading' })
try {
const res = await wx.request({
url: 'https://api.example.com/orders',
method: 'GET'
})
if (res.data.length === 0) {
this.setData({ listStatus: 'empty' })
} else {
this.setData({
listStatus: 'success',
orderList: res.data
})
}
} catch (err) {
this.setData({ listStatus: 'error' })
}
},
goShopping() {
wx.switchTab({ url: '/pages/index/index' })
}
})
四、进阶技巧:组件通信
4.1 父组件调用子组件方法
// 页面中获取组件实例
const smartList = this.selectComponent('#smartList')
smartList.setData({ status: 'loading' })
4.2 子组件向父组件传递数据
// 组件中触发事件,携带数据
this.triggerEvent('itemclick', {
item: this.data.currentItem,
index: this.data.currentIndex
})
五、发布到npm实现团队复用
5.1 准备npm包结构
miniprogram_dist/
├── smart-list.js
├── smart-list.json
├── smart-list.wxml
└── smart-list.wxss
5.2 package.json配置
{
"name": "@your-team/miniprogram-smart-list",
"version": "1.0.0",
"description": "微信小程序智能列表容器组件",
"main": "miniprogram_dist/smart-list.js",
"miniprogram": "miniprogram_dist",
"keywords": ["miniprogram", "wechat", "component"],
"author": "广西优睿科技",
"license": "MIT"
}
5.3 发布命令
npm login
npm publish --access public
5.4 项目中安装使用
npm install @your-team/miniprogram-smart-list
然后在 project.config.json 中配置:
{
"packNpmManually": true,
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./miniprogram_npm"
}
]
}
六、最佳实践总结
| 实践项 | 说明 |
|---|---|
| 属性命名 | 使用小驼峰,提供默认值和类型校验 |
| 事件命名 | 使用小写连字符,如 item-click |
| 样式隔离 | 默认 isolated 模式,避免样式污染 |
| 插槽设计 | 预留具名插槽扩展性 |
| 文档完善 | 提供 README 和使用示例 |
本文作者:优睿科技