Skip to main content

svelte

Svelte 5 + Astro 整合最佳實踐指南。當需要建立 Svelte 元件、使用 runes API、整合 Astro islands、或用 Testing Library 測試 Svelte 元件時使用。

インストールへ移動

ソース情報

リポジトリ
recca0120/codeatlas
ソースの最終更新活動
2026年3月28日 05:34
検出された SKILL.md の言語
中国語
スター
1
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
svelte
description
Svelte 5 + Astro 整合最佳實踐指南。當需要建立 Svelte 元件、使用 runes API、整合 Astro islands、或用 Testing Library 測試 Svelte 元件時使用。
# Svelte 5 + Astro Guide (v5.53+) ## Runes API ### $state ```svelte <script> let count = $state(0); let items = $state([{ text: "hello" }]); // deep reactive proxy </script> <button onclick={() => count++}>{count}</button> ``` `$state.raw()` — 無深層 proxy,需整個重新賦值: ```js let data = $state.raw({ big: "object" }); data = { big: "new" }; // 觸發更新 ``` `$state.snapshot()` — 提取 plain object(給外部 lib 用)。 ### $derived ```js let doubled = $derived(count * 2); let total = $derived.by(() => items.reduce((s, i) => s + i.value, 0)); ``` ### $effect ```js $effect(() => { console.log(count); }); // 自動追蹤依賴 $effect.pre(() => { /* beforeUpdate */ }); ``` ### $props ```svelte <script lang="ts"> let { name, age = 25, onchange }: { name: string; age?: number; onchange?: (val: string) => void; } = $props(); </script> ``` ## 事件處理(Svelte 5) 不用 `on:` — 直接用 HTML 事件屬性: ```svelte <button onclick={handler}>Click</button> <button onclick={(e) => { e.preventDefault(); handler(e); }}>Click</button> ``` 元件事件 = callback props: ```svelte <!-- Parent --> <Child onmessage={(msg) => console.log(msg)} /> <!-- Child --> <script> let { onmessage } = $props(); </script> <button onclick={() => onmessage?.("hello")}>Send</button> ``` ## Snippets(取代 Slots) ```svelte <!-- 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 整合 ```js // astro.config.mjs import svelte from "@astrojs/svelte"; export default defineConfig({ integrations: [svelte()] }); ``` ```astro <Counter client:load /> <!-- 立即 hydrate --> <Chart client:visible /> <!-- 進入視窗才 hydrate --> <Widget client:idle /> <!-- 瀏覽器 idle 時 hydrate --> <App client:only="svelte" /> <!-- 純 client-side --> ``` 不加 `client:*` = 靜態 HTML,零 JS。 ### 傳資料給 Svelte ```astro <Chart client:visible items={data} title="Sales" /> ``` 只接受可序列化型別。**不能傳 function**。 ### 跨 island 共享狀態(Nanostores) ```bash npm install nanostores @nanostores/svelte ``` ```js // stores/cart.ts import { atom } from "nanostores"; export const isOpen = atom(false); ``` ```svelte <script> import { isOpen } from "../stores/cart"; </script> <button onclick={() => isOpen.set(true)}>Open ({$isOpen})</button> ``` ## 測試(Vitest + Testing Library) ```bash npm install -D @testing-library/svelte @testing-library/jest-dom @testing-library/user-event ``` ### vitest.config.ts ```ts 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。可個別關閉: ```ts svelteTesting({ autoCleanup: false, resolveBrowser: false }) ``` ### vitest-setup.ts ```ts import "@testing-library/jest-dom/vitest"; ``` tsconfig.json 加上: ```json { "compilerOptions": { "types": ["@testing-library/jest-dom"] } } ``` --- ### 基本模式:render + props + user interaction ```ts 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: { ... } }` 形式: ```ts render(MyComponent, { name: "Alice" }); // 等同 render(MyComponent, { props: { name: "Alice" } }); ``` ### 更新 props(rerender) ```ts 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 元件內的 `$state` / `$derived` 不需特殊處理 — 透過 DOM 驗證即可: ```svelte <!-- 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> ``` ```ts 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(async data loading) $effect 中的非同步操作需搭配 `waitFor` 或 `findBy*`: ```svelte <!-- 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} ``` ```ts 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 imports(vi.mock) ```ts // 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) }; }); ``` 每次測試後記得清理: ```ts afterEach(() => { vi.restoreAllMocks(); }); ``` ### 測試 `{#each}` 迴圈 + 過濾列表(保留原始排名) 常見場景:列表有 filter 但顯示的 rank 需保持原始位置。 ```svelte <!-- 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> ``` ```ts 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 }, ];
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る