良好的feature-based-目錄結構與具體示例
背景
先拆”業務邊界”,不是拆組件
從業務角度來説,這個訂單頁其實有3個部分:
- 核心 - 瀏覽能力
- 訂單列表
- 基礎篩選
- 分頁
- Extension - 可選 - 插件能力
- 高級篩選
- 導出
- 狀態變更
- Detail - 按需能力
- 訂單詳情單牀
重構目錄結構
把 “ 按技術類型” 改為 “按業務角色”
如果一開始就能拆成這樣,其實80%的問題就已經解決了.
src/
├── features/
│ ├── user/
│ │ ├── pages/
│ │ │ ├── UserList.vue
│ │ │ └── UserDetail.vue
│ │ │
│ │ ├── components/
│ │ │ ├── UserTable.vue
│ │ │ └── UserForm.vue
│ │ │
│ │ ├── api/
│ │ │ └── user.api.ts
│ │ │
│ │ ├── store/
│ │ │ └── user.store.ts
│ │ │
│ │ ├── hooks/
│ │ │ └── useUser.ts
│ │ │
│ │ ├── types/
│ │ │ └── user.types.ts
│ │ │
│ │ └── index.ts
│ │
│ ├── order/
│ │ ├── pages/
│ │ ├── components/
│ │ ├── api/
│ │ ├── store/
│ │ └── index.ts
│ │
│ └── product/
│ └── ...
│
├── shared/
│ ├── components/
│ │ ├── BaseTable.vue
│ │ ├── BaseModal.vue
│ │ └── BaseButton.vue
│ │
│ ├── hooks/
│ │ └── useRequest.ts
│ │
│ ├── utils/
│ └── styles/
│
├── router/
│ ├── routes/
│ │ ├── user.routes.ts
│ │ ├── order.routes.ts
│ │ └── product.routes.ts
│ │
│ └── index.ts
│
├── app.vue
└── main.ts
讓 Page 變成 “純裝配層”
// OrderPage.vue(正確版本)
<template>
<div class="order-page">
<!-- Core:必須同步 -->
<BaseFilter />
<OrderList @select="openDetail" />
<!-- Extension:按需 -->
<Suspense>
<AdvancedFilter v-if="showAdvanced" />
</Suspense>
<Suspense>
<ExportPanel v-if="canExport" />
</Suspense>
<Suspense>
<StatusAction v-if="canChangeStatus" />
</Suspense>
<!-- Detail:用户觸發 -->
<Suspense>
<OrderDetailDialog
v-if="showDetail"
:order-id="currentOrderId"
@close="closeDetail"
/>
</Suspense>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
// core 同步加載
import BaseFilter from '../core/BaseFilter.vue'
import OrderList from '../core/OrderList.vue'
// extension 異步加載
const AdvancedFilter = defineAsyncComponent(
() => import('../extensions/advanced-filter/AdvancedFilter.vue')
)
const ExportPanel = defineAsyncComponent(
() => import('../extensions/export/ExportPanel.vue')
)
const StatusAction = defineAsyncComponent(
() => import('../extensions/status/StatusAction.vue')
)
// detail 異步
const OrderDetailDialog = defineAsyncComponent(
() => import('../detail/OrderDetailDialog.vue')
)
import { useOrderCore } from '../core/useOrderCore'
import { useOrderDetail } from '../detail/useOrderDetail'
const {
showAdvanced,
canExport,
canChangeStatus
} = useOrderCore()
const {
showDetail,
currentOrderId,
openDetail,
closeDetail
} = useOrderDetail()
</script>
- Page 裏沒有業務規則
- 沒有 if 權限判斷
- 只負責 “拼裝能力”
把 規則 收斂到 對應邊界
// useOrderCore.ts(核心邏輯)
export function useOrderCore() {
const showAdvanced = ref(false)
const canExport = computed(() => {
return permission.value.includes('order:export')
})
const canChangeStatus = computed(() => {
return permission.value.includes('order:status')
})
return {
showAdvanced,
canExport,
canChangeStatus
}
}
規則集中,邊界生效
Extension 是“插件”,不污染 Core
不需要知道
OrderList的存在
// extensions/export/useExport.ts
export function useExport() {
const exportOrders = async () => {
// 導出邏輯
}
return { exportOrders }
}
// ExportPanel.vue
<template>
<button @click="exportOrders">導出</button>
</template>
<script setup>
import { useExport } from './useExport'
const { exportOrders } = useExport()
</script>
Detail 完全獨立
Detail 不依賴 Core,Core 也不依賴 Detail。
// detail/useOrderDetail.ts
export function useOrderDetail() {
const showDetail = ref(false)
const currentOrderId = ref<string | null>(null)
const openDetail = (id: string) => {
currentOrderId.value = id
showDetail.value = true
}
const closeDetail = () => {
showDetail.value = false
}
return {
showDetail,
currentOrderId,
openDetail,
closeDetail
}
}
正確的拆分,是先讓業務“各司其職”,
lazy 只是順手的事。