返回到Storybook 简介
章节
  • 入门
  • 简单组件
  • 组合组件
  • 数据
  • 屏幕
  • 部署
  • 可视化测试
  • 插件
  • 总结
  • 贡献

构建一个简单组件

在隔离环境中构建一个简单组件

我们将遵循一个 组件驱动开发 (CDD) 的方法构建 UI。这是一个从“自下而上”构建 UI 的过程,从组件开始,以屏幕结束。CDD 有助于你在构建 UI 时应对不断增加的复杂性。

Task

Task component in three states

Task 是我们应用的核心组件。每个任务根据其所处的具体状态显示略有不同。我们显示一个选中(或未选中)的复选框、一些关于任务的信息,以及一个“固定”按钮,允许我们将任务在列表中上下移动。综上所述,我们需要这些 props

  • title – 描述任务的字符串
  • state - 任务当前在哪一个列表,以及是否已完成?

当我们开始构建 Task 时,我们首先编写对应于上面勾勒的不同类型任务的测试状态。然后我们使用 Storybook 在隔离环境中,使用模拟数据创建组件。在此过程中,我们将手动测试组件在每种状态下的外观。

设置

首先,让我们创建任务组件及其配套的故事文件:src/components/Task.vuesrc/components/Task.stories.js

我们将从 Task 的基本实现开始,只需接收我们已知需要的属性以及你可以对任务执行的两个操作(在列表之间移动)

复制
src/components/Task.vue
<template>
  <div class="list-item">
    <label for="title" :aria-label="task.title">
      <input type="text" readonly :value="task.title" id="title" name="title" />
    </label>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Task',
  props: {
    task: {
      type: Object,
      required: true,
      default: () => ({ id: '', state: '', title: '' }),
      validator: (task) => ['id', 'state', 'title'].every((key) => key in task)
    }
  }
}
</script>

上面,我们根据 Todos 应用现有的 HTML 结构渲染了 Task 的简单标记。

下面我们在故事文件中构建 Task 的三种测试状态

复制
src/components/Task.stories.js
import { fn } from '@storybook/test';

import Task from './Task.vue';

export const ActionsData = {
  onPinTask: fn(),
  onArchiveTask: fn(),
};

export default {
  component: Task,
  title: 'Task',
  tags: ['autodocs'],
  //👇 Our exports that end in "Data" are not stories.
  excludeStories: /.*Data$/,
  args: {
    ...ActionsData
  }
};

export const Default = {
  args: {
    task: {
      id: '1',
      title: 'Test Task',
      state: 'TASK_INBOX',
    },
  },
};

export const Pinned = {
  args: {
    task: {
      ...Default.args.task,
      state: 'TASK_PINNED',
    },
  },
};

export const Archived = {
  args: {
    task: {
      ...Default.args.task,
      state: 'TASK_ARCHIVED',
    },
  },
};

💡 Actions 有助于你在隔离环境中构建 UI 组件时验证交互。通常你无法访问应用上下文中的函数和状态。使用 fn() 来模拟它们。

Storybook 中有两种基本的组织级别:组件及其子故事。将每个故事视为组件的一种排列。每个组件可以拥有任意数量的故事。

  • 组件
    • 故事
    • 故事
    • 故事

为了让 Storybook 了解我们正在文档化的组件,我们创建一个包含以下内容的 default 导出

  • component -- 组件本身
  • title -- 如何在 Storybook 侧边栏中对组件进行分组或分类
  • tags -- 自动生成组件文档
  • excludeStories -- 故事所需但不需要在 Storybook 中渲染的附加信息
  • args -- 定义组件期望用来模拟自定义事件的 action args

为了定义我们的故事,我们将使用 Component Story Format 3 (也称为 CSF3 ) 来构建每个测试用例。这种格式旨在以简洁的方式构建每个测试用例。通过导出包含每个组件状态的对象,我们可以更直观地定义测试,并更高效地创作和重用故事。

Arguments,简称 args,允许我们在不重启 Storybook 的情况下使用 controls 插件实时编辑组件。一旦 args 值发生变化,组件也会随之改变。

fn() 允许我们创建一个回调函数,当点击时,它会出现在 Storybook UI 的 Actions 面板中。因此,当我们构建一个固定按钮时,我们将能够在 UI 中确定按钮点击是否成功。

由于我们需要将同一组 actions 传递给组件的所有排列,因此将它们捆绑到一个单独的 ActionsData 变量中并在每次故事定义中传递它们是很方便的。捆绑组件所需的 ActionsData 的另一个好处是,你可以 export 它们,并在重用此组件的组件故事中使用它们,稍后我们将看到。

创建故事时,我们使用一个基础 task arg 来构建组件期望的任务形状。通常根据实际数据建模。同样,export 这种形状将使我们能够在后面的故事中重用它,稍后我们将看到。

配置

我们需要对 Storybook 的配置文件进行一些更改,以便它能注意到我们最近创建的故事,并允许我们使用应用的 CSS 文件(位于 src/index.css)。

首先将你的 Storybook 配置文件 (.storybook/main.js) 更改为以下内容

复制
.storybook/main.js
/** @type { import('@storybook/vue3-vite').StorybookConfig } */
const config = {
- stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
+ stories: ['../src/components/**/*.stories.js'],
  staticDirs: ['../public'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/vue3-vite',
    options: {},
  },
};
export default config;

完成上述更改后,在 .storybook 文件夹内,将你的 preview.js 更改为以下内容

复制
.storybook/preview.js
+ import '../src/index.css';

//👇 Configures Storybook to log the actions( onArchiveTask and onPinTask ) in the UI.
/** @type { import('@storybook/vue3').Preview } */
const preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
};

export default preview;

parameters 通常用于控制 Storybook 功能和插件的行为。在本例中,我们不会为此目的使用它们。相反,我们将导入我们应用的 CSS 文件。

完成这些后,重新启动 Storybook 服务器应该会生成 Task 的三种状态的测试用例

构建状态

现在我们已经设置好 Storybook,导入了样式,并构建了测试用例,我们可以快速开始实现组件的 HTML 以匹配设计。

目前组件仍然是初级的。首先,编写实现设计的代码,无需深入太多细节

复制
src/components/Task.vue
<template>
  <div :class="classes">
    <label
      :for="'checked' + task.id"
      :aria-label="'archiveTask-' + task.id"
      class="checkbox"
    >
      <input
        type="checkbox"
        :checked="isChecked"
        disabled
        :name="'checked' + task.id"
        :id="'archiveTask-' + task.id"
      />
      <span class="checkbox-custom" @click="archiveTask" />
    </label>
    <label :for="'title-' + task.id" :aria-label="task.title" class="title">
      <input
        type="text"
        readonly
        :value="task.title"
        :id="'title-' + task.id"
        name="title"
        placeholder="Input title"
      />
    </label>
    <button
      v-if="!isChecked"
      class="pin-button"
      @click="pinTask"
      :id="'pinTask-' + task.id"
      :aria-label="'pinTask-' + task.id"
    >
      <span class="icon-star" />
    </button>
  </div>
</template>

<script>
import { reactive, computed } from 'vue';

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Task',
  props: {
    task: {
      type: Object,
      required: true,
      default: () => ({ id: '', state: '', title: '' }),
      validator: task => ['id', 'state', 'title'].every(key => key in task),
    },
  },
  emits: ['archive-task', 'pin-task'],

  setup(props, { emit }) {
    props = reactive(props);
    return {
      classes: computed(() => ({
        'list-item TASK_INBOX': props.task.state === 'TASK_INBOX',
        'list-item TASK_PINNED': props.task.state === 'TASK_PINNED',
        'list-item TASK_ARCHIVED': props.task.state === 'TASK_ARCHIVED',
      })),
      /**
       * Computed property for checking the state of the task
       */
      isChecked: computed(() => props.task.state === 'TASK_ARCHIVED'),
      /**
       * Event handler for archiving tasks
       */
      archiveTask() {
        emit('archive-task', props.task.id);
      },
      /**
       * Event handler for pinning tasks
       */
      pinTask() {
        emit('pin-task', props.task.id);
      },
    };
  },
};
</script>

上面额外的标记与我们之前导入的 CSS 相结合,生成了以下 UI

组件已构建完成!

现在我们已经成功构建了一个组件,而无需服务器或运行整个前端应用。下一步是按照类似的方式逐一构建剩余的 Taskbox 组件。

正如你所见,在隔离环境中开始构建组件是容易且快速的。我们可以期待产生更高质量、更少 bug、更精美的 UI,因为可以深入测试每种可能的状态。

发现可访问性问题

可访问性测试是指使用自动化工具,根据基于 WCAG 规则和其他业界公认的最佳实践的一系列启发式规则来审计渲染的 DOM 的做法。它们是第一道质量保证防线,用于捕获明显的无障碍违规行为,确保应用程序尽可能多的人可以使用,包括视力障碍、听力问题和认知障碍等残疾人士。

Storybook 包含一个官方的 accessibility addon。它由 Deque 的 axe-core 提供支持,可以捕获高达 57% 的 WCAG 问题

让我们看看它是如何工作的!运行以下命令安装插件

复制
yarn add --dev @storybook/addon-a11y

然后,更新你的 Storybook 配置文件 (.storybook/main.js) 来启用它

复制
.storybook/main.js
/** @type { import('@storybook/vue3-vite').StorybookConfig } */
const config = {
  stories: ['../src/components/**/*.stories.js'],
  staticDirs: ['../public'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
+   '@storybook/addon-a11y',
  ],
  framework: {
    name: '@storybook/vue3-vite',
    options: {},
  },
};
export default config;

最后,重新启动 Storybook 以在 UI 中查看新启用的插件。

Task accessibility issue in Storybook

循环查看我们的故事时,我们可以看到插件在其中一个测试状态下发现了一个可访问性问题。消息 “元素必须有足够的颜色对比度” 本质上意味着任务标题和背景之间没有足够的对比度。我们可以通过在应用 CSS 文件(位于 src/index.css)中将文本颜色更改为更深的灰色来快速解决它。

复制
src/index.css
.list-item.TASK_ARCHIVED input[type="text"] {
- color: #a0aec0;
+ color: #4a5568;
  text-decoration: line-through;
}

就是这样!我们已经迈出了确保 UI 可访问性的第一步。随着我们不断增加应用的复杂性,我们可以对所有其他组件重复此过程,而无需启动额外的工具或测试环境。

💡 别忘了用 git 提交你的更改!
保持你的代码与本章同步。在 GitHub 上查看 b586083。
这份免费指南对你有帮助吗?发推点赞并帮助其他开发者找到它。
下一章
组合组件
从更简单的组件组装一个组合组件
✍️ 在 GitHub 上编辑 – 欢迎 PR!
加入社区
6,975位开发者,还在增长中
为什么为什么选择 Storybook组件驱动 UI
开源软件
Storybook - Storybook 中文

特别鸣谢 Netlify CircleCI