返回Storybook 简介
章节
  • 开始上手
  • 简单的组件
  • 复合组件
  • 数据
  • 页面
  • 部署
  • 视觉测试
  • 可访问性测试
  • 结论
  • 贡献

集成数据

了解如何将数据集成到你的 UI 组件中

到目前为止,我们已经创建了独立的无状态组件——非常适合 Storybook,但直到我们为它们提供一些应用程序中的数据,它们最终都没有多大用处。

本次教程不侧重于构建应用程序的细节,所以我们在这里不深入探讨。但我们会花点时间来看一个常见的模式,即如何通过容器组件来连接数据。

容器组件

我们当前的 TaskList 组件是“展示型”的,因为它不与自身实现之外的任何东西进行交互。要向其中获取数据,我们需要一个“容器”。

在本教程中,我们将使用 Svelte 的 runes,这是一种强大的响应式系统,提供了显式、细粒度的响应式原语来实现一个简单的 store。我们将使用 $state rune 来构建应用程序的一个简单数据模型,并帮助我们管理任务的状态。

首先,我们将构建一个简单的 store,它响应在一个名为 store.svelte.ts 的文件中(故意保持简单)对任务状态进行更改的操作,该文件位于 src/lib/state 目录中。

复制
src/lib/state/store.svelte.ts
// 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

复制
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

复制
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 使用展示型版本。

复制
src/lib/components/PureTaskList.stories.svelte
<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,
  }}
/>
💡 别忘了使用 git 提交你的更改!

既然我们的组件现在由 Svelte store 填充了一些实际数据,我们可以将其连接到 src/App.svelte 并在那里渲染该组件。不用担心,我们将在下一章处理这个问题。

这个免费指南对您有帮助吗?请在 Twitter 上分享以表示赞赏,并帮助其他开发者发现它。
下一章
页面
用组件构建一个页面
✍️ 在 GitHub 上编辑 – PR 欢迎!
加入社区
7,424开发者及更多
原因为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与进来博客
展示探索关于
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI