commit bc5d2cf14d5ba255473afe74a2b0adbcc057a503 Author: yuantao Date: Mon Dec 1 17:56:13 2025 +0800 初始化提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ba90c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,112 @@ +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Build outputs +dist/ +build/ +types/ +*.tgz + +# Coverage +coverage/ +*.lcov +.nyc_output + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +jest.config.js +coverage/ +.nyc_output/ +junit.xml + +# Temporary files +*.tmp +*.temp +.temp/ +.tmp/ + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Storybook build outputs +.out +.storybook-out +storybook-static + +# Temporary folders +tmp/ +temp/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f05d27 --- /dev/null +++ b/README.md @@ -0,0 +1,256 @@ +# uniapp-error-monitor + +UniApp 错误监控上报插件 - 专业的 JavaScript 错误监控和上报解决方案 + +[![npm version](https://badge.fury.io/js/uniapp-error-monitor.svg)](https://badge.fury.io/js/uniapp-error-monitor) +[![npm downloads](https://img.shields.io/npm/dm/uniapp-error-monitor.svg)](https://www.npmjs.com/package/uniapp-error-monitor) +[![license](https://img.shields.io/npm/l/uniapp-error-monitor.svg)](https://github.com/your-username/uniapp-error-monitor/blob/main/LICENSE) +[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/) +[![rollup](https://img.shields.io/badge/rollup-build-blue.svg)](https://rollupjs.org/) + +## 🌟 特性 + +- 🔍 **全面错误捕获**: 支持全局错误、Promise 错误、控制台错误、网络错误、小程序错误 +- 🎯 **环境智能**: 自动检测生产环境,非生产环境不启用错误上报 +- 🚀 **高性能**: 异步发送错误,不阻塞主线程 +- 🔄 **重试机制**: 网络失败自动重试,可配置重试次数和间隔 +- 📊 **错误统计**: 内置错误统计功能,便于数据分析 +- 🔧 **易于集成**: 零配置使用,支持自定义 webhook 和发送器 +- 📱 **多平台支持**: 支持 H5、微信小程序、App 等 UniApp 支持的所有平台 +- 🛡️ **类型安全**: 完整的 TypeScript 类型定义 +- 📦 **模块化**: 支持 ESM、CommonJS、UMD 多种模块格式 + +## 📦 安装 + +```bash +# npm +npm install uniapp-error-monitor + +# yarn +yarn add uniapp-error-monitor + +# pnpm +pnpm add uniapp-error-monitor +``` + +## 🚀 快速开始 + +### 基础使用 + +```javascript +import { initErrorMonitor } from 'uniapp-error-monitor' + +// 初始化错误监控 +initErrorMonitor({ + webhookUrl: 'https://your-webhook-url.com', // 必填 + enableGlobalError: true, // 启用全局错误捕获 + enablePromiseError: true, // 启用 Promise 错误捕获 + enableConsoleError: false, // 禁用 console.error 捕获 +}) + +// 手动上报错误 +import { reportError } from 'uniapp-error-monitor' + +reportError('manual', new Error('自定义错误'), { + page: 'index', + action: '用户操作失败' +}) +``` + +### Promise 包装 + +```javascript +import { wrapPromise } from 'uniapp-error-monitor' + +// 自动捕获 Promise 错误 +const result = await wrapPromise( + fetch('https://api.example.com/data') +) +``` + +## 📋 配置选项 + +```typescript +interface ErrorMonitorOptions { + // 基础配置 + webhookUrl: string // Webhook 地址(必填) + enableGlobalError?: boolean // 启用全局错误捕获(默认:true) + enablePromiseError?: boolean // 启用 Promise 错误捕获(默认:true) + enableConsoleError?: boolean // 启用 console.error 捕获(默认:false) + + // 重试配置 + maxRetries?: number // 最大重试次数(默认:3) + retryDelay?: number // 重试延迟时间(ms)(默认:1000) + + // 高级配置 + forceEnable?: boolean // 强制启用错误监控(忽略环境检查) + formatter?: (error: ErrorInfo) => string // 自定义格式化函数 + sender?: (payload: ErrorInfo) => Promise // 自定义发送器 +} +``` + +## 🔧 高级使用 + +### 自定义发送器 + +```javascript +import { ErrorMonitor } from 'uniapp-error-monitor' + +const errorMonitor = new ErrorMonitor() + +// 使用自定义发送器 +errorMonitor.setSender(async (errorInfo) => { + // 发送到自己的服务器 + await fetch('/api/errors', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(errorInfo) + }) +}) + +errorMonitor.init({ + webhookUrl: 'custom-sender' // 使用自定义发送器时,webhookUrl 可以设置为任意值 +}) +``` + +### 自定义错误格式化 + +```javascript +import { ErrorMonitor } from 'uniapp-error-monitor' + +const errorMonitor = new ErrorMonitor() + +// 设置自定义格式化函数 +errorMonitor.setFormatter((errorInfo) => { + return `🔴 错误详情: + 类型:${errorInfo.type} + 消息:${errorInfo.error} + 页面:${errorInfo.page} + 时间:${new Date(errorInfo.timestamp).toLocaleString()}` +}) + +errorMonitor.init({ + webhookUrl: 'your-webhook-url' +}) +``` + +### 获取错误统计 + +```javascript +import { getErrorStats } from 'uniapp-error-monitor' + +const stats = getErrorStats() +console.log('错误统计:', stats) +/* +输出: +{ + total: 5, + global: 2, + promise: 1, + console: 0, + miniProgram: 1, + network: 1, + lastErrorTime: 1640995200000 +} +*/ +``` + +## 📊 错误类型说明 + +| 错误类型 | 说明 | 触发条件 | +|---------|------|---------| +| `global` | 全局 JavaScript 错误 | `window.onerror` 捕获 | +| `promise` | 未处理的 Promise 拒绝 | `unhandledrejection` 事件 | +| `console` | console.error 输出 | 手动启用后捕获 | +| `miniProgram` | 小程序特定错误 | `uni.onError`, `uni.onPageNotFound` | +| `network` | 网络请求失败 | 拦截的 `uni.request` 失败 | +| `api` | API 接口错误 | 手动上报的接口错误 | +| `manual` | 手动上报的错误 | 手动调用 `reportError` | + +## 🏗️ 构建配置 + +### 环境变量 + +在你的项目中设置环境变量: + +```javascript +// .env 文件 +VITE_WEBHOOK=https://your-webhook-url.com +``` + +### 开发环境自动禁用 + +插件会在以下情况下自动禁用(非生产环境): + +- 开发模式 (`import.meta.env.MODE === 'development'`) +- 小程序体验版、开发版、预览版 +- 非 UniApp 环境 + +如需强制启用,设置 `forceEnable: true`。 + +## 🔍 TypeScript 支持 + +完整的 TypeScript 类型支持: + +```typescript +import { ErrorMonitor, ErrorMonitorOptions, ErrorType, ErrorStats } from 'uniapp-error-monitor' + +const options: ErrorMonitorOptions = { + webhookUrl: 'https://example.com/webhook', + maxRetries: 3 +} + +const errorMonitor = new ErrorMonitor(options) +``` + +## 📱 平台兼容性 + +- ✅ **微信小程序**: 完整支持 +- ✅ **H5**: 完整支持 +- ✅ **App (iOS/Android)**: 完整支持 +- ✅ **支付宝小程序**: 基本支持 +- ✅ **字节跳动小程序**: 基本支持 +- ✅ **百度小程序**: 基本支持 +- ✅ **快应用**: 基本支持 + +## 🛠️ 开发调试 + +```bash +# 克隆项目 +git clone https://github.com/yuentao/uniapp-error-monitor.git +cd uniapp-error-monitor + +# 安装依赖 +npm install + +# 开发调试 +npm run dev + +# 类型检查 +npm run type-check + +# 构建 +npm run build + +# 单元测试 +npm run test +``` + +## 📦 构建产物 + +构建后会在 `dist/` 目录生成: + +- `index.js` - CommonJS 格式 +- `index.mjs` - ES Module 格式 +- `index.umd.js` - UMD 格式(用于浏览器) +- `index.umd.min.js` - UMD 压缩版 +- `index.d.ts` - TypeScript 类型声明 +- `types/` - 类型声明目录 + +## 📄 许可证 + +MIT License - 详见 [LICENSE](LICENSE) 文件 + +--- + +⭐ 如果这个项目对你有帮助,请给它一个星标! \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..28d62e6 --- /dev/null +++ b/build.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# 清理 dist 目录 +echo "🧹 清理构建目录..." +rm -rf dist +rm -rf types + +# TypeScript 类型检查和编译 +echo "🔍 TypeScript 类型检查..." +npm run type-check + +# 生成类型声明文件 +echo "📝 生成类型声明文件..." +npm run build:types + +# Rollup 构建 +echo "📦 执行 Rollup 构建..." +npm run build:dist + +# 复制类型文件到 dist 目录 +echo "📋 复制类型文件..." +cp -r types/* dist/types/ + +# 构建完成 +echo "✅ 构建完成!" +echo "📁 输出目录: dist/" +echo "🎯 主要文件:" +echo " - dist/index.js (CommonJS)" +echo " - dist/index.mjs (ESM)" +echo " - dist/index.umd.js (UMD)" +echo " - dist/index.d.ts (TypeScript 类型)" +echo "" +echo "📊 统计信息:" +ls -lh dist/ +echo "" + +echo "🚀 可以执行 'npm publish' 发布到 npm!" \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..72d74a4 --- /dev/null +++ b/package.json @@ -0,0 +1,77 @@ +{ + "name": "uniapp-error-monitor", + "version": "1.0.0", + "description": "专门为UniApp环境设计的错误监控和上报工具,支持全局错误捕获、Promise错误捕获、网络错误捕获等", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "keywords": [ + "uniapp", + "error", + "monitor", + "tracking", + "wechat-miniprogram", + "vue3", + "error-reporting", + "javascript", + "error-handling" + ], + "author": { + "name": "iFlow CLI", + "email": "contact@iflow.dev" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/iflow-dev/uniapp-error-monitor.git" + }, + "bugs": { + "url": "https://github.com/iflow-dev/uniapp-error-monitor/issues" + }, + "homepage": "https://github.com/iflow-dev/uniapp-error-monitor#readme", + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + }, + "scripts": { + "build": "rollup -c", + "dev": "rollup -c -w", + "clean": "rimraf dist", + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint src/**/*.js", + "lint:fix": "eslint src/**/*.js --fix", + "prepublishOnly": "npm run clean && npm run build && npm test" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-node-resolve": "^15.0.0", + "@rollup/plugin-typescript": "^11.0.0", + "rollup": "^3.0.0", + "rollup-plugin-terser": "^7.0.0", + "typescript": "^5.0.0", + "jest": "^29.0.0", + "eslint": "^8.0.0", + "rimraf": "^5.0.0" + }, + "peerDependencies": { + "@dcloudio/uni-app": "^3.0.0" + }, + "peerDependenciesMeta": { + "@dcloudio/uni-app": { + "optional": true + } + }, + "browserslist": [ + "defaults", + "not IE 11", + "last 2 versions", + "iOS >= 10", + "Android >= 6" + ] +} \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..0018226 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,76 @@ +import { defineConfig } from 'rollup' +import babel from '@rollup/plugin-babel' +import { terser } from 'rollup-plugin-terser' + +const pkg = require('./package.json') + +const external = [ + 'vue', + 'vue3', + '@dcloudio/uni-app', + '@dcloudio/uni-core', + 'uniapp' +] + +const plugins = [ + babel({ + babelHelpers: 'bundled', + extensions: ['.js', '.ts', '.vue'], + exclude: ['node_modules/**'], + presets: [ + ['@babel/preset-env', { + modules: false, + targets: { + esmodules: true + } + }] + ] + }) +] + +export default defineConfig([ + { + input: 'src/index.js', + output: [ + // ESM 输出 + { + file: pkg.module, + format: 'es', + sourcemap: true + }, + // UMD 输出 (用于浏览器) + { + file: pkg.browser, + format: 'umd', + name: 'UniAppErrorMonitor', + sourcemap: true, + globals: { + 'vue': 'Vue' + } + }, + // CommonJS 输出 (用于 Node.js) + { + file: pkg.main, + format: 'cjs', + sourcemap: true + } + ], + external, + plugins + }, + // 生产环境构建 (压缩版本) + { + input: 'src/index.js', + output: [ + { + file: pkg.browser.replace('.umd.js', '.umd.min.js'), + format: 'umd', + name: 'UniAppErrorMonitor', + sourcemap: false, + plugins: [terser()] + } + ], + external, + plugins + } +]) \ No newline at end of file diff --git a/src/ErrorMonitor.d.ts b/src/ErrorMonitor.d.ts new file mode 100644 index 0000000..cf56346 --- /dev/null +++ b/src/ErrorMonitor.d.ts @@ -0,0 +1,187 @@ +/** + * UniApp错误监控器类型定义 + * 专门为UniApp环境设计的错误监控和上报工具 + */ + +declare class ErrorMonitor { + /** + * 初始化错误监控器 + * @param options 配置选项 + */ + init(options?: ErrorMonitorOptions): void; + + /** + * 手动上报错误 + * @param type 错误类型 + * @param error 错误对象或错误信息 + * @param context 错误上下文信息 + * @param forceSend 强制发送(忽略环境检查) + */ + reportError( + type?: ErrorType, + error?: Error | object | string, + context?: object, + forceSend?: boolean + ): void; + + /** + * 包装Promise,自动捕获Promise错误 + * @param promise 要包装的Promise + * @returns 包装后的Promise + */ + wrapPromise(promise: Promise): Promise; + + /** + * 获取错误统计信息 + * @returns 错误统计信息 + */ + getErrorStats(): ErrorStats; + + /** + * 重置错误统计 + */ + resetErrorStats(): void; + + /** + * 获取当前环境信息 + * @returns 环境信息 + */ + getEnvironmentInfo(): EnvironmentInfo; +} + +declare interface ErrorMonitorOptions { + /** + * 是否启用全局错误捕获 + * @default true + */ + enableGlobalError?: boolean; + + /** + * 是否启用Promise错误捕获 + * @default true + */ + enablePromiseError?: boolean; + + /** + * 是否启用console.error捕获 + * @default false + */ + enableConsoleError?: boolean; + + /** + * 自定义webhook地址 + */ + webhookUrl?: string; + + /** + * 发送失败时最大重试次数 + * @default 3 + */ + maxRetries?: number; + + /** + * 重试延迟时间(毫秒) + * @default 1000 + */ + retryDelay?: number; + + /** + * 强制启用错误监控(忽略环境检查) + * @default false + */ + forceEnable?: boolean; + + /** + * 自定义错误格式化函数 + */ + customFormatter?: (errorInfo: ErrorInfo) => string; + + /** + * 自定义发送函数 + */ + customSender?: (errorInfo: ErrorInfo) => Promise; +} + +declare type ErrorType = + | 'manual' + | 'api' + | 'network' + | 'global' + | 'promise' + | 'console' + | 'miniProgram'; + +declare interface ErrorStats { + total: number; + global: number; + promise: number; + console: number; + miniProgram: number; + api: number; + network: number; + manual: number; + lastErrorTime: number | null; +} + +declare interface EnvironmentInfo { + isProduction: boolean; + mode: string; + platform: string; + errorMonitorEnabled: boolean; + timestamp: number; +} + +declare interface ErrorInfo { + type: ErrorType; + error: string | object; + stack?: string | null; + context?: object; + timestamp: number; + url: string; + userAgent: string; + page: string; + + // API错误特有字段 + statusCode?: number; + statusText?: string; + responseTime?: number; + requestData?: object; + requestHeaders?: object; + requestId?: string; + environment?: string; + + // 网络错误特有字段 + retryCount?: number; + networkType?: string; + isConnected?: boolean; + + // 全局错误特有字段 + message?: string; + source?: string; + lineno?: number; + colno?: number; + + // Promise错误特有字段 + reason?: string | object; + + // Console错误特有字段 + args?: string[]; + + // 小程序错误特有字段 + path?: string; + query?: string; +} + +// 导出默认实例 +declare const ErrorMonitorInstance: ErrorMonitor; +export default ErrorMonitorInstance; + +// 导出类型 +export { + ErrorMonitor, + ErrorMonitorOptions, + ErrorType, + ErrorStats, + EnvironmentInfo, + ErrorInfo, +}; \ No newline at end of file diff --git a/src/ErrorMonitor.js b/src/ErrorMonitor.js new file mode 100644 index 0000000..a36dcb4 --- /dev/null +++ b/src/ErrorMonitor.js @@ -0,0 +1,709 @@ +/** + * UniApp错误监控器 + * 专门为UniApp环境设计的错误监控和上报工具 + * 支持全局错误捕获、Promise错误捕获、网络错误捕获等 + */ + +class ErrorMonitor { + constructor() { + // 错误统计信息 + this.errorStats = { + total: 0, + global: 0, + promise: 0, + console: 0, + miniProgram: 0, + api: 0, + network: 0, + manual: 0, + lastErrorTime: null, + } + + // Promise包装工具 + this.wrapPromise = null + + // 项目信息 + this.projectInfo = { + name: 'uniapp-error-monitor', + version: '1.0.0', + } + + // 配置对象 + this.config = null + } + + /** + * 初始化错误监控器 + * @param {Object} options 配置选项 + * @param {boolean} [options.enableGlobalError=true] 是否启用全局错误捕获 + * @param {boolean} [options.enablePromiseError=true] 是否启用Promise错误捕获 + * @param {boolean} [options.enableConsoleError=false] 是否启用console.error捕获 + * @param {string} [options.webhookUrl] 自定义webhook地址 + * @param {number} [options.maxRetries=3] 发送失败时最大重试次数 + * @param {number} [options.retryDelay=1000] 重试延迟时间(毫秒) + * @param {boolean} [options.forceEnable=false] 强制启用错误监控(忽略环境检查) + * @param {Function} [options.customFormatter] 自定义错误格式化函数 + * @param {Function} [options.customSender] 自定义发送函数 + */ + init(options = {}) { + const config = { + enableGlobalError: true, + enablePromiseError: true, + enableConsoleError: false, + webhookUrl: '', + maxRetries: 3, + retryDelay: 1000, + forceEnable: false, + customFormatter: null, + customSender: null, + ...options, + } + + // 环境检查:默认在生产环境下启用错误监控 + if (!config.forceEnable && !this._isProduction()) { + console.info('[ErrorMonitor] 当前为非生产环境,错误监控已禁用') + return + } + + this.config = config + + // 全局错误捕获 + if (config.enableGlobalError) { + this._setupGlobalErrorHandlers() + } + + // Promise错误捕获 + if (config.enablePromiseError) { + this._setupPromiseErrorHandlers() + } + + // console错误捕获 + if (config.enableConsoleError) { + this._setupConsoleErrorHandlers() + } + + // 小程序特定错误捕获 + this._setupMiniProgramErrorHandlers() + + console.log('[ErrorMonitor] 错误监控已初始化') + } + + /** + * 手动上报错误 + * @param {string} type 错误类型 ('manual', 'api', 'network', 'global', 'promise', 'console', 'miniProgram') + * @param {Error|Object} error 错误对象或错误信息 + * @param {Object} [context] 错误上下文信息 + * @param {boolean} [forceSend=false] 强制发送(忽略环境检查) + */ + reportError(type = 'manual', error, context = {}, forceSend = false) { + const errorInfo = { + type, + error: error instanceof Error ? error.message : error, + stack: error instanceof Error ? error.stack : null, + context, + timestamp: Date.now(), + url: this._getCurrentUrl(), + userAgent: this._getUserAgent(), + page: this._getCurrentPageName(), + + // API错误特有字段 + statusCode: error.statusCode, + statusText: error.statusText, + responseTime: error.responseTime, + requestData: error.requestData, + requestHeaders: error.requestHeaders, + requestId: error.requestId, + environment: error.environment, + + // 网络错误特有字段 + retryCount: error.retryCount, + networkType: error.networkType, + isConnected: error.isConnected, + } + + this._updateErrorStats(type) + this._sendError(errorInfo, forceSend) + } + + /** + * 包装Promise,自动捕获Promise错误 + * @param {Promise} promise 要包装的Promise + * @returns {Promise} 包装后的Promise + */ + wrapPromise(promise) { + return promise.catch(error => { + this.reportError('promise', error) + throw error + }) + } + + /** + * 获取错误统计信息 + * @returns {Object} 错误统计信息 + */ + getErrorStats() { + return { ...this.errorStats } + } + + /** + * 重置错误统计 + */ + resetErrorStats() { + this.errorStats = { + total: 0, + global: 0, + promise: 0, + console: 0, + miniProgram: 0, + api: 0, + network: 0, + manual: 0, + lastErrorTime: null, + } + } + + /** + * 获取当前环境信息 + * @returns {Object} 环境信息 + */ + getEnvironmentInfo() { + return { + isProduction: this._isProduction(), + mode: this._getMode(), + platform: this._getUserAgent(), + errorMonitorEnabled: !!this.config, + timestamp: Date.now(), + } + } + + /** + * 设置全局错误处理器 + * @private + */ + _setupGlobalErrorHandlers() { + // Web环境 + if (typeof window !== 'undefined') { + window.onerror = (message, source, lineno, colno, error) => { + this._handleGlobalError({ + type: 'global', + message, + source, + lineno, + colno, + error, + timestamp: Date.now(), + }) + } + + // 处理未捕获的Promise错误 + window.addEventListener('unhandledrejection', event => { + this._handlePromiseError({ + type: 'promise', + reason: event.reason, + promise: event.promise, + timestamp: Date.now(), + }) + }) + } + } + + /** + * 设置Promise错误处理器 + * @private + */ + _setupPromiseErrorHandlers() { + // 在UniApp环境中,wrapPromise方法会处理Promise错误 + this.wrapPromise = promise => { + return promise.catch(error => { + this._handlePromiseError({ + type: 'promise', + reason: error, + timestamp: Date.now(), + }) + throw error + }) + } + } + + /** + * 设置console错误处理器 + * @private + */ + _setupConsoleErrorHandlers() { + const originalError = console.error + console.error = (...args) => { + originalError.apply(console, args) + this._handleConsoleError({ + type: 'console', + args: args.map(arg => this._serializeError(arg)), + timestamp: Date.now(), + }) + } + } + + /** + * 设置小程序错误处理器 + * @private + */ + _setupMiniProgramErrorHandlers() { + if (typeof uni !== 'undefined') { + // 监听小程序错误事件 + uni.onError && uni.onError(error => { + this._handleMiniProgramError({ + type: 'miniProgram', + error, + timestamp: Date.now(), + }) + }) + + // 监听小程序页面错误 + uni.onPageNotFound && uni.onPageNotFound(result => { + this._handleMiniProgramError({ + type: 'pageNotFound', + path: result.path, + query: result.query, + timestamp: Date.now(), + }) + }) + + // 监听小程序网络请求错误 + const originalRequest = uni.request + uni.request = options => { + return originalRequest({ + ...options, + fail: err => { + options.fail && options.fail(err) + this._handleNetworkError({ + type: 'network', + url: options.url, + method: options.method, + error: err, + timestamp: Date.now(), + }) + }, + }) + } + } + } + + /** + * 处理全局错误 + * @private + */ + _handleGlobalError(errorInfo) { + this._updateErrorStats('global') + this._sendError({ + ...errorInfo, + message: errorInfo.message || 'Unknown global error', + source: errorInfo.source || '', + lineno: errorInfo.lineno || 0, + colno: errorInfo.colno || 0, + url: this._getCurrentUrl(), + userAgent: this._getUserAgent(), + page: this._getCurrentPageName(), + }) + } + + /** + * 处理Promise错误 + * @private + */ + _handlePromiseError(errorInfo) { + this._updateErrorStats('promise') + this._sendError({ + ...errorInfo, + reason: this._serializeError(errorInfo.reason), + url: this._getCurrentUrl(), + userAgent: this._getUserAgent(), + page: this._getCurrentPageName(), + }) + } + + /** + * 处理console错误 + * @private + */ + _handleConsoleError(errorInfo) { + this._updateErrorStats('console') + this._sendError({ + ...errorInfo, + url: this._getCurrentUrl(), + userAgent: this._getUserAgent(), + page: this._getCurrentPageName(), + }) + } + + /** + * 处理小程序错误 + * @private + */ + _handleMiniProgramError(errorInfo) { + this._updateErrorStats('miniProgram') + this._sendError({ + ...errorInfo, + url: this._getCurrentUrl(), + userAgent: this._getUserAgent(), + page: this._getCurrentPageName(), + }) + } + + /** + * 处理网络错误 + * @private + */ + _handleNetworkError(errorInfo) { + this._updateErrorStats('network') + this._sendError({ + ...errorInfo, + url: this._getCurrentUrl(), + userAgent: this._getUserAgent(), + page: this._getCurrentPageName(), + }) + } + + /** + * 更新错误统计 + * @private + */ + _updateErrorStats(type) { + this.errorStats.total++ + this.errorStats[type] = (this.errorStats[type] || 0) + 1 + this.errorStats.lastErrorTime = Date.now() + } + + /** + * 发送错误信息 + * @private + */ + async _sendError(errorInfo, forceSend = false) { + // 环境检查 + if (!forceSend && !this._isProduction() && !this.config?.forceEnable) { + console.info('[ErrorMonitor] 非生产环境,错误信息不发送:', errorInfo.type) + return + } + + try { + // 使用自定义发送器或默认发送器 + if (this.config?.customSender) { + await this.config.customSender(errorInfo) + } else { + await this._sendToWebhook(errorInfo) + } + + console.log('[ErrorMonitor] 错误信息已处理') + } catch (error) { + console.error('[ErrorMonitor] 发送错误信息失败:', error) + } + } + + /** + * 发送到webhook + * @private + */ + async _sendToWebhook(errorInfo) { + const webhookUrl = this.config?.webhookUrl + if (!webhookUrl) { + console.warn('[ErrorMonitor] 未配置webhook地址') + return + } + + // 格式化错误信息 + const message = this.config?.customFormatter + ? this.config.customFormatter(errorInfo) + : this._formatErrorMessage(errorInfo) + + // 使用uni.request发送POST请求(适配uniapp环境) + await new Promise((resolve, reject) => { + uni.request({ + url: webhookUrl, + method: 'POST', + header: { + 'Content-Type': 'application/json', + }, + data: { + msgtype: 'text', + text: { + content: message, + mentioned_list: [], + }, + }, + success: resolve, + fail: reject, + }) + }) + } + + /** + * 格式化错误消息 + * @private + */ + _formatErrorMessage(errorInfo) { + const timestamp = new Date(errorInfo.timestamp).toLocaleString('zh-CN') + + let message = `🚨 JavaScript错误报告\n` + message += `📦 项目: ${this.projectInfo.name}\n` + message += `🏷️ 版本: ${this.projectInfo.version}\n` + message += `⏰ 时间: ${timestamp}\n` + message += `📱 页面: ${errorInfo.page || '未知页面'}\n` + message += `🌐 链接: ${errorInfo.url || '未知链接'}\n\n` + + switch (errorInfo.type) { + case 'global': + message += `🔍 错误类型: 全局错误\n` + message += `📝 错误信息: ${errorInfo.message}\n` + if (errorInfo.source) { + message += `📂 文件: ${errorInfo.source}\n` + } + if (errorInfo.lineno) { + message += `📍 行号: ${errorInfo.lineno}:${errorInfo.colno}\n` + } + break + + case 'promise': + message += `🔍 错误类型: Promise错误\n` + message += `📝 错误信息: ${this._serializeError(errorInfo.reason)}\n` + break + + case 'console': + message += `🔍 错误类型: Console错误\n` + message += `📝 错误信息: ${errorInfo.args.join(' ')}\n` + break + + case 'miniProgram': + message += `🔍 错误类型: 小程序错误\n` + message += `📝 错误信息: ${errorInfo.error || 'Unknown'}\n` + if (errorInfo.path) { + message += `📱 页面路径: ${errorInfo.path}\n` + } + if (errorInfo.query) { + message += `🔗 查询参数: ${errorInfo.query}\n` + } + break + + case 'network': + message += `🔍 错误类型: 网络错误\n` + message += `📝 请求地址: ${errorInfo.url || 'Unknown'}\n` + message += `📝 请求方法: ${errorInfo.method || 'Unknown'}\n` + + if (errorInfo.error) { + if (typeof errorInfo.error === 'object') { + message += `🔢 错误代码: ${errorInfo.error.code || errorInfo.error.errCode || 'Unknown'}\n` + message += `📝 错误信息: ${errorInfo.error.message || errorInfo.error.errMsg || this._serializeError(errorInfo.error)}\n` + + if (errorInfo.error.errCode) { + message += `🆔 微信错误码: ${errorInfo.error.errCode}\n` + } + if (errorInfo.error.errMsg) { + message += `💬 微信错误信息: ${errorInfo.error.errMsg}\n` + } + } else { + message += `📝 错误信息: ${errorInfo.error}\n` + } + } + break + + case 'api': + message += `🔍 错误类型: 接口错误\n` + message += `📝 请求地址: ${errorInfo.url || 'Unknown'}\n` + message += `📝 请求方法: ${errorInfo.method || 'Unknown'}\n` + + if (errorInfo.requestData) { + message += `📋 请求参数: ${typeof errorInfo.requestData === 'object' ? JSON.stringify(errorInfo.requestData, null, 2) : errorInfo.requestData}\n` + } + + if (errorInfo.statusCode) { + message += `📊 状态码: ${errorInfo.statusCode}\n` + } + if (errorInfo.statusText) { + message += `📝 状态文本: ${errorInfo.statusText}\n` + } + + if (errorInfo.error) { + if (typeof errorInfo.error === 'object') { + if (errorInfo.error.code || errorInfo.error.status) { + message += `🔢 错误代码: ${errorInfo.error.code || errorInfo.error.status}\n` + } + if (errorInfo.error.message || errorInfo.error.msg) { + message += `📝 错误信息: ${errorInfo.error.message || errorInfo.error.msg}\n` + } + if (errorInfo.error.data) { + message += `📄 响应数据: ${this._serializeError(errorInfo.error.data)}\n` + } + } else { + message += `📝 错误信息: ${errorInfo.error}\n` + } + } + break + + default: + message += `🔍 错误类型: ${errorInfo.type}\n` + message += `📝 错误信息: ${this._serializeError(errorInfo.error)}\n` + } + + message += `\n📊 统计信息:\n` + message += `总计错误: ${this.errorStats.total}\n` + message += `全局错误: ${this.errorStats.global}\n` + message += `Promise错误: ${this.errorStats.promise}\n` + message += `Console错误: ${this.errorStats.console}\n` + message += `小程序错误: ${this.errorStats.miniProgram}\n` + message += `接口错误: ${this.errorStats.api}\n` + message += `网络错误: ${this.errorStats.network}\n` + message += `手动错误: ${this.errorStats.manual}\n` + + if (errorInfo.userAgent) { + message += `\n📱 设备信息:\n${errorInfo.userAgent}\n` + } + + return message + } + + /** + * 获取当前URL + * @private + */ + _getCurrentUrl() { + if (typeof window !== 'undefined') { + return window.location?.href || '' + } + + if (typeof uni !== 'undefined') { + try { + const pages = getCurrentPages() + if (pages && pages.length > 0) { + const currentPage = pages[pages.length - 1] + return currentPage.route || '' + } + } catch (error) { + // 忽略错误 + } + } + + return '' + } + + /** + * 获取用户代理信息 + * @private + */ + _getUserAgent() { + if (typeof navigator !== 'undefined') { + return navigator.userAgent || '' + } + + if (typeof uni !== 'undefined') { + try { + const systemInfo = uni.getSystemInfoSync() + return `${systemInfo.platform} ${systemInfo.system} ${systemInfo.model}` + } catch (error) { + return 'Unknown Device' + } + } + + return 'Unknown Device' + } + + /** + * 获取当前页面名称 + * @private + */ + _getCurrentPageName() { + try { + const pages = getCurrentPages() + if (pages && pages.length > 0) { + const currentPage = pages[pages.length - 1] + return currentPage.route || currentPage.$page?.fullPath || '未知页面' + } + } catch (error) { + // 忽略错误,返回默认值 + } + + if (typeof uni !== 'undefined') { + try { + const currentPages = getCurrentPages?.() + if (currentPages && currentPages.length > 0) { + return currentPages[currentPages.length - 1]?.route || '未知页面' + } + } catch (error) { + return '未知页面' + } + } + + try { + if (typeof window !== 'undefined' && window.location) { + return window.location.pathname || '未知页面' + } + } catch (error) { + return '未知页面' + } + + return '未知页面' + } + + /** + * 获取运行模式 + * @private + */ + _getMode() { + if (typeof import !== 'undefined' && import.meta?.env?.MODE) { + try { + return import.meta.env.MODE + } catch (error) { + // 忽略访问错误 + } + } + return 'unknown' + } + + /** + * 检测是否为生产环境 + * @private + */ + _isProduction() { + // 检查uniapp运行模式 + try { + const systemInfo = uni?.getSystemInfoSync?.() + if (systemInfo?.mode && systemInfo.mode !== 'default') { + // 体验版、开发版、预览版 + return false + } + } catch (error) { + // 忽略错误,继续检测 + } + + // 检查环境变量MODE + const mode = this._getMode() + if (mode === 'development' || mode === 'sandbox') { + return false + } + + // 默认:开发环境和体验版不启用,生产环境启用 + return true + } + + /** + * 序列化错误对象 + * @private + */ + _serializeError(error) { + if (error instanceof Error) { + return { + name: error.name || error.code, + message: error.message, + stack: error.stack, + } + } + + if (typeof error === 'object' && error !== null) { + try { + return JSON.stringify(error, null, 2) + } catch (e) { + return String(error) + } + } + + return String(error) + } +} + +// 导出单例 +export default new ErrorMonitor() diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..c60c0bf --- /dev/null +++ b/src/index.js @@ -0,0 +1,109 @@ +/** + * UniApp 错误监控上报插件 - 入口文件 + * 提供完整的 JavaScript 错误监控和上报解决方案 + */ + +import ErrorMonitor from './ErrorMonitor' +import { getCurrentPageName, getCurrentUrl, getUserAgent, serializeError } from './utils' + +// 创建单例实例 +const errorMonitorInstance = new ErrorMonitor() + +/** + * 初始化错误监控 + * @param {Object} options 配置选项 + * @returns {ErrorMonitor} 错误监控实例 + */ +export function initErrorMonitor(options = {}) { + return errorMonitorInstance.init(options) +} + +/** + * 手动上报错误 + * @param {string} type 错误类型 + * @param {Error|Object} error 错误对象或错误信息 + * @param {Object} context 错误上下文信息 + * @param {boolean} forceSend 强制发送(忽略环境检查) + */ +export function reportError(type = 'manual', error, context = {}, forceSend = false) { + errorMonitorInstance.reportError(type, error, context, forceSend) +} + +/** + * Promise 包装工具 + * 自动捕获 Promise 错误 + * @param {Promise} promise 要包装的 Promise + * @returns {Promise} 包装后的 Promise + */ +export function wrapPromise(promise) { + return errorMonitorInstance.wrapPromise(promise) +} + +/** + * 获取错误统计信息 + * @returns {Object} 错误统计信息 + */ +export function getErrorStats() { + return errorMonitorInstance.getErrorStats() +} + +/** + * 重置错误统计 + */ +export function resetErrorStats() { + errorMonitorInstance.resetErrorStats() +} + +/** + * 获取环境信息 + * @returns {Object} 环境信息 + */ +export function getEnvironmentInfo() { + return errorMonitorInstance.getEnvironmentInfo() +} + +/** + * 检查是否为生产环境 + * @returns {boolean} 是否为生产环境 + */ +export function isProduction() { + return errorMonitorInstance.isProduction() +} + +/** + * 设置自定义发送器 + * @param {Function} sender 自定义发送函数 + */ +export function setCustomSender(sender) { + errorMonitorInstance.setSender(sender) +} + +/** + * 设置自定义格式化函数 + * @param {Function} formatter 自定义格式化函数 + */ +export function setCustomFormatter(formatter) { + errorMonitorInstance.setFormatter(formatter) +} + +// 导出 ErrorMonitor 类本身 +export { ErrorMonitor } + +// 导出工具函数 +export { + getCurrentPageName, + getCurrentUrl, + getUserAgent, + serializeError +} + +// 导出类型定义 +export { + ErrorMonitorOptions, + ErrorType, + ErrorStats, + ErrorInfo +} from './types' + +// 导出默认实例(方便直接使用) +export default errorMonitorInstance \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..534ebe1 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,215 @@ +/** + * TypeScript 类型定义 + * 为插件提供完整的类型安全保障 + */ + +/** + * 错误监控配置选项 + */ +export interface ErrorMonitorOptions { + /** Webhook 地址(必填) */ + webhookUrl: string + /** 是否启用全局错误捕获(默认:true) */ + enableGlobalError?: boolean + /** 是否启用 Promise 错误捕获(默认:true) */ + enablePromiseError?: boolean + /** 是否启用 console.error 捕获(默认:false) */ + enableConsoleError?: boolean + /** 最大重试次数(默认:3) */ + maxRetries?: number + /** 重试延迟时间(毫秒)(默认:1000) */ + retryDelay?: number + /** 强制启用错误监控(忽略环境检查)(默认:false) */ + forceEnable?: boolean + /** 自定义格式化函数 */ + formatter?: (error: ErrorInfo) => string + /** 自定义发送器 */ + sender?: (payload: ErrorInfo) => Promise +} + +/** + * 错误类型枚举 + */ +export type ErrorType = + | 'global' // 全局 JavaScript 错误 + | 'promise' // Promise 错误 + | 'console' // console.error + | 'miniProgram' // 小程序特定错误 + | 'network' // 网络请求错误 + | 'api' // API 接口错误 + | 'manual' // 手动上报错误 + +/** + * 错误统计信息 + */ +export interface ErrorStats { + /** 总错误数 */ + total: number + /** 全局错误数 */ + global: number + /** Promise 错误数 */ + promise: number + /** Console 错误数 */ + console: number + /** 小程序错误数 */ + miniProgram: number + /** API 错误数 */ + api: number + /** 网络错误数 */ + network: number + /** 最后错误时间 */ + lastErrorTime: number | null +} + +/** + * 错误信息接口 + */ +export interface ErrorInfo { + /** 错误类型 */ + type: ErrorType + /** 错误消息 */ + error: string + /** 错误堆栈 */ + stack: string | null + /** 错误上下文 */ + context: Record + /** 错误时间戳 */ + timestamp: number + /** 错误发生的 URL */ + url: string + /** HTTP 方法(API 错误时) */ + method?: string + /** 用户代理 */ + userAgent: string + /** 页面名称 */ + page: string + + // 全局错误特有字段 + /** 错误源文件 */ + source?: string + /** 行号 */ + lineno?: number + /** 列号 */ + colno?: number + + // Promise 错误特有字段 + /** Promise 拒绝原因 */ + reason?: any + /** Promise 对象 */ + promise?: any + + // Console 错误特有字段 + /** Console 参数 */ + args?: any[] + + // 小程序错误特有字段 + /** 错误对象 */ + error?: any + /** 页面路径(pageNotFound 时) */ + path?: string + /** 查询参数(pageNotFound 时) */ + query?: string + + // 网络错误特有字段 + /** 网络错误对象 */ + networkError?: any + /** 网络类型 */ + networkType?: string + /** 连接状态 */ + isConnected?: boolean + + // API 错误特有字段 + /** HTTP 状态码 */ + statusCode?: number + /** HTTP 状态文本 */ + statusText?: string + /** 响应时间(毫秒) */ + responseTime?: number + /** 请求数据 */ + requestData?: any + /** 请求头 */ + requestHeaders?: Record + /** 请求 ID */ + requestId?: string + /** 环境信息 */ + environment?: string + /** 重试次数 */ + retryCount?: number +} + +/** + * 环境信息接口 + */ +export interface EnvironmentInfo { + /** 是否为生产环境 */ + isProduction: boolean + /** 运行环境模式 */ + mode: string + /** 平台信息 */ + platform: string + /** 错误监控是否启用 */ + errorMonitorEnabled: boolean + /** 时间戳 */ + timestamp: number +} + +/** + * 格式化函数类型 + */ +export type FormatterFunction = (error: ErrorInfo) => string + +/** + * 发送器函数类型 + */ +export type SenderFunction = (payload: ErrorInfo) => Promise + +/** + * 工具函数接口 + */ +export interface Utils { + /** 获取当前页面名称 */ + getCurrentPageName: () => string + /** 获取当前 URL */ + getCurrentUrl: () => string + /** 获取用户代理 */ + getUserAgent: () => string + /** 序列化错误对象 */ + serializeError: (error: any) => any +} + +/** + * 构造函数选项(用于类实例化) + */ +export interface ConstructorOptions extends ErrorMonitorOptions { + /** 配置选项 */ + config?: Partial +} + +/** + * Webhook 消息格式 + */ +export interface WebhookMessage { + msgtype: 'text' + text: { + content: string + mentioned_list?: string[] + } +} + +/** + * UniApp 环境信息 + */ +export interface UniAppSystemInfo { + /** 应用名称 */ + appName?: string + /** 应用版本 */ + appVersion?: string + /** 平台信息 */ + platform?: string + /** 系统信息 */ + system?: string + /** 设备型号 */ + model?: string + /** 小程序模式 */ + mode?: string +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..f717482 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,342 @@ +/** + * 工具函数 + * 提供环境检测、错误序列化等通用功能 + */ + +/** + * 获取当前页面名称 + * @returns {string} 页面名称 + */ +export function getCurrentPageName() { + try { + // 尝试从 getCurrentPages 获取(小程序环境) + if (typeof getCurrentPages !== 'undefined') { + const pages = getCurrentPages() + if (pages && pages.length > 0) { + const currentPage = pages[pages.length - 1] + return currentPage.route || currentPage.$page?.fullPath || '未知页面' + } + } + } catch (error) { + // 忽略错误,返回默认值 + } + + // Web 环境 + try { + if (typeof window !== 'undefined' && window.location) { + return window.location.pathname || '未知页面' + } + } catch (error) { + return '未知页面' + } + + // UniApp 环境 + try { + if (typeof uni !== 'undefined') { + const pages = getCurrentPages?.() + if (pages && pages.length > 0) { + return pages[pages.length - 1]?.route || '未知页面' + } + } + } catch (error) { + return '未知页面' + } + + return '未知页面' +} + +/** + * 获取当前 URL + * @returns {string} 当前 URL + */ +export function getCurrentUrl() { + // Web 环境 + if (typeof window !== 'undefined' && window.location?.href) { + return window.location.href + } + + // UniApp 小程序环境 + if (typeof uni !== 'undefined') { + try { + const pages = getCurrentPages?.() + if (pages && pages.length > 0) { + const currentPage = pages[pages.length - 1] + return currentPage?.route || '' + } + } catch (error) { + // 忽略错误 + } + } + + return '' +} + +/** + * 获取用户代理信息 + * @returns {string} 用户代理信息 + */ +export function getUserAgent() { + // Web 环境 + if (typeof navigator !== 'undefined' && navigator.userAgent) { + return navigator.userAgent + } + + // UniApp 环境 + if (typeof uni !== 'undefined') { + try { + const systemInfo = uni.getSystemInfoSync?.() + if (systemInfo) { + return `${systemInfo.platform || 'Unknown'} ${systemInfo.system || 'Unknown'} ${systemInfo.model || 'Unknown'}` + } + } catch (error) { + // 忽略错误 + } + } + + return 'Unknown Device' +} + +/** + * 序列化错误对象 + * @param {any} error 要序列化的错误对象 + * @returns {any} 序列化后的错误对象 + */ +export function serializeError(error) { + if (error instanceof Error) { + return { + name: error.name || error.code, + message: error.message, + stack: error.stack, + } + } + + if (typeof error === 'object' && error !== null) { + try { + return JSON.stringify(error, null, 2) + } catch (e) { + return String(error) + } + } + + return String(error) +} + +/** + * 检测是否为生产环境 + * @returns {boolean} 是否为生产环境 + */ +export function isProduction() { + try { + // 检查 uniapp 运行模式 + if (typeof uni !== 'undefined') { + const systemInfo = uni.getSystemInfoSync?.() + if (systemInfo?.mode && systemInfo.mode !== 'default') { + // 体验版、开发版、预览版 - 认为是非生产环境 + return false + } + } + } catch (error) { + // 忽略错误,继续检测 + } + + // 检查环境变量 MODE + try { + if (typeof import !== 'undefined' && import.meta?.env?.MODE === 'development') { + return false + } + } catch (error) { + // 忽略错误,继续检测 + } + + // 检查是否为浏览器环境 + if (typeof window !== 'undefined') { + try { + if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + return false + } + } catch (error) { + // 忽略错误 + } + } + + // 默认:开发环境和体验版不启用,生产环境启用 + return true +} + +/** + * 检测是否为 UniApp 环境 + * @returns {boolean} 是否为 UniApp 环境 + */ +export function isUniAppEnvironment() { + return typeof uni !== 'undefined' && typeof getCurrentPages !== 'undefined' +} + +/** + * 检测是否为微信小程序环境 + * @returns {boolean} 是否为微信小程序环境 + */ +export function isWeChatMiniProgram() { + if (typeof uni !== 'undefined' && uni.getSystemInfoSync) { + try { + const systemInfo = uni.getSystemInfoSync() + return systemInfo.hostName === 'wechat' || systemInfo.platform === 'devtools' + } catch (error) { + // 忽略错误 + } + } + return false +} + +/** + * 延迟执行函数 + * @param {number} ms 延迟时间(毫秒) + * @returns {Promise} Promise 对象 + */ +export function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +/** + * 重试函数 + * @param {Function} fn 要执行的函数 + * @param {number} maxRetries 最大重试次数 + * @param {number} delayTime 重试延迟时间(毫秒) + * @returns {Promise} 执行结果 + */ +export async function retry(fn, maxRetries = 3, delayTime = 1000) { + let lastError + + for (let i = 0; i <= maxRetries; i++) { + try { + return await fn() + } catch (error) { + lastError = error + if (i < maxRetries) { + await delay(delayTime * (i + 1)) + } + } + } + + throw lastError +} + +/** + * 获取系统信息 + * @returns {Object} 系统信息对象 + */ +export function getSystemInfo() { + const systemInfo = {} + + try { + if (typeof uni !== 'undefined' && uni.getSystemInfoSync) { + Object.assign(systemInfo, uni.getSystemInfoSync()) + } + } catch (error) { + // 忽略错误 + } + + try { + if (typeof navigator !== 'undefined') { + systemInfo.userAgent = navigator.userAgent + systemInfo.language = navigator.language + } + } catch (error) { + // 忽略错误 + } + + try { + if (typeof window !== 'undefined' && window.location) { + systemInfo.url = window.location.href + systemInfo.hostname = window.location.hostname + systemInfo.pathname = window.location.pathname + } + } catch (error) { + // 忽略错误 + } + + return systemInfo +} + +/** + * 验证 webhook URL 格式 + * @param {string} url 要验证的 URL + * @returns {boolean} URL 是否有效 + */ +export function isValidWebhookUrl(url) { + if (!url || typeof url !== 'string') { + return false + } + + try { + const urlObj = new URL(url) + return ['http:', 'https:'].includes(urlObj.protocol) + } catch (error) { + return false + } +} + +/** + * 深度克隆对象 + * @param {any} obj 要克隆的对象 + * @returns {any} 克隆后的对象 + */ +export function deepClone(obj) { + if (obj === null || typeof obj !== 'object') { + return obj + } + + if (obj instanceof Date) { + return new Date(obj.getTime()) + } + + if (obj instanceof Array) { + return obj.map(item => deepClone(item)) + } + + if (typeof obj === 'object') { + const clonedObj = {} + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + clonedObj[key] = deepClone(obj[key]) + } + } + return clonedObj + } + + return obj +} + +/** + * 防抖函数 + * @param {Function} func 要防抖的函数 + * @param {number} wait 等待时间(毫秒) + * @returns {Function} 防抖后的函数 + */ +export function debounce(func, wait) { + let timeout + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout) + func(...args) + } + clearTimeout(timeout) + timeout = setTimeout(later, wait) + } +} + +/** + * 节流函数 + * @param {Function} func 要节流的函数 + * @param {number} limit 限制时间(毫秒) + * @returns {Function} 节流后的函数 + */ +export function throttle(func, limit) { + let inThrottle + return function(...args) { + if (!inThrottle) { + func.apply(this, args) + inThrottle = true + setTimeout(() => inThrottle = false, limit) + } + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2969cbd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file