发布于 2026-01-06 6 阅读
0

学习 Svelte:将番茄钟计时器和任务与 Props 和 Stores 连接起来 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

学习 Svelte:使用属性和存储连接番茄钟计时器和任务

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

本文最初发表于Barbarian Meets Coding 网站。您通常可以在那里和Twitter上找到我。

Svelte是一个现代 Web 框架,它采用了一种全新的 Web 应用构建方式,将大部分工作从运行时转移到编译时。作为一种编译器优先的框架,Svelte 能够实现一些其他框架无法实现的有趣功能,例如在运行时从应用程序中“消失”,或者允许以组件为中心的开发模式,使 HTML、JavaScript 和 CSS 能够以非常符合 Web 标准的方式共存于同一个 Svelte 文件中。

在本系列教程中,我将首次使用 Svelte 构建应用程序,并全程跟随我的教程进行操作。我将使用我常用的项目1来学习新的框架:一个番茄工作法应用程序。它比待办事项列表要复杂一些,因为它至少包含两个需要相互交互的组件(计时器和任务列表)。

在本系列的第五部分中,我们终于将所有内容整合起来,把番茄工作法融入到我们的任务集中。耶!让我们开始吧!

还没读过本系列的其他文章?那么您可能需要看看这篇关于 Svelte 入门的资源列表,以及构建番茄工作法应用程序的第一三部分。

番茄工作法与任务

所以,我们一边是番茄钟计时器,另一边是任务列表。它们各自独立运行,是完全独立的组件。一个可以倒计时番茄钟,另一个可以管理一系列任务。为了更好地支持番茄工作法,下一步我们需要让它们相互通信,以便用户可以:

  1. 选择要重点关注的任务
  2. 开始一个番茄钟,然后全神贯注地完成这项任务 25 分钟。
  3. 完成一个番茄钟后休息一下
  4. 或者取消一个番茄钟,并写下原因。

它们如何相互通信呢?可以通过共享一些状态,并通过 props 在组件之间传递,或者使用 Svelte store。

让我们实施这两种方案,并讨论它们的优缺点。

通过属性共享状态

Svelte道具是什么?

props ( properties的缩写)是 Svelte 用来向组件传递数据的机制。本质上,props 定义了组件的接口,也就是它如何与外部世界交互。

到目前为止,我们几乎没有提到道具,因为番茄钟计时器和任务列表都是各自独立的。但现在我们需要让这两个组件相互通信。具体来说:

  1. 我们需要该TaskList组件能够与外部世界通信,告知用户已选择某个任务。
  2. 我们需要告知用户PomodoroTimer已选择哪个任务。

选择任务

首先,我们更新TaskList组件,使用户可以选择任务。我们定义一个selectedTask变量来保存该信息:

<script>
  let activeTask;
  // more code...
</script>
Enter fullscreen mode Exit fullscreen mode

我们更新了模板,使用新按钮来选择任务:

{#if tasks.length === 0}
  <p>You haven't added any tasks yet. You can do it! Add new tasks and start kicking some butt!</p>
{:else}
  <ul>
    {#each tasks as task}
      <li>
        <!-- NEW STUFF -->
        <button on:click={() => selectTask(task)}>&gt;</button>
        <!--- END NEW STUFF -->
        <input class="description" type="text" bind:value={task.description} bind:this={lastInput}>
        <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
        <button on:click={() => removeTask(task)}>X</button>
      </li>
    {/each}
  </ul>
{/if}
<button class="primary" on:click={addTask}>Add a new task</button>
{#if tasks.length != 0}
  <p>
    Today you'll complete {allExpectedPomodoros} pomodoros.
  </p>
{/if}
Enter fullscreen mode Exit fullscreen mode

现在,每当用户点击按钮时,>我们都会调用selectTask将 activeTask 设置为所选任务的函数:

function selectTask(task) {
  activeTask = task;
}
Enter fullscreen mode Exit fullscreen mode

每当用户删除任务时,我们会检查它是否是待activeTask处理任务,如果是,我们会将其清理干净:

function removeTask(task){
  tasks = tasks.remove(task);
  if (activeTask === task) {
    selectTask(undefined);
  }
}
Enter fullscreen mode Exit fullscreen mode

太棒了!现在我们需要一种方法来告诉用户某个任务已被选中。我们可以使用 CSS 高亮显示当前选中的任务。一种方法是将元素class的属性设置li.active如下所示:

{#each tasks as task}
  <li class={activeTask === task ? 'active': ''}>
     <!-- task --->
  </li>
{/each}
Enter fullscreen mode Exit fullscreen mode

但是 Svelte 提供了一种简写语法,可以根据组件的状态更方便地添加或删除类:

{#each tasks as task}
  <li class:active={activeTask === task}>
     <!-- task --->
  </li>
{/each}
Enter fullscreen mode Exit fullscreen mode

现在我们需要.active在组件内部添加一些与该类关联的样式:

  .active input,
  .active button {
    border-color: var(--accent);
    background-color: var(--accent);
    color: white;
    transition: background-color .2s, color .2s, border-color .2s;
  }
Enter fullscreen mode Exit fullscreen mode

最后,我们还提供了一种在组件内选择要处理的任务的方法TaskList.svelte

一款番茄工作法应用,带有计时器和一系列任务。用户点击按钮并选择任务。

通知外界已选择一项任务

太棒了!下一步是让组件外部知道任务已被选中。Svelte 允许我们通过事件分发来实现这一点。在组件内部,我们可以定义自己的领域特定事件,并根据需要分发它们。

适用于我们用例的事件可以命名为selectedTask

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

function selectTask(task) {
  activeTask = task;
  // dispatch(eventName, eventData);
  dispatch('taskSelected', {
    task: activeTask,
  });
}
Enter fullscreen mode Exit fullscreen mode

所以现在,每当用户选择一个任务时,我们都会调用selectTask以下函数:

  1. 更新当前任务
  2. taskSelected通过分发包含当前活动任务的事件,通知外部世界某个任务已被选中。

在我们的应用程序组件中,我们可以像订阅任何其他标准 DOM 事件一样订阅这个新事件:

<main>
  <h1>{title}</h1>
  <PomodoroTimer />
  <TaskList on:taskSelected={updateActiveTask}/>
</main>
Enter fullscreen mode Exit fullscreen mode

App.svelte组件现在将存储其自身的 activeTask 版本:

<script>
  let title = "il Pomodoro";
  import TaskList from './TaskList.svelte';
  import PomodoroTimer from './PomodoroTimer.svelte';

  let activeTask;
  function updateActiveTask(event){
    activeTask = event.detail.task;
  }
</script>
Enter fullscreen mode Exit fullscreen mode

然后我们可以把番茄钟计时器发送给我们的朋友:

<main>
  <h1>{title}</h1>
  <PomodoroTimer {activeTask} />
  <TaskList on:taskSelected={updateActiveTask}/>
</main>
Enter fullscreen mode Exit fullscreen mode

番茄钟计时器满足活跃任务

但为了实现这一点,我们需要在PomodoroTimer组件内部定义一个新的属性:

<script>
export let activeTask;
</script>
Enter fullscreen mode Exit fullscreen mode

由于只有在有任务处于活动状态时,用户才能与番茄钟计时器进行交互,因此我们可以先在这种情况下禁用番茄钟计时器:

<section>
  <time>
    {formatTime(pomodoroTime)}
  </time>
  <footer>
    <button 
      class="primary" on:click={startPomodoro} 
      disabled={currentState !== State.idle || !activeTask}>start</button>
    <button on:click={cancelPomodoro} 
      disabled={currentState !== State.inProgress || !activeTask}>cancel</button>
  </footer>
</section>
Enter fullscreen mode Exit fullscreen mode

凉爽的!

一款番茄工作法应用,带有计时器和一系列任务。用户点击按钮并选择任务。选择任务后,番茄计时器即会启动。

最后,我们可以在完成一个番茄钟后,增加该任务所花费的番茄钟数。我们更新了函数completePomodoroPomodoroTimer.svelte包含此功能:

function completePomodoro(){
  // We add one more pomodoro to the active task
  activeTask.actualPomodoros++; 
  completedPomodoros++;
  if (completedPomodoros === 4) {
    rest(LONG_BREAK_S);
    completedPomodoros = 0;
  } else {
    rest(SHORT_BREAK_S);
  }
}
Enter fullscreen mode Exit fullscreen mode

但如果用户在番茄钟运行时删除任务会发生什么?良好的用户体验应该阻止用户这样做,要么在番茄钟运行时禁用删除按钮,要么向用户显示提示。不过,目前我们暂且将其作为一项附加练习或未来改进方案。

我们目前还没有显示完成任务所花费的番茄钟数,所以别忘了添加这个功能。回到TaskList.svelte组件中,我们更新组件标记以显示该信息:

  <ul>
    {#each tasks as task}
      <li class:active={activeTask === task}>
        <button on:click={() => selectTask(task)}>&gt;</button>
        <input class="description" type="text" bind:value={task.description} bind:this={lastInput}>
        <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
        <!-- NEW input -->
        <input class="pomodoros small" bind:value={task.actualPomodoros} disabled >
        <!-- END NEW -->
        <button on:click={() => removeTask(task)}>X</button>
      </li>
    {/each}
  </ul>
Enter fullscreen mode Exit fullscreen mode

我们的风格:

.pomodoros.small { 
  max-width: 40px;
  text-align: center;
}
.active input[disabled] {
  opacity: 0.6;
}
Enter fullscreen mode Exit fullscreen mode

锵锵!我们终于有了一款能正常运行的番茄工作法应用程序:

一款番茄钟应用,包含计时器和一系列任务。用户点击按钮并选择任务。选择任务后,番茄钟计时器启动。用户点击开始按钮,计时器开始倒计时。

一种耦合度略低的替代方法

在实现上述任务和计时器集成时,我对TaskList组件和定时器PomodoroTimer都修改同一个对象这种做法感到有些不满activeTask。应用程序中能够访问和修改同一数据的地方越多,就越难推断应用程序的状态及其随时间的变化。这反过来意味着,与该数据相关的错误可能会在应用程序的许多不同地方出现。此外,将数据activeTask向上传递给父组件,然后再由父App组件向下传递,这种做法也显得有些繁琐PomodoroTimer

PomodoroTimer这里提供一种替代方案,虽然牺牲了独立性TaskList,但可以减少所需的代码量并降低数据耦合度:

  1. PomodoroTimer组件包含在TaskList组件内部
  2. 我们拥有所需的所有数据,因此可以PomodoroTimer根据需要启用/禁用该功能。
  3. 计时器不会将值传递activeTaskPomodoroTimer,而是通过事件来通知任务已完成,并且TaskList会更新该事件activeTask
<PomodoroTimer disable={activeTask} on:completedPomodoro={() => activeTask.actualPomodoros++}/>
<ul>
  <!-- list of tasks remains unchanged -->
</ul>
Enter fullscreen mode Exit fullscreen mode

使用存储共享状态

在 Svelte 中,我们还可以使用store来共享状态。与通过 props 共享状态与 DOM 树和应用程序结构高度耦合不同,通过 store 共享状态完全独立于 DOM。使用 Svelte store,您只需导入一个 store,即可在应用程序的任何组件之间共享数据,无论它们位于何处。

什么是商店?

store 是一个对象,它可以帮助你在 Svelte 应用的各个组件之间共享数据。Store 独立于 DOM 树和应用的结构,因此当需要在彼此远离或需要解耦的组件之间共享状态时非常有用。Store 也是响应式的:每当其值发生变化时,它都会通知其消费者,以便它们能够做出相应的反应。组件可以通过 `subscribe`subscribe方法订阅 store 来成为其消费者$(以下是一些示例)。

有关商店的更多信息,请参阅 Svelte 的商店文档。

活动任务存储

让我们创建一个新的 store,以便在组件之间共享当前活动任务TaskListPomodoroTimer组件TaskList仍然拥有完整的任务列表,并将继续负责根据用户输入选择活动任务。这意味着我们可以复用之前示例中的大部分内容。不同之处在于:首先,不会再有taskSelected事件,更重要的是,它将activeTask是一个 Svelte store。

我们先从在单独的文件中创建商店开始tasksStore.js

import { writable } from 'svelte/store';

export const activeTask = writable();
// The initial value of this store is undefined.
// You can provide an initial value by passing it as an argument
// to the writable function. For example:
// 
// const count = writable(0);
Enter fullscreen mode Exit fullscreen mode

activeTask是一个可写的存储,通俗地说,就是组件可以使用它来写入信息,这些信息随后可以在组件之间共享。除了共享信息之外,存储还具有响应式特性,这意味着当数据发生变化时,它们会通知组件。让我们看看如何利用这些特性来实现组件TaskList之间的通信PomodoroTimer

可写存储

可写存储是指组件可以写入的存储。此外,subscribe它们还具有 `setValue`set和 `updateValue`update方法,允许组件设置存储中的值,或通过应用函数来更新当前值。

下一步是将storeTaskList导入并替换组件中的activeTask原有变量。let activeTask

// import activeTask store
import {activeTask} from './tasksStore.js';

// remove old variable
// let activeTask
Enter fullscreen mode Exit fullscreen mode

由于activeTask它现在是一个存储对象,我们不能像以前那样直接设置它的值。所以,我们需要改成:

  function selectTask(task) {
    activeTask = task;
  }
Enter fullscreen mode Exit fullscreen mode

我们需要使用set商店的方法:

  function selectTask(task) {
    activeTask.set(task);
  }
Enter fullscreen mode Exit fullscreen mode

同样,activeTask这里不再指代 activeTask 本身,而是指代存储其值的 store。要检索任务的当前值,您需要使用相应的get方法。因此,不再是:

function removeTask(task){
  if (activeTask === task){
    selectTask(undefined);
  }
  tasks = tasks.remove(task);
}
Enter fullscreen mode Exit fullscreen mode

我们写道:

// import get from svelte/store
import { get } from 'svelte/store';

// use it to retrieve the current value
// of the activeTask store and therefore
// the current task that is active
function removeTask(task){
  if (get(activeTask) === task){
    selectTask(undefined);
  }
  tasks = tasks.remove(task);
}
Enter fullscreen mode Exit fullscreen mode

使用set`and`可能相当冗长,因此 Svelte 提供了一种替代语法,允许您在组件内部get通过在 store 前面加上一个符号来直接更改和检索 store 的值。$

利用这种便捷的语法,我们可以将之前的示例更新为以下示例:

// use it to retrieve the current value
// of the activeTask store and therefore
// the current task that is active.
function removeTask(task){
  if ($activeTask === task){
    selectTask(undefined);
  }
  tasks = tasks.remove(task);
}

// Use it to update the value of the activeTask.
function selectTask(task) {
  $activeTask = task;
}
Enter fullscreen mode Exit fullscreen mode

这看起来和原始实现非常相似。是不是很棒?我们用它来管理状态,但它看起来几乎就像设置和读取一个普通的 JavaScript 变量一样。

Get and Set 还是 $store?应该用哪个?

如果在组件内部,则没有必要使用 get 和 set。建议使用更简洁、更清晰的 $store 语法。

在组件之外,当需要获取或更新 store 的值时,必须使用 get 和 set 方法。为什么呢?因为 $store 语法会利用组件的生命周期事件自动订阅和取消订阅 store。组件挂载时,你会订阅;组件销毁时,你会取消订阅。然而,在原生 JavaScript 模块中,并没有生命周期的概念。因此,编译器无法判断何时才是取消订阅 store 的合适时机,从而导致内存泄漏。

我们还可以将其$activeTask用于组件模板中,以检查给定元素是否li属于当前任务并将其高亮显示:

<ul>
  {#each tasks as task}
    <!-- update $activeTask here -->
    <li class:active={$activeTask === task}>
    <!-- END update -->
      <button on:click={() => selectTask(task)}>&gt;</button>
      <input class="description" type="text" bind:value={task.description} bind:this={lastInput}>
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
      <input class="pomodoros small" bind:value={task.actualPomodoros} disabled >
      <button on:click={() => removeTask(task)}>X</button>
    </li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

activeTask现在,当用户在组件中选择某个选项时,我们就可以在组件中设置该选项的值TaskList。下一步是移除所有对activeTask`from`的引用App.svelte,并更新我们的PomodoroTimer组件以使用新的 store。

我们使用之前学到的completePomodoro相同语法更新该方法:$activeTask

import { activeTask } from './tasksStore.js';

function completePomodoro(){
  // Get the current active task and add a pomodoro
  $activeTask.actualPomodoros++; 
  completedPomodoros++;
  if (completedPomodoros === 4) {
    rest(LONG_BREAK_S);
    completedPomodoros = 0;
  } else {
    rest(SHORT_BREAK_S);
  }
}
Enter fullscreen mode Exit fullscreen mode

该模板用于在任务处于活动状态或非活动状态时启用和禁用计时器:

<section>
  <time>
    {formatTime(pomodoroTime)}
  </time>
  <footer>
    <button class="primary" 
      on:click={startPomodoro} 
      disabled={currentState !== State.idle || !$activeTask}>start</button>
    <button 
      on:click={cancelPomodoro} 
      disabled={currentState !== State.inProgress || !$activeTask}>cancel</button>
  </footer>
</section>
Enter fullscreen mode Exit fullscreen mode

如果您现在查看一下页面(记住您可以使用 运行本地开发环境npm run dev),您会很高兴地发现一切仍然正常运行。太棒了!

Svelte有哪些类型的商店?

除了可写存储(Writable Store)之外,Svelte 还提供了可读存储(Readable Store)和派生存储(Different Store)。可读存储是指组件无法直接更新其值的存储。派生存储是指其值派生自其他存储的存储。当这些存储发生更改时,派生存储中的值也会随之更新。

如果您想了解更多关于 Svelte store 的信息,请查看 Svelte 文档

道具与商店

现在我们已经分别使用 props 和 store 完成了两个不同版本的番茄工作法应用程序,让我们花点时间反思并比较一下这两种方法:

道具

Svelte 组件使用 props 定义其与外部世界的接口。props 允许父组件与子组件相互通信。您可以使用 props 将数据从父组件向下传递给子组件,也可以使用事件数据从子组件向上传递给父组件。

道具专家

  • 来回传递数据属性非常简单。
  • 理解与组件交互所使用的契约非常简单,因为它由组件的 props 定义。
  • 使用 props 跟踪数据流就像查看数据如何通过 props 在组件内部流动,以及如何通过事件从组件中流出一样简单。

道具

  • 这种状态管理方式会在组件之间建立耦合,使应用程序变得有些僵化:如果新的需求迫使你将组件移动到页面中的不同位置,你可能需要更新向该组件提供信息的方式。

何时使用道具

综上所述,对于完全隔离的底层组件(例如日期选择器、自动完成功能等)或彼此靠近(在 DOM 中)且属于密切相关单元的组件来说,props 似乎是一个很好的解决方案。

商店

Svelte store 提供了一种极其便捷的方式,可以在组件之间以松耦合的方式共享数据。由于只需导入它们即可开始访问和修改数据,因此它们可以用于在应用程序 DOM 树中的任何位置与任何组件进行通信。

商店专业人士

  • 它们比 props 更灵活,允许你与应用程序 DOM 树中位置相距甚远的组件进行通信。它们不会强制你一步一步地在 DOM 树中传递信息,只需导入一次,你就可以访问和修改数据。
  • 它们在组件之间建立了一种松耦合。使用 store 在组件之间进行通信,可以构建灵活的 Web 应用程序,即使需要更改应用程序的布局,也无需修改数据处理逻辑。也就是说,如果两个组件使用 store 进行通信,突然需要将其中一个组件移动到页面另一端,这完全没问题,只需将其移走即可,无需任何额外的代码更改。相比之下,如果两个组件都通过 props 进行通信,则必须更改状态管理策略。

商店优惠

  • 组件间的数据交互不像使用 props 时那样直接。由于交互不再发生在组件之间,而是发生在组件和 store 之间,因此可能更难理解组件上的操作如何影响其他组件。

何时使用商店

  • 当您需要在应用程序 DOM 树中位置相距较远的组件之间进行通信时,请使用 store。
  • 当您希望保持选择的灵活性并让组件之间保持松耦合时(例如,如果您预计可能需要这样做),请使用 store。

在 Svelte 中还有其他共享状态的方法吗?

除了 props 和 store 之外,Svelte 还提供了一种折衷方案:Context API。Context API 允许组件之间进行通信,而无需在 DOM 树深处传递大量的 props 或事件。它仅包含两个方法:`getState()`setContext(key, value)getContext(key)`getState()`。父组件可以使用 ` getState setContext(key, value)()` 方法保存一些数据,然后该组件的任何子组件都可以使用 `getState()` 方法检索这些数据getContext(key)

您可以在 Svelte 教程中找到如何使用 Context API的示例

想找番茄钟应用程序的源代码吗?

无需再找了!您可以在GitHub上找到它,可以直接克隆使用,或者在Svelte REPL上立即进行修改。

关于苗条身材的更多思考

使用 Svelte的体验依然非常愉快。除了我之前提到的几点(1、2、3 之外我还发现:

  • 使用属性和事件来传递组件信息非常容易。语法非常简洁明了、轻量级且易于记忆。
  • 我非常喜欢 Svelte 内置的状态管理解决方案,以及它能够以响应式的方式轻松更改数据存储或读取数据。

总结

在本文中,我们终于将所有内容连接起来,实现了一个可用的番茄钟计时器。太棒了!我们学习了如何使用 props 和 events 在 DOM 树中彼此靠近的组件之间进行通信,以及如何使用 store 以更松耦合的方式在组件之间共享数据。

本系列的后续文章将深入探讨测试、异步编程、动画等内容。下次见!祝您度过美好的一天!


  1. 来看看我用 Knockout.js 写的这个超级老旧的番茄工作法应用,那是我刚开始做 Web 开发的时候写的 。↩

文章来源:https://dev.to/vintharas/learn-svelte-connecting-the-pomodoro-timer-and-tasks-with-props-and-stores-p0k