480 lines
15 KiB
Markdown
480 lines
15 KiB
Markdown
# 019-modular.mdc (Deep Reference)
|
||
|
||
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||
|
||
---
|
||
|
||
|
||
# 🧩 Modular Architecture Standards
|
||
|
||
## 核心原则
|
||
|
||
| 原则 | 说明 | 违反案例 |
|
||
|------|------|---------|
|
||
| **单一职责** | 一个文件只做一件事 | 1000 行的 `index.vue` 包含业务+状态+样式 |
|
||
| **高内聚低耦合** | 同模块内紧密,跨模块通过接口 | 直接引用另一模块的内部 Service |
|
||
| **依赖方向单一** | 只允许 `UI → Service → Repository → Model` | Service 引用 Controller |
|
||
| **显式边界** | 模块通过公开的 index.ts 暴露 API | `import { InternalHelper } from '../order/utils/internal'` |
|
||
| **禁止循环依赖** | A 不能引用 B 的同时 B 引用 A | order 模块和 user 模块互相引用 |
|
||
|
||
---
|
||
|
||
## 前端模块划分
|
||
|
||
### 标准目录结构
|
||
|
||
```
|
||
src/
|
||
├── api/ # API 请求层(按模块分文件)
|
||
│ ├── user.ts # 用户相关接口
|
||
│ ├── order.ts # 订单相关接口
|
||
│ ├── permission.ts # 权限相关接口
|
||
│ └── index.ts # 统一导出
|
||
│
|
||
├── composables/ # 可复用逻辑(按功能命名 use*.ts)
|
||
│ ├── useTable.ts # 表格通用逻辑
|
||
│ ├── useForm.ts # 表单通用逻辑
|
||
│ ├── useAuth.ts # 认证状态
|
||
│ ├── useDevice.ts # 设备检测
|
||
│ ├── usePermission.ts # 权限检查
|
||
│ └── useWebSocket.ts # WebSocket 管理
|
||
│
|
||
├── stores/ # Pinia 状态(按模块分文件)
|
||
│ ├── user.ts
|
||
│ ├── menu.ts
|
||
│ ├── setting.ts
|
||
│ └── index.ts # 统一导出
|
||
│
|
||
├── components/ # 全局公共组件
|
||
│ ├── ArtTable/ # 每个复杂组件独立目录
|
||
│ │ ├── index.vue # 主组件
|
||
│ │ ├── TableToolbar.vue # 子组件(内聚)
|
||
│ │ ├── TablePagination.vue
|
||
│ │ └── types.ts # 组件专属类型
|
||
│ ├── ArtForm/
|
||
│ └── ArtChart/
|
||
│
|
||
├── views/ # 页面(按业务域分目录)
|
||
│ ├── order/ # 订单业务域
|
||
│ │ ├── index.vue # 列表页
|
||
│ │ ├── detail.vue # 详情页
|
||
│ │ ├── components/ # 页面专属组件(不对外)
|
||
│ │ │ ├── OrderCard.vue
|
||
│ │ │ └── OrderTimeline.vue
|
||
│ │ ├── composables/ # 页面专属 composable
|
||
│ │ │ └── useOrderFilter.ts
|
||
│ │ └── types.ts # 页面专属类型
|
||
│ │
|
||
│ ├── user/
|
||
│ ├── permission/
|
||
│ └── dashboard/
|
||
│
|
||
├── utils/ # 纯工具函数(无副作用)
|
||
│ ├── format.ts # 格式化
|
||
│ ├── validate.ts # 校验
|
||
│ ├── crypto.ts # 加密工具
|
||
│ └── date.ts # 日期工具
|
||
│
|
||
├── directives/ # 自定义指令(每个指令独立文件)
|
||
│ ├── auth.ts # v-auth
|
||
│ ├── roles.ts # v-roles
|
||
│ └── index.ts # 统一注册
|
||
│
|
||
└── types/ # 全局类型定义
|
||
├── api.ts # API 响应/请求类型
|
||
├── models.ts # 业务实体类型
|
||
└── common.ts # 通用类型
|
||
```
|
||
|
||
### 文件拆分阈值
|
||
|
||
| 指标 | 警告 | 必须拆分 |
|
||
|------|------|---------|
|
||
| 文件行数 | > 200 行 | > 400 行 |
|
||
| 组件内 ref 数量 | > 8 个 | > 15 个 |
|
||
| 单个 composable 行数 | > 100 行 | > 200 行 |
|
||
| 单个 store 行数 | > 150 行 | > 300 行 |
|
||
|
||
### 组件拆分决策树
|
||
|
||
```
|
||
一个 .vue 文件是否应该拆分?
|
||
├─ 文件 > 200 行? → 考虑拆分
|
||
├─ 包含多个独立 UI 块? → 拆分为子组件
|
||
├─ 业务逻辑超过 50 行? → 提取为 composable
|
||
├─ 该组件在 > 2 个地方复用? → 移至 components/
|
||
└─ 否 → 保持不变
|
||
```
|
||
|
||
---
|
||
|
||
## 后端模块划分(Hyperf DDD)
|
||
|
||
### 标准目录结构
|
||
|
||
```
|
||
app/
|
||
├── Controller/ # HTTP 入口层(只做参数接收 + 响应格式化)
|
||
│ ├── OrderController.php
|
||
│ ├── UserController.php
|
||
│ └── Traits/
|
||
│ └── ApiResponse.php # 响应格式 Trait
|
||
│
|
||
├── Service/ # 业务逻辑层(核心业务规则)
|
||
│ ├── OrderService.php
|
||
│ ├── UserService.php
|
||
│ └── Dto/ # 数据传输对象
|
||
│ ├── OrderCreateDto.php
|
||
│ └── OrderListDto.php
|
||
│
|
||
├── Repository/ # 数据访问层(数据库/缓存操作)
|
||
│ ├── OrderRepository.php
|
||
│ ├── UserRepository.php
|
||
│ └── Contracts/ # Repository 接口
|
||
│ └── OrderRepositoryInterface.php
|
||
│
|
||
├── Model/ # Eloquent ORM 模型
|
||
│ ├── Order.php
|
||
│ ├── User.php
|
||
│ └── Traits/ # 可复用模型 Trait
|
||
│ ├── HasCreator.php
|
||
│ ├── HasSoftDeletes.php
|
||
│ └── HasDataPermission.php
|
||
│
|
||
├── Request/ # 表单验证(每个操作独立 Request)
|
||
│ ├── Order/
|
||
│ │ ├── CreateOrderRequest.php
|
||
│ │ ├── UpdateOrderRequest.php
|
||
│ │ └── ListOrderRequest.php
|
||
│ └── User/
|
||
│
|
||
├── Event/ # 事件定义(命名:名词+动词过去式)
|
||
│ ├── OrderCreated.php
|
||
│ ├── OrderStatusChanged.php
|
||
│ └── UserLoggedIn.php
|
||
│
|
||
├── Listener/ # 事件监听器(一个事件一个 Listener)
|
||
│ ├── SendOrderNotification.php
|
||
│ ├── LogOrderStatusChange.php
|
||
│ └── UpdateUserLastLogin.php
|
||
│
|
||
├── Middleware/ # 中间件(每个职责独立文件)
|
||
│ ├── AuthMiddleware.php
|
||
│ ├── PermissionMiddleware.php
|
||
│ ├── RateLimitMiddleware.php
|
||
│ └── AntiScrapingMiddleware.php
|
||
│
|
||
├── Exception/ # 异常定义(每类业务一个异常)
|
||
│ ├── BusinessException.php
|
||
│ ├── AuthException.php
|
||
│ ├── PermissionException.php
|
||
│ └── Handler/
|
||
│ └── AppExceptionHandler.php
|
||
│
|
||
├── Job/ # 异步任务(AsyncQueue)
|
||
│ ├── SendNotificationJob.php
|
||
│ └── GenerateReportJob.php
|
||
│
|
||
└── Command/ # 命令行工具
|
||
└── SyncPermissionCommand.php
|
||
```
|
||
|
||
### 各层职责边界
|
||
|
||
```php
|
||
// ✅ Controller — 只做参数绑定 + 调用 Service + 格式化响应
|
||
class OrderController
|
||
{
|
||
public function store(CreateOrderRequest $request): array
|
||
{
|
||
$dto = CreateOrderDto::fromRequest($request);
|
||
$order = $this->orderService->create($dto);
|
||
return $this->success(OrderResource::make($order));
|
||
}
|
||
}
|
||
|
||
// ✅ Service — 只做业务逻辑,调用 Repository 取数据
|
||
class OrderService
|
||
{
|
||
public function create(CreateOrderDto $dto): Order
|
||
{
|
||
// 业务规则:库存检查
|
||
$this->inventoryService->checkStock($dto->productId, $dto->quantity);
|
||
|
||
$order = $this->orderRepository->create($dto->toArray());
|
||
|
||
// 发事件(不直接发通知,解耦)
|
||
event(new OrderCreated($order));
|
||
|
||
return $order;
|
||
}
|
||
}
|
||
|
||
// ✅ Repository — 只做数据库操作,不包含业务规则
|
||
class OrderRepository
|
||
{
|
||
public function create(array $data): Order
|
||
{
|
||
return Order::create($data);
|
||
}
|
||
|
||
public function findWithItems(int $id): ?Order
|
||
{
|
||
return Order::with(['items', 'creator'])->find($id);
|
||
}
|
||
}
|
||
|
||
// ❌ 反模式:Controller 直接操作 Model
|
||
class BadController
|
||
{
|
||
public function store(Request $request): array
|
||
{
|
||
$order = Order::create($request->all()); // ← 跳过 Service 层
|
||
Mail::to($order->user)->send(new OrderMail($order)); // ← Controller 不应发邮件
|
||
return ['data' => $order];
|
||
}
|
||
}
|
||
```
|
||
|
||
### 模块通信规范
|
||
|
||
| 通信方式 | 使用场景 | 实现 |
|
||
|---------|---------|------|
|
||
| **直接依赖注入** | 同层级调用(Service → Service) | `__construct` 注入 |
|
||
| **事件系统** | 跨模块解耦通知 | `event(new OrderCreated($order))` |
|
||
| **消息队列** | 耗时操作异步化 | `AsyncQueue::push(new SendEmailJob(...))` |
|
||
| **共享 Repository** | 跨模块读取数据 | 注入对方 Repository(只读) |
|
||
| **禁止** | 跨模块调用内部方法 | ✘ `$userService->_internalCalc()` |
|
||
|
||
```php
|
||
// ✅ 事件解耦:订单模块不直接依赖通知模块
|
||
// app/Event/OrderCreated.php
|
||
class OrderCreated
|
||
{
|
||
public function __construct(public readonly Order $order) {}
|
||
}
|
||
|
||
// app/Listener/SendOrderNotification.php(通知模块监听)
|
||
#[Listener]
|
||
class SendOrderNotification implements ListenerInterface
|
||
{
|
||
public function listen(): array
|
||
{
|
||
return [OrderCreated::class];
|
||
}
|
||
|
||
public function process(object $event): void
|
||
{
|
||
$this->notificationService->notify($event->order->user_id, 'order_created', [
|
||
'order_id' => $event->order->id,
|
||
]);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 禁止循环依赖
|
||
|
||
```
|
||
依赖方向规则(严格单向):
|
||
|
||
Controller → Service → Repository → Model
|
||
↓
|
||
Event → Listener
|
||
↓
|
||
Job(异步)
|
||
|
||
✅ Service 可以依赖 Repository
|
||
✅ Service 可以发布 Event
|
||
✅ Listener 可以依赖 Service
|
||
❌ Repository 不能依赖 Service
|
||
❌ Model 不能依赖 Service/Repository
|
||
❌ Event 不能依赖 Service(Event 是纯数据)
|
||
```
|
||
|
||
### 检测循环依赖
|
||
|
||
```bash
|
||
# PHP 项目
|
||
composer require maglnet/composer-require-checker --dev
|
||
|
||
# 前端项目(ESLint 插件)
|
||
npm install eslint-plugin-import --save-dev
|
||
# .eslintrc 中配置 import/no-cycle
|
||
```
|
||
|
||
---
|
||
|
||
## Vue 组件模块化规范
|
||
|
||
### 组件分类
|
||
|
||
| 类型 | 位置 | 命名 | 特征 |
|
||
|------|------|------|------|
|
||
| **基础组件** | `src/components/` | `Art*` | 无业务逻辑,高复用 |
|
||
| **业务组件** | `src/views/{domain}/components/` | `{Domain}*` | 包含业务,不跨域复用 |
|
||
| **页面组件** | `src/views/{domain}/index.vue` | 路由直接映射 | 整合子组件和 store |
|
||
| **布局组件** | `src/layouts/` | `*Layout` | 全局页面框架 |
|
||
|
||
### 单文件组件结构(SFC)
|
||
|
||
```vue
|
||
<!-- 标准顺序:script → template → style -->
|
||
<script setup>
|
||
// 1. 导入(分组排列)
|
||
import { computed, ref } from 'vue' // Vue core
|
||
import { useRouter } from 'vue-router' // Router
|
||
import { storeToRefs } from 'pinia' // Pinia
|
||
import { useOrderStore } from '@/stores/order' // Stores
|
||
import { useTable } from '@/composables/useTable' // Composables
|
||
import { OrderApi } from '@/api/order' // API
|
||
import OrderCard from './components/OrderCard.vue' // Local components
|
||
|
||
// 2. Props & Emits(JS 对象语法)
|
||
const props = defineProps({
|
||
domain: { type: String, required: true },
|
||
readonly: { type: Boolean, default: false },
|
||
})
|
||
|
||
const emit = defineEmits(['updated', 'deleted'])
|
||
|
||
// 3. Store
|
||
const orderStore = useOrderStore()
|
||
const { orders, loading } = storeToRefs(orderStore)
|
||
|
||
// 4. Composable(复杂逻辑提取)
|
||
const { tableData, pagination, fetchData } = useTable(OrderApi.list)
|
||
|
||
// 5. 本地状态(按功能分组,加注释分隔)
|
||
// --- Dialog state ---
|
||
const dialogVisible = ref(false)
|
||
const currentOrder = ref(null)
|
||
|
||
// --- Filter state ---
|
||
const searchForm = ref({ keyword: '', status: '' })
|
||
|
||
// 6. Computed
|
||
const filteredOrders = computed(() =>
|
||
orders.value.filter((o) => o.domain === props.domain)
|
||
)
|
||
|
||
// 7. Methods(按功能分组)
|
||
function openDialog(order) {
|
||
currentOrder.value = order
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
async function handleDelete(id) {
|
||
await orderStore.delete(id)
|
||
emit('deleted', id)
|
||
}
|
||
|
||
// 8. Lifecycle
|
||
onMounted(() => fetchData())
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 模板只做渲染,不包含复杂逻辑 -->
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* 组件作用域样式 */
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
## Composable 拆分规范
|
||
|
||
```typescript
|
||
// ✅ 单一职责:useOrderFilter 只负责过滤逻辑
|
||
// src/views/order/composables/useOrderFilter.ts
|
||
export function useOrderFilter(orders) {
|
||
const status = ref('')
|
||
const keyword = ref('')
|
||
const dateRange = ref(null)
|
||
|
||
const filtered = computed(() => {
|
||
return orders.value.filter((order) => {
|
||
if (status.value && order.status !== status.value) return false
|
||
if (keyword.value && !order.orderNo.includes(keyword.value)) return false
|
||
if (dateRange.value) {
|
||
const [start, end] = dateRange.value
|
||
if (order.createdAt < start || order.createdAt > end) return false
|
||
}
|
||
return true
|
||
})
|
||
})
|
||
|
||
function reset() {
|
||
status.value = ''
|
||
keyword.value = ''
|
||
dateRange.value = null
|
||
}
|
||
|
||
return { status, keyword, dateRange, filtered, reset }
|
||
}
|
||
|
||
// ❌ 反模式:把表格、过滤、编辑逻辑都放在一个 composable
|
||
export function useOrderPage() { // 太大了
|
||
// ... 300 行混合逻辑
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 前端导入层级边界
|
||
|
||
```
|
||
导入方向规则(严格单向):
|
||
|
||
views/ → composables/ → stores/ → api/ → utils/
|
||
│ │
|
||
│ └→ components/(仅消费,不反向引用 views/)
|
||
└→ components/
|
||
|
||
✅ views/ 可以引用 composables/、stores/、api/、components/
|
||
✅ composables/ 可以引用 stores/、api/、utils/
|
||
✅ stores/ 可以引用 api/、utils/
|
||
✅ components/ 可以引用 composables/、utils/(通用组件不引用 stores/)
|
||
❌ components/core/ 不得引用 stores/(通用组件不依赖业务状态)
|
||
❌ api/ 不得引用 stores/ 或 views/
|
||
❌ utils/ 不得引用任何其他层(纯函数,无副作用)
|
||
❌ 跨业务域直接引用内部组件(通过公开 index.ts 导出)
|
||
```
|
||
|
||
**ESLint 配置建议**:
|
||
|
||
```typescript
|
||
// .eslintrc — import/no-restricted-paths
|
||
{
|
||
rules: {
|
||
'import/no-restricted-paths': ['error', {
|
||
zones: [
|
||
{ target: './src/utils', from: './src/stores' },
|
||
{ target: './src/utils', from: './src/api' },
|
||
{ target: './src/api', from: './src/stores' },
|
||
{ target: './src/components/core', from: './src/stores' },
|
||
],
|
||
}],
|
||
},
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 规则汇总
|
||
|
||
- 单个 `.vue` 文件超过 200 行必须拆分,超过 400 行禁止提交
|
||
- 业务逻辑超过 50 行必须提取为 composable
|
||
- Composable 必须以 `use` 开头,返回对象必须解构友好
|
||
- 每个 API 模块对应一个文件(`src/api/{module}.ts`)
|
||
- 禁止在 Controller 中操作 Model(必须经过 Service + Repository)
|
||
- 禁止在 Model 中调用 Service(保持 Model 纯净)
|
||
- 禁止跨业务域直接引用内部组件(通过公开 `index.ts` 导出)
|
||
- 事件名必须是:`{域}` + `{动作的过去时}` (如 `OrderCreated`)
|
||
- 每次 PR 提交前运行 `eslint --rule import/no-cycle` 检查循环依赖
|
||
- 前端导入方向必须单向:`views → composables → stores → api → utils`
|
||
- 通用组件 (`components/core/`) 禁止依赖业务状态 (`stores/`)
|