封装自定义组件时,一个非常现实的问题是:组件的内部结构往往不能完全写死。比如你写了一个卡片组件,标题区域、内容区域、底部按钮区域都希望由使用方决定放什么。微信小程序提供了slot插槽机制来解决这个问题,它允许组件的wxml中预留“占位符”,由引用组件的页面把实际内容塞进去。slot分为默认插槽、具名插槽和作用域插槽三种,三者能力层层递进,适用的场景也各不相同。本文结合完整代码逐一拆解它们的用法与坑点。

默认插槽:最简单的内容分发
默认插槽是slot最基础的形态。组件的wxml中直接写一个<slot/>标签,这个位置就会被页面传入的子节点替换。它适合组件只需要一个“内容出口”的场景,例如一个统一风格的容器壳子,内部内容完全交给外部。
先看组件的定义。假设组件名为card,它的wxml如下:
<view class="card">
<view class="card-title">{{title}}</view>
<!-- 默认插槽:外部传入的内容会渲染到这里 -->
<slot></slot>
</view>组件的js部分使用Component构造器即可,不需要任何特殊配置:
Component({
properties: {
title: {
type: String,
value: ''
}
}
})页面中使用时,直接在组件标签内部写内容:
<card title="订单详情"> <view>这里是页面塞进来的内容</view> <text>可以是任意wxml节点</text> </card>
有一点需要特别注意:默认插槽只允许存在一个。如果组件里写了两个不带name属性的slot,控制台会报错。另外,如果组件的wxml中声明了slot但页面没有传内容,slot位置就什么都不渲染,也可以在slot标签内部写默认内容作为兜底,当外部没有传入子节点时会显示默认内容。
具名插槽:多区域布局的标配
当一个组件需要多个内容出口时,就必须使用具名插槽。比如一个列表项组件,左侧放头像、中间放正文、右侧放操作按钮,三个区域的内容都希望由页面决定。具名插槽通过给slot加name属性来区分不同出口,页面则用slot属性指定内容投放到哪个出口。
使用具名插槽前,必须开启multipleSlots选项,这是一个非常常见的坑:忘了配置的话,所有具名插槽都不会生效。组件js写法:
Component({
options: {
// 启用多slot支持,具名插槽必须开启
multipleSlots: true
},
properties: {}
})组件wxml中定义三个命名出口:
<view class="list-item">
<view class="left">
<slot name="avatar"></slot>
</view>
<view class="middle">
<slot name="content"></slot>
</view>
<view class="right">
<slot name="action"></slot>
</view>
</view>页面中使用时,节点上的slot属性值要和组件内的name对应起来:
<list-item>
<view slot="avatar">
<image src="/images/avatar.png" class="avatar"/>
</view>
<view slot="content">
<view class="title">小程序插槽详解</view>
<view class="desc">具名插槽支持多区域内容分发</view>
</view>
<view slot="action">
<button size="mini">删除</button>
</view>
</list-item>具名插槽和默认插槽可以混用:组件内可以同时有一个不带name的slot和若干带name的slot,页面中没写slot属性的子节点会进入默认插槽。这种组合在实际项目中非常实用,比如一个弹窗组件,标题和底部按钮用具名插槽,中间主体内容用默认插槽。
作用域插槽:让页面拿到组件内部数据
前两种插槽有一个共同局限:插槽内容是页面写的,写的时候无法访问组件内部的data。但实际开发中经常遇到这样的需求——组件内部持有一组列表数据,但每一项怎么渲染希望由页面决定。传统做法是把数据通过properties传出去,再在页面里循环渲染,数据流绕了一圈。作用域插槽正是为这个场景设计的:组件把内部数据“挂”在slot上,页面在插槽内容中接收后再使用。
先看组件的写法。在基础库2.29.0之后,官方推荐使用纯数据字段配合自定义写法,但通用写法是在slot上绑定数据:
<!-- 组件 wxml -->
<view class="list">
<block wx:for="{{items}}" wx:key="id">
<slot item="{{item}}" index="{{index}}"></slot>
</block>
</view>
// 组件 js
Component({
data: {
items: [
{ id: 1, name: '默认插槽', level: '基础' },
{ id: 2, name: '具名插槽', level: '进阶' },
{ id: 3, name: '作用域插槽', level: '高级' }
]
}
})页面侧通过bind:插槽名的语法接收数据(对具名作用域插槽)或使用bind:item这类属性获取默认插槽传递的数据。以具名作用域插槽为例:
<!-- 组件wxml,具名作用域插槽 -->
<slot name="item" item="{{item}}" index="{{index}}"></slot>
<!-- 页面wxml -->
<data-list>
<view slot="item" bind:item="onSlotItem" wx:for="{{slotItems}}" wx:key="id">
<text>{{item.name}} - {{item.level}}</text>
</view>
</data-list>需要说明的是,小程序早期版本对作用域插槽的支持并不完善,在基础库较低时通常借助抽象节点或events方式模拟。如果项目基础库版本在2.29.0以上,可以直接使用官方提供的标准作用域插槽语法,组件在slot上声明的属性会以事件形式传给页面,页面在绑定的回调里拿到数据后自行setData渲染。这种模式把“数据归组件、渲染归页面”的职责划分得非常清晰。
三种插槽的选择策略与常见坑
三种插槽怎么选,可以按一个简单原则判断:只要一个出口就用默认插槽;多个出口且内容固定由页面提供,用具名插槽;插槽内容依赖组件内部数据的,用作用域插槽。下面把关键差异整理成表格方便对比:
| 类型 | 配置要求 | 数据来源 | 典型场景 |
|---|---|---|---|
| 默认插槽 | 无需配置 | 页面侧数据 | 容器壳、卡片主体 |
| 具名插槽 | multipleSlots: true | 页面侧数据 | 列表项多区域、弹窗 |
| 作用域插槽 | 基础库版本要求 | 组件内部数据 | 自定义渲染的列表、表格 |
最后列几个高频踩坑点。第一,样式隔离问题:页面写进插槽的内容属于页面作用域,它不能直接使用组件内wxss定义的class,组件的样式也无法作用到插槽内容上,需要公共样式请抽取到app.wxss或使用styleIsolation配置。第二,事件绑定:插槽内容上绑定的事件处理器必须写在页面js里,写在组件js里不会生效。第三,生命周期:插槽内容节点的生命周期跟随页面而非组件,不要在组件里通过selectComponent去操作插槽节点,取不到预期结果。第四,开启multipleSlots后,页面中所有带slot属性的节点才会被正确分发,写错name值不会报错但内容会静默丢失,排查时优先检查name拼写是否一致。
掌握这三种插槽后,你封装的组件就能从“功能固定的小部件”进化为“结构开放的基础设施”,配合properties和事件系统,足以支撑绝大多数复杂业务组件的设计。建议在真实项目中先从具名插槽练手,比如自己封装一个通用的底部弹层组件,把头部标题、主体内容和底部操作栏全部做成插槽,你会对内容分发的理解更上一层楼。