# 02 — 项目架构与目录结构 > 参考:vue-vben-admin(三层包划分)、soybean-admin(文件路由 + store 纯函数分离)、ant-design-pro(页面 co-location)、vue-pure-admin(页面内聚五件套);补充:gin-vue-admin(分域 api/view) --- ## 一、组织目录的三种流派 ### 1.1 流派对照 | 流派 | 代表 | 结构 | 优点 | 代价 | |------|------|------|------|------| | **按层分**(layer-first) | 早期中台 | `components/ views/ api/ stores/` 每层内平铺全部业务 | 上手简单 | 业务散在 5 个目录,删模块要全局找 | | **按域分**(domain-first) | **本标准(Vue 单包)/ gva** | 层目录保留,但 `api/user/`、`views/user/`、`stores/` 域内聚合 | 删域=删目录,心智清晰 | 需要约定维持层内域目录一致 | | **按功能分包**(feature-first) | adp 页面级、vben 包级 | 页面目录自带 service/mock/类型/样式;框架能力独立成包 | 高内聚,可整体搬移 | 需要晋升机制防止复制泛滥 | **结论:中小工程用「按域分 + 页面 co-location」混合式**——全局按域(api/views 域目录对齐),页面私有代码留在页面目录内。 ### 1.2 四标杆的目录精髓 ``` vue-vben-admin(依赖方向三层,分层标杆) apps/* ──▶ packages/effects/* ──▶ packages/@core/* (app 是唯一允许直接依赖具体 UI 库的层; @core 零 UI 依赖零业务;effects 通过 adapter 适配 UI 生态) ant-design-pro(页面五件套 co-location) src/pages/table-list/ ├── index.tsx # 页面(ProTable 声明式列配置) ├── service.ts # 本页接口(手写层) ├── data.d.ts # 本页类型 ├── _mock.ts # 本页 mock(config 显式 include) └── components/ ├── CreateForm.tsx # 新建弹窗(trigger 模式) └── UpdateForm.tsx # 编辑弹窗(StepsForm 进 Modal) soybean-admin(生成物 + store 纯函数分离,★★★★☆) src/ ├── router/elegant/ # 文件路由生成物(routes/transform/imports + RouteKey 类型 d.ts) ├── service/{api,request}/ # api=纯请求函数、request=实例拦截器,两层分离 ├── store/modules// # 每模块 index.ts(状态编排) + shared.ts(可单测纯函数) ├── typings/{api,...} # 全局 namespace 类型(Api/App/Storage/Env) └── views/_builtin/ # 403/404/500/login 内置页统一收纳,业务页平级 vue-pure-admin(页面内聚五件套,★★★★☆) src/views/system/user/ ├── index.vue # 纯模板(零逻辑,只解构 hook) ├── form/index.vue # 弹窗表单 ├── tree.vue # 左树 └── utils/{hook.tsx,rule.ts,types.ts} # 逻辑/校验/类型全在本域 gin-vue-admin(补充:全局分域 + 指令/钩子独立) src/ ├── api/ # 一域一文件:user.js menu.js ... ├── pinia/modules/ # 一域一 store ├── view/superAdmin/ view/example/ # 按业务分组 ├── directive/auth.js # v-auth 指令独立目录 ├── hooks/responsive.js # 组合式函数独立目录 └── plugin// # 业务插件 = 自带 view/api/assets 的小应用 ``` --- ## 二、标准目录结构(Vue 工程) ### 2.1 目标结构 ``` src/ ├── main.js # 入口:仅装配(EP按需、pinia、router、指令、全局组件) ├── App.vue ├── api/ # ★ 按功能域分目录,与 views 域目录同名 │ ├── auth/ # captcha.js mfa.js │ ├── user/ # users.js roles.js permissions.js │ ├── rbac/ # 域小可单文件,大了再拆 │ ├── system/ # menu.js modules.js logs.js ... │ └── / # 业务域 ├── views/ # ★ 域目录与 api/ 一一对应 │ ├── user/ # Users.vue + components/XxxFormDialog.vue ... │ ├── / │ └── _builtin/ # 403/404/500/login 内置页统一收纳(soybean 范式) ├── stores/ # Pinia:一域一文件(auth.js menu.js site.js ...) ├── composables/ # useXxx(依赖业务上下文的钩子) ├── components/ # ★ 全局组件(需晋升,见第四章) ├── layout/ # 布局骨架(业务页只挂内容区,禁止重写布局) ├── router/ # index.js(含守卫) modules/(路由分文件) ├── config/ # theme.js modules.js(纯配置,无副作用导入) ├── typings/ # 全局契约类型(TS 渐进路线,soybean 四区) ├── utils/ # request.js permission.js(无状态工具) └── styles/ # 全部文件必须有入口引用链 ``` React 工程同构映射:`views→pages`、`composables→hooks`、`stores→store`(zustand slices)、`components→components`(含 barrel 出口)。 ### 2.2 层内规则(必须) 1. **api 域目录与 views 域目录强制同名**(`api/cron/` ↔ `views/cron/`);**禁止**在 api 根级别放散文件 2. **store 文件名 = 导出名**(`stores/auth.js` → `useAuthStore`);单 store ≤300 行;推荐 soybean 的「index + shared」目录式(状态编排与纯函数分离) 3. **layout/ 是禁改区**:业务页通过页面容器组件的 header/slot 定制,不碰布局骨架(可裁剪底座的前提) 4. **components/ 只放「晋升组件」**(见第四章晋升机制),新页面私有组件一律 `views/<域>/components/` ### 2.3 可裁剪分层与模块开关(底座/脚手架推荐) ``` ┌─────────────────────────────────────────────────────┐ │ 壳层 shell 登录/MFA/403/500 —— 不挂主布局 │ ├─────────────────────────────────────────────────────┤ │ 系统层 system 用户/RBAC/审计/设置 —— 底座自带 │ ├─────────────────────────────────────────────────────┤ │ 示例层 examples 演示模块 —— fork 后可整层删除 │ └─────────────────────────────────────────────────────┘ ``` - 路由 `meta.layer: 'shell' | 'system' | 'examples'` 声明归属 - 运行时开关:**源真相在后端**(如 `GET/PUT /api/system/modules`),localStorage 仅作缓存——**禁止**把 localStorage 当源真相 - 构建期裁剪:环境变量(如 `VITE_ENABLE_EXAMPLES=false`)+ 路由中字面量比较,保证打包器 tree-shake 掉示例 chunk - 示例层代码**不得 import 系统层**内聚单元,保证整层可摘除;示例域附删除指引文档(EXAMPLES.md 惯例) **分层是目录结构之外的第二根轴**:每个新模块必须先回答「我在哪一层」。 ### 2.4 命名规范与类型目录 | 对象 | 规范 | 示例 | |------|------|------| | 组件文件 | PascalCase | `UserFormDialog.vue` | | 工具 / composable | camelCase,composable 带 use 前缀 | `request.js`、`usePagedQuery.js` | | 目录 | kebab-case(views 域目录用业务域名) | `user-management/`、`cron/` | | store 文件 | 文件名与导出同名 | `stores/auth.js` → `useAuthStore` | | API 文件 | 资源名复数 | `api/user/users.js` | | 常量 | UPPER_SNAKE_CASE | `SYSTEM_ORDER` | 类型目录(TS 渐进路线,soybean 范式):全局契约类型集中 `typings/`(Api / App / Storage / Env 四区 namespace,storage 与 env 均键级类型);页面私有类型随页面(`data.d.ts` / `utils/types.ts`);**禁止**同一类型定义在多个组件内重复出现。 --- ## 三、页面 co-location(adp 五件套) ### 3.1 什么时候页面自带 service ``` 全局 api/<域>/xxx.js # 多页面复用、域级语义(users.js 的 list/create/delete) 页面内 service.ts # 只服务本页的接口(dashboard 聚合、报表导出) ``` 判定:**接口第二个消费者出现时**,从页面 `service.ts` 提升到 `api/<域>/`。反向不成立——不要预防性提升。 ### 3.2 页面目录模板 ``` views//.vue # 页面骨架:容器 + 列表组件 + 子组件编排(≤400 行) views//components/ # 本页私有组件(表单弹窗、详情抽屉、单元格渲染器) views//composables/ # 本页私有逻辑(可选,超过 2 个页面组件共享时建) views//utils/ # 本页 hook/rule/types(pure 范式,可选) views//EXAMPLES.md # 仅示例层:删除指引 ``` --- ## 四、公共组件晋升机制 ``` 第 1 次使用:写在页面 components/ 里 第 2 次使用:复制一份,页面内注明来源 ← 允许的重复 第 3 次使用:晋升 src/components/,删除两份复制 ← 必须执行 ``` 晋升时同步做三件事: 1. **去业务化**:props 收口(`fetcher`/`permissions` 进来,具体 API 调用出去) 2. **注册出口**:Vue 端全局组件在 `main.js` 注册;React 端进 `components/index.ts` barrel(adp:按「布局组件 / 业务组件」分组导出) 3. **写使用说明**:组件头部注释 3 行(用途/必需 props/已知限制) 必备的两个全局页面组件(详见 05/07 章):**PageContainer**(页面容器:页头/面包屑/slot)与 **ListPage**(分页列表:fetcher + 筛选 slot + 表格透传 + 分页)。 --- ## 五、架构守护:让目录约定不被腐蚀 | 守护手段 | 出处 | 可落地做法 | |----------|------|------------| | 循环依赖扫描 | vben `check:circular` | `npx madge --circular src/` 进 CI | | 依赖方向检查 | vben `check:dep`(@core 禁依赖 UI 库) | 示例层禁 import 系统层:ESLint `no-restricted-imports` 按路径分组 | | 死代码检测 | adp coverage 排除生成物 | CI 跑 `knip`/`depcheck`,styles 无入口文件直接报错 | | 生成物隔离 | adp `services/ant-design-pro/`(OpenAPI 生成,禁手改) | 生成物独立目录 + lint ignore | | 路由自动收集 | pure `router/modules/` + `import.meta.glob` | 新增路由文件零注册代码 | --- ## 六、最佳实践清单 ✅ **要做** 1. api 域目录与 views 域目录同名;新域从脚手架起步(自动生成五处对照:菜单/路由/页面/API/权限码) 2. 页面私有组件不出页面目录;晋升走「三次法则」 3. 每个新模块先声明 `meta.layer`,示例层保持可整层摘除 4. store 按域拆分、文件名=导出名、单 store ≤300 行 5. 目录结构变化(新增域/晋升组件/删示例)在同一 PR 内更新架构文档 ❌ **不要** 1. 不要在 api 根级别放散文件 2. 不要把页面私有组件直接丢进 `src/components/` 3. 不要让业务页 import layout 内部组件(只准用 slot 协议) 4. 不要建「future/」「temp/」「new/」这类无语义目录 5. 不要保留无入口引用的样式文件和空目录 --- ## 七、AI 检查清单(对比目标工程逐项核对) **P0** 1. api 与 views 域目录是否同名对齐?有无 api 根级散文件? 2. store 文件名与导出名是否一致?有无超 300 行的 store? 3. 有无空目录、无引用样式、描述已不存在目录的文档残留? **P1** 4. 页面骨架是否 ≤400 行?超 400 行的视图列出拆分建议(参照 07 章) 5. 私有组件是否越界进全局 components/?全局组件是否都有注册出口与头注释? 6. 若为可裁剪底座:meta.layer 是否全覆盖?模块开关源真相是否在后端? 7. CI 是否有循环依赖与死代码检查? **P2** 8. 是否值得引入路由文件自动收集(`router/modules/` + glob)或文件路由(soybean @elegant-router)? 9. `typings/` 全局类型目录是否建立(Api/App/Storage/Env 四区)? --- > 下一篇:[03-请求层与数据流](03-请求层与数据流.md) —— 前端工程的血管系统。