小程序的页面由组件构成。官方提供的基础组件能满足大部分展示需求,但当业务开始出现重复的卡片、弹窗、表单块时,把这些结构抽成自定义组件,是让代码保持可控的关键一步。这篇文章从实际项目出发,讲清楚自定义组件的定义、通信、样式隔离与复用方式。

什么时候该抽组件
不是所有结构都值得封装。我的判断标准有三条:
- 同一段结构在三个以上页面出现,且未来可能继续增加;
- 结构内部有独立的状态逻辑,比如展开收起、倒计时、分页加载;
- 需要被不同业务方复用,接口应当稳定。
只出现一两次的布局,直接写在页面里更省事。过早抽象会增加理解成本,尤其是给后来接手的人。
定义一个自定义组件
以常见的商品卡片为例。在项目里新建 components/goods-card 目录,包含四个文件:js、json、wxml、wxss。json 中必须声明 component: true,否则它会被当成普通页面处理。
{
"component": true,
"usingComponents": {}
}
组件逻辑写在 Component() 中,而不是 Page()。属性、数据、方法分开声明,阅读时一目了然:
Component({
properties: {
goods: {
type: Object,
value: {}
},
showPrice: {
type: Boolean,
value: true
}
},
data: {
pressed: false
},
methods: {
onTapCard() {
this.triggerEvent('select', { id: this.data.goods.id })
},
onTouchStart() {
this.setData({ pressed: true })
},
onTouchEnd() {
this.setData({ pressed: false })
}
}
})
wxml 中使用属性绑定,注意属性名在模板里要写成短横线形式:
<view class="card {{pressed ? 'card--pressed' : ''}}"
bindtap="onTapCard"
bindtouchstart="onTouchStart"
bindtouchend="onTouchEnd">
<image class="card__cover" src="{{goods.cover}}" mode="aspectFill" />
<view class="card__body">
<text class="card__title">{{goods.title}}</text>
<text wx:if="{{showPrice}}" class="card__price">¥{{goods.price}}</text>
</view>
</view>
父子通信的三种方式
属性向下传递
父组件通过 properties 传数据,子组件只读。这里有一个容易忽略的点:对象类型的属性如果直接修改内部字段,不会触发视图更新,也不符合单向数据流。需要变更时,应该通过事件通知父级,由父级 setData 后再传下来。
事件向上通知
triggerEvent 是子传父的标准做法。事件名建议用动词,如 select、change、submit,避免和基础事件混淆。父组件监听时写 bind:select 或 bindselect,前者可读性更好。
通过 selectComponent 主动调用
父组件可以用 this.selectComponent('#card') 拿到子组件实例,调用其方法。这种方式适合命令式操作,比如让弹窗组件执行 open()。但不要滥用,否则组件之间会形成隐式依赖,测试和重构都会变难。
样式隔离与外部类
自定义组件默认开启样式隔离,组件内的 class 不会影响外部,外部样式也进不来。这既是保护也是限制。如果需要让父级控制部分样式,在组件 json 中声明:
{
"component": true,
"styleIsolation": "apply-shared"
}
或者更精细地使用 externalClasses,把指定类名暴露出去:
Component({
externalClasses: ['custom-class']
})
模板里写 class="card custom-class",父组件传入 custom-class="my-card" 即可覆盖部分外观。我倾向于只暴露一两个类名,而不是完全放开样式隔离,这样组件的视觉边界仍然清晰。
把组件放进 npm 或私有库
当组件数量超过十个,复制粘贴就开始出问题。做法有两种:一是把通用组件抽成独立 npm 包,通过 npm install 引入,配合开发者工具的“构建 npm”;二是放在项目内的 components 目录,用统一的 usingComponents 路径管理。
个人项目我更推荐第二种,简单直接。多项目复用时再考虑发包,注意在 package.json 中声明 miniprogram 字段指向组件目录,否则构建后路径会出错。
性能上的几个细节
- 属性传大对象时,尽量在父级做裁剪,只传组件真正需要的字段,减少 setData 的数据量;
- 长列表中的组件开启
virtualHost,可以去掉组件外层多余的节点,减少渲染层级; - 频繁更新的数据(如倒计时)放在组件内部维护,避免父级整体刷新;
- 组件销毁时清理定时器和事件监听,在
lifetimes.detached中处理。
Component({
lifetimes: {
attached() {
this.timer = setInterval(() => {
this.setData({ now: Date.now() })
}, 1000)
},
detached() {
clearInterval(this.timer)
}
}
})
组件开发的核心不是写得多,而是边界划得准。属性只暴露必要的数据,事件只传递必要的信息,样式只开放必要的入口。做到这三点,组件才能在不同页面之间稳定复用,而不是每接一个页面就要改一次源码。

评论 (0)
还没有评论,快来抢沙发吧~