Files
skt-vuetify-templates/docs/architecture-strategy.md
T
skytek_xinliang ad00f5c195 docs: clarify optional page drivers in page guide
Update documentation to show that simple pages can define page models
directly in views without creating a page driver. Adjust examples,
section numbering, and naming guidance to better distinguish simple view
state from reusable page-driver patterns.docs: clarify optional page drivers in page guide

Update documentation to show that simple pages can define page models
directly in views without creating a page driver. Adjust examples,
section numbering, and naming guidance to better distinguish simple view
state from reusable page-driver patterns.
2026-05-27 11:18:19 +08:00

22 KiB
Raw Blame History

資料流與元件分層優化策略

一、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(組裝多個來源)

範例:

// views/maint/Example.vue — 簡單頁面直接在 view 組裝 page model
const studentStore = useStudentStore()
const pageModel = computed<MaintenancePageModel>(() => ({
  type: 'maintenance',
  title: '單筆資料維護',
  records: studentStore.students,
  loading: false,
  error: null,
}))

3.3 查詢(Query)與命令(Command)分離

類型 資料流 錯誤處理 狀態位置
Query usePageDriverpageModel → 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/         ← 新增:頁面資料協調(僅複雜頁面需要)
│   │   └── 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 Shellsrc/shell/

  • 職責layout 切換、全域 overlaysnackbar、dialog)、頁籤容器、事件總線橋接。
  • 禁止:頁面專屬業務流程、頁面資料組裝、特定 dialog 內容。
  • 對齊App Store 的 App.svelte + browser.ts 的 overlay 部分。

Layer 2: Page Driversrc/views/

  • 職責
    1. 呼叫 useXxxPage() 取得 pageModel
    2. pageModel 與事件處理器傳給對應的 PageXxx.vue
    3. 處理 route param 解析(僅限轉換,不含業務邏輯)。
  • 目標行數< 80 行。
  • 禁止:大量模板、dialog 定義、form 欄位、直接操作 store。
<!-- views/maint/SingleRecord.vue優化後 -->
<script setup lang="ts">
import PageMaintenance from '@/components/pages/PageMaintenance.vue'
import { useSingleRecordMaintenancePage } from '@/composables/page-drivers/useSingleRecordMaintenancePage'

const { pageModel, commands, formPanelProps, formPanelEvents } = useSingleRecordMaintenancePage()
</script>

<template>
  <PageMaintenance :page="pageModel" />
</template>

Layer 3: Page Componentsrc/components/pages/

  • 職責:組裝完整頁面的 section 順序、處理 page-level slot override、分發頁面級事件。
  • 命名:一律 Page 前綴。
  • 對齊App Store 的 ProductPage.svelteTodayPage.svelteDefaultPage.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 / Shelfsrc/components/sections/

  • 職責:決定「這一區的佈局方式」(水平捲軸、網格、列表、摺疊面板)。
  • 禁止:知道上層 page 的業務邏輯、不直接呼叫 API。
  • 對齊App Store 的 Shelf.svelteShelfItemLayout.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 / Atomsrc/components/items/

  • 職責:純粹呈現單一資料單位。
  • 禁止:知道自己是水平捲軸還是網格、不管理任何狀態。
  • 對齊App Store 的 BrickItem.svelteLargeLockupItem.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
        └── SectionFormPaneldialog shell
              ├── MntDialogCard(外殼:標題、toolbar、actions
              └── ItemFormFieldGroup(內容:欄位)
  • SectionFormPanel 管理 dialog 的開關、mode、loading、saving。
  • ItemFormFieldGroup 純粹呈現欄位,不知道自己在 dialog 裡。
  • View 中不再出現 <teleport><v-overlay><v-dialog> 的具體定義。

五、重構優先順序與遷移路徑

Phase 1:建立基礎設施(不動既有 view) 已完成

  1. 新增 src/models/:定義領域模型與 Page union type。
    • src/models/student.ts:抽出 StudentRecordstores/students.ts 改為 re-export 以保持向後相容。
    • src/models/page.ts:定義 BasePageModelMaintenancePageModelPageModel union。
  2. 新增 src/shell/:從 App.vue 抽出 GlobalOverlays.vueAppTabs.vue
    • src/shell/AppShell.vuelayout 切換與全域 overlay 掛載點。
    • src/shell/AppTabs.vue:頁籤管理與 router-view 容器。
    • src/shell/GlobalOverlays.vuesnackbar、搜尋 dialog、訊息 dialog。
  3. 新增 src/components/pages/:建立第一個 PageMaintenance.vue(可從 PageMaint.vue 擴展)。
    • 定義 MaintenancePageModel props 與 create/edit/view/delete/search emits。
    • 使用 PageMaint.vue 作為佈局外殼,搜尋與表格區塊以 slot 開放,不綁定特定領域型別。
  4. 新增 src/composables/page-drivers/:建立第一個 page driver 範例。
    • 協調搜尋條件、分頁與 pageModel
    • 提供 load()resetSearch() 供 Page Driver 呼叫。
    • 後續已刪除純包裝型 driver(如 useMaintenancePage)。僅當頁面需要協調多個 composable 時才建立 page driver。

Phase 2:遷移最厚的 viewSingleRecord.vue 已完成

  1. SingleRecord.vue 縮減為 route-level Page Driver。
    • 目前只負責呼叫 useSingleRecordMaintenancePage(),並組裝 PageMaintenanceSectionSearchPanelSectionDataTableSectionFormPanel
    • 行數已由 921 行縮減至 52 行,達成 < 80 行目標。
  2. 將搜尋區塊抽出到 src/components/sections/SectionSearchPanel.vue
    • 負責搜尋欄位佈局與 reset 事件,不直接操作 store。
  3. 將表格與分頁抽出到 src/components/sections/SectionDataTable.vue
    • 負責 headers、資料列 slot、操作按鈕與分頁 footer,透過 emit 回傳 view/edit/delete/page 事件。
  4. 將 dialog 抽出到 src/components/sections/SectionFormPanel.vue
    • 包含側邊 overlay、MntDialogCard、record navigation toolbar 與確認 dialog。
    • View 中不再直接定義 <teleport><v-overlay> 或多個確認 dialog。
  5. 將表單欄位抽出到 src/components/items/ItemFormFieldGroup.vue
    • 只呈現欄位與欄位錯誤,透過 v-modelclear-field-error 與上層互動。
  6. 將 CRUD command 流程抽出到 src/composables/commands/useCrudCommands.ts
    • 負責新增、載入編輯/檢視資料、儲存前確認、實際儲存與 highlight 流程。
  7. 新增 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. 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. 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. MasterDetailB.vueMasterDetailC.vue 依 Page Driver + Page Component 模式重構。
    • src/views/maint/MasterDetailB.vuesrc/views/maint/MasterDetailC.vue 均縮減為 10 行。
    • 新增 src/composables/page-drivers/useMasterDetailBMaintenancePage.tssrc/composables/page-drivers/useMasterDetailCMaintenancePage.ts
    • 新增 src/components/pages/PageMasterDetailBMaintenance.vuesrc/components/pages/PageMasterDetailCMaintenance.vue
  4. 通用方向已落地為「每頁 page driver + page component」與既有 useCrudCommands()
    • Phase 3 未再抽出跨 A/B/C 的大型共用 driver,避免在 demo 變體尚未收斂前建立過早抽象。

Phase 4:非 maintenance 頁面統一 已完成

後續簡化時,Settings/FncPage 的薄 page driver 已 inline 回 view,型別移至 page component 自身。

  1. Home.vueSettings.vueFncPage.vue 套用 Page Driver + Page Component 模式。
    • src/views/Home.vue 縮減為 17 行,新增 src/components/pages/PageHome.vuesrc/composables/page-drivers/useHomePage.ts
    • src/views/Settings.vue 縮減為 10 行,新增 src/components/pages/PageSettings.vuesrc/composables/page-drivers/useSettingsPage.ts
    • src/views/FncPage.vue 縮減為 10 行,新增 src/components/pages/PageFunction.vuesrc/composables/page-drivers/useFunctionPage.ts
  2. 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.vueGlobalOverlays.vue
Page Driver src/views/ SingleRecord.vueroute view,不改名)
Page Component src/components/pages/ PageMaintenance.vue
Section / Shelf src/components/sections/ SectionDataTable.vueSectionSearchPanel.vue
Item / Atom src/components/items/ ItemDataRow.vueItemFormField.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/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.tspage.ts

七、對齊檢查清單(新增/重構時使用)

  • 這個 view 超過 100 行了嗎?→ 考慮抽出 Page Component。
  • 這個元件知道自己是水平捲軸還是網格嗎?→ Item 層不該知道,移到 Section 層。
  • 這個 dialog 定義在 view 裡嗎?→ 抽出到 SectionFormPanel。
  • 這個元件直接呼叫 studentStore.updateStudent() 嗎?→ 改為觸發 command 或 emit event。
  • 這個狀態需要跨頁共享嗎?→ 是:store;否:composable。
  • 這個邏輯是「取得資料」還是「執行動作」?→ query 用 page drivercommand 用 command composable。
  • 這個元件只服務單一 domain 嗎?→ 是:留在 components/items/components/sections/ 的 domain 子目錄;否:才進 base/
  • 這個抽象降低了理解成本嗎?→ 否:不要抽。

本文件取代 docs/frontend-layering.mdsrc/components/GUIDE.md 成為新增功能與重構的最高準則。既有檔案可保留作為歷史參考,但後續開發以本文為準。