学习 Svelte:在番茄工作法应用中添加、编辑和估算任务
本文最初发表于Barbarian Meets Coding 网站。
Svelte是一个现代 Web 框架,它采用了一种全新的 Web 应用构建方式,将大部分工作从运行时转移到编译时。作为一种编译器优先的框架,Svelte 能够实现一些其他框架无法实现的有趣功能,例如在运行时从应用程序中“消失”,或者允许以组件为中心的开发模式,使 HTML、JavaScript 和 CSS 能够以非常符合 Web 标准的方式共存于同一个 Svelte 文件中。
在本系列教程中,我将首次使用 Svelte 构建一个应用程序,并全程跟随我的教程进行操作。我将使用我常用的项目[^1]来学习新的框架:一个番茄工作法应用程序。它比待办事项列表要复杂一些,因为它至少包含两个需要相互交互的组件(计时器和任务列表)。
在本系列的第三部分中,我们将继续我们的项目,使其能够创建任务列表并估算完成这些任务所需的番茄钟数量。让我们开始吧!
还没读过本系列的其他文章?那么您可以看看这份Svelte 入门资源列表,或者番茄工作法应用构建的第一部分。
开始每日番茄工作法
当你使用番茄工作法时,每天开始工作之前,第一件事就是坐下来按照以下步骤操作:
- 决定你今天想要完成哪些任务,
- 估算一下需要多少个番茄钟才能完成这些任务,然后
- 根据你实际能够完成的番茄钟数量来确定优先级。
让我们改进我们简易的番茄钟应用程序,通过提供创建和估算任务的方法来支持这种初始流程。
定义一种任务建模方法
我们首先需要做的是设计一种任务建模方法。在我们当前版本的应用程序中,任务只是一个字符串,它代表了我们需要完成的任何操作的描述:
<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>
但我们需要让任务包含更多信息,例如我们预计任务需要多少个番茄钟,任务的状态(是否已完成?),以及任务实际花费的番茄钟数。
Task因此,我们将使用一个新文件中的类来对任务进行建模,Task.js并添加一些初始字段以涵盖我们的初始用例:
export class Task {
constructor(description="", expectedPomodoros=1) {
this.description = description;
this.expectedPomodoros = expectedPomodoros;
this.actualPomodoros = 0;
}
}
现在我们可以将原始示例中的字符串替换为该类的实例:
<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>
目前用户界面保持不变。我们只是改变了任务的底层呈现方式。现在,让我们来实现添加新任务的功能。
创建新任务
本教程的目标是尽快实现一个可用的番茄工作法应用程序,因此我们将专注于快速完成,暂时忽略用户体验和精美设计。我们将着重开发基本核心功能,之后再进行完善和优化。
为了快速实现一个可编辑的任务列表,用户可以根据需要随意添加和删除任务,我们将采用以下方法。具体来说:
- 通过为每个任务添加输入框,使所有任务都可编辑。
- 添加一个按钮以添加新任务
- 在每个任务旁边添加一个用于删除任务的按钮
使任务可编辑
为了使我们的任务可编辑,我们将更新TaskList.svelte组件。不再使用普通的列表元素:
<ul>
{#each tasks as task}
<li>{task}</li>
{/each}
</ul>
我们将使用以下输入:
<ul>
{#each tasks as task}
<li>
<input type="text" value={task.description}>
<input type="number" value={task.expectedPomodoros}>
</li>
{/each}
</ul>
上面的例子看起来好像有效,但实际上并非如此。它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>
现在我们可以编辑任务描述以及每个任务预计耗时的番茄钟数量。每当我们更新底层数据时,输入数据都会相应更新;同样地,每当我们更新输入数据时,所做的更改也会反映在数据中。
我们稍微调整一下样式,让输入框更好地与内容相匹配:
<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>
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>
每次点击此按钮,我们都会将一项任务添加到今天要完成的任务列表中。为此,我们使用on:{event}指令处理点击事件,以便每次用户点击该按钮时,都会创建一个新任务并将其添加到列表中:
<button on:click={addTask}>Add a new task</button>
该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>
现在我点击添加新任务的按钮……却没有任何反应。嗯……
经过一番摸索和故障排除,我发现 Svelte 判断变量是否发生变化的方式是通过新的赋值。因此,我们需要将上面的代码更新为以下内容:
function addTask(){
tasks = tasks.concat(new Task());
}
我还学到了一些有趣的东西:
- Svelte 对 sourcemap 的支持很好,所以我可以在 Chrome 开发者工具中查看 Svelte 代码。但是,我无法在方法内部设置断点或使用logpoint
addTask。 - 借助
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>
- 无论是在Svelte Playground中还是在本地开发 Svelte 项目时,都很容易查看生成的代码。将任务推送到现有数组后生成的 JavaScript 输出如下:
function addTask() {
tasks.push(new Task());
}
而如果我们更新变量的值,tasks则会生成以下代码:
function addTask() {
$$invalidate(1, tasks = tasks.concat(new Task()));
}
该$$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>
并创建一个新removeTask方法来执行实际的删除操作:
function removeTask(task){
const index = tasks.indexOf(task);
tasks = [...tasks.slice(0, index), ...tasks.slice(index+1)];
}
JavaScript 真的应该有个array.prototype.remove方法……用 FizzBuzz 实现,我们来做吧(千万不要在家或工作场所这样做。只适用于零风险的业余项目)。
我添加了一个新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)];
}
并更新我们的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>
现在可以删除任务了:
使用 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>
如果我们现在看一下我们的应用程序,就会发现每次组件渲染时,控制台都会打印出这条消息。现在我们需要获取对创建的输入元素的引用。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>
现在我们有了对该输入的引用,我们可以利用它在创建新任务时将其置于焦点位置:
<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>
这个方案看起来相当不稳妥,原因有很多,比如我总觉得它之所以能勉强过关,仅仅是因为新创建的输入框是 DOM 中的最后一个输入框。但目前来说,它确实有效。有时候,正确的方案就是目前可行的方案。我们很快就能还清这笔技术债务。
目前,请保持良好的专注状态:
设定每日番茄钟目标并坚持下去
为了更好地支持番茄工作法的启动流程,我们最后想要添加的功能是,让用户能够清楚地了解自己需要完成多少个番茄钟。一个快捷的方法是,将所有任务的预计番茄钟数量相加,并显示给用户。
这是一个完美的功能,因为它很简单,而且可以让我们在 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>
该$:语法告诉 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>
我们完成了!
如果没有任务会发生什么?
好的,还有最后一件事。还有一个细节需要完善。如果没有任务怎么办?
现在我们看到的只是一片空白,但如果能给用户一些鼓励的信息,让他们充满力量地开始新的一天,那就太好了。让我们一起努力吧!
我们可以利用 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}
想找番茄钟应用程序的源代码吗?
无需再找了!您可以在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




