From 04420a43f18ea0931183214adfe5e4c7eaf66a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=A9=E4=BB=9488?= <> Date: Fri, 27 Feb 2026 09:39:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=AB=E7=A0=81=E8=87=AA=E5=8A=A8=E6=89=93?= =?UTF-8?q?=E5=BC=80=E5=A5=97=E9=A4=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- h5/App.vue | 237 +++++++++++++++++-------- h5/ERROR_REPORT.md | 282 ++++++++++++++++++++++++++++++ h5/api/index.js | 81 ++++++--- h5/index.html | 229 +++++++++++++++++++++++- h5/lu/axios.js | 112 +++++++----- h5/lu/errorReport.js | 270 ++++++++++++++++++++++++++++ h5/lu/index.js | 2 + h5/pages/main/bgcx/bgcx.vue | 40 +++-- h5/pages/main/index/index.vue | 67 ++++--- h5/pages/main/order/order.vue | 51 +++--- h5/pages/main/order/src/order.vue | 2 +- 11 files changed, 1176 insertions(+), 197 deletions(-) create mode 100644 h5/ERROR_REPORT.md create mode 100644 h5/lu/errorReport.js diff --git a/h5/App.vue b/h5/App.vue index 27e03b8..d8e4a59 100644 --- a/h5/App.vue +++ b/h5/App.vue @@ -4,17 +4,14 @@ * user:sa0ChunLuyu * date:2024年8月7日 20:05:05 */ - import { - ref - } from 'vue' import { $api, - $response, + $response, } from '@/api' import { onShow, - onHide, - onLoad,onError + onLoad, + onError } from '@dcloudio/uni-app' import { @@ -22,99 +19,201 @@ } from '@/store' const $store = useStore() + // 初始化错误上报 + try { + if (typeof uni.$lu !== 'undefined' && uni.$lu.errorReport) { + // 初始化全局错误捕获 + uni.$lu.errorReport.initErrorHandler() + console.log('[错误上报] 已初始化') + + // 处理早期错误缓冲区(index.html 中捕获的错误) + if (typeof window.flushErrorBuffer === 'function') { + try { + var earlyErrors = window.flushErrorBuffer() + if (earlyErrors && earlyErrors.length > 0) { + console.log('[错误上报] 早期错误数量:', earlyErrors.length) + // 重新上报早期错误 + earlyErrors.forEach(function(err) { + try { + uni.$lu.errorReport.reportError(err, 'early-error') + } catch (e) { + console.warn('[错误上报] 重新上报早期错误失败', e) + } + }) + } + } catch (e) { + console.warn('[错误上报] 处理早期错误缓冲区失败', e) + } + } + } + } catch (e) { + console.warn('[错误上报] 初始化失败', e) + } + const setConfigStore = () => { let config = {} try { const config_str = uni.getStorageSync('CONFIG_CONFIG') if (config_str) config = JSON.parse(config_str) } catch (e) { - uni.showToast({ - icon:"none", - title:e.message || '解析配置失败' - }) - console.warn('CONFIG_CONFIG 解析失败', e) + console.warn('CONFIG_CONFIG 解析失败', e) } $store.config = config - if (!config.color) { - document.body.classList.toggle('grayscale'); + try { + // 安全检查:确保 document.body 存在 + if (document && document.body && !config.color) { + document.body.classList.toggle('grayscale'); + } + } catch (e) { + console.warn('设置样式失败', e) + } + let openid_str = '' + try { + openid_str = uni.getStorageSync('OPENID') + } catch (e) { + console.warn('获取 OPENID 失败', e) + } + let url = '' + try { + url = window.location.href + } catch (e) { + console.warn('获取 URL 失败', e) } - const openid_str = uni.getStorageSync('OPENID') - let url = window.location.href if (!openid_str) { - if (url.indexOf('/pages/main/login/login') === -1) { + if (url && url.indexOf('/pages/main/login/login') === -1) { uni.redirectTo({ url: '/pages/main/login/login' }) } } - const save_info_str = uni.getStorageSync('SAVE_INFO') - if (!!save_info_str) { - $store.save_info = JSON.parse(save_info_str) - } else { + try { + const save_info_str = uni.getStorageSync('SAVE_INFO') + if (!!save_info_str) { + $store.save_info = JSON.parse(save_info_str) + } else { + $store.resetSaveInfo() + } + } catch (e) { + console.warn('解析 SAVE_INFO 失败', e) $store.resetSaveInfo() } } const getConfigConfig = async () => { - const response = await $api('ConfigConfig') - $response(response, () => { - uni.setStorageSync('CONFIG_CONFIG', JSON.stringify(response.data.config)) - setConfigStore() - }) + try { + const response = await $api('ConfigConfig') + $response(response, () => { + try { + uni.setStorageSync('CONFIG_CONFIG', JSON.stringify(response.data.config)) + setConfigStore() + } catch (e) { + console.error('保存配置失败', e) + } + }) + } catch (e) { + console.error('获取配置接口失败', e) + } } const getConfigVersion = async () => { - const response = await $api('ConfigVersion') - $response(response, () => { - const config_version = uni.getStorageSync('CONFIG_VERSION') - let get_config = false - if (!config_version) { - get_config = true - } else { - if (config_version !== response.data.version) { - get_config = true + try { + const response = await $api('ConfigVersion') + $response(response, () => { + try { + const config_version = uni.getStorageSync('CONFIG_VERSION') + let get_config = false + if (!config_version) { + get_config = true + } else { + if (config_version !== response.data.version) { + get_config = true + } + } + uni.setStorageSync('CONFIG_VERSION', response.data.version) + //if (!!get_config) { + if (true) { + getConfigConfig() + } else { + setConfigStore() + } + } catch (e) { + console.error('处理版本配置失败', e) } - } - uni.setStorageSync('CONFIG_VERSION', response.data.version) - //if (!!get_config) { - if (true) { - getConfigConfig() + }) + } catch (e) { + console.error('获取版本接口失败', e) + } + } + const handleFontSize = () => { + try { + // 设置网页字体为默认大小 + WeixinJSBridge.invoke('setFontSizeCallback', { + 'fontSize': 0 + }); + // 重写设置网页字体大小的事件 + WeixinJSBridge.on('menu:setfont', function() { + WeixinJSBridge.invoke('setFontSizeCallback', { + 'fontSize': 0 + }); + }); + } catch (e) { + console.warn('设置字体大小失败', e) + } + } + onShow(() => { + try { + if (typeof WeixinJSBridge == "object" && typeof WeixinJSBridge.invoke == "function") { + handleFontSize(); } else { - setConfigStore() + try { + if (document && document.addEventListener) { + document.addEventListener("WeixinJSBridgeReady", handleFontSize, false); + } else if (document && document.attachEvent) { + document.attachEvent("WeixinJSBridgeReady", handleFontSize); + document.attachEvent("onWeixinJSBridgeReady", handleFontSize); + } + } catch (e) { + console.warn('监听 WeixinJSBridge 失败', e) + } } - }) - } - const handleFontSize=()=> { - // 设置网页字体为默认大小 - WeixinJSBridge.invoke('setFontSizeCallback', { - 'fontSize': 0 - }); - // 重写设置网页字体大小的事件 - WeixinJSBridge.on('menu:setfont', function() { - WeixinJSBridge.invoke('setFontSizeCallback', { - 'fontSize': 0 - }); - }); - } - onShow(() => { - if (typeof WeixinJSBridge == "object" && typeof WeixinJSBridge.invoke == "function") { - - handleFontSize(); - } else { - if (document.addEventListener) { - document.addEventListener("WeixinJSBridgeReady", handleFontSize, false); - } else if (document.attachEvent) { - document.attachEvent("WeixinJSBridgeReady", handleFontSize); - document.attachEvent("onWeixinJSBridgeReady", handleFontSize); - } - } + } catch (e) { + console.warn('onShow 初始化失败', e) + } console.log(`\n %c 鹿和 %c https://sa0.online/ \n\n`, 'color: #ffffff; background: #fd6b60; padding:5px 0;', 'color: #fd6b60;background: #ffffff; padding:5px 0;') - getConfigVersion() + try { + getConfigVersion() + } catch (e) { + console.error('获取配置版本失败', e) + } + }) + onError((err) => { + console.error('[全局错误]', err) + + // iOS WebView 特殊处理:强制清理可能阻塞的loading + const isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent) + if (isIOS) { + try { + uni.hideLoading() + console.log('[iOS清理] 强制关闭loading') + } catch (e) { + console.warn('[iOS清理loading失败]', e) + } + } + + // 上报错误 + try { + if (typeof uni.$lu !== 'undefined' && uni.$lu.errorReport) { + uni.$lu.errorReport.reportError(err, 'uni-app-error') + } + } catch (e) { + console.warn('[错误上报] 上报失败', e) + } }) + onLoad(()=>{ - - + + }) diff --git a/h5/ERROR_REPORT.md b/h5/ERROR_REPORT.md new file mode 100644 index 0000000..d2e45ca --- /dev/null +++ b/h5/ERROR_REPORT.md @@ -0,0 +1,282 @@ +# H5 错误上报功能说明 + +## 概述 +已为H5项目添加了全局错误上报功能,能够捕获和上报前端运行时错误,即使发生白屏也能获取到错误信息。 + +## 错误上报接口 +- **接口地址**: `https://tj-h5.hnxdfe.com/api/H5/test` +- **请求方式**: POST +- **Content-Type**: application/json + +## 上报的错误类型 + +### 1. JavaScript 运行时错误 (type: 'error') +- 语法错误 +- 引用错误 +- 类型错误 +- 范围错误等 + +### 2. Promise 未处理错误 (type: 'promise') +- Promise rejection 错误 +- async/await 未捕获的异常 + +### 3. uni-app 错误 (type: 'uni-app-error') +- uni-app 生命周期错误 +- 页面加载错误 + +### 4. 早期错误 (type: 'error' / phase: 'early') +- 应用初始化前的错误 +- 脚本加载错误 +- 白屏问题的主要来源 + +### 5. 手动上报错误 (type: 'manual') +- 业务逻辑中的自定义错误 + +## 错误数据格式 + +```json +{ + "type": "error", // 错误类型 + "message": "xxx is not defined", // 错误信息 + "stack": "Error: xxx...", // 堆栈信息 + "url": "https://...", // 当前页面URL + "ua": "Mozilla/5.0...", // 用户代理 + "timestamp": "2024-01-01T00:00:00.000Z", // 时间戳 + "openId": "xxxxx", // 用户OpenID(如果存在) + "phase": "early" // 阶段标识(仅早期错误) +} +``` + +## 功能特性 + +### 1. 多重上报保障 +错误上报采用三层保障机制,确保即使白屏也能上报: + +1. **优先使用 navigator.sendBeacon** + - 即使页面关闭也能发送 + - 不阻塞页面卸载 + - 最可靠的方式 + +2. **备用方案 fetch with keepalive** + - keepalive 选项保证请求完成 + - 适用于大多数浏览器 + +3. **兜底方案 uni.request** + - 使用 uni-app 的请求方法 + - 100ms 延迟避免阻塞 + +### 2. 早期错误捕获 +- 在 `index.html` 中尽早初始化错误监听 +- 捕获应用初始化前的错误 +- 使用错误缓冲区存储早期错误 +- 应用加载后统一重新上报 + +### 3. 性能监控 +自动上报以下性能指标: +- `loadTime`: 页面总加载时间 +- `domReadyTime`: DOM就绪时间 +- `firstPaint`: 首次渲染时间 + +## 使用方法 + +### 自动捕获(默认开启) +所有错误会自动上报,无需额外配置。 + +### 手动上报错误 +```javascript +// 在业务代码中手动上报 +uni.$lu.errorReport.report('自定义错误信息', { + // 额外的业务数据 + userId: '123', + action: 'submitOrder' +}) +``` + +### 手动上报错误对象 +```javascript +try { + // 可能出错的操作 +} catch (error) { + uni.$lu.errorReport.reportError(error, 'custom-error') +} +``` + +### 上报性能数据 +```javascript +uni.$lu.errorReport.reportPerformance({ + customMetric: 1234, + operation: 'loadData' +}) +``` + +## 日志输出 + +在开发环境中,可以在控制台看到以下日志: + +``` +[早期错误捕获] 已初始化 +[错误上报] 已初始化 +[错误上报] 早期错误数量: 2 +[错误上报] { type: 'error', message: '...', ... } +``` + +## 后端处理建议 + +### 1. 数据存储 +建议将错误数据存储到数据库或日志系统: +- MongoDB: 适合存储灵活的JSON数据 +- MySQL/PostgreSQL: 可以使用JSON字段 +- ELK Stack: 适合日志分析和检索 + +### 2. 错误统计 +可以统计以下指标: +- 错误类型分布 +- 错误发生频率 +- 受影响的用户数 +- 错误来源页面 +- iOS vs Android 错误对比 + +### 3. 告警机制 +可以设置告警规则: +- 某类错误短时间内激增 +- 同一用户连续报错 +- 白屏类型错误(phase: 'early') + +## 调试方法 + +### 1. 测试错误上报 +在浏览器控制台执行: +```javascript +// 触发一个错误 +throw new Error('测试错误上报') +``` + +### 2. 查看网络请求 +打开开发者工具 -> Network -> 查看POST请求到 `/api/H5/test` 的数据 + +### 3. 查看早期错误缓冲区 +```javascript +console.log(window.__ERROR_BUFFER__) +``` + +### 4. 小程序 WebView 设备调试(重要) + +由于小程序 WebView 无法查看控制台,已添加以下调试功能: + +#### 调试按钮 +页面右下角会显示三个调试按钮: + +1. **🐛 查看日志**(红色)- 切换日志面板显示/隐藏 +2. **🧪 测试**(绿色)- 测试日志功能是否正常工作 +3. **🗑️ 清空**(橙色)- 清空所有日志 + +#### 自动显示日志 +- 发生错误时,页面顶部会自动显示黑色半透明的调试面板 +- 面板中会显示完整的错误信息和调试日志 +- 点击 "🐛 查看日志" 按钮可手动切换显示状态 + +#### 日志内容 +日志面板中会显示: +- `[时间] [原始error事件]` - 原始错误事件的详细信息 +- `[时间] [早期错误捕获]` - 处理后的错误信息 +- `[时间] 调试系统已初始化` - 调试系统启动时的测试日志 +- 所有调试日志的时间戳和详细信息 + +#### 测试日志功能 +1. 点击 **🧪 测试** 按钮 +2. 如果日志面板出现并显示测试信息,说明日志功能正常 +3. 如果点击没反应,可能是被小程序 WebView 的安全策略限制 + +#### 获取完整日志 +如果需要将日志发送给开发者: + +方法1:截图 +- 将调试面板的内容截图 + +方法2:复制日志 +```javascript +// 在浏览器控制台执行(需要能访问控制台) +console.log(JSON.stringify(window.__DEBUG_LOGS__, null, 2)) +``` + +方法3:通过小程序调试工具 +- 如果小程序有调试模式,可以通过调试工具访问 `window.__DEBUG_LOGS__` + +#### 调试面板特性 +- 自动滚动到最新日志 +- 每条日志有分隔线,方便阅读 +- 自动换行,长文本不会溢出 +- 点击 "🐛 查看日志" 按钮切换显示/隐藏 + +## 注意事项 + +1. **隐私保护**: 错误数据不包含敏感信息,但建议后端做好脱敏处理 +2. **性能影响**: 错误上报使用异步方式,不影响页面性能 +3. **网络限制**: 如果网络不可用,错误会失败但不影响应用运行 +4. **跨域问题**: 确保接口支持 CORS 或在同一域名下 + +## iOS 白屏问题排查 + +通过错误上报,可以收集以下信息来排查 iOS 白屏问题: + +1. **早期错误 (phase: 'early')** + - 查看应用初始化前的错误 + - 可能是脚本加载或语法错误 + +2. **用户代理 (ua)** + - 识别具体的 iOS 版本 + - 区分不同的 WebView 环境 + +3. **错误堆栈 (stack)** + - 精确定位错误位置 + - 了解错误调用链 + +4. **URL 信息** + - 判断是否特定页面白屏 + - 检查路由参数是否正确 + +## 实际案例分析 + +### 案例1: iOS 14.5 微信小程序白屏 +```json +{ + "type": "error", + "message": "{}", + "stack": null, + "url": "https://tj-h5.hnxdfe.com/h5/#/pages/main/login/login?openid=oosgJj-yr2IrwSmBb3_ERy1hGMos", + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.63(0x18003f2f) NetType/4G Language/zh_CN miniProgram/wxece13dd0a5b22c73", + "timestamp": "2026-02-05T11:43:07.707Z", + "phase": "early" +} +``` + +**问题分析**: +- 错误信息为 `{}`,堆栈为 `null` +- 说明错误对象构建时出错,可能是早期错误对象结构异常 +- 发生在登录页面,URL 中带有 openid 参数 +- 环境是 iOS 14.5 + 微信 8.0.63 + 小程序 WebView + +**可能原因**: +1. 微信小程序 WebView 在早期阶段触发了 error 事件 +2. error 事件对象的结构与预期不同 +3. 某些初始化脚本在特定环境下执行失败 + +**已优化**: +- 增强了 `buildErrorInfo` 函数,处理更多异常情况 +- 添加了对 null/undefined 的特殊处理 +- JSON.stringify 失败时的降级处理 +- 确保 message 和 stack 永远不会是 null 或空对象 + +**建议后续观察**: +- 检查是否还有类似 `{}` 或 `null` 的错误信息 +- 统计 iOS 14.5 的错误频率 +- 检查登录页面是否是白屏高发页面 +- 检查 openid 参数处理是否正常 + +## 扩展建议 + +未来可以考虑: +1. 添加错误采样率(避免上报过多) +2. 添加用户反馈功能(用户主动报告问题) +3. 集成第三方错误监控(如 Sentry) +4. 添加错误聚合和去重机制 diff --git a/h5/api/index.js b/h5/api/index.js index 931c189..664816d 100644 --- a/h5/api/index.js +++ b/h5/api/index.js @@ -17,33 +17,66 @@ export const $url = (url_key) => { } } export const $api = async (url_key, data = {}, opt = {}) => { - const opt_data = { - ...$config, - ...opt, - } - const $store = useStore() - if (!(url_key in $store.api_map)) { - const api_map = await $post({ - url: opt_data.config.api_map_url - }, opt_data) - if (!api_map.status) { - uni.$lu.toast('获取接口失败') + let loadingTimer = null + + try { + const opt_data = { + ...$config, + ...opt, + } + const $store = useStore() + + // 防止loading一直显示的兜底机制(30秒后强制关闭) + loadingTimer = setTimeout(() => { + try { + uni.hideLoading() + console.warn('[api] 强制关闭loading(超时保护)') + } catch (e) { + console.warn('[api] 强制关闭loading失败', e) + } + }, 30000) + + if (!(url_key in $store.api_map)) { + const api_map = await $post({ + url: opt_data.config.api_map_url + }, opt_data) + if (!api_map.status) { + if (typeof uni.$lu !== 'undefined' && uni.$lu.toast) { + uni.$lu.toast('获取接口失败') + } + if (loadingTimer) clearTimeout(loadingTimer) + return false + } + $store.api_map = api_map.data.list + } + if (!(url_key in $store.api_map)) { + if (typeof uni.$lu !== 'undefined' && uni.$lu.toast) { + uni.$lu.toast(`接口不存在 [${url_key}]`) + } + if (loadingTimer) clearTimeout(loadingTimer) return false } - $store.api_map = api_map.data.list - } - if (!(url_key in $store.api_map)) { - uni.$lu.toast(`接口不存在 [${url_key}]`) - return false - } - const openid = uni.getStorageSync('OPENID') - if (!!openid) { - data.openid = openid + const openid = uni.getStorageSync('OPENID') + if (!!openid) { + data.openid = openid + } + const result = await $post({ + url: $store.api_map[url_key], + data + }, opt_data) + + // 清除定时器 + if (loadingTimer) clearTimeout(loadingTimer) + + return result + } catch (e) { + console.error('[api] API调用失败', url_key, e) + + // 清除定时器 + if (loadingTimer) clearTimeout(loadingTimer) + + throw e } - return await $post({ - url: $store.api_map[url_key], - data - }, opt_data) } export const $image = (path) => { diff --git a/h5/index.html b/h5/index.html index 309d38e..5ae0893 100644 --- a/h5/index.html +++ b/h5/index.html @@ -8,7 +8,234 @@ document.write( '') -