返回Storybook 简介
章节
  • 开始
  • 简单组件
  • 复合组件
  • 数据
  • 屏幕
  • 部署
  • 可视化测试
  • 插件
  • 结论
  • 贡献

连接数据

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

到目前为止,我们已经创建了隔离的无状态组件——这对 Storybook 来说很棒,但除非我们在应用中给它们一些数据,否则最终没有帮助。

本教程不关注构建应用的细节,因此我们不会在此处深入探讨这些细节。但我们将花一点时间看一下使用容器组件连接数据的常用模式。

容器组件

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

此示例使用 Pinia,Vue 的默认数据管理库,为我们的应用构建一个简单的数据模型。然而,此处使用的模式同样适用于其他数据管理库,如 Apollo 和 MobX。

使用以下命令将必要的依赖项添加到你的项目中

Copy
yarn add pinia

首先,我们将在 src 目录中创建一个名为 store.js 的简单 Pinia store,它响应更改任务状态的操作(有意保持简单)

Copy
src/store.js
/* A simple Pinia store/actions implementation.
 * A true app would be more complex and separated into different files.
 */
import { defineStore } from 'pinia';

/*
 * 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' },
];

/*
 * The store is created here.
 * You can read more about Pinia defineStore in the docs:
 * https://pinia.vuejs.ac.cn/core-concepts/
 */
export const useTaskStore = defineStore({
  id: 'taskbox',
  state: () => ({
    tasks: defaultTasks,
    status: 'idle',
    error: null,
  }),
  actions: {
    archiveTask(id) {
      const task = this.tasks.find((task) => task.id === id);
      if (task) {
        task.state = 'TASK_ARCHIVED';
      }
    },
    pinTask(id) {
      const task = this.tasks.find((task) => task.id === id);
      if (task) {
        task.state = 'TASK_PINNED';
      }
    },
  },
  getters: {
    getFilteredTasks: (state) => {
      const filteredTasks = state.tasks.filter(
        (t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED'
      );
      return filteredTasks;
    },
  },
});

然后我们将更新我们的 TaskList 以从 store 中读取数据。首先,让我们将现有的展示型版本移动到文件 `src/components/PureTaskList.vue`(将组件重命名为 `PureTaskList`),并用容器包装它。

In src/components/PureTaskList.vue

Copy
src/components/PureTaskList.vue
<template>
  <div class="list-items">
    <template v-if="loading">
      <div v-for="n in 6" :key="n" class="loading-item">
        <span class="glow-checkbox" />
        <span class="glow-text">
          <span>Loading</span> <span>cool</span> <span>state</span>
        </span>
      </div>
    </template>

    <div v-else-if="isEmpty" 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>

    <template v-else>
      <Task
        v-for="task in tasksInOrder"
        :key="task.id"
        :task="task"
        @archive-task="onArchiveTask"
        @pin-task="onPinTask"
      />
    </template>
  </div>
</template>
<script>
import Task from './Task.vue';
import { reactive, computed } from 'vue';

export default {
  name: 'PureTaskList',
  components: { Task },
  props: {
    tasks: { type: Array, required: true, default: () => [] },
    loading: { type: Boolean, default: false },
  },
  emits: ['archive-task', 'pin-task'],

  setup(props, { emit }) {
    props = reactive(props);
    return {
      isEmpty: computed(() => props.tasks.length === 0),
      tasksInOrder: computed(() => {
        return [
          ...props.tasks.filter((t) => t.state === 'TASK_PINNED'),
          ...props.tasks.filter((t) => t.state !== 'TASK_PINNED'),
        ];
      }),
      /**
       * Event handler for archiving tasks
       */
      onArchiveTask(taskId) {
        emit('archive-task', taskId);
      },
      /**
       * Event handler for pinning tasks
       */
      onPinTask(taskId) {
        emit('pin-task', taskId);
      },
    };
  },
};
</script>

In src/components/TaskList.vue

Copy
src/components/TaskList.vue
<template>
  <PureTaskList :tasks="tasks" @archive-task="archiveTask" @pin-task="pinTask" />
</template>

<script>
import PureTaskList from './PureTaskList.vue';

import { computed } from 'vue';

import { useTaskStore } from '../store';

export default {
  components: { PureTaskList },
  name: 'TaskList',
  setup() {
    //👇 Creates a store instance
    const store = useTaskStore();

    //👇 Retrieves the tasks from the store's state auxiliary getter function
    const tasks = computed(() => store.getFilteredTasks);

    //👇 Dispatches the actions back to the store
    const archiveTask = (task) => store.archiveTask(task);
    const pinTask = (task) => store.pinTask(task);

    return {
      tasks,
      archiveTask,
      pinTask,
    };
  },
};
</script>

将 TaskList 的展示型版本分开的原因是它更容易测试和隔离。因为它不依赖于 store 的存在,所以从测试的角度来看更容易处理。让我们将 `src/components/TaskList.stories.js` 重命名为 `src/components/PureTaskList.stories.js`,并确保我们的 stories 使用展示型版本

Copy
src/components/PureTaskList.stories.js
+ import PureTaskList from './PureTaskList.vue';

import * as TaskStories from './Task.stories';

export default {
+ component: PureTaskList,
+ title: 'PureTaskList',
  tags: ['autodocs'],
  decorators: [() => ({ template: '<div style="margin: 3em;"><story/></div>' })],
  args: {
    ...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 提交你的更改!

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

使你的代码与本章保持同步。在 GitHub 上查看 30e306d。
这个免费指南对你有帮助吗?发推文表示赞赏并帮助其他开发者找到它。
下一章
屏幕
用组件构建屏幕
✍️ 在 GitHub 上编辑 – 欢迎 PR!
加入社区
6,721位开发者及更多
为什么Why StorybookComponent-driven UI
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI