主题
生命周期、分包与自定义组件(二)
本页对应 生命周期、分包与自定义组件,共 7 段视频,约 1 小时 13 分。
学习目标
- 能在自定义组件中使用默认插槽传递页面结构,并说明小程序插槽标签必须闭合的语法要求
- 能通过
options.multipleSlots配置与name、slot属性实现具名插槽,并验证未开启配置时具名插槽不生效的现象 - 能说出
options中styleIsolation、multipleSlots、addGlobalClass的推荐配置,并解释组件内使用标签选择器、ID 选择器、属性选择器会污染全局样式的原因 - 能区分
created与attached两个组件生命周期钩子,说明created中setData无效的原因,并用attached结合wx.getSystemInfoSync()完成自定义导航栏状态栏高度的动态适配 - 能在
properties中定义父传子属性(含type与value默认值),并在子组件中通过条件渲染使用该属性;能说明属性类型不匹配时的强制类型转换行为 - 能在子组件中用
this.triggerEvent触发自定义事件、在父组件中用bind绑定并从事
知识点
基本语法
为什么需要:小程序内置组件覆盖不了所有业务场景,重复结构(如自定义导航栏)需要封装成组件复用。怎么想:自定义组件在结构上仍由 .wxml、.wxss、.js、.json 组成,和页面很像,但必须明确告诉小程序“这是组件”。怎么做:用开发者工具创建组件后,检查 .json 是否有 component: true,.js 是否调用 Component 函数;使用时在页面或全局 usingComponents 中注册。易错点:组件名和路径要对应;注册路径写错时页面直接找不到组件。
json
{
"usingComponents": {
"自定义组件名称": "自定义组件的路径",
"navigation-bar": "./navigation-bar/index"
}
}组件样式
为什么需要:封装组件后,页面经常要微调组件内部样式。怎么想:默认存在样式隔离,页面样式不能直接影响组件;需要开放能力时,可用 addGlobalClass 或外部样式类。怎么做:options.addGlobalClass: true 允许页面修改组件样式,但仍应使用组件本身类名;externalClasses 声明可接收外部类名的样式类。易错点:组件内不要用标签选择器、ID选择器、属性选择器。转写稿中调试过:在组件里写标签选择器会影响到全局,而且不受样式隔离限制;所以推荐统一用类名。
javascript
Component({
options: {
addGlobalClass: true,
},
// 开发自定义的样式类
externalClasses: ['custom-class', 'title-class']
})xml
<navigation-bar
custom-class="my-navigation-bar"
title-class="my-navigation-bar-title"
></navigation-bar>xml
<view class="navigation-bar custom-class">
<view class="navigation-bar-title title-class">
自定义标题
</view>
</view>slot(插槽)
为什么需要:properties 适合传数据,但有时父组件要传“页面结构”,例如在导航栏中插入标题、图标。怎么想:插槽就是结构占位,用法与 Vue 类似;小程序默认只支持一个 <slot />,多插槽要开启配置。怎么做:组件内用 <slot /> 占位;多插槽时设置 options: { multipleSlots: true },并给 <slot name="..."/> 命名,父级用 slot="..." 指定。易错点:转写稿中先写两个插槽并加 slot="left",发现不生效,两个内容都落到后面;开启 multipleSlots: true 后才正确。具名插槽同样需要开启;<slot> 标签必须闭合,否则会报错。
生命周期
为什么需要:组件创建、初始化完成时需要执行逻辑,例如获取状态栏高度并动态设置顶部间距。怎么想:组件生命周期写在 lifetimes 中;created 类似 Vue 的 created,attached 类似 mounted。怎么做:常用 attached 调接口、setData。转写稿用 wx.getSystemInfoSync() 取 statusBarHeight,单位是 px,再绑定到 padding-top。易错点:created 中调用 this.setData 不报错但无效;调试时把同样代码从 created 挪到 attached 才生效。换 iPhone5 时状态栏高度从 44 变为 20,说明要动态获取,不能写死。
javascript
Component({
lifetimes: {
created() {
// code...
},
attached() {
// code...
}
}
})组件通信
为什么需要:父页面要把数据传给组件,组件也要把内部数据传回父页面。怎么想:父传子用 properties,子传父用自定义事件。怎么做:properties 可声明类型和默认值;父级通过属性传值。子组件用 this.triggerEvent('事件类型', 参数) 触发,父级用 bind:事件类型="回调" 监听,参数从事件对象 detail 中取。易错点:转写稿中给 Boolean 属性传中文,被强制转换成 true;类型不符会有警告。properties 和 data 不能同名;组件方法要写在 methods 中,页面事件才与 data 同级。外部样式类不会出现在 properties 里。
javascript
Component({
properties: {
back: Boolean,
delta: {
type: Number,
value: 1
}
}
})xml
<navigation-bar back delta="1">
<text slot="title">自定义标题</text>
</navigation-bar>javascript
Component({
externalClasses: ['custom-class'], // 外部样式类
properties: {
time: {
type: Number,
value: 60,
},
useSlot: Boolean,
},
lifetimes: {
attached() {
// 开始倒计时
this.countStart()
},
},
methods: {
countStart() {
let timer = null
// 停止定时器
if (this.data.time === 0) return clearTimeout(timer)
// 更新渲染
this.setData({
time: (this.data.time -= 1),
})
// 定时器
timer = setTimeout(this.countStart.bind(this), 1000)
// 传递数据到组件外部
this.triggerEvent('change', this.data.time)
},
},
})css
.navigation-bar {
left: 0;
top: 0;
z-index: 999;
display: flex;
align-items: center;
height: 44px;
background-color: #fff;
}
.navigation-bar-title {
flex: 1;
text-align: center;
font-size: 14px;
}
.navigation-bar-back {
position: absolute;
left: 10px;
width: 44rpx;
height: 44rpx;
background-image: url(https://lotjol.github.io/static-store/enjoy-plus/images/back-arrow.svg);
background-repeat: no-repeat;
}Vant 组件库
为什么需要:项目里不必所有组件都手写,组件库能提升开发效率。怎么想:Vant 有 Vue 版和小程序版,必须确认进入的是小程序文档。怎么做:根目录先 npm init 生成 package.json,再安装;移除全局 "style": "v2";按官方文档在 project.config.json 配置 npm 相关项并构建 npm,生成 miniprogram_npm;最后在 app.json 全局注册。易错点:转写稿强调不要直接照抄官方配置里的输出目录,按当前项目实际配置为 ./;构建前没有 package.json 会导致配置无法完成。
bash
npm install @vant/weappcss
.buttons {
display: flex;
justify-content: space-around;
padding: 20rpx;
margin: 20rpx;
border-radius: 10rpx;
background-color: #fff;
}本节围绕框架提供的三类接口:用户信息填写、应用实例、页面栈。转写稿主要讲自定义组件的插槽、生命周期和通讯,其中“先绑事件→触发后打印事件对象→再取值”的调试顺序可以迁移到本节;转写稿里常说的“副组件”应纠正为“父组件”,“制定事件”应纠正为“自定义事件”。下面只按笔记中的接口整理,不扩展笔记未出现的 API。
3.1 昵称和头像
为什么需要: 用户昵称和头像是很多业务的基础资料。小程序提供了专门的填写入口,避免自己从普通输入框和图片选择器里拼凑,减少交互和兼容问题。
怎么想: 昵称走 input 的专用类型,头像走 button 的专用开放能力。两者都不要只盯着“界面长什么样”,而要先确认“事件有没有触发、事件对象里有没有值”。按转写稿里的调试习惯:先绑定事件,触发后打印事件对象,再决定取哪个字段。
怎么做: 把示例中的昵称输入框 type 设置为 nickname,监听 blur 事件,在回调里通过 ev.detail 获取昵称;头像按钮把 open-type 设置为 chooseAvatar,监听 chooseavatar 事件,在回调里通过 ev.detail.avatarUrl 取头像地址,再绑定到 image 的 src 上。示例结构先保留如下,实际练习时再改对应属性:
xml
<view class="profile">
<view class="nickname">
<label for="">昵称:</label>
<input type="text" />
</view>
<view class="avatar">
<label for="">头像:</label>
<button>
<image src="" mode="aspectFill"/>
</button>
</view>
</view>css
.profile {
min-height: 300rpx;
padding: 30rpx;
border-radius: 10rpx;
background-color: #fff;
}
.nickname, .avatar {
display: flex;
align-items: center;
height: 80rpx;
padding: 0 10rpx;
border-bottom: 1rpx solid #eee;
}
.nickname label, .avatar label {
margin-right: 20rpx;
}
.nickname input {
flex: 1;
border: none;
text-align: right;
}
.avatar button {
flex: 1;
height: 60rpx;
padding: 0 20rpx;
text-align: right;
background-color: transparent;
}
.avatar image {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
vertical-align: baseline;
background-image: url(https://lotjol.github.io/static-store/enjoy-plus/images/user_placeholder.png);
background-size: cover;
}易错点: 开发者工具里昵称的 blur 需要失去焦点两次才触发,不要误以为事件没绑上;头像事件名是 chooseavatar,没有大写字母;open-type 的值是 chooseAvatar,注意大小写;昵称输入框不要仍停留在 type="text"。调试时先打印 ev.detail,再取值,避免凭感觉写字段。
3.2 应用实例
为什么需要: 有些数据和逻辑需要在多个页面间共享。每个页面各自保存一份会重复、难同步,应用实例就是全局唯一的共享位置。
怎么想: 在 app.js 中调用 App 注册应用实例,这个实例全局唯一,页面里通过 getApp 获取。共享数据和方法都挂在这个实例上,方法里的 this 指向应用实例本身。
怎么做:
javascript
App({
message: '应用实例中的数据...',
updateMessage() {
// this 指向应用实例本身
}
})javascript
// 获取应用实例
const app = getApp()
Page({
onLoad() {
// 查看应用实例中的内容
console.log(app)
}
})易错点: 不要把 getApp 当成每个页面新建实例;它拿到的是同一个应用实例。方法里访问实例数据时注意 this 指向;共享数据要集中放在应用实例里,不要在多个页面各写一份。
3.3 页面栈
为什么需要: 页面栈记录当前打开的全部页面历史,通过它可以查看页面实例,拿到页面数据、生命周期和路径等信息,便于做返回、调试和页面间处理。
怎么想: 页面栈本质是一个数组,每个单元是一个页面实例(调用 Page 函数时注册)。全局调用 getCurrentPages 获取当前页面栈,再按数组方式查看。
怎么做: 在页面或调试中获取页面栈后,可以查看页面实例的 data、setData、onShow/onLoad 等生命周期,以及 route 页面路径。后续开发还会用到其它信息,以实际打印结果为准。
易错点: 页面栈是“当前打开历史”,不是路由配置表;每个元素是页面实例,不是普通路径字符串;不要把它当永久存储。调试时先打印页面栈结构,再决定取哪个字段。
课件图示





代码示例
示例一:自定义组件(样式隔离 + 多插槽 + 生命周期 + 父传子 + 子传父)
components/navigation-bar/index.json
json
{
"component": true,
"usingComponents": {}
}components/navigation-bar/index.wxml
xml
<view class="navigation-bar custom-class" style="padding-top: {{statusBarHeight}}px">
<view wx:if="{{back}}" class="navigation-bar-back" bindtap="onBack"></view>
<slot name="left"></slot>
<view class="navigation-bar-title title-class" bindtap="onTitleTap">
<slot name="title"></slot>
</view>
<slot></slot>
</view>components/navigation-bar/index.wxss
css
.navigation-bar {
left: 0;
top: 0;
z-index: 999;
display: flex;
align-items: center;
height: 44px;
background-color: #fff;
}
.navigation-bar-title {
flex: 1;
text-align: center;
font-size: 14px;
}
.navigation-bar-back {
position: absolute;
left: 10px;
width: 44rpx;
height: 44rpx;
background-image: url(https://lotjol.github.io/static-store/enjoy-plus/images/back-arrow.svg);
background-repeat: no-repeat;
}components/navigation-bar/index.js
js
Component({
options: {
addGlobalClass: true, // 允许页面修改组件内部样式
multipleSlots: true // 开启多插槽
},
// 外部样式类
externalClasses: ['custom-class', 'title-class'],
properties: {
back: Boolean,
delta: {
type: Number,
value: 1
}
},
data: {
statusBarHeight: 0
},
lifetimes: {
created() {
// 组件创建时触发,此处调用 this.setData 无效
},
attached() {
// 组件初始化完毕时触发
const { statusBarHeight } = wx.getSystemInfoSync()
this.setData({ statusBarHeight })
}
},
methods: {
onTitleTap() {
// 子传父:触发自定义事件
this.triggerEvent('getheight', this.data.statusBarHeight)
},
onBack() {
wx.navigateBack({ delta: this.data.delta })
}
}
})pages/test/index.json
json
{
"usingComponents": {
"navigation-bar": "/components/navigation-bar/index"
}
}pages/test/index.wxml
xml
<navigation-bar
back
delta="{{1}}"
custom-class="my-navigation-bar"
title-class="my-navigation-bar-title"
bind:getheight="onGetHeight"
>
<text slot="left">left</text>
<text slot="title">自定义标题</text>
</navigation-bar>pages/test/index.js
js
Page({
onGetHeight(ev) {
// 父组件通过事件对象 detail 获取子组件传出的参数
wx.showToast({ title: `状态栏高度为${ev.detail}`, icon: 'none' })
}
})要点解释与易错点
.json中必须写"component": true,.js中必须调用Component函数,否则会被当成页面处理。usingComponents中属性名是组件名、属性值是路径,/、./开头都可以,但 Vant 的路径不能带这两种前缀。- 插槽默认只能生效一个;只要用到具名插槽(哪怕只有一个),就必须同时开启
multipleSlots: true,否则slot="title"的内容会被丢到默认插槽里。 - 小程序标签必须闭合,
<slot />或<slot></slot>均可,不闭合会报错。 created中调用this.setData不报错但不生效,需要更新数据一律放到attached中。addGlobalClass: true后页面才能改组件样式,但组件内不能使用标签选择器、ID 选择器、属性选择器,它们不受样式隔离约束,会污染全局。externalClasses中定义的名字(如custom-class)只是“占位类名”,接收的是外部传入的真实类名,组件 wxml 里要写成class="navigation-bar custom-class"。triggerEvent的第一个参数是自定义事件名,父组件用bind:事件名监听,参数从ev.detail中取,不是ev.target.dataset。- 组件内绑定事件的方法必须写在
methods里(页面才是与data同级)。
示例二:接入 Vant 组件库
bash
# 1. 在项目根目录初始化 npm,生成 package.json
npm init -y
# 2. 安装 Vant 小程序版
npm install @vant/weappproject.config.json(在 setting 中手动补充,路径不要照抄官方文档)
json
{
"setting": {
"packNpmManually": true,
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./"
}
]
}
}app.json
json
{
"usingComponents": {
"van-button": "@vant/weapp/button/index"
}
}pages/index/index.wxml
xml
<view class="buttons">
<van-button type="primary">主要按钮</van-button>
<van-button type="default">默认按钮</van-button>
</view>css
.buttons {
display: flex;
justify-content: space-around;
padding: 20rpx;
margin: 20rpx;
border-radius: 10rpx;
background-color: #fff;
}要点解释与易错点
- 必须先
npm init生成package.json,否则安装的依赖不会被记录,project.config.json里的packageJsonPath也无法配置。 - 小程序中通过 npm 下载的包必须执行「工具 → 构建 npm」后才可使用,构建后会生成
miniprogram_npm目录。 app.json中要移除"style": "v2",否则 Vant 组件样式会异常。miniprogramNpmDistDir官方文档写的是./miniprogram/,直接照抄会报错导致模拟器无法启动,应改为"./"。- 全局注册时路径不以
/或./开头,写成@vant/weapp/button/index。 - Vant 有 Vue 版和小程序版两个官网,界面高度相似,务必确认标题是「Vant Weapp」。
示例三:应用实例与页面栈(含昵称头像)
app.js
js
App({
message: '应用实例中的数据...',
updateMessage() {
// this 指向应用实例本身
this.message = '数据已更新'
}
})pages/index/index.wxml
xml
<view class="profile">
<view class="nickname">
<label for="">昵称:</label>
<input type="nickname" bindblur="onNicknameBlur" />
</view>
<view class="avatar">
<label for="">头像:</label>
<button open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
<image src="{{avatarUrl}}" mode="aspectFill" />
</button>
</view>
</view>pages/index/index.js
js
// 获取应用实例
const app = getApp()
Page({
data: {
avatarUrl: ''
},
onLoad() {
// 查看应用实例中的内容
console.log(app.message)
// 页面栈:数组,记录当前打开的全部页面实例
const pages = getCurrentPages()
console.log(pages[pages.length - 1].route)
},
onNicknameBlur(ev) {
// 通过 ev.detail 获取用户昵称
console.log(ev.detail.value)
},
onChooseAvatar(ev) {
// 通过 ev.detail.avatarUrl 获取头像临时路径
this.setData({ avatarUrl: ev.detail.avatarUrl })
}
})要点解释与易错点
App()注册的应用实例全局唯一,任何页面都能通过getApp()拿到,可用于共享数据与方法。- 昵称输入框
type="nickname",监听的是blur事件,且在开发者工具中需要「失去焦点两次」才会触发。 - 头像按钮
open-type="chooseAvatar",事件名是chooseavatar(全小写),不是chooseAvatar。 - 页面栈
getCurrentPages()返回数组,最后一项才是当前页面,可拿到data、setData、onShow、route等信息。 getSystemInfoSync返回的statusBarHeight单位是 px,不是 rpx,绑到行内样式时直接用px即可。
常见错误
- 现象:新建组件后无法注册或行为像页面;原因:组件
.json缺少component: true,或.js误用Page;修法:组件配置补component: true,逻辑改用Component。 - 现象:多个
<slot>或具名插槽不生效,内容都落到同一位置;原因:未开启options.multipleSlots,或name与使用端slot不匹配;修法:组件开启多插槽配置,逐一核对插槽名与填充位置。 - 现象:页面样式覆盖不了组件内部;原因:默认样式隔离,且组件内使用了标签/ID/属性选择器;修法:按需开启
addGlobalClass并用类名覆盖,或用externalClasses开放关键类名;组件内避免标签/ID/属性选择器。 - 现象:
created中setData不报错但不生效;原因:该阶段不能更新渲染;修法:把依赖渲染的数据初始化放到attached或后续生命周期。 - 现象:父传子布尔属性传了字符串却变成
true,或类型警告;原因:properties类型与传入值不匹配,被强制转换;修法:用完整type/value配置,模板传值按类型写,布尔值用数据绑定而非随意字符串。 - 现象:子传父监听不到或拿不到参数;原因:事件写在
methods外、triggerEvent名称不一致、父级回调取错位置;修法:组件方法放methods,父级bind:自定义事件,回调从ev.detail取参。 - 现象:Vant 安装后组件仍不可用;原因:未
npm init、未构建 npm、project.config.json路径照抄、未移除"style": "v2"或看错文档版本;修法:确认小程序版文档,按“初始化→安装→配置→构建”逐项检查,路径按项目实际调整。
练习
- 封装一个导航栏组件:支持
back、delta、标题插槽和右侧插槽,要求开启多插槽,并开放 1–2 个外部样式类;写出父页面使用代码与验证点。 - 封装倒计时按钮:
attached开始倒计时,点击后通过自定义事件把剩余秒数传给父页面,父页面用轻提示展示;要求说明事件名、回调参数位置和清理定时器的位置。 - 对同一个自定义组件分别用
addGlobalClass与externalClasses覆盖样式;列出两种方式各自的选择器限制、模板写法和调试检查点。 - 给出一份 Vant 接入失败排查清单:从文档版本、依赖初始化、安装、配置、构建、页面注册六处定位问题,并写出每步的验证命令或观察点。
- 设计一个跨页数据共享小练习:用应用实例保存共享数据,用页面栈读取当前页面路径与页面数据;要求写出获取入口、读取位置和至少两个调试观察点。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 自定义组件-slot插槽 | 08:41 |
| 2 | 自定义组件-经验分享 | 06:07 |
| 3 | 自定义组件-生命周期 | 11:27 |
| 4 | 组件通讯-父传子 | 13:15 |
| 5 | 组件通讯-父传子-补充 | 04:51 |
| 6 | 组件通讯-子传父 | 10:13 |
| 7 | Vant组件库-准备工作 | 19:14 |
小结
- 自定义组件优先检查三件事:
component: true、Component函数、usingComponents注册路径。 - 样式覆盖要主动设计:默认隔离下,全局类与外部样式类按需开放,组件内部坚持类名选择器。
- 插槽能力要显式开启;具名插槽必须同时保证配置、
name、使用端slot三者一致。 - 生命周期选错会表现为“代码执行了但页面没变”,数据驱动渲染优先放
attached验证。 - 组件通信按边界处理:外部数据走
properties,内部状态走data,子传父统一走triggerEvent+ev.detail。 - 第三方组件库接入先确认文档版本,再按初始化、安装、配置、构建、注册顺序排查,不能跳步。
- 调试组件时看组件自身数据面板,不要只盯页面
AppData;页面栈与应用实例适合定位跨页共享问题。