一、为什么会出现小程序?
App太重,H5网页太糙
小程序是 App 和 H5 网页之间的“黄金平衡点”。它既保留了 App 流畅的体验和强大的功能,又拥有了 H5 网页无需下载、极其轻便的优势。
App、H5、小程序对比
| 对比维度 | 原生App | H5网页 | 小程序 |
|---|---|---|---|
| 获取与安装 | 太重:需去应用商店搜索、下载几百兆安装包、注册登录,门槛极高。 | 最轻:无需下载,复制个链接就能打开。 | 适中:无需下载安装,扫码或微信搜索“即用即走”,不占手机内存。 |
| 性能与流畅度 | 极佳:直接调用手机系统底层硬件,滑动丝滑,加载极快。 | 较差:依赖浏览器渲染,每次打开都要重新加载,复杂动效容易卡顿。 | 较好:体验接近原生 App,加载快、流畅度高,告别了 H5 的卡顿感。 |
| 系统权限与功能 | 无限制:能调用手机所有底层功能(如后台常驻、复杂蓝牙等)。 | 极受限:几乎没有系统权限,无法调用摄像头、蓝牙等高级硬件功能。 | 较丰富:依托微信生态,能调用支付、定位、扫码等常用底层 API,满足 90% 业务需求。 |
| 开发与维护成本 | 极高:需 iOS 和安卓两套代码,开发周期长,每次更新都要重新提交应用商店审核。 | 极低:一套代码多端运行,修改服务器代码即可,无需审核,迭代极快。 | 适中:一套代码(如 uni-app)即可生成,开发门槛较低,更新只需提交微信审核,非常便捷。 |
| 传播与分享 | 困难:用户很难主动去分享一个 App,获客成本高昂。 | 容易:链接可随意发到任何聊天软件或朋友圈,但容易流失。 | 极佳:背靠微信庞大流量,可一键转发给好友或微信群,裂变传播极快。 |
| 用户留存与触达 | 强:图标常驻桌面,可通过系统推送随时唤醒沉睡用户。 | 极弱:关闭网页就找不到了,无法主动触达用户。 | 中等:用完即走导致留存较弱,但可通过微信订阅消息在限定时间内触达用户。 |
二、小程序生命周期
1. 应用级别生命周期(App 级)
| 生命周期函数 | 触发时机 | 核心作用与实战场景 | Vue3 对标 |
|---|---|---|---|
onLaunch | 小程序首次启动时触发,全局仅执行 1 次 | 全局初始化:检查登录状态、获取用户信息、初始化全局配置(如globalData)。 | App 初始化 |
onShow | 小程序启动或从后台切回前台时触发,可多次执行 | 全局状态刷新:检查 Token 是否过期、恢复全局音频/视频播放、埋点统计。 | App activated |
onHide | 小程序从前台切到后台时触发 | 全局资源释放:保存临时表单草稿、暂停全局定时器、停止音乐播放。 | App deactivated |
2. 页面级别生命周期(Page 级)
| 生命周期函数 | 触发时机 | 核心作用与实战场景 | Vue3 对标 |
|---|---|---|---|
onLoad | 页面首次加载时触发,仅执行 1 次 | 接收参数与初始请求:接收页面跳转传来的参数(options),发起核心的接口请求(如获取商品详情)。 | onMounted(初始化) |
onShow | 页面显示或从后台切回前台时触发,可多次执行 | 数据动态刷新:比如从详情页返回列表页时,刷新列表的未读消息数或最新状态。 | onActivated |
onReady | 页面初次渲染完成时触发,仅执行 1 次 | DOM 操作与动画:获取节点信息(如获取元素宽高)、启动页面入场动画、初始化地图/图表组件。 | onMounted |
onHide | 页面被隐藏或切到后台时触发 | 暂停与保存:暂停当前页面的视频播放、停止页面专属的倒计时器、保存表单进度。 | onDeactivated |
onUnload | 页面被卸载(关闭)时触发 | 彻底清理:清除定时器(防止内存泄漏)、取消未完成的网络请求、解绑事件监听。 | onUnmounted |
3. 组件级别生命周期(Component 级)
| 核心阶段 | uni-app(Vue3)生命周期 | 原生小程序生命周期 | 触发时机与核心作用 | 实战开发场景 |
|---|---|---|---|---|
| 创建阶段 | setup | created | 实例刚创建,数据初始化。 此时组件实例刚刚被创建,视图层(DOM)还未生成。 | 初始化组件内部状态、定义响应式变量、接收父组件传参。注意:不能操作 DOM。 |
| 挂载阶段 | onMounted | attached | 进入节点树,DOM 准备就绪。 组件实例进入页面节点树,视图层已渲染完毕。 | 最常用的初始化时机。发起组件自身的网络请求、初始化第三方图表(如 ECharts)、获取 DOM 节点宽高。 |
| 布局完成 | (Vue3 无直接对应) | ready | 视图层布局完成。 在 attached之后触发,此时可以获取节点信息。 | 在原生小程序中,若需要精确获取组件在屏幕上的位置或宽高,放在这里最稳妥。 |
| 更新阶段 | onUpdated | (原生无直接对应) | 数据变更,视图重新渲染后。 响应式数据更新导致 DOM 重新渲染后触发。 | 在数据更新后,执行依赖最新 DOM 的操作(如:列表渲染完后自动滚动到底部、重新计算长列表高度)。 |
| 销毁阶段 | onUnmounted | detached | 离开节点树,组件销毁。 组件被从页面节点树移除时触发。 | 极其重要的清理时机。清除定时器(clearInterval)、解绑全局事件监听、断开 WebSocket,防止内存泄漏。 |
三、小程序生命周期流程
1. 应用级生命周期流程(App.vue)
应用级生命周期负责管理整个小程序从出生到死亡的全局状态。
- 启动与初始化(
onLaunch):当用户首次打开小程序时触发,全局仅执行 1 次。通常在这里做全局配置、检查登录状态等。 - 进入前台(
onShow):小程序启动,或者从后台切回前台时触发(可多次执行)。适合做全局状态刷新、恢复播放等。 - 进入后台(
onHide):当用户点击右上角胶囊关闭、切回微信聊天界面或手机锁屏时触发。适合做数据保存、暂停全局定时器等“善后”工作。 - 销毁(后台机制):小程序进入后台后,如果超过一定时间(如 10 分钟)未切回前台,或者系统内存不足,小程序会被系统彻底销毁。下次再打开时,就会重新走一遍
onLaunch(冷启动)。
2. 页面级生命周期流程(Page 级)
页面级生命周期是日常开发中最常用的,它管理单个页面的生老病死。
- 页面加载(
onLoad):页面首次加载时触发,仅执行 1 次。主要用于接收路由参数和发起核心网络请求。 - 页面显示(
onShow):页面显示/切入前台时触发,可多次执行。每次从其他页面返回,或从后台切回前台都会触发,适合刷新页面数据。 - 初次渲染完成(
onReady):页面初次渲染完成时触发,仅执行 1 次。此时 DOM 节点已经准备好,适合获取元素宽高或初始化图表。 - 页面隐藏(
onHide):页面被隐藏(如navigateTo到下一个页面,或切到后台)时触发。适合暂停当前页面的视频或定时器。 - 页面卸载(
onUnload):页面被销毁(如navigateBack返回上一页,或redirectTo重定向)时触发。必须在这里清除定时器和事件监听,防止内存泄漏。
四、小程序登录流程(判断登录是否过期)
1. 小程序登录时序
- 小程序客户端调用
uni.login,向微信服务器发起获取临时凭证的请求。 - 微信服务器验证通过后,向小程序客户端返回临时凭证(
code)。 - 小程序客户端通过自定义网络请求,将临时凭证(
code)发送至开发者服务器。 - 开发者服务器将临时凭证(
code)、AppID 和 AppSecret 发送至微信服务器。 - 微信服务器验证通过后,向开发者服务器返回用户唯一标识(
openid)和会话密钥(session_key)。 - 开发者服务器生成自定义登录态(
Token)。 - 开发者服务器将自定义登录态(
Token)返回给小程序客户端。 - 小程序客户端将
Token存储至本地。
<script setup> import { onLaunch } from '@dcloudio/uni-app'; onLaunch(() => { console.log('App 启动,开始静默登录...'); // 1. 调用 uni.login 获取临时 code uni.login({ provider: 'weixin', // 指定微信登录 success: async (loginRes) => { if (loginRes.code) { // 2. 将 code 发送给咱们自己的后端 const res = await uni.request({ url: 'https://your-api.com/api/login', // 替换为你的后端接口 method: 'POST', data: { code: loginRes.code } }); // 3. 接收后端返回的 Token,并存储在本地 if (res.data.token) { uni.setStorageSync('user_token', res.data.token); console.log('登录成功,Token已保存'); } } }, fail: (err) => { console.error('微信登录失败', err); } }); }); </script>2. 登录过期判断
校验微信底层登录态(session_key)
微信的session_key具有一定的时效性,微信官方并没有公开具体的过期时间,而是根据用户使用小程序的行为动态续期。为了判断其是否过期,开发者需要使用微信提供的检测接口:
- 前端校验:通过调用
wx.checkSession接口来检测当前用户的微信登录态是否有效。如果接口调用成功,说明session_key未过期;如果调用失败,则说明已经失效,需要重新执行wx.login登录流程。 - 注意事项:由于
wx.checkSession接口平均耗时约 200ms,如果在每次发起网络请求前都进行校验,会产生较大的性能开销。因此,不建议在每次请求前都调用此接口。
校验开发者服务器登录态(自定义 Token)
除了微信底层的登录态,开发者服务器自身维护的登录态(Token)也存在过期的可能。在实际业务中,推荐采用以下策略来判断:
- 接口调用前:仅校验前端本地是否存在有效的
Token,不校验后端及微信的登录态,以节省每次请求的校验开销。 - 接口调用后:在获取到后端的响应数据后进行解析。如果后端返回了与登录态相关的错误码(例如 HTTP 401 状态码),则说明后端的
Token已经失效。此时前端应重置本地的登录态,并自动触发重新登录流程,然后重新发起业务请求。
五、小程序支付流程
- 小程序前端调用
uni.login,向微信服务器获取用户的openid。 - 小程序前端将订单信息及
openid发送至开发者后端服务器。 - 开发者后端服务器调用微信支付统一下单接口,向微信服务器请求生成预付单。
- 微信服务器返回预支付交易会话标识(
prepay_id)给开发者后端服务器。 - 开发者后端服务器对支付参数进行二次签名,并将结果返回给小程序前端。
- 小程序前端调用
uni.requestPayment,向微信客户端发起调起支付的请求。 - 用户在微信客户端完成验密及支付授权。
- 微信服务器向开发者后端服务器异步推送支付成功通知。
- 微信客户端向小程序前端返回支付结果回调。
- 小程序前端根据回调结果向用户展示支付状态。
<template> <view class="pay-page"> <button class="pay-btn" :disabled="isPaying" @click="handlePay"> {{ isPaying ? '支付处理中...' : '立即支付' }} </button> </view> </template> <script setup> import { ref } from 'vue'; // 1. 定义防抖状态,防止用户手抖连续点击导致重复支付 const isPaying = ref(false); // 2. 发起支付的主流程 const handlePay = async () => { // 如果正在支付,直接拦截 if (isPaying.value) return; isPaying.value = true; try { // 【第一步】向你的后端请求支付参数 // 实际项目中,这里通常还会先调用创建订单的接口拿到 orderId const payParams = await requestPayParams('ORDER_20240526001'); // 【第二步】调起微信支付 await wxPay(payParams); // 【第三步】支付动作完成,跳转到支付结果页 // 注意:这里的 success 只代表用户完成了支付动作,真正的结果以后端为准 uni.redirectTo({ url: '/pages/pay/result?status=success&orderId=ORDER_20240526001' }); } catch (err) { console.error('支付流程异常:', err); // 【第四步】异常兜底处理 if (err.errMsg && err.errMsg.includes('cancel')) { // 用户在支付密码框点击了“取消” uni.showToast({ title: '支付已取消', icon: 'none' }); } else if (err.code === 'ORDER_EXPIRED') { // 订单超时等后端自定义错误 uni.showModal({ title: '提示', content: '当前订单已超过支付时效,请重新下单', showCancel: false }); } else { // 其他未知错误 uni.showToast({ title: '支付失败,请重试', icon: 'none' }); } } finally { // 无论成功失败,解除按钮禁用状态 isPaying.value = false; } }; // 3. 封装:请求后端获取支付参数 const requestPayParams = (orderId) => { return new Promise((resolve, reject) => { uni.request({ url: 'https://your-api.com/api/pay/create', // 替换为你的后端接口 method: 'POST', data: { orderId }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); // 返回后端生成的支付参数对象 } else { reject(res.data); } }, fail: reject }); }); }; // 4. 封装:调起微信支付 const wxPay = (payParams) => { return new Promise((resolve, reject) => { uni.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, // 格式必须是 prepay_id=*** signType: payParams.signType, // V3接口通常为 RSA,V2接口为 MD5 paySign: payParams.paySign, success: (res) => { console.log('支付动作完成:', res); resolve(res); }, fail: (err) => { reject(err); } }); }); }; </script>六、小程序路由跳转
| 路由方法 | 核心参数 | 说明 | 页面栈的变化 |
|---|---|---|---|
wx.navigateTo | url(必填) | 保留当前页面,跳转到应用内的某个非 tabBar 页面。可以使用wx.navigateBack返回到原页面。 | 新页面入栈。当前页面被隐藏(触发onHide),新页面被压入栈顶(触发onLoad、onShow)。页面栈深度加 1。 |
wx.redirectTo | url(必填) | 关闭当前页面,跳转到应用内的某个非 tabBar 页面。不允许返回到原页面。 | 当前页面出栈,新页面入栈。相当于“替换”了当前页面。原页面被卸载(触发onUnload),页面栈深度保持不变。 |
wx.switchTab | url(必填) | 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。不能携带参数。 | 页面全部出栈,只留下新的 Tab 页面。页面栈被清空,仅包含目标 tabBar 页面。 |
wx.reLaunch | url(必填) | 关闭所有页面,打开到应用内的某个页面(任意页面均可)。 | 页面全部出栈,只留下新的页面。相当于强制重启小程序到指定页面。 |
wx.navigateBack | delta(选填) | 关闭当前页面,返回上一页面或多级页面。delta表示返回的层数,默认为 1。 | 页面不断出栈,直到目标返回页。页面栈深度减少delta的值。若返回到栈底则停止。 |
- 页面栈上限:小程序的页面栈通常最多只有 10 层。如果频繁使用
wx.navigateTo而不及时返回或重定向,很容易导致页面栈溢出,之后再调用navigateTo会失效。因此在长流程(如电商下单、表单填写)的终点,建议使用redirectTo或reLaunch。 - API 使用限制:
wx.navigateTo和wx.redirectTo只能打开非 tabBar 页面,而wx.switchTab只能打开 tabBar 页面。如果路径配置错误,API 会调用失败。 - 不要直接修改页面栈:虽然可以通过
getCurrentPages()获取当前页面栈实例,但千万不要尝试直接修改这个数组,否则会导致路由和页面状态出现严重错误。 - 路由参数获取:通过路由跳转携带的参数(如 url 中拼接的
?id=123),可以在目标页面的onLoad(options)生命周期函数中直接通过options对象获取。
七、小程序的实现原理
1. Web 单线程的执行机制(传统 H5 网页)
传统网页的渲染和 JS 逻辑是互斥的单线程模型。
- 执行机制:浏览器中,JavaScript 引擎线程(处理逻辑)和 GUI 渲染线程(处理页面显示)是互斥的。同一时刻只能执行一个任务,如果 JS 执行了耗时的计算或死循环,页面渲染就会被阻塞,导致用户感觉“页面卡死”。
- 为什么设计成单线程:主要是为了避免复杂的并发问题。如果允许多线程同时操作 DOM(比如一个线程在修改按钮颜色,另一个线程在删除按钮),会导致不可预测的渲染混乱和竞态条件。
- 痛点:复杂的 JS 运算会直接抢占 UI 渲染的资源,导致页面卡顿、掉帧。
2. 小程序双线程的执行机制
为了解决 Web 单线程的卡顿问题,并满足微信的安全管控需求,小程序采用了逻辑层与视图层分离的双线程模型。
- 逻辑层(App Service):运行在独立的 JavaScript 引擎中(iOS 用 JavaScriptCore,Android 用 V8 等)。它负责处理所有的 JS 业务逻辑、数据请求、API 调用等。
- 视图层(View Layer):运行在 WebView 线程中。它负责解析 WXML 和 WXSS,进行页面的 UI 渲染。一个小程序有多个页面,就会有多个 WebView 线程。
- 核心优势:
- 性能隔离:即使逻辑层在进行极其复杂的数据计算,也不会阻塞视图层的渲染。用户的滑动、点击依然能保持丝滑流畅。
- 安全管控:逻辑层是一个纯粹的 JS 沙箱环境,没有
window、document对象,开发者无法直接操作 DOM。这从根源上防止了恶意脚本攻击和页面篡改。
3. 小程序实现的底层原理(Native Bridge 通信)
既然逻辑层和视图层是两个独立的线程,它们之间是如何协同工作的呢?这就涉及到了小程序的底层通信原理。
- Native 层中转(JSBridge):这两个线程之间是完全隔离、不共享内存的。它们之间的任何数据传递和事件触发,都必须通过宿主环境(微信客户端的 Native 层)作为桥梁进行异步转发。
- 数据驱动视图(
setData原理):- 当逻辑层数据发生变化时,调用
setData。 - 框架将数据序列化为字符串(JSON),通过 Native 层转发给视图层。
- 视图层接收到数据后,进行反序列化,利用虚拟 DOM(Virtual DOM)机制进行 Diff 算法对比,最后将差异应用到真实的 DOM 树上完成渲染。
- 当逻辑层数据发生变化时,调用
- 事件触发流程:用户在界面点击按钮(视图层),WebView 拦截事件,将事件信息序列化后通过 Native 层传递给逻辑层,逻辑层找到对应的 JS 事件处理函数执行。
八、小程序启动与销毁
1. 小程序的两种启动方式
小程序的启动主要分为两种状态:
- 冷启动(Cold Start):当用户首次打开小程序,或者小程序被系统销毁后再次打开时,就会触发冷启动。此时,小程序需要重新加载全部资源并执行初始化(触发
onLaunch),耗时相对较长。 - 热启动(Hot Start):如果用户之前打开过小程序,但在一定时间内再次打开,此时小程序并未被销毁,只是从“后台”状态切换回“前台”状态。这个过程就是热启动,它无需重新加载资源,几乎是瞬间恢复。
2. 小程序销毁机制(什么情况下会销毁?)
当用户点击小程序右上角的“关闭”按钮,或者按 Home 键离开时,小程序并不会立即被销毁,而是进入了“后台”状态。只有在满足以下特定条件时,小程序才会被彻底销毁(下次打开即为冷启动):
- ① 后台停留时间过长:小程序进入后台后,如果长时间没有再次切回前台,系统会主动将其销毁。不同平台的判定时间有所不同:
- 抖音小程序:后台停留超过 5 分钟会被主动销毁。
- B站小程序:进入后台并被挂起后,超过 30 分钟未进入前台会被销毁。
- 其他平台:部分平台(如涂鸦小程序)的后台存活时间可能长达 10 分钟。
- ② 系统资源紧张(内存告急):当手机系统内存不足时,宿主应用(如微信、抖音)会主动回收后台的小程序。例如,当客户端连续收到系统内存告警时,会根据策略主动销毁后台的小程序,以保证宿主 App 的正常运行。
- ③ 后台小程序数量达到上限:系统对同时存在于后台的小程序数量是有上限的。例如,在 iOS 系统上,最多允许有 5 个小程序同时存在。如果用户打开了第 6 个小程序,那么最早进入后台的那一个就会被强制销毁。
- ④ 业务异常或主动配置(直接销毁,不进入缓存):在某些特殊场景下,小程序关闭时会直接销毁,不进行内存缓存:
- 页面异常:如果用户关闭小程序时页面处于白屏状态,系统会直接销毁,避免下次打开时恢复异常页面。
- 流程未结束:如果关闭时小程序仍有启动流程或网络请求在执行中,会直接销毁。
- 主动禁用缓存:开发者可以在全局配置(如
disableCache: true)中强制设置退出时销毁小程序,确保每次打开都是全新的冷启动。 - 非正式版本:开发版、体验版等非正式版本的小程序,通常不做内存缓存,关闭即销毁。
九、小程序的渲染流程
1. 启动与初始化阶段
当用户点击小程序图标时,宿主环境(如微信客户端)会启动小程序进程,并同步初始化两个核心线程:
- 渲染线程(视图层):初始化 WebView 环境,加载基础库和渲染框架,准备接收渲染指令。
- 逻辑线程(逻辑层):加载基础库,执行
app.js创建 App 实例,并触发App.onLaunch和App.onShow生命周期。
2. 页面加载与数据准备阶段
初始化完成后,系统开始加载具体的页面:
- 逻辑层执行:加载入口页面对应的
page.js,创建 Page 实例,收集初始化数据(initData),并依次触发页面的onLoad和onShow生命周期。 - 数据下发:逻辑层将准备好的
initData序列化后,通过 Native 层(JSBridge)派发给渲染线程。
3. 首次渲染阶段(构建视图树)
渲染线程接收到初始数据后,开始执行真正的渲染工作:
- 解析与编译:解析页面的 WXML 模板和 WXSS 样式文件,将其转换为对应的节点树和样式规则。
- 构建视图树:将节点树与样式规则合并,生成最终的“视图树”(Render Tree),仅包含可见节点。
- 首次绘制:将视图树交由底层图形系统进行绘制,完成页面的首次内容绘制(FCP)和首次有意义绘制(FMP)。渲染完成后,会通知逻辑层触发
onReady生命周期。
4. 运行时更新阶段(数据驱动视图)
在页面展示后,用户的交互或网络请求会触发数据更新,进而引发重新渲染:
- 触发更新:开发者调用
setData修改数据。逻辑层会对新旧数据进行 Diff 对比,找出发生变化的部分。 - 跨线程通信:逻辑层将变化的数据序列化为消息,通过 Bridge 异步发送给渲染线程。
- 局部重绘:渲染线程接收消息并反序列化,精准更新对应的 DOM 节点,触发页面的局部重排与重绘,最终通知逻辑层更新完成。
十、小程序提速手段
启动性能优化(打破首屏加载瓶颈)
代码包体积优化(最影响首次打开速度)
- 代码分包:将主包体积严格控制在 2MB 以内。主包只放首页和 TabBar 页面,其他低频页面(如个人中心、活动详情)拆分为子包,按需加载。
- 清理冗余:删除未使用的页面、组件、图片和样式,避免打包多余的第三方库。
提前发出请求与预加载
- 数据预加载:在页面跳转前(如点击列表项时),提前请求下一页的数据并挂载到全局(如
getApp().globalData)。新页面加载时直接使用,可大幅缩短首屏渲染时间。 App.js瘦身:不要在onLaunch中做耗时操作(如复杂的同步计算、同步请求),非必要逻辑可放到首页的onShow或延迟执行。
优化首屏渲染体验
initData预热:在页面初始化时,构造好核心渲染数据的骨架结构。底层框架会提前创建 DOM 元素,待真实数据返回时仅替换内容,无需重新创建元素,节省大量渲染时间。- 骨架屏:在页面加载时展示骨架屏占位,降低用户的等待焦虑。
2. 运行时性能优化(保证交互流畅度)
极致优化setData(最核心的痛点)
- 减少调用频率:严禁在
for循环、高频滚动事件或定时器中调用setData。尽量合并多次数据更新,一次性传给视图层。 - 控制数据量:单次
setData的数据量建议控制在 100KB 以内,避免 JSON 序列化耗时过长。 - 路径更新:使用数据路径语法精确更新(如
'list[0].name': 'newName'),避免整个大对象或大数组的全量刷新。
长列表与渲染优化
- 虚拟列表:对于成百上千条数据的列表,必须使用分页加载或虚拟列表技术,只渲染用户可视区域内的 DOM 节点,防止内存暴涨和滑动卡顿。
- 避免复杂模板逻辑:不要在 WXML 的
{{}}中写复杂的表达式或函数调用。复杂的数据处理应在 JS 层(逻辑层)计算好再传给视图层。
图片与静态资源优化
- 图片懒加载:使用
lazy-load属性,仅加载可视区域内的图片,减少初始网络请求和内存占用。 - WebP 格式与云端化:将图片转换为 WebP 格式(体积可减小 30%~50%),并将大图、封面图全部上传至云端(OSS/CDN),不要放在本地包中。
内存管理(防止闪退)
- 及时清理资源:在页面或组件卸载时(
onUnload/onUnmounted),务必清理定时器(clearInterval)、解绑全局事件监听,防止内存泄漏。 - 控制页面层级:避免连续跳转 5 层以上的页面,不用的页面及时使用
redirectTo关闭,避免页面栈过深占用过多内存。