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

学习 Svelte:在番茄工作法应用中添加、编辑和估算任务

学习 Svelte:在番茄工作法应用中添加、编辑和估算任务

本文最初发表于Barbarian Meets Coding 网站

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

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

在本系列的第三部分中,我们将继续我们的项目,使其能够创建任务列表并估算完成这些任务所需的番茄钟数量。让我们开始吧!

还没读过本系列的其他文章?那么您可以看看这份Svelte 入门资源列表,或者番茄工作法应用构建的第一部分。

开始每日番茄工作法

当你使用番茄工作法时,每天开始工作之前,第一件事就是坐下来按照以下步骤操作:

  1. 决定你今天想要完成哪些任务,
  2. 估算一下需要多少个番茄钟才能完成这些任务,然后
  3. 根据你实际能够完成的番茄钟数量来确定优先级。

让我们改进我们简易的番茄钟应用程序,通过提供创建和估算任务的方法来支持这种初始流程。

定义一种任务建模方法

我们首先需要做的是设计一种任务建模方法。在我们当前版本的应用程序中,任务只是一个字符串,它代表了我们需要完成的任何操作的描述:

<script>
  const tasks = [
    "plan some fun trip with Teo",
    "buy some flowers to my wife",
    "write an article about Svelte"
  ];
</script>

<style>
  ul {
    list-style: none;
  }
</style>

<ul>
  {#each tasks as task}
    <li>{task}</li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

但我们需要让任务包含更多信息,例如我们预计任务需要多少个番茄钟,任务的状态(是否已完成?),以及任务实际花费的番茄钟数。

Task因此,我们将使用一个新文件中的类来对任务进行建模,Task.js并添加一些初始字段以涵盖我们的初始用例:

export class Task {
  constructor(description="", expectedPomodoros=1) {
    this.description = description;
    this.expectedPomodoros = expectedPomodoros;
    this.actualPomodoros = 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以将原始示例中的字符串替换为该类的实例:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';

  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];
</script>
Enter fullscreen mode Exit fullscreen mode

目前用户界面保持不变。我们只是改变了任务的底层呈现方式。现在,让我们来实现添加新任务的功能。

创建新任务

本教程的目标是尽快实现一个可用的番茄工作法应用程序,因此我们将专注于快速完成,暂时忽略用户体验和精美设计。我们将着重开发基本核心功能,之后再进行完善和优化。

为了快速实现一个可编辑的任务列表,用户可以根据需要随意添加和删除任务,我们将采用以下方法。具体来说:

  1. 通过为每个任务添加输入框,使所有任务都可编辑。
  2. 添加一个按钮以添加新任务
  3. 在每个任务旁边添加一个用于删除任务的按钮

使任务可编辑

为了使我们的任务可编辑,我们将更新TaskList.svelte组件。不再使用普通的列表元素:

<ul>
  {#each tasks as task}
    <li>{task}</li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

我们将使用以下输入:

<ul>
  {#each tasks as task}
    <li>
      <input type="text" value={task.description}>
      <input type="number" value={task.expectedPomodoros}>
    </li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

上面的例子看起来好像有效,但实际上并非如此。它value={task.description}只能单向工作,即从数据到模板。但如果用户尝试编辑任务,新的描述或番茄钟不会反映在数据中。要在数据和模板之间建立双向数据绑定,需要使用以下bind:value指令:

<ul>
  {#each tasks as task}
    <li>
      <input type="text" bind:value={task.description}>
      <input type="number" bind:value={task.expectedPomodoros}>
    </li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

现在我们可以编辑任务描述以及每个任务预计耗时的番茄钟数量。每当我们更新底层数据时,输入数据都会相应更新;同样地,每当我们更新输入数据时,所做的更改也会反映在数据中。

醒目的标题“番茄工作法”后面跟着一系列可编辑的任务,这些任务都位于输入框中。

我们稍微调整一下样式,让输入框更好地与内容相匹配:

<style>
  ul {
    list-style: none;
  }
  .description {
    min-width: 400px;
  }
  .pomodoros { 
    max-width: 100px;
  }
</style>

<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description}>
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
    </li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

Svelte 的样式作用域仅限于组件本身,所以我可以input直接设置元素的样式(nth-child例如使用选择器),但我喜欢使用语义化的命名类,原因有二:

  • 它们更容易阅读和理解。
  • 如果将来我更改了输入框的顺序,也不会破坏应用程序的样式。

现在看起来好多了!真棒!

醒目的标题“番茄工作法”后面跟着一系列可编辑的任务,这些任务都位于输入框中。

添加新任务

接下来,我们希望能够添加新任务。因此,我们添加一个按钮来实现该功能:

<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description} >
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
    </li>
  {/each}
  <button>Add a new task</button>
</ul>

Enter fullscreen mode Exit fullscreen mode

每次点击此按钮,我们都会将一项任务添加到今天要完成的任务列表中。为此,我们使用on:{event}指令处理点击事件,以便每次用户点击该按钮时,都会创建一个新任务并将其添加到列表中:

<button on:click={addTask}>Add a new task</button>
Enter fullscreen mode Exit fullscreen mode

addTask函数属于 Svelte 组件中标签内的行为部分script

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';

  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];

  function addTask(){
    tasks.push(new Task());
  }
</script>
Enter fullscreen mode Exit fullscreen mode

现在我点击添加新任务的按钮……却没有任何反应。嗯……

经过一番摸索和故障排除,我发现 Svelte 判断变量是否发生变化的方式是通过新的赋值。因此,我们需要将上面的代码更新为以下内容:

function addTask(){
  tasks = tasks.concat(new Task());
}
Enter fullscreen mode Exit fullscreen mode

我还学到了一些有趣的东西:

  • Svelte 对 sourcemap 的支持很好,所以我可以在 Chrome 开发者工具中查看 Svelte 代码。但是,我无法在方法内部设置断点或使用logpointaddTask
  • 借助console.log内部addTask视图和{@debug tasks}Svelte 模板,我发现列表一直在增长,但模板却始终没有更新。修复此问题后,随着列表的持续增长,{@debug tasks}更新后的任务列表被执行并记录下来。
<script>
  import {Task} from './Task.js';

  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];

  function addTask(){
    tasks.push(new Task());
    console.log(tasks); // => this grows everytime
  }
</script>

<!-- this was only executed the first time -->
{@debug tasks}
<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description} >
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
    </li>
  {/each}
  <button on:click={addTask}>Add a new task</button>
</ul>
Enter fullscreen mode Exit fullscreen mode
  • 无论是在Svelte Playground中还是在本地开发 Svelte 项目时,都很容易查看生成的代码。将任务推送到现有数组后生成的 JavaScript 输出如下:
function addTask() {
  tasks.push(new Task());
}
Enter fullscreen mode Exit fullscreen mode

而如果我们更新变量的值,tasks则会生成以下代码:

function addTask() {
  $$invalidate(1, tasks = tasks.concat(new Task()));
}
Enter fullscreen mode Exit fullscreen mode

$$invalidate函数必须能够警告 Svelte 数据已更改,并且模板(依赖于 的部分tasks)需要重新渲染。

总之!现在我们可以添加新任务了:

醒目的标题“番茄工作法”后面是一系列可编辑的任务,每个任务都以输入框的形式呈现。还有一个按钮可以添加新任务。

删除现有任务

我们可以添加任务,所以我们也应该能够在更改优先级时删除任务。为此,我们为每个任务添加一个新按钮:

<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description}>
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
      <!-- NEW STUFF -->
      <button on:click={() => removeTask(task)}>X</button>
      <!-- END NEW STUFF -->
    </li>
  {/each}
  <button on:click={addTask}>Add a new task</button>
</ul>
Enter fullscreen mode Exit fullscreen mode

并创建一个新removeTask方法来执行实际的删除操作:

function removeTask(task){
  const index = tasks.indexOf(task);
  tasks = [...tasks.slice(0, index), ...tasks.slice(index+1)];
}
Enter fullscreen mode Exit fullscreen mode

JavaScript 真的应该有个array.prototype.remove方法……用 FizzBu​​zz 实现,我们来做吧(千万不要在家或工作场所这样做。只适用于零风险的业余项目)。

我添加了一个新ArrayExtensions.js文件,里面有这个漂亮的东西:

/**
 * Returns a new array without the item passed as an argument
 */
Array.prototype.remove = function (item) {
    const index = this.indexOf(item);
    return [...this.slice(0, index), ...this.slice(index+1)];
}
Enter fullscreen mode Exit fullscreen mode

并更新我们的TaskList.svelte组件:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';
  import './ArrayExtensions.js';

  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];

  function addTask(){
    tasks = tasks.concat(new Task());
  }
  function removeTask(task){
    // It looks way nicer, doesn't it?
    tasks = tasks.remove(task);
  }
</script>
Enter fullscreen mode Exit fullscreen mode

现在可以删除任务了:

醒目的标题“番茄工作法”后面是一系列可编辑的任务,每个任务都以输入框的形式呈现。还有一个按钮可以添加新任务。

使用 Svelte 生命周期钩子可以略微提升用户体验

如果创建新任务时,新创建的任务描述能够自动显示在屏幕上,岂不是很棒?这样,我们应用的键盘用户就可以在“添加新任务”按钮上按回车键,输入任务内容和预估时间,然后再按一次回车键,如此反复。这样就能最大限度地提高效率

为了实现这种功能,我们需要知道何时向 DOM 添加了新的输入框,并让该输入框获得焦点。快速浏览 Svelte 文档后,我发现可以通过钩入组件的生命周期来解决这类问题。`afterUpdate`生命周期钩子会在 DOM 更新为新数据后执行,因此它看起来是一个不错的选择:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';
  import './ArrayExtensions.js';

  // Rest of the code has been collapsed for simplicity's sake

  afterUpdate(() => {
    console.log('Hello! I was updated!'):
  });
</script>
Enter fullscreen mode Exit fullscreen mode

如果我们现在看一下我们的应用程序,就会发现每次组件渲染时,控制台都会打印出这条消息。现在我们需要获取对创建的输入元素的引用。Svelte 有一个特殊的指令或许可以帮到我们bind:this

你可以这样使用它:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';
  import './ArrayExtensions.js';
  let lastInput;

  // rest of the code collapsed for simplicity's sake
</script>

<style>
/** styles collapsed **/
</style>


<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description} 
       bind:this={lastInput}>  <!-- THIS IS NEW! -->
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
      <button on:click={() => removeTask(task)}>X</button>
    </li>
  {/each}
  <button on:click={addTask}>Add a new task</button>
</ul>
Enter fullscreen mode Exit fullscreen mode

现在我们有了对该输入的引用,我们可以利用它在创建新任务时将其置于焦点位置:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';
  import './ArrayExtensions.js';
  let taskAddedPendingFocus = false;
  let lastInput;

  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];

  function addTask(){
    tasks = tasks.concat(new Task());
    taskAddedPendingFocus = true;
  }
  function removeTask(task){
    tasks = tasks.remove(task);
  }
  function focusNewTask(){
    if (taskAddedPendingFocus && lastInput) {
      lastInput.focus();
      taskAddedPendingFocus = false;
    }
  }

  afterUpdate(focusNewTask);
</script>
Enter fullscreen mode Exit fullscreen mode

这个方案看起来相当不稳妥,原因有很多,比如我总觉得它之所以能勉强过关,仅仅是因为新创建的输入框是 DOM 中的最后一个输入框。但目前来说,它确实有效。有时候,正确的方案就是目前可行的方案。我们很快就能还清这笔技术债务。

目前,请保持良好的专注状态:

一个动画 GIF,显示我正在向待办事项列表中添加新任务,并将注意力集中在描述字段中。

设定每日番茄钟目标并坚持下去

为了更好地支持番茄工作法的启动流程,我们最后想要添加的功能是,让用户能够清楚地了解自己需要完成多少个番茄钟。一个快捷的方法是,将所有任务的预计番茄钟数量相加,并显示给用户。

这是一个完美的功能,因为它很简单,而且可以让我们在 Svelte 中尝试响应式系统。

在 Svelte 中,你可以创建由其他现有属性计算得出的属性。在本例中,我们需要一个新属性,它表示所有任务当前番茄钟的总和。这样的属性可以这样写:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';
  import './ArrayExtensions.js';
  let taskAddedPendingFocus = false;
  let lastInput;
  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];
  $: allExpectedPomodoros = tasks.reduce((acc , t) => acc + t.expectedPomodoros, 0);

  /** rest of the code omitted for the sake of clarity. **/
</script>
Enter fullscreen mode Exit fullscreen mode

$:语法告诉 Svelte 该allExpectedPomodoros属性是一个响应式值,并且需要在任何更新时更新它tasks(有趣的是,这实际上是JavaScript 中的有效语法,但我一生中从未用过)。

现在我们可以把它添加到我们的标记中:

<ul>
  {#each tasks as task}
    <li>
      <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}
  <button on:click={addTask}>Add a new task</button>
</ul>
<!-- New stuff here -->
<p>
  Today you'll complete {allExpectedPomodoros} pomodoros.
</p>
Enter fullscreen mode Exit fullscreen mode

我们完成了!

一个动画 GIF,显示我正在向待办事项列表中添加新任务,并将注意力集中在描述字段中。

如果没有任务会发生什么?

好的,还有最后一件事。还有一个细节需要完善。如果没有任务怎么办?

现在我们看到的只是一片空白,但如果能给用户一些鼓励的信息,让他们充满力量地开始新的一天,那就太好了。让我们一起努力吧!

我们可以利用 Svelte 的{#if} and {:else}模块功能,在尚无任务时显示一条消息。例如:

{#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>
        <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 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

当用户尚未创建任何任务时,会显示一条鼓舞人心的消息。

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

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

一些其他的思考

总的来说,使用 Svelte 的体验非常愉快。以下是我对上一篇文章的一些补充想法

  • 总的来说,大部分功能仍然按预期运行,而且很容易排查和恢复错误。我之前很惊讶它array.push没有触发组件渲染,但深入研究这个问题后,我明白编译器更容易理解赋值操作引起的更改。这确实很有道理,毕竟更新现有值比学习新的 API(例如setState)要容易得多。
  • 很遗憾,我无法svelte在 Chrome 开发者工具的代码中设置断点或日志点。我原本以为可以,但或许需要一些我不知道的额外设置。这感觉像是开发环境应该默认支持的功能
  • Svelte 教程和 Playground 能够访问 Svelte 编译器生成的代码,这一点真的很棒。查看生成的代码很有意思,我发现 `array.push` 并没有产生失效调用。(这也表明 Svelte 确实有一个运行时环境,虽然很小,尽管人们经常宣传说应用生成后它就完全消失了。)
  • 事件处理、元素数据绑定、if 和 else 代码块的语法虽然非标准,但有时却让人感觉似曾相识,而且总体来说很容易上手。(当然,这可能是因为我之前接触过很多其他框架,它们也实现了类似的功能,只是语法略有不同。)
  • 响应$:式值非常容易在组件中实现和渲染。

今天的内容就到这里啦。希望您喜欢这篇文章!祝您一切顺利!

文章来源:https://dev.to/vintharas/discovering-svelte-adding-editing-and-estimating-tasks-in-the-pomodoro-technique-app-26dk