Files
antd-admin/docs/specs/2026-04-24-with-lingui-optimization-plan.md
T
zuiidea c5221c71be feat(with-lingui): orders CRUD, shared hooks, auth refresh, and docs
- Add second sample resource (orders): API, schemas, MSW, routes, menu, Lingui, and E2E.

- Introduce useCrudToasts, useUrlSearchState, useTableFitHeight; extend useResourceCRUD with optimistic updates; add Vitest coverage for hooks and createHandler.

- Implement token refresh in http layer, RouteError, and new E2E specs (auth-refresh, rbac, url-state).

- Replace gen-resource codegen with English add-resource.instructions.md; fix Copilot instruction YAML in with-lingui and basic.

- Add with-lingui CI workflow, Cursor thin rules, optimization plan doc, and refresh README/AGENTS.

- Bump packages/create and lockfile where applicable.

Made-with: Cursor
2026-04-27 14:47:55 +08:00

14 KiB
Raw Blame History

apps/with-lingui 模板优化计划

状态:待评审 日期:2026-04-24 范围:apps/with-lingui(apps/basic 与主模板抽象稳定后再考虑同步,当前不做回填) 面向:人类贡献者 + AI Agent(结合 .github/instructions/ 指令体系执行)

0. 背景

apps/with-lingui 作为仓库主模板,技术栈(React 19 / AntD 6 / TanStack Router & Query / Zod v4 / Lingui / MSW 2 / Vite+)已属第一梯队,AI 友好度(scoped instructions + 分包策略)领先同类开源 admin 模板。本计划聚焦下一轮可见收益,把页面级样板下沉到 hook/工具链,把 AI 指令从"规范"升级为"可执行食谱"。

详细评价参见内部讨论记录,本文只列执行项。

1. 目标与非目标

目标

  • 安全性:闭合 JWT 刷新流、移除 store/http 跨层耦合。
  • 可维护性:将 routes/_auth/users/index.tsx(491 行)中的通用能力下沉为 hook,使新增资源页 <200 行、<30 分钟。
  • 可观测性:RBAC / URL 状态等进入核心 E2E;体积以 vp build 日志与 Code review 把关(不在 CI 中跑自动化 bundle 上限工具)。
  • AI 协作:补齐"加资源食谱"与 Cursor 规则镜像;不提供 codegen 脚本,以 add-resource.instructions.md 为唯一执行清单(对照 users / orders)。

非目标

  • 不替换现有技术栈(Vite+ / Lingui / AntD 6 / TanStack 保持不变)。
  • apps/basic 回填:抽象未稳定前不做;不与 with-lingui 强行双轨同步。
  • 不引入新的 UI 或样式库(遵循 frontend.instructions.md)。

2. 执行项(按优先级)

P0:正确性与启动性能

  • P0-1 解耦 http ↔ auth store
    • 现状:apps/with-lingui/src/utils/http.ts L35–46 直接 localStorage.getItem("auth-storage") 再 JSON.parse,与 stores/auth.ts 的 partialize 结构强耦合。
    • 方案:
      • 在 stores/auth.ts 导出 getAccessToken() / getTokens() 工具。
      • http.ts 改为 import { getAccessToken } from "@/stores/auth"。
      • getAuthHeaders() 保留但内部改调用工具。
    • 验收:vp check --no-fmt + 登录 E2E 通过;搜索仓库不再出现 "auth-storage" 字面量(除 store 自身)。
  • P0-2 实现 401/403 拦截与 refresh token 流
    • 现状:AuthTokensSchema 已含 refreshToken,但 request() 未使用;403 由各页面自行处理。
    • 方案:
      • utils/http.ts 拦截 401:调 POST /api/auth/refresh(新增端点常量 + schema + MSW handler),并发去重(inflightRefresh: Promise | null),成功后回放原请求。
      • 拦截 403:通过 @tanstack/react-router 的 router.navigate({ to: "/403" });为此导出一个可注入 router 的 installHttpInterceptors(router),在 main.tsx 调用。
      • 刷新失败 → useAuthStore.getState().logout() + 导向 /login。
    • 新增文件:
      • src/api/auth.ts 增加 REFRESH 常量。
      • src/mocks/handlers/auth.ts 增加 refresh handler(含错误分支模拟)。
    • 验收:新增 E2E e2e/auth-refresh.spec.ts 覆盖:过期 access token → 自动刷新 → 请求成功;刷新失败 → 登出并跳登录。
  • P0-3 启动链并行化
    • 现状:src/main.tsx L26–49 串行 5 步:enableMocking → rehydrate settings → loadLocale → rehydrate auth → fetchSession → render。
    • 方案:
      • loadLocaleCatalog 与 useAuthStore.persist.rehydrate() 并行(Promise.all)。
      • fetchSessionAndApplyToStore() 从 main.tsx 移除,改到 routes/_auth.tsx 的 beforeLoad 内按需触发(路由守卫已经会校验 isAuthenticated)。
      • 保留渲染前仅做"i18n 激活 + 主题 hydrate"两个硬依赖。
    • 验收:本地 DevTools Performance 首屏 TTI 改善 ≥ 150ms;刷新已登录页仍能正确拉取 session。

P1:架构清晰度

  • P1-1 抽出 useTableFitHeight hook
    • 现状:routes/_auth/users/index.tsx L291–384 约 90 行 DOM 测量(ResizeObserver + rAF 重试 + 主布局 clipBottom 计算)写在页面内。
    • 方案:
      • 新文件 src/hooks/useTableFitHeight.ts,输入 { pageShellRef, toolbarRef, middleRef, tableFrameRef, deps },返回 { tableAreaMaxHeight, tableScrollY, lockScrollHeight }。
      • 页面只保留 ref 声明与调用。
    • 验收:users/index.tsx 行数 < 260;暗色 / 不同分辨率下无双滚动条(以核心 E2E + 人工回归为准)。
  • P1-2 抽出 useCrudToasts hook
    • 现状:users/index.tsx L101–161 每个资源都要写 ~60 行 create/update/delete lifecycle toasts。
    • 方案:
      • 新文件 src/hooks/useCrudToasts.ts,接收 { resourceLabel: () => string }(配合 Lingui macro),内部返回 createLifecycle / updateLifecycle / deleteLifecycle 三个对象,可直接传入 useResourceCRUD。
      • 文案用 Lingui macro;允许覆盖单条文案。
    • 验收:users/index.tsx 中不再出现 MESSAGE_KEY_USER_* 常量与裸 t\... successfully` 重复;至少一个新资源页引用并通过 E2E。
  • P1-3 抽出 useUrlSearchState hook
    • 现状:users/index.tsx 手写 keywordInput 本地态与 search.keyword 同步 + applySearch。
    • 方案:
      • 新文件 src/hooks/useUrlSearchState.ts,基于 useNavigate + Route.useSearch(),接收 zod schema,返回 { search, patch, keyword, setKeyword, commitKeyword }。
      • users/index.tsx 与未来资源页共享。
    • 验收:users.spec.ts 新增"刷新保留分页/关键词/排序"用例通过。
  • P1-4 useResourceCRUD 人因优化
    • 现状:src/hooks/useResourceCRUD.ts 调用方需手写三个泛型 + queryKey 与 invalidateKey 两次。
    • 方案:
      • invalidateKey 默认取 queryKey[0]。
      • 导出 CrudResult<T> 类型别名。
      • 可选 optimistic: { update?: boolean; delete?: boolean },内部 onMutate 里做乐观 setQueryData;失败自动回滚。
    • 验收:用户列表删除 / 编辑在慢网(Playwright route 延时)下立即反馈;回滚路径有单测覆盖。
    • 落地:optimistic: { update, delete } + cancelQueries / 失败时 setQueryData 恢复快照;pnpm run test:unit(src/hooks/useResourceCRUD.test.ts)覆盖 merge/delete 与 rollback 契约;users / orders 已开启乐观更新。
  • P1-5 MSW handler 端也跑一次 schema 解析
    • 现状:mock 与契约漂移只能在前端 Zod.parse 抛错时发现,堆栈离源头远。
    • 方案:
      • src/mocks/createHandler.ts:successWithSchema;paginatedListSchema + paginatedWithSchema(itemSchema, payload);successWithNullBody 用于 data: null 响应。
      • handlers/auth.ts / user.ts / orders.ts 全部经 schema 出包;新增 handler 须沿用同一模式。
    • 验收:故意改错 mock 字段名,dev 控制台首条报错来自 mock 层;vp test 下 handler 单元测试覆盖。
    • 落地:src/mocks/createHandler.test.ts(pnpm run test:unit)覆盖 successWithSchema / paginatedWithSchema / successWithNullBody 的校验与 Zod 抛错路径。

P2:体验与工程化

  • P2-1 统一错误兜底
    • 根路由 __root.tsx 追加 errorComponent;QueryClient.defaultOptions.mutations.onError 挂接全局 App.useApp().message.error。
    • 全站 message/modal/notification 统一使用 App.useApp()(禁止静态方法),通过 oxlint 规则(或 code review checklist)约束。
  • P2-2 品牌 token 化
    • 在 src/hooks/tokenBuilders.ts 定义 BRAND_TOKENS(primary、link、info、success 等),深浅色分别给一份;替换默认 AntD 蓝。
    • 允许通过 VITE_BRAND_PRIMARY 环境变量覆盖,便于二次分发。
  • P2-3 Bundle 体积(已决议:不再采用自动化 CI 预算)
    • 不再采用 size-limit / @size-limit/file(及同类在 CI 中卡阈值的方案);已从 apps/with-lingui 与 with-lingui workflow 移除。
    • 仍通过 manualChunks + vp build 输出 观察各 chunk;重大依赖或分片变更在 PR 描述中注明体积影响即可。
    • refactor.instructions.md 仅保留「分包与避免无谓大依赖」的叙述性建议,不写死 CI 阈值命令。
  • P2-4 i18n 预热
    • __root.tsx 在 requestIdleCallback 内预拉另一语言 catalog;切换瞬时完成。
    • document.documentElement 预留 dir 属性钩子,为未来 RTL 留接口(不实现 RTL 样式)。

P3:AI 友好度放大

  • P3-1 新增"加资源"食谱
    • 新文件 apps/with-lingui/.github/instructions/add-resource.instructions.md(英文正文),front-matter 指定触发关键词(add resource / CRUD / scaffold 等)。
    • 内容为 6 步 checklist + 每步文件路径 + 验证命令,示例见下方"附录 A"。
  • P3-2 Cursor 薄规则(方案 A)
    • 细则单源:apps/with-lingui/.github/instructions/*.instructions.md。
    • 仓库根 .cursor/rules/with-lingui-*.mdc 五份:仅 description + globs + 指向上述路径;不重复粘贴长文;.gitignore 放行 .cursor/rules/ 以便进库。
    • apps/with-lingui/README.md 与 instructions/README.md 说明用法(必要时 @ 引用 .instructions.md)。
  • P3-3 加资源(无脚本 codegen)
    • 实现形态:仅 add-resource.instructions.md 食谱 + users / orders 示例;由贡献者或 Agent 按步骤手工落地。
    • 产出与原先 codegen 相当:api/<name>.ts、schemas 片段、mock、路由、e2e、菜单等。
    • 验收:按食谱新增一资源后 pnpm exec vp check --no-fmt && pnpm test:e2e:core 通过(可将新 spec 纳入 core 或单跑)。

3. 落地节奏

阶段 包含项 预期工时
Sprint 1 P0-1 / P0-2 / P0-3 / P1-1 2–3 天
Sprint 2 P1-2 / P1-3 / P1-4 / P1-5 / P3-1 2 天
Sprint 3 P2-1 ~ P2-4 + P3-2 + P3-3(食谱) 2 天
Sprint 4 (预留)抽象稳定后再评估 apps/basic 与其它增强项 待定

每 Sprint 结束:vp check --no-fmt && pnpm run test:e2e:core && pnpm run build(在 apps/with-lingui 下),并关注构建日志中的 chunk 体积。

4. 风险与对策

风险 影响 对策
refresh token 并发去重实现错误,风暴请求 线上故障 用 inflightRefresh(单 Promise)串行化刷新,避免并发风暴
乐观更新回滚遗漏 数据不一致 useResourceCRUD 回滚路径单测覆盖(optimistic: true 下 4xx 用例)
手工加资源时覆盖已有文件 代码丢失 改前 grep / diff;食谱「注意」节强调不覆盖手写代码
apps/basic 与 apps/with-lingui 抽象漂移 双倍维护 回填暂缓;待 hook/契约稳定后再设固定回填节点;尽量参数化而非分叉

5. 验收标准总览

  • vp check --no-fmt 全绿
  • pnpm test:e2e:core(login / users / auth-refresh / rbac / url-state 等,以 package.json 脚本为准)全绿
  • CI 含 vp build;bundle 不设自动化上限门禁(见 P2-3)
  • routes/_auth/users/index.tsx 行数 ≤ 260
  • 按 add-resource.instructions.md 能在约 30 分钟内产出可用 CRUD 页面(熟练后)

附录 A:add-resource.instructions.md 草稿

以仓库内正式文件为准:apps/with-lingui/.github/instructions/add-resource.instructions.md;下文仅作计划期摘要。

---
applyTo: "src/api/**/*.ts,src/mocks/handlers/**/*.ts,src/routes/_auth/**/*.tsx"
description: "Use when adding a new CRUD resource (orders, roles, tenants, etc.). Keywords: add resource, new resource, scaffold, CRUD page, 新资源."
---

# Add Resource Recipe

## Steps

1. `src/api/schemas.ts` — Add `<Name>Schema`, `Create<Name>RequestSchema`, `Update<Name>RequestSchema`. Reuse `PaginatedResponseSchema`.
2. `src/api/<name>.ts` — Export `<NAME>_ENDPOINTS = { list, create, update: (id) => ..., delete: (id) => ... }`.
3. `src/mocks/data.ts` — Append seed data.
4. `src/mocks/handlers/<name>.ts` — Mirror `handlers/user.ts`; wrap responses with `successWithSchema`.
5. `src/mocks/handlers/index.ts` — Register the new handlers array.
6. `src/routes/_auth/<name>/index.tsx` — Duplicate `users/index.tsx`, replace types, compose `useResourceCRUD + useCrudToasts + useUrlSearchState + useTableFitHeight`.
7. `e2e/<name>.spec.ts` — Copy `users.spec.ts`, adjust selectors.

## Validation

- `vp check --no-fmt`
- `pnpm test:e2e -- --grep <name>`
- `vp build` 后对照构建日志关注主 chunk / vendor 体积(无 CI 硬阈值)

附录 B:文件改动预估

文件 类型
apps/with-lingui/src/utils/http.ts 重构
apps/with-lingui/src/stores/auth.ts 增加导出
apps/with-lingui/src/main.tsx 简化
apps/with-lingui/src/routes/_auth.tsx 新增 beforeLoad
apps/with-lingui/src/hooks/useTableFitHeight.ts 新增
apps/with-lingui/src/hooks/useCrudToasts.ts 新增
apps/with-lingui/src/hooks/useUrlSearchState.ts 新增
apps/with-lingui/src/hooks/useResourceCRUD.ts 增强
apps/with-lingui/src/mocks/createHandler.ts 增强
apps/with-lingui/src/mocks/handlers/auth.ts 补 refresh
apps/with-lingui/src/routes/_auth/users/index.tsx 大幅精简
apps/with-lingui/.github/instructions/add-resource.instructions.md 新增
.cursor/rules/*.mdc 新增(5 份镜像)
e2e/{auth-refresh,rbac,url-state,...}.spec.ts 新增 / 演进
.github/workflows/with-lingui-ci.yml 质量 + build(无 size-limit)