学习 Svelte:使用属性和存储连接番茄钟计时器和任务
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
本文最初发表于Barbarian Meets Coding 网站。您通常可以在那里和Twitter上找到我。
Svelte是一个现代 Web 框架,它采用了一种全新的 Web 应用构建方式,将大部分工作从运行时转移到编译时。作为一种编译器优先的框架,Svelte 能够实现一些其他框架无法实现的有趣功能,例如在运行时从应用程序中“消失”,或者允许以组件为中心的开发模式,使 HTML、JavaScript 和 CSS 能够以非常符合 Web 标准的方式共存于同一个 Svelte 文件中。
在本系列教程中,我将首次使用 Svelte 构建应用程序,并全程跟随我的教程进行操作。我将使用我常用的项目1来学习新的框架:一个番茄工作法应用程序。它比待办事项列表要复杂一些,因为它至少包含两个需要相互交互的组件(计时器和任务列表)。
在本系列的第五部分中,我们终于将所有内容整合起来,把番茄工作法融入到我们的任务集中。耶!让我们开始吧!
还没读过本系列的其他文章?那么您可能需要看看这篇关于 Svelte 入门的资源列表,以及构建番茄工作法应用程序的第一、二、三部分。
番茄工作法与任务
所以,我们一边是番茄钟计时器,另一边是任务列表。它们各自独立运行,是完全独立的组件。一个可以倒计时番茄钟,另一个可以管理一系列任务。为了更好地支持番茄工作法,下一步我们需要让它们相互通信,以便用户可以:
- 选择要重点关注的任务
- 开始一个番茄钟,然后全神贯注地完成这项任务 25 分钟。
- 完成一个番茄钟后休息一下
- 或者取消一个番茄钟,并写下原因。
但它们如何相互通信呢?可以通过共享一些状态,并通过 props 在组件之间传递,或者使用 Svelte store。
让我们实施这两种方案,并讨论它们的优缺点。
通过属性共享状态
Svelte道具是什么?
props ( properties的缩写)是 Svelte 用来向组件传递数据的机制。本质上,props 定义了组件的接口,也就是它如何与外部世界交互。
。
到目前为止,我们几乎没有提到道具,因为番茄钟计时器和任务列表都是各自独立的。但现在我们需要让这两个组件相互通信。具体来说:
- 我们需要该
TaskList组件能够与外部世界通信,告知用户已选择某个任务。 - 我们需要告知用户
PomodoroTimer已选择哪个任务。
选择任务
首先,我们更新TaskList组件,使用户可以选择任务。我们定义一个selectedTask变量来保存该信息:
<script>
let activeTask;
// more code...
</script>
我们更新了模板,使用新按钮来选择任务:
{#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)}>></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}
现在,每当用户点击按钮时,>我们都会调用selectTask将 activeTask 设置为所选任务的函数:
function selectTask(task) {
activeTask = task;
}
每当用户删除任务时,我们会检查它是否是待activeTask处理任务,如果是,我们会将其清理干净:
function removeTask(task){
tasks = tasks.remove(task);
if (activeTask === task) {
selectTask(undefined);
}
}
太棒了!现在我们需要一种方法来告诉用户某个任务已被选中。我们可以使用 CSS 高亮显示当前选中的任务。一种方法是将元素class的属性设置li为.active如下所示:
{#each tasks as task}
<li class={activeTask === task ? 'active': ''}>
<!-- task --->
</li>
{/each}
但是 Svelte 提供了一种简写语法,可以根据组件的状态更方便地添加或删除类:
{#each tasks as task}
<li class:active={activeTask === task}>
<!-- task --->
</li>
{/each}
现在我们需要.active在组件内部添加一些与该类关联的样式:
.active input,
.active button {
border-color: var(--accent);
background-color: var(--accent);
color: white;
transition: background-color .2s, color .2s, border-color .2s;
}
最后,我们还提供了一种在组件内选择要处理的任务的方法TaskList.svelte:
通知外界已选择一项任务
太棒了!下一步是让组件外部知道任务已被选中。Svelte 允许我们通过事件分发来实现这一点。在组件内部,我们可以定义自己的领域特定事件,并根据需要分发它们。
适用于我们用例的事件可以命名为selectedTask:
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function selectTask(task) {
activeTask = task;
// dispatch(eventName, eventData);
dispatch('taskSelected', {
task: activeTask,
});
}
所以现在,每当用户选择一个任务时,我们都会调用selectTask以下函数:
- 更新当前任务
taskSelected通过分发包含当前活动任务的事件,通知外部世界某个任务已被选中。
在我们的应用程序组件中,我们可以像订阅任何其他标准 DOM 事件一样订阅这个新事件:
<main>
<h1>{title}</h1>
<PomodoroTimer />
<TaskList on:taskSelected={updateActiveTask}/>
</main>
该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>
然后我们可以把番茄钟计时器发送给我们的朋友:
<main>
<h1>{title}</h1>
<PomodoroTimer {activeTask} />
<TaskList on:taskSelected={updateActiveTask}/>
</main>
番茄钟计时器满足活跃任务
但为了实现这一点,我们需要在PomodoroTimer组件内部定义一个新的属性:
<script>
export let activeTask;
</script>
由于只有在有任务处于活动状态时,用户才能与番茄钟计时器进行交互,因此我们可以先在这种情况下禁用番茄钟计时器:
<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>
凉爽的!
最后,我们可以在完成一个番茄钟后,增加该任务所花费的番茄钟数。我们更新了函数completePomodoro以PomodoroTimer.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);
}
}
但如果用户在番茄钟运行时删除任务会发生什么?良好的用户体验应该阻止用户这样做,要么在番茄钟运行时禁用删除按钮,要么向用户显示提示。不过,目前我们暂且将其作为一项附加练习或未来改进方案。
我们目前还没有显示完成任务所花费的番茄钟数,所以别忘了添加这个功能。回到TaskList.svelte组件中,我们更新组件标记以显示该信息:
<ul>
{#each tasks as task}
<li class:active={activeTask === task}>
<button on:click={() => selectTask(task)}>></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>
我们的风格:
.pomodoros.small {
max-width: 40px;
text-align: center;
}
.active input[disabled] {
opacity: 0.6;
}
锵锵!我们终于有了一款能正常运行的番茄工作法应用程序:
一种耦合度略低的替代方法
在实现上述任务和计时器集成时,我对TaskList组件和定时器PomodoroTimer都修改同一个对象这种做法感到有些不满activeTask。应用程序中能够访问和修改同一数据的地方越多,就越难推断应用程序的状态及其随时间的变化。这反过来意味着,与该数据相关的错误可能会在应用程序的许多不同地方出现。此外,将数据activeTask向上传递给父组件,然后再由父App组件向下传递,这种做法也显得有些繁琐PomodoroTimer。
PomodoroTimer这里提供一种替代方案,虽然牺牲了独立性TaskList,但可以减少所需的代码量并降低数据耦合度:
- 将
PomodoroTimer组件包含在TaskList组件内部 - 我们拥有所需的所有数据,因此可以
PomodoroTimer根据需要启用/禁用该功能。 - 计时器不会将值传递
activeTask给PomodoroTimer,而是通过事件来通知任务已完成,并且TaskList会更新该事件activeTask。
<PomodoroTimer disable={activeTask} on:completedPomodoro={() => activeTask.actualPomodoros++}/>
<ul>
<!-- list of tasks remains unchanged -->
</ul>
使用存储共享状态
在 Svelte 中,我们还可以使用store来共享状态。与通过 props 共享状态与 DOM 树和应用程序结构高度耦合不同,通过 store 共享状态完全独立于 DOM。使用 Svelte store,您只需导入一个 store,即可在应用程序的任何组件之间共享数据,无论它们位于何处。
什么是商店?
store 是一个对象,它可以帮助你在 Svelte 应用的各个组件之间共享数据。Store 独立于 DOM 树和应用的结构,因此当需要在彼此远离或需要解耦的组件之间共享状态时非常有用。Store 也是响应式的:每当其值发生变化时,它都会通知其消费者,以便它们能够做出相应的反应。组件可以通过 `subscribe`
subscribe方法订阅 store 来成为其消费者$(以下是一些示例)。有关商店的更多信息,请参阅 Svelte 的商店文档。
活动任务存储
让我们创建一个新的 store,以便在组件之间共享当前活动任务TaskList。PomodoroTimer组件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);
这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
由于activeTask它现在是一个存储对象,我们不能像以前那样直接设置它的值。所以,我们需要改成:
function selectTask(task) {
activeTask = task;
}
我们需要使用set商店的方法:
function selectTask(task) {
activeTask.set(task);
}
同样,activeTask这里不再指代 activeTask 本身,而是指代存储其值的 store。要检索任务的当前值,您需要使用相应的get方法。因此,不再是:
function removeTask(task){
if (activeTask === task){
selectTask(undefined);
}
tasks = tasks.remove(task);
}
我们写道:
// 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);
}
使用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;
}
这看起来和原始实现非常相似。是不是很棒?我们用它来管理状态,但它看起来几乎就像设置和读取一个普通的 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)}>></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>
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);
}
}
该模板用于在任务处于活动状态或非活动状态时启用和禁用计时器:
<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>
如果您现在查看一下页面(记住您可以使用 运行本地开发环境npm run dev),您会很高兴地发现一切仍然正常运行。太棒了!
Svelte有哪些类型的商店?
除了可写存储(Writable Store)之外,Svelte 还提供了可读存储(Readable Store)和派生存储(Different Store)。可读存储是指组件无法直接更新其值的存储。派生存储是指其值派生自其他存储的存储。当这些存储发生更改时,派生存储中的值也会随之更新。
道具与商店
现在我们已经分别使用 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 以更松耦合的方式在组件之间共享数据。
本系列的后续文章将深入探讨测试、异步编程、动画等内容。下次见!祝您度过美好的一天!
-
来看看我用 Knockout.js 写的这个超级老旧的番茄工作法应用,那是我刚开始做 Web 开发的时候写的 。↩