8af82f5900
Update documentation rules for GUIDE.md files to keep higher-level guides focused on constraints, conventions, and indexes. Add base and section component guides to the LLM development index, clarify component layering responsibilities, and fix architecture references from README to GUIDE.md.docs: reorganize component guide structure and indexes Update documentation rules for GUIDE.md files to keep higher-level guides focused on constraints, conventions, and indexes. Add base and section component guides to the LLM development index, clarify component layering responsibilities, and fix architecture references from README to GUIDE.md.
21 KiB
21 KiB
資料流與元件分層優化策略
一、Apple App Store 專案的核心架構特徵
1 單一業務邏輯門面 2 Intent / Action 分離(查詢與命令) 3 Page Model 驅動 UI(資料驅動) 4 Shelf / Item 分層(容器與內容分離) 5 Svelte Context 作為跨層依賴注入 6 命令式外殼 + 聲明式 UI
Read only when needed: what apple do
二、我們專案的現況診斷
Read only when needed: analyse now
三、優化後的資料流策略
3.1 核心資料流(單向 + 集中閘道)
┌─────────────────────────────────────────────────────────────┐
│ App Shell │
│ (App.vue → Layout → Global Overlays: Snackbar/Dialogs) │
└──────────────────────────┬──────────────────────────────────┘
│ page model (reactive)
▼
┌─────────────────────────────────────────────────────────────┐
│ Page Driver │
│ (views/*.vue — 極薄,只負責:組裝 page model / 事件轉發) │
└──────────────────────────┬──────────────────────────────────┘
│ props / emits
▼
┌─────────────────────────────────────────────────────────────┐
│ Page Component │
│ (PageXxx.vue — 組裝完整頁面,決定 Section 順序與 override) │
└──────────────────────────┬──────────────────────────────────┘
│ section data
▼
┌─────────────────────────────────────────────────────────────┐
│ Section / Shelf │
│ (決定佈局:水平/網格/列表;不關心內部呈現) │
└──────────────────────────┬──────────────────────────────────┘
│ item data
▼
┌─────────────────────────────────────────────────────────────┐
│ Item / Atom │
│ (純粹內容呈現;透過 provide/inject 取得 domain context) │
└─────────────────────────────────────────────────────────────┘
橫向:
composables → 可重用流程(CRUD state machine、form validation、editable grid)
stores → 跨頁共享狀態(auth、menu、favorites、messages)
services → HTTP 閘道(只封裝 ky,不持有 UI 狀態)
3.2 Page Model 作為主要資料單位
- 新增
src/models/page.ts:定義各頁面的統一介面。 - View 的職責從「管理資料 + 管理狀態 + 組裝模板」縮減為「呼叫 usePageDriver() 取得 page model,傳給 Page component」。
- Page model 可以來自:
- store(已有快取)
- service(直接 API)
- composable(組裝多個來源)
範例:
// src/composables/usePageDriver.ts
export function useMaintenancePage() {
const studentStore = useStudentStore()
const { records, loading, error, load } = useCrudDriver({
store: studentStore,
loadAction: () => studentStore.fetchStudents(),
})
const pageModel = computed(() => ({
title: '單筆資料維護',
records: records.value,
loading: loading.value,
error: error.value,
}))
return { pageModel, load }
}
3.3 查詢(Query)與命令(Command)分離
| 類型 | 資料流 | 錯誤處理 | 狀態位置 |
|---|---|---|---|
| Query | usePageDriver → pageModel → props |
<PageErrorBoundary> 或 page-level fallback |
composable 內部 ref |
| Command | executeCommand() → await service.action() → 重新載入 query |
snackbar / dialog / field error | composable 內部 ref |
- Query 對應 App Store 的 Intent:取得資料、回傳 model。
- Command 對應 App Store 的 Action:執行副作用、不回傳 model、觸發 Query 重新整理。
3.4 Promise-based 頁面載入(可選進階)
<!-- PageResolver.vue — 對齊 App Store 的 PageResolver.svelte -->
<template>
<Suspense>
<template #default>
<component :is="pageComponent" :page="resolvedPage" />
</template>
<template #fallback>
<PageLoadingSpinner />
</template>
</Suspense>
</template>
- 若頁面資料支援 async setup 或
usePageDriver回傳 Promise,可用 Vue<Suspense>達到與 App Store{#await page}類似的效果。 - 短期:維持 reactive ref,但將 loading / error 統一封裝在
usePageDriver。 - 長期:當多數頁面都使用統一 driver 後,可考慮引入 Suspense。
3.5 全域狀態 vs 頁面狀態的邊界
| 狀態類型 | 存放位置 | 生命周期 |
|---|---|---|
| 認證、選單、語言、主題 | src/stores/*.ts |
應用級 |
| 搜尋條件、分頁、dialog visible | src/composables/useXxxPage.ts |
頁面級(離開頁面可選保留或重置) |
| 表單 dirty / validation | src/composables/useXxxForm.ts |
dialog / form 級 |
| 表格排序、過濾 | src/composables/useXxxTable.ts |
區塊級 |
四、優化後的元件分層策略
4.1 五層結構
src/
├── shell/ ← 新增:App Shell(原 App.vue 拆分)
│ ├── AppShell.vue ← layout 切換、全域 overlay 掛載點
│ ├── GlobalOverlays.vue ← snackbar、確認 dialog、toast
│ └── AppTabs.vue ← 頁籤(從 App.vue 抽出)
│
├── views/ ← 維持:Page Driver(極薄)
│ └── maint/
│ └── SingleRecord.vue ← ~50 行:組裝 pageModel + 掛載 PageMaintDriver
│
├── components/
│ ├── pages/ ← 新增:Page Component 層
│ │ ├── PageMaintenance.vue
│ │ └── PageReport.vue
│ │
│ ├── sections/ ← 新增:Section / Shelf 層
│ │ ├── SectionSearchPanel.vue
│ │ ├── SectionDataTable.vue
│ │ └── SectionFormPanel.vue
│ │
│ ├── items/ ← 新增:Item / Atom 層(領域獨立)
│ │ ├── ItemDataRow.vue
│ │ └── ItemFormField.vue
│ │
│ ├── layouts/ ← 維持:App Shell Layout
│ │ ├── MainLayout.vue
│ │ └── PlainLayout.vue
│ │
│ └── base/ ← 維持:真正跨頁共用
│ └── DraggableDialog.vue
│
├── composables/
│ ├── page-drivers/ ← 新增:頁面資料協調
│ │ └── useMaintenancePage.ts
│ ├── commands/ ← 新增:命令流程(對齊 Jet Action)
│ │ └── useCrudCommands.ts
│ ├── forms/ ← 維持/重組:表單狀態機
│ │ └── useForm.ts
│ └── layout/ ← 維持
│
├── models/ ← 新增:領域模型與 Page Union
│ ├── page.ts
│ └── student.ts
│
├── stores/ ← 維持:跨頁共享狀態
├── services/ ← 維持:HTTP 閘道
└── router/ ← 維持:路由與 meta
4.2 各層職責與規範
Layer 1: App Shell(src/shell/)
- 職責:layout 切換、全域 overlay(snackbar、dialog)、頁籤容器、事件總線橋接。
- 禁止:頁面專屬業務流程、頁面資料組裝、特定 dialog 內容。
- 對齊:App Store 的
App.svelte+browser.ts的 overlay 部分。
Layer 2: Page Driver(src/views/)
- 職責:
- 呼叫
useXxxPage()取得pageModel。 - 將
pageModel與事件處理器傳給對應的PageXxx.vue。 - 處理 route param 解析(僅限轉換,不含業務邏輯)。
- 呼叫
- 目標行數:< 80 行。
- 禁止:大量模板、dialog 定義、form 欄位、直接操作 store。
<!-- views/maint/SingleRecord.vue(優化後) -->
<script setup lang="ts">
import { useMaintenancePage } from '@/composables/page-drivers/useMaintenancePage'
import PageMaintenance from '@/components/pages/PageMaintenance.vue'
const { pageModel, load } = useMaintenancePage({
title: '單筆資料維護',
records: [],
})
load()
</script>
<template>
<PageMaintenance :page="pageModel" />
</template>
Layer 3: Page Component(src/components/pages/)
- 職責:組裝完整頁面的 section 順序、處理 page-level slot override、分發頁面級事件。
- 命名:一律
Page前綴。 - 對齊:App Store 的
ProductPage.svelte、TodayPage.svelte、DefaultPage.svelte。
<!-- components/pages/PageMaintenance.vue -->
<template>
<PageMaintShell :title="page.title">
<template #search>
<SectionSearchPanel :fields="page.searchFields" @search="emit('search', $event)" />
</template>
<template #content>
<SectionDataTable :records="page.records" @edit="emit('edit', $event)" />
</template>
</PageMaintShell>
<!-- 頁面級 dialog 外掛,內容再拆到 SectionFormPanel -->
<SectionFormPanel
v-model="formVisible"
:mode="formMode"
:record="activeRecord"
@save="emit('save', $event)"
/>
</template>
Layer 4: Section / Shelf(src/components/sections/)
- 職責:決定「這一區的佈局方式」(水平捲軸、網格、列表、摺疊面板)。
- 禁止:知道上層 page 的業務邏輯、不直接呼叫 API。
- 對齊:App Store 的
Shelf.svelte、ShelfItemLayout.svelte。
<!-- SectionDataTable.vue -->
<template>
<v-data-table
:headers="resolvedHeaders"
:items="records"
fixed-header
>
<template v-for="slot in customSlots" :key="slot.key" #[slot.key]="{ item }">
<slot :name="slot.key" :item="item">
<!-- 預設 item 渲染 -->
<component :is="slot.itemComponent" :data="item" />
</slot>
</template>
</v-data-table>
</template>
Layer 5: Item / Atom(src/components/items/)
- 職責:純粹呈現單一資料單位。
- 禁止:知道自己是水平捲軸還是網格、不管理任何狀態。
- 對齊:App Store 的
BrickItem.svelte、LargeLockupItem.svelte。
<!-- ItemDataRow.vue -->
<template>
<div class="d-flex ga-2">
<v-chip size="small" :color="statusColor(data.status)">
{{ data.status }}
</v-chip>
<span>{{ data.name }}</span>
</div>
</template>
4.3 容器/內容分離的具體規範
| 場景 | 容器(Section) | 內容(Item) |
|---|---|---|
| 資料表格 | SectionDataTable(決定 headers、分頁、排序) |
ItemDataRow(決定單列呈現) |
| 搜尋面板 | SectionSearchPanel(決定展開/收合、grid 佈局) |
ItemFormField(單一輸入框呈現) |
| 圖文列表 | SectionCardGrid(決定欄數、gap、RWD) |
ItemProductCard(卡片內容) |
| 表單對話框 | SectionFormPanel(決定 dialog 外殼、actions) |
ItemFormFieldGroup(欄位群組) |
- 原則:若同一組資料在不同頁面需要「水平捲軸 vs 網格」兩種呈現,只換 Section,不換 Item。
4.4 Provide / Inject 作為跨層依賴注入
對齊 App Store 的 getJet() / getI18n(),在 Vue 中建立明確的 inject API:
// src/providers/page.ts
import { inject, provide } from 'vue'
import type { PageDriver } from '@/composables/page-drivers/types'
const PageDriverKey = Symbol('page-driver')
export function providePageDriver(driver: PageDriver) {
provide(PageDriverKey, driver)
}
export function usePageDriverInjected(): PageDriver {
const driver = inject(PageDriverKey)
if (!driver) throw new Error('usePageDriverInjected called without provider')
return driver
}
- 提供時機:Page Component (
PageXxx.vue) 在掛載時 provide。 - 使用時機:深層的 Item 元件需要觸發 page-level action 時 inject,避免 props drilling。
- 禁止:用 provide/inject 傳遞會頻繁變動的 UI 狀態(如
dialogVisible)。這類狀態應透過 props + emits。
4.5 Dialog 外層化策略
將 dialog 從 view 中完全抽出,形成「Dialog Shell + Content Slot」模式:
views/xxx.vue
└── PageXxx.vue
├── SectionDataTable
└── SectionFormPanel(dialog shell)
├── MntDialogCard(外殼:標題、toolbar、actions)
└── ItemFormFieldGroup(內容:欄位)
SectionFormPanel管理 dialog 的開關、mode、loading、saving。ItemFormFieldGroup純粹呈現欄位,不知道自己在 dialog 裡。- View 中不再出現
<teleport>、<v-overlay>、<v-dialog>的具體定義。
五、重構優先順序與遷移路徑
Phase 1:建立基礎設施(不動既有 view) ✅ 已完成
- 新增
src/models/:定義領域模型與 Page union type。src/models/student.ts:抽出StudentRecord,stores/students.ts改為 re-export 以保持向後相容。src/models/page.ts:定義BasePageModel、MaintenancePageModel與PageModelunion。
- 新增
src/shell/:從App.vue抽出GlobalOverlays.vue與AppTabs.vue。src/shell/AppShell.vue:layout 切換與全域 overlay 掛載點。src/shell/AppTabs.vue:頁籤管理與 router-view 容器。src/shell/GlobalOverlays.vue:snackbar、搜尋 dialog、訊息 dialog。
- 新增
src/components/pages/:建立第一個PageMaintenance.vue(可從PageMaint.vue擴展)。- 定義
MaintenancePageModelprops 與create/edit/view/delete/searchemits。 - 使用
PageMaint.vue作為佈局外殼,搜尋與表格區塊以 slot 開放,不綁定特定領域型別。
- 定義
- 新增
src/composables/page-drivers/:建立useMaintenancePage.ts。- 透過 options 傳入 title 與 records,協調搜尋條件、分頁與
pageModel。 - 提供
load()與resetSearch()供 Page Driver 呼叫。
- 透過 options 傳入 title 與 records,協調搜尋條件、分頁與
Phase 2:遷移最厚的 view(SingleRecord.vue) ✅ 已完成
- 將
SingleRecord.vue縮減為 route-level Page Driver。- 目前只負責呼叫
useSingleRecordMaintenancePage(),並組裝PageMaintenance、SectionSearchPanel、SectionDataTable、SectionFormPanel。 - 行數已由 921 行縮減至 52 行,達成 < 80 行目標。
- 目前只負責呼叫
- 將搜尋區塊抽出到
src/components/sections/SectionSearchPanel.vue。- 負責搜尋欄位佈局與 reset 事件,不直接操作 store。
- 將表格與分頁抽出到
src/components/sections/SectionDataTable.vue。- 負責 headers、資料列 slot、操作按鈕與分頁 footer,透過 emit 回傳 view/edit/delete/page 事件。
- 將 dialog 抽出到
src/components/sections/SectionFormPanel.vue。- 包含側邊 overlay、
MntDialogCard、record navigation toolbar 與確認 dialog。 - View 中不再直接定義
<teleport>、<v-overlay>或多個確認 dialog。
- 包含側邊 overlay、
- 將表單欄位抽出到
src/components/items/ItemFormFieldGroup.vue。- 只呈現欄位與欄位錯誤,透過
v-model與clear-field-error與上層互動。
- 只呈現欄位與欄位錯誤,透過
- 將 CRUD command 流程抽出到
src/composables/commands/useCrudCommands.ts。- 負責新增、載入編輯/檢視資料、儲存前確認、實際儲存與 highlight 流程。
- 新增
src/composables/page-drivers/useSingleRecordMaintenancePage.ts作為本頁協調層。- 集中組裝 page model、搜尋狀態、表格分頁、表單狀態、CRUD command 與 dialog flow。
SingleRecord.vue不再直接操作studentStore。
Phase 3:推廣到所有 maintenance 頁面 ✅ 已完成
EditableGrid.vue依 Page Driver + Page Component 模式重構。src/views/maint/EditableGrid.vue縮減為 10 行 route-level wiring。- 新增
src/composables/page-drivers/useEditableGridMaintenancePage.ts。 - 新增
src/components/pages/PageEditableGridMaintenance.vue,保留既有src/components/maint/EditableGrid.vue作為主要內容元件。
MasterDetailA.vue依 Page Driver + Page Component 模式重構。src/views/maint/MasterDetailA.vue縮減為 34 行。- 新增
src/composables/page-drivers/useMasterDetailAMaintenancePage.ts。 - 新增
src/components/pages/PageMasterDetailAMaintenance.vue承接原本主從維護 UI。
MasterDetailB.vue、MasterDetailC.vue依 Page Driver + Page Component 模式重構。src/views/maint/MasterDetailB.vue與src/views/maint/MasterDetailC.vue均縮減為 10 行。- 新增
src/composables/page-drivers/useMasterDetailBMaintenancePage.ts、src/composables/page-drivers/useMasterDetailCMaintenancePage.ts。 - 新增
src/components/pages/PageMasterDetailBMaintenance.vue、src/components/pages/PageMasterDetailCMaintenance.vue。
- 通用方向已落地為「每頁 page driver + page component」與既有
useCrudCommands()。- Phase 3 未再抽出跨 A/B/C 的大型共用 driver,避免在 demo 變體尚未收斂前建立過早抽象。
Phase 4:非 maintenance 頁面統一 ✅ 已完成
Home.vue、Settings.vue、FncPage.vue套用 Page Driver + Page Component 模式。src/views/Home.vue縮減為 17 行,新增src/components/pages/PageHome.vue與src/composables/page-drivers/useHomePage.ts。src/views/Settings.vue縮減為 10 行,新增src/components/pages/PageSettings.vue與src/composables/page-drivers/useSettingsPage.ts。src/views/FncPage.vue縮減為 10 行,新增src/components/pages/PageFunction.vue與src/composables/page-drivers/useFunctionPage.ts。
App.vue最終只保留 shell 掛載。src/App.vue縮減為 7 行,只掛載AppShell。src/shell/AppShell.vue承接 layout 切換、layout props/events、breadcrumb actions、tabs router-view 與GlobalOverlays掛載。src/composables/layout/useAppShell.ts承接 menu 合併、favorite、breadcrumb、layout action、手動/強制登出流程。
六、命名規範總結
| 層級 | 目錄 | 檔名前綴/範例 |
|---|---|---|
| App Shell | src/shell/ |
AppShell.vue、GlobalOverlays.vue |
| Page Driver | src/views/ |
SingleRecord.vue(route view,不改名) |
| Page Component | src/components/pages/ |
PageMaintenance.vue |
| Section / Shelf | src/components/sections/ |
SectionDataTable.vue、SectionSearchPanel.vue |
| Item / Atom | src/components/items/ |
ItemDataRow.vue、ItemFormField.vue |
| Layout | src/components/layouts/ |
MainLayout.vue(維持) |
| Base | src/components/base/ |
DraggableDialog.vue(維持) |
| Page Driver Composable | src/composables/page-drivers/ |
useMaintenancePage.ts |
| Command Composable | src/composables/commands/ |
useCrudCommands.ts |
| Form Composable | src/composables/forms/ |
useForm.ts |
| Domain Store | src/stores/ |
students.ts(維持) |
| Service Module | src/services/modules/ |
students.ts(維持) |
| Domain Model | src/models/ |
student.ts、page.ts |
七、對齊檢查清單(新增/重構時使用)
- 這個 view 超過 100 行了嗎?→ 考慮抽出 Page Component。
- 這個元件知道自己是水平捲軸還是網格嗎?→ Item 層不該知道,移到 Section 層。
- 這個 dialog 定義在 view 裡嗎?→ 抽出到 SectionFormPanel。
- 這個元件直接呼叫
studentStore.updateStudent()嗎?→ 改為觸發 command 或 emit event。 - 這個狀態需要跨頁共享嗎?→ 是:store;否:composable。
- 這個邏輯是「取得資料」還是「執行動作」?→ query 用 page driver,command 用 command composable。
- 這個元件只服務單一 domain 嗎?→ 是:留在
components/items/或components/sections/的 domain 子目錄;否:才進base/。 - 這個抽象降低了理解成本嗎?→ 否:不要抽。
本文件取代 docs/frontend-layering.md 與 src/components/GUIDE.md 成為新增功能與重構的最高準則。既有檔案可保留作為歷史參考,但後續開發以本文為準。