建立第一個 Applet
本指南將帶你建立一個簡單的 Applet,展示如何使用 AppFuse Web 快速開發業務功能模組。
什麼是 Applet?
Applet 是獨立的業務功能模組,具有以下特性:
- 獨立路由:擁有自己的 URL 路徑
- 完整功能:包含 UI、狀態、業務邏輯
- 可插拔:可獨立開發、測試、部署
- 標準佈局:使用
<AppletShell>提供一致的外觀
範例:任務管理 Applet
我們將建立一個簡單的任務管理 Applet,包含:
- 任務列表顯示
- 新增任務表單
- 任務狀態切換
步驟 1:定義類型
在 src/types/ 建立 task.types.ts:
export interface Task {
id: string;
title: string;
description?: string;
completed: boolean;
createdAt: string;
updatedAt?: string;
}
export interface TaskCreateRequest {
title: string;
description?: string;
}
export interface TaskUpdateRequest {
title?: string;
description?: string;
}
步驟 2:建立 API 服務
在 src/services/ 建立 task.service.ts:
import { api } from './api';
import type { Task, TaskCreateRequest, TaskUpdateRequest } from '@/types/task.types';
export const taskService = {
findAll: async (completed?: boolean) => {
const params = completed !== undefined ? { completed } : {};
return api.get<Task[]>('/api/tasks', { params });
},
findById: async (id: string) => {
return api.get<Task>(`/api/tasks/${id}`);
},
create: async (data: TaskCreateRequest) => {
return api.post<Task>('/api/tasks', data);
},
update: async (id: string, data: TaskUpdateRequest) => {
return api.put<Task>(`/api/tasks/${id}`, data);
},
delete: async (id: string) => {
return api.delete(`/api/tasks/${id}`);
},
markAsCompleted: async (id: string) => {
return api.patch<Task>(`/api/tasks/${id}/complete`);
},
};
步驟 3:建立 Mock Handlers(開發階段)
在 src/mocks/handlers/ 建立 task.handlers.ts:
import { http, HttpResponse } from 'msw';
import type { Task } from '@/types/task.types';
// Mock 資料
let tasks: Task[] = [
{
id: '1',
title: 'Learn AppFuse Web',
description: 'Complete the getting started guide',
completed: false,
createdAt: new Date().toISOString(),
},
{
id: '2',
title: 'Build first Applet',
description: 'Create a task management Applet',
completed: true,
createdAt: new Date().toISOString(),
},
];
export const taskHandlers = [
// GET /api/tasks
http.get('/api/tasks', ({ request }) => {
const url = new URL(request.url);
const completed = url.searchParams.get('completed');
let filtered = tasks;
if (completed !== null) {
filtered = tasks.filter(
(t) => t.completed === (completed === 'true')
);
}
return HttpResponse.json(filtered);
}),
// POST /api/tasks
http.post('/api/tasks', async ({ request }) => {
const data = await request.json() as { title: string; description?: string };
const newTask: Task = {
id: String(tasks.length + 1),
title: data.title,
description: data.description,
completed: false,
createdAt: new Date().toISOString(),
};
tasks.push(newTask);
return HttpResponse.json(newTask, { status: 201 });
}),
// PATCH /api/tasks/:id/complete
http.patch('/api/tasks/:id/complete', ({ params }) => {
const task = tasks.find((t) => t.id === params.id);
if (!task) {
return new HttpResponse(null, { status: 404 });
}
task.completed = true;
task.updatedAt = new Date().toISOString();
return HttpResponse.json(task);
}),
// DELETE /api/tasks/:id
http.delete('/api/tasks/:id', ({ params }) => {
tasks = tasks.filter((t) => t.id !== params.id);
return new HttpResponse(null, { status: 204 });
}),
];
在 src/mocks/handlers/index.ts 中註冊:
import { authHandlers } from './auth.handlers';
import { taskHandlers } from './task.handlers';
export const handlers = [
...authHandlers,
...taskHandlers,
];
步驟 4:建立 Applet 組件
在 src/applets/tasks/ 建立 TaskListApplet.tsx:
import { useState, useEffect } from 'react';
import { AppletShell, DataTable, Button, TextField, Dialog } from '@appfuse/appfuse-web';
import { taskService } from '@/services/task.service';
import type { Task } from '@/types/task.types';
export function TaskListApplet() {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [newTask, setNewTask] = useState({ title: '', description: '' });
// 載入任務列表
useEffect(() => {
loadTasks();
}, []);
const loadTasks = async () => {
setLoading(true);
try {
const data = await taskService.findAll();
setTasks(data);
} catch (error) {
console.error('Failed to load tasks:', error);
} finally {
setLoading(false);
}
};
// 建立新任務
const handleCreate = async () => {
try {
await taskService.create(newTask);
setShowCreateDialog(false);
setNewTask({ title: '', description: '' });
loadTasks();
} catch (error) {
console.error('Failed to create task:', error);
}
};
// 標記為完成
const handleComplete = async (id: string) => {
try {
await taskService.markAsCompleted(id);
loadTasks();
} catch (error) {
console.error('Failed to complete task:', error);
}
};
// 刪除任務
const handleDelete = async (id: string) => {
try {
await taskService.delete(id);
loadTasks();
} catch (error) {
console.error('Failed to delete task:', error);
}
};
return (
<AppletShell
title="任務管理"
description="管理你的任務清單"
actions={
<Button
variant="primary"
onClick={() => setShowCreateDialog(true)}
>
新增任務
</Button>
}
>
<DataTable
data={tasks}
loading={loading}
columns={[
{
key: 'title',
header: '標題',
},
{
key: 'description',
header: '描述',
},
{
key: 'completed',
header: '狀態',
render: (task) => (
<span className={task.completed ? 'text-green-600' : 'text-gray-400'}>
{task.completed ? '已完成' : '進行中'}
</span>
),
},
{
key: 'actions',
header: '操作',
render: (task) => (
<div className="flex gap-2">
{!task.completed && (
<Button
size="sm"
variant="secondary"
onClick={() => handleComplete(task.id)}
>
完成
</Button>
)}
<Button
size="sm"
variant="danger"
onClick={() => handleDelete(task.id)}
>
刪除
</Button>
</div>
),
},
]}
/>
{/* 新增任務對話框 */}
<Dialog
open={showCreateDialog}
onClose={() => setShowCreateDialog(false)}
title="新增任務"
>
<div className="space-y-4">
<TextField
label="標題"
value={newTask.title}
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
required
/>
<TextField
label="描述"
value={newTask.description}
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
multiline
rows={3}
/>
<div className="flex justify-end gap-2">
<Button
variant="secondary"
onClick={() => setShowCreateDialog(false)}
>
取消
</Button>
<Button
variant="primary"
onClick={handleCreate}
disabled={!newTask.title}
>
建立
</Button>
</div>
</div>
</Dialog>
</AppletShell>
);
}
步驟 5:註冊路由
在 src/routes/AppRoutes.tsx 中添加路由:
import { Routes, Route } from 'react-router-dom';
import { TaskListApplet } from '@/applets/tasks/TaskListApplet';
export function AppRoutes() {
return (
<Routes>
{/* 其他路由 */}
<Route path="/tasks" element={<TaskListApplet />} />
</Routes>
);
}
步驟 6:註冊到 Application Launcher
在應用程式啟動器中註冊 Applet,讓使用者可以從主選單存取:
// src/config/applets.config.ts
export const appletRegistry = [
{
id: 'tasks',
name: '任務管理',
icon: 'CheckSquare',
path: '/tasks',
description: '管理你的任務清單',
},
// 其他 Applets...
];
步驟 7:測試 Applet
啟動開發伺服器:
npm run dev
訪問 http://localhost:5173/tasks 查看你的 Applet。
進階功能
使用 React Hook Form 處理表單
import { useForm } from 'react-hook-form';
import { Input } from '@appfuse/appfuse-web/form';
import { schema, validatorResolver } from '@appfuse/appfuse-web/form/validator';
const taskSchema = schema.object({
title: schema.string().required(),
description: schema.string(),
});
function TaskForm() {
const { control, handleSubmit } = useForm({
resolver: validatorResolver(taskSchema),
defaultValues: {
title: '',
description: '',
},
});
const onSubmit = async (data) => {
await taskService.create(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input name="title" control={control} label="Title" />
<Input name="description" control={control} label="Description" />
<button type="submit" className="btn btn-primary">Save</button>
</form>
);
}
使用 RTK Query 管理伺服器狀態
參考 狀態管理指南 了解如何使用 RTK Query。
專案結構總覽
完成後的專案結構:
src/
├── applets/tasks/
│ └── TaskListApplet.tsx
├── services/
│ └── task.service.ts
├── mocks/handlers/
│ └── task.handlers.ts
├── types/
│ └── task.types.ts
└── routes/
└── AppRoutes.tsx