Skip to content

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

本页对应 生命周期、分包与自定义组件,共 7 段视频,约 49 分钟。

学习目标 ​

  • 能够在 app.json 中配置 lazyCodeLoading: requiredComponents,并通过代码质量扫描验证组件按需注入生效。
  • 能够查阅 Vant 组件文档,完成导航栏组件的全局注册,并使用其属性、插槽和事件实现页面导航功能。
  • 能够通过审查元素定位 Vant 组件内部类名,覆盖标题、图标及按下态样式,并正确使用 !important、伪元素和 custom-class。
  • 能够在 app.wxss 根选择器中定义 CSS 变量作为全局主题色,并使用 var() 应用变量,验证修改一处后全局样式同步变化。
  • 能够使用 getApp() 获取应用实例并读写全局数据,使用 getCurrentPages() 获取页面栈、取最后一项作为当前页面实例并读取其 route。
  • 能够根据业务场景选择 onLoad 或 onShow,使用本地存储 API 读取图书列表并通过 setData 渲染到页面。

知识点 ​

3.1 昵称和头像 ​

为什么需要: 用户资料如果让用户手填,体验差、容易填错。小程序提供了昵称和头像的快捷获取方式,适合做个人资料页。

怎么想: 昵称用 input 的 type="nickname",在 blur 时取 ev.detail;头像用 button 的 open-type="chooseAvatar",监听 chooseavatar 事件,从 ev.detail.avatarUrl 取临时地址。

怎么做: 先保留本节示例结构:

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;
}

在上面的结构上做两处关键设置:input 改为 type="nickname" 并绑定 blur;button 加 open-type="chooseAvatar" 并绑定 chooseavatar。回调里分别取 ev.detail.nickname、ev.detail.avatarUrl,再用 setData 显示。

易错点: 事件名是 chooseavatar,没有大写字母;开发者工具里 blur 有时需要失去焦点两次才触发;昵称不是从 ev.target.value 取,而是从 ev.detail 取;头像拿到的是临时路径,后续按业务需要上传或保存。

3.2 应用实例 ​

为什么需要: 多个页面可能需要共享 token、版本号、用户信息或公共方法。每个页面各存一份会重复且难同步,因此需要全局唯一的应用实例。

怎么想: app.js 中调用 App() 会注册应用实例,全局唯一;任意页面通过 getApp() 获取。共享数据可放 globalData,也可以直接挂在实例上,但 globalData 是更清晰的规范。

怎么做:

javascript
App({
  message: '应用实例中的数据...',
  updateMessage() {
    // this 指向应用实例本身
  }
})
javascript
// 获取用应实例
const app = getApp()
Page({
  onLoad() {
    // 查看应用实例中的内容
    console.log(app)
  }
})

转写稿中的调试过程是:在 app.js 的 globalData 里放 token,在页面中 getApp() 后打印,能看到实例上的数据、生命周期和自定义方法;也可以在 App() 外层直接写 version,通过 app.version 读取。修改时先 const app = getApp(),再 app.globalData.token = '...',刷新后打印可看到新值。

易错点: this 在 App() 方法里指向应用实例;globalData 只是约定,不是强制;不要把应用实例和页面 data 混用;修改全局数据后,已显示的页面不会自动更新,需要页面自己 setData。

3.3 页面栈 ​

为什么需要: 有时要拿当前页面的实例、路由、参数,或调用页面上的 setData、生命周期。页面栈记录了当前打开过的页面历史,能帮我们定位当前页面。

怎么想: 全局调用 getCurrentPages() 得到数组,每个单元是一个页面实例。数组最后一项才是当前页面;TabBar 页切换时,switchTab 会关闭其他非 Tab 页,所以 Tab 页里通常只有一项;普通页面跳转会继续往数组后面追加。

怎么做: 在控制台或页面方法中打印页面栈,观察 data、setData、onShow、onLoad、route 等信息。取当前页面实例时:

javascript
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
console.log(currentPage.route)

转写稿中的调试是:在 Tab 页打印只有一项;跳到非 Tab 的 test 页面后再打印,数组变成两项,当前页在最后;绑定按钮取 length - 1,能拿到当前页面实例并打印 route。

易错点: 函数是 getCurrentPages,不是单数;不要默认第一项是当前页;Tab 页数组通常只有一项;页面实例不要长期缓存。生命周期选择也要看业务:查询只在首次加载可用 onLoad,购物车等每次显示都要最新数据则用 onShow。

课件图示 ​

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

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

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

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

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

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

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

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

代码示例 ​

1. 获取用户昵称与头像(input + button) ​

WXML(pages/profile/profile.wxml)

xml
<view class="profile">
  <view class="nickname">
    <label for="">昵称:</label>
    <input
      type="nickname"
      value="{{nickName}}"
      placeholder="请输入昵称"
      bindblur="onNicknameBlur"
    />
  </view>
  <view class="avatar">
    <label for="">头像:</label>
    <button open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
      <image src="{{avatarUrl}}" mode="aspectFill" />
    </button>
  </view>
</view>

WXSS(pages/profile/profile.wxss)

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;
}

JS(pages/profile/profile.js)

js
Page({
  data: {
    nickName: '',
    avatarUrl: ''
  },

  // 昵称:input 失去焦点时,从 ev.detail 中取
  onNicknameBlur(ev) {
    console.log(ev.detail)
    this.setData({ nickName: ev.detail.value })
  },

  // 头像:chooseavatar 事件(全小写),从 ev.detail.avatarUrl 中取
  onChooseAvatar(ev) {
    console.log(ev.detail)
    this.setData({ avatarUrl: ev.detail.avatarUrl })
  }
})

要点解释 / 易错点

  • input 的 type 必须是 nickname,才会出现微信昵称的快捷填写。
  • 昵称走的是 blur 事件,在开发者工具里需要失焦两次才会触发,真机正常。
  • 头像按钮的 open-type="chooseAvatar",事件名是 bindchooseavatar——全部小写,没有大写字母,写成 bindChooseAvatar 不生效。
  • 两者数据都从 ev.detail 里取:昵称是 ev.detail.value,头像是 ev.detail.avatarUrl。
  • 图片用 mode="aspectFill" 做圆形裁切,占位图放在 background-image 里。

2. 应用实例 getApp + 按需注入配置 ​

app.json

json
{
  "pages": [
    "pages/index/index"
  ],
  "lazyCodeLoading": "requiredComponents"
}

app.js

js
App({
  globalData: {
    token: ''
  },
  version: '1.0.0',
  message: '应用实例中的数据...',
  updateMessage() {
    // this 指向应用实例本身
  }
})

页面 JS(pages/index/index.js)

js
// 获取应用实例(全局唯一)
const app = getApp()

Page({
  onLoad() {
    // 查看应用实例中的内容
    console.log(app)
    console.log(app.globalData.token)
    console.log(app.version)
  },

  setToken() {
    // 直接赋值即可修改全局数据
    app.globalData.token = 'ABCDE'
    console.log(getApp().globalData.token)
  }
})

页面 WXML(pages/index/index.wxml)

xml
<button type="primary" bindtap="setToken">全局设置</button>

要点解释 / 易错点

  • App() 注册的应用实例全局唯一,任意页面用 getApp() 拿到的都是同一个对象。
  • globalData 只是书写规范,不是强制容器;像 version 直接挂在实例上也一样能通过 app.version 取到。
  • 修改全局数据不需要 setData,直接 app.globalData.token = xxx 赋值即可;但要刷新页面数据仍需页面自己 setData。
  • 不要在 app.js 里调用 getApp(),那时实例还没注册完,取到的是 undefined。
  • lazyCodeLoading 的值只有 "requiredComponents" 一种写法(懒加载,用到哪个组件才注入哪个),加到 app.json 里就能消掉代码质量报告里的「按需注入」未通过项。

3. 页面栈 getCurrentPages + 生命周期 + 本地存储 ​

页面 JS(pages/books/books.js)

js
Page({
  data: {
    list: []
  },

  // 加载时把本地存储读出来,回显到页面
  onLoad() {
    const list = wx.getStorageSync('list')
    if (list) {
      this.setData({ list })
    }
  },

  // 查询书单并写入本地存储
  getBooks() {
    // 模拟请求返回:res.data.data 是书单数组
    const res = { data: { data: ['书1', '书2', '书3'] } }

    this.setData({ list: res.data.data })
    wx.setStorageSync('list', res.data.data)
  },

  // 取当前页面实例
  getCurrentPage() {
    const pages = getCurrentPages()      // 页面栈,是一个数组
    const current = pages[pages.length - 1]  // 最后一项才是当前页面

    console.log(current)          // 页面实例
    console.log(current.data)     // 页面的初始数据
    console.log(current.route)    // 页面路径
  }
})

页面 WXML(pages/books/books.wxml)

xml
<button type="primary" bindtap="getBooks">查询书单</button>
<button bindtap="getCurrentPage">获取当前页面实例</button>

<view wx:for="{{list}}" wx:key="*this">{{item}}</view>

要点解释 / 易错点

  • getCurrentPages() 是全局函数,返回的是数组,数组最后一项才是当前页面,取 pages[pages.length - 1]。
  • tabBar 页面之间切换,wx.switchTab 会关闭其他非 tabBar 页面,所以页面栈里永远只有一项;普通 navigateTo 跳转才会往后追加。
  • onLoad 只在页面首次加载触发一次,适合只读一次本地缓存;像购物车这种每次显示都要拿最新数据的,要用 onShow。
  • wx.setStorageSync 的第二个参数要传真正要存的数据(这里是 res.data.data),不是整个 res,也不是 this.data 整个对象。
  • 页面实例上能直接看到 data、setData、onLoad/onShow 等生命周期、route 等,借助页面栈就能跨页面拿这些信息。

常见错误 ​

  1. 昵称输入后取不到值 → 监听成了 input 事件或未触发失焦 → 用 type="nickname",监听 blur,从 ev.detail 取值;开发者工具中失焦两次再测。
  2. 头像按钮点击无反应 → open-type 或事件名大小写写错 → 写 open-type="chooseAvatar",监听全小写 chooseavatar,取 ev.detail.avatarUrl。
  3. 用 getApp() 改了全局数据,页面仍显示旧值 → 把全局对象当成响应式数据 → 修改后手动 setData,或在使用页面重新读取最新值。
  4. 用 getCurrentPages() 取当前页取错 → 取了 pages[0],或忽略 tabBar 切页会清历史栈 → 取 pages[pages.length - 1];tabBar 场景只按当前页处理。
  5. 给 Vant 组件直接写 class 改样式无效 → 组件样式隔离、权重不足,字体图标还需命中伪元素 → 查文档用外部样式类/custom-class,审查元素后补权重;图标补 ::before,按下白块覆盖 hover-class。
  6. 代码质量报告仍提示组件未按需注入/图片超 200K → 配置位置写错,或只做分包没换大图 → app.json 配 "lazyCodeLoading": "requiredComponents";大图改网络资源并删本地大图。

练习 ​

  1. 完成昵称与头像采集:输入昵称后失焦取值,点击头像按钮选择头像并回显,处理开发者工具需失焦两次的情况。
  2. 在 app.js 中维护 token,在页面按钮点击时通过 getApp() 修改它,再在另一个页面读取展示;说明页面展示是否需要额外 setData。
  3. 用 getCurrentPages() 打印页面栈,分别验证普通页面跳转与 tabBar 切换后的数组长度、最后一项 route。
  4. 用 Vant 导航栏替换自定义导航栏:配置标题、监听左侧点击并弹提示;再通过审查元素修改返回文字、箭头颜色和按下白块。
  5. 开启组件按需注入并扫描代码质量;把超过 200K 的本地图片替换为网络资源,确认报告变化。

原视频对照 ​

#视频时长
1启用组件按需注入05:01
2Vant组件库-如何查文档14:42
3Vant组件-修改组件样式05:53
4CSS变量-定义全局主题色04:32
5框架接口-获取App实例05:42
6框架接口-获取页面栈和页面实例08:40
7页面生命周期-图书本地存储04:35

小结 ​

  1. getApp() 与 getCurrentPages() 是框架级入口,分别面向应用实例和页面实例。
  2. 全局数据可读写,但不会自动驱动页面视图更新,视图更新仍要回到 setData。
  3. 页面栈最后一项才是当前页面;tabBar 切页后不能按普通历史栈理解。
  4. 组件库优先查文档,按 props、事件、插槽、外部样式类的顺序找方案。
  5. 改第三方组件样式要靠审查元素、权重、伪元素和按下态类名,不要盲写类名。
  6. lazyCodeLoading 固定在 app.json 配置;大图优化应走网络资源,不要只依赖分包。
  7. onLoad 与 onShow 按业务触发频率选择,首次加载与每次显示要区分。
  8. CSS 变量适合统一维护主题色,集中定义比散落硬编码更易切换。