258 lines
8.8 KiB
JavaScript
Executable File
258 lines
8.8 KiB
JavaScript
Executable File
/**
|
||
=========================================================
|
||
Vision UI PRO React - v1.0.0
|
||
=========================================================
|
||
Product Page: https://www.creative-tim.com/product/vision-ui-dashboard-pro-react
|
||
Copyright 2021 Creative Tim (https://www.creative-tim.com/)
|
||
Design and Coded by Simmmple & Creative Tim
|
||
=========================================================
|
||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Visionware.
|
||
*/
|
||
|
||
import React, { useEffect, useRef } from "react";
|
||
import { useDispatch } from 'react-redux';
|
||
import { useLocation } from 'react-router-dom';
|
||
|
||
// Routes
|
||
import AppRoutes from './routes';
|
||
|
||
// Providers
|
||
import AppProviders from './providers/AppProviders';
|
||
|
||
// Components
|
||
import GlobalComponents from './components/GlobalComponents';
|
||
import { PerformancePanel } from './components/PerformancePanel';
|
||
|
||
// Hooks
|
||
import { useGlobalErrorHandler } from './hooks/useGlobalErrorHandler';
|
||
|
||
// Redux
|
||
// ⚡ PostHog 延迟加载:移除同步导入,首屏减少 ~180KB
|
||
// import { initializePostHog } from './store/slices/posthogSlice';
|
||
import { updateScreenSize } from './store/slices/deviceSlice';
|
||
import { injectReducer } from './store';
|
||
|
||
// Utils
|
||
import { logger } from './utils/logger';
|
||
import { performanceMonitor } from './utils/performanceMonitor';
|
||
|
||
// ⚡ PostHog 延迟加载:移除同步导入
|
||
// import { trackEvent, trackEventAsync } from '@lib/posthog';
|
||
|
||
// Contexts
|
||
import { useAuth } from '@contexts/AuthContext';
|
||
|
||
// ⚡ PostHog 延迟加载模块(动态导入后缓存)
|
||
let posthogModule = null;
|
||
let posthogSliceModule = null;
|
||
|
||
/**
|
||
* ⚡ 延迟加载 PostHog 模块
|
||
* 返回 { trackEvent, trackEventAsync, initializePostHog, posthogReducer }
|
||
*/
|
||
const loadPostHogModules = async () => {
|
||
if (posthogModule && posthogSliceModule) {
|
||
return { posthogModule, posthogSliceModule };
|
||
}
|
||
|
||
try {
|
||
const [posthog, posthogSlice] = await Promise.all([
|
||
import('@lib/posthog'),
|
||
import('./store/slices/posthogSlice'),
|
||
]);
|
||
|
||
posthogModule = posthog;
|
||
posthogSliceModule = posthogSlice;
|
||
|
||
return { posthogModule, posthogSliceModule };
|
||
} catch (error) {
|
||
logger.error('App', 'PostHog 模块加载失败', error);
|
||
return null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* ⚡ 异步追踪事件(延迟加载 PostHog 后调用)
|
||
* @param {string} eventName - 事件名称
|
||
* @param {object} properties - 事件属性
|
||
*/
|
||
const trackEventLazy = async (eventName, properties = {}) => {
|
||
// 等待模块加载完成
|
||
if (!posthogModule) {
|
||
const modules = await loadPostHogModules();
|
||
if (!modules) return;
|
||
}
|
||
|
||
// 使用异步追踪,不阻塞主线程
|
||
posthogModule.trackEventAsync(eventName, properties);
|
||
};
|
||
|
||
/**
|
||
* AppContent - 应用核心内容
|
||
* 负责 PostHog 初始化和渲染路由
|
||
*/
|
||
function AppContent() {
|
||
const dispatch = useDispatch();
|
||
const location = useLocation();
|
||
const { isAuthenticated } = useAuth();
|
||
|
||
// ✅ 使用 Ref 存储页面进入时间和路径(避免闭包问题)
|
||
const pageEnterTimeRef = useRef(Date.now());
|
||
const currentPathRef = useRef(location.pathname);
|
||
|
||
// 🎯 ⚡ PostHog 空闲时加载 + Redux 初始化(首屏不加载 ~180KB)
|
||
useEffect(() => {
|
||
const initPostHogRedux = async () => {
|
||
try {
|
||
const modules = await loadPostHogModules();
|
||
if (!modules) return;
|
||
|
||
const { posthogSliceModule } = modules;
|
||
|
||
// 动态注入 PostHog reducer
|
||
injectReducer('posthog', posthogSliceModule.default);
|
||
|
||
// 初始化 PostHog
|
||
dispatch(posthogSliceModule.initializePostHog());
|
||
|
||
// ⚡ 刷新注入前缓存的事件(避免丢失)
|
||
const pendingEvents = posthogSliceModule.flushPendingEventsBeforeInjection();
|
||
if (pendingEvents.length > 0) {
|
||
logger.info('App', `刷新 ${pendingEvents.length} 个注入前缓存的事件`);
|
||
pendingEvents.forEach(({ eventName, properties }) => {
|
||
posthogModule.trackEventAsync(eventName, properties);
|
||
});
|
||
}
|
||
|
||
logger.info('App', 'PostHog 模块空闲时加载完成,Redux 初始化已触发');
|
||
} catch (error) {
|
||
logger.error('App', 'PostHog 加载失败', error);
|
||
}
|
||
};
|
||
|
||
// ⚡ 使用 requestIdleCallback 在浏览器空闲时加载,最长等待 3 秒
|
||
if ('requestIdleCallback' in window) {
|
||
const idleId = requestIdleCallback(initPostHogRedux, { timeout: 3000 });
|
||
return () => cancelIdleCallback(idleId);
|
||
} else {
|
||
// 降级:Safari 等不支持 requestIdleCallback 的浏览器使用 setTimeout
|
||
const timer = setTimeout(initPostHogRedux, 1000);
|
||
return () => clearTimeout(timer);
|
||
}
|
||
}, [dispatch]);
|
||
|
||
// ⚡ 性能监控:标记 React 初始化完成
|
||
useEffect(() => {
|
||
performanceMonitor.mark('react-ready');
|
||
}, []);
|
||
|
||
// 📱 设备检测:监听窗口尺寸变化
|
||
useEffect(() => {
|
||
let resizeTimer;
|
||
const handleResize = () => {
|
||
// 防抖:避免频繁触发
|
||
clearTimeout(resizeTimer);
|
||
resizeTimer = setTimeout(() => {
|
||
dispatch(updateScreenSize());
|
||
}, 150);
|
||
};
|
||
|
||
window.addEventListener('resize', handleResize);
|
||
window.addEventListener('orientationchange', handleResize);
|
||
|
||
return () => {
|
||
clearTimeout(resizeTimer);
|
||
window.removeEventListener('resize', handleResize);
|
||
window.removeEventListener('orientationchange', handleResize);
|
||
};
|
||
}, [dispatch]);
|
||
|
||
// ✅ 首次访问追踪(🔴 关键事件:立即加载模块,确保数据不丢失)
|
||
useEffect(() => {
|
||
const hasVisited = localStorage.getItem('has_visited');
|
||
|
||
if (!hasVisited) {
|
||
const urlParams = new URLSearchParams(location.search);
|
||
const eventData = {
|
||
referrer: document.referrer || 'direct',
|
||
utm_source: urlParams.get('utm_source'),
|
||
utm_medium: urlParams.get('utm_medium'),
|
||
utm_campaign: urlParams.get('utm_campaign'),
|
||
landing_page: location.pathname,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
// 🔴 关键事件:立即加载 PostHog 模块并同步追踪(不使用 trackEventLazy)
|
||
// 确保首次访问数据不会因用户快速离开而丢失
|
||
(async () => {
|
||
const modules = await loadPostHogModules();
|
||
if (modules) {
|
||
// 使用同步追踪(trackEvent),而非异步追踪(trackEventAsync)
|
||
modules.posthogModule.trackEvent('first_visit', eventData);
|
||
logger.info('App', '首次访问事件已同步追踪', eventData);
|
||
}
|
||
})();
|
||
|
||
localStorage.setItem('has_visited', 'true');
|
||
}
|
||
}, [location.search, location.pathname]);
|
||
|
||
// ✅ 页面浏览时长追踪
|
||
useEffect(() => {
|
||
// 计算上一个页面的停留时长
|
||
const calculateAndTrackDuration = () => {
|
||
const exitTime = Date.now();
|
||
const duration = Math.round((exitTime - pageEnterTimeRef.current) / 1000); // 秒
|
||
|
||
// 只追踪停留时间 > 1 秒的页面(过滤快速跳转)
|
||
if (duration > 1) {
|
||
// ⚡ 使用延迟加载的异步追踪,不阻塞页面切换
|
||
trackEventLazy('page_view_duration', {
|
||
path: currentPathRef.current,
|
||
duration_seconds: duration,
|
||
is_authenticated: isAuthenticated,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
}
|
||
};
|
||
|
||
// 路由切换时追踪上一个页面的时长
|
||
if (currentPathRef.current !== location.pathname) {
|
||
calculateAndTrackDuration();
|
||
|
||
// 更新为新页面
|
||
currentPathRef.current = location.pathname;
|
||
pageEnterTimeRef.current = Date.now();
|
||
}
|
||
|
||
// 页面关闭/刷新时追踪时长
|
||
const handleBeforeUnload = () => {
|
||
calculateAndTrackDuration();
|
||
};
|
||
|
||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||
|
||
return () => {
|
||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||
};
|
||
}, [location.pathname, isAuthenticated]);
|
||
|
||
return <AppRoutes />;
|
||
}
|
||
|
||
/**
|
||
* App - 应用根组件
|
||
* 设置全局错误处理,提供 Provider 和全局组件
|
||
*/
|
||
export default function App() {
|
||
// 全局错误处理
|
||
useGlobalErrorHandler();
|
||
|
||
return (
|
||
<AppProviders>
|
||
<AppContent />
|
||
<GlobalComponents />
|
||
<PerformancePanel />
|
||
</AppProviders>
|
||
);
|
||
} |