集成数据
到目前为止,我们已经创建了独立的无状态组件——非常适合 Storybook,但直到我们为它们提供一些应用程序中的数据,它们最终都没有多大用处。
本次教程不侧重于构建应用程序的细节,所以我们在这里不深入探讨。但我们会花点时间来看一个常见的模式,即如何通过容器组件来连接数据。
容器组件
我们当前的 TaskList 组件是“展示型”的,因为它不与自身实现之外的任何东西进行交互。要向其中获取数据,我们需要一个“容器”。
在本教程中,我们将使用 Svelte 的 runes,这是一种强大的响应式系统,提供了显式、细粒度的响应式原语来实现一个简单的 store。我们将使用 $state rune 来构建应用程序的一个简单数据模型,并帮助我们管理任务的状态。
首先,我们将构建一个简单的 store,它响应在一个名为 store.svelte.ts 的文件中(故意保持简单)对任务状态进行更改的操作,该文件位于 src/lib/state 目录中。
// A simple Svelte state management implementation using runes update methods and initial data.
// A true app would be more complex and separated into different files.
import type { TaskData } from '../../types';
interface TaskBoxState {
tasks: TaskData[];
status: 'idle' | 'loading' | 'failed' | 'succeeded';
error: string | null;
}
/*
* The initial state of our store when the app loads.
* Usually, you would fetch this from a server. Let's not worry about that now
*/
const defaultTasks: TaskData[] = [
{ id: '1', title: 'Something', state: 'TASK_INBOX' },
{ id: '2', title: 'Something more', state: 'TASK_INBOX' },
{ id: '3', title: 'Something else', state: 'TASK_INBOX' },
{ id: '4', title: 'Something again', state: 'TASK_INBOX' },
];
const initialState: TaskBoxState = {
tasks: defaultTasks,
status: 'idle',
error: null,
};
export const store: TaskBoxState = $state(initialState);
// Function that archives a task
export function archiveTask(id: string) {
const filteredTasks = store.tasks
.map((task): TaskData =>
task.id === id ? { ...task, state: 'TASK_ARCHIVED' as TaskData['state'] } : task
)
.filter((t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED');
store.tasks = filteredTasks;
}
// Function that pins a task
export function pinTask(id: string) {
const task = store.tasks.find((task) => task.id === id);
if (task) {
task.state = 'TASK_PINNED';
}
}
然后,我们将更新 TaskList 以从中读取数据。首先,让我们将现有的展示型版本移到 src/lib/components/PureTaskList.svelte 文件中,并用一个容器将其包装起来。
在 src/lib/components/PureTaskList.svelte 中
<!--This file moved from TaskList.svelte-->
<script lang="ts">
import type { TaskData } from '../../types';
import Task from './Task.svelte';
import LoadingRow from './LoadingRow.svelte';
interface Props {
/** Checks if it's in loading state */
loading?: boolean;
/** The list of tasks */
tasks: TaskData[];
/** Event to change the task to pinned */
onPinTask: (id: string) => void;
/** Event to change the task to archived */
onArchiveTask: (id: string) => void;
}
const {
loading = false,
tasks = [],
onPinTask,
onArchiveTask,
}: Props = $props();
let noTasks = $derived(tasks.length === 0);
let tasksInOrder = $derived([
...tasks.filter((t) => t.state === 'TASK_PINNED'),
...tasks.filter((t) => t.state !== 'TASK_PINNED'),
]);
</script>
{#if loading}
<div class="list-items" data-testid="loading" id="loading">
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
</div>
{/if}
{#if !loading && noTasks}
<div class="list-items">
<div class="wrapper-message">
<span class="icon-check"></span>
<p class="title-message">You have no tasks</p>
<p class="subtitle-message">Sit back and relax</p>
</div>
</div>
{/if}
{#each tasksInOrder as task}
<Task {task} {onPinTask} {onArchiveTask} />
{/each}
在 src/lib/components/TaskList.svelte 中
<script lang="ts">
import { archiveTask, pinTask, store } from '../state/store.svelte';
import PureTaskList from './PureTaskList.svelte';
</script>
<PureTaskList
loading={store.status === "loading"}
tasks={store.tasks}
onPinTask={pinTask}
onArchiveTask={archiveTask}
/>
将 TaskList 的展示型版本单独存放的原因是它更容易测试和隔离。由于它不依赖于 store 的存在,因此从测试角度来看,它更容易处理。让我们将 src/lib/components/TaskList.stories.svelte 重命名为 src/lib/components/PureTaskList.stories.svelte,并确保我们的 story 使用展示型版本。
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import PureTaskList from './PureTaskList.svelte';
import MarginDecorator from './MarginDecorator.svelte';
import * as TaskStories from './Task.stories.svelte';
export const TaskListData = [
{ ...TaskStories.TaskData, id: '1', title: 'Task 1' },
{ ...TaskStories.TaskData, id: '2', title: 'Task 2' },
{ ...TaskStories.TaskData, id: '3', title: 'Task 3' },
{ ...TaskStories.TaskData, id: '4', title: 'Task 4' },
{ ...TaskStories.TaskData, id: '5', title: 'Task 5' },
{ ...TaskStories.TaskData, id: '6', title: 'Task 6' },
];
const { Story } = defineMeta({
component: PureTaskList,
title: 'PureTaskList',
tags: ['autodocs'],
excludeStories: /.*Data$/,
decorators: [() => MarginDecorator],
args: {
...TaskStories.TaskData.events,
},
});
</script>
<Story
name="Default"
args={{
tasks: TaskListData,
loading: false,
}}
/>
<Story
name="WithPinnedTasks"
args={{
tasks: [
...TaskListData.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
],
}}
/>
<Story
name="Loading"
args={{
tasks: [],
loading: true,
}}
/>
<Story
name="Empty"
args={{
tasks: TaskListData.slice(0, 0),
loading: false,
}}
/>
既然我们的组件现在由 Svelte store 填充了一些实际数据,我们可以将其连接到 src/App.svelte 并在那里渲染该组件。不用担心,我们将在下一章处理这个问题。