Skip to content

生命周期、分包与自定义组件(二) ​

本页对应 生命周期、分包与自定义组件,共 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/weapp
css
.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 页面路径。后续开发还会用到其它信息,以实际打印结果为准。

易错点: 页面栈是“当前打开历史”,不是路由配置表;每个元素是页面实例,不是普通路径字符串;不要把它当永久存储。调试时先打印页面栈结构,再决定取哪个字段。

课件图示 ​

课件第6页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 本

课件第12页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 l

课件第14页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 l

课件第18页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 l

课件第19页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 l

代码示例 ​

示例一:自定义组件(样式隔离 + 多插槽 + 生命周期 + 父传子 + 子传父) ​

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/weapp

project.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" 或看错文档版本;修法:确认小程序版文档,按“初始化→安装→配置→构建”逐项检查,路径按项目实际调整。

练习 ​

  1. 封装一个导航栏组件:支持 back、delta、标题插槽和右侧插槽,要求开启多插槽,并开放 1–2 个外部样式类;写出父页面使用代码与验证点。
  2. 封装倒计时按钮:attached 开始倒计时,点击后通过自定义事件把剩余秒数传给父页面,父页面用轻提示展示;要求说明事件名、回调参数位置和清理定时器的位置。
  3. 对同一个自定义组件分别用 addGlobalClass 与 externalClasses 覆盖样式;列出两种方式各自的选择器限制、模板写法和调试检查点。
  4. 给出一份 Vant 接入失败排查清单:从文档版本、依赖初始化、安装、配置、构建、页面注册六处定位问题,并写出每步的验证命令或观察点。
  5. 设计一个跨页数据共享小练习:用应用实例保存共享数据,用页面栈读取当前页面路径与页面数据;要求写出获取入口、读取位置和至少两个调试观察点。

原视频对照 ​

#视频时长
1自定义组件-slot插槽08:41
2自定义组件-经验分享06:07
3自定义组件-生命周期11:27
4组件通讯-父传子13:15
5组件通讯-父传子-补充04:51
6组件通讯-子传父10:13
7Vant组件库-准备工作19:14

小结 ​

  • 自定义组件优先检查三件事:component: true、Component 函数、usingComponents 注册路径。
  • 样式覆盖要主动设计:默认隔离下,全局类与外部样式类按需开放,组件内部坚持类名选择器。
  • 插槽能力要显式开启;具名插槽必须同时保证配置、name、使用端 slot 三者一致。
  • 生命周期选错会表现为“代码执行了但页面没变”,数据驱动渲染优先放 attached 验证。
  • 组件通信按边界处理:外部数据走 properties,内部状态走 data,子传父统一走 triggerEvent + ev.detail。
  • 第三方组件库接入先确认文档版本,再按初始化、安装、配置、构建、注册顺序排查,不能跳步。
  • 调试组件时看组件自身数据面板,不要只盯页面 AppData;页面栈与应用实例适合定位跨页共享问题。