接入数据
到目前为止,我们已经创建了独立的无状态组件——这对于 Storybook 来说很棒,但最终在我们给它们应用中的一些数据之前,它们并没有什么用处。
本教程不侧重于构建应用程序的细节,因此我们不会在此深入探讨这些细节。但是,我们将花一点时间来了解使用容器组件连接数据的常用模式。
容器组件
我们当前编写的 TaskList
组件是“展示型”的,因为它不与其自身实现之外的任何东西对话。为了将数据导入其中,我们需要一个“容器”。
此示例使用 Svelte 的 Stores,Svelte 的默认数据管理 API,为我们的应用程序构建一个简单的数据模型。然而,这里使用的模式同样适用于其他数据管理库,如 Apollo 和 MobX。
首先,我们将构建一个简单的 Svelte store,它响应更改任务状态的操作,该 store 位于 src
目录中名为 store.js
的文件中(有意保持简单)
// A simple Svelte store implementation with update methods and initial data.
// A true app would be more complex and separated into different files.
import { writable } from 'svelte/store';
/*
* 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 = [
{ 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 TaskBox = () => {
// Creates a new writable store populated with some initial data
const { subscribe, update } = writable({
tasks: defaultTasks,
status: 'idle',
error: false,
});
return {
subscribe,
// Method to archive a task, think of a action with redux or Pinia
archiveTask: (id) =>
update((store) => {
const filteredTasks = store.tasks
.map((task) =>
task.id === id ? { ...task, state: 'TASK_ARCHIVED' } : task
)
.filter((t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED');
return { ...store, tasks: filteredTasks };
}),
// Method to archive a task, think of a action with redux or Pinia
pinTask: (id) => {
update((store) => {
const task = store.tasks.find((t) => t.id === id);
if (task) {
task.state = 'TASK_PINNED';
}
return store;
});
},
};
};
export const taskStore = TaskBox();
然后,我们将更新我们的 TaskList
以从 store 中读取数据。首先,让我们将现有的展示型版本移动到文件 src/components/PureTaskList.svelte
并用容器包裹它。
在 src/components/PureTaskList.svelte
中
<!--This file moved from TaskList.svelte-->
<script>
import Task from './Task.svelte';
import LoadingRow from './LoadingRow.svelte';
/* Sets the loading state */
export let loading = false;
/* Defines a list of tasks */
export let tasks = [];
/* Reactive declaration (computed prop in other frameworks) */
$: noTasks = tasks.length === 0;
$: emptyTasks = noTasks && !loading;
$: tasksInOrder = [
...tasks.filter((t) => t.state === 'TASK_PINNED'),
...tasks.filter((t) => t.state !== 'TASK_PINNED'),
];
</script>
{#if loading}
<div class="list-items">
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
</div>
{/if}
{#if emptyTasks}
<div class="list-items">
<div class="wrapper-message">
<span class="icon-check" />
<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} on:onPinTask on:onArchiveTask />
{/each}
在 src/components/TaskList.svelte
中
<script>
import PureTaskList from './PureTaskList.svelte';
import { taskStore } from '../store';
function onPinTask(event) {
taskStore.pinTask(event.detail.id);
}
function onArchiveTask(event) {
taskStore.archiveTask(event.detail.id);
}
</script>
<PureTaskList
tasks={$taskStore.tasks}
on:onPinTask={onPinTask}
on:onArchiveTask={onArchiveTask}
/>
将 TaskList
的展示型版本分开的原因是它更容易测试和隔离。因为它不依赖于 store 的存在,所以从测试的角度来看,处理起来要容易得多。让我们将 src/components/TaskList.stories.js
重命名为 src/components/PureTaskList.stories.js
,并确保我们的 stories 使用展示型版本
import PureTaskList from './PureTaskList.svelte';
import MarginDecorator from './MarginDecorator.svelte';
import * as TaskStories from './Task.stories';
export default {
component: PureTaskList,
title: 'PureTaskList',
tags: ['autodocs'],
//👇 The auxiliary component will be added as a decorator to help show the UI correctly
decorators: [() => MarginDecorator],
render: (args) => ({
Component: PureTaskList,
props: args,
on: {
...TaskStories.actionsData,
},
}),
};
export const Default = {
args: {
// Shaping the stories through args composition.
// The data was inherited from the Default story in task.stories.js.
tasks: [
{ ...TaskStories.Default.args.task, id: '1', title: 'Task 1' },
{ ...TaskStories.Default.args.task, id: '2', title: 'Task 2' },
{ ...TaskStories.Default.args.task, id: '3', title: 'Task 3' },
{ ...TaskStories.Default.args.task, id: '4', title: 'Task 4' },
{ ...TaskStories.Default.args.task, id: '5', title: 'Task 5' },
{ ...TaskStories.Default.args.task, id: '6', title: 'Task 6' },
],
},
};
export const WithPinnedTasks = {
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Default story.
tasks: [
...Default.args.tasks.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
],
},
};
export const Loading = {
args: {
tasks: [],
loading: true,
},
};
export const Empty = {
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Loading story.
...Loading.args,
loading: false,
},
};
现在我们已经有一些实际数据填充我们的组件,这些数据是从 Svelte store 中获得的,我们可以将其连接到 src/App.svelte
并在那里渲染组件。不用担心。我们将在下一章中处理它。