بنقرة واحدة
svelte
Svelte 5 + Astro 整合最佳實踐指南。當需要建立 Svelte 元件、使用 runes API、整合 Astro islands、或用 Testing Library 測試 Svelte 元件時使用。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Svelte 5 + Astro 整合最佳實踐指南。當需要建立 Svelte 元件、使用 runes API、整合 Astro islands、或用 Testing Library 測試 Svelte 元件時使用。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Zod v4 schema validation 最佳實踐指南。當需要定義 schema、驗證/解析 JSON 資料、type inference、或處理 unknown data 時使用。
GitHub GraphQL API 最佳實踐指南。當需要使用 GraphQL 查詢使用者資料、處理 cursor pagination、計算 rate limit、或除錯 GraphQL errors 時使用。
gayanvoice/top-github-users 架構參考指南。當需要了解 GitHub 使用者排行榜的資料抓取管線、國家設定、排行計算邏輯、已知問題、或社群需求時使用。
Commander.js v14 CLI 框架最佳實踐。當需要建立 CLI 工具、解析命令列參數、設計 subcommands 時使用。
GitHub Actions CI/CD 最佳實踐指南。當需要設定 workflow、cron 排程、GitHub Pages 部署、使用 Octokit API、或處理 rate limiting 時使用。
Globe.GL 3D 地球視覺化指南。當需要建立互動式 3D 地球、國家熱力圖、點擊導航、或整合 Astro 時使用。
| name | svelte |
| description | Svelte 5 + Astro 整合最佳實踐指南。當需要建立 Svelte 元件、使用 runes API、整合 Astro islands、或用 Testing Library 測試 Svelte 元件時使用。 |
<script>
let count = $state(0);
let items = $state([{ text: "hello" }]); // deep reactive proxy
</script>
<button onclick={() => count++}>{count}</button>
$state.raw() — 無深層 proxy,需整個重新賦值:
let data = $state.raw({ big: "object" });
data = { big: "new" }; // 觸發更新
$state.snapshot() — 提取 plain object(給外部 lib 用)。
let doubled = $derived(count * 2);
let total = $derived.by(() => items.reduce((s, i) => s + i.value, 0));
$effect(() => { console.log(count); }); // 自動追蹤依賴
$effect.pre(() => { /* beforeUpdate */ });
<script lang="ts">
let { name, age = 25, onchange }: {
name: string;
age?: number;
onchange?: (val: string) => void;
} = $props();
</script>
不用 on: — 直接用 HTML 事件屬性:
<button onclick={handler}>Click</button>
<button onclick={(e) => { e.preventDefault(); handler(e); }}>Click</button>
元件事件 = callback props:
<!-- Parent -->
<Child onmessage={(msg) => console.log(msg)} />
<!-- Child -->
<script> let { onmessage } = $props(); </script>
<button onclick={() => onmessage?.("hello")}>Send</button>
<!-- Parent -->
<Card>
{#snippet header()}<h2>Title</h2>{/snippet}
<p>Body (children)</p>
</Card>
<!-- Card.svelte -->
<script> let { header, children } = $props(); </script>
{@render header?.()}
{@render children()}
// astro.config.mjs
import svelte from "@astrojs/svelte";
export default defineConfig({ integrations: [svelte()] });
<Counter client:load /> <!-- 立即 hydrate -->
<Chart client:visible /> <!-- 進入視窗才 hydrate -->
<Widget client:idle /> <!-- 瀏覽器 idle 時 hydrate -->
<App client:only="svelte" /> <!-- 純 client-side -->
不加 client:* = 靜態 HTML,零 JS。
<Chart client:visible items={data} title="Sales" />
只接受可序列化型別。不能傳 function。
npm install nanostores @nanostores/svelte
// stores/cart.ts
import { atom } from "nanostores";
export const isOpen = atom(false);
<script>
import { isOpen } from "../stores/cart";
</script>
<button onclick={() => isOpen.set(true)}>Open ({$isOpen})</button>
npm install -D @testing-library/svelte @testing-library/jest-dom @testing-library/user-event
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { svelteTesting } from "@testing-library/svelte/vite";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [svelte(), svelteTesting()],
test: {
environment: "jsdom",
setupFiles: ["./vitest-setup.ts"],
},
});
svelteTesting() 自動做兩件事:browser resolve condition + afterEach cleanup。可個別關閉:
svelteTesting({ autoCleanup: false, resolveBrowser: false })
import "@testing-library/jest-dom/vitest";
tsconfig.json 加上:
{ "compilerOptions": { "types": ["@testing-library/jest-dom"] } }
import { render, screen } from "@testing-library/svelte";
import userEvent from "@testing-library/user-event";
import Counter from "./Counter.svelte";
test("increments on click", async () => {
const user = userEvent.setup();
render(Counter, { initial: 5 });
expect(screen.getByRole("button")).toHaveTextContent("5");
await user.click(screen.getByRole("button"));
expect(screen.getByRole("button")).toHaveTextContent("6");
});
props 可直接攤平傳入,也可用 { props: { ... } } 形式:
render(MyComponent, { name: "Alice" });
// 等同
render(MyComponent, { props: { name: "Alice" } });
test("reacts to prop changes", async () => {
const { rerender } = render(Greeting, { name: "Alice" });
expect(screen.getByText("Hello Alice")).toBeInTheDocument();
await rerender({ name: "Bob" });
expect(screen.getByText("Hello Bob")).toBeInTheDocument();
});
rerender() 內部用 Object.assign 更新 $state 化的 props,再 await tick()。
元件內的 $state / $derived 不需特殊處理 — 透過 DOM 驗證即可:
<!-- PriceCalc.svelte -->
<script lang="ts">
let { price, taxRate = 0.1 }: { price: number; taxRate?: number } = $props();
let quantity = $state(1);
let total = $derived(price * quantity * (1 + taxRate));
</script>
<input type="number" aria-label="quantity" bind:value={quantity} />
<p data-testid="total">{total}</p>
test("derived total updates when quantity changes", async () => {
const user = userEvent.setup();
render(PriceCalc, { price: 100 });
expect(screen.getByTestId("total")).toHaveTextContent("110"); // 100*1*1.1
const input = screen.getByLabelText("quantity");
await user.clear(input);
await user.type(input, "3");
expect(screen.getByTestId("total")).toHaveTextContent("330"); // 100*3*1.1
});
$effect 中的非同步操作需搭配 waitFor 或 findBy*:
<!-- UserProfile.svelte -->
<script lang="ts">
import { fetchUser } from "./api";
let { userId }: { userId: string } = $props();
let user = $state<{ name: string } | null>(null);
let loading = $state(true);
$effect(() => {
loading = true;
fetchUser(userId).then((u) => {
user = u;
loading = false;
});
});
</script>
{#if loading}<p>Loading...</p>{/if}
{#if user}<p>{user.name}</p>{/if}
import { render, screen, waitFor } from "@testing-library/svelte";
import { vi } from "vitest";
// Mock 模組
vi.mock("./api", () => ({
fetchUser: vi.fn(),
}));
import { fetchUser } from "./api";
import UserProfile from "./UserProfile.svelte";
test("loads and displays user", async () => {
vi.mocked(fetchUser).mockResolvedValue({ name: "Alice" });
render(UserProfile, { userId: "1" });
expect(screen.getByText("Loading...")).toBeInTheDocument();
// findBy* 自帶 waitFor,等待元素出現
expect(await screen.findByText("Alice")).toBeInTheDocument();
});
// Mock 整個模組
vi.mock("./api", () => ({
fetchUsers: vi.fn().mockResolvedValue([]),
}));
// Mock default export(如 Svelte component)
vi.mock("./HeavyChart.svelte", () => ({
default: { /* mock component */ },
}));
// Mock 部分匯出,保留其餘
vi.mock("./utils", async (importOriginal) => {
const actual = await importOriginal<typeof import("./utils")>();
return { ...actual, expensiveCalc: vi.fn().mockReturnValue(42) };
});
每次測試後記得清理:
afterEach(() => {
vi.restoreAllMocks();
});
{#each} 迴圈 + 過濾列表(保留原始排名)常見場景:列表有 filter 但顯示的 rank 需保持原始位置。
<!-- Leaderboard.svelte -->
<script lang="ts">
let { users }: { users: { rank: number; name: string; score: number }[] } = $props();
let filter = $state("");
let filtered = $derived(
users.filter((u) => u.name.toLowerCase().includes(filter.toLowerCase()))
);
</script>
<input aria-label="filter" bind:value={filter} />
<table>
<tbody>
{#each filtered as user (user.rank)}
<tr><td>{user.rank}</td><td>{user.name}</td><td>{user.score}</td></tr>
{/each}
</tbody>
</table>
test("filtering preserves original rank", async () => {
const user = userEvent.setup();
const users = [
{ rank: 1, name: "Alice", score: 100 },
{ rank: 2, name: "Bob", score: 90 },
{ rank: 3, name: "Alicia", score: 80 },
];
render(Leaderboard, { users });
// 初始 3 列
expect(screen.getAllByRole("row")).toHaveLength(3);
// 輸入 filter
await user.type(screen.getByLabelText("filter"), "Ali");
// 剩 2 列,但 rank 保持原始值
const rows = screen.getAllByRole("row");
expect(rows).toHaveLength(2);
expect(rows[0]).toHaveTextContent("1"); // Alice rank=1
expect(rows[1]).toHaveTextContent("3"); // Alicia rank=3
});
test("select changes category", async () => {
const user = userEvent.setup();
render(FilterPanel);
await user.selectOptions(screen.getByRole("combobox"), "technology");
expect(screen.getByRole("combobox")).toHaveValue("technology");
});
test("calls onsubmit with form data", async () => {
const handleSubmit = vi.fn();
const user = userEvent.setup();
render(LoginForm, { onsubmit: handleSubmit });
await user.type(screen.getByLabelText("Email"), "a@b.com");
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(handleSubmit).toHaveBeenCalledWith(
expect.objectContaining({ email: "a@b.com" })
);
});
import { render } from "@testing-library/svelte";
render(MyComponent, {
props: { name: "test" },
context: new Map([["theme", "dark"]]),
});
不用 Testing Library 時,直接用 Svelte API:
import { mount, unmount, flushSync } from "svelte";
test("low level mount", () => {
const target = document.createElement("div");
document.body.appendChild(target);
const component = mount(Counter, { target, props: { initial: 0 } });
target.querySelector("button")!.click();
flushSync(); // 強制同步更新 DOM
expect(target.querySelector("button")!.textContent).toBe("1");
unmount(component);
target.remove();
});
檔名必須是 .svelte.test.ts:
// counter.svelte.test.ts
import { flushSync } from "svelte";
test("reactive state without component", () => {
let count = $state(0);
let doubled = $derived(count * 2);
expect(doubled).toBe(0);
count = 5;
// $derived 在同步 context 中自動更新
expect(doubled).toBe(10);
});
test("effect runs on state change", () => {
const log: number[] = [];
const cleanup = $effect.root(() => {
let count = $state(0);
$effect(() => {
log.push(count);
});
flushSync(); // 觸發 pending effects
expect(log).toEqual([0]);
count = 1;
flushSync();
expect(log).toEqual([0, 1]);
});
cleanup(); // 清除 effect root
});
.svelte(如 .svelte.ts、.svelte.test.ts),否則 $state / $derived 等 runes 不會被編譯svelteTesting() plugin — 自動處理 browser resolve + cleanupmount() 測試需要 flushSync() — Testing Library 的 fireEvent / act 已自動處理$effect 中的非同步操作(await / setTimeout 之後)不會被追蹤依賴 — 用 waitFor 或 findBy* 等待 DOM 更新.svelte.ts 模組的狀態變更需要 flushSync() 才能觸發 DOM 更新,Testing Library 的 auto-retry 只對元件內部 reactivity 有效flushSync() 不能在 effect 執行期間呼叫 — 只能在 state change 後、effect 外部使用{ default: ... } 中screen.getByRole / getByLabelText 等語意查詢,避免用 container querySelector(無 auto-retry 且容易因結構變動而壞掉)