wsj
3 days ago d26274d89d377067c1e2648192cffbf89464b361
feat: 拆包
5 files modified
2 files added
140 ■■■■■ changed files
.claude/skills/new-page/SKILL.md 8 ●●●●● patch | view | raw | blame | history
CLAUDE.md 5 ●●●●● patch | view | raw | blame | history
src/components/PageLoading.scss 14 ●●●●● patch | view | raw | blame | history
src/components/PageLoading.tsx 12 ●●●●● patch | view | raw | blame | history
src/layouts/BasicLayout.tsx 6 ●●●●● patch | view | raw | blame | history
src/router/routeConfig.tsx 21 ●●●●● patch | view | raw | blame | history
vite.config.ts 74 ●●●●● patch | view | raw | blame | history
.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 更新文档
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。新增页面时保持懒加载即可,无需再调整拆包配置。
src/components/PageLoading.scss
New file
@@ -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;
  }
}
src/components/PageLoading.tsx
New file
@@ -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;
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">
          {/* 路由懒加载边界放在内容区内,切页时侧边栏与头部保持不动 */}
          <Suspense fallback={<PageLoading />}>
          <Outlet />
          </Suspense>
        </Content>
        <Footer className="app-footer">
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 */
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,
        },
      },
    },
  };
});