Svelte-现实世界指南-全-

Svelte 现实世界指南(全)

原文:zh.annas-archive.org/md5/14dc6d5ba3099ee8ed407418d0a0711b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在当今的数字时代,网络开发不断演变,几乎每天都有新的工具和框架出现。其中,Svelte 独树一帜,在最近的一次开发者调查中被选为最受欢迎的框架(survey.stackoverflow.co/2022/#section-most-loved-dreaded-and-wanted-web-frameworks-and-technologies)。Svelte 通过优化性能和提供直观的设计,以及功能齐全,带来了全新的视角。

虽然其他资源涵盖了 Svelte 的众多功能,但本书提供了一个独特的视角和方法。我们将深入探讨关键 Svelte 功能,解密核心概念。通过实际、真实的案例,我们不仅会教授“如何”,还会解释每种方法的“为什么”。通过理解底层原理和思维过程,你将能够无缝地将所学知识整合到你的 Svelte 项目中。

本书面向对象

本书专为具有 JavaScript、CSS 和一般网络开发实践基础知识的网络开发人员和软件工程师量身定制。无论你是 Svelte 的新手,渴望深入了解,还是已经接触过其基础知识但希望通过实际案例和模式提升你的专业知识,这本指南都是为你准备的。除了提供指导外,内容还深入探讨了每个概念背后的“为什么”和“如何”,确保读者不仅理解材料,而且能够有效地在专业环境和各种项目中应用它们。如果你准备好不仅学习 Svelte,还要掌握其复杂性和实际应用,那么这本书是你下一本必读的书籍。

本书涵盖内容

第一章Svelte 中的生命周期,概述了 Svelte 的生命周期、它们各自的功能、调用它们的规则以及重用和组合这些生命周期函数的策略。

第二章实现样式和主题,深入探讨了六种独特的样式化 Svelte 组件的方法。你还将学习如何定义主题以及启用用户自定义 Svelte 组件的必要知识。

第三章管理属性和状态,加深了你对于 Svelte 中属性和状态的理解。这一章节解密了属性、状态和绑定,并讨论了一向绑定和双向绑定的区别。它还展示了从属性推导状态的方法。

第四章组件组合,提供了从父组件控制子组件内容的技巧。你将探索 <slot> 元素以及各种 Svelte 特殊元素,如 <svelte:self><svelte:fragment>

第五章, 使用动作创建自定义事件,开始探索 Svelte 动作的三章内容。本章首先探讨了使用 Svelte 动作创建自定义事件的想法。

第六章, 使用动作集成库,提供了使用动作将第三方 JavaScript 库集成到 Svelte 中的实用指南。

第七章, 使用 Svelte 动作进行渐进式增强,解释了渐进式增强的概念,并帮助你理解如何利用 Svelte 动作逐步增强你的 Svelte 应用程序。

第八章, 上下文与存储,深入探讨了 Svelte 上下文和存储。你将了解何时以及如何使用 Svelte 上下文和存储。

第九章, 实现自定义存储,通过实用的分步指南教你如何实现自定义 Svelte 存储,并在过程中通过多个真实世界示例进行讲解。

第十章, 使用 Svelte 存储进行状态管理,为你提供了在 Svelte 应用程序中管理应用程序状态的实际技巧。你还将学习如何在 Svelte 中使用第三方状态管理库。

第十一章, 无渲染组件,探讨了无渲染组件的概念,这是一种不渲染任何自身 HTML 元素的可重用组件。我们将系统地通过实现此类组件。

第十二章, 存储与动画,探讨了内置的tweenedspring存储。你将学习如何在 Svelte 应用程序中应用它们,以及如何为这些动画存储自定义插值。

第十三章, 使用过渡,提供了对 Svelte 中过渡的全面理解。你将学习如何在 Svelte 中使用过渡,何时以及如何播放过渡,以及它们在底层是如何工作的。

第十四章, 探索自定义过渡,探讨了在 Svelte 中编写自定义过渡的想法。你将了解 Svelte 过渡合约,并通过实际示例,你将逐步指导创建 Svelte 中的自定义过渡。

第十五章, 过渡中的无障碍性,揭示了过渡中的无障碍性考虑,特别是针对有前庭功能障碍的用户。你将获得制作尊重用户偏好并满足所有人的负责任过渡的见解。

为了充分利用本书

你需要具备基本的 Web 开发知识,并对 JavaScript、CSS 和 HTML 有基本的了解。

本书涵盖的软件/硬件 操作系统要求
Svelte 4 Windows, macOS, 或 Linux
JavaScript

如果你正在使用本书的数字版,我们建议你亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助你避免与代码的复制和粘贴相关的任何潜在错误。

下载示例代码文件

你可以从 GitHub(github.com/PacktPublishing/Real-World-Svelte)下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富的图书和视频目录的代码包可供选择,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书使用了多种文本约定。

文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”

代码块是这样设置的:

const folder = [
  { type: 'file', name: 'a.js' },
  { type: 'file', name: 'b.js' },
  { type: 'folder', name: 'c', children: [
    { type: 'file', name: 'd.js' },
  ]},
];

当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将被设置为粗体:

<!-- filename: JsonTree.svelte -->
<script>
  export let data;
</script>
<ul>
  {#each Object.entries(data) as [key, value]}
    <li>
      {key}:
      {#if typeof value === 'object'}
        <svelte:self data={value} />
      {:else}
        {value}
      {/if}
    <li>
  {/each}
</ul>

<form>元素默认情况下,当你点击action属性时,它会携带<form>元素内<input>元素中填写的值。”

小贴士或重要提示

它看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈: 如果你对此书的任何方面有疑问,请通过customercare@packtpub.com给我们发邮件,并在邮件的主题中提及书名。

勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,如果你能向我们报告这一点,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。

盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供材料的链接。

如果你有兴趣成为作者: 如果你在一个你擅长的主题上,并且你感兴趣的是撰写或为本书做出贡献,请访问authors.packtpub.com

分享你的想法

一旦你阅读了《Real-World Svelte》,我们很乐意听到你的想法!请点击此处直接进入此书的亚马逊评论页面并分享你的反馈。

你的评价对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。

下载本书的免费 PDF 副本

感谢你购买这本书!

你喜欢在路上阅读,但无法携带你的印刷书籍到处走吗?

你的电子书购买是否与你的选择设备不兼容?

别担心,现在每购买一本 Packt 书籍,你都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从你喜欢的技术书籍中搜索、复制和粘贴代码到你的应用程序中。

优惠远不止于此,你还可以获得独家折扣、时事通讯和每日免费内容的每日邮箱访问权限。

按照以下简单步骤获取福利:

  1. 扫描下面的二维码或访问以下链接

packt.link/free-ebook/9781804616031

  1. 提交你的购买证明

  2. 就这些!我们将直接将你的免费 PDF 和其他福利发送到你的邮箱。

第一部分:编写 Svelte 组件

在本节中,我们将为编写 Svelte 组件打下基础。我们将从深入研究 Svelte 组件的生命周期开始。然后,我们将学习如何为我们的 Svelte 组件添加样式和主题。之后,我们将探讨组件之间数据传递的复杂性,最后总结将组件组合成一个统一的 Svelte 应用程序的技术。

本部分包含以下章节:

  • 第一章, Svelte 中的生命周期

  • 第二章, 实现样式和主题

  • 第三章, 管理属性和状态

  • 第四章, 组件编写

第一章:Svelte 中的生命周期

Svelte 是一个前端框架。你可以使用 Svelte 来构建网站和 Web 应用程序。一个 Svelte 应用程序由组件组成。你可以在具有.svelte扩展名的文件中编写 Svelte 组件。每个.svelte文件都是一个 Svelte 组件。

当你创建和使用一个 Svelte 组件时,该组件会经历组件生命周期的各个阶段。Svelte 提供了生命周期函数,允许你钩入组件的不同阶段。

在本章中,我们将首先讨论 Svelte 中的各种生命周期和生命周期函数。在心中有了对生命周期的清晰认识后,你将学习使用生命周期函数的基本规则。这是非常重要的,因为你会发现这种理解将使我们能够以很多创造性的方式使用生命周期函数。

本章包含以下主题的章节:

  • Svelte 生命周期函数是什么?

  • 调用生命周期函数的规则

  • 如何重用和组合生命周期函数

技术要求

编写 Svelte 应用程序非常简单,不需要任何付费工具。尽管大多数付费工具都有附加价值,但我们决定只使用免费工具,以便您无限制地获取本书的内容。

你将需要以下内容:

  • Visual Studio Code 作为集成开发环境(code.visualstudio.com/)

  • 一个不错的网络浏览器(例如 Chrome、Firefox 或 Edge)

  • Node.js 作为 JavaScript 运行环境(nodejs.org/)

本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/Real-World-Svelte/tree/main/Chapter01

所有章节的代码可以在github.com/PacktPublishing/Real-World-Svelte找到。

理解 Svelte 的生命周期函数

当使用 Svelte 组件时,它在其生命周期中会经历不同的阶段:挂载、更新和销毁。这类似于人类。我们一生中会经历各种阶段,如出生、成长、老年和死亡。我们称这些不同的阶段为生命周期。

在我们讨论 Svelte 中的生命周期之前,让我们先看看一个 Svelte 组件。

<script>
  import { onMount, beforeUpdate, afterUpdate, onDestroy } from 'svelte';
  let count = 0;
  onMount(() => { console.log('onMount!'); });
  beforeUpdate(() => { console.log('beforeUpdate!'); });
  afterUpdate(() => { console.log('afterUpdate!'); });
  onDestroy(() => { console.log('onDestroy!'); });
</script>
<button on:click={() => { count ++; }}>
  Counter: {count}
</button>

你能告诉我代码的每个部分何时执行吗?

代码的并非所有部分都会同时执行;代码的不同部分会在组件生命周期的不同阶段执行。

Svelte 组件有四个不同的生命周期阶段:初始化、挂载、更新和销毁。

初始化组件

当你创建一个组件时,组件首先会进入初始化阶段。你可以将其视为设置阶段,此时组件会设置其内部状态。

这就是第 2-7 行正在执行的地方。

声明并初始化 count 变量。调用 onMountbeforeUpdateafterUpdateonDestroy 生命周期函数,并传入回调函数,以在组件生命周期的特定阶段注册它们。

在组件初始化后,Svelte 开始在模板中创建元素,在这种情况下,是一个 <button> 元素和用于 "Counter: "{count} 的文本元素。

挂载组件

在所有元素创建完成后,Svelte 将按顺序将它们插入到 文档对象模型 (DOM) 中。这被称为挂载阶段,其中元素被挂载到 DOM 上。

如果你向一个元素添加 Svelte 动作,那么这些动作将与元素一起被调用:

<script>
  function action(node) {}
</script>
<div use:action>

我们将在 第五章7 中更深入地探讨 Svelte 动作。

如果你向元素添加事件监听器,这就是 Svelte 将事件监听器附加到元素的时候。

在前一个示例中,Svelte 在将按钮插入 DOM 之后将其附加到 click 事件监听器上。

当我们向一个元素添加绑定时,绑定的变量会根据元素的值进行更新:

<script>
  let element;
</script>
<div bind:this={element} />

这时,element 变量将更新为 Svelte 创建的 <div> 元素的引用。

如果你向一个元素添加转换,这就是转换被初始化并开始播放的时候。

以下是一个向元素添加转换的示例。你可以使用 transition:, in:, 和 out: 指令向元素添加转换。我们将在 第十三章15 中更深入地探讨 Svelte 转换:

<div in:fade />

在处理完所有指令,use:(动作),on:(事件监听器),bind: 绑定,in:transition:(转换)之后,通过调用在 onMount 生命周期函数中注册的所有函数,挂载阶段结束。

这时,第 4 行的函数将被执行,你将在日志中看到打印出 "onMount!"

更新组件

当你点击按钮时,click 事件监听器被调用。第 9 行的函数被执行。count 变量被增加。

在 Svelte 根据最新的 count 变量值修改 DOM 之前,将调用在 beforeUpdate 生命周期函数中注册的函数。

第 5 行的函数将被执行,你将在日志中看到打印出文本 "beforeUpdate!"

在此阶段,如果你尝试检索按钮内的文本内容,它仍然是 "``Counter: 0"

然后 Svelte 继续修改 DOM,将按钮的文本内容更新为 "``Counter: 1"

在更新组件内的所有元素之后,Svelte 会调用在 afterUpdate 生命周期函数中注册的所有函数。

第 6 行的函数被执行,你将在日志中看到打印出文本 "afterUpdate!"

如果你再次点击按钮,Svelte 将通过另一个周期的 beforeUpdate,然后更新 DOM 元素,然后 afterUpdate

销毁组件

当条件成立时,向用户显示的条件性组件将保持不变;当条件不再成立时,Svelte 将继续销毁组件。

假设我们例子中的组件现在进入了销毁阶段。

Svelte 会调用在 onDestroy 生命周期函数中注册的所有函数。第 7 行的函数会被执行,你将在日志中看到打印出的文本 "onDestroy!"

之后,Svelte 会从 DOM 中移除元素。

如果需要,Svelte 会清理指令,例如移除事件监听器和从动作中调用销毁方法。

就这样!如果你尝试再次创建组件,新的周期会再次开始。

Svelte 组件的生命周期从初始化、挂载、更新和销毁开始。Svelte 提供生命周期方法,允许你在组件的不同阶段运行函数。

由于生命周期函数只是从 'svelte' 导出的函数,你可以在任何地方导入和使用它们吗?导入和使用它们时有什么规则或约束吗?

让我们来看看。

调用生命周期函数的一条规则

调用组件生命周期函数的唯一规则是,你应该在组件初始化期间调用它们。如果没有组件正在初始化,Svelte 会通过抛出错误来抱怨。

让我们看看以下示例:

<script>
  import { onMount } from 'svelte';
  function buttonClicked() {
    onMount(() => console.log('onMount!'));
  }
</script>
<button on:click={buttonClicked} />

当你点击按钮时,它会调用 buttonClicked,这将调用 onMount。由于在调用 onMount 时没有组件正在初始化(在你点击按钮时,上面的组件已经初始化并挂载),Svelte 会抛出错误:

Error: Function called outside component initialization

是的,Svelte 不允许在组件初始化阶段之外调用生命周期函数。这条规则规定了你可以何时调用生命周期函数。它没有规定在哪里或如何调用生命周期函数。这允许我们重构生命周期函数并以其他方式调用它们。

重构生命周期函数

如果你仔细观察调用生命周期函数的规则,你会注意到它关乎何时调用它们,而不是在哪里调用它们。

<script> 标签的顶层调用生命周期函数并不是必需的。

在以下示例中,setup 函数在组件初始化期间被调用,然后反过来调用 onMount 函数:

<script>
  import { onMount } from 'svelte';
  setup();
  function setup() {
    onMount(() => console.log('onMount!'));
  }
</script>

由于组件仍在初始化中,这是完全正常的。

同样,在组件内部导入 onMount 函数也不是必需的。正如你在以下示例中看到的,你可以在另一个文件中导入它;只要在组件初始化期间调用 onMount 函数,那就完全没问题:

// file-a.js
import { onMount } from 'svelte';
export function setup() {
  onMount(() => console.log('onMount!'));
}

在前面的代码片段中,我们将之前定义的setup函数移动到一个名为file-a.js的新模块中。然后,在原始的 Svelte 组件中,我们不是定义setup函数,而是从file-a.js中导入它,如下面的代码片段所示:

<script>
  import { setup } from './file-a.js';
  setup();
</script>

由于setup函数调用了onMount函数,同样的规则也适用于setup函数!您不能再在组件初始化之外调用setup函数。

应该注册哪个组件?

仅看setup函数,您可能会想知道,当您调用onMount函数时,Svelte 是如何知道您指的是哪个组件的生命周期的?

内部,Svelte 跟踪哪个组件正在初始化。当您调用生命周期函数时,它将您的函数注册到正在初始化的组件的生命周期中。

因此,相同的setup函数可以在不同的组件中调用,并为不同的组件注册onMount函数。

这解锁了本章的第一个模式:重用生命周期函数。

在 Svelte 组件中重用生命周期函数

在上一节中,我们了解到我们可以将生命周期函数的调用提取到一个函数中,并在其他组件中重用该函数。

让我们看看一个例子。在这个例子中,组件在屏幕上添加了 5 秒后,将调用showPopup函数。我想在其他组件中重用调用showPopup的逻辑:

<script>
  import { onMount } from 'svelte';
  import { showPopup } from './popup';
  onMount(() => {
    const timeoutId = setTimeout(() => {
      showPopup();
    }, 5000);
    return () => clearTimeout(timeoutId);
  });
</script>

这里,我可以将逻辑提取到一个函数中,showPopupOnMount

// popup-on-mount.js
import { onMount } from 'svelte';
import { showPopup } from './popup';
export function showPopupOnMount() {
  onMount(() => {
    const timeoutId = setTimeout(() => {
      showPopup();
    }, 5000);
    return () => clearTimeout(timeoutId);
  });
}

现在,我可以导入这个函数并在任何组件中重用它:

<script>
  import { showPopupOnMount } from './popup-on-mount';
  showPopupOnMount();
</script>

您可能想知道,为什么不只提取回调函数并重用它呢?

// popup-on-mount.js
import { showPopup } from './popup';
export function showPopupOnMount() {
  const timeoutId = setTimeout(() => {
    showPopup();
  }, 5000);
  return () => clearTimeout(timeoutId);
}

在这里,我们将setTimeoutclearTimeout逻辑提取到showPopupOnMount中,并将该函数传递给onMount

<script>
  import { onMount } from 'svelte';
  import { showPopupOnMount } from './popup-on-mount';
  onMount(showPopupOnMount);
</script>

在我看来,重构和重用的第二种方法不如第一种方法好。将整个生命周期函数的调用提取到函数中有几个优点,因为它允许您做更多的事情:

  • 您可以将不同的输入参数传递给您的生命周期函数。

    假设您希望允许不同的组件自定义显示弹出窗口前的持续时间。以这种方式传递要容易得多:

    <script>
      import { showPopupOnMount } from './popup-on-mount';
      showPopupOnMount(2000); // change it to 2s
    </script>
    
  • 您可以从函数中返回值。

    假设您想返回onMount函数中使用的timeoutId,这样您就可以在用户在组件中的任何按钮上点击时取消它。

    如果您只是重用回调函数,这将几乎不可能做到,因为回调函数返回的值将用于注册onDestroy生命周期函数:

    <script>
      import { showPopupOnMount } from './popup-on-mount';
      const timeoutId = showPopupOnMount(2000);
    </script>
    <button on:click={() => clearTimeout(timeoutId)} />
    

    看看这样写,实现它以返回任何内容是多么容易:

    // popup-on-mount.js
    export function showPopupOnMount(duration) {
      let timeoutId;
      onMount(() => {
        timeoutId = setTimeout(() => {
          showPopup();
        }, duration ?? 5000);
        return () => clearTimeout(timeoutId);
      });
      return timeoutId;
    }
    
  • 您可以将更多逻辑与生命周期函数一起封装。

    有时,您在生命周期函数回调函数中的代码在隔离环境中不起作用;它会与其他变量交互并修改它们。为了重用此类生命周期函数,必须将这些变量和逻辑封装到一个可重用的函数中。

    为了说明这一点,让我们看看一个新的例子。

    在这里,我有一个计数器,当组件被添加到屏幕上时开始计数:

    <script>
      import { onMount } from 'svelte';
      let counter = 0;
      onMount(() => {
        const intervalId = setInterval(() => counter++, 1000);
        return () => clearInterval(intervalId);
      });
    </script>
    <span>{counter}</span>
    

    counter 变量与 onMount 生命周期函数相关联;为了重用此逻辑,应将 counter 变量和 onMount 函数一起提取到一个可重用的函数中:

    import { writable } from 'svelte/store';
    import { onMount } from 'svelte';
    export function startCounterOnMount() {
      const counter = writable(0);
      onMount(() => {
        const intervalId = setInterval(() => counter.update($counter => $counter + 1), 1000);
        return () => clearInterval(intervalId);
      });
      return counter;
    }
    

    在这个例子中,我们使用一个 writable Svelte 存储使 counter 变量变得响应式。我们将在本书的 第三部分 中更深入地探讨 Svelte 存储。

    目前,您需要理解的是,Svelte 存储允许 Svelte 跨模块跟踪变量的变化,并且您可以通过在 Svelte 存储变量前加上 $ 来订阅和检索存储的值。例如,如果您有一个名为 counter 的 Svelte 存储,那么要获取 Svelte 存储的值,您需要使用 $counter 变量。

    现在,我们可以在任何 Svelte 组件中使用 startCounterOnMount 函数:

    <script>
      import { startCounterOnMount } from './counter';
      const counter = startCounterOnMount();
    </script>
    <span>{$counter}</span>
    

希望我已经说服您提取生命周期函数调用的优点。让我们通过一个例子来试试。

练习 1 – 更新计数器

在下面的示例代码中,我想知道组件经过了多少次更新周期。

利用每次组件经过更新周期时都会调用 afterUpdate 回调函数的事实,我创建了一个计数器,每次 afterUpdate 回调函数被调用时都会递增。

为了帮助我们仅测量特定用户操作的更新计数,我们有开始测量和停止测量的函数,因此更新计数器仅在测量时递增:

<script>
  import { afterUpdate } from 'svelte';
  let updateCount = 0;
  let measuring = false;
  afterUpdate(() => {
    if (measuring) {
      updateCount ++;
    }
  });
  function startMeasuring() {
    updateCount = 0;
    measuring = true;
  }
  function stopMeasuring() {
    measuring = false;
  }
</script>
<button on:click={startMeasuring}>Measure</button>
<button on:click={stopMeasuring}>Stop</button>
<span>Updated {updateCount} times</span>

为了重用 counter: 的所有逻辑——更新周期的计数以及测量开始和停止——我们应该将所有这些移动到一个函数中,最终看起来像这样:

<script>
  import { createUpdateCounter } from './update-counter';
  const { updateCount, startMeasuring, stopMeasuring } = createUpdateCounter();
</script>
<button on:click={startMeasuring}>Measure</button>
<button on:click={stopMeasuring}>Stop</button>
<span>Updated {$updateCount} times</span>

更新计数器返回一个包含 updateCount 变量和 startMeasuring 以及 stopMeasuring 函数的对象。

createUpdateCounter 函数的实现留作练习,您可以在 github.com/PacktPublishing/Real-World-Svelte/tree/main/Chapter01/01-update-counter 查看答案。

我们已经学习了如何提取生命周期函数并重用它,所以让我们更进一步,在下一个模式中重用多个生命周期函数:组合生命周期函数。

将生命周期函数组合成可重用钩子

到目前为止,我们主要讨论了重用单个生命周期函数。然而,没有任何阻止我们将多个生命周期函数组合起来执行一个函数。

这里摘录了来自 svelte.dev/examples/update 的示例。该示例显示了一个消息列表。当新消息被添加到列表中时,容器会自动滚动到最底部以显示新消息。在代码片段中,我们看到这种自动滚动行为是通过结合使用 beforeUpdateafterUpdate 实现的:

<script>
  import { beforeUpdate, afterUpdate } from 'svelte';
  let div;
  let autoscroll;
  beforeUpdate(() => {
    autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
  });
  afterUpdate(() => {
    if (autoscroll) div.scrollTo(0, div.scrollHeight);
  });
</script>
<div bind:this={div} />

要在其他组件中重用这个 autoscroll 逻辑,我们可以将 beforeUpdateafterUpdate 逻辑一起提取到一个新函数中:

export function setupAutoscroll() {
  let div;
  let autoscroll;
  beforeUpdate(() => {
    autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
  });
  afterUpdate(() => {
    if (autoscroll) div.scrollTo(0, div.scrollHeight);
  });
  return {
  setDiv(_div) {
  div = _div;
    },
  };
}

然后,我们可以在任何组件中使用提取的函数,setupAutoScroll

<script>
  import { setupAutoscroll } from './autoscroll';
  const { setDiv } = setupAutoscroll();
  let div;
  $: setDiv(div);
</script>
<div bind:this={div} />

在重构后的 setupAutoscroll 函数中,我们返回一个 setDiv 函数,以便我们可以在 setupAutoscroll 函数内部更新 div 的引用。

正如你所见,通过遵循在组件初始化期间调用生命周期函数的一条规则,你可以将多个生命周期函数组合成可重用的钩子。到目前为止你所学的足以组合生命周期函数,但未来还有更多替代方案。在接下来的章节中,你将探索第 5 章 中的 Svelte 动作和第 8 章 中的 Svelte 存储,进一步扩展你的选择。以下是一些这些替代方案的预览。

另一种实现方式可能是将 div 设为一个可写存储,并在 setupAutoscroll 函数中返回它。这样,我们就可以直接绑定到 div 可写存储,而无需手动调用 setDiv

或者,我们可以返回一个遵循 Svelte 动作合约的函数,并在 div 上使用该动作:

export function setupAutoscroll() {
  let div;
  // ...
  return function (node) {
    div = node;
    return {
      destroy() {
        div = undefined;
      },
    };
  };
}

setupAutoscroll 现在返回一个动作,我们使用这个动作在我们的 div 容器上:

<script>
  import { setupAutoscroll } from './autoscroll';
  const autoscroll = setupAutoscroll();
</script>
<div use:autoscroll />

我们将在本书的后面部分更详细地讨论 Svelte 动作合约。

我们已经看到如何将生命周期函数提取到单独的文件中,并在多个 Svelte 组件中重用它。目前,组件独立调用生命周期函数,并作为独立单元运行。是否有可能同步或协调使用相同生命周期函数的组件之间的动作?让我们来找出答案。

在组件间协调生命周期函数

由于我们在组件间重用相同的函数,我们可以全局跟踪使用相同生命周期函数的组件。

让我给你举一个例子。在这里,我想跟踪屏幕上有多少组件正在使用我们的生命周期函数。

要计算组件的数量,我们可以在模块级别定义一个变量,并在我们的生命周期函数中更新它:

import { onMount, onDestroy } from 'svelte';
import { writable } from 'svelte/store';
let counter = writable(0);
export function setupGlobalCounter() {
  onMount(() => counter.update($counter => $counter + 1));
  onDestroy(() => counter.update($counter => $counter - 1));
  return counter;
}

由于 counter 变量是在 setupGlobalCounter 函数外部声明的,因此相同的 counter 变量实例被用于并共享在所有组件之间。

当任何组件被挂载时,它将增加 counter,任何引用 counter 的组件都将更新为最新的计数器值。

当你想要在组件之间设置一个共享的通信通道并在组件被销毁时在onDestroy中将其拆除时,这种模式非常有用。

让我们尝试在我们的下一个练习中使用这项技术。

练习 2 – 滚动阻止器

通常情况下,当你将一个弹出组件添加到屏幕上时,你希望文档不可滚动,这样用户就能专注于弹出窗口,并且只在该弹出窗口内滚动。

这可以通过将bodyoverflow CSS 属性设置为"hidden"来实现。

编写一个可重用的函数,该函数由弹出组件使用,在弹出组件安装时禁用滚动。当弹出组件被销毁时,恢复初始的overflow属性值。

请注意,屏幕上可以同时安装多个弹出组件,因此只有当所有弹出窗口都被销毁时,你才应该恢复overflow属性值。

你可以在github.com/PacktPublishing/Real-World-Svelte/tree/main/Chapter01/02-scroll-blocker查看答案。

摘要

在本章中,我们探讨了 Svelte 组件的生命周期。我们看到了组件生命周期的不同阶段,并学习了生命周期函数回调将在何时被调用。

我们还介绍了调用生命周期函数的规则。这有助于我们实现生命周期函数的重用和组合的不同模式。

在下一章中,我们将开始探讨为 Svelte 组件进行样式化和主题化的不同模式。

第二章:实现样式和主题

没有样式的情况下,组件内的 h1 元素将与其他组件中的 h1 元素看起来相同。Svelte 允许您使用 层叠样式表 (CSS),这是一种用于样式化和格式化网页内容的语言,来设置您的元素样式,使它们具有不同的外观和感觉。

在本章中,我们将首先讨论不同的 Svelte 组件样式设置方法。然后,我们将看到一些示例,包括将流行的 CSS 框架 Tailwind CSS 集成到 Svelte 中。

接下来,我们将讨论主题。当您在 Svelte 组件中一致地应用一组样式时,您将在组件中看到整体样式主题。我们将讨论如何跨组件同步样式,以及如何允许组件用户自定义样式。

到本章结束时,您将学会各种样式设置方法,并能够根据场景选择合适的方案和适用正确的方法。

本章包括以下内容:

  • 设置 Svelte 组件样式的多种方法

  • 使用 Tailwind CSS 设置 Svelte 组件样式的多种方法

  • 将主题应用到 Svelte 组件

技术要求

您可以在 GitHub 上找到本章使用的代码:github.com/PacktPublishing/Real-World-Svelte/tree/main/Chapter02

以六种不同的方式设置 Svelte 组件的样式

在 Svelte 组件中,您有定义结构和内容的元素。通过样式设置,您可以改变元素的外观和感觉,超出浏览器的默认设置。

Svelte 组件可以通过六种不同的方式来设置样式。让我们来探索在 Svelte 组件内部应用样式的不同方法。

使用 style 属性进行样式设置

首先,您可以通过 style 属性向元素添加内联样式:

<div style="color: blue;" />

前面的代码片段将 div 内文本的颜色变为蓝色。

与 HTML 元素中的 style 属性类似,您可以在 style 属性中添加多个 CSS 样式声明:

<div style="color: blue; font-size: 2rem;" />

在 Svelte 中添加多个 CSS 样式声明的语法与在 HTML 中做的方式相同。在前面的代码片段中,我们将 div 内的文本颜色改为蓝色,并设置为 2 rem 大小。

style 属性的值是一个字符串。您可以使用动态表达式来构建 style 属性:

<div style="color: {color};" />

当您在 style 属性中拥有多个 CSS 样式声明时,有时会变得杂乱无章:

<div style="color: {color}; font-size: {fontSize}; background: {background}; border-top: {borderTop};" />

使用 style: 指令

Svelte 提供了 style: 指令,允许您将 style 属性拆分为多个属性,在添加换行和缩进后,这可能会使代码更易于阅读:

<div
  style:color={color}
  style:font-size={fontSize}
  style:background={background}
  style:border-top={borderTop}
/>

style: 指令遵循以下语法:

style:css-property-name={value}

CSS 属性名称可以是任何 CSS 属性,包括 CSS 自定义属性:

<div style:--main-color={color} />

如果样式名称与它所依赖的值名称相匹配,那么您可以使用 style: 指令的简写形式:

<div
  style:color
  style:font-size={fontSize}
  style:background
  style:border-top={borderTop}
/>

style: 指令中声明的样式具有比 style 属性更高的优先级。在下面的例子中,h1 文本颜色是红色而不是蓝色:

<div style:color="red" style="color: blue;" />

除了逐个为元素添加内联样式来样式化元素外,接下来的方法允许我们编写 CSS 选择器来针对多个元素并将它们一起样式化。

添加 <style>

在每个 Svelte 组件中,你可以有一个顶级 <style> 块。

<style> 块内,你可以有 CSS 规则,针对组件内的元素:

<div>First div</div>
<div>Second div</div>
<style>
  div {
    color: blue;
  }
</style>

当你想要在组件内的多个元素上应用相同的样式时,这种方法很有用。

在前面的代码中,CSS 规则会将文档中的所有 div 元素都变成蓝色吗?

不。<style> 块内的 CSS 规则的作用域限定在组件内,这意味着 CSS 规则只会应用于组件内的元素,而不会应用于应用中的其他地方的 div 元素。

因此,你不必担心 <style> 块内的 CSS 规则会改变组件外的元素。

但是,这是如何工作的?Svelte 如何确保 CSS 规则只应用于同一组件内的元素?

让我们探索一下。

Svelte 如何作用域组件内的 CSS 规则

让我们稍微偏离一下,来理解 Svelte 如何确保组件内的 CSS 规则是作用域限定的。

当 Svelte 编译一个组件时,Svelte 编译器会遍历每个 CSS 规则,并尝试将每个元素与 CSS 规则的选择器匹配:

<div>row</div>
<style>
  div { color: red; }
</style>

当一个元素匹配选择器时,Svelte 编译器将为组件生成一个唯一的 CSS 类名并将其应用于该元素。同时,Svelte 通过在选择器中包含生成的类名的类选择器来限制被 CSS 规则选择的元素的作用域。

元素和 CSS 规则的转换看起来像这样:

<div class="svelte-q5jdbb">row</div>
<style>
  div.svelte-q5jdbb { color: red; }
</style>

在这里,"svelte-g5jdbb" 是由计算 CSS 内容的哈希值生成的唯一 CSS 类名。当 CSS 内容发生变化时,哈希值也会不同。由于 Svelte 编译器只将 CSS 类名应用于组件内的元素,因此不太可能将样式应用于组件外的其他元素。

这种转换默认发生在编译过程中。你不需要做任何额外的事情。这里的例子只是为了说明目的。

了解 <style> 块内的 CSS 规则是作用域限定的,当你想要将样式应用于具有相同节点名的所有元素时,使用 CSS 类型选择器作为你的 CSS 规则通常就足够了。然而,如果你只想样式化某些元素,比如在上一个例子中只样式化第二个 div 元素,你可以在第二个 div 元素上添加一个 class 属性来区分其他 div 元素。

是的,添加一个 class 属性是我们样式化 Svelte 组件的另一种方式。

添加 class 属性

CSS 类选择器在 Svelte 中的工作方式与在 CSS 中相同。

当你向元素添加 class 属性时,你可以使用 CSS 类选择器来定位它。

在下面的示例中,我们将 highlight 类添加到了第二个 div 元素上,因此只有第二个 div 元素具有黄色背景:

<div>First div</div>
<div class="highlight">Second div</div>
<style>
  .highlight {
    background-color: yellow;
  }
</style>

class 属性的值可以是字符串或动态表达式。

你可以条件性地将类应用到元素上:

<div class="{toHighlight ? "highlight" : ""} {toBold ? "bold" : ""}" />

在前面的示例中,当 toHighlighttoBold 的值都为 true 时,class 属性的值计算为 "highlight bold"。因此,两个类,highlightbold,被应用到 div 元素上。

这种根据变量条件性地将类应用到元素上的模式非常常见,因此 Svelte 提供了 class: 指令来简化它。

使用 class: 指令简化类属性

在前面的示例中,我们当 toHighlight 变量为真值时条件性地应用了 highlight 类,当 toBold 变量为真值时应用了 bold 类。

这可以通过 class: 指令来简化,如下所示:

class:class-name={condition}

使用 class: 指令简化前面的示例,我们得到以下代码:

<div class:highlight={toHighlight} class:bold={toBold} />

就像 style: 属性一样,如果类的名称与条件变量的名称相同,你可以进一步简化为 class: 指令的缩写形式。

如果添加 highlight 类的条件是一个名为 highlight 的变量,那么前面的示例可以重写如下:

<div class:highlight />

将所有这些放在一起,在下面的代码示例中,当 highlight 变量值为 true 时,div 元素具有黄色背景,否则具有透明背景:

<script>
  export let highlight = true;
</script>
<div class:highlight />
<style>
  .highlight {
    background-color: yellow;
  }
</style>

我们迄今为止探索的所有将样式应用到元素的方法都涉及在 Svelte 组件内编写 CSS 声明。然而,可以在 Svelte 组件外部定义样式。

从外部 CSS 文件应用样式

假设你向应用程序的 HTML 中添加了一个样式元素,如下所示:

<html>
  <head>
    <style>.title { color: blue }</style>
  </head>
</html>

或者,你可以在应用程序的 HTML 中包含外部 CSS 文件,如下所示:

<html>
  <head>
    <link rel="stylesheet" href="external.css">
  <head>
</html>

在这两种情况下,它们中编写的 CSS 规则被应用到应用程序中的所有元素上,包括 Svelte 组件内的元素。

如果你正在使用构建工具,如 webpack、Rollup 或 Vite 来打包你的应用程序,通常需要配置你的构建工具以使用 import 语句导入 CSS 文件,就像导入任何 JS 文件一样(一些工具,如 Vite,甚至默认配置为允许像导入 JS 文件一样导入 CSS 文件!):

import './style.css';

导入 CSS 模块

在 Vite 中,当你将 CSS 文件命名为以 .module.css 结尾时,该 CSS 文件被视为 CSS 模块 文件。CSS 模块是一个 CSS 文件,其中定义的所有类名都具有局部作用域:

/* filename: style.module.css */
.highlight {
  background-color: yellow;
}

这意味着在 CSS 模块中指定的 CSS 类名不会与任何其他地方指定的类名冲突,即使是与相同名称的类名也不会。

这是因为构建工具会将 CSS 模块中的类名转换成独特的东西,这不太可能与任何其他名称冲突。

以下是一个示例,说明前面的 CSS 模块在构建后 CSS 规则将如何呈现:

/* the class name 'highlight' transformed into 'q3tu41d' */
.q3tu41d {
  background-color: yellow;
}

当从 JavaScript 模块导入 CSS 模块时,CSS 模块导出一个对象,其中包含原始类名到转换后类名的映射:

import styles from './style.module.css';
styles.highlight; // 'q3tu41d'

在前面的代码片段中,导入的 styles 模块是一个对象,我们可以通过 styles.highlight 获取转换后的类名,即 'q3tu41d'

这允许你在 Svelte 组件中使用转换后的类名:

<script>
  import styles from './style.module.css';
</script>
<div class="{styles.highlight}" />

我们已经看到了六种不同的方式来设置 Svelte 组件的样式,但你是如何选择何时使用哪一种方式的呢?

选择为 Svelte 组件设置样式的技巧:

我们到目前为止看到的每种方法都有其优缺点。大多数时候,选择哪种方法来设置 Svelte 组件的样式取决于个人偏好和便利性。

在选择为我的 Svelte 组件设置样式的技巧时,以下是我的一些个人偏好:

  • <style> 块与内联样式:

    大多数时候,我发现我编写的样式与显示元素的逻辑关系不大,而更多的是关于元素的外观。因此,当样式与元素一起内联时,我发现这会干扰我阅读组件的流程。我更喜欢将所有样式放在 <style> 块的一个地方。

  • 使用 style: 指令和 class: 指令控制样式:

    当元素的样式属性依赖于一个变量时,我会使用 style: 指令或 class: 指令而不是 style 属性或 class 属性。我发现这更易于阅读,同时也发现这是一个强烈的信号,告诉我元素的样式是动态的。

    当我仅基于一个变量更改一个样式属性时,我会使用 style: 指令。然而,当使用同一个变量更改多个样式属性时,我更倾向于声明一个 CSS 类来将所有样式组合在一起,并通过 class: 指令来控制它。

  • 在多个 Svelte 组件中重用 CSS 声明的 CSS 模块:

    在撰写本书时,Svelte 中没有内置方法来在多个 Svelte 组件中共享相同的 CSS 声明。因此,你可能希望通过 CSS 模块来共享 CSS 声明。

    然而,更常见的情况是,当你需要在多个 Svelte 组件中为元素使用相同的 CSS 声明时,你可能会遇到一个不太完美的组件结构。可能可以将元素抽象成一个 Svelte 组件。

我们已经看到了如何在 Svelte 组件内部和外部定义自己的样式;然而,有时采用他人编写的样式比设计自己的样式要容易得多。

在下一节中,我们将探讨如何在 Svelte 中使用流行的 CSS 框架 Tailwind CSS。

使用 Tailwind CSS 样式化 Svelte

Tailwind CSS 是一个以实用类为首要的 CSS 框架。它包含预定义的类,如 flexpt-4text-center,你可以在你的标记中直接使用这些类:

<div class="flex pt-4 text-center" />

我们将使用 Vite 的 Svelte 模板作为基础来设置 Tailwind CSS。如果你不熟悉设置 Vite 的 Svelte 模板,以下是一些快速设置步骤:

  1. 运行 Vite 设置工具:

    my-project-name containing the basic files necessary for a Svelte project.
    
  2. 进入 my-project-name 文件夹并安装依赖项:

    cd my-project-name
    npm install
    
  3. 依赖项安装完成后,你可以启动开发服务器:

    npm run dev
    

随着 Svelte 项目运行起来,让我们看看我们需要做什么来设置 Tailwind CSS。

设置 Tailwind CSS

Tailwind CSS 提供了一个 tailwindcss CLI 工具,使得设置变得更加简单。按照以下步骤操作:

  1. 要在 Svelte + Vite 项目中设置 Tailwind CSS,我们首先安装所需的依赖项:

    @tailwind directive – a Tailwind CSS directive, which you’ll see later.
    
  2. 安装完 tailwindcss 后,运行命令以生成 tailwind.config.jspostcss.config.js

    tailwind.config.js file keeps the configuration for Tailwind CSS. Tailwind CSS works by scanning template files for class names and generating the corresponding styles into a CSS file.
    
  3. tailwind.config.js 中指定我们的 Svelte 组件的位置,以便 Tailwind CSS 知道在哪里扫描 Svelte 组件以获取类名:

    module.exports = {
      content: [
        // all files ends with .svelte in the src folder
        "./src/**/*.svelte"
      ],
    };
    

    postcss.config.js 文件保存了 PostCSS 的配置。默认生成的配置目前是好的。

  4. ./src/main.css 中创建一个 CSS 文件以添加 Tailwind 指令:

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
  5. ./src/main.js 文件中导入新创建的 ./src/main.css

    import './src/main.css';
    

    这与之前看到的导入外部 CSS 文件类似。

  6. 启动 Vite dev 服务器:

    text-center and text-sky-400 Tailwind CSS classes onto the elements in your Svelte component.For example, head over to `./src/App.svelte` and add the following: `<h1 class="text-center` `text-sky-400">Hello World</h1>`
    

你刚刚创建了一个带有两个 Tailwind CSS 类 <h1> 元素:text-centertext-sky-400。这将使 <h1> 元素中的文本居中对齐,并呈现天蓝色。当你使用 Tailwind CSS 实用类名称在你的 Svelte 组件中时,这些类的样式声明会被生成并替换掉 @tailwind utilities 指令。

Tailwind CSS 能够从 class 属性和 class: 指令中提取类名,允许你静态或条件性地将 Tailwind CSS 样式应用到元素上:

<script>
  export let condition = false;
</script>
<h1 class="text-center">Center aligned title</h1>
<h1 class:text-center={condition}>
  Conditionally center aligned
</h1>

Tailwind CSS 包含了许多实用类:你可以在 tailwindcss.com/ 上了解更多关于 Tailwind CSS 的信息。

由于 Tailwind CSS 类是全局可用的,因此你可以将相同的 CSS 类应用到同一应用程序内的 Svelte 组件上,以保持相同的视觉和感觉。然而,如果你使用自己的样式声明这样做,你可能必须在组件之间重新指定相同的颜色或尺寸以保持相同的视觉。将来更改值也会成为问题,因为我们必须通过值搜索用法并更新它们。

CSS 自定义属性是解决这个问题的方案。它们允许我们在一个地方定义一个值,然后在其他多个地方引用它。让我们看看我们如何在 Svelte 组件中使用 CSS 自定义属性。

使用 CSS 自定义属性为主题化 Svelte 组件

让我们对 CSS 自定义属性进行快速知识检查:

  • 你可以像定义其他 CSS 属性一样定义 CSS 自定义属性,只是 CSS 自定义属性的名称以两个短横线开头:

    --text-color: blue;
    
  • 要引用 CSS 自定义属性,请使用 var() 函数:

    color: var(--text-color);
    
  • CSS 自定义属性像任何其他 CSS 属性一样具有级联性:

    .foo {
      --text-color: blue;
    }
    div {
      --text-color: red;
    }
    

    <div> 元素的 --text-color CSS 自定义属性的值是红色,除了具有名为 foo 的类的 <div> 元素。

  • CSS 自定义属性的值从其父元素继承:

    <div>
      <span />
    </div>
    

    如果前一个示例中 <div>--text-color 值是红色,那么如果没有其他 CSS 规则应用到 <span> 上,<span>--text-color 值也是红色。

定义 CSS 自定义属性

要为 Svelte 组件指定一组尺寸和颜色,我们可以在应用程序的根组件中定义它们作为 CSS 自定义属性:

<div>
 <ChildComponent />
</div>
<style>
 div {
  --text-color: #eee;
  --background-color: #333;
  --text-size: 14px;
 }
</style>

在前一个示例中,我们在组件的根元素(<div> 元素)处定义了 CSS 自定义属性。由于 CSS 自定义属性值从父元素继承,子元素和子组件内的元素从 <div> 元素继承值。

由于我们正在定义组件 <div> 元素的 CSS 自定义属性,因此不是 <div> 元素子代元素的元素将无法访问该值。

如果你希望为所有元素定义变量,即使是那些不是根组件根元素的子代元素,你可以在文档的根处使用 :root 选择器来定义 CSS 自定义属性:

<style>
  :root {
    --text-color: #eee;
    --background-color: #333;
    --text-size: 14px;
  }
</style>

对于 :root,你不需要使用 :global() 伪选择器,因为它始终指向文档的根,并且永远不会作用域到组件。

在 CSS Modules 中,:global() 伪选择器用于定义全局样式,这些样式适用于局部模块作用域之外。在 Svelte 中,当在组件的 <style> 块中使用时,它允许你定义不会作用域到组件的 CSS 规则,使它们在整个 Svelte 应用程序中的元素可用并适用。

作为在<style>块中定义 CSS 自定义属性的替代方案,你可以通过style属性直接在元素本身上定义它们:

<div style="--text-color: #eee; --background-color: #333; --text-size: 14px;">
  <ChildComponent />
</div>

如你所回忆,使用style:指令这样做可以使代码看起来更整洁:

<div
  style:--text-color="#eee"
  style:--background-color="#333"
  style:--text-size="14px">
  <ChildComponent />
</div>

要在子组件中引用该值,你使用var()函数:

<style>
  div {
   color: var(--text-color);
  }
</style>

使用 CSS 自定义属性的优点在于,我们可以动态地更改 CSS 自定义属性的值,引用该 CSS 自定义属性的元素样式将自动更新。

例如,我可以根据条件指定<div>--text-color值为#222#eee

<script>
  export let condition = false;
</script>
<div style:--text-color={condition ? '#222' : '#eee'} >
  <ChildComponent />
</div>

当条件为true时,var(--text-color)的值为#222,当条件变为false时,值变为#eee

如你所见,CSS 自定义属性使得同步元素样式变得容易得多。

现在,让我们看看使用 CSS 自定义属性的实际情况:创建深色/浅色主题模式。

示例 – 实现深色/浅色主题模式

深色模式是一种颜色方案,其中文本颜色较浅,背景颜色较深。深色模式背后的理念是在保持颜色对比度比率的条件下减少屏幕发出的光线,从而使内容仍然可读。来自设备的较少光线使得阅读更加舒适,尤其是在低光环境中。

大多数操作系统允许用户设置是否使用深色或浅色主题的偏好,并且主要浏览器支持prefers-color-scheme媒体查询来指示用户的系统偏好:

@media (prefers-color-scheme: dark) {
  /* Dark theme styles go here */
}
@media (prefers-color-scheme: light) {
  /* Light theme styles go here */
}

在我们开始之前,让我们确定所需的变量。

为了简化问题,我们只更改背景色和文本色,因此将是--background-color--text-color

可能你的应用程序还有其他颜色,例如强调色、阴影色和边框色,这些颜色在深色和浅色主题中需要不同的颜色。

由于这些颜色将被应用到各个地方,我们将在根元素上使用:root伪类来定义它们:

<style>
  @media (prefers-color-scheme: dark) {
    :root {
      --background-color: black;
      --text-color: white;
    }
  }
  @media (prefers-color-scheme: light) {
    :root {
      --background-color: white;
      --text-color: black;
    }
  }
</style>

现在,在我们的 Svelte 组件中,我们需要开始设置文本颜色以使用var(--text-color)

<style>
  * {
    color: var(--text-color);
  }
</style>

就这样;当系统偏好设置为深色主题时,文本颜色将是白色,当设置为浅色主题时,文本颜色将是黑色。

由于 CSS 自定义属性的继承特性,CSS 自定义属性的值将由设置了该值的最近父元素确定。

这为允许组件用户指定组件样式而无需通过更高特异性的 CSS 规则覆盖样式声明打开了大门。

允许用户更改组件的样式

假设你使用 CSS 在<style>块中为组件设置样式,如下所示:

<p>Hello World</p>
<style>
  p {
   color: red;
  }
</style>

如果你想从组件外部修改p元素的色彩,你需要了解以下内容:

  • 使用div :global(p)选择器来覆盖颜色;然而,由于我们不知道组件的实现细节,我们无法确定我们的选择器是否具有更高的特定性:

    <div>
      <Component />
    </div>
    <style>
      div :global(p) {
        color: blue;
      }
    </style>
    
  • 组件的元素结构

    要知道要覆盖哪个元素的色彩,我们需要了解组件的元素结构,以及我们想要更改颜色的文本所在的元素是否是段落元素。

    组件的 CSS 规则和元素结构不应成为组件的公共 API 的一部分。通过具有更高特定性的 CSS 规则覆盖组件的样式是不推荐的。对 CSS 规则或元素结构的小幅调整很可能会破坏我们的 CSS 规则覆盖。

    一种更好的方法是公开一个 CSS 自定义属性的列表,这些属性可以用来覆盖组件的样式:

    <p>Hello World</p>
    <style>
      p {
       color: var(--text-color, red);
      }
    </style>
    

    var()函数接受一个可选的第二个参数,它是如果第一个参数中的变量名不存在时的后备值。

    如果你使用组件而没有定义--text-color,那么段落的颜色将回退到红色。

    在以下代码片段中,段落的颜色是红色:

    <div>
      <Component />
    </div>
    
    

    这里,"display:contents"是为了确保额外的div不参与<Component />内容的布局。

    
    

如果我们在使用 CSS 自定义属性时指定后备值,我们可能会发现自己在重复后备值几次。如果我们打算更改后备值,这将变得麻烦。让我们看看我们如何对齐后备值。

对齐后备值

如果我们在多个元素中使用var(--text-color, red),你可能会很快意识到我们也应该定义一个 CSS 自定义属性作为后备值,以免我们重复多次使用该值,将来可能难以找到并替换所有这些值。

要定义另一个 CSS 自定义属性,你必须在组件的根元素上定义它。如果该值仅限于你的组件及其子组件,那么你不应该在文档根元素上通过:root定义 CSS 自定义属性:

<p>Hello World</p>
<style>
  p {
    --fallback-color: red;
    color: var(--text-color, var(--fallback-color));
  }
</style>

然而,这种方法要求我们在使用var(--text-color)的任何地方都使用var(--fallback-color)

一种稍微更好的方法是定义一个新的 CSS 自定义属性,如果已定义,则其值为--text-color,否则为红色作为后备:

<style>
  p {
   --internal-text-color: var(--text-color, red);
   color: var(--internal-text-color);
  }
</style>

这样,var(--internal-text-color)的值将始终被定义,并且使用单个 CSS 自定义属性对后续元素进行操作更为方便。

摘要

在本章中,我们介绍了六种不同的方法来样式化 Svelte 组件。那么,你知道你打算使用哪种方法来样式化你的 Svelte 组件吗?你现在应该知道选择最适合场景的方法。

我们首先了解了如何在 Svelte 项目中使用 Tailwind CSS。在开始时,需要一些初始设置来让 Tailwind CSS 启动并运行,但像 Tailwind CSS 这样的 CSS 框架通常自带预定义的 CSS 类,大多数情况下,你使用 class 属性或 class: 指令来应用它们。

最后,我们介绍了如何使用 CSS 自定义属性来为主题 Svelte 组件,以及如何允许组件用户自定义组件的样式。现在,你可以在允许他人使用不同于你创建的默认样式的条件下创建和分享 Svelte 组件。

在下一章中,我们将探讨如何管理 Svelte 组件的 props 和 states。

第三章:管理属性和状态

在网络开发的世界里,有效地管理数据至关重要。无论是组件之间的信息流还是组件的内部状态,适当的数据管理是功能性和响应式网络应用程序的基石。

在本章中,我们将深入研究在 Svelte 应用程序中管理属性和状态的核心概念。首先,我们将明确 Svelte 中的属性和状态是什么,为理解更高级的主题打下基础。然后,我们将探讨绑定的概念,这是 Svelte 中用于保持状态和元素值或组件属性同步的功能。

然后,我们将探讨组件内的数据流,突出单向数据流和双向数据流之间的差异以及它们为什么重要。接下来,我们将讨论如何使用 Svelte 的响应式声明从属性派生状态。最后,我们将提供管理复杂派生状态的技巧,并解释如何根据这些派生状态更新属性。

到本章结束时,你将牢固地理解如何在 Svelte 组件中管理数据,并掌握实用的技巧和策略来有效地应对常见挑战。

在本章中,你将学习以下内容:

  • 定义属性和状态

  • 理解绑定

  • 单向与双向数据流

  • 使用响应式声明从属性派生状态

  • 管理复杂的派生状态

  • 使用派生状态更新属性

在我们开始讨论属性和状态之前,让我们首先定义在 Svelte 中属性和状态是什么。

技术要求

你可以在 GitHub 上找到本章中使用的所有代码示例:github.com/PacktPublishing/Real-World-Svelte/tree/main/Chapter03

定义属性和状态

在 Svelte 中,属性和状态都用于在组件内管理数据。属性是一种将数据从父组件传递到子组件的方式。这使得子组件更加灵活和可重用,因为它可以根据需要从父组件获取不同的数据。

另一方面,状态是在组件内部初始化和管理的内部数据,与从外部来源接收的属性不同。状态允许组件自包含和模块化。

定义属性

让我们从属性开始。在 Svelte 中,属性使用 export 关键字定义。当你在一个 Svelte 组件中导出一个变量时,它就变成了一个可以传递数据的属性,你可以从父组件传递数据。

这里有一个简单的例子:

<!-- file: Child.svelte -->
<script>
  export let message;
</script>
<h1>{message}</h1>

在前面的代码片段中,我们在名为 Child.svelte 的文件中定义了一个 Svelte 组件。在 Svelte 组件中,message 是一个属性。你可以从父组件传递数据到 message,如下所示:

<!-- file: Parent.svelte -->
<script>
  import Child from './Child.svelte';
</script>
<Child message="Hello, World!" />

在前面的代码片段中,我们在名为 Parent.svelte 的文件中定义了另一个 Svelte 组件。在这个组件中,我们导入并使用来自 Child.svelteChild 组件。由于 Parent 组件包含了 Child 组件,因此 Parent 组件被认为是导入的 Child 组件的父组件。

在父组件中,你可以通过 <Child /> 组件的 message 属性传递 "Hello, World!" 来设置子组件的 message 道具,如前面的代码片段所示。

总结来说,道具是通过 export 关键字定义的,它们的值是从父组件传递到子组件的。

定义状态

接下来,让我们看看状态。状态是任何在组件内部使用和管理的数据。它不像道具那样从父组件传递进来。相反,它是在组件内部定义的。

这里有一个说明状态的例子:

<!-- file: Counter.svelte -->
<script>
  let count = 0;
  function increment() {
    count += 1;
  }
</script>
<button on:click={increment}>Click me</button>
<p>{count}</p>

在这个例子中,count 是一个状态变量。它不是作为道具传递的,而是在 Counter 组件内部定义和管理的。当你点击按钮时,会调用 increment 函数,该函数修改 count 状态。

总结来说,道具是从父组件传递到组件的变量,而状态是组件内部管理的数据。

道具与状态

如果你仔细观察,道具和状态都代表数据。它们之间的区别取决于你考虑的组件的上下文。

例如,让我们考虑两个组件,组件 A 和组件 B

让我们从组件 A 开始:

<!-- A.svelte -->
<script>
  export let height;
</script>

在组件 A 中,我们定义了一个名为 height 的道具。

现在让我们看看组件 B

<!-- B.svelte -->
<script>
  import A from './A.svelte';
  let height = 0;
</script>
<A height={height} />

在组件 B 中,我们定义了一个名为 height 的状态,并将其值作为道具传递给组件 A

从组件 B 的角度来看,height 被视为状态,但从组件 A 的角度来看,它被视为道具。一个变量是道具还是状态取决于它被查看的组件的上下文。在本质上,它们基本上是同一件事。

在这个例子中,由于 JavaScript 以值的方式传递原始变量,可能不会立即明显地看出组件 A 和组件 B 中的 height 变量指向同一件事。

然而,如果我们定义一个对象作为状态并通过道具传递给另一个组件,那么就会变得明显,一个组件中的状态和另一个组件中的道具都指向同一个对象引用。

让我们修改我们的例子来说明这一点:

<!-- A.svelte -->
<script>
  export let height;
  setInterval(() => console.log('A:', height), 1000);
</script>
<!-- B.svelte -->
<script>
  import A from './A.svelte';
  let height = { value: 100 };
  setInterval(() => console.log('B:', height), 1000);
</script>
<A height={height} />

在这个代码片段中,我添加了一个 setInterval 函数,每秒钟在两个组件中打印出 height 变量的值。在组件 B 中,我将状态 height 修改为对象。因为 JavaScript 中的对象是通过引用传递的,所以作为属性传递给组件 A 的组件 B 中的状态 height 也是通过引用传递的。这意味着组件 A 中的 height 属性引用的是与组件 B 中的状态 height 相同的对象。

如果我们在组件 B 中添加一个 <button> 元素来修改 height 对象,如下所示,你将能够看到组件 AB 都会在控制台中打印出相同的更新后的 height 变量值。这是因为它们打印的是相同对象引用的值:

<button on:click={() => { height.value += 10; }} />

点击前面代码片段中的按钮会导致控制台打印出 A: { value: 110 }B: { value: 110 }。这表明组件 AB 中的 height 变量引用的是相同的对象引用。当组件 B 中的 height.value 更改为 110 时,这个变化也会反映在组件 A 中的 height 变量上。

现在我们已经了解了 Svelte 中的 props 和状态是什么,接下来让我们谈谈绑定。

理解绑定

Svelte 中的绑定允许你保持组件状态值与 <input /> 元素值的同步。如果状态发生变化,输入会更新;反之,如果输入发生变化,状态也会更新。

以下代码片段是创建 Svelte 中绑定的一个示例:

<script>
  let name = "John";
</script>
<input bind:value={name} />

绑定是通过 bind: 指令创建的。在前面代码片段中,输入元素的值被绑定到 name 变量。当你输入时,name 变量将自动更新。反之,当你更改 name 变量的值时,输入元素的值也会自动更新。

如所示,绑定创建了一个双向数据流,使得数据变化可以从元素传播到组件状态,以及从组件状态传播到元素。

之前的例子演示了在元素上的绑定,但绑定也可以在组件上工作。你可以使用 bind: 指令将组件的属性与你的组件状态链接起来,如下面的代码所示:

<script>
  import Profile from './Profile.svelte';
  let name = "John";
</script>
<Profile bind:username={name} />

在前面的代码片段中,我们将 <Profile> 组件的 username 属性绑定到 name 状态变量。当你更新 name 状态变量时,username 属性的值将自动反映新的值;反之,如果你在 <Profile> 组件内部更新 username 属性的值,name 状态变量的值也会自动更新以匹配。

为了进一步演示这种行为,让我们对代码进行一些小的修改。这是组件的更新版本:

<script>
  import Profile from './Profile.svelte';
  let name = "John";
</script>
<p>Name from App: {name}</p>
<Profile bind:username={name} />
<p> element and a <button> element. The <p> element shows the value of the name state variable, and the <button> element, when clicked, updates the value of the name state variable to Svelte. Due to the binding, when the button is clicked, it will also update the username props in the <Profile> component.
Here’s the updated version of the `<``Profile>` component:

Profile 中的名称:{username}

<button on:click={() => username = "World"}>从个人资料更新


 In this code snippet, we are looking at the `<Profile>` component. This component receives a prop called `username`, whose value is displayed inside a `<p>` element. We’ve also added a button, and when clicked, it will update the value of the `username` prop to `"World"`.
Because of the binding we established in the parent component, any change to the `username` prop in this `<Profile>` component will also update the `name` state variable in the parent component.
Indeed, if you click on the button in the parent component, both the `name` state variable in the parent component and the `username` prop in the `<Profile>` component will update to `"Svelte"`. On the other hand, if you click on the button in the `<Profile>` component, both the `name` state variable in the parent component and the `username` prop in the `<Profile>` component will change to `"World"`. This is the power of two-way binding in Svelte, allowing you to easily synchronize data between parent and child components.
When we talk about binding in Svelte, we often refer to *two-way data binding*. But what exactly does *two-way* mean? And is there such a thing as *one-way* data binding? Let’s delve deeper into the concept of data flow to clarify these terms.
One-way versus two-way data flow
When you pass data from one component to another component either through props or binding, data flows from one component to another component. The term data flow refers to how data is passed or transmitted between components or elements within a web application.
Understanding data flow is important when designing an application’s architecture, as it helps to establish clear lines of communication between components and determine how information is shared and updated throughout the system.
Data flow can be unidirectional (one-way) or bidirectional (two-way), depending on how data is transferred between components.
In one-way data flow, data moves in a single direction, from a parent to a child component, or from a component to a DOM element. This unidirectional flow is achieved through component props or DOM element attributes.
For example, consider the following code snippets featuring two components, component `A` and component `B`.
In component `B`, we define a prop named `value`:


 In component `A`, we import component `B` and pass the value of a variable named `data` to the prop of component `B`:


 Here, the value of `data` in component `A` is passed to the `value` prop in component `B`, illustrating the data flow from component `A` to component `B`.
If you draw up the data flow in a diagram, this is what a one-way data flow looks like:
![](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/rlwd-svlt/img/B18887_03_1.jpg)

Figure 3.1: One-way data flow
The boxes show the data state and value prop, and the arrow shows how changing one value will affect another.
In one-way data flow, changes to data in the parent component automatically propagate to the child component, but the reverse is not true.
Building on the example with component `A` and component `B`, if the value of the `data` variable in component `A` changes, this change would automatically update the `value` props in component `B`. However, any changes made directly to the `value` props in component `B` would not affect the `data` variable in component `A`. Data changes only flow from component `A` to component `B`, but not in the reverse direction.
Having a one-way data flow makes the application easier to reason about and debug, as data changes follow a predictable path. For example, when the `data` variable in component `A` changes unexpectedly, isolating the issue becomes more straightforward. Because of the one-way data flow, we know that any changes to the `data` variable in component `A` will only originate from component `A`, not from component `B`.
On the other hand, two-way data flow allows data to flow in both directions, enabling automatic updates between the parent and child components, or between a component and DOM elements. Two-way data flow in Svelte is achieved through component or DOM element bindings.
For example, consider the following code snippets featuring two components, component `C` and component `D`.
In component `D`, we define a prop named `value`:


 In component `C`, we import component `D` and bind the value of a variable named `data` to the prop of component `D`:


 Here, the value of `data` in component `C` is bound to the `value` prop in component `D`, illustrating the two-way data flow between component `C` to component `D`.
Here is a diagram showing how the data flows in a two-way data flow:
![](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/rlwd-svlt/img/B18887_03_2.jpg)

Figure 3.2: Two-way data flow
In a two-way data flow, when you change the data in the parent component, the child component is automatically updated; conversely, changing the data in the child component automatically updates the parent.
Building on the example with component `C` and component `D`, if the value of the `data` variable in component `C` changes, this change would automatically update the `value` props in component `D`. Conversely, unlike one-way data flow, any changes made directly to the `value` props in component `D` would update the `data` variable in component `C`. Data changes flow from component `C` to component `D` as well as from component `D` to component `C`.
While this bidirectional flow of data can make it easier to keep data in different components synchronized, it can also introduce complexity and make the application harder to debug, since data changes can originate from multiple sources.
For example, when the `data` variable in component `C` changes unexpectedly, isolating the issue becomes more complex. Because of two-way data flow, any changes to the `data` variable in component `C` could originate from component `C`, as well as from component `D`. This complexity can escalate further if the `value` prop of component `D` is also bound to the prop of another component.
In summary, one-way data flow offers predictability and easier debugging, while two-way data flow makes it much easier to synchronize data across multiple components but at the cost of added complexity.
Now that we’ve delved into both one-way and two-way data flows via props, let’s explore how to create state variables that derive their values from props.
Deriving states from props with a reactive declaration
It’s common in Svelte to create new state variables based on the values of props.
For instance, a `<DateLabel />` component might accept a `date` value as a prop and display a formatted date inside a `<label>` element. To use the `<DateLabel>` component, you might write the following:

<DateLabel date={new Date(2023,5,5)} />


 To display the date as formatted text, you could first define a variable named `label`, deriving its value from the `date` prop:


 In this code snippet, we defined a variable called `label` and derived its value from the `date` prop using the `toLocaleDateString()` method. This variable is then used inside a `<label>` element to display the formatted date.
In the preceding code snippet, the `label` variable is initialized when the `<DateLabel>` component is first created. However, if the `date` prop changes after the component has been initialized, the `label` variable won’t update to reflect the new value. This is not the intended behavior, as the `<DateLabel>` component should always display the most up-to-date formatted date based on the latest `date` prop.
To solve this issue, you can use Svelte’s reactive declarations to keep the `label` variable updated whenever the `date` prop changes.
Svelte’s reactive declarations utilize a special `$:` syntax to mark a statement as reactive. This means that whenever the variables used in that statement change, the statement itself will be re-run.
Let’s modify our component code to use Svelte’s reactive declaration:


 In this code snippet, by modifying the declaration of `label` to a reactive declaration, the component will automatically re-compute the `label` variable whenever the `date` prop is modified, ensuring that the `<DateLabel>` component is always displaying the most current formatted date.
So, how does Svelte know when a reactive declaration statement should be re-run?
Svelte re-runs a reactive declaration statement whenever any of its dependent variables change. The Svelte compiler identifies these dependencies by analyzing the statement.
For example, in the reactive declaration `$: label = date.toLocaleDateString();`, Svelte recognizes that the dependency for this statement is the `date` variable. Therefore, whenever the `date` changes, the statement will re-run and update the value of the `label` variable.
A good rule of thumb for identifying dependencies in a reactive declaration is to look for any variable on the right side of the equal sign (`=`). These variables are considered dependencies of the reactive declaration.
You can include multiple dependencies within a single reactive declaration. For instance, let’s say we want to add a new `locale` prop to the `<DateLabel>` component. To use this new prop, you might write something like this:

<DateLabel date={new Date(2023, 5, 5)} locale="de-DE" />


 In this code snippet, we pass in a new `locale` prop with the value `de-DE` to format the date in German. To accommodate this new `locale` prop, we’ll need to modify our `<DateLabel>` component as follows:


 In this updated code, the reactive declaration `$: label = date.toLocaleDateString(locale);` now has two dependencies: `date` and `locale`. Svelte will automatically re-run this statement whenever either of these variables changes, ensuring that the `label` value stays up to date with the latest `date` and `locale` props.
Now that we’ve covered the basics of props, bindings, states, and derived states, it’s crucial to note that as components become more complex, managing these elements can quickly become overwhelming. In the next section, we’ll explore some tips for effectively managing complex derived states to keep them manageable.
Managing complex derived states
As your Svelte application grows more complex, it will likely involve a greater number of interconnected components with multiple props and derived states. When dealing with this complexity, tracking updates and changes can become a complex task. Each prop or state change can affect other parts of your component, making it challenging to manage and predict how your component will behave.
To make this easier, here are some guidelines to consider:

*   *Maintain one-way data flow for* *derived states*

    While it’s possible to derive state from props and other states, it’s crucial to maintain a one-way data flow to simplify both debugging and understanding. Consider the following Svelte example:

    ```

    <script>

    export let valueA;

    export let valueB;

    $: valueC = valueA + 5;

    $: valueD = valueB + valueC;

    $: valueC = Math.min(valueC, valueD / 2);

    </script>

    在观察数据流时,我们可以看到 valueC 依赖于 valueA,valueD 依赖于 valueB 和 valueC,然后 valueC 又反过来依赖于 valueD。因此,valueC 的实际计算方式并不明确,它可能是 valueA + 5 或 valueC 和 valueD / 2 中的最小值。这种复杂性使得代码难以理解,并增加了出现错误的可能性。

    ```js

     *   *Group similar* *logic together*

    Consider this tip a stylistic suggestion. When dealing with multiple reactive declarations, it’s beneficial to group those that are related together. Utilize blank lines to create a visual separation between unrelated reactive declarations. This not only improves readability but also aids in code maintenance.

    It is worth noting that the Svelte compiler takes care of execution order based on dependencies regardless of how you arrange your declarations. For example, the following two code snippets will behave identically due to Svelte’s handling of dependencies:

    ```

    <script>

    export let a;

    $: b = a * 2;

    $: c = b * 2;

    </script>

    b 在 c 声明之前被声明为响应式。然而,它的行为与以下片段相同,其中 c 在 b 声明之前。让我们看看下一个:

    ```js

    ```

    <script>

    导出 let a;

    $: c = b * 2;

    $: b = a * 2;

    </script>

    ```js

    The Svelte compiler analyzes the dependencies of each declaration and executes them in the correct order. In this case, it evaluates `b = a * 2` before `c = b * 2` since the latter depends on the value of `b`, established by the former declaration.

     *   *Avoid reassigning* *props directly*

    It might be tempting to modify the value of a prop directly, especially when you want to transform its value or provide a default fallback. For example, you might consider writing something like this to set a default value for an undefined prop:

    ```

    <script>

    export let data;

    $: data = data ?? 100;

    </script>

    相反,最好声明一个新的状态变量来管理这种行为:

    ```js

    ```

    <script>

    export let data;

    $: dataWithDefault = data ?? 100;

    </script>

    ```js

    In this improved example, we introduce a new variable, `dataWithDefault`, which takes on either the value of the `data` prop or a default value of `100` if `data` is undefined. This approach makes the component’s behavior more transparent and easier to debug.

     *   *Be cautious when updating* *derived states*

    Modifying a derived state directly can introduce inconsistencies, particularly when that state is based on props or other state variables.

    Consider the following example:

    ```

    <script>

    export let value;

    $: double = value * 2;

    $: triple = value * 3;

    </script>

    <input bind:value={double} type="number" />

    <input bind:value={triple} type="number" />

    ```js

    In this example, we have a single prop named `value` and two derived states, `double` and `triple`, which are two and three times the value of the `value` prop respectively. The component features two input boxes, each bound to `double` and `triple` using two-way binding.

    Here, `triple` can be modified in two ways: either by updating the `value` prop, which will keep `triple` at three times the value of `value`, or by directly changing the value in the input box, thereby directly altering `triple`.

    If you type into the input box bound to `triple`, you’ll find that its value diverges from being strictly three times the `value` prop. This inconsistency arises because `triple` is now subject to changes from multiple sources, causing it to go *out of sync* with the original `value`.

    If you were to map out a diagram illustrating the data flow between `value` `double` and `triple`, you’d get the following diagram:

![](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/rlwd-svlt/img/B18887_03_3.jpg)

Figure 3.3: Data flows between props, states, and the input elements
Notice that both `double` and `triple` have two incoming arrows, suggesting multiple pathways for changing their values. Conversely, `value` has no incoming arrows, indicating that modifying `triple` alone would cause `value` and `double` to go out of sync.
Therefore, it is recommended to refrain from manually updating derived states, as this complicates debugging and makes the component’s behavior harder to reason about.
If you wish to modify the input while maintaining the synchrony between `value`, `double`, and `triple`, a solution will be discussed in the following section.
By keeping these tips in mind, you can better manage complex derived states and make your components more maintainable and easier to understand.
In our last tip, we highlighted that updating the derived states can lead to inconsistencies between states and props and noted that there’s a solution to modify the input while keeping everything in sync. That solution is what we will explore next.
Updating props using derived states
In an attempt to synchronize the `value` prop with changes to the input bound to `triple`, one might be tempted to add another reactive declaration. This declaration would update the `value` prop to be one-third of `triple` whenever `triple` changes. Here is the proposed modification:


 As we discussed earlier, it’s best practice to maintain a one-way data flow for derived states to simplify debugging and data management. Indeed, the Svelte compiler flags the preceding code snippet for cyclical dependencies. This is because `double` is derived from `value`, and `value` is in turn dependent on `double`.
However, Svelte’s compiler determines dependency relationships based solely on the reactive declarations. By refactoring the code to make these relationships less obvious, you can bypass the compiler’s cyclical dependency check. Here’s a refactored version of the code that does just that:


 In the provided code snippet, we’ve shifted the equations `value = double / 2` and `value = triple / 3` into separate functions named `updateValueFromDouble` and `updateValueFromTriple`. This change lets us evade Svelte’s compiler warning about cyclical dependencies.
However, there’s a catch. If you try altering the `triple` input, it updates `value` but doesn’t refresh `double`. This happens because Svelte avoids infinite update loops. Changing `triple` would set off a chain reaction—updating `value`, then `double`, then back to `value`, and so on.
This is how the data flow looks right now:
![](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/rlwd-svlt/img/B18887_03_4.jpg)

Figure 3.4: Data flows between props, states, and the input elements
As you can see in the diagram, we update the value of `value`, `double`, and `triple` through reactive declarations, creating a loop in the data flow, indicated by the bold arrows.
Therefore, using derived states to update their original properties via reactive declarations isn’t advisable.
A better approach to keep `value`, `double`, and `triple` in sync is to establish value as the single source of truth. Since both `double` and `triple` are derived from `value`, any changes to the input should first update `value`. This, in turn, triggers the reactive declarations to automatically recalculate `double` and `triple`.
Here’s the updated code:

<input value={double} type="number" on:change={e => updateValueFromDouble(e.target.value)} />

<input value as the sole source of truth. Instead of binding the input elements directly to double and triple, we’ve added event listeners that update value based on user input. This change automatically updates double and triple through reactive declarations, which then refresh the displayed values in the input fields.

通过更新的代码,数据流现在已简化,如下面的图所示:

图片

图 3.5:props、states 和输入元素之间的数据流

doubletriple的值直接从value派生,然后填充输入字段。当你修改输入时,它会直接改变value,进而自动更新doubletriple,以及输入字段本身。

因此,这是通过关注数据流并保持单一事实来源来维护valuedoubletriple同步状态的方法。

摘要

理解如何有效地处理 props 和 state 对于创建健壮的 Svelte 应用至关重要。本章向您展示了 Svelte 如何使用 props、bindings 和 reactive declarations 来促进组件间的数据传递和状态变化。

在组件内部关注数据流至关重要。拥有统一和有序的数据流使得代码更容易跟踪和调试。良好的数据管理为轻松构建更复杂和动态的应用铺平了道路。

在下一章中,我们将探讨如何组合 Svelte 组件来构建更复杂的应用。


第四章:组合组件

随着您的应用程序的增长,将所有逻辑都塞入单个组件变得不切实际。您需要将应用程序拆分为更小的、模块化的组件,并将它们组装成更复杂的应用程序。

在本章中,我们将探索各种有效组合组件的技术。我们将首先检查如何使用插槽向组件注入自定义内容。然后,我们将讨论如何在组件内条件性地渲染不同的 HTML 元素。我们还将深入研究递归组件,这对于显示嵌套或分层数据非常有用。

我们将通过实际示例引导您了解每个主题,确保您学习的技巧既实用又适用于现实世界的场景。到本章结束时,您将拥有一套更丰富的策略来构建您的 Svelte 应用程序中的组件。

在本章中,您将学习以下内容:

  • 从父组件的角度操纵子组件的外观

  • 通过插槽传递动态内容

  • 渲染不同的 HTML 元素和组件类型

  • 为递归数据创建递归组件

  • 容器/表现模式

让我们从探索我们可以操纵和控制子组件内容的各种方式开始。

技术要求

本章中所有的代码都可以在github.com/PacktPublishing/Real-World-Svelte/tree/main/Chapter04找到。

操纵子组件的外观

当您组合多个组件时,您需要管理每个子组件的外观和行为。尽管子组件处理自己的显示和逻辑,但它仍然提供了一些控件来调整其外观和行为。从父组件的角度来看,您将想要协调这些子组件以实现所需的总体功能。

在本节中,我们将探索控制子组件外观的各种方法,从最常用到最不常用的方法。了解这些选项将使您拥有制作既灵活又有效的组件的工具。

控制子组件外观的选项列表包括以下内容:

  • 通过 props 控制:这可能是在子组件的行为和外观上产生影响的最直接方式。通过从父组件传递 props 到子组件,您可以使子组件高度动态并对外部变化做出响应。

    为了演示父组件如何使用 props 在 Svelte 中控制内容,请考虑以下代码示例:

    <script>
      import Child from './Child.svelte';
      let message = 'Hello from Parent';
    </script>
    <Child message={message} />
    Child component and passed the message prop to it. The child component, implemented in the following code snippet, then uses this message prop to display text within a <p> element:
    
    <script>
      export let message;
    </script>
    <p>{message}</p>
    

    正如你所见,父组件可以通过修改message属性值的值来指定Child组件中显示的文本。控制属性是一种简单而有效的方式来操纵子组件的内容。如果你对属性感兴趣,我们在第三章中对此进行了广泛讨论。

  • parent组件:

    <script>
      import Child from './Child.svelte';
      import { setContext } from 'svelte';
      setContext('message', 'Hello from parent');
    </script>
    <Child />
    

    在之前的代码中,我们使用setContext创建了一个名为message的上下文,其值为'Hello from parent',然后导入并使用了Child组件,而没有向其发送任何属性。

    以下是为Child组件的代码:

    <script>
      import { getContext } from 'svelte';
      const message = getContext('message');
    </script>
    <p>{message}</p>
    

    在这里,使用getContext读取message上下文值,然后在一个<p>元素中显示。如所示,父组件可以通过更改上下文值来影响子组件的<p>元素中的文本。

    若要深入了解 Svelte 的上下文功能,你可以参考第八章,我们将更详细地探讨这个主题。

  • 控制样式:操纵子组件的外观并不仅仅是控制传递给它的数据。它还涉及到调整或修改其样式。

    你可以通过 CSS 自定义属性来修改组件样式。这种方法提供了更大的设计灵活性,并确保子组件能够适应父组件或更广泛应用程序中的各种上下文或主题。

    若要深入了解如何使用 CSS 自定义属性来更改组件样式,请参阅第二章

  • 通过插槽传递动态内容:Svelte 的插槽功能允许你将自定义内容插入子组件的特定区域。这提供了一种灵活的方式来更好地控制组件的内容,而无需修改其内部行为。

    在下一节中,我们将讨论插槽及其使用方法。

正如你所见,有各种方式来塑造子组件的外观和行为。当我们在一个父组件内组合不同的组件时,目标是使它们以协调的方式协同工作。你可以使用所讨论的方法的组合来实现这一点。

大多数这些方法将在单独的章节中介绍,在下一节中,我们将关注如何通过传递内容通过插槽来动态改变子组件的外观。

通过插槽传递动态内容

在构建复杂应用程序时,一种大小并不总是适合所有情况。在组件的模块化和定制灵活性之间取得平衡是至关重要的。

以一个通用的Card组件为例。有时你可能希望为特定用例包含特殊的标题、独特的列表或自定义页脚。几乎不可能预见所有需求,因此设计既模块化又可维护的组件,同时仍然允许定制,是至关重要的。

这正是 Svelte 槽位功能大放异彩的地方。Card 组件旨在包含所有可能的功能,目标是简单、干净的基座,可以通过组合进行增强。这种方法允许您根据需求的变化,拼凑出更复杂、定制的组件。

在 Svelte 组件中,一个 <slot> 元素是组件内的一个占位符,您可以在其中注入来自父组件的任何类型的内容。以下是您如何在 Svelte 组件中定义槽位的方法:

<!-- filename: Card.svelte -->
<div class="card">
  <slot></slot>
</div>

在前面的代码片段中,我们在 <div> 元素内部定义了一个 <slot> 元素。任何来自父组件的动态内容都将插入到 <div> 元素中。

现在,要将父组件中的动态内容传递到子组件的槽位中,您需要将该内容放置在子组件的开闭标签之间:

<script>
  import Card from './Card.svelte';
</script>
<Card>
  <h1>Special headline</h1>
</Card>

在此代码片段中,我们在 <Card> 组件的开闭标签之间传递了一个 <h1> 元素。这实际上用我们提供的 <h1> 元素替换了 Card 组件中的 <slot> 元素。

可能存在不需要从父组件将动态内容传递到子组件槽位的情况。对于这些情况,Svelte 允许您在 <slot> 元素内定义默认内容,当父组件没有提供自定义内容时提供回退选项。

提供默认槽位内容

要为您的 <slot> 元素提供默认内容,您可以将它们放置在 <slot> 元素内部,如下所示:

<div class="card">
  <slot>
    <div>this is the default content</div>
  </slot>
</div>

在前面的代码片段中,我们定义了 <div>,其中包含 <slot> 元素内的文本 This is the default content。这作为槽位的默认内容。

当您在父组件中使用此 Card 组件且不提供槽位内容时,如以下代码片段所示,默认文本将自动出现:

<script>
  import Card from './Card.svelte';
</script>
<Card />

通过利用槽位的默认内容,您可以创建更灵活的 Svelte 组件,当没有提供自定义内容时,提供合理的回退选项。

在我们迄今为止看到的示例中,我们被限制为仅将来自父组件的动态内容插入到一个位置。但如果我们想要多个动态内容插入点怎么办?让我们探索如何实现这一点。

有多个带有命名槽位的槽位

槽位非常出色,您也不限于单个槽位。您可以在组件中定义多个槽位并为它们命名,以针对特定的动态内容区域。

您可以使用 <slot> 元素上的 name 属性来为槽位命名,如下面的代码所示:

<!-- filename: Card.svelte -->
<div class="card">
  <header>
    <slot name="header"></slot>
  </header>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

在前面的代码片段中,我们定义了两个命名槽位,一个命名为 "header",另一个命名为 "footer"

要将动态内容针对这些命名槽位,您需要在父组件中使用 <svelte:fragment> 元素:

<script>
  import Card from './Card.svelte';
</script>
<Card>
  <svelte:fragment slot="header">
    <h1>Special headline</h1>
  </svelte:fragment>
  <svelte:fragment slot="footer">
    Custom footer
  </svelte:fragment>
</Card>

在这里,我们向 Card 组件传递了两个 <svelte:fragment> 元素。第一个具有槽位属性设置为 "header",第二个槽位属性设置为 "footer"

slot属性中指定的名称将与Card组件内的命名插槽相对应。在<svelte:fragment slot="header">内的内容替换了Card组件中的<slot name="header">。同样,在<svelte:fragment slot="footer">内的内容取代了组件中的<slot name="footer">

有时,你可能希望父组件的动态内容了解子组件中的数据。为了处理这种情况,Svelte 提供了一个称为插槽属性的功能,允许你从子组件将数据传递到插槽中的内容。让我们看看它是如何工作的。

通过插槽属性传递数据

你可以使用插槽属性,如下所示,从子组件将数据传递回父组件中的动态内容:

<!-- filename: Card.svelte -->
<div class="card">
  <header>
    <slot name="header" width={30} height={50}></slot>
  </header>
</div>

在代码示例中,我们除了传递了用于命名插槽的name属性外,还向<slot>元素传递了两个额外的属性。这些额外的属性,widthheight,作为插槽属性;它们的值可以在创建动态内容时在父组件中访问。

这里有一个示例,说明你如何在使用<svelte:fragment>时使用这些插槽属性值:

<script>
  import Card from './Card.svelte';
</script>
<Card>
  <svelte:fragment slot="header" let:width let:height>
    <h1>Dimension: {width} x {height}</h1>
  </svelte:fragment>
</Card>

在前面的代码片段中,你可以看到我们在<svelte:fragment>元素内部使用了let:widthlet:height。这些被称为let:绑定。

let:绑定使我们能够访问由子Card组件提供的widthheight插槽属性。然后,这些宽度和高度的值在<h1>元素中使用,以显示尺寸。这样,我们可以根据来自子组件的数据动态调整父组件中的内容。

现在我们已经涵盖了如何通过插槽传递动态内容、如何定义多个命名的插槽以及如何将子组件中的数据传递给父组件中的动态插槽内容,你现在已经具备了创建更灵活和可重用 Svelte 组件的能力。

无渲染组件

使用插槽组合组件的许多常见模式之一是无渲染组件模式。这种技术允许你构建纯函数组件,将展示细节留给父组件。我们专门在第十一章中深入探讨了这一迷人的主题。

现在我们已经探讨了如何通过插槽传递动态内容,接下来让我们深入了解 Svelte 的另一个令人兴奋的特性,它允许你动态渲染不同的元素或组件类型。

渲染不同的 HTML 元素或组件类型

在任何动态应用程序中,都会有一个时刻你需要比静态组件或元素提供的更多灵活性。假设你在运行时才知道需要渲染的元素或组件的类型呢?

让我们假设您正在构建一个表单生成器,表单字段的类型——无论是 <Input><Checkbox> 还是 <Select>——由动态数据决定。您如何在这些组件之间无缝切换,尤其是当它们共享同一组属性时?

一个简单的方法是使用 Svelte 的 {#if} 块有条件地渲染所需的组件。以下是一个示例代码片段:

<script>
  import Input from './Input.svelte';
  import Checkbox from './Checkbox.svelte';
  import Select from './Select.svelte';
  let type = "input"; // Could be "checkbox" or "select"
</script>
{#if type === "input"}
  <Input value={value} onChange={onChange} />
{:else if type === "checkbox"}
  <Checkbox value={value} onChange={onChange} />
{:else}
  <Select value={value} onChange={onChange} />
{/if}

在代码片段中,我们使用 {#if} 块来选择三种不同的组件类型,并将相同的属性值传递给每个组件。如果您发现自己需要管理更多组件类型,这可能导致一系列冗长且难以维护的条件块。

有没有更有效的方法来处理这个问题?

Svelte 提供了两个专用元素,<svelte:element><svelte:component>,正是为了这个目的。<svelte:element> 元素允许您根据变量数据动态创建不同的 HTML 元素类型,而 <svelte:component> 元素允许您动态渲染不同的 Svelte 组件类型。

这里是如何使用 <svelte:component> 重新编写上一个示例的:

<script>
  import Input from './Input.svelte';
  import Checkbox from './Checkbox.svelte';
  import Select from './Select.svelte';
  let type = "input"; // Could be "checkbox" or "select"
  let DynamicComponent;
  if (type === "input") {
    DynamicComponent = Input;
  } else if (type === "checkbox") {
    DynamicComponent = Checkbox;
  } else {
    DynamicComponent = Select;
  }
</script>
<svelte:component
  this={DynamicComponent}
  value={value}
  onChange={onChange}
DynamicComponent variable is used to hold the type of component that will be rendered. This component type is then passed to the this attribute within the <svelte:component> element. The <svelte:component> element also accepts other props such as value and onChange.
With the preceding code, what happens is that `<svelte:component>` renders the designated component stored in the `this` attribute, simultaneously forwarding any props passed to `<svelte:component>` to this dynamically rendered component. For example, if the value of `DynamicComponent` is the `Select` component, then the preceding code is effectively the same as this code:

posted @ 2025-10-24 10:06  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报