构建一个简单的组件
我们将遵循组件驱动开发 (CDD) 方法来构建我们的 UI。这是一个“自下而上”构建 UI 的过程,从组件开始,到屏幕结束。CDD 帮助您扩展在构建 UI 时面临的复杂性。
任务
Task
是我们应用程序的核心组件。每个任务的显示方式略有不同,具体取决于其所处的确切状态。我们显示一个选中(或未选中)的复选框、一些关于任务的信息和一个“置顶”按钮,允许我们上下移动列表中的任务。综合起来,我们需要以下 props
title
– 描述任务的字符串state
- 任务当前在哪个列表中,以及是否已选中?
当我们开始构建 Task
时,我们首先编写与上面草图中的不同任务类型相对应的测试状态。然后我们使用 Storybook 来隔离创建组件,并使用模拟数据。我们将手动测试组件在每种状态下的外观。
准备设置
首先,让我们创建任务组件及其配套的故事文件:src/app/components/task.component.ts
和 src/app/components/task.stories.ts
。
我们将从 Task
组件的基线实现开始,只需接受我们知道需要的输入以及可以对任务执行的两个操作(在列表之间移动它)
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-task',
standalone: false,
template: `
<div class="list-item">
<label [attr.aria-label]="task.title + ''" for="title">
<input
type="text"
[value]="task.title"
readonly="true"
id="title"
name="title"
/>
</label>
</div>
`,
})
export default class TaskComponent {
/**
* The shape of the task object
*/
@Input() task: any;
// tslint:disable-next-line: no-output-on-prefix
@Output()
onPinTask = new EventEmitter<Event>();
// tslint:disable-next-line: no-output-on-prefix
@Output()
onArchiveTask = new EventEmitter<Event>();
}
上面,我们基于 Todos 应用程序的现有 HTML 结构,为 Task
组件渲染了直接的标记。
下面我们在故事文件中构建 Task 的三个测试状态
import type { Meta, StoryObj } from '@storybook/angular';
import { fn } from '@storybook/test';
import TaskComponent from './task.component';
export const ActionsData = {
onArchiveTask: fn(),
onPinTask: fn(),
};
const meta: Meta<TaskComponent> = {
title: 'Task',
component: TaskComponent,
//👇 Our exports that end in "Data" are not stories.
excludeStories: /.*Data$/,
tags: ['autodocs'],
args: {
...ActionsData,
},
};
export default meta;
type Story = StoryObj<TaskComponent>;
export const Default: Story = {
args: {
task: {
id: '1',
title: 'Test Task',
state: 'TASK_INBOX',
},
},
};
export const Pinned: Story = {
args: {
task: {
...Default.args?.task,
state: 'TASK_PINNED',
},
},
};
export const Archived: Story = {
args: {
task: {
...Default.args?.task,
state: 'TASK_ARCHIVED',
},
},
};
💡 Actions 帮助您在隔离构建 UI 组件时验证交互。通常,您将无法访问应用程序上下文中拥有的函数和状态。使用 fn()
在其中存根它们。
Storybook 中有两个基本的组织级别:组件及其子故事。将每个故事视为组件的排列。每个组件可以根据需要拥有任意数量的故事。
- 组件
- 故事
- 故事
- 故事
为了告诉 Storybook 我们正在记录的组件,我们创建一个包含以下内容的 default
导出
component
-- 组件本身title
-- 如何在 Storybook 侧边栏中分组或分类组件tags
-- 为我们的组件自动生成文档excludeStories
-- 故事所需的附加信息,但不应在 Storybook 中呈现args
-- 定义组件期望模拟自定义事件的操作 args
为了定义我们的故事,我们将使用 Component Story Format 3(也称为 CSF3)来构建我们的每个测试用例。此格式旨在以简洁的方式构建我们的每个测试用例。通过导出包含每个组件状态的对象,我们可以更直观地定义我们的测试,并更有效地编写和重用故事。
参数或简称为 args
,允许我们使用 controls addon 实时编辑我们的组件,而无需重启 Storybook。一旦 args
值更改,组件也会随之更改。
fn()
允许我们创建一个回调,当单击时,该回调会出现在 Storybook UI 的 Actions 面板中。因此,当我们构建一个置顶按钮时,我们将能够确定 UI 中的按钮单击是否成功。
由于我们需要将相同的一组操作传递给我们组件的所有排列,因此将它们捆绑到一个 ActionsData
变量中并在每次将它们传递到我们的故事定义中是很方便的。捆绑组件需要的 ActionsData
的另一个好处是您可以 export
它们并在重用此组件的组件的故事中使用它们,我们稍后会看到。
在创建故事时,我们使用基本的 task
arg 来构建组件期望的任务形状。通常从实际数据看起来的样子建模。同样,export
这个形状将使我们能够在以后的故事中重用它,我们将会看到。
配置
我们还需要对 Storybook 配置进行一个小更改,以注意到我们最近创建的故事。将您的配置文件 (.storybook/main.ts
) 更改为以下内容
import type { StorybookConfig } from '@storybook/angular';
const config: StorybookConfig = {
- stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
+ stories: ['../src/app/components/**/*.stories.ts'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/angular',
options: {},
},
};
export default config;
完成此操作后,重启 Storybook 服务器应该会为 TaskComponent 的三种状态生成测试用例
指定数据要求
最佳实践是指定组件期望的数据形状。它不仅是自我记录的,而且还有助于及早发现问题。在这里,我们将使用 Typescript 并为 Task
模型创建一个接口。
在 app
目录中,添加一个名为 models
的新目录,然后添加一个名为 task.model.ts
的新文件
export interface Task {
id?: string;
title?: string;
state?: string;
}
构建状态
现在我们已经设置了 Storybook、导入了样式并构建了测试用例,我们可以快速开始实现组件的 HTML 以匹配设计。
该组件目前仍然是初步的。首先,编写实现设计的代码,而无需过于详细
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Task } from '../models/task.model';
@Component({
selector: 'app-task',
standalone: false,
template: `
<div class="list-item {{ task?.state }}">
<label
[attr.aria-label]="'archiveTask-' + task?.id"
for="checked-{{ task?.id }}"
class="checkbox"
>
<input
type="checkbox"
disabled="true"
[defaultChecked]="task?.state === 'TASK_ARCHIVED'"
name="checked-{{ task?.id }}"
id="checked-{{ task?.id }}"
/>
<span class="checkbox-custom" (click)="onArchive(task?.id)"></span>
</label>
<label
[attr.aria-label]="task?.title + ''"
for="title-{{ task?.id }}"
class="title"
>
<input
type="text"
[value]="task?.title"
readonly="true"
id="title-{{ task?.id }}"
name="title-{{ task?.id }}"
placeholder="Input title"
/>
</label>
<button
*ngIf="task?.state !== 'TASK_ARCHIVED'"
class="pin-button"
[attr.aria-label]="'pinTask-' + task?.id"
(click)="onPin(task?.id)"
>
<span class="icon-star"></span>
</button>
</div>
`,
})
export default class TaskComponent {
/**
* The shape of the task object
*/
@Input() task?: Task;
// tslint:disable-next-line: no-output-on-prefix
@Output()
onPinTask = new EventEmitter<Event>();
// tslint:disable-next-line: no-output-on-prefix
@Output()
onArchiveTask = new EventEmitter<Event>();
/**
* @ignore
* Component method to trigger the onPin event
* @param id string
*/
onPin(id: any) {
this.onPinTask.emit(id);
}
/**
* @ignore
* Component method to trigger the onArchive event
* @param id string
*/
onArchive(id: any) {
this.onArchiveTask.emit(id);
}
}
上面的附加标记,结合我们现有的 CSS(请参阅 src/styles.css 和 angular.json 进行配置),产生以下 UI
组件已构建!
我们现在已经成功构建了一个组件,而无需服务器或运行整个前端应用程序。下一步是以类似的方式逐个构建其余的 Taskbox 组件。
如您所见,开始隔离构建组件既简单又快速。我们可以期望生产出更高质量的 UI,错误更少,并且更精致,因为可以深入研究并测试每种可能的状态。
捕获可访问性问题
可访问性测试是指使用自动化工具根据基于 WCAG 规则和其他行业公认的最佳实践的一组启发式方法来审核渲染的 DOM 的实践。它们充当 QA 的第一道防线,以捕获明显的无障碍违规行为,确保应用程序尽可能多的人可以使用,包括视力障碍、听力问题和认知障碍等残疾人士。
Storybook 包括一个官方的 可访问性插件。由 Deque 的 axe-core 提供支持,它可以捕获高达 57% 的 WCAG 问题。
让我们看看它是如何工作的!运行以下命令来安装插件
npm install @storybook/addon-a11y --save-dev
然后,更新您的 Storybook 配置文件 (.storybook/main.ts
) 以启用它
import type { StorybookConfig } from '@storybook/angular';
const config: StorybookConfig = {
stories: ['../src/app/components/**/*.stories.ts'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
+ '@storybook/addon-a11y',
],
framework: {
name: '@storybook/angular',
options: {},
},
};
export default config;
最后,重启您的 Storybook 以查看 UI 中启用的新插件。
循环浏览我们的故事,我们可以看到该插件在一个测试状态中发现了一个可访问性问题。消息 “元素必须具有足够的颜色对比度” 本质上意味着任务标题和背景之间没有足够的对比度。我们可以通过在应用程序的 CSS(位于 src/styles.css
中)中将文本颜色更改为较深的灰色来快速修复它。
.list-item.TASK_ARCHIVED input[type="text"] {
- color: #a0aec0;
+ color: #4a5568;
text-decoration: line-through;
}
就是这样!我们已经迈出了确保 UI 变得可访问的第一步。当我们继续为我们的应用程序添加复杂性时,我们可以为所有其他组件重复此过程,而无需启动额外的工具或测试环境。