diff --git a/src/components/RouterContainer.tsx b/src/components/RouterContainer.tsx index 307da7f..03e51cd 100644 --- a/src/components/RouterContainer.tsx +++ b/src/components/RouterContainer.tsx @@ -1,11 +1,41 @@ -import { FEATURES } from '@/config/features'; +import { getFeatureByKey } from '@/config/features'; +import { loadPage } from '@/config/pageLoaders/index'; import { useRouter } from '@/providers/RouterProvider'; -import { Suspense } from 'react'; +import type { PageType } from '@/types/storage'; +import { type ComponentType, useEffect, useState } from 'react'; import PageErrorBoundary from '@/components/PageErrorBoundary'; import PageSkeleton from '@/components/PageSkeleton'; import { cn } from '@/lib/utils'; import { AlertTriangle } from 'lucide-react'; +function LoadedPage({ pageKey }: { pageKey: PageType }) { + const [Page, setPage] = useState(null); + + useEffect(() => { + let cancelled = false; + + loadPage(pageKey) + .then((mod) => { + if (!cancelled) { + setPage(() => mod.default); + } + }) + .catch((err) => { + console.error('[Router Page Load Error]', err); + }); + + return () => { + cancelled = true; + }; + }, [pageKey]); + + if (!Page) { + return ; + } + + return ; +} + export default function RouterContainer() { const { currentPage, isLoaded } = useRouter(); @@ -16,8 +46,7 @@ export default function RouterContainer() { return ; } - const currentFeature = FEATURES.find((f) => f.key === currentPage); - const MatchedComponent = currentFeature?.component; + const currentFeature = getFeatureByKey(currentPage); return (
- } - > - - {MatchedComponent ? ( - - ) : ( -
-
- -
-

页面未找到

-

- 该功能不存在或已被移除。 -

+ + {currentFeature ? ( + + ) : ( +
+
+
- )} - - +

页面未找到

+

+ 该功能不存在或已被移除。 +

+
+ )} +
); } diff --git a/src/config/__tests__/features.lazy.test.ts b/src/config/__tests__/features.lazy.test.ts index 4556a40..5588792 100644 --- a/src/config/__tests__/features.lazy.test.ts +++ b/src/config/__tests__/features.lazy.test.ts @@ -67,19 +67,21 @@ describe('features 懒加载', () => { expect(pageLoadTracker.loaded).toEqual([]); }); - it('渲染 lazy 组件时才应加载对应页面模块', async () => { - const React = await import('react'); - const { render, waitFor } = await import('@testing-library/react'); - const { FEATURES } = await import('@/config/features'); + it('loadPage 应仅加载对应页面模块', async () => { + const { loadPage } = await import('@/config/pageLoaders/index'); - const DashboardPage = FEATURES.find((f) => f.key === 'dashboard')!.component; + await loadPage('dashboard'); - render( - React.createElement(React.Suspense, { fallback: null }, React.createElement(DashboardPage)), - ); + expect(pageLoadTracker.loaded).toEqual(['Dashboard']); + }); - await waitFor(() => { - expect(pageLoadTracker.loaded).toEqual(['Dashboard']); - }); + it('loadPage 切换页面时不应加载无关页面模块', async () => { + const { loadPage } = await import('@/config/pageLoaders/index'); + + await loadPage('timestamp'); + + expect(pageLoadTracker.loaded).toEqual(['Timestamp']); + expect(pageLoadTracker.loaded).not.toContain('QrCode'); + expect(pageLoadTracker.loaded).not.toContain('TestDataGenerator'); }); }); diff --git a/src/config/__tests__/features.test.ts b/src/config/__tests__/features.test.ts index db69010..d6b06d0 100644 --- a/src/config/__tests__/features.test.ts +++ b/src/config/__tests__/features.test.ts @@ -19,13 +19,11 @@ describe('features', () => { expect(feature).toHaveProperty('label'); expect(feature).toHaveProperty('description'); expect(feature).toHaveProperty('defaultVisible'); - expect(feature).toHaveProperty('component'); + expect(feature).not.toHaveProperty('component'); expect(typeof feature.key).toBe('string'); expect(typeof feature.label).toBe('string'); expect(typeof feature.description).toBe('string'); expect(typeof feature.defaultVisible).toBe('boolean'); - expect(feature.component).toBeDefined(); - expect(['function', 'object']).toContain(typeof feature.component); if (feature.key !== 'dashboard') { expect(feature).toHaveProperty('icon'); diff --git a/src/config/features.tsx b/src/config/featureMeta.ts similarity index 74% rename from src/config/features.tsx rename to src/config/featureMeta.ts index 1ca5594..c2a274e 100644 --- a/src/config/features.tsx +++ b/src/config/featureMeta.ts @@ -1,4 +1,4 @@ -import { type ComponentType, lazy } from 'react'; +import type { ComponentType } from 'react'; import type { LucideProps } from 'lucide-react'; import type { PageType } from '@/types/storage'; import { @@ -15,18 +15,6 @@ import { export type PaletteColorKey = 'primary' | 'success' | 'warning' | 'error' | 'secondary' | 'info'; -// 懒加载页面组件 -const DashboardPage = lazy(() => import('@/pages/Dashboard')); -const TimestampPage = lazy(() => import('@/pages/Timestamp')); -const StorageCleanerPage = lazy(() => import('@/pages/StorageCleaner')); -const QrCodePage = lazy(() => import('@/pages/QrCode')); -const TextStatisticsPage = lazy(() => import('@/pages/TextStatistics')); -const JwtPage = lazy(() => import('@/pages/Jwt')); -const JsonToolsPage = lazy(() => import('@/pages/JsonTools')); -const Base64ConverterPage = lazy(() => import('@/pages/Base64Converter')); -const RightClickRestorerPage = lazy(() => import('@/pages/RightClickRestorer')); -const TestDataGeneratorPage = lazy(() => import('@/pages/TestDataGenerator')); - export interface FeatureConfig { key: PageType; label: string; @@ -34,7 +22,6 @@ export interface FeatureConfig { themeColorKey?: PaletteColorKey; icon?: ComponentType; defaultVisible: boolean; - component: ComponentType; } export const FEATURES: FeatureConfig[] = [ @@ -43,7 +30,6 @@ export const FEATURES: FeatureConfig[] = [ label: '仪表盘', description: '', defaultVisible: true, - component: DashboardPage, }, { key: 'timestamp', @@ -52,7 +38,6 @@ export const FEATURES: FeatureConfig[] = [ themeColorKey: 'primary', icon: Clock, defaultVisible: true, - component: TimestampPage, }, { key: 'storageCleaner', @@ -61,7 +46,6 @@ export const FEATURES: FeatureConfig[] = [ themeColorKey: 'warning', icon: Database, defaultVisible: true, - component: StorageCleanerPage, }, { key: 'qrCode', @@ -70,7 +54,6 @@ export const FEATURES: FeatureConfig[] = [ themeColorKey: 'success', icon: QrCode, defaultVisible: true, - component: QrCodePage, }, { key: 'textStatistics', @@ -79,7 +62,6 @@ export const FEATURES: FeatureConfig[] = [ themeColorKey: 'secondary', icon: FileText, defaultVisible: true, - component: TextStatisticsPage, }, { key: 'jwt', @@ -88,7 +70,6 @@ export const FEATURES: FeatureConfig[] = [ themeColorKey: 'info', icon: Key, defaultVisible: true, - component: JwtPage, }, { key: 'jsonTools', @@ -97,7 +78,6 @@ export const FEATURES: FeatureConfig[] = [ themeColorKey: 'primary', icon: GitCompareArrows, defaultVisible: true, - component: JsonToolsPage, }, { key: 'base64Converter', @@ -106,7 +86,6 @@ export const FEATURES: FeatureConfig[] = [ themeColorKey: 'info', icon: ArrowLeftRight, defaultVisible: true, - component: Base64ConverterPage, }, { key: 'rightClickRestorer', @@ -115,7 +94,6 @@ export const FEATURES: FeatureConfig[] = [ themeColorKey: 'success', icon: MousePointerClick, defaultVisible: true, - component: RightClickRestorerPage, }, { key: 'testDataGenerator', @@ -124,7 +102,6 @@ export const FEATURES: FeatureConfig[] = [ themeColorKey: 'warning', icon: FileSpreadsheet, defaultVisible: true, - component: TestDataGeneratorPage, }, ]; diff --git a/src/config/features.ts b/src/config/features.ts new file mode 100644 index 0000000..63648e3 --- /dev/null +++ b/src/config/features.ts @@ -0,0 +1,9 @@ +export type { FeatureConfig, PaletteColorKey } from '@/config/featureMeta'; +export { + FEATURES, + getAllFeatureKeys, + getDefaultPageOrder, + getDefaultVisibleFeatureKeys, + getEntryPointType, + getFeatureByKey, +} from '@/config/featureMeta'; diff --git a/src/config/pageLoaders/base64Converter.ts b/src/config/pageLoaders/base64Converter.ts new file mode 100644 index 0000000..4a8a28d --- /dev/null +++ b/src/config/pageLoaders/base64Converter.ts @@ -0,0 +1,5 @@ +import type { ComponentType } from 'react'; + +export default function loadBase64Converter(): Promise<{ default: ComponentType }> { + return import('@/pages/Base64Converter'); +} diff --git a/src/config/pageLoaders/dashboard.ts b/src/config/pageLoaders/dashboard.ts new file mode 100644 index 0000000..e4b3377 --- /dev/null +++ b/src/config/pageLoaders/dashboard.ts @@ -0,0 +1,5 @@ +import type { ComponentType } from 'react'; + +export default function loadDashboard(): Promise<{ default: ComponentType }> { + return import('@/pages/Dashboard'); +} diff --git a/src/config/pageLoaders/index.ts b/src/config/pageLoaders/index.ts new file mode 100644 index 0000000..4a96f8e --- /dev/null +++ b/src/config/pageLoaders/index.ts @@ -0,0 +1,33 @@ +import type { ComponentType } from 'react'; +import type { PageType } from '@/types/storage'; + +type PageModule = { default: ComponentType }; + +export function loadPage(key: PageType): Promise { + switch (key) { + case 'dashboard': + return import('@/config/pageLoaders/dashboard').then((m) => m.default()); + case 'timestamp': + return import('@/config/pageLoaders/timestamp').then((m) => m.default()); + case 'storageCleaner': + return import('@/config/pageLoaders/storageCleaner').then((m) => m.default()); + case 'qrCode': + return import('@/config/pageLoaders/qrCode').then((m) => m.default()); + case 'textStatistics': + return import('@/config/pageLoaders/textStatistics').then((m) => m.default()); + case 'jwt': + return import('@/config/pageLoaders/jwt').then((m) => m.default()); + case 'jsonTools': + return import('@/config/pageLoaders/jsonTools').then((m) => m.default()); + case 'base64Converter': + return import('@/config/pageLoaders/base64Converter').then((m) => m.default()); + case 'rightClickRestorer': + return import('@/config/pageLoaders/rightClickRestorer').then((m) => m.default()); + case 'testDataGenerator': + return import('@/config/pageLoaders/testDataGenerator').then((m) => m.default()); + default: { + const _exhaustive: never = key; + return Promise.reject(new Error(`Unknown page: ${String(_exhaustive)}`)); + } + } +} diff --git a/src/config/pageLoaders/jsonTools.ts b/src/config/pageLoaders/jsonTools.ts new file mode 100644 index 0000000..ae62f3d --- /dev/null +++ b/src/config/pageLoaders/jsonTools.ts @@ -0,0 +1,5 @@ +import type { ComponentType } from 'react'; + +export default function loadJsonTools(): Promise<{ default: ComponentType }> { + return import('@/pages/JsonTools'); +} diff --git a/src/config/pageLoaders/jwt.ts b/src/config/pageLoaders/jwt.ts new file mode 100644 index 0000000..86e4954 --- /dev/null +++ b/src/config/pageLoaders/jwt.ts @@ -0,0 +1,5 @@ +import type { ComponentType } from 'react'; + +export default function loadJwt(): Promise<{ default: ComponentType }> { + return import('@/pages/Jwt'); +} diff --git a/src/config/pageLoaders/qrCode.ts b/src/config/pageLoaders/qrCode.ts new file mode 100644 index 0000000..879596c --- /dev/null +++ b/src/config/pageLoaders/qrCode.ts @@ -0,0 +1,5 @@ +import type { ComponentType } from 'react'; + +export default function loadQrCode(): Promise<{ default: ComponentType }> { + return import('@/pages/QrCode'); +} diff --git a/src/config/pageLoaders/rightClickRestorer.ts b/src/config/pageLoaders/rightClickRestorer.ts new file mode 100644 index 0000000..aa70570 --- /dev/null +++ b/src/config/pageLoaders/rightClickRestorer.ts @@ -0,0 +1,5 @@ +import type { ComponentType } from 'react'; + +export default function loadRightClickRestorer(): Promise<{ default: ComponentType }> { + return import('@/pages/RightClickRestorer'); +} diff --git a/src/config/pageLoaders/storageCleaner.ts b/src/config/pageLoaders/storageCleaner.ts new file mode 100644 index 0000000..e9212b8 --- /dev/null +++ b/src/config/pageLoaders/storageCleaner.ts @@ -0,0 +1,5 @@ +import type { ComponentType } from 'react'; + +export default function loadStorageCleaner(): Promise<{ default: ComponentType }> { + return import('@/pages/StorageCleaner'); +} diff --git a/src/config/pageLoaders/testDataGenerator.ts b/src/config/pageLoaders/testDataGenerator.ts new file mode 100644 index 0000000..adbb3fa --- /dev/null +++ b/src/config/pageLoaders/testDataGenerator.ts @@ -0,0 +1,5 @@ +import type { ComponentType } from 'react'; + +export default function loadTestDataGenerator(): Promise<{ default: ComponentType }> { + return import('@/pages/TestDataGenerator'); +} diff --git a/src/config/pageLoaders/textStatistics.ts b/src/config/pageLoaders/textStatistics.ts new file mode 100644 index 0000000..558460e --- /dev/null +++ b/src/config/pageLoaders/textStatistics.ts @@ -0,0 +1,5 @@ +import type { ComponentType } from 'react'; + +export default function loadTextStatistics(): Promise<{ default: ComponentType }> { + return import('@/pages/TextStatistics'); +} diff --git a/src/config/pageLoaders/timestamp.ts b/src/config/pageLoaders/timestamp.ts new file mode 100644 index 0000000..498ef61 --- /dev/null +++ b/src/config/pageLoaders/timestamp.ts @@ -0,0 +1,5 @@ +import type { ComponentType } from 'react'; + +export default function loadTimestamp(): Promise<{ default: ComponentType }> { + return import('@/pages/Timestamp'); +} diff --git a/wxt.config.ts b/wxt.config.ts index d17a215..d8747fe 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -22,14 +22,10 @@ function manualChunksForHtmlOnly(): Plugin { ) { return 'vendor-react'; } - // 二维码活态感知依赖分流 - if (id.includes('qr-scanner') || id.includes('qrious')) { - return 'vendor-qr'; - } - // 拖拽排序高级组件分流 - if (id.includes('@dnd-kit')) { - return 'vendor-dnd'; - } + + // qrious / qr-scanner / @dnd-kit 不单独拆 vendor chunk: + // 独立 vendor 会与 Vite preload 辅助函数共 chunk,被 App Shell 静态拉取。 + // 这些依赖随各自页面 async chunk 加载即可。 return undefined; }, @@ -77,6 +73,7 @@ export default defineConfig({ vite: () => ({ plugins: [manualChunksForHtmlOnly()], build: { + modulePreload: false, minify: 'terser', terserOptions: { format: { ascii_only: true, comments: false },