# 資料流與元件分層優化策略 ## 一、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](./what-apple-do.md) ## 二、我們專案的現況診斷 Read only when needed: [analyse now](./analyse-now.md) ## 三、優化後的資料流策略 ### 3.1 核心資料流(單向 + 集中閘道) ``` ┌─────────────────────────────────────────────────────────────┐ │ App Shell │ │ (App.vue → Layout → Global Overlays: Snackbar/Dialogs) │ └──────────────────────────┬──────────────────────────────────┘ │ reactive / props ▼ ┌─────────────────────────────────────────────────────────────┐ │ View │ │ (views/*.vue — 自含 page model、頁面 UI 與 section 組合) │ └──────────────────────────┬──────────────────────────────────┘ │ 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 的職責從「管理資料 + 管理狀態 + 組裝模板」縮減為「呼叫 composable 取得 page model,組裝 section 元件」。 - Page model 可以來自: - store(已有快取) - service(直接 API) - composable(組裝多個來源) 範例: ```ts // views/maint/Example.vue — 簡單頁面直接在 view 組裝 page model const studentStore = useStudentStore() const pageModel = computed(() => ({ type: 'maintenance', title: '單筆資料維護', records: studentStore.students, loading: false, error: null, })) ``` ### 3.3 查詢(Query)與命令(Command)分離 | 類型 | 資料流 | 錯誤處理 | 狀態位置 | |------|--------|----------|----------| | **Query** | `usePageDriver` → `pageModel` → props | `` 或 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 頁面載入(可選進階) ```vue ``` - 若頁面資料支援 async setup 或 `usePageDriver` 回傳 Promise,可用 Vue `` 達到與 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/ ← 維持:自含頁面,邏輯與 UI 同檔 │ └── maint/ │ └── SingleRecord.vue ← ~50 行:組裝 pageModel + MaintShell 外殼 │ ├── components/ │ ├── sections/ ← 新增:Section / Shelf 層 │ │ ├ │ │ ├── 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/ ← 新增:頁面資料協調(僅複雜頁面需要) │ │ └── useSingleRecordMaintenancePage.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/`) - **職責**: 1. 呼叫 `useXxxPage()` 取得 `pageModel`。 2. 將 `pageModel` 與事件處理器傳給對應的 `PageXxx.vue`。 3. 處理 route param 解析(僅限轉換,不含業務邏輯)。 - **目標行數**:< 80 行。 - **禁止**:大量模板、dialog 定義、form 欄位、直接操作 store。 ```vue ``` #### Layer 3: View(`src/views/`) - **職責**:自含頁面的完整入口 — 組裝 page model、協調 composable、撰寫頁面 template。 - **禁止**:頁面 UI 不再拆到另一個 page component 層。 - **對齊**:標準 Vue SPA 慣例。 ```vue ``` #### Layer 4: Section / Shelf(`src/components/sections/`) - **職責**:決定「這一區的佈局方式」(水平捲軸、網格、列表、摺疊面板)。 - **禁止**:知道上層 page 的業務邏輯、不直接呼叫 API。 - **對齊**:App Store 的 `Shelf.svelte`、`ShelfItemLayout.svelte`。 ```vue ``` #### Layer 5: Item / Atom(`src/components/items/`) - **職責**:純粹呈現單一資料單位。 - **禁止**:知道自己是水平捲軸還是網格、不管理任何狀態。 - **對齊**:App Store 的 `BrickItem.svelte`、`LargeLockupItem.svelte`。 ```vue ``` ### 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: ```ts // 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 中不再出現 ``、``、`` 的具體定義。 --- ## 五、重構優先順序與遷移路徑 ### Phase 1:建立基礎設施(不動既有 view) ✅ 已完成 1. [x] 新增 `src/models/`:定義領域模型與 Page union type。 - `src/models/student.ts`:抽出 `StudentRecord`,`stores/students.ts` 改為 re-export 以保持向後相容。 - `src/models/page.ts`:定義 `BasePageModel`、`MaintenancePageModel` 與 `PageModel` union。 2. [x] 新增 `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。 3. [x] 新增 `src/components/pages/`:建立第一個 `PageMaintenance.vue`(可從 `PageMaint.vue` 擴展)。 - 定義 `MaintenancePageModel` props 與 `create/edit/view/delete/search` emits。 - 使用 `MaintShell.vue` 作為佈局外殼,搜尋與表格區塊以 slot 開放,不綁定特定領域型別。 4. [x] 新增 `src/composables/page-drivers/`:建立第一個 page driver 範例。 - 協調搜尋條件、分頁與 `pageModel`。 - 提供 `load()` 與 `resetSearch()` 供 Page Driver 呼叫。 - 後續已刪除純包裝型 driver(如 `useMaintenancePage`)。僅當頁面需要協調多個 composable 時才建立 page driver。 ### Phase 2:遷移最厚的 view(SingleRecord.vue) ✅ 已完成 1. [x] 將 `SingleRecord.vue` 縮減為 route-level Page Driver。 - 目前只負責呼叫 `useSingleRecordMaintenancePage()`,並組裝 `PageMaintenance`、`SectionSearchPanel`、`SectionDataTable`、`SectionFormPanel`。 - 行數已由 921 行縮減至 52 行,達成 < 80 行目標。 2. [x] 將搜尋區塊抽出到 `src/components/sections/SectionSearchPanel.vue`。 - 負責搜尋欄位佈局與 reset 事件,不直接操作 store。 3. [x] 將表格與分頁抽出到 `src/components/sections/SectionDataTable.vue`。 - 負責 headers、資料列 slot、操作按鈕與分頁 footer,透過 emit 回傳 view/edit/delete/page 事件。 4. [x] 將 dialog 抽出到 `src/components/sections/SectionFormPanel.vue`。 - 包含側邊 overlay、`MntDialogCard`、record navigation toolbar 與確認 dialog。 - View 中不再直接定義 ``、`` 或多個確認 dialog。 5. [x] 將表單欄位抽出到 `src/components/items/ItemFormFieldGroup.vue`。 - 只呈現欄位與欄位錯誤,透過 `v-model` 與 `clear-field-error` 與上層互動。 6. [x] 將 CRUD command 流程抽出到 `src/composables/useCrudCommands.ts`。 - 負責新增、載入編輯/檢視資料、儲存前確認、實際儲存與 highlight 流程。 7. [x] 新增 `src/composables/page-drivers/useSingleRecordMaintenancePage.ts` 作為本頁協調層。 - 集中組裝 page model、搜尋狀態、表格分頁、表單狀態、CRUD command 與 dialog flow。 - `SingleRecord.vue` 不再直接操作 `studentStore`。 ### Phase 3:推廣到所有 maintenance 頁面 ✅ 已完成 > 後續簡化時,B/C/EditableGrid 的薄 page driver 已 inline 回 view,只保留有真實複雜邏輯的 driver。 1. [x] `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` 作為主要內容元件。 2. [x] `MasterDetailA.vue` 依 Page Driver + Page Component 模式重構。 - `src/views/maint/MasterDetailA.vue` 縮減為 34 行。 - 新增 `src/composables/page-drivers/useMasterDetailAMaintenancePage.ts`。 - 新增 `src/components/pages/PageMasterDetailAMaintenance.vue` 承接原本主從維護 UI。 3. [x] `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`。 4. [x] 通用方向已落地為「每頁 page driver + page component」與既有 `useCrudCommands()`。 - Phase 3 未再抽出跨 A/B/C 的大型共用 driver,避免在 demo 變體尚未收斂前建立過早抽象。 ### Phase 4:非 maintenance 頁面統一 ✅ 已完成 > 後續簡化時,Settings/FncPage 的薄 page driver 已 inline 回 view,型別移至 page component 自身。 1. [x] `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`。 2. [x] `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、手動/強制登出流程。 ### Phase 5:移除 Page Component 層 ✅ 已完成 > 所有 page component 已合併回對應的 view,`src/components/pages/` 目錄已刪除。page driver 簡化為僅複雜頁面才使用的選配層,view 回歸標準 Vue SPA 慣例:自含 page model + 頁面 UI + section 組合。 --- ## 六、命名規範總結 | 層級 | 目錄 | 檔名前綴/範例 | |------|------|---------------| | App Shell | `src/shell/` | `AppShell.vue`、`GlobalOverlays.vue` | | View(自含頁面) | `src/views/` | `SingleRecord.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/` | `useSingleRecordMaintenancePage.ts` | | Command Composable | `src/composables/` | `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 超過 200 行了嗎?→ 考慮抽出 Section 或 composable。 - [ ] 這個元件知道自己是水平捲軸還是網格嗎?→ 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` 成為新增功能與重構的最高準則。既有檔案可保留作為歷史參考,但後續開發以本文為準。*