svelte

📁 fil512/upship 📅 Jan 25, 2026
4
总安装量
3
周安装量
#50772
全站排名
安装命令
npx skills add https://github.com/fil512/upship --skill svelte

Agent 安装分布

opencode 2
codex 2
claude-code 2
windsurf 1
continue 1

Skill 文档

Svelte Skill

Overview

This skill provides expertise for building reactive web applications with Svelte. It covers component architecture, the reactivity system, stores for state management, real-time updates with WebSockets, and SvelteKit for full-stack applications.

Why Svelte

Comparison with Vanilla JS

Aspect Vanilla JS Svelte
Reactivity Manual DOM updates Automatic – count++ just works
Components Template strings Single-file components
State Global variables Stores with subscriptions
Bundle size 0kb (but more code) ~2kb runtime
Learning curve None Gentle (closest to vanilla)

Key Benefits

  1. Compile-time magic – No virtual DOM, compiles to efficient vanilla JS
  2. Less boilerplatelet count = 0 is reactive by default
  3. Built-in transitionstransition:fade for animations
  4. Scoped CSS – Styles in components don’t leak
  5. Stores – Simple reactive state that works with WebSockets

Core Concepts

Reactivity

Svelte’s reactivity is based on assignments:

<script>
  let count = 0;

  // Reactive statements run when dependencies change
  $: doubled = count * 2;
  $: console.log('count changed to', count);

  function increment() {
    count++;  // This triggers UI update automatically
  }
</script>

<button on:click={increment}>
  Count: {count} (doubled: {doubled})
</button>

Array/Object Reactivity

Svelte tracks assignments, not mutations:

<script>
  let items = ['a', 'b', 'c'];

  // BAD: mutation doesn't trigger update
  function addBad() {
    items.push('d');  // UI won't update!
  }

  // GOOD: reassignment triggers update
  function addGood() {
    items = [...items, 'd'];  // UI updates
  }

  // Also works: assign back to self
  function addAlso() {
    items.push('d');
    items = items;  // Triggers update
  }
</script>

Component Structure

Single-file components with script, markup, and style:

<!-- PlayerCard.svelte -->
<script>
  // Props with defaults
  export let name;
  export let cash = 0;
  export let isActive = false;

  // Local state
  let expanded = false;

  // Event dispatcher
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  function handleClick() {
    dispatch('select', { name });
  }
</script>

<div class="player-card" class:active={isActive} on:click={handleClick}>
  <h3>{name}</h3>
  <p>Cash: £{cash}</p>

  {#if expanded}
    <slot />  <!-- Nested content goes here -->
  {/if}
</div>

<style>
  /* Scoped to this component only */
  .player-card {
    padding: 1rem;
    border: 2px solid #333;
    border-radius: 8px;
  }

  .player-card.active {
    border-color: #4a9eff;
    background: rgba(74, 158, 255, 0.1);
  }
</style>

Using Components

<!-- Game.svelte -->
<script>
  import PlayerCard from './PlayerCard.svelte';

  let players = [
    { id: 1, name: 'Germany', cash: 15 },
    { id: 2, name: 'Britain', cash: 12 }
  ];
  let activePlayerId = 1;

  function handleSelect(event) {
    console.log('Selected:', event.detail.name);
  }
</script>

{#each players as player (player.id)}
  <PlayerCard
    name={player.name}
    cash={player.cash}
    isActive={player.id === activePlayerId}
    on:select={handleSelect}
  >
    <p>Ships: {player.ships?.length ?? 0}</p>
  </PlayerCard>
{/each}

Stores

Writable Stores

For shared state across components:

// stores/gameState.js
import { writable, derived } from 'svelte/store';

// Create a writable store
export const gameState = writable(null);

// Derived stores compute from other stores
export const currentPlayer = derived(
  gameState,
  $state => $state?.players?.[$state?.currentPlayerIndex]
);

export const isMyTurn = derived(
  [gameState, currentPlayer],
  ([$state, $player]) => $player?.id === myPlayerId
);

// Helper functions to update state
export function updateGameState(newState) {
  gameState.set(newState);
}

export function updatePlayer(playerId, changes) {
  gameState.update(state => ({
    ...state,
    players: {
      ...state.players,
      [playerId]: { ...state.players[playerId], ...changes }
    }
  }));
}

Using Stores in Components

<script>
  import { gameState, currentPlayer, isMyTurn } from './stores/gameState.js';

  // $ prefix auto-subscribes to store
  $: console.log('Game state updated:', $gameState);
</script>

<div>
  <h2>Turn: {$gameState?.turn}</h2>
  <p>Current player: {$currentPlayer?.name}</p>

  {#if $isMyTurn}
    <button>Take Action</button>
  {:else}
    <p>Waiting for {$currentPlayer?.name}...</p>
  {/if}
</div>

Custom Stores

Create stores with custom methods:

// stores/player.js
import { writable } from 'svelte/store';

function createPlayerStore() {
  const { subscribe, set, update } = writable({
    cash: 0,
    officers: 0,
    engineers: 0,
    gasCubes: { hydrogen: 0, helium: 0 }
  });

  return {
    subscribe,
    set,
    reset: () => set({ cash: 0, officers: 0, engineers: 0, gasCubes: { hydrogen: 0, helium: 0 } }),
    addCash: (amount) => update(p => ({ ...p, cash: p.cash + amount })),
    spendCash: (amount) => update(p => ({ ...p, cash: p.cash - amount })),
    buyGas: (type, amount) => update(p => ({
      ...p,
      gasCubes: { ...p.gasCubes, [type]: p.gasCubes[type] + amount }
    }))
  };
}

export const player = createPlayerStore();

Real-Time Updates with WebSocket

Socket Store Pattern

// stores/socket.js
import { writable, get } from 'svelte/store';
import { io } from 'socket.io-client';
import { gameState } from './gameState.js';

export const connected = writable(false);
export const connectionError = writable(null);

let socket = null;

export function connect(serverUrl) {
  socket = io(serverUrl, {
    reconnection: true,
    reconnectionAttempts: 10,
    reconnectionDelay: 1000
  });

  socket.on('connect', () => {
    connected.set(true);
    connectionError.set(null);
    console.log('Connected to server');
  });

  socket.on('disconnect', () => {
    connected.set(false);
  });

  socket.on('connect_error', (error) => {
    connectionError.set(error.message);
  });

  // Game state updates from server
  socket.on('state-update', (newState) => {
    gameState.set(newState);
  });

  socket.on('state-sync', (fullState) => {
    gameState.set(fullState);
  });

  return socket;
}

export function joinGame(gameId, playerId) {
  if (socket) {
    socket.emit('join-game', { gameId, playerId });
  }
}

export function sendAction(action) {
  if (socket) {
    socket.emit('game-action', action);
  }
}

export function disconnect() {
  if (socket) {
    socket.disconnect();
    socket = null;
    connected.set(false);
  }
}

Using Socket in Components

<!-- Game.svelte -->
<script>
  import { onMount, onDestroy } from 'svelte';
  import { connect, joinGame, sendAction, disconnect, connected } from './stores/socket.js';
  import { gameState, currentPlayer } from './stores/gameState.js';

  export let gameId;
  export let playerId;

  onMount(() => {
    connect('http://localhost:3000');
    joinGame(gameId, playerId);
  });

  onDestroy(() => {
    disconnect();
  });

  function handleEndTurn() {
    sendAction({ type: 'END_TURN' });
  }
</script>

{#if !$connected}
  <div class="connecting">Connecting to server...</div>
{:else if !$gameState}
  <div class="loading">Loading game state...</div>
{:else}
  <div class="game">
    <h1>Turn {$gameState.turn}</h1>
    <p>Current player: {$currentPlayer?.name}</p>

    <button on:click={handleEndTurn}>End Turn</button>
  </div>
{/if}

Conditional Rendering and Loops

If/Else Blocks

{#if loading}
  <Spinner />
{:else if error}
  <ErrorMessage {error} />
{:else if items.length === 0}
  <EmptyState />
{:else}
  <ItemList {items} />
{/if}

Each Blocks with Keys

<!-- Key is crucial for list updates -->
{#each ships as ship (ship.id)}
  <Ship {...ship} on:launch={handleLaunch} />
{:else}
  <p>No ships in hangar</p>
{/each}

Await Blocks

{#await fetchGameState()}
  <p>Loading...</p>
{:then state}
  <GameBoard {state} />
{:catch error}
  <p>Error: {error.message}</p>
{/await}

Transitions and Animations

Built-in Transitions

<script>
  import { fade, fly, slide, scale } from 'svelte/transition';
  import { flip } from 'svelte/animate';

  let visible = true;
  let items = [];
</script>

{#if visible}
  <div transition:fade={{ duration: 300 }}>
    Fades in and out
  </div>
{/if}

<!-- One-way transitions -->
{#if showNotification}
  <div in:fly={{ y: -50, duration: 300 }} out:fade>
    Notification!
  </div>
{/if}

<!-- Animate list reordering -->
{#each items as item (item.id)}
  <div animate:flip={{ duration: 300 }}>
    {item.name}
  </div>
{/each}

Custom Transitions

<script>
  function whoosh(node, { duration = 400 }) {
    return {
      duration,
      css: (t) => {
        const eased = t;  // Could use easing function
        return `
          transform: scale(${eased}) rotate(${(1 - eased) * 360}deg);
          opacity: ${eased};
        `;
      }
    };
  }
</script>

{#if show}
  <div transition:whoosh>Whoooosh!</div>
{/if}

Event Handling

DOM Events

<button on:click={handleClick}>Click</button>
<button on:click={() => count++}>Inline</button>

<!-- Event modifiers -->
<button on:click|preventDefault={submit}>Submit</button>
<button on:click|stopPropagation={handleClick}>Stop Bubble</button>
<button on:click|once={init}>Initialize Once</button>
<form on:submit|preventDefault={handleSubmit}>...</form>

<!-- Keyboard events -->
<input on:keydown|self={(e) => e.key === 'Enter' && submit()} />

Component Events

<!-- Child component -->
<script>
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  function handleSelect() {
    dispatch('select', { id: item.id, name: item.name });
  }
</script>

<!-- Parent component -->
<Card on:select={(e) => console.log(e.detail.name)} />

<!-- Forward DOM events -->
<button on:click>
  This click bubbles to parent
</button>

Bindings

Two-Way Binding

<script>
  let name = '';
  let agreed = false;
  let selected = 'a';
  let quantity = 1;
</script>

<input bind:value={name} />
<input type="checkbox" bind:checked={agreed} />
<input type="number" bind:value={quantity} min="1" max="10" />

<select bind:value={selected}>
  <option value="a">Option A</option>
  <option value="b">Option B</option>
</select>

<!-- Group binding -->
<script>
  let selectedColors = [];
</script>
{#each ['red', 'green', 'blue'] as color}
  <label>
    <input type="checkbox" bind:group={selectedColors} value={color} />
    {color}
  </label>
{/each}

Element Bindings

<script>
  let inputElement;
  let divWidth;
  let divHeight;
</script>

<input bind:this={inputElement} />
<button on:click={() => inputElement.focus()}>Focus</button>

<div bind:clientWidth={divWidth} bind:clientHeight={divHeight}>
  Size: {divWidth}x{divHeight}
</div>

SvelteKit

Project Structure

my-app/
├── src/
│   ├── lib/           # Shared components and utilities
│   │   ├── components/
│   │   │   ├── PlayerCard.svelte
│   │   │   └── GameBoard.svelte
│   │   ├── stores/
│   │   │   ├── gameState.js
│   │   │   └── socket.js
│   │   └── utils/
│   ├── routes/        # File-based routing
│   │   ├── +page.svelte       # /
│   │   ├── +layout.svelte     # Shared layout
│   │   ├── game/
│   │   │   ├── +page.svelte   # /game
│   │   │   └── [id]/
│   │   │       └── +page.svelte  # /game/:id
│   │   └── api/       # API routes
│   │       └── games/
│   │           └── +server.js
│   ├── app.html
│   └── app.css
├── static/            # Static assets
├── svelte.config.js
└── package.json

Page Load Functions

// routes/game/[id]/+page.js
export async function load({ params, fetch }) {
  const response = await fetch(`/api/games/${params.id}`);

  if (!response.ok) {
    throw error(404, 'Game not found');
  }

  const game = await response.json();

  return {
    game,
    gameId: params.id
  };
}
<!-- routes/game/[id]/+page.svelte -->
<script>
  export let data;  // From load function

  $: ({ game, gameId } = data);
</script>

<h1>Game: {game.name}</h1>

API Routes

// routes/api/games/+server.js
import { json } from '@sveltejs/kit';

export async function GET({ url }) {
  const games = await db.getGames();
  return json(games);
}

export async function POST({ request }) {
  const { name, playerId } = await request.json();
  const game = await db.createGame(name, playerId);
  return json(game, { status: 201 });
}

TypeScript Support

<script lang="ts">
  interface Player {
    id: string;
    name: string;
    cash: number;
    faction: 'germany' | 'britain' | 'usa' | 'italy';
  }

  interface Ship {
    id: string;
    name: string;
    status: 'hangar' | 'on_route' | 'destroyed';
  }

  export let player: Player;
  export let ships: Ship[] = [];

  let selectedShip: Ship | null = null;

  function selectShip(ship: Ship): void {
    selectedShip = ship;
  }
</script>

Migration from Vanilla JS

Before (Vanilla)

// Vanilla JS pattern
let gameState = null;
const stateElement = document.getElementById('game-state');

function render() {
  stateElement.innerHTML = `
    <h2>Turn ${gameState.turn}</h2>
    <p>Cash: £${gameState.players[userId].cash}</p>
    ${gameState.players[userId].ships.map(ship => `
      <div class="ship">${ship.name}</div>
    `).join('')}
  `;
}

async function fetchState() {
  const res = await fetch(`/api/state/${gameId}`);
  gameState = await res.json();
  render();
}

// Poll every 2 seconds
setInterval(fetchState, 2000);

After (Svelte)

<script>
  import { onMount } from 'svelte';
  import { gameState } from './stores/gameState.js';
  import { connect, joinGame } from './stores/socket.js';

  export let gameId;
  export let userId;

  $: player = $gameState?.players?.[userId];

  onMount(() => {
    connect('http://localhost:3000');
    joinGame(gameId, userId);
  });
</script>

{#if $gameState}
  <h2>Turn {$gameState.turn}</h2>
  <p>Cash: £{player.cash}</p>

  {#each player.ships as ship (ship.id)}
    <div class="ship">{ship.name}</div>
  {/each}
{:else}
  <p>Loading...</p>
{/if}

Best Practices

Component Organization

lib/components/
├── ui/               # Generic reusable components
│   ├── Button.svelte
│   ├── Modal.svelte
│   └── Tooltip.svelte
├── game/             # Game-specific components
│   ├── GameBoard.svelte
│   ├── PlayerPanel.svelte
│   └── ShipCard.svelte
└── layout/           # Layout components
    ├── Header.svelte
    └── Sidebar.svelte

Props and Events Naming

<script>
  // Props: noun or adjective
  export let player;
  export let isActive = false;
  export let maxItems = 10;

  // Events: on:verbNoun pattern
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  // dispatch('select'), dispatch('launch'), dispatch('close')
</script>

<!-- Usage follows same pattern -->
<ShipCard
  ship={myShip}
  isSelected={selectedId === myShip.id}
  on:launch={handleLaunch}
  on:select={handleSelect}
/>

Reactive Statement Order

<script>
  export let items;
  export let filter;

  // Derived values first (these update when deps change)
  $: filteredItems = items.filter(i => i.type === filter);
  $: totalCount = filteredItems.length;

  // Side effects last (log, dispatch events, etc.)
  $: if (totalCount === 0) {
    console.log('No items match filter');
  }
</script>

Avoiding Common Mistakes

<script>
  // MISTAKE 1: Mutating without reassignment
  let items = [1, 2, 3];
  items.push(4);  // Won't trigger update!
  items = [...items, 4];  // Correct

  // MISTAKE 2: Destructuring props loses reactivity
  export let player;
  const { name } = player;  // name won't update!
  $: ({ name } = player);   // Reactive destructure

  // MISTAKE 3: Not using key in each
  {#each items as item}  // Bad for updates
  {#each items as item (item.id)}  // Good

  // MISTAKE 4: Store in template without $
  import { count } from './stores';
  // {count} shows store object, not value
  // {$count} shows the value
</script>

Testing Svelte Components

// PlayerCard.test.js
import { render, fireEvent } from '@testing-library/svelte';
import PlayerCard from './PlayerCard.svelte';

describe('PlayerCard', () => {
  it('displays player name and cash', () => {
    const { getByText } = render(PlayerCard, {
      props: { name: 'Germany', cash: 15 }
    });

    expect(getByText('Germany')).toBeInTheDocument();
    expect(getByText('Cash: £15')).toBeInTheDocument();
  });

  it('dispatches select event on click', async () => {
    const { getByRole, component } = render(PlayerCard, {
      props: { name: 'Germany', cash: 15 }
    });

    const selectHandler = vi.fn();
    component.$on('select', selectHandler);

    await fireEvent.click(getByRole('button'));

    expect(selectHandler).toHaveBeenCalledWith(
      expect.objectContaining({
        detail: { name: 'Germany' }
      })
    );
  });
});

When This Skill Activates

Use this skill when:

  • Building Svelte components
  • Managing state with Svelte stores
  • Implementing real-time updates via WebSocket
  • Migrating vanilla JS to Svelte
  • Setting up SvelteKit projects
  • Adding TypeScript to Svelte
  • Creating reactive UI patterns
  • Optimizing Svelte performance