跳至主要内容

Applet 設計模式

本文解釋 AppFuse Web 的核心架構模式:Applet。

什麼是 Applet?

Applet 是獨立的業務功能模組,類似於微前端的概念,但在單一應用程式內實現。

Application
├── Applet: 商品管理
├── Applet: 訂單管理
├── Applet: 客戶管理
└── Applet: 系統設定

為什麼使用 Applet?

問題:傳統 SPA 的挑戰

傳統 SPA 隨著功能增長,常遇到:

  1. 程式碼耦合:不同功能的程式碼混在一起
  2. 難以維護:修改一處可能影響其他地方
  3. 協作困難:多人同時開發容易衝突
  4. 載入緩慢:所有功能打包在一起

解決方案:Applet 模式

Applet 將應用程式拆分為獨立模組:

傳統 SPAApplet 模式
功能散落各處功能集中在 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

AppletComponent
路由有獨立路由無路由
狀態可有內部狀態通常無狀態或受控
大小完整功能模組單一 UI 元素
用途業務功能UI 復用

實作建議

狀態管理

Applet 內部狀態 → useState, useReducer
跨 Applet 狀態 → Redux (features/)
伺服器狀態 → RTK Query
URL 狀態 → useSearchParams

通訊模式

Applet 之間不應直接通訊,而是透過:

  1. URL 參數:傳遞 ID 或簡單資料
  2. Redux Store:共享全局狀態
  3. 事件:使用 custom events(謹慎使用)
// ✅ 透過 URL 導航
navigate(`/customers/${customerId}`);

// ✅ 透過 Redux 共享
dispatch(selectCustomer(customerId));

// ❌ 避免直接引用
import { customerData } from '../customers/store';

下一步