返回Storybook 简介
章节
  • 入门
  • 简单组件
  • 复合组件
  • 数据
  • 屏幕
  • 部署
  • 视觉测试
  • 插件
  • 结论
  • 贡献

接入数据

了解如何将数据接入到你的 UI 组件

到目前为止,我们已经创建了独立的无状态组件——这对于 Storybook 来说很棒,但最终在我们给它们应用中的一些数据之前,它们并没有什么用处。

本教程不侧重于构建应用程序的细节,因此我们不会在此深入探讨这些细节。但是,我们将花一点时间来了解使用容器组件连接数据的常用模式。

容器组件

我们当前编写的 TaskList 组件是“展示型”的,因为它不与其自身实现之外的任何东西对话。为了将数据导入其中,我们需要一个“容器”。

此示例使用 Svelte 的 Stores,Svelte 的默认数据管理 API,为我们的应用程序构建一个简单的数据模型。然而,这里使用的模式同样适用于其他数据管理库,如 ApolloMobX

首先,我们将构建一个简单的 Svelte store,它响应更改任务状态的操作,该 store 位于 src 目录中名为 store.js 的文件中(有意保持简单)

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

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

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

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

现在我们已经有一些实际数据填充我们的组件,这些数据是从 Svelte store 中获得的,我们可以将其连接到 src/App.svelte 并在那里渲染组件。不用担心。我们将在下一章中处理它。

这个免费指南对您有帮助吗?发推文表示赞赏并帮助其他开发者找到它。
下一章
屏幕
用组件构建屏幕
✍️ 在 GitHub 上编辑 – 欢迎 PR!
加入社区
6,721位开发者及更多
为什么为什么选择 Storybook组件驱动的 UI
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI