构建一个简单组件
我们将遵循组件驱动开发 (CDD) 方法来构建 UI。这是一个从“自下而上”构建 UI 的过程,从组件开始,到屏幕结束。CDD 有助于你在构建 UI 时应对不断增加的复杂性。
任务
Task
是我们应用的核心组件。每个任务的显示方式会根据其所处的具体状态而略有不同。我们显示一个已选中(或未选中)的复选框、一些关于任务的信息以及一个“固定”按钮,允许我们在列表中上下移动任务。综合考虑,我们需要这些 props
title
– 描述任务的字符串state
- 任务当前在哪个列表中,以及是否已完成?
当我们开始构建 Task
时,首先编写对应于上面勾勒出的不同类型任务的测试状态。然后使用 Storybook 通过模拟数据独立构建组件。在此过程中,我们将对组件在每种状态下的外观进行“视觉测试”。
准备工作
首先,让我们创建任务组件及其配套的故事文件:src/components/Task.svelte
和 src/components/Task.stories.js
。
我们将从 Task
的基本实现开始,只需传入我们知道需要的属性以及对任务可以执行的两个操作(在列表之间移动它)。
<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 的三种测试状态
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
-- 一个函数,提供对故事如何渲染的额外控制
为了定义我们的故事,我们将使用 Component Story Format 3(也称为 CSF3 )来构建我们的每个测试用例。这种格式旨在以简洁的方式构建我们的每个测试用例。通过导出包含每个组件状态的对象,我们可以更直观地定义测试,并更有效地编写和重用故事。
参数,简称 args
,允许我们在不重启 Storybook 的情况下使用 controls 插件实时编辑组件。一旦一个 args
值改变,组件也会随之改变。
action()
允许我们创建一个回调,当点击时会出现在 Storybook UI 的 Actions 面板中。因此,当我们构建一个固定按钮时,将能够在 UI 中确定按钮点击是否成功。
由于我们需要将同一组 actions 传递给组件的所有变体,因此方便的做法是将它们打包到一个 actionsData
变量中,并在每次定义故事时传入。将组件所需的 actionsData
打包起来的另一个好处是,你可以 export
它们,并在重用此组件的组件故事中使用它们,正如我们稍后会看到的那样。
创建故事时,我们使用一个基本的 task
arg 来构建组件所期望的任务形状。这通常模拟实际数据的样子。同样,export
这个形状将使我们能够在后面的故事中重用它,正如我们稍后会看到的。
配置
我们需要对 Storybook 的配置文件进行一些更改,以便它注意到我们最近创建的故事,并允许我们使用应用程序的 CSS 文件(位于 src/index.css
)。
首先,将你的 Storybook 配置文件(.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
更改为以下内容
+ 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 以匹配设计。
目前组件还很基础。首先,编写实现设计但不深入细节的代码
<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 组件。
如你所见,独立构建组件既简单又快速。我们可以期待生成质量更高、bug 更少、更精美的 UI,因为可以深入测试每一种可能的状态。
捕获可访问性问题
可访问性测试是指使用自动化工具根据基于 WCAG 规则和其他行业公认的最佳实践的一系列启发式方法来审计渲染后的 DOM。它们是 QA 的第一道防线,用于捕获明显的可访问性违规行为,确保应用程序对尽可能多的人可用,包括视力障碍、听力问题和认知障碍等残疾人士。
Storybook 包含一个官方的可访问性插件。它由 Deque 的 axe-core 提供支持,可以捕获高达 57% 的 WCAG 问题。
让我们看看它是如何工作的!运行以下命令安装插件
yarn add --dev @storybook/addon-a11y
然后,更新你的 Storybook 配置文件(.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 中看到新插件已启用。
浏览我们的故事,我们可以看到该插件发现了一个测试状态存在可访问性问题。消息 “元素必须具有足够的颜色对比度” 基本上意味着任务标题与背景之间没有足够的对比度。我们可以通过在应用程序的 CSS(位于 src/index.css
)中将文本颜色更改为更深的灰色来快速修复它。
.list-item.TASK_ARCHIVED input[type="text"] {
- color: #a0aec0;
+ color: #4a5568;
text-decoration: line-through;
}
就是这样!我们已经迈出了确保 UI 可访问性的第一步。随着我们不断增加应用程序的复杂性,我们可以对所有其他组件重复此过程,而无需启动额外的工具或测试环境。