Applet 設計模式
本文解釋 AppFuse Web 的核心架構模式:Applet。
什麼是 Applet?
Applet 是獨立的業務功能模組,類似於微前端的概念,但在單一應用程式內實現。
Application
├── Applet: 商品管理
├── Applet: 訂單管理
├── Applet: 客戶管理
└── Applet: 系統設定
為什麼使用 Applet?
問題:傳統 SPA 的挑戰
傳統 SPA 隨著功能增長,常遇到:
- 程式碼耦合:不同功能的程式碼混在一起
- 難以維護:修改一處可能影響其他地方
- 協作困難:多人同時開發容易衝突
- 載入緩慢:所有功能打包在一起
解決方案:Applet 模式
Applet 將應用程式拆分為獨立模組:
| 傳統 SPA | Applet 模式 |
|---|---|
| 功能散落各處 | 功能集中在 Applet |
| 全局共享狀態 | Applet 內部狀態 |
| 路由集中配置 | Applet 擁有自己的路由 |
| 整體打包 | 可按需載入 |
Applet 的結構
每個 Applet 是一個自包含的功能單元:
src/applets/orders/
├── OrderListApplet.tsx # 列表頁面
├── OrderDetailApplet.tsx # 詳情頁面
├── OrderFormApplet.tsx # 編輯頁面
├── components/ # Applet 專用組件
│ ├── OrderCard.tsx
│ └── OrderStatusBadge.tsx
├── hooks/ # Applet 專用 hooks
│ └── useOrderActions.ts
├── services/ # API 服務
│ └── orderService.ts
├── types/ # TypeScript 類型
│ └── order.types.ts
└── index.ts # 導出入口
Applet 的特性
1. 獨立路由
每個 Applet 擁有自己的 URL 路徑:
// 訂單 Applet 的路由
/orders → OrderListApplet
/orders/:id → OrderDetailApplet
/orders/:id/edit → OrderFormApplet
2. 標準佈局
使用 AppletShell 提供一致的視覺結構:
import { AppletShell } from '@appfuse/appfuse-web';
function OrderListApplet() {
return (
<AppletShell
title="訂單管理"
description="管理所有訂單"
actions={<CreateButton />}
>
<OrderTable />
</AppletShell>
);
}
3. 可插拔
Applet 可以:
- 獨立開發和測試
- 動態載入(lazy loading)
- 根據權限顯示或隱藏
// 動態載入
const OrderApplet = lazy(() => import('./applets/orders'));
// 權限控制
<RoleGuard roles={['SALES', 'MANAGER']}>
<Route path="/orders/*" element={<OrderApplet />} />
</RoleGuard>
何時建立 Applet?
適合建立 Applet
- 完整的業務功能(如:訂單管理、客戶管理)
- 有獨立的路由入口
- 需要特定權限才能存取
- 可能需要獨立開發/維護
不適合建立 Applet
- 共用的 UI 組件 → 放在
components/ - 工具函數 → 放在
utils/ - 全局狀態 → 放在
features/
Applet vs Component
| Applet | Component | |
|---|---|---|
| 路由 | 有獨立路由 | 無路由 |
| 狀態 | 可有內部狀態 | 通常無狀態或受控 |
| 大小 | 完整功能模組 | 單一 UI 元素 |
| 用途 | 業務功能 | UI 復用 |
實作建議
狀態管理
Applet 內部狀態 → useState, useReducer
跨 Applet 狀態 → Redux (features/)
伺服器狀態 → RTK Query
URL 狀態 → useSearchParams
通訊模式
Applet 之間不應直接通訊,而是透過:
- URL 參數:傳遞 ID 或簡單資料
- Redux Store:共享全局狀態
- 事件:使用 custom events(謹慎使用)
// ✅ 透過 URL 導航
navigate(`/customers/${customerId}`);
// ✅ 透過 Redux 共享
dispatch(selectCustomer(customerId));
// ❌ 避免直接引用
import { customerData } from '../customers/store';
下一步
- 建立第一個 Applet - 實作教學
- 專案結構 - 完整目錄說明
- 路由系統 - 路由配置