15 KiB
15 KiB
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
各层职责边界
// ✅ 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() |
// ✅ 事件解耦:订单模块不直接依赖通知模块
// 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 是纯数据)
检测循环依赖
# 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)
<!-- 标准顺序: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 拆分规范
// ✅ 单一职责: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 配置建议:
// .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/)