docs: reorganize architecture strategy documentation

Split current project diagnostics into a dedicated analysis document and
trim the main architecture strategy to focus on core guidance. This makes
the documentation easier to navigate and separates observed issues from
recommended architectural principles.docs: reorganize architecture strategy documentation

Split current project diagnostics into a dedicated analysis document and
trim the main architecture strategy to focus on core guidance. This makes
the documentation easier to navigate and separates observed issues from
recommended architectural principles.
This commit is contained in:
skytek_xinliang
2026-05-19 14:13:10 +08:00
parent 9ae91418e0
commit 96b96bcaaa
12 changed files with 1373 additions and 1077 deletions
+61 -157
View File
@@ -1,139 +1,19 @@
# 資料流與元件分層優化策略
> 分析標的:`~/code/apps.apple.com-main`Apple App Store Web — Svelte + Jet 架構)
> 適用目標:`/home/carl/git/skt-vuetify-templates`Vue 3 + Vuetify + Pinia
> 原則:不強制遷移既有程式碼,但所有新增功能與重構以本文件為最高準則。
---
## 一、Apple App Store 專案的核心架構特徵
### 1.1 單一業務邏輯門面Jet Facade
1 單一業務邏輯門面
2 Intent / Action 分離(查詢與命令)
3 Page Model 驅動 UI(資料驅動)
4 Shelf / Item 分層(容器與內容分離)
5 Svelte Context 作為跨層依賴注入
6 命令式外殼 + 聲明式 UI
```
browser.ts → bootstrap → Jet.load → runtime + objectGraph
UI 層僅透過 jet.dispatch(intent) / jet.perform(action) 溝通
```
- **Jet** 封裝所有業務邏輯:路由、資料取得、動作分發、metrics。
- UI 層**不直接**呼叫 API、不直接操作 storage、不直接操作 history。
- 所有外部依賴(fetch、storage、locale、user)統一注入 `Dependencies`,再組裝成 `ObjectGraph`
### 1.2 Intent / Action 分離(查詢與命令)
| 類型 | 職責 | 回傳值 | 例子 |
|------|------|--------|------|
| **Intent** | 取得頁面資料(Query | `Promise<Page>` | `RouteUrlIntent` → 回傳 `ProductPage` |
| **Action** | 執行副作用(Command | `'performed' \| 'unsupported'` | `FlowAction` → 導航到新頁面 |
- `FlowAction` 是主要導航機制:內含 `destination Intent` + `pageUrl` + `presentationContext`
- Action handler 註冊採用**型別註冊制**:`jet.onAction('flowAction', handler)`
### 1.3 Page Model 驅動 UI(資料驅動)
```ts
// App.svelte 的介面極簡
export let page: Promise<Page> | Page
```
- 整個應用由單一 `page` prop 驅動。
- `PageResolver` 處理 `Promise<Page>` 的 loading / error 狀態。
- `Page.svelte` 用 type guard 分發到對應的 page component`isProductPage(page)``<ProductPage>`
- **Page 是 union type**,不是 route-based 的硬編碼映射。
### 1.4 Shelf / Item 分層(容器與內容分離)
```
Page (TodayPage / ProductPage / ...)
└── Shelf[] (水平捲軸 / 網格)
└── ShelfItemLayout (佈局抽象:HorizontalShelf or Grid)
└── Item (BrickItem / LargeLockupItem / ...)
```
- **Shelf** = 容器邏輯:決定是水平捲軸還是網格、rowsPerColumn、邊框。
- **ShelfItemLayout** = 佈局中介:根據 `isHorizontal` 選擇 `HorizontalShelf``Grid`
- **Item** = 純粹內容渲染:只關心單一資料單位的呈現,不知道自己是水平還是網格。
- **FallbackShelf** = 優雅的降級策略:遇到未實作的 shelf 類型顯示 placeholder,不 crash。
### 1.5 Svelte Context 作為跨層依賴注入
```ts
// bootstrap.ts
context.set('jet', jet)
context.set('i18n', i18nStore)
// 深層元件
const jet = getJet() // 從 Svelte Context 取得
const i18n = getI18n() // 從 Svelte Context 取得
```
- 避免 props drillingJet、i18n、accessibility layout、today-card layout 都透過 context 傳遞。
- Context 在啟動時注入,生命周期與應用一致,不是用來傳遞 UI 狀態的。
### 1.6 命令式外殼 + 聲明式 UI
```ts
// browser.ts(命令式啟動層)
const app = new App({ target: container, context, hydrate: true })
registerActionHandlers({
jet,
updateApp: (props) => app.$set(props), // 橋接命令式 → 聲明式
})
```
- 導航、歷史管理、scroll 復原由命令式的 action handler 處理。
- UI 渲染完全聲明式,只接收 `page``isFirstPage` 兩個 prop。
---
Read only when needed: [what apple do](./what-apple-do.md)
## 二、我們專案的現況診斷
### 2.1 App.vue 過度臃腫(~590 行)
| 職責 | 行數 | 應屬層級 |
|------|------|----------|
| Layout 切換 | ~20 | App Shell |
| Tabs 管理 | ~80 | Page Driver |
| Breadcrumb 組裝 | ~40 | Layout |
| Favorites 管理 | ~60 | Store |
| Search Dialog | ~80 | App Shell / Widget |
| Message Dialog | ~60 | App Shell / Widget |
| Snackbar | ~10 | Global Overlay |
| Logout / Force logout | ~30 | Auth Flow |
| HTTP Toast | ~20 | Service Layer |
- **問題**App.vue 同時承擔 App Shell、Page Driver、Global Widget、Auth Flow 四種責任。
- **對比**App Store 的 `App.svelte` 只有 161 行,只負責 `Navigation + PageResolver + Footer`
### 2.2 Views 過厚(SingleRecord.vue ~830 行)
- 混雜:表格呈現、搜尋表單、dialog 模板、表單狀態、CRUD 流程、驗證邏輯、分頁、snackbar。
- **對比**App Store 的 `ProductPage.svelte` 只有 77 行,只負責「把 page 轉成 DefaultPageRequirements + 一個 slot override」。
### 2.3 缺乏統一的頁面資料門面
```
現況:
view → store → service(直接鏈式呼叫)
view 自己管理 loading / error / dialog visible
App Store
UI → jet.dispatch(intent) → runtime → controller → page model
UI 只接收 page model,不管理載入狀態
```
### 2.4 Dialog 狀態與模板內嵌於 View
- `SingleRecord.vue` 內含 5 個 `ConfirmDialog` 實例 + 1 個大 form overlay。
- 任何 dialog 更動都需要修改 view 檔案。
### 2.5 沒有容器/內容分離的 Section 層
- 表格、表單、搜尋區塊都是直接寫在 view 或 page component 中。
- 缺乏類似 `ShelfItemLayout` 的通用佈局抽象:「這一區是水平捲軸還是網格」應該由容器決定,裡面的內容元件不應該知道。
---
Read only when needed: [analyse now](./analyse-now.md)
## 三、優化後的資料流策略
@@ -188,7 +68,7 @@ App Store
```ts
// src/composables/usePageDriver.ts
export function useStudentMaintenancePage() {
export function useMaintenancePage() {
const studentStore = useStudentStore()
const { records, loading, error, load } = useCrudDriver({
store: studentStore,
@@ -264,7 +144,7 @@ src/
├── components/
│ ├── pages/ ← 新增:Page Component 層
│ │ ├── PageStudentMaintenance.vue
│ │ ├── PageMaintenance.vue
│ │ └── PageReport.vue
│ │
│ ├── sections/ ← 新增:Section / Shelf 層
@@ -273,7 +153,7 @@ src/
│ │ └── SectionFormPanel.vue
│ │
│ ├── items/ ← 新增:Item / Atom 層(領域獨立)
│ │ ├── ItemStudentRow.vue
│ │ ├── ItemDataRow.vue
│ │ └── ItemFormField.vue
│ │
│ ├── layouts/ ← 維持:App Shell Layout
@@ -285,11 +165,11 @@ src/
├── composables/
│ ├── page-drivers/ ← 新增:頁面資料協調
│ │ └── useStudentMaintenancePage.ts
│ │ └── useMaintenancePage.ts
│ ├── commands/ ← 新增:命令流程(對齊 Jet Action)
│ │ └── useStudentCrudCommands.ts
│ │ └── useCrudCommands.ts
│ ├── forms/ ← 維持/重組:表單狀態機
│ │ └── useStudentForm.ts
│ │ └── useForm.ts
│ └── layout/ ← 維持
├── models/ ← 新增:領域模型與 Page Union
@@ -321,15 +201,18 @@ src/
```vue
<!-- views/maint/SingleRecord.vue優化後 -->
<script setup lang="ts">
import { useStudentMaintenancePage } from '@/composables/page-drivers/useStudentMaintenancePage'
import PageStudentMaintenance from '@/components/pages/PageStudentMaintenance.vue'
import { useMaintenancePage } from '@/composables/page-drivers/useMaintenancePage'
import PageMaintenance from '@/components/pages/PageMaintenance.vue'
const { pageModel, load } = useStudentMaintenancePage()
const { pageModel, load } = useMaintenancePage({
title: '單筆資料維護',
records: [],
})
load()
</script>
<template>
<PageStudentMaintenance :page="pageModel" />
<PageMaintenance :page="pageModel" />
</template>
```
@@ -340,7 +223,7 @@ load()
- **對齊**App Store 的 `ProductPage.svelte``TodayPage.svelte``DefaultPage.svelte`
```vue
<!-- components/pages/PageStudentMaintenance.vue -->
<!-- components/pages/PageMaintenance.vue -->
<template>
<PageMaintShell :title="page.title">
<template #search>
@@ -392,7 +275,7 @@ load()
- **對齊**App Store 的 `BrickItem.svelte``LargeLockupItem.svelte`
```vue
<!-- ItemStudentRow.vue -->
<!-- ItemDataRow.vue -->
<template>
<div class="d-flex ga-2">
<v-chip size="small" :color="statusColor(data.status)">
@@ -407,7 +290,7 @@ load()
| 場景 | 容器(Section | 內容(Item |
|------|-----------------|--------------|
| 資料表格 | `SectionDataTable`(決定 headers、分頁、排序) | `ItemStudentRow`(決定單列呈現) |
| 資料表格 | `SectionDataTable`(決定 headers、分頁、排序) | `ItemDataRow`(決定單列呈現) |
| 搜尋面板 | `SectionSearchPanel`(決定展開/收合、grid 佈局) | `ItemFormField`(單一輸入框呈現) |
| 圖文列表 | `SectionCardGrid`(決定欄數、gap、RWD | `ItemProductCard`(卡片內容) |
| 表單對話框 | `SectionFormPanel`(決定 dialog 外殼、actions | `ItemFormFieldGroup`(欄位群組) |
@@ -461,20 +344,41 @@ views/xxx.vue
## 五、重構優先順序與遷移路徑
### Phase 1:建立基礎設施(不動既有 view)
### Phase 1:建立基礎設施(不動既有 view) ✅ 已完成
1. 新增 `src/models/`:定義領域模型與 Page union type。
2. 新增 `src/shell/` `App.vue` 抽出 `GlobalOverlays.vue``AppTabs.vue`
3. 新增 `src/components/pages/`:建立第一個 `PageStudentMaintenance.vue`(可從 `PageMaint.vue` 擴展)
4. 新增 `src/composables/page-drivers/`:建立 `useStudentMaintenancePage.ts`
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。
- 使用 `PageMaint.vue` 作為佈局外殼,搜尋與表格區塊以 slot 開放,不綁定特定領域型別。
4. [x] 新增 `src/composables/page-drivers/`:建立 `useMaintenancePage.ts`
- 透過 options 傳入 title 與 records,協調搜尋條件、分頁與 `pageModel`
- 提供 `load()``resetSearch()` 供 Page Driver 呼叫。
### Phase 2:遷移最厚的 viewSingleRecord.vue
### Phase 2:遷移最厚的 viewSingleRecord.vue ✅ 已完成
1.`SingleRecord.vue` 的模板抽出到 `PageStudentMaintenance.vue`
2. 將 dialog 抽出到 `SectionFormPanel.vue`
3. 將表單欄位抽出到 `ItemFormFieldGroup.vue``ItemFormField.vue[]`
4. 將 CRUD 流程抽出到 `composables/commands/useStudentCrudCommands.ts`
5. `SingleRecord.vue` 目標縮減到 < 80 行
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 中不再直接定義 `<teleport>``<v-overlay>` 或多個確認 dialog。
5. [x] 將表單欄位抽出到 `src/components/items/ItemFormFieldGroup.vue`
- 只呈現欄位與欄位錯誤,透過 `v-model``clear-field-error` 與上層互動。
6. [x] 將 CRUD command 流程抽出到 `src/composables/commands/useCrudCommands.ts`
- 負責新增、載入編輯/檢視資料、儲存前確認、實際儲存與 highlight 流程。
7. [x] 新增 `src/composables/page-drivers/useSingleRecordMaintenancePage.ts` 作為本頁協調層。
- 集中組裝 page model、搜尋狀態、表格分頁、表單狀態、CRUD command 與 dialog flow。
- `SingleRecord.vue` 不再直接操作 `studentStore`
### Phase 3:推廣到所有 maintenance 頁面
@@ -494,14 +398,14 @@ views/xxx.vue
|------|------|---------------|
| App Shell | `src/shell/` | `AppShell.vue``GlobalOverlays.vue` |
| Page Driver | `src/views/` | `SingleRecord.vue`route view,不改名) |
| Page Component | `src/components/pages/` | `PageStudentMaintenance.vue` |
| Page Component | `src/components/pages/` | `PageMaintenance.vue` |
| Section / Shelf | `src/components/sections/` | `SectionDataTable.vue``SectionSearchPanel.vue` |
| Item / Atom | `src/components/items/` | `ItemStudentRow.vue``ItemFormField.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/` | `useStudentMaintenancePage.ts` |
| Command Composable | `src/composables/commands/` | `useStudentCrudCommands.ts` |
| Form Composable | `src/composables/forms/` | `useStudentForm.ts` |
| 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` |