Chapter 02

组件基础:模板语法与作用域样式

掌握 Svelte 模板语法、作用域 CSS 的编译原理、动态样式绑定与过渡动画

模板语法基础

变量插值 {expression}

Svelte 使用单大括号 {} 在模板中插入 JavaScript 表达式。大括号内可以放任意有效的 JavaScript 表达式——变量、函数调用、三元运算符、模板字符串等。

<script lang="ts">
  let name = $state('World');
  let count = $state(42);
  let user = $state({ name: 'Alice', age: 30 });

  function greet(n: string) {
    return `Hello, ${n}!`;
  }
</script>

<!-- 基础变量插值 -->
<h1>Hello, {name}!</h1>

<!-- 表达式 -->
<p>计算结果:{count * 2 + 1}</p>

<!-- 函数调用 -->
<p>{greet(name)}</p>

<!-- 三元运算符 -->
<p>{count > 0 ? '正数' : '非正数'}</p>

<!-- 对象属性 -->
<p>{user.name} 今年 {user.age} 岁</p>

<!-- 属性中的插值 -->
<img src="/{name.toLowerCase()}.png" alt={name} />
<input placeholder="输入 {name} 的名字" />

HTML 插值 {@html}

默认情况下,Svelte 会对插值内容进行 HTML 转义以防止 XSS 攻击。如果你确定内容是安全的 HTML,可以使用 {@html} 直接渲染 HTML 字符串。

<script lang="ts">
  // 安全的 HTML(来自服务器的 Markdown 渲染结果)
  let htmlContent = $state('<strong>加粗文本</strong> 和 <em>斜体</em>');
</script>

<!-- 普通插值:会显示 HTML 标签字符串 -->
<p>{htmlContent}</p>
<!-- 输出:&lt;strong&gt;加粗文本... -->

<!-- {@html}:渲染为真实 HTML -->
<p>{@html htmlContent}</p>
<!-- 输出:加粗文本斜体 -->
XSS 安全警告

永远不要对用户输入的内容使用 {@html},这会导致跨站脚本(XSS)攻击。只对来自可信来源(如服务端渲染的 Markdown)的 HTML 使用此功能。

CSS 作用域原理

编译后的 hash class

Svelte 最优雅的特性之一是自动 CSS 作用域。你在 <style> 块中写的样式,Svelte 编译器会自动给每条规则加上一个唯一的 hash 属性选择器,同时也给对应的 HTML 元素加上相同的 data 属性,从而实现样式隔离。

<!-- 源代码 -->
<h1>标题</h1>
<p>段落</p>

<style>
  h1 { color: red; }
  p { margin: 1rem; }
</style>
<!-- 编译输出 -->
<h1 class="svelte-abc123">标题</h1>
<p class="svelte-abc123">段落</p>

/* 编译后的 CSS */
h1.svelte-abc123 { color: red; }
p.svelte-abc123 { margin: 1rem; }

:global() — 突破作用域

有时你需要从父组件影响子组件内部的元素样式,可以使用 :global() 跳出作用域限制:

<style>
  /* 全局样式:影响所有 h1,不限组件 */
  :global(h1) {
    font-family: system-ui;
  }

  /* 组合:父组件内(作用域内)的子组件的 .btn */
  .wrapper :global(.btn) {
    border-radius: 4px;
  }

  /* 全局关键帧动画 */
  :global(@keyframes) spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
  }
</style>

动态 class 与 style

class: 指令

Svelte 提供了简洁的 class: 指令来条件性地添加 CSS 类,比字符串拼接更清晰:

<script lang="ts">
  let isActive = $state(false);
  let isDisabled = $state(false);
  let theme = $state('light');
</script>

<!-- class: 指令:当条件为 true 时添加 class -->
<button
  class:active={isActive}
  class:disabled={isDisabled}
  onclick={() => (isActive = !isActive)}
>
  点击切换状态
</button>

<!-- 多个 class 混合 -->
<div class="base-class {theme}" class:active={isActive}>
  内容
</div>

<!-- 使用对象形式(Svelte 5)-->
<div class={{ active: isActive, disabled: isDisabled }}>
  内容
</div>

style: 指令

<script lang="ts">
  let color = $state('#FF3E00');
  let size = $state(16);
</script>

<!-- style: 指令:动态设置单个 CSS 属性 -->
<p style:color style:font-size="{size}px">
  动态样式文本
</p>

<!-- 也可以使用传统的 style 属性(字符串) -->
<p style="color: {color}; font-size: {size}px;">
  传统方式
</p>

<!-- CSS 自定义属性(CSS Variables)-->
<div style="--accent: {color}">
  <p style="color: var(--accent)">使用 CSS 变量</p>
</div>

过渡动画

内置过渡函数

Svelte 内置了多种过渡动画函数,通过 transition:in:out: 指令使用,当元素进入或离开 DOM 时自动触发:

<script lang="ts">
  import { fade, fly, slide, scale, blur, draw } from 'svelte/transition';
  import { flip } from 'svelte/animate';
  import { elasticOut } from 'svelte/easing';

  let visible = $state(true);
</script>

<button onclick={() => (visible = !visible)}>切换</button>

{#if visible}
  <!-- fade:淡入淡出 -->
  <div transition:fade={{ duration: 300 }}>淡入淡出</div>

  <!-- fly:从指定位置飞入 -->
  <p transition:fly={{ y: 20, duration: 400 }}>从下方飞入</p>

  <!-- scale:缩放进入,带弹性缓动 -->
  <div in:scale={{ duration: 300, easing: elasticOut }}>
    弹性缩放
  </div>

  <!-- 不同的进入/离开动画 -->
  <p in:fly={{ x: -100 }} out:fly={{ x: 100 }}>
    左入右出
  </p>
{/if}
fade
透明度从 0 到 1(进入)或从 1 到 0(离开)。参数:duration(毫秒),delay,easing。
fly
元素同时移动和淡入。参数:x(水平偏移),y(垂直偏移),duration,easing。
slide
元素高度/宽度渐变(折叠效果)。常用于手风琴菜单。
scale
元素从缩放到正常大小(进入)或反向(离开)。可配合弹性缓动函数。
blur
高斯模糊过渡,元素从模糊变清晰。适合焦点切换效果。
draw
SVG 路径绘制动画,让 SVG 线条看起来像正在被绘制。

自定义过渡函数

<script lang="ts">
  import { cubicOut } from 'svelte/easing';

  // 自定义过渡:从右侧旋转飞入
  function spinIn(node: Element, { duration = 400 } = {}) {
    return {
      duration,
      css: (t: number) => {
        const eased = cubicOut(t);
        return `
          transform: rotate(${(1 - eased) * 360}deg) scale(${eased});
          opacity: ${eased};
        `;
      }
    };
  }
</script>

{#if visible}
  <div in:spinIn={{ duration: 600 }}>
    旋转飞入!
  </div>
{/if}
transition vs in/out 的区别

transition: 将相同的动画用于进入和离开。in: 只用于进入,out: 只用于离开。当你需要不同的进场和退场动画时,分开使用 in:out:

animate:列表重排动画

{#each} 列表中的元素顺序改变时,animate: 指令自动为每个元素从旧位置移动到新位置添加过渡动画(FLIP 动画)。

<script lang="ts">
  import { flip } from 'svelte/animate';
  import { fade } from 'svelte/transition';

  let items = $state([
    { id: 1, text: '买菜', done: false },
    { id: 2, text: '写代码', done: false },
    { id: 3, text: '读书', done: false }
  ]);

  function toggle(id: number) {
    const item = items.find(i => i.id === id);
    if (item) item.done = !item.done;
    items = [...items.filter(i => !i.done), ...items.filter(i => i.done)];
  }
</script>

<ul>
  {#each items as item (item.id)}
    <li
      animate:flip={{ duration: 300 }}
      out:fade={{ duration: 200 }}
    >
      <input type="checkbox" checked={item.done}
        onchange={() => toggle(item.id)} />
      <span class:done={item.done}>{item.text}</span>
    </li>
  {/each}
</ul>

<style>
  .done { text-decoration: line-through; opacity: 0.5; }
</style>

Svelte 的特殊 HTML 标签

{@html content}
渲染原始 HTML 字符串,不转义。注意:存在 XSS 风险,只在信任的内容上使用(如 Markdown 渲染后的 HTML)。
{@const variable = expression}
在模板中声明局部常量。常用于在 {#each} 块内计算派生值,避免重复计算或在模板中写复杂表达式。
<svelte:head>
向 document.head 插入内容,如 <title>、<meta>、<link>。适合在页面组件中动态更新 SEO 相关标签。
<svelte:component this={ComponentClass}>
动态渲染组件:根据变量决定渲染哪个组件。当 ComponentClass 为 null 时不渲染。适合插件系统、动态表单字段或根据用户权限渲染不同视图。
<svelte:element this={tag}>
动态渲染 HTML 标签(Svelte 5 新增):<svelte:element this="h1">标题</svelte:element>。tag 可以是任意有效的 HTML 标签名字符串。适合 CMS 内容渲染、无障碍标题层级动态调整。
<script lang="ts">
  let markdownHtml = $state('<p>来自 <strong>Markdown</strong> 的内容</p>');

  let users = $state([
    { name: 'Alice', score: 88 },
    { name: 'Bob', score: 95 }
  ]);

  // 动态组件:根据视图模式切换
  import ChartView from './ChartView.svelte';
  import TableView from './TableView.svelte';
  let viewMode = $state<'chart' | 'table'>('table');
  const ViewComponent = $derived(viewMode === 'chart' ? ChartView : TableView);

  // 动态标签:内容层级自适应
  let headingLevel = $state(1);
  const headingTag = $derived(`h${headingLevel}`);
</script>

<!-- 渲染 Markdown 转换后的 HTML -->
<div class="prose">{@html markdownHtml}</div>

<!-- @const 声明局部变量:避免在模板中重复写相同表达式 -->
{#each users as user}
  {@const grade = user.score >= 90 ? 'A' : user.score >= 80 ? 'B' : 'C'}
  {@const isPassing = user.score >= 60}
  <p class:pass={isPassing}>{user.name}:{user.score} 分 ({grade} 级)</p>
{/each}

<!-- 动态 head 内容 -->
<svelte:head>
  <title>用户列表 — 我的应用</title>
</svelte:head>

<!-- 动态组件切换(Svelte 5 推荐用 {#if} 替代,svelte:component 仍支持)-->
<button onclick={() => viewMode = viewMode === 'chart' ? 'table' : 'chart'}>
  切换视图
</button>
<svelte:component this={ViewComponent} data={users} />

<!-- 动态 HTML 标签 -->
<svelte:element this={headingTag}>
  动态标题级别 (h{headingLevel})
</svelte:element>

本章小结

本章核心要点