Aller au contenu

Feuilles chauffantes

Svelte - Applications Web améliorées cybernétiques

Svelte est une nouvelle approche radicale pour construire des interfaces utilisateur. Alors que les cadres traditionnels comme React et Vue font la majeure partie de leur travail dans le navigateur, Svelte déplace ce travail dans une étape de compilation qui se produit lorsque vous construisez votre application.

Copier toutes les commandes Générer PDF

Sommaire

  • [Installation] (#installation)
  • [Création de projets] (#project-creation)
  • [Structure du projet] (#project-structure)
  • [Component Syntaxe] (#component-syntax)
  • [Réactivité] (#reactivity)
  • [Props] (#props)
  • [Logic et Templating] (#logic-and-templating)
  • [Événements] (#events)
  • [Bindings] (#bindings)
  • [Fonctions du cycle de vie] (#lifecycle-functions)
  • [Stores] (#stores)
  • [Transitions et animations] (#transitions-and-animations)
  • [SvelteKit] (#sveltekit)
  • [Déploiement] (#deployment)
  • [Essais] (#testing)
  • [Meilleures pratiques] (#best-practices)

Installation

Préalables

# Node.js version requirements
node --version  # Should be 16 or later

# Check npm version
npm --version

# Update npm if needed
npm install -g npm@latest

Créer un projet Svelte

# Create new SvelteKit project (recommended)
npm create svelte@latest my-app

# Create a basic Svelte project (without SvelteKit)
npm create vite@latest my-app -- --template svelte

# Navigate to project
cd my-app

# Install dependencies
npm install

# Start development server
npm run dev
```_

### Installation manuelle (Vite)
```bash
# Create project directory
mkdir my-svelte-project
cd my-svelte-project

# Initialize package.json
npm init -y

# Install Vite and Svelte plugin
npm install --save-dev vite @sveltejs/vite-plugin-svelte

# Install Svelte
npm install svelte

# Create vite.config.js
// vite.config.js
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte()]
});
```_

### Paquet.json Scripts
```json
{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "vite preview",
    "check": "svelte-check --tsconfig ./tsconfig.json"
  }
}

Création de projets

Configuration du projet SvelteKit

# Interactive setup
npm create svelte@latest my-app

# Select options:
# - Skeleton project (basic setup)
# - Demo app (with examples)
# - Library project

# Add TypeScript
# Add ESLint
# Add Prettier
# Add Playwright for browser testing
# Add Vitest for unit testing

Commandes de développement

# Start development server
npm run dev

# Start with specific port
npm run dev -- --port 3000

# Start with specific host
npm run dev -- --host 0.0.0.0

# Start with open browser
npm run dev -- --open

Construire des commandes

# Build for production
npm run build

# Preview production build
npm run preview

# Check for issues
npm run check

Structure du projet

Structure du projet SvelteKit

my-sveltekit-app/
├── src/
   ├── lib/                # Library code (components, utils)
      ├── components/     # Reusable components
      └── utils/          # Utility functions
   ├── routes/             # File-based routing
      ├── +page.svelte    # Homepage
      ├── about/          # /about route
         └── +page.svelte
      └── blog/
          ├── +page.svelte  # /blog index
          └── [slug]/
              └── +page.svelte
   ├── app.html            # HTML template
   └── app.d.ts            # TypeScript declarations
├── static/               # Static assets
├── tests/                # Test files
├── package.json
├── svelte.config.js      # SvelteKit configuration
└── tsconfig.json

Structure du projet

my-vite-svelte-app/
├── public/               # Static assets
├── src/
   ├── assets/           # Assets
   ├── lib/              # Svelte components
   └── main.js           # App entry point
├── index.html            # HTML template
├── package.json
└── vite.config.js

Composante Syntaxe

Composante de base

<!-- src/lib/components/Greeting.svelte -->
<script>
  // Component logic
  let name = 'World';
</script>

<main>
  <h1>Hello, {name}!</h1>
  <p>This is a Svelte component.</p>
</main>

<style>
  /* Scoped styles */
  main {
    text-align: center;
    padding: 2rem;
  }

  h1 {
    color: #ff3e00;
    font-size: 2.5rem;
  }

  p {
    color: #333;
  }
</style>

Composant avec Props

<!-- src/lib/components/Welcome.svelte -->
<script>
  export let name;
  export let message = 'Welcome to our app!';
</script>

<div>
  <h2>Hello, {name}!</h2>
  <p>{message}</p>
</div>

<!-- Usage -->
<!-- <Welcome name="John" /> -->
<!-- <Welcome name="Jane" message="Glad to have you here!" /> -->

Composante avec logique

<!-- src/lib/components/Counter.svelte -->
<script>
  let count = 0;

  function increment() {
    count += 1;
  }

  function decrement() {
    count -= 1;
  }

  function reset() {
    count = 0;
  }
</script>

<div class="counter">
  <h3>Counter</h3>
  <p>Count: {count}</p>

  <div class="buttons">
    <button on:click={decrement}>-</button>
    <button on:click={increment}>+</button>
    <button on:click={reset}>Reset</button>
  </div>
</div>

<style>
  .counter {
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    padding: 1rem;
    margin: 1rem 0;
    text-align: center;
  }

  .buttons {
    display: flex;
    gap: 0.5rem;
    justify-content: center;
    margin-top: 1rem;
  }
</style>

Réactivité

Attributions réactives

<script>
  let count = 0;

  function handleClick() {
    // Svelte automatically detects this assignment is reactive
    count += 1;
  }
</script>

<button on:click={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

Déclarations réactives

<script>
  let count = 0;

  // This will be re-evaluated whenever `count` changes
  $: doubled = count * 2;
  $: quadrupled = doubled * 2;

  function handleClick() {
    count += 1;
  }
</script>

<p>{count} * 2 = {doubled}</p>
<p>{doubled} * 2 = {quadrupled}</p>

<button on:click={handleClick}>Increment</button>

Déclarations réactives

<script>
  let count = 0;

  // This block of code runs whenever `count` changes
  $: {
    console.log(`The count is ${count}`);
    if (count >= 10) {
      alert('Count is 10 or more!');
    }
  }

  function handleClick() {
    count += 1;
  }
</script>

<button on:click={handleClick}>Count: {count}</button>

Mise à jour des tableaux et des objets

<script>
  let numbers = [1, 2, 3];
  let user = { name: 'John', age: 30 };

  function addNumber() {
    // To trigger reactivity, you must assign to the variable
    numbers = [...numbers, numbers.length + 1];
  }

  function changeName() {
    user.name = 'Jane';
    // This won't trigger reactivity on its own

    // To make it reactive, reassign the object
    user = user;
  }
</script>

<p>Numbers: {numbers.join(', ')}</p>
<button on:click={addNumber}>Add Number</button>

<p>User: {user.name}, {user.age}</p>
<button on:click={changeName}>Change Name</button>

Props

Déclarer les produits

<script>
  // Declare props with `export let`
  export let name;
  export let age = 25; // Default value
  export let tags = [];
  export let user = { id: 1, name: 'Guest' };

  // Renaming props
  export let title as heading;
</script>

<h2>{heading}</h2>
<p>Welcome, {name} ({age})</p>

{#if tags.length > 0}
  <p>Tags: {tags.join(', ')}</p>
{/if}

Passage des accessoires

<!-- ParentComponent.svelte -->
<script>
  import UserProfile from './UserProfile.svelte';

  let user = {
    name: 'John Doe',
    age: 30,
    tags: ['developer', 'svelte']
  };
</script>

<UserProfile 
  name={user.name}
  age={user.age}
  tags={user.tags}
  title="User Profile"
/>

<!-- Shorthand for passing props -->
<UserProfile {...user} title="User Profile" />

Type de vérification avec TypeScript

<script lang="ts">
  export let name: string;
  export let age: number;
  export let active: boolean = false;
  export let onSave: (data: any) => void;
</script>

<p>{name} is {age} years old.</p>
<button on:click={() => onSave({ name, age })}>
  Save
</button>

Logique et Templatation

Rendu conditionnel

<script>
  let loggedIn = false;
  let user = { name: 'John' };
</script>

{#if loggedIn}
  <p>Welcome back, {user.name}!</p>
{:else}
  <p>Please log in.</p>
{/if}

{#if user.role === 'admin'}
  <p>Admin controls</p>
{:else if user.role === 'editor'}
  <p>Editor controls</p>
{:else}
  <p>User controls</p>
{/if}

En boucle

<script>
  let cats = [
    { id: '1', name: 'Whiskers' },
    { id: '2', name: 'Mittens' },
    { id: '3', name: 'Shadow' }
  ];
</script>

<ul>
  {#each cats as cat (cat.id)}
    <li>{cat.name}</li>
  {/each}
</ul>

<!-- With index -->
<ul>
  {#each cats as cat, i}
    <li>{i + 1}: {cat.name}</li>
  {/each}
</ul>

<!-- With else block -->
{#each cats as cat}
  <p>{cat.name}</p>
{:else}
  <p>No cats found.</p>
{/each}

Attendez les blocs

<script>
  async function getPosts() {
    const res = await fetch('https://api.example.com/posts');
    if (!res.ok) throw new Error('Failed to fetch posts');
    return await res.json();
  }

  let promise = getPosts();
</script>

{#await promise}
  <p>Loading posts...</p>
{:then posts}
  <ul>
    {#each posts as post}
      <li>{post.title}</li>
    {/each}
  </ul>
{:catch error}
  <p>Error: {error.message}</p>
{/await}

HTML Rendu

<script>
  let htmlContent = '<h1>This is raw HTML</h1><p>Use with caution.</p>';
</script>

<!-- Render raw HTML -->
{@html htmlContent}

<!-- Debugging -->
{@debug count, user}

Événements

DOM Événements

<script>
  function handleClick() {
    alert('Button clicked!');
  }

  function handleInput(event) {
    console.log('Input value:', event.target.value);
  }
</script>

<button on:click={handleClick}>Click me</button>
<input on:input={handleInput} placeholder="Type something" />

<!-- Inline handler -->
<button on:click={() => console.log('Clicked!')}>Inline</button>

Modificateurs d'événements

<!-- Prevent default action -->
<form on:submit|preventDefault={handleSubmit}></form>

<!-- Stop event propagation -->
<div on:click={handleOuter}>
  <button on:click|stopPropagation={handleInner}>Inner</button>
</div>

<!-- Run handler only once -->
<button on:click|once={doSomething}>Once</button>

<!-- Run handler if event is not passive -->
<div on:wheel|passive={handleWheel}></div>

<!-- Run handler during capture phase -->
<div on:click|capture={handleCapture}></div>

<!-- Self modifier -->
<div on:click|self={handleSelf}></div>

Événements liés aux composantes

<!-- InnerComponent.svelte -->
<script>
  import { createEventDispatcher } from 'svelte';

  const dispatch = createEventDispatcher();

  function notify() {
    dispatch('message', {
      text: 'Hello from the inner component!'
    });
  }
</script>

<button on:click={notify}>Notify Parent</button>

<!-- ParentComponent.svelte -->
<script>
  import InnerComponent from './InnerComponent.svelte';

  function handleMessage(event) {
    alert(event.detail.text);
  }
</script>

<InnerComponent on:message={handleMessage} />

Transmission des événements

<!-- CustomButton.svelte -->
<button on:click>
  <slot />
</button>

<!-- ParentComponent.svelte -->
<script>
  import CustomButton from './CustomButton.svelte';

  function handleClick() {
    console.log('Custom button clicked!');
  }
</script>

<CustomButton on:click={handleClick}>Click me</CustomButton>

Reliures

Reliure de valeur

<script>
  let name = 'John';
  let message = 'Hello';
  let selected = 'option2';
  let checked = true;
  let value = 5;
</script>

<!-- Text input -->
<input bind:value={name} />

<!-- Textarea -->
<textarea bind:value={message}></textarea>

<!-- Select -->
<select bind:value={selected}>
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
</select>

<!-- Checkbox -->
<input type="checkbox" bind:checked={checked} />

<!-- Range slider -->
<input type="range" bind:value min="0" max="10" />

Reliures de groupe

<script>
  let flavors = ['Vanilla', 'Chocolate', 'Strawberry'];
  let selectedFlavors = ['Vanilla'];

  let scoop = 'one';
</script>

<!-- Checkbox group -->
{#each flavors as flavor}
  <label>
    <input type="checkbox" bind:group={selectedFlavors} value={flavor} />
    {flavor}
  </label>
{/each}

<!-- Radio group -->
<label>
  <input type="radio" bind:group={scoop} value="one" /> One scoop
</label>
<label>
  <input type="radio" bind:group={scoop} value="two" /> Two scoops
</label>

Reliure des composants

<!-- InnerComponent.svelte -->
<script>
  export let value;
</script>

<input bind:value />

<!-- ParentComponent.svelte -->
<script>
  import InnerComponent from './InnerComponent.svelte';
  let text = 'Initial value';
</script>

<InnerComponent bind:value={text} />
<p>Value: {text}</p>

Autres liaisons

<!-- bind:this -->
<script>
  let myCanvas;

  onMount(() => {
    const ctx = myCanvas.getContext('2d');
    // Draw on canvas
  });
</script>

<canvas bind:this={myCanvas}></canvas>

<!-- bind:clientWidth, bind:clientHeight -->
<script>
  let width, height;
</script>

<div bind:clientWidth={width} bind:clientHeight={height}>
  Dimensions: {width} x {height}
</div>

Fonctions du cycle de vie

sur le mois

<script>
  import { onMount } from 'svelte';

  onMount(() => {
    // Runs after the component is first rendered to the DOM
    console.log('Component mounted');

    const interval = setInterval(() => {
      console.log('tick');
    }, 1000);

    // Cleanup function
    return () => {
      clearInterval(interval);
      console.log('Component unmounted');
    };
  });
</script>

avant et après Mise à jour

<script>
  import { beforeUpdate, afterUpdate } from 'svelte';

  let count = 0;

  beforeUpdate(() => {
    // Runs before the DOM is updated
    console.log('Before update');
  });

  afterUpdate(() => {
    // Runs after the DOM is updated
    console.log('After update');
  });
</script>

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

surDestroy

<script>
  import { onDestroy } from 'svelte';

  onDestroy(() => {
    // Runs when the component is about to be destroyed
    console.log('Component destroyed');
  });
</script>

cochez

<script>
  import { tick } from 'svelte';

  let text = 'Initial';

  async function updateText() {
    text = 'Updated';

    // Wait for the DOM to update
    await tick();

    console.log('DOM updated');
  }
</script>

<p>{text}</p>
<button on:click={updateText}>Update</button>

Magasins

Magasins

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

export const count = writable(0);

export const user = writable({ name: 'Guest', loggedIn: false });
<!-- Component.svelte -->
<script>
  import { count, user } from './stores.js';

  function increment() {
    count.update(n => n + 1);
  }

  function login() {
    user.set({ name: 'John', loggedIn: true });
  }

  // Auto-subscription with $ prefix
  $: console.log(`Count is ${$count}`);
</script>

<h1>Count: {$count}</h1>
<button on:click={increment}>Increment</button>

{#if $user.loggedIn}
  <p>Welcome, {$user.name}!</p>
{:else}
  <button on:click={login}>Login</button>
{/if}

Magasins lisibles

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

export const time = readable(new Date(), (set) => {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  return () => clearInterval(interval);
});
<!-- Clock.svelte -->
<script>
  import { time } from './stores.js';

  $: formattedTime = $time.toLocaleTimeString();
</script>

<p>Current time: {formattedTime}</p>

Magasins dérivés

// stores.js
import { derived } from 'svelte/store';
import { time } from './stores.js';

export const elapsed = derived(
  time,
  $time => Math.round(($time - new Date()) / 1000)
);
<!-- Elapsed.svelte -->
<script>
  import { elapsed } from './stores.js';
</script>

<p>Elapsed time: {$elapsed}s</p>

Magasins personnalisés

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

function createCount() {
  const { subscribe, set, update } = writable(0);

  return {
    subscribe,
    increment: () => update(n => n + 1),
    decrement: () => update(n => n - 1),
    reset: () => set(0)
  };
}

export const count = createCount();

Transitions et Animations

Transitions

<script>
  import { fade, fly, slide, scale } from 'svelte/transition';
  let visible = true;
</script>

<label>
  <input type="checkbox" bind:checked={visible} />
  Toggle visibility
</label>

{#if visible}
  <div transition:fade>Fades in and out</div>
  <div transition:fly={{ y: 200, duration: 2000 }}>Flies in and out</div>
  <div transition:slide>Slides in and out</div>
  <div transition:scale>Scales in and out</div>
{/if}

Animations

<script>
  import { flip } from 'svelte/animate';
  let list = [1, 2, 3];

  function shuffle() {
    list = list.sort(() => Math.random() - 0.5);
  }
</script>

<button on:click={shuffle}>Shuffle</button>

{#each list as item (item)}
  <div animate:flip>{item}</div>
{/each}

Motion

<script>
  import { spring } from 'svelte/motion';

  let coords = spring({ x: 50, y: 50 }, {
    stiffness: 0.1,
    damping: 0.25
  });

  function handleMousemove(event) {
    coords.set({ x: event.clientX, y: event.clientY });
  }
</script>

<div on:mousemove={handleMousemove} class="container">
  <div class="dot" style="transform: translate({$coords.x}px, {$coords.y}px)"></div>
</div>

SvelteKit

Routage

# File-based routing
src/routes/
├── +page.svelte        # Homepage
├── about/+page.svelte    # /about
├── blog/[slug]/+page.svelte # /blog/[slug]
└── +layout.svelte      # Root layout

Chargement des données

<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
  export let data;
  const { post } = data;
</script>

<h1>{post.title}</h1>
<div>{@html post.content}</div>

<!-- src/routes/blog/[slug]/+page.js -->
export async function load({ params }) {
  const res = await fetch(`https://api.example.com/posts/${params.slug}`);
  const post = await res.json();

  return { post };
}

Routes des API

// src/routes/api/posts/+server.js
export async function GET() {
  const posts = await getPostsFromDB();
  return new Response(JSON.stringify(posts));
}

export async function POST({ request }) {
  const data = await request.json();
  const newPost = await createPost(data);
  return new Response(JSON.stringify(newPost), { status: 201 });
}

Mise en page

<!-- src/routes/+layout.svelte -->
<header>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
    <a href="/blog">Blog</a>
  </nav>
</header>

<main>
  <slot />
</main>

<footer>
  <p>&copy; 2024 My App</p>
</footer>

Déploiement

Site statique (statique Adaptateur)

// svelte.config.js
import adapter from '@sveltejs/adapter-static';

export default {
  kit: {
    adapter: adapter({
      pages: 'build',
      assets: 'build',
      fallback: 'index.html'
    })
  }
};

Vercel

// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';

export default {
  kit: {
    adapter: adapter()
  }
};

Netlifier

// svelte.config.js
import adapter from '@sveltejs/adapter-netlify';

export default {
  kit: {
    adapter: adapter()
  }
};

Serveur Node.js

// svelte.config.js
import adapter from '@sveltejs/adapter-node';

export default {
  kit: {
    adapter: adapter()
  }
};

Essais

Vitesse

# Install Vitest
npm install --save-dev vitest @testing-library/svelte
// tests/Counter.test.js
import { render, fireEvent } from '@testing-library/svelte';
import Counter from '../src/lib/components/Counter.svelte';

it('increments counter', async () => {
  const { getByText } = render(Counter);
  const button = getByText('Increment');

  await fireEvent.click(button);

  expect(getByText('Count: 1')).toBeInTheDocument();
});

Dramaturge

# Install Playwright
npm install --save-dev @playwright/test
// tests/e2e/home.spec.js
import { test, expect } from '@playwright/test';

test('homepage has correct title', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveTitle(/My App/);
});

Meilleures pratiques

  • Organisation constituante: Gardez les composants petits et concentrés. Utilisez un répertoire lib/components_ pour les composants réutilisables.
  • Réactivité: Utiliser des instructions réactives ($:) pour calculer les valeurs. Évitez la logique complexe dans les modèles.
  • Stores: Utilisez des magasins pour la gestion globale de l'État. Créer des magasins personnalisés pour une logique complexe.
  • Props: Utilisez des accessoires pour transmettre les données aux composants enfants. Utilisez export let pour déclarer les accessoires.
  • Événements Utiliser les événements composants pour la communication entre l'enfant et le parent. Utilisez createEventDispatcher.
  • ** Cycle de vie** : Utilisez onMount pour récupérer les données et interagir avec le DOM. Utilisez des fonctions de nettoyage pour éviter les fuites de mémoire.
  • SvelteKit: Utilisez SvelteKit pour de nouveaux projets pour obtenir le routage, le chargement des données et le rendu côté serveur hors de la boîte.

Résumé

Svelte est un cadre puissant et efficace pour la construction d'applications web. Son approche de compilation-temps se traduit par des paquets plus petits et des performances plus rapides. Les principales caractéristiques sont les suivantes :

  • Réactivité: Système de réactivité simple et puissant
  • Component-based: Construire des applications avec des composants réutilisables
  • Scoped Styles: CSS est appliqué aux composants par défaut
  • Stores: solution de gestion intégrée de l'état
  • Transitions et animations: Ensemble riche d'outils pour créer des UI fluides
  • SvelteKit: Un cadre complet pour la construction des applications Svelte

En tirant parti des caractéristiques uniques de Svelte, vous pouvez construire des applications web rapides, modernes et durables avec une grande expérience de développeur.