微信小程序开发全攻略·文章版|07 自定义组件 微信小程序开发全攻略·文章版07 自定义组件微信小程序自定义组件从 Component 构造器到封装可复用 Dialog一篇讲透父子通信目录1. 什么是自定义组件为什么要自己造组件2. Component 构造器的四个成员3. properties 对外属性与 observer 监听4. 组件生命周期与页面生命周期5. 插槽 slot让使用者往里填内容6. 实战封装一个可复用的 Dialog 弹窗7. 父子组件通信triggerEvent 派发事件8. 本模块视频清单9. 常用 API 速查表10. 学习打卡清单11. 知识图谱知识图谱本模块 Component 构造器 → properties/data/methods → 插槽内容分发 → triggerEvent 父子通信 → Dialog 实战 的知识地图可静态呈现如下模块七 · 自定义组件 │ ├─ ① 注册与使用 │ ├─ 组件四件套xxx.wxml / xxx.wxss / xxx.js(Component) / xxx.json │ └─ 页面 jsonusingComponents { my-dialog: /components/dialog/dialog } │ ├─ ② Component() 四大成员 │ ├─ properties 对外接收type / value / observer │ ├─ data 内部状态this.setData 更新 │ ├─ methods 组件内所有函数都放这里 │ └─ lifetimes attached(初始化) / ready(布局完) / detached(清理) │ pageLifetimes 所在页面 show / hide / resize │ ├─ ③ 插槽 slot内容分发 │ ├─ 默认插槽slot/slot ← 使用方匿名内容 │ ├─ 具名插槽slot namefooter ← 使用方 slotfooter │ └─ 多插槽开关options: { multipleSlots: true } │ └─ ④ 父子通信单向数据流 父 ──properties(visible/title)──▶ 子 子 ──triggerEvent(confirm, detail)──▶ 父bind:confirm 监听e.detail 取Dialog 实战中各部分的职责文件在组件中的角色dialog.wxmlmask 遮罩 title 默认插槽正文 具名插槽按钮区dialog.wxss全屏半透明遮罩、居中白色卡片、圆角与间距dialog.jsproperties 收 visible/titlemethods 里 triggerEvent 派 cancel/confirm页面usingComponents 注册、isShow 控制显隐、bind:cancel/confirm 接收事件 交互式知识图谱在线版模块七 · 自定义组件摘要这是《2026 最全微信小程序开发全攻略》的第七篇笔记对应课程第 70-74 集约 51 分钟。学到这里你已经会用官方组件画界面、串页面、联网登录了本篇带你再上一层楼 —— 从 会用组件 进阶到 自己造组件。文章讲清楚用Component()构造器如何封装一段可复用 UI如何通过usingComponents在页面注册properties /data/methods / 组件生命周期 / 页面生命周期各自干什么插槽 slot 怎么让使用方向组件内部填自定义内容以及最关键的父子通信 —— 父组件用 properties 传值、子组件用 triggerEvent 派发事件。最后亲手封装一个可复用的自定义 Dialog 弹窗。学完本文你应当能把项目里重复的卡片、弹窗、导航条抽成独立组件做到一次编写、处处复用。本文适合已掌握页面开发、想开始组件化重构的同学。相关延伸阅读李沐《动手学深度学习》191 集课程全解析模块拆解与学习路径吴恩达《面向开发者的提示词工程》精读吴恩达《Model Context Protocol (MCP) 教程》精读一多模态大模型教程笔记ViT・CLIP・SAM・Stable Diffusion1. 什么是自定义组件为什么要自己造组件官方组件view、text、button 等能覆盖基础场景但项目里常遇到 同样的卡片、同样的弹窗、同样的导航条 在多个页面反复粘贴复制。改一个样式要改五处、漏改一处就出现不一致维护起来非常痛苦。自定义组件就是把这一段结构、样式和逻辑打包成一个独立单元像官方组件一样在任意页面复用达到一次编写、处处使用、统一维护的目的。你在页面里写my-dialog title提示 /这种标签本质上和用button没有区别 —— 只是这个按钮是你自己造的。为什么一定要学自定义组件可以从三个角度想。第一是消除重复一个购物车角标在首页、分类页、我的页都要出现不封装就得抄三份以后要改成 超过 99 显示 99就得改三个文件漏一个就是线上 bug。第二是统一维护封装成cart-badge后改一次所有用到的地方同步生效。第三是降低心智负担页面 wxml 里全是view classcard...几百行谁都不想读换成goods-card item{{goods}} /一眼就知道这块是商品卡。组件和页面长得很像但定位不同初学最容易混淆这里先把区别列清楚对比项页面Page自定义组件Component注册构造器Page({...})Component({...})是否进路由必须在 app.json 的 pages 注册有独立路径不进路由不能单独打开必须被页面引用数据接口data 直接平铺properties 对外 data 对内分开方法位置直接平铺在 Page 根上必须写在 methods 里样式作用域页面样式默认样式隔离不污染外部用途一整个屏幕可复用的一小块 UI一句话记忆页面是一整屏组件是能反复用的一块页面用 Page组件用 Component。从使用体验上看自定义组件和官方组件已经没有任何区别你在 wxml 里写my-dialog工具会自动做语法补全和属性提示传错类型还会警告。这意味着一旦你封装好一个高质量组件团队其他人用起来和用官方组件一样顺手 —— 他们不需要读你组件的内部实现只看你暴露的 properties 说明和事件列表就够了。这正是组件化 封装复杂度、暴露简单接口 的精髓把麻烦藏在组件里把简单留给调用方。顺带说清楚组件的数据更新机制它和页面几乎一模一样组件内部也有自己的data修改时同样要调用this.setData({...})框架再据此重新渲染组件的 wxml。不要以为进了组件就要换一套写法 —— 数据驱动视图这条主线从页面到组件是一脉相承的。区别只在于页面的 data 是 全部由自己掌控而组件的数据分成了两半一半 properties 由外面喂进来、一半 data 由自己管理更新时对这两半一视同仁地用 setData但只能 setData 自己的 data不能 setData 进来的 properties。components/ └── dialog/ ├── dialog.wxml # 组件结构 ├── dialog.wxss # 组件样式 ├── dialog.js # Component() 构造器 └── dialog.json # 组件配置声明 usingComponents 等⚠️易错点① 组件 js 用Component()而不是Page()—— 这是新手最常犯的错② 组件必须在使用页面的 json 中注册后才能当标签用③ 组件间样式默认隔离样式隔离页面写的样式不会随意污染组件内部。2. Component 构造器的四个成员组件要在使用它的页面或全局 app.json的usingComponents中注册标签名可自取。先看一个最小骨架// components/dialog/dialog.js最小骨架 Component({ properties: { title: { type: String, value: 提示 } }, data: { visible: true }, methods: { onTapClose() { console.log(点击了关闭); } } });在页面 json 里注册// pages/index/index.json 注册组件 { usingComponents: { my-dialog: /components/dialog/dialog } }在页面 wxml 里像官方标签一样使用!-- pages/index/index.wxml 中使用组件 -- my-dialog title欢迎/my-dialog概念说明Component()注册自定义组件的构造器区别于页面的Page()usingComponents在页面 json 或 app.json 中声明 标签名 → 组件路径 的映射组件路径建议写绝对路径/components/...避免相对路径出错组件到底在哪注册也是新手常问的问题。规则很简单在哪个页面用就注册在哪个页面的 json 里如果几乎每个页面都要用比如顶部导航条就注册到全局app.json的usingComponents里。局部注册只在本页可用全局注册所有页面可用代价是全局注册的组件即使没用到也会被打包所以通用才上全局专用留在页面。注册之后标签名就是你在 json 里写的那个 key比如my-dialog就对应my-dialog。传参时注意字符串可以直接写title欢迎但要传数字、布尔、对象这类非字符串必须用双花括号visible{{isShow}}—— 少了两层花括号传进去的就是字符串false而不是布尔值这是另一个高频坑。再强调一次文件位置的约定自定义组件建议统一放在项目根目录的components/下每类组件一个文件夹文件夹名和内部四个文件名保持一致dialog 文件夹里就是 dialog.wxml、dialog.wxss、dialog.js、dialog.json。这套约定不是强制的但它让任何打开项目的人一眼就能找到某个组件在哪。等组件多起来你会感谢自己当初按规矩建文件夹 —— 否则五个长得差不多的 js 文件散落在各个页面目录里找人都费劲。养成这个小习惯项目越做越多人也能轻松接手维护。这一小步看似琐碎却是专业工程和写着玩的小程序之间的一道重要分水岭值得从一开始就养成。⚠️易错点组件里的函数必须写在methods里不能像页面那样直接平铺在 Component 根上组件更新数据同样用this.setData。4. 组件生命周期与页面生命周期组件有两套生命周期一套是组件自身的lifetimes一套是它所在页面的pageLifetimes。前者关心组件自己 被挂上、布局好、被卸载后者关心 我所在的页面显示了还是隐藏了。// components/dialog/dialog.js组件生命周期示例 Component({ // 对外属性 properties: { visible: { type: Boolean, value: false } }, // 内部数据 data: { innerText: 组件内部数据 }, // 方法组件内的函数都要放这里 methods: { handleTap() { this.setData({ innerText: 被点过了 }); } }, // 组件自身生命周期 lifetimes: { attached() { // 组件实例被 attached 到页面节点树时触发常做初始化、请求数据 console.log(组件已 attached); }, ready() { // 组件布局完成后触发可获取节点尺寸 console.log(组件 ready); }, detached() { // 组件被移出节点树时触发做清理定时器、事件监听 console.log(组件 detached); } }, // 所在页面的生命周期 pageLifetimes: { show() { console.log(页面显示); }, hide() { console.log(页面隐藏); } } });Component({ // 对外属性 properties: { visible: { type: Boolean, value: false } }, // 内部数据 data: { innerText: 组件内部数据 }, // 方法组件内的函数都要放这里 methods: { handleTap() { this.setData({ innerText: 被点过了 }); } }, // 组件自身生命周期 lifetimes: { attached() { // 组件实例被 attached 到页面节点树时触发常做初始化、请求数据 console.log(组件已 attached); }, ready() { // 组件布局完成后触发可获取节点尺寸 console.log(组件 ready); }, detached() { // 组件被移出节点树时触发做清理定时器、事件监听 console.log(组件 detached); } }, // 所在页面的生命周期 pageLifetimes: { show() { console.log(页面显示); }, hide() { console.log(页面隐藏); } } });生命周期触发时机典型用途lifetimes.attached组件实例进入页面节点树初始化、请求数据lifetimes.ready组件布局完成获取节点尺寸位置lifetimes.detached组件被卸载清理定时器、解绑事件监听pageLifetimes.show/hide/resize所在页面显示 / 隐藏 / 尺寸变化暂停 / 恢复组件内动画或计时这里还得补一个和生命周期配套的重要概念 ——样式隔离。默认情况下组件的 wxss 只作用于组件内部页面写的.title { color: red }不会污染组件里的.title反过来组件样式也不会漏到页面上。这正是组件能 安全复用 的前提你不用关心使用方页面里有什么类名冲突。如果确实需要打破隔离比如想让组件接受外部主题色可以在 js 里通过options: { styleIsolation: shared }调整但初学阶段保持默认即可。关于清理再强调一次组件如果在 attached 里setInterval起了定时器、或在全局wx.onNetworkStatusChange挂了监听就必须在 detached 里对应地#x20;clearInterval、wx.offNetworkStatusChange。否则组件被反复打开关闭定时器越积越多页面就会莫名卡顿 —— 这是组件化开发最隐蔽的内存泄漏来源。⚠️易错点①lifetimes推荐用对象形式写而不是把 attached/ready 直接平铺在 Component 根上的旧写法② 组件卸载时务必在 detached 里清理定时器和全局事件监听否则会造成内存泄漏。5. 插槽 slot让使用者往里填内容组件封装好结构后有时还想让使用方在组件内部塞自定义的 WXML 内容—— 比如 Dialog 里到底写什么正文、放哪几个按钮。插槽 slot就是组件内部预留的 占位出口使用方写在组件标签内的内容会被投射到对应 slot 位置。组件内部定义插槽!-- components/dialog/dialog.wxml定义插槽 -- view classdialog view classtitle{{title}}/view !-- 默认插槽使用方直接写在标签里的内容 -- view classbodyslot/slot/view !-- 具名插槽 -- view classfooterslot namefooter/slot/view /view要使用多个具名插槽必须在组件 js 中开启options.multipleSlotsComponent({ options: { multipleSlots: true // 允许多个具名插槽 }, properties: { title: String } });使用页面往插槽里填内容!-- pages/index/index.wxml往插槽填内容 -- my-dialog title确认下单 view这里是默认插槽的正文内容/view view slotfooter button bindtaponCancel取消/button button bindtaponConfirm确定/button /view /my-dialog插槽类型用法默认插槽组件内写一个slot/slot使用方标签内的匿名内容投射到此具名插槽slot namexxx使用方用slotxxx指定内容归属多插槽开关组件 js 中options: { multipleSlots: true }否则多插槽不生效⚠️易错点① 不开multipleSlots时写多个具名插槽不会生效② 一个组件不写 slot使用方在标签内填的内容根本不会显示③ 插槽内容的样式归属以使用方页面为准注意样式作用域。插槽到底解决了什么痛点举个具体例子你就懂了。假设你要封装一个list-panel面板组件它有统一的外框、标题栏、底部分隔线但 面板里面到底放商品、放文章还是放表单每次用都不一样。如果没有插槽你就只能把面板内部也做成 properties 传进去 —— 可内部是一段 WXML 结构根本没法用属性传。有了插槽就简单了组件只负责画外框和标题中间留一个slot使用方想往里塞什么就塞什么。这样外框逻辑只写一遍内部内容却能千变万化。默认插槽和具名插槽怎么选组件里只有一处 留白就用默认插槽使用方标签里直接写内容即可有多处不同位置要留白比如正文一处、底部按钮区一处就用具名插槽靠slot名字把内容分别投递到对应位置。判断标准很朴素你想让使用方填几处就留几个出口。最后提醒一个刚做组件时容易忽略的细节插槽虽然让使用方往里填了内容但这些内容在运行时到底算 谁的小程序里插槽内容的样式归使用方页面管而不是组件本身。也就是说你在组件 wxss 里写的样式管不到使用方塞进插槽的那段内容想统一插槽内容的外观要么靠使用方自己写要么在组件里再约定一层结构和类名。这个规则初次接触会觉得反直觉但想清楚后其实合理 —— 组件不应该强行规定别人塞进来的内容长什么样。6. 实战封装一个可复用的 Dialog 弹窗结合前面所学封装一个 Dialog父组件通过properties传入visible控制显隐和title标题正文用默认插槽、按钮区用具名插槽。!-- components/dialog/dialog.wxml -- view classmask wx:if{{visible}} view classdialog view classdialog-title{{title}}/view view classdialog-body slot/slot /view view classdialog-footer slot namefooter/slot /view /view /view/* components/dialog/dialog.wxss */ .mask { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; } .dialog { width: 600rpx; background: #fff; border-radius: 20rpx; padding: 30rpx; } .dialog-title { font-size: 34rpx; font-weight: bold; text-align: center; } .dialog-body { margin: 24rpx 0; font-size: 28rpx; color: #333; } .dialog-footer { display: flex; gap: 20rpx; }// components/dialog/dialog.js Component({ options: { multipleSlots: true }, properties: { visible: { type: Boolean, value: false }, title: { type: String, value: 提示 } } });父页面使用!-- pages/index/index.wxml -- button bindtapshowDialog打开弹窗/button my-dialog visible{{isShow}} title温馨提示 text确定要删除这条记录吗删除后不可恢复。/text view slotfooter button sizemini bindtaponCancel取消/button button sizemini typeprimary bindtaponConfirm确定/button /view /my-dialog// pages/index/index.js Page({ data: { isShow: false }, showDialog() { this.setData({ isShow: true }); }, onCancel() { this.setData({ isShow: false }); }, onConfirm() { this.setData({ isShow: false }); // 执行删除等业务 } });⚠️易错点① 显隐由父组件通过visible属性控制组件内部不要自己改 properties单向数据流② 弹层遮罩position: fixed要配合全屏背景③ 一定记得在页面 json 的usingComponents注册my-dialog。这个 Dialog 为什么要把 显隐 交给父组件管而不是组件自己内部一个data.visible控制因为弹窗 什么时候弹、弹完做什么 是业务决定的首页点删除要弹、设置页点退出也要弹触发时机和后续动作都在父页面。组件只负责 给我一个 visible 我就显示、给我 title 我就显示标题至于为什么显示、点完确定干嘛组件一概不知。这样这个 Dialog 才能在几十个页面里通用 —— 如果把显隐写死在组件内部它就变成了一个和业务耦合的、没法复用的页面片段。这正是单向数据流的价值组件只管表现父组件管状态。遮罩层用position: fixed; inset: 0;是为了让它铺满整个屏幕、盖住下面所有内容再用半透明黑底压暗背景把视觉焦点逼到中间的白卡片上。display: flex配合居中就是为了让卡片无论内容多高都能稳稳落在屏幕正中。这些样式和组件逻辑无关纯是通用弹窗的外观所以一次写好、处处生效。7. 父子组件通信triggerEvent 派发事件Dialog 的 取消 / 确定 按钮如果收在子组件内部但点了之后要通知父组件做关闭或执行业务怎么办小程序通过子组件调用this.triggerEvent(事件名, detail数据)向外派发自定义事件父组件用bind:事件名监听。这就是组件化的 子 → 父 通信通道。把子按钮收进组件内部!-- components/dialog/dialog.wxml把按钮收进组件内 -- view classmask wx:if{{visible}} view classdialog view classdialog-title{{title}}/view view classdialog-bodyslot/slot/view view classdialog-footer button sizemini bindtaphandleCancel取消/button button sizemini typeprimary bindtaphandleConfirm确定/button /view /view /view子组件派发事件// components/dialog/dialog.js 派发事件 Component({ properties: { visible: { type: Boolean, value: false }, title: { type: String, value: 提示 } }, methods: { handleCancel() { // 向父组件派发 cancel 事件 this.triggerEvent(cancel, { from: dialog }); }, handleConfirm() { // 派发 confirm 事件并带上 detail 数据 this.triggerEvent(confirm, { id: 1001 }); } } });父页面监听并接收!-- 父页面监听自定义事件 -- my-dialog visible{{isShow}} title温馨提示 bind:cancelonDialogCancel bind:confirmonDialogConfirm text确定要删除这条记录吗/text /my-dialog// pages/index/index.js 接收事件 Page({ data: { isShow: false }, onDialogCancel() { this.setData({ isShow: false }); }, onDialogConfirm(e) { // e.detail 就是 triggerEvent 第二个参数传过来的数据 console.log(用户点了确定携带数据, e.detail); this.setData({ isShow: false }); // 执行业务wx.request 删除接口…… } });父子通信的完整方向通信方向机制父 → 子通过properties属性传值如 visible /title子 → 父子组件this.triggerEvent(name, detail)父组件bind:name监听用e.detail取数据学到这里你已经具备了独立封装任意业务组件的全部积木用 properties 接外部配置用 data 管内部状态用 methods 写行为用 lifetimes 做初始化和清理用 slot 留出内容位用 triggerEvent 把内部动作上报给父级。回头看一个真实项目无非就是把商品卡、价格标签、空状态提示、确认弹窗这些反复出现的片段一个个按这套模式拆成组件再在页面里像搭积木一样拼起来。组件化不是什么高深的新东西它只是把 重复就该复用 这个朴素工程原则落到了小程序的具体语法上。通信方向机制父 → 子通过properties属性传值如 visible /title子 → 父子组件this.triggerEvent(name, detail)父组件bind:name监听用e.detail取数据为什么父子通信要设计成这样而不是让子组件直接改父组件数据因为这是一条单向数据流数据只能从父组件通过 properties 往下流子组件想要影响父组件只能 发个消息 通知父组件由父组件自己决定改不改。这样做的好处是界面上所有数据的变化都能追溯到唯一来源不会出现父子互相改数据、状态乱套的局面。triggerEvent其实有三个参数第三个是事件选项初学了解即可this.triggerEvent(confirm, { id: 1001 }, { bubbles: true, // 事件是否向上冒泡到更上层组件 composed: true, // 冒泡是否能穿透到组件外部 capturePhase: false // 是否启用捕获阶段 });默认bubbles为 false意味着事件只被直接父组件那一层的bind:confirm捕获不会继续往上冒。绝大多数业务场景用默认值就够了只有当组件嵌套很深、想让最外层统一处理时才需要开冒泡。整个通信闭环再用一句话串一遍父组件用属性把数据 递 给子组件properties子组件用事件把消息 喊 给父组件triggerEvent e.detail。这一递一喊就是小程序组件化通信的全部。那到底什么时候该把一段 UI 抽成组件判断标准很实用同一段结构在两个及以上页面重复出现就值得抽只在一个地方用一次先别过度设计。新手常犯的另一个极端是 上来就把每个 view 都拆成组件结果文件数量翻倍、跳来跳去反而更乱。正确节奏是先用页面把功能做出来当你发现自己在复制粘贴第三遍同一段 wxml 和 wxss 时再动手抽组件 —— 这时你也更清楚这段结构哪些该做成可配置的 properties、哪些该留成插槽。组件化之后你会发现页面 wxml 变得非常短像一份 装配清单一个goods-list、一个cart-badge、一个my-dialog几行就把页面拼出来了。具体细节全藏在各自组件里想改商品卡样式就打开 goods-card想改弹窗逻辑就打开 dialog互不干扰。这就是组件化带来的可读性和可维护性也是从 会写页面 到 会做工程 的分水岭。⚠️易错点①子组件不能直接改父组件传进来的 properties要通知父组件改②triggerEvent第二个参数才是附带数据父组件在e.detail里取③ 监听写法bind:事件名与bind事件名均可但事件名要与triggerEvent完全一致④ 不要在子组件里this.setData一个 properties 字段违反单向数据流。8. 本模块视频清单本模块共 5 集全局第 70-74 集总时长约 51 分钟全部为 B 站合集 BV18VTW6TEwE 的分 P集章节标题时长链接70自定义组件 7:15观看 p7071自定义组件_常用属性和方法 6:48观看 p7172自定义组件_插槽 13:49观看 p7273自定义组件_Dialog 15:57观看 p7374自定义组件_Dialog 事件处理 7:32观看 p74注本批各集均无视频内嵌字幕与本地 AI 总结依据 集标题 微信官方标准 API 补全标 Component、properties、slot、triggerEvent 等用法以微信官方最新文档为准。9. 常用 API 速查表名称作用关键参数 / 备注Component({...})注册自定义组件区别于页面 Page ()含 properties/data/methods/lifetimesusingComponents注册组件标签页面 json 或 app.json标签名 → 组件路径properties父传入数据type、value、observer 监听变化properties.x.observer监听属性变化(newVal, oldVal)避免死循环 setDatadata组件内部状态this.setData 更新methods组件方法集合组件内所有函数都放这里lifetimes.attached组件挂载进入节点树常做初始化lifetimes.ready布局完成可查询节点尺寸lifetimes.detached组件卸载清理定时器 / 监听pageLifetimes页面级生命周期show / hide / resizeslot默认插槽使用方匿名内容投射于此slot namex具名插槽使用方 slotx 指定options.multipleSlots开启多插槽true 后多个具名插槽才生效this.triggerEvent(name, detail)子向父派发事件父用 bind:name 监听e.detail 取数据10. 学习打卡清单完成第 70 集自定义组件完成第 71 集自定义组件_常用属性和方法完成第 72 集自定义组件_插槽完成第 73 集自定义组件_Dialog完成第 74 集自定义组件_Dialog 事件处理已用 Component () 写出组件骨架并在页面 usingComponents 注册使用已为一个 properties 加上 observer 监听其变化已在组件中使用 attached/detached 做初始化与清理已用默认插槽 具名插槽开启 multipleSlots实现内容分发已封装 Dialog 并通过 triggerEvent bind:confirm/cancel 完成父子通信