Storybook 组件驱动开发(二)-- 组件实践

1,608 阅读6分钟

上篇总结了 Storybook 的安装和基本使用,现在就到了实际演练的时候了,本篇基于设计草图,完成一个任务清单 UI 界面。 咱们尝试采用 “组件驱动开发” 的流程,从下到上,从组件开发到最后组装为页面。这样的开发方式,可以控制当前开发工作的复杂度,更专注与当前需要搭建的 UI 模块。

界面草图设计使用 excalidraw.com,使用它绘制图案,自带手绘风格,是个制作示意图的好工具。

基于用例开发

清单的核心组件就是任务卡,每个任务会根据当前的任务状态,呈现不同的外观。从设计图上看,主要包含 checkbox(选中或者未选中)、任务描述信息、任务执行人头像、以及一些任务信息标识(是否带有子任务,截至日期,重复状态等等)。

task card design

基于这些信息,我们可以先构建描述任务的数据结构:

export interface Task{
    taskID: string, // 任务ID
    isComplete: boolean, // 是否已完成
    description: string, // 任务描述
    executorID?: string, // 任务执行人ID
    subTaskIDs?: string[], // 子任务ID
    deadline?: Date, // 截止日期
    ...
}

注意,除了任务ID、描述,以及任务完成状态以外,其他信息都是可选的(用 "?:" 表示),这样就方便快速创建一条只有任务描述的任务了,可以看出。这个组件可以呈现的情况也有好几种。
话不多说,let's rock the code!😎

准备工作

在开发这个组件之前,先计划一下,这个组件可能会存在哪些使用情况,编写对应的 story。
然后再根据 story 的描述完善组件,使用模拟数据测试组件页面,在 Storybook 的界面独立渲染组件,看它是否符合设计(有点测试驱动开发的意思了)。

创建组件草稿

首先先创建一个简略的任务卡组件: src/app/components/task-card.component.ts

这个组件有一些最基础实现,

  • 一个输入属性,类型为刚刚创建的 ”Task“ 类型。
  • 两个输出事件:任务点击事件,任务完成事件。
// src/app/components/task-card.component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Task } from 'src/app/models/task';

@Component({
  selector: 'app-task-card',
  template: `
    <div (click)="handleTaskClick()">
      <input type="checkbox" [(ngModel)]="task.isComplete" (click)="handleTaskCheck($event)" />
      <span>{{task.description}}</span>
    </div>
  `,
})
export class TaskCardComponent {
  @Input() task: Task;

  @Output() clickTask = new EventEmitter<Task>();

  @Output() checkTask = new EventEmitter<Task>();

  handleTaskCheck(e): void{
    e.stopPropagation();
    this.checkTask.emit(this.task)
  }

  handleTaskClick(): void{
    this.clickTask.emit(this.task)
  }
}

创建对应的 story

有了怎么一个简陋得近乎寒酸的组件之后,咱们在这个组件文件夹中,再新建一个 story 文件(src/app/components/task-card.component.stories.ts),来给它创建一些测试状态。

// src/app/components/task-card.component.stories.ts

import { FormsModule } from '@angular/forms';
import { action } from '@storybook/addon-actions';
import { Task } from '../models/task';
import { TaskCardComponent } from './task-card.component';

export default {
    title: '任务卡',
    component: TaskCardComponent,
    // 排除以 Data 命名的导出,防止它们也生成 story 链接
    excludeStories: /.*Data$/, 
    decorators: [
      moduleMetadata({
          imports: [
              // 给 [(ngModel)] 绑定语法添加需要用到的依赖模块。
              FormsModule
          ],
      })
    ]
};

// 事件集合
export const actionsData = {
    clickTaskAction: action('clickTask'),
    checkTaskAction: action('checkTask'),
};

// 模拟数据
export const taskData: Task = {
    id: '1',
    description: '测试任务',
    isComplete: false
};

// 模板方法
const Template = (args: TaskCardComponent) => ({
    props: args,
});

// 创建第一个基础测试,简单的显示任务卡界面
export const basic = Template.bind({});
basic.args = {
    task: taskData,
    checkTask: actionsData.checkTaskAction,
    clickTask: actionsData.clickTaskAction
};
basic.storyName = '基本展示';

注意✨:这里导出了 2 个模拟数据(事件,模拟数据),方便在 story 之间复用,同时使用了 ”excludeStories“ 来排除这两个不需要被 Storybook 渲染的导出。

运行 npm run storybook,即可看到组件已经在 Storybook 的沙盒中独立运行了,它展示了组件的外观、输入输出属性。并且可以实时修改输入值,查看界面绑定效果,点击组件可以看到它的事件输出。 story基本使用

完善任务卡

除了刚刚那种最基础的状态,还需要加入几个不同情况的测试,确保组件满足设计需求。

// 显示任务完整信息
export const fullInfo = Template.bind({});
fullInfo.args = {
    task: {
        ...basic.args.task,
        isComplete: false,
        subTasks: [
            { id:'2',isComplete: true, },
            { id:'3',isComplete: false, }
        ],
        priority: 'low',
        endTime: new Date(),
        remindTime: new Date(),
        repeat: 'daily',
    }
};
fullInfo.storyName = '完整信息';

// 显示任务卡已完成时的状态
export const completeTask = Template.bind({});
completeTask.args = {
    task: { ...basic.args.task, isComplete: true }
};
completeTask.storyName = '已完成状态';

目前三个测试状态都写好了,不过可以看到,目前的组件只有一个简单的 checkbox 和 span,三个 story 的渲染结果都没太大不同,完全不符合设计。
所以,就需要根据设计图开发组件了。在修改组件的时候,Storybook 也会热更新,方便实时查看组件的变化,直到每个 story 的渲染结果都满足设计要求。

篇幅起见,咱们这里就省略繁琐的 HTML 和 CSS 的代码,给组件加 “亿” 点点开发细节,现在三种 story 分别的效果如下。

任务卡

本篇主要是阐述开发流程,所以常见的 CSS 样式编写无关紧要。需要注意的是,这个组件用到了 Angular Material 的 Card、Icon、Checkbox 组件,所以在 stories.ts 文件中,要注意引入所需的依赖:

export default {
    title: '任务卡',
    ...
    decorators: [
      moduleMetadata({
        imports: [
          MatCardModuleMatIconModuleMatCheckboxModule
        ],
      })
    ]
} as Meta;

可以看出,写法和正常编写 @NgModule 装饰器并无不同,只需要把这个 stories.ts 看作一个独立的 Angular Module 即可。

任务列表

完成这个任务卡组件之后,现在咱们来看看任务列表组件,它和之前最大的不同在于,它需要和刚才的任务卡组件组合使用。设计草稿如下:

任务列表设计草图

这个列表包含多个任务卡,可以拖动切换排列顺序,可以根据任务卡的完成情况在表头显示,可以点击右上角详细添加任务,或者在底部快速创建任务,在任务量大的时候,以滚动条容纳。

所以类似的,根据设计需求,我们先创建一个基本的组件,并创建多个 story。

// src/component/task-list.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Task } from 'src/app/models/task';

@Component({
  selector: 'app-task-card',
  template: `
    <div class="task-wrapper" (click)="handleTaskClick($event)">
      <ul>
        <li *ngFor="let task of tasks>{{task.description}}</li>
      </ul>
    </div>
  `,
})
export class TaskCardComponent {
  @Input() listName:string

  @Input() tasks: Task[];
}

类似任务卡组件,然后创建 story,篇幅因素就不显示完整代码了,主要包括

  1. 传入多个任务卡信息,展示基本呈现,以及任务列表的完成状态正确显示。
  2. 点击底部快速创建任务,输出事件包含新建任务的信息。
  3. 空列表的呈现。
  4. 加载中的呈现。
  5. 超出高度后滚动条的呈现。
    ...

需要注意的地方有✨

  1. 要组合使用任务卡组件,需要在声明中引入它。
    decorators: [
       moduleMetadata({
         declarations: [TaskCardComponent]
       }),
       ...
     ]
    
  2. 可以通过“装饰器”可以给列表组件包裹一个外层 div,限制它的外部环境高度。
    decorators: [
     ...
     (storyFunc) => {
       const story = storyFunc();
       return {
           ...story,
           template: `<div style="height: 800px; padding:20px">${story.template}</div>`,
       };
     }
    ]
    

按 story 的描述开发组件后,再次加上“亿”点点细节可得:

任务列表

略过了和任务卡类似的 story 配置情况,以及 HTML、CSS的实现,可以看到,最后的成果已经按照设计图实现了各种边缘情况,如果需要,同样还可以加入更多的测试情况,让组件的可靠性好了不少。

总结

本篇简单的体验了一下组件驱动的开发流程。
在我日常的 UI 开发流程,常常是先搭建一个个基本页面布局,路由结构,再逐步细化各个部分的组件,对后台接口,编写业务逻辑,完成页面功能。这种自上而下的开发流程就存在几个痛点:

  1. 开发前期工作不够敏捷,实际开发界面之前,需要先写好大的布局,路由等。很难快速看到最后成果,不能及时和设计人员保持协作。
  2. 组件开发缺乏明确索引,边缘场景考虑不够,容易没考虑清楚场景之前,就开始开撸代码了,组件缺少可靠性。
  3. 对接接口前,往往需要在组件中编写假数据,污染源码。
  4. 组件缺乏展示和说明,协作时采用他人的组件往往需要看源码,或者反复询问,沟通成本大。

本篇基于草图,(任务卡,任务列表),也可以按这个顺序构建更大的页面。先按设计图划分组件,在零路由,业务页面的情况下,至下而上的逐步完成了两个组件:

  1. 先构建一个组件草稿,有个基本结构。
  2. 根据需求分析组件使用的各个场景,明确需要开发的功能点。
  3. 基于场景针对性的完善组件,实现小单元性的敏捷反馈。
  4. 在沙盒环境中测试和组装组件,保持源码纯净。

除了作为组件开发的沙河,Storybook 还可以使用 MDX(markdown + jsx)来编写文档和测试用例,构建一份精美的组件库官方网站!so,咱们下篇见!😎