主题
生命周期、分包与自定义组件(一)
本页对应 生命周期、分包与自定义组件,共 8 段视频,约 1 小时 43 分。
学习目标
- 能够在
app.js中使用App()注册应用,并演示onLaunch、onShow、onHide的触发时机,通过onLaunch的options获取场景值与启动参数。 - 能够在小程序启动时调用更新管理器检查版本更新,并使用模态弹窗提示用户,用户确认后应用更新并重启。
- 能够在
app.json中配置subPackages的root与pages完成分包,并将指定页面和资源划入分包,说明主包体积限制及tabBar页面不能放入分包的规则。 - 能够配置
preloadRule实现分包预下载,设置页面路径、网络类型和预下载分包,并验证控制台出现预加载成功提示。 - 能够创建自定义组件(
component: true、Component()),通过usingComponents完成全局或局部注册并在页面中复用。 - 能够通过
globalClass或externalClasses修改自定义组件样式,并使用navigationStyle: custom实现自定义导航栏及标题居中布局。
知识点
分包加载要解决的核心问题
为什么需要:微信小程序对单个包体积有硬限制,主包超过 2M 会影响预览、上传和上线;即使没超,也可以把非首屏页面拆出去,让首次启动只加载主包。转写稿里导入第三天素材时,项目体积约 3.3M,点击上传直接报错:当前体积 3328KB,超过最大限制 2M。这说明“代码体积”不是抽象概念,而是会卡住发布的真实问题。
怎么想:先判断哪些内容属于首屏必须加载,哪些属于二级页面、低频模块。比如房屋列表、详情、报修等可以按业务模块拆到分包;大图片如果确实必须本地存放,也可以随分包放,避免压在主包里。配置文件中能忽略的通常只是静态资源,代码本身不能靠忽略解决。
怎么做:在全局配置里用 subPackages 声明分包根目录和页面路径:
json
{
"subPackages": [
{
"root": "subpkg_test",
"pages": [
"pages/test/index"
]
},
{
"root": "subpkg_user",
"pages": [
"pages/profile/index"
]
}
],
}root 是分包根目录,pages 是分包内页面路径。根目录和页面不存在时,开发者工具会自动创建。配置保存后,工具会按路由生成对应页面文件。
易错点:严格 JSON 不能写注释、不能用单引号,末尾逗号也要删掉;上面示例若直接使用,需要去掉最后一个逗号。tabBar 页面不能放进分包,只能留在主包。分包不是无限拆,转写稿提到所有分包总体积也不能超过 20M;主包体积最好控制在 1.5M 以内,否则首启速度仍会受影响。
把页面和资源一起拆出去
为什么需要:只拆页面但不拆大资源,主包仍可能超限。转写稿中通过“详情 → 依赖分析”发现 picture-02 单张图就接近 1M,几张图叠加就把主包撑大了。
怎么想:按业务模块划分分包,例如 packages/house、packages/repair;模块内页面和该模块才用到的图片、静态资源放在同一分包目录。主包只保留首页、tabBar 页面和公共资源。
怎么做:配置 root 后,在分包目录下放 static 等资源目录,把大图从主包移到分包;主包页面跳转分包页面时,路径仍按根目录写法,如 /packages/house/pages/list。转写稿演示移动两张图后,主包体积下降,预览二维码重新出现,说明上传成功。
易错点:分包之间资源相互独立,不要跨分包互相引用;分包页面路径要和 root 拼接正确。转写稿还提醒,主包与分包资源引用有边界,配置时以官方文档为准,避免把公共资源散落到各分包。
分包预加载:把“即将访问”的包提前下载
为什么需要:纯按需加载是用户点进分包页面才开始下载,网络慢时会有等待。预加载可以在进入某个主包页面时,提前下载目标分包,等真正跳转时直接打开。
怎么想:找出用户下一步高概率访问的页面,例如从框架页进入个人中心,就在框架页配置预加载 subpkg_user。不是所有包都预加载,否则会浪费流量、拖慢当前页面。
怎么做:在全局配置中用 preloadRule:
json
{
"preloadRule": {
"pages/framework/index": {
"network": "all",
"packages": ["subpkg_user"]
}
},
}含义是加载 pages/framework/index 时自动加载 subpkg_user 分包。network 默认通常是 Wi-Fi,all 表示所有网络都预加载;packages 是数组,可以配多个分包。转写稿演示配置后在控制台看到预加载成功提示。
易错点:preloadRule 与 subPackages 同级;key 是页面路径,value 里写网络和包名;严格 JSON 同样不能有末尾逗号。预加载只是优化,不能替代分包配置;未配置预加载时,仍是跳转后才开始加载。调试时可先看控制台是否有预加载成功/失败提示,再检查页面路径和分包 root 是否写错。
基本语法:组件不是页面,先分清入口
为什么需要:页面中的搜索框、导航栏等结构如果反复写,会重复且难维护;把它们抽成自定义组件,才能复用和隔离逻辑。怎么想:组件结构仍由 .wxml、.wxss、.js、.json 构成,但必须让工具识别“这是组件”。怎么做:课堂演示中通过开发者工具右键新建 Component;关键区别是 .json 必须有 component: true,.js 调用 Component 而不是页面的 Page。使用时在页面或全局注册,配置项为 usingComponents,属性名是组件名,值是路径。
json
{
"usingComponents": {
"自定义组件名称": "自定义组件的路径",
"navigation-bar": "./navigation-bar/index"
}
}易错点:转写稿中特意对比了新建 Page 与 Component:Page 的入口是 Page,Component 的入口是 Component;新建 Page 还会自动注册路由,删页面时要手动删路由。全局注册写在 app.json,局部注册写在页面 .json;全局路径常以 / 开头,局部可用 ./ 相对路径。
组件样式:默认隔离,修改要有“入口”
为什么需要:组件封装后,页面样式不应随便污染组件内部;但业务又常要改组件外观。怎么想:小程序页面和组件样式默认隔离,所以页面直接写组件内部类名通常不生效。怎么做:方案一在组件 Component 中设置 options.addGlobalClass: true,允许页面样式影响组件,但页面必须使用组件本身的类名;方案二用 externalClasses 声明外部样式类,把父级类名传入组件结构。
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>易错点:转写稿调试时,页面改组件背景色不生效,开启 addGlobalClass 后生效但被组件内部样式权重覆盖,需要提高权重(如 !important)。组件内定义样式不要用标签选择器、ID 选择器或属性选择器;标签选择器会影响页面同类标签。外部样式类可理解为“特殊 props”:先在 externalClasses 声明坑位,再在组件结构类名中占位,父级传类名进来。
slot:先占位,再决定单槽还是多槽
为什么需要:组件结构固定、内容可变时,需要把父级内容塞进组件内部。怎么想:<slot /> 就是占位;默认只能一个,多个插槽要开启多插槽并用 name 区分。怎么做:组件内写 <slot />;多插槽设置 options: { multipleSlots: true },不同 <slot name="title" /> 用 name 区分;使用时单插槽直接在组件中间填内容,多插槽用 slot 属性指定位置。易错点:忘记开启 multipleSlots 却写多个插槽;使用多插槽时没写 slot 属性导致内容不知道落到哪。
生命周期:created 与 attached 的时机
为什么需要:组件创建、初始化完成时需要执行不同逻辑。怎么想:生命周期写在 lifetimes 中,不要和旧写法混淆。怎么做:
javascript
Component({
lifetimes: {
created() {
// code...
},
attached() {
// code...
}
}
})易错点:created 类似 Vue 的 created,组件创建时触发,不能调用 this.setData;attached 类似 mounted,组件初始化完毕时触发,实际开发使用最频繁。
组件通信:properties 收数据,triggerEvent 发事件
为什么需要:父页面要把数据传给子组件,子组件也要把状态变化通知外部。怎么想:父传子用自定义属性 properties;子传父用自定义事件。怎么做:
javascript
Component({
properties: {
back: Boolean,
delta: {
type: Number,
value: 1
}
}
})xml
<navigation-bar back delta="1">
<text slot="title">自定义标题</text>
</navigation-bar>事件监听写 bind:事件类型="事件回调",触发写 this.triggerEvent('事件类型', 参数)。倒计时示例中,attached 调 countStart,每次 setData 后 triggerEvent('change', this.data.time) 把剩余时间传出。易错点:properties 的 type/value 要写清;事件名要和 bind: 后一致;定时器逻辑注意终止条件。
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;
}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)
},
},
})Vant 组件库:安装后必须构建
为什么需要:常用 UI 不必从零封装。怎么想:注意 Vant 有 Vue 版和小程序版,别装错。怎么做:根目录安装 npm install @vant/weapp;在小程序开发者工具中构建 npm,生成 miniprogram_npm;移除全局配置 "style": "v2",否则样式受影响;在 app.json 全局注册 Vant 组件,组件路径不以 / 或 ./ 开头。
bash
npm install @vant/weapp易错点:只安装不构建,组件找不到;忘记移除 "style": "v2";路径写法不符合 Vant 注册要求。示例按钮区域样式保留如下:
css
.buttons {
display: flex;
justify-content: space-around;
padding: 20rpx;
margin: 20rpx;
border-radius: 10rpx;
background-color: #fff;
}课件图示




代码示例
示例一:分包加载与分包预下载(app.json)
json
{
"pages": [
"pages/index/index",
"pages/framework/index"
],
"subPackages": [
{
"root": "subpkg_test",
"pages": [
"pages/test/index"
]
},
{
"root": "subpkg_user",
"pages": [
"pages/profile/index"
]
}
],
"preloadRule": {
"pages/framework/index": {
"network": "all",
"packages": ["subpkg_user"]
}
}
}关键行与易错点
subPackages(也可写subpackages)是数组,每一项用root指定分包根目录、pages指定该分包内的页面路径;root和页面路径不存在时,开发者工具会按配置自动创建目录和文件。- 分包页面的真实路径 =
root+pages中的路径,例如subpkg_user/pages/profile/index;跳转时依旧按完整路径写,和普通页面没有区别。 root不要写成/subpkg_user这种以斜杠开头的形式;pages里也不要再重复写root。pages数组(主包)里不能出现分包页面;tabBar配置的页面必须放在主包,不允许分包。preloadRule与subPackages同级,它的 key 是页面路径,value 中network可取all/wifi(默认wifi),packages是数组,可一次预下载多个分包。- 主包体积建议不超过 1.5M、单个包不超过 2M、所有分包总和不超过 20M;超出 2M 会直接上传失败(报错信息中会提示
max limit)。 - 该文件是严格 JSON:不能写注释、键和值必须用双引号、末尾不能有逗号,语法错误会导致整个页面白屏。
示例二:自定义导航栏组件(含样式隔离与外部样式类)
components/navigation-bar/index.json
json
{
"component": true,
"usingComponents": {}
}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
}
},
lifetimes: {
created() {
// 组件创建时触发,此时不能调用 this.setData
},
attached() {
// 组件初始化完毕时触发,最常用
}
},
methods: {
onBack() {
// 点击返回的处理逻辑
}
}
})components/navigation-bar/index.wxml
xml
<view class="navigation-bar custom-class">
<view wx:if="{{back}}" class="navigation-bar-back" bindtap="onBack"></view>
<view class="navigation-bar-title title-class">
<slot name="title">自定义标题</slot>
</view>
</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;
}页面中使用(pages/framework/index.json / .wxml / .wxss)
json
{
"navigationStyle": "custom",
"usingComponents": {
"navigation-bar": "/components/navigation-bar/index"
}
}xml
<navigation-bar
back
delta="1"
custom-class="my-navigation-bar"
title-class="my-navigation-bar-title"
>
<text slot="title">自定义标题</text>
</navigation-bar>css
.my-navigation-bar {
background-color: gold;
}
.my-navigation-bar-title {
color: #fff !important;
}关键行与易错点
- 组件同样是 4 个文件,但有两处必须改:
.json中必须有"component": true,.js中调用的是Component()而不是Page()。用工具右键新建时一定要选「新建 Component」,选错成「新建 Page」还会自动往app.json里写路由,删文件后要手动删路由。 - 注册组件用
usingComponents,键是自定义组件名、值是路径;全局注册写在app.json中(路径建议以/开头),局部注册写在页面.json中(可用./相对路径)。注册后即可当标签使用,无需 import。 - 干掉默认导航栏用页面配置
"navigationStyle": "custom",它是页面的一级配置,别写成嵌套;一般只按页面关闭,不建议全局关闭。 - 组件样式默认是隔离的,页面里写类名改不动组件内部样式。两种方案:①
options: { addGlobalClass: true },但必须使用组件本身的类名,且权重不够时要加!important;②externalClasses: ['custom-class', 'title-class'],在组件内把类名当作「坑位」写在class上,外部通过同名属性传类名进来。 - 组件内部写样式时不要用标签选择器、ID 选择器、属性选择器,标签选择器会影响到页面中所有同类标签。
- 多插槽必须
options: { multipleSlots: true },并且<slot name="title">与使用方的slot="title"对应;只有一个插槽时直接在组件中间填充内容即可。 created中不能调用this.setData;数据初始化完毕后的逻辑放在attached中。
示例三:倒计时组件与自定义事件通信
components/count-down/index.json
json
{
"component": true
}components/count-down/index.js
js
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)
}
}
})components/count-down/index.wxml
xml
<view class="count-down custom-class">
<slot wx:if="{{useSlot}}"></slot>
<text wx:else>{{time}}s</text>
</view>页面中使用
json
{
"usingComponents": {
"count-down": "/components/count-down/index"
}
}xml
<count-down time="10" custom-class="my-count-down" bind:change="onTimeChange"></count-down>js
Page({
onTimeChange(e) {
// 自定义事件通过 e.detail 拿到 triggerEvent 传入的参数
console.log('剩余时间:', e.detail)
}
})关键行与易错点
- 父传子用
properties:back: Boolean是简写,完整写法是{ type, value };WXML 上传delta="1"拿到的是数字类型,由type: Number完成转换,不声明type会拿到字符串。 - 子传父用
this.triggerEvent('change', 参数)触发,父组件用bind:change="回调"监听;参数在回调的e.detail中,不是e.currentTarget。 - 事件名是自定义的,
bind:后面必须与triggerEvent的第一个参数完全一致。 countStart中用setTimeout递归并bind(this)保证this指向组件实例;this.data.time === 0时提前return停止计时,否则会无限循环。- 外部样式类
custom-class既要在externalClasses中声明,也要在组件 WXML 的class中占位,缺一不可,否则外部传入的类名不生效。
常见错误
1. 分包页面跳转报 “Page is not found” 现象:subPackages 里已经配好 packages_house,点击跳转却提示页面不存在。 原因:navigateTo 的 url 只写了 pages/list/index,漏掉了分包的 root。 修法:写全路径 /packages_house/pages/list/index,从开发者工具底部“复制路径”拿到的才是完整路径。
2. 把 tabBar 页面挪进分包后编译报错 现象:配置完分包,工具直接报 tabBar 相关错误。 原因:tabBar 页面必须是主包页面,这是平台限制,不是配置写错。 修法:把该页面移回主包 pages 中,只把二级页面(列表、详情、表单)分出去。
3. 自定义组件被当成页面渲染 / 报 “Component is not found” 现象:组件放进页面后不显示,或者控制台提示找不到组件。 原因:组件的 .json 里少了 "component": true,四个文件不会被识别为一个组件。 修法:补上该配置;同时确认 .js 里用的是 Component({}) 而不是 Page({})——用 Page 注册时 properties 收不到值、attached 也不会按预期执行。
4. 在页面里改组件内部样式,怎么都不生效 现象:审查元素看到类名了,页面 wxss 里写了 background-color 却没反应。 原因:组件样式默认隔离,外部样式进不去;即使开了 addGlobalClass,还可能因为权重不够被组件内部样式覆盖。 修法:组件 options 里加 addGlobalClass: true,页面侧必要时加 !important 或提高选择器权重;更推荐用 externalClasses 从外部传类名进去。
5. 组件内写了标签选择器,把整个页面搞乱 现象:组件里写了 view { ... },结果页面里所有 view 都变了样。 原因:组件内使用标签选择器会作用到页面上的同类标签,即使开了样式隔离也一样。 修法:组件内一律用类选择器,禁用标签、ID、属性选择器。
6. 多插槽内容全塞进第一个位置 现象:写了两个 <slot name="title"> 和 <slot name="right">,填充的内容却都跑到第一处。 原因:没有开启多插槽。 修法:组件 options 里加 multipleSlots: true,使用侧用 slot="title" 显式指定。
7. 在 created 里调用 setData 报错 现象:组件一创建就报 setData 相关错误,数据没渲染。 原因:created 阶段组件实例还没初始化完,不能 setData,也不能操作节点。 修法:数据初始化、请求、定时器统一放到 attached;created 只做纯 JS 层面的赋值。
8. 布尔属性传 back="false" 结果组件当成 true 现象:<navigation-bar back="false"> 里的返回箭头还是显示出来了。 原因:属性值没加 {{}} 时是字符串 "false",转布尔恒为真。 修法:写 back="{{false}}";数字同理写 delta="{{2}}"。
9. 生命周期写在页面里不触发 现象:onLaunch、onShow 写在某个页面的 js 里,启动时完全没打印。 原因:应用生命周期的注册入口是 App({}),位置固定在 app.js,页面里写不生效。 修法:挪到 app.js 的 App() 中;页面级逻辑仍写在 Page() 里。
10. Vant 组件引入后样式错乱或路径找不到 现象:usingComponents 写了 @vant/weapp/button/index,编译提示找不到组件。 原因:npm 包没构建,或者全局 "style": "v2" 还在,导致样式异常。 修法:先“构建 npm”生成 miniprogram_npm,删掉 app.json 里的 "style": "v2",路径写 @vant/weapp/button/index(不带 ./ 或 /)。
练习
练习 1|按模块拆分主包 导入第三天的 MP demo(当前 3.3M,无法上传),按业务模块拆出至少两个分包(如 packages_house、packages_repair),把对应二级页面和大图资源分别移入各自分包。要求:主包体积降到 1.5M 以下,从首页能正常 navigateTo 到两个分包的列表页,并截图“详情 → 代码依赖分析”前后对比。
练习 2|分包预加载配置 在练习 1 的基础上,为首页 pages/index/index 配置 preloadRule,network 设为 all,同时预加载两个分包。打开首页后在控制台确认预加载成功日志;再把 network 改回默认(仅 wifi),切换到“3G”网络模拟环境,对比两次控制台输出的差异并说明原因。
练习 3|封装 navigation-bar 组件 封装一个导航栏组件并全局注册,要求:支持 back(布尔,默认 false)、delta(数字,默认 1)两个属性;提供名为 title 的具名插槽;点击返回区域时调用 wx.navigateBack({ delta })。在页面 json 中关闭默认导航栏后使用它,并在两个不同页面中复用,验证全局注册与页面局部注册的差别。
练习 4|用 externalClasses 定制倒计时按钮 封装一个倒计时按钮组件:attached 时开始每秒递减,剩余秒数通过 triggerEvent('change', time) 抛给页面;通过 externalClasses 暴露 custom-class,让页面从外部改变按钮背景色和圆角。要求页面中同时放两个该组件,传入不同的外部类名,验证样式互不影响。
练习 5|应用更新提示 在 app.js 的 onLaunch 中接入 wx.getUpdateManager():检测到新版本 → 下载完成 → wx.showModal 提示“是否重启应用新版本”,用户点确定则 applyUpdate()。用开发者工具“编译设置 → 下一次编译时模拟更新”验证全流程,并额外打印 options.scene 与 options.query,确认扫码进入时的场景值和启动参数。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 反馈-复习-学习目标 | 19:59 |
| 2 | 生命周期-应用生命周期 | 13:46 |
| 3 | 生命周期-更新应用提示 | 09:39 |
| 4 | 分包加载-分包配置 | 16:45 |
| 5 | 分包加载-分包预下载 | 08:25 |
| 6 | 自定义组件-基本用法 | 13:44 |
| 7 | 自定义组件-自定义导航栏 | 08:11 |
| 8 | 自定义组件-修改组件样式 | 13:10 |
小结
- 分包拆不拆,看的是“首屏要不要”,不是“放不放得下”。 2M 是硬上限(超了连预览都发不出去),1.5M 才是体验红线,先拆大资源再拆页面。
- tabBar 是分包的唯一硬禁区,其余二级页面按业务模块整块搬走即可,跳转路径只是多了
root前缀,写法没有本质变化。 - 预加载是拿流量换速度的赌注,只在用户大概率会去的入口页配,配得越宽,首屏被拖慢的风险越大。
- 组件默认样式隔离是保护而非障碍。 改样式的优先级应当是:
externalClasses传类名 >addGlobalClass+ 权重,前者可控、后者容易污染。 - 组件内禁止标签选择器,这条比“记得开样式隔离”更容易踩,也更容易造成全局污染。
- 组件通信只有两条主干道:
properties下行、triggerEvent上行。数据一复杂就该考虑拆组件而不是加属性。 - 生命周期选时机的判断标准很简单:需要
setData或拿实例的,一律放attached;created只做纯逻辑。 - 组件库是别人的组件,引入前先确认“构建 npm + 移除
style: v2”这两步,再评估样式覆盖成本,别等到页面写完了才发现改不动。