feat(mp): AES-256-GCM 加密存储 + 安全日志 + ErrorBoundary 升级 + BLE 并发修复
- secure-storage-aes: AES-256-GCM 替代 XOR,保留 XOR 迁移读取 - crypto-polyfill: wx.getRandomValuesSync → crypto.getRandomValues - logger.ts: dev/prod 区分日志级别,生产不输出详情 - ErrorBoundary: 错误分类(network/render/unknown) + 结构化日志 - DataSyncScheduler: isSyncing 互斥防并发重复同步 - app.tsx 首行导入 crypto-polyfill
This commit is contained in:
@@ -1,31 +1,77 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import './index.scss';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
retryCount: number;
|
||||
errorCategory: ErrorCategory;
|
||||
}
|
||||
|
||||
type ErrorCategory = 'network' | 'render' | 'unknown';
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
function classifyError(error: Error): ErrorCategory {
|
||||
const msg = error.message?.toLowerCase() || '';
|
||||
if (
|
||||
msg.includes('network') ||
|
||||
msg.includes('fetch') ||
|
||||
msg.includes('timeout') ||
|
||||
msg.includes('request:fail')
|
||||
) {
|
||||
return 'network';
|
||||
}
|
||||
if (
|
||||
msg.includes('cannot read properties') ||
|
||||
msg.includes('is not defined') ||
|
||||
msg.includes('is not a function') ||
|
||||
msg.includes('render')
|
||||
) {
|
||||
return 'render';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function logError(error: Error, info: React.ErrorInfo, category: ErrorCategory): void {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const entry = {
|
||||
ts: new Date().toISOString(),
|
||||
category,
|
||||
message: error.message,
|
||||
stack: isDev ? error.stack : undefined,
|
||||
componentStack: isDev ? info.componentStack : undefined,
|
||||
};
|
||||
|
||||
if (isDev) {
|
||||
console.error('[ErrorBoundary]', JSON.stringify(entry, null, 2));
|
||||
} else {
|
||||
console.error('[ErrorBoundary]', entry.ts, entry.category, entry.message);
|
||||
}
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, retryCount: 0 };
|
||||
this.state = { hasError: false, retryCount: 0, errorCategory: 'unknown' };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(): Partial<State> {
|
||||
return { hasError: true };
|
||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
||||
return { hasError: true, errorCategory: classifyError(error) };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
console.error('[ErrorBoundary]', error, info.componentStack);
|
||||
this.setState((prev) => ({ retryCount: prev.retryCount + 1 }));
|
||||
const category = classifyError(error);
|
||||
logError(error, info, category);
|
||||
this.setState((prev) => ({
|
||||
retryCount: prev.retryCount + 1,
|
||||
errorCategory: category,
|
||||
}));
|
||||
}
|
||||
|
||||
handleRetry = () => {
|
||||
@@ -34,21 +80,28 @@ export default class ErrorBoundary extends Component<Props, State> {
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
const exceeded = this.state.retryCount >= MAX_RETRIES;
|
||||
const isNetwork = this.state.errorCategory === 'network';
|
||||
const title = isNetwork ? '网络连接失败' : '页面出了点问题';
|
||||
const desc = exceeded
|
||||
? '请重启小程序后重试'
|
||||
: isNetwork
|
||||
? '请检查网络后重试'
|
||||
: '请返回重试';
|
||||
|
||||
return (
|
||||
<View className='error-boundary'>
|
||||
<View className='error-icon-wrap'>
|
||||
<Text className='error-icon-text'>!</Text>
|
||||
</View>
|
||||
<Text className='error-title'>页面出了点问题</Text>
|
||||
<Text className='error-desc'>
|
||||
{exceeded ? '请重启小程序' : '请返回重试'}
|
||||
</Text>
|
||||
<Text className='error-title'>{title}</Text>
|
||||
<Text className='error-desc'>{desc}</Text>
|
||||
{!exceeded && (
|
||||
<View
|
||||
className='error-retry-btn'
|
||||
onClick={this.handleRetry}
|
||||
>
|
||||
<View className='error-retry-btn' onClick={this.handleRetry}>
|
||||
<Text className='error-retry-text'>重新加载</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user