From d26274d89d377067c1e2648192cffbf89464b361 Mon Sep 17 00:00:00 2001
From: wsj <593216709@qq.com>
Date: Wed, 28 Oct 2026 23:34:32 +0800
Subject: [PATCH] feat: 拆包
---
src/components/PageLoading.tsx | 12 ++++
vite.config.ts | 74 ++++++++++++++++++++++++
src/components/PageLoading.scss | 14 ++++
src/router/routeConfig.tsx | 21 ++++--
CLAUDE.md | 5 +
.claude/skills/new-page/SKILL.md | 8 ++
src/layouts/BasicLayout.tsx | 8 ++
7 files changed, 128 insertions(+), 14 deletions(-)
diff --git a/.claude/skills/new-page/SKILL.md b/.claude/skills/new-page/SKILL.md
index 0bb9b31..39ae836 100644
--- a/.claude/skills/new-page/SKILL.md
+++ b/.claude/skills/new-page/SKILL.md
@@ -99,9 +99,13 @@
### 4.2 注册路由
-在 `src/router/routeConfig.tsx` 中增加配置,并补充组件与图标的 import:
+在 `src/router/routeConfig.tsx` 中增加配置,并补充图标 import 与懒加载页面组件:
```tsx
+// 顶部:import { lazy } from 'react';
+const Invoices = lazy(() => import('@/pages/business/invoices'));
+
+// appRoutes 中
{
path: '/business/invoices',
title: '发票管理',
@@ -110,7 +114,7 @@
},
```
-放入现有分组时写在其 `children` 中;新增一级分组时保持与现有分组相同的结构。
+页面组件必须用 `lazy(() => import(...))` 注册,不要静态 import,否则会把页面打进首屏 chunk。放入现有分组时写在其 `children` 中;新增一级分组时保持与现有分组相同的结构。
### 4.3 更新文档
diff --git a/CLAUDE.md b/CLAUDE.md
index 3837553..81b014f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -32,6 +32,7 @@
```
- **路由与菜单**:`routeConfig.tsx` 是唯一数据源,配置 `path`、`title`、`icon`、`element` 后侧边栏与面包屑自动生效。支持 2-3 级嵌套;分组路由(无 `element`)通过 `<Outlet />` 渲染并默认跳转到第一个子页面;未匹配路由显示 404。
+- **路由懒加载**:页面组件统一通过 `lazy(() => import('@/pages/...'))` 注册,`BasicLayout` 的内容区已提供 `<Suspense>` 边界;新增页面不要用静态 import,避免所有页面打进入口 chunk。
- **登录态**:`src/store/auth.ts` 持久化 `token` 与 `user`;`src/api/auth.ts` 调用 `POST /auth/tenant-login`(`{ phone, password, tenantCode }`)。未登录访问受保护页面跳转 `/login`,登录成功后返回原地址。
- **布局与样式**:页面只负责内容区。全局 CSS 变量 `--app-primary`、`--app-bg`、`--app-border`、`--app-text`、`--app-text-secondary`;通用类 `.page-header`、`.page-toolbar`、`.filter-card`、`.table-card`、`.text-muted`;Ant Design 主题在 `main.tsx` 的 `ConfigProvider` 统一配置。
@@ -107,7 +108,7 @@
/new-page 根据 <接口文档路径>,添加一个【页面名】页面,放到【分组名】目录下
```
-仅改动现有页面(如调整某一列)等不涉及脚手架的场景才直接改代码。新页面必须在 `src/router/routeConfig.tsx` 用完整路径注册(`path`、`title`、`icon`、`element`,需要分组时使用 `children`),完成后执行 `npm run typecheck`。
+仅改动现有页面(如调整某一列)等不涉及脚手架的场景才直接改代码。新页面必须在 `src/router/routeConfig.tsx` 用完整路径注册(`path`、`title`、`icon`、`element`,需要分组时使用 `children`),页面组件使用 `lazy(() => import(...))` 懒加载,完成后执行 `npm run typecheck`。
## 五、接口与状态约定
@@ -185,4 +186,4 @@
- 使用 BrowserRouter,生产环境需配置 SPA fallback:所有前端路由回退到 `index.html`;构建产物在 `dist/`。
- 开发环境通过 Vite 代理 `/proxy-api` 转发到 `VITE_API_PROXY_TARGET`(未配置时取 `VITE_API_BASE_URL`),浏览器端不会跨域;生产环境需后端开启 CORS,或将前端与接口部署在同一域名下由反向代理转发。
-- 当前主包较大(Ant Design 全量依赖),后续可通过路由懒加载 / 手动分包优化。
+- 已启用路由懒加载与依赖拆包:`vite.config.ts` 的 `manualChunks` 将 React、antd 核心、其余依赖拆成长缓存 chunk,ProComponents 及业务页面依赖由 Rollup 按需拆分为异步 chunk。新增页面时保持懒加载即可,无需再调整拆包配置。
diff --git a/src/components/PageLoading.scss b/src/components/PageLoading.scss
new file mode 100644
index 0000000..4502c40
--- /dev/null
+++ b/src/components/PageLoading.scss
@@ -0,0 +1,14 @@
+.page-loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ min-height: 320px;
+ padding: 48px 0;
+
+ &__text {
+ color: var(--app-text-secondary);
+ font-size: 13px;
+ }
+}
diff --git a/src/components/PageLoading.tsx b/src/components/PageLoading.tsx
new file mode 100644
index 0000000..feb96e9
--- /dev/null
+++ b/src/components/PageLoading.tsx
@@ -0,0 +1,12 @@
+import { Spin } from 'antd';
+import './PageLoading.scss';
+
+/** 路由懒加载时的占位内容,尽量保持内容区高度,避免页面跳动。 */
+const PageLoading = () => (
+ <div className="page-loading" role="status" aria-live="polite">
+ <Spin size="large" />
+ <span className="page-loading__text">页面加载中…</span>
+ </div>
+);
+
+export default PageLoading;
diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx
index 713fe4d..fc6b1c4 100644
--- a/src/layouts/BasicLayout.tsx
+++ b/src/layouts/BasicLayout.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from 'react';
+import { Suspense, useEffect, useMemo, useState } from 'react';
import {
BellOutlined,
DownOutlined,
@@ -20,6 +20,7 @@
Tooltip,
} from 'antd';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
+import PageLoading from '@/components/PageLoading';
import { buildMenuItems, getOpenKeys } from '@/router/menu';
import { appRoutes, findBreadcrumb } from '@/router/routeConfig';
import { useAuthStore } from '@/store/auth';
@@ -181,7 +182,10 @@
</Header>
<Content className="app-content">
- <Outlet />
+ {/* 路由懒加载边界放在内容区内,切页时侧边栏与头部保持不动 */}
+ <Suspense fallback={<PageLoading />}>
+ <Outlet />
+ </Suspense>
</Content>
<Footer className="app-footer">
diff --git a/src/router/routeConfig.tsx b/src/router/routeConfig.tsx
index dbb62a7..226fa47 100644
--- a/src/router/routeConfig.tsx
+++ b/src/router/routeConfig.tsx
@@ -1,4 +1,4 @@
-import type { ReactNode } from 'react';
+import { lazy, type ReactNode } from 'react';
import {
ApartmentOutlined,
AppstoreOutlined,
@@ -12,13 +12,18 @@
TeamOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
-import Orders from '@/pages/business/orders';
-import CustomerList from '@/pages/customers/list';
-import Dashboard from '@/pages/dashboard';
-import AuditLogs from '@/pages/platform/audit-logs';
-import Departments from '@/pages/system/departments';
-import Roles from '@/pages/system/roles';
-import Users from '@/pages/system/users';
+
+/**
+ * 页面组件统一按路由懒加载:首屏只加载布局与当前页面,
+ * 其余页面在切换菜单时按需加载,避免所有页面打进同一个入口 chunk。
+ */
+const Orders = lazy(() => import('@/pages/business/orders'));
+const CustomerList = lazy(() => import('@/pages/customers/list'));
+const Dashboard = lazy(() => import('@/pages/dashboard'));
+const AuditLogs = lazy(() => import('@/pages/platform/audit-logs'));
+const Departments = lazy(() => import('@/pages/system/departments'));
+const Roles = lazy(() => import('@/pages/system/roles'));
+const Users = lazy(() => import('@/pages/system/users'));
export interface AppRouteItem {
/** 完整路径,同时作为菜单 key */
diff --git a/vite.config.ts b/vite.config.ts
index af0bd62..5c245e3 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,6 +2,71 @@
import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';
+/**
+ * 布局、登录等入口页面会用到的 antd 组件。
+ * 这些模块首屏必载,单独放进 antd chunk 以稳定缓存;
+ * 表格、日期选择等只在业务页面使用的组件则交给 Rollup 按需分包。
+ *
+ * 注意:不要匹配 `antd/es/index.js`(`from 'antd'` 的 barrel 文件)。
+ * manualChunks 会把 chunk 当成入口保留全部导出,barrel 进手动 chunk 后
+ * 会把所有 antd 组件都拉进首屏(约 1.3MB),失去按需分包的意义。
+ */
+const ANTD_CORE_DIRS = [
+ '_util',
+ 'app',
+ 'avatar',
+ 'breadcrumb',
+ 'button',
+ 'config-provider',
+ 'dropdown',
+ 'form',
+ 'input',
+ 'layout',
+ 'locale',
+ 'menu',
+ 'message',
+ 'modal',
+ 'notification',
+ 'space',
+ 'spin',
+ 'style',
+ 'tag',
+ 'theme',
+ 'tooltip',
+ 'version',
+ 'wave',
+].join('|');
+
+/** 第三方依赖拆包规则,页面本身通过路由懒加载按需加载。 */
+const resolveVendorChunk = (id: string): string | undefined => {
+ if (!id.includes('node_modules')) return undefined;
+
+ const path = id.replace(/\\/g, '/');
+
+ // 图标与 antd 互相引用,放在同一个 chunk,避免循环依赖。
+ if (
+ new RegExp(
+ `\\/node_modules\\/(antd\\/es\\/(${ANTD_CORE_DIRS})\\/|@ant-design\\/(cssinjs|cssinjs-utils|colors|fast-color|icons|icons-svg)\\/)`,
+ ).test(path)
+ ) {
+ return 'antd';
+ }
+
+ if (
+ /\/node_modules\/(react|react-dom|scheduler|react-router|react-router-dom|@remix-run\/[^/]+)\//.test(
+ path,
+ )
+ ) {
+ return 'react';
+ }
+
+ if (/\/node_modules\/(axios|zustand|dayjs)\//.test(path)) {
+ return 'vendor';
+ }
+
+ return undefined;
+};
+
/** 开发环境通过 Vite 代理访问后端,避免浏览器跨域。 */
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
@@ -26,5 +91,14 @@
},
},
},
+ build: {
+ // 首屏 chunk 已控制在 600KB 以内;按需加载的 ProComponents 业务包约 830KB。
+ chunkSizeWarningLimit: 900,
+ rollupOptions: {
+ output: {
+ manualChunks: resolveVendorChunk,
+ },
+ },
+ },
};
});
--
Gitblit v1.9.3