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

构建一个简单组件

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

我们将遵循组件驱动开发 (CDD) 方法来构建我们的 UI。这是一个“自下而上”构建 UI 的过程,从组件开始,到屏幕结束。CDD 帮助您扩展在构建 UI 时面临的复杂性。

任务

Task component in three states

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

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

当我们开始构建 Task 时,我们首先编写与上面草绘的不同类型的任务相对应的测试状态。然后,我们使用 Storybook 在隔离环境中,使用模拟数据构建组件。我们将“可视化测试”组件在每种状态下的外观。

设置

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

我们将从 Task 的基线实现开始,简单地接受我们知道我们需要哪些属性,以及您可以对任务执行的两个操作(在列表之间移动它)

复制
src/components/Task.svelte
<script>
  import { createEventDispatcher } from 'svelte';

  const dispatch = createEventDispatcher();

  /** Event handler for the Pin Task */
  function PinTask() {
    dispatch('onPinTask', {
      id: task.id,
    });
  }

  /** Event handler for the Archive Task */
  function ArchiveTask() {
    dispatch('onArchiveTask', {
      id: task.id,
    });
  }

  /** Composition of the task */
  export let task = {
    id: '',
    title: '',
    state: '',
  };
</script>

<div class="list-item">
  <label for="title" aria-label={task.title}>
    <input type="text" value={task.title} name="title" readonly />
  </label>
</div>

上面,我们基于 Todos 应用程序的现有 HTML 结构,为 Task 渲染了简单的标记。

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

复制
src/components/Task.stories.js
import Task from './Task.svelte';

import { action } from '@storybook/addon-actions';

export const actionsData = {
  onPinTask: action('onPinTask'),
  onArchiveTask: action('onArchiveTask'),
};

export default {
  component: Task,
  title: 'Task',
  tags: ['autodocs'],
  //👇 Our exports that end in "Data" are not stories.
  excludeStories: /.*Data$/,
  render: (args) => ({
    Component: Task,
    props: args,
    on: {
      ...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 组件时验证交互。通常,您无法访问在应用程序上下文中拥有的函数和状态。使用 action() 在其中进行存根。

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

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

为了告诉 Storybook 我们正在记录的组件,我们创建一个包含以下内容的 default 导出

  • component -- 组件本身
  • title -- 如何在 Storybook 应用程序的侧边栏中引用组件
  • excludeStories -- 故事所需的信息,但不应由 Storybook 应用程序呈现
  • tags -- 自动为我们的组件生成文档
  • render -- 一个函数,用于提供对故事渲染方式的额外控制

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

参数或 args 简称,允许我们使用 controls 插件实时编辑我们的组件,而无需重启 Storybook。一旦 args 值更改,组件也会随之更改。

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

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

在创建故事时,我们使用基本 task arg 来构建组件期望的任务的形状。通常从实际数据的外观建模而来。同样,export 此形状将使我们能够在以后的故事中重用它,我们将会看到。

配置

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

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

复制
.storybook/main.js
/** @type { import('@storybook/svelte-vite').StorybookConfig } */
const config = {
- stories: [
-   '../src/**/*.stories.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/svelte-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/svelte').Preview } */
const preview = {
  actions: { argTypesRegex: "^on.*" },
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

parameters 通常用于控制 Storybook 的功能和插件的行为。在我们的例子中,我们将使用它们来配置 actions(模拟回调)的处理方式。

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

一旦我们完成了这些,重启 Storybook 服务器应该会为三个 Task 状态产生测试用例

构建状态

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

该组件目前仍然是初步的。首先,编写实现设计效果的代码,而无需过多细节

复制
src/components/Task.svelte
<script>
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  /** Event handler for the Pin Task */
  function PinTask() {
    dispatch('onPinTask', { id: task.id });
  }

  /** Event handler for the Archive Task */
  function ArchiveTask() {
    dispatch('onArchiveTask', { id: task.id });
  }

  /** Composition of the task */
  export let task = {
    id: '',
    title: '',
    state: ''
  };

  /* Reactive declaration (computed prop in other frameworks) */
  $: isChecked = task.state === "TASK_ARCHIVED";
</script>

<div class="list-item {task.state}">
  <label
    for={`checked-${task.id}`}
    class="checkbox"
    aria-label={`archiveTask-${task.id}`}
  >
    <input
      type="checkbox"
      checked={isChecked}
      disabled
      name={`checked-${task.id}`}
      id={`archiveTask-${task.id}`}
    />
    <!-- svelte-ignore a11y-click-events-have-key-events -->
    <span
      class="checkbox-custom"
      role="button"
      on:click={ArchiveTask}
      tabindex="-1"
      aria-label={`archiveTask-${task.id}`}
    />
  </label>
  <label for={`title-${task.id}`} aria-label={task.title} class="title">
    <input
      type="text"
      value={task.title}
      readonly
      name="title"
      id={`title-${task.id}`}
      placeholder="Input title"
    />
  </label>
  {#if task.state !== 'TASK_ARCHIVED'}
    <button
      class="pin-button"
      on:click|preventDefault={PinTask}
      id={`pinTask-${task.id}`}
      aria-label={`pinTask-${task.id}`}
    >
      <span class="icon-star" />
    </button>
  {/if}
</div>

上面添加的标记与我们之前导入的 CSS 相结合,产生了以下 UI

组件已构建!

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

如您所见,开始在隔离环境中构建组件既简单又快速。我们可以期望生产更高质量的 UI,错误更少,并且更加完善,因为可以深入研究并测试每种可能的状态。

捕获可访问性问题

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

Storybook 包括一个官方的 可访问性插件。由 Deque 的 axe-core 提供支持,它可以捕获高达 57% 的 WCAG 问题

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

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

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

复制
.storybook/main.js
/** @type { import('@storybook/svelte-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/svelte-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 上编辑 – 欢迎 PR!
加入社区
6,721位开发者及更多
为什么为什么选择 Storybook组件驱动的 UI
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI