news 2026/3/11 17:02:47

uni-app——uni-app 小程序 Loading 遮罩卡死页面的排查与最佳实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
uni-app——uni-app 小程序 Loading 遮罩卡死页面的排查与最佳实践

问题现象

测试反馈了一个诡异的问题:

iPhone 上操作审批页面一段时间后,整个页面"卡死"了

  • 按钮点不动
  • 输入框无法输入
  • 下拉框打不开
  • 切换页面也没反应
  • 只能杀掉小程序重新进入
正常状态: 卡死状态: ┌─────────────────────┐ ┌─────────────────────┐ │ [选择预算科目 ▼] │ │ ┌─────────────────┐│ │ │ │ │ Loading... ││ ← 透明遮罩 │ [提交申请] │ ───→ │ │ (已消失) ││ 阻挡所有点击 │ │ │ └─────────────────┘│ │ 可以正常操作 │ │ 无法进行任何操作 │ └─────────────────────┘ └─────────────────────┘

这个问题很难复现,测试也说不清具体是哪个操作导致的,只说"用着用着就卡了"。

问题定位

排查思路

页面"卡死"但没有报错,可能的原因:

  1. JS 死循环→ 会导致页面白屏或明显卡顿,排除
  2. 内存泄漏→ 通常是渐进式变慢,不是突然卡死,排除
  3. 遮罩层未关闭→ 全屏遮罩会阻挡所有点击事件 ✅

定位问题代码

搜索代码中所有showLoading调用,发现了问题:

// 问题代码:打开预算科目选择器constopenBudgetSelector=async()=>{uni.showLoading({title:'加载中',mask:true// 关键:开启全屏遮罩})try{constres=awaitapi.getBudgetSubjects()// 如果这里超时或无响应...budgetList.value=res.data showBudgetPopup.value=true}catch(e){uni.showToast({title:'加载失败',icon:'none'})}uni.hideLoading()// 问题:如果请求一直 pending,这行永远不会执行!}

问题根因

用户点击"选择预算科目" ↓ uni.showLoading({ mask: true }) ← 创建全屏透明遮罩 ↓ await api.getBudgetSubjects() ← 接口请求 ↓ ┌───────────────────────────────────────┐ │ 情况1:接口正常返回 │ │ → hideLoading() 执行 │ │ → 遮罩消失,一切正常 │ ├───────────────────────────────────────┤ │ 情况2:接口超时/无响应 │ │ → Promise 一直 pending │ │ → hideLoading() 永远不执行 │ │ → 遮罩永远存在,页面"卡死" │ └───────────────────────────────────────┘

uni.showLoading({ mask: true })mask参数会创建一个全屏透明遮罩,阻止用户点击页面上的任何元素。这本是为了防止用户在加载过程中重复操作,但如果hideLoading()没有被调用,遮罩就会一直存在。

为什么 catch 没有处理?

try{constres=awaitapi.getBudgetSubjects()// ...}catch(e){uni.showToast({title:'加载失败',icon:'none'})}uni.hideLoading()// 在 try-catch 外部

问题在于:接口无响应不等于接口报错

  • 接口返回 4xx/5xx:会进入 catch,然后执行 hideLoading ✅
  • 接口超时(如果设置了 timeout):会进入 catch,然后执行 hideLoading ✅
  • 接口一直 pending(服务器不响应):不会进入 catch,Promise 永远等待 ❌

解决方案

方案一:使用 finally 确保关闭(基础修复)

constopenBudgetSelector=async()=>{uni.showLoading({title:'加载中',mask:true})try{constres=awaitapi.getBudgetSubjects()budgetList.value=res.data showBudgetPopup.value=true}catch(e){uni.showToast({title:'加载失败',icon:'none'})}finally{// finally 无论成功失败都会执行uni.hideLoading()}}

但这还不够,如果接口一直 pending,finally 也不会执行。

方案二:添加超时机制(推荐)

使用Promise.race实现超时兜底:

// 超时工具函数constwithTimeout=(promise,ms,timeoutError='请求超时')=>{consttimeout=newPromise((_,reject)=>{setTimeout(()=>reject(newError(timeoutError)),ms)})returnPromise.race([promise,timeout])}// 使用超时机制constopenBudgetSelector=async()=>{uni.showLoading({title:'加载中',mask:true})try{// 8秒超时constres=awaitwithTimeout(api.getBudgetSubjects(),8000,'加载超时,请重试')budgetList.value=res.data showBudgetPopup.value=true}catch(e){uni.showToast({title:e.message||'加载失败',icon:'none'})}finally{uni.hideLoading()}}

原理

Promise.race([接口请求, 8秒超时]) ↓ ┌─────────────────────────────────────┐ │ 谁先完成就返回谁的结果 │ │ │ │ • 接口在8秒内返回 → 正常处理 │ │ • 8秒内未返回 → 抛出超时错误 │ │ │ │ 无论哪种情况,finally 都会执行 │ └─────────────────────────────────────┘

方案三:页面卸载时清理(完整方案)

用户可能在请求过程中离开页面,需要在onUnload中清理:

import{ref,onUnmounted}from'vue'import{onUnload}from'@dcloudio/uni-app'// 记录是否有 loadingconstisLoading=ref(false)constopenBudgetSelector=async()=>{isLoading.value=trueuni.showLoading({title:'加载中',mask:true})try{constres=awaitwithTimeout(api.getBudgetSubjects(),8000)budgetList.value=res.data showBudgetPopup.value=true}catch(e){uni.showToast({title:e.message||'加载失败',icon:'none'})}finally{isLoading.value=falseuni.hideLoading()}}// 页面卸载时强制关闭 loadingonUnload(()=>{if(isLoading.value){uni.hideLoading()}})

封装通用的 Loading 管理

为了避免每个页面都重复处理,可以封装一个通用的 Hook:

// hooks/useLoading.tsimport{ref}from'vue'import{onUnload}from'@dcloudio/uni-app'interfaceLoadingOptions{title?:stringmask?:booleantimeout?:number}exportconstuseLoading=()=>{constisLoading=ref(false)// 页面卸载时自动清理onUnload(()=>{if(isLoading.value){uni.hideLoading()isLoading.value=false}})/** * 执行带 Loading 的异步操作 */constwithLoading=async<T>(asyncFn:()=>Promise<T>,options:LoadingOptions={}):Promise<T>=>{const{title='加载中',mask=true,timeout=15000}=options isLoading.value=trueuni.showLoading({title,mask})try{// 超时处理consttimeoutPromise=newPromise<never>((_,reject)=>{setTimeout(()=>reject(newError('请求超时')),timeout)})constresult=awaitPromise.race([asyncFn(),timeoutPromise])returnresult}finally{isLoading.value=falseuni.hideLoading()}}/** * 手动控制 Loading */constshowLoading=(title='加载中',mask=true)=>{isLoading.value=trueuni.showLoading({title,mask})}consthideLoading=()=>{if(isLoading.value){isLoading.value=falseuni.hideLoading()}}return{isLoading,withLoading,showLoading,hideLoading}}

使用示例

<script setup> import { useLoading } from '@/hooks/useLoading' const { withLoading } = useLoading() // 方式一:包装异步函数(推荐) const loadBudgetList = async () => { try { const res = await withLoading( () => api.getBudgetSubjects(), { title: '加载预算科目...', timeout: 8000 } ) budgetList.value = res.data } catch (e) { uni.showToast({ title: e.message, icon: 'none' }) } } // 方式二:手动控制 const { showLoading, hideLoading } = useLoading() const handleSubmit = async () => { showLoading('提交中...') try { await api.submitForm(form) uni.showToast({ title: '提交成功' }) } catch (e) { uni.showToast({ title: '提交失败', icon: 'none' }) } finally { hideLoading() } } </script>

其他需要注意的场景

1. 多个 Loading 并发

// ❌ 问题:第一个 hideLoading 会关闭所有 loadingconstloadData=async()=>{loadList()// showLoadingloadStats()// showLoading// 第一个完成时调用 hideLoading,第二个还在加载但 loading 已消失}// ✅ 解决:使用计数器letloadingCount=0constshowLoading=()=>{loadingCount++if(loadingCount===1){uni.showLoading({title:'加载中',mask:true})}}consthideLoading=()=>{loadingCount--if(loadingCount<=0){loadingCount=0uni.hideLoading()}}

2. Loading 与 Toast 冲突

// ❌ 问题:showToast 会自动关闭 showLoadinguni.showLoading({title:'加载中'})// ... 某处代码uni.showToast({title:'提示信息'})// 这会关闭 loading!// ✅ 解决:使用自定义 Toast 组件,或等 loading 结束再 toast

3. 页面切换时的 Loading

// ❌ 问题:跳转页面后 loading 可能残留consthandleClick=async()=>{uni.showLoading({mask:true})awaitapi.doSomething()uni.navigateTo({url:'/pages/next'})// 跳走了,hideLoading 没执行}// ✅ 解决:跳转前关闭consthandleClick=async()=>{uni.showLoading({mask:true})try{awaitapi.doSomething()uni.hideLoading()// 先关闭uni.navigateTo({url:'/pages/next'})}finally{uni.hideLoading()// 兜底}}

最佳实践清单

Loading 使用规范

┌─────────────────────────────────────────────────────────────┐ │ Loading 最佳实践 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 始终使用 try-finally 确保 hideLoading 执行 │ │ │ │ 2. 为异步操作添加超时机制(推荐 8-15 秒) │ │ │ │ 3. 在 onUnload 中清理可能残留的 Loading │ │ │ │ 4. 多个并发请求时使用计数器管理 │ │ │ │ 5. 封装通用 Hook,避免每个页面重复处理 │ │ │ │ 6. 页面跳转前确保关闭 Loading │ │ │ └─────────────────────────────────────────────────────────────┘

代码模板

// 标准的 Loading + 异步操作模板constdoAsyncAction=async()=>{uni.showLoading({title:'处理中',mask:true})try{// 带超时的异步操作constresult=awaitPromise.race([actualAsyncOperation(),newPromise((_,reject)=>setTimeout(()=>reject(newError('操作超时')),10000))])// 成功处理returnresult}catch(error){// 错误处理uni.showToast({title:error.message||'操作失败',icon:'none'})throwerror}finally{// 必定执行uni.hideLoading()}}

总结

  1. 问题根因uni.showLoading({ mask: true })创建的全屏遮罩,在接口无响应时不会自动关闭,导致页面"卡死"

  2. 核心解决方案

    • 使用finally确保hideLoading()执行
    • 使用Promise.race添加超时机制
    • onUnload中清理残留 Loading
  3. 最佳实践

    • 封装通用的 Loading 管理 Hook
    • 统一处理超时、清理、并发等场景
    • 页面跳转前确保关闭 Loading
  4. 记忆口诀

    showLoading 要配 finally,超时兜底不能忘,页面卸载要清理

这个问题虽然简单,但影响很大(页面完全无法操作),而且不容易复现和定位。通过规范的异步操作处理模式,可以从根本上避免这类问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/3 23:24:46

机器学习 —— 数据泄露

摘要&#xff1a;机器学习中数据泄露会导致模型过拟合&#xff0c;主要分为目标泄露&#xff08;使用预测时无法获取的特征&#xff09;和训练-测试集污染&#xff08;预处理时混入测试集信息&#xff09;。防止措施包括&#xff1a;严格划分训练/测试集、仅使用可获取特征、采…

作者头像 李华
网站建设 2026/3/11 15:28:41

大数据领域 OLAP 的实时数据分析平台搭建

大数据领域 OLAP 的实时数据分析平台搭建 关键词&#xff1a;大数据、OLAP、实时数据分析平台、数据仓库、架构设计 摘要&#xff1a;本文围绕大数据领域 OLAP 的实时数据分析平台搭建展开。首先介绍了搭建此平台的背景&#xff0c;包括目的、预期读者等信息。接着阐述了 OLAP …

作者头像 李华
网站建设 2026/3/7 4:02:38

CANN 性能调优指南:如何榨干昇腾芯片算力?

从模型转换到推理部署&#xff0c;全链路解锁昇腾 NPU 极致性能 &#x1f9e9; 引言&#xff1a;为什么你的模型没跑满昇腾算力&#xff1f; 你是否遇到过以下情况&#xff1f; 昇腾 910 理论算力 256 TFLOPS&#xff08;FP16&#xff09;&#xff0c;但实测仅用到 30%&#…

作者头像 李华
网站建设 2026/3/9 5:21:59

LLM - 从 0 打造专业 Agent Skill:一套能落地的完整实践指南

文章目录引言&#xff1a;为什么该重视 Agent Skill&#xff1f;一、先搞清楚&#xff1a;Skill 到底解决什么问题&#xff1f;1.1 传统用法的三大痛点1.2 一句话理解 Skill1.3 Skill 相比其他方案的定位1.4 什么时候值得做成 Skill&#xff1f;二、四个核心设计原则&#xff1…

作者头像 李华
网站建设 2026/3/11 4:54:27

关于 lint-staged 的解析

1. 它是什么可以把代码仓库想象成一个文件柜&#xff0c;里面存放了许多文件。当开发人员修改代码时&#xff0c;这些改动并不会直接扔进文件柜&#xff0c;而是先放在一个叫“暂存区”的篮子里。这个篮子里的文件&#xff0c;就是准备被正式归档&#xff08;提交&#xff09;的…

作者头像 李华
网站建设 2026/3/4 2:44:51

Lucide React 详解

1. 它是什么Lucide React 是一个为 React 应用提供的图标组件库。它本质上是一套封装成 React 组件的矢量图标集合。可以把它理解为一套精心设计、风格统一的“图形字”&#xff0c;但它是用代码&#xff08;SVG&#xff09;的形式提供的&#xff0c;而不是字体文件。生活中常见…

作者头像 李华