Storybook 组件驱动开发(一)-- 基本使用

2,143 阅读10分钟

Storybook 是当前最流行的 UI 组件开发环境,主要功能就是提供组件独立运行环境,方便编写组件使用样例,每个样例称之为一个 “story”。有了它,就可以方便的在项目中提取通用组件,管理自己的组件库。
并且,它还可以通过丰富的插件实现可交互测试,响应式布局,文档生成、设计稿预览等功能,让开发工作更加专注,和设计人员的协作更轻松。 项目和Storybook的关系

安装 Storybook

Storybook 有很丰富的自定义配置项和大量的插件,可以适配各种开发场景,不过这也让项目的搭建变得很复杂,很新人劝退。🤣

好在最近更新的 6.0 版本之后,项目可以实现零配置搭建,就像 Spring Boot 对 Spring 的简化,让项目创建变得格外简单,现在一行命令就可以完成项目搭建。

Storybook 支持各种主流组件框架,这里以 Angular 来阐述。

创建 Angular 项目

首先默认全局已经安装了 Angular CLI:npm install -g @angular/cli。顺便一提,Angular 11 大幅提升了构建速度和打包体积,没有破坏性改动,强烈安利。😋

在指定目录(如 D:\projects)下运行 ng new storybook-example 创建一个 Angular 项目,根据提示交互性的配置项目生成即可。

初始化 Storybook

接着在这个项目路径下(D:\projects\storybook-example)运行命令 npx sb init 即可完成 Storybook 的搭建。它会根据项目的依赖自动配置 Storybook。只需要这么一行命令,Storybook 就可以自动安装到项目中去。

安装完毕后,运行 npm run storybook 即可看到 storybook 成功运行了,so easy!😎 storybook welcome

初始化做了什么?

虽然项目运行起来了,不过突然自动创建了一堆未知的文件,也让人心里没底,来看看项目初始化做了哪些事情吧。

  • 📦 安装所需的依赖包:因为识别到当前目录下是一个 Angular 项目,所以安装的是 Angular 版本的 Storybook 依赖包。
    • "@compodoc/compodoc": Angular 的组件文档生成工具。
    • "@storybook/addon-actions": 用以记录事件触发的插件。
    • "@storybook/addon-essentials": 官方维护的插件集合,带有默认配置。
    • "@storybook/addon-links": 用以给组件 story 创建链接。
    • "@storybook/angular": storybook 针对 Angular 的依赖。
  • 🛠 设置 npm 脚本:
    • "storybook": 本地运行 Storybook
    • "build-storybook": 编译打包 Storybook 项目
  • 🛠 在项目根目录创建 .storybook 文件夹,添加默认配置:
    • main.js:项目的全局配置文件,定义了 story 的查找路径,以及引入的插件。
    • preview.js:项目的渲染配置,包括全局样式的引入,通用性的变量等。
    • tsconfig.json:自动识别项目采用 typescript 后自动生成的配置文件
  • 📝 在 src/stories 目录下创建三个组件(button、header、page),以及它们的 story 示例

编写 stories

story 用于展示组件某个状态,每个组件可以包含任意多个 story,用来测试组件的各种场景。根据默认配置,只需要在组件的文件夹中,以 **.component.stories.ts 的格式创建即可。

story 语法

基本编写语法很简单,是 export 任意多个 function,每一个就是一个 story。导出主要分两种:

  1. default export:默认导出,提供组件级别的配置信息,例如以下配置会注明组件的归类,并提供 Button 组件的信息,以便渲染这个组件。
    // Button.stories.ts
    
    import Button from './button.component';
    
    export default {
      title: 'Components/Button',
      component: Button,
    }
    
  2. named export:命名导出,用以描述 story,如上所说,一个组件可以有若干个 story。
    // Button.stories.ts
    
    // 创建一个模板,方便在后续的 story 中复用
    const Template = (args: Button) => ({
      props: args,
    });
    
    export const Primary = Template.bind({});  // 复制 Template
    Primary.args = { background: '#ff0', label: 'Button' };
    Primary.storyName = "主要状态" // 自定义 story 名
    
    export const Secondary = Template.bind({});
    Secondary.args = { ...Primary.args, label: '😄👍😍💯' }; // 复用上一个 story 的配置
    
    export const Tertiary = Template.bind({});
    Tertiary.args = { ...Primary.args, label: '📚📕📈🤓' } // 再来一个
    
    通过复制模板 function,可以创建若干个 story,传入不同的参数,就可以分别渲染出组件的不同状态。每个 story 的名字默认是 function 名,也可以自定义。

Args(属性)

上一节看到了怎么去写一个 Story 文件,不过里面反复用到的 args 是什么呢?
它代表组件的输入属性(Angular 中的 @input(),Vue/React 中的 props),它有 2 个层级,方便灵活配置。

  1. story 层级:

    // Button.stories.ts
    
    const Template = (args: Button) => ({
      props: args,
    });
    
    // 在这个 story 中传入组件属性,只影响当前 story
    export const Primary = Template.bind({});
    Primary.args = {
      primary: true,
      label: 'Primary',
    };
    
  2. 组件层级:

    // Button.stories.ts
    
    import Button from './button.component';
    
    // 组件层级(默认导出)中传入组件属性,
    // 这个 Button 组件的所有 stories 的 primary 属性都会是 true
    export default {
      title: "Button",
      component: Button,
      args: {
        primary: true,
      },
    }
    

就像上一节所看到的,不同的 story 的 args 是可以复用的,这里用到了 ES6 的解构语法:

const Primary = ButtonStory.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
}

// 复用 Primary story 的 args,并覆盖 primary 属性
const Secondary = ButtonStory.bind({});
Secondary.args = {
  ...Primary.args, // 合并上一个 args 对象
  primary: false,
}

简单的导出几个 function,这个按钮组件的测试用例就写好了,看看效果 storybook 按钮示例

可以看到,这个按钮组件可以独立于项目运行了,并且右边工具栏可以自由更改它的属性,实时查看属性改变后的效果,还可以自动生成组件文档。

有 story 做示例,有实时的控制台,有文档,再也不怕写好的组件别人不知道怎么用了。😎

额外的配置项

除了写给组件写 story,很多时候也会需要配置插件,或者给组件提供额外的功能,这里看看是如何配置的吧。

Parameters(参数)

Parameters 用以配置 Storybook 和 插件,具有全局、组件、story 三个层级。

Story 拥有大量的插件,以下以简单的 backgrounds 插件举例,它用来控制组件容器的背景色,默认自带黑/白两色。

  1. 全局定义在根目录 .storybook/preview.js 下,会影响所有的 stories。这样配置后,每个 story 界面下都可以选择红/绿两色背景:

    // .storybook/preview.js
    
    export const parameters = {
      backgrounds: {
        values: [
          { name: 'red', value: '#f00' },
          { name: 'green', value: '#0f0' },
        ],
      },
    };
    
  2. 组件层级下定义,会让这个组件的所有 stories 都可以选择指定的背景色

    // Button.story.ts
    
    export default {
      title: 'Button',
      component: Button,
      parameters: {
        backgrounds: {
          values: [
            { name: 'red', value: '#f00' },
            { name: 'green', value: '#0f0' },
          ],
        },
      },
    };
    
  3. story 层级下的定义只会影响当前 story,其他 story 就只能选择默认的黑/白两色了。

    // Button.story.ts
    
    export const Primary = Template.bind({});
    Primary.args = {
      primary: true,
      label: 'Button',
    };
    // 红绿背景只在这个 story 下可以选择
    Primary.parameters = {
      backgrounds: {
        values: [
          { name: 'red', value: '#f00' },
          { name: 'green', value: '#0f0' },
        ],
      },
    };
    

Parameters 的配置是可以继承,同名的子级会覆盖父级的定义。

Decorators(装饰器)

每个 Decorator 也是 function,用来包裹 story,保持原有的 story 不变的情况下,额外给它添加额外的 DOM 元素、引入上下文环境、添加假数据等等。
和 Parameters 一样,它也可以定义在全局/组件/story 三个层级,每个 Decorator 按定义顺序依次执行,从全局到 story。

例如,用一个额外的 <div> 包裹每个 story 的组件渲染:

// button.stories.ts

import { Meta, Story } from '@storybook/angular';
import { ListComponent } from './list.component';

export default {
  title: 'Example/List',
  component: ListComponent,
  decorators: [
    (storyFunc) => {
      const story = storyFunc();

      return {
        ...story,
        template: `<div style="height: 60px">${story.template}</div>`,
      };
    }
  ]
} as Meta;

这样一来,这个列表组件的所有 story,都会展示出它在一个 60 像素高的容器内的呈现效果。

除了给组件包裹额外的元素,再例如为复合组件添加组件依赖:

// List.stories.ts

import { moduleMetadata } from '@storybook/angular';
import { CommonModule } from '@angular/common';

import List from './list.component';
import ListItem from './list-item.component'


// 给 list 组件添加它需要的组件和模块依赖
export default {
  title: 'List',
  component: List,
  decorators: [
    moduleMetadata({
      declarations: [ListItem],
      imports: [CommonModule],
    }),
  ],
};

const Template = (args: List) => ({
  component: List,
  props: args,
});

就像平常需要在 ngModule 中声明似的,moduleMetadata 装饰器可以轻松测试各种组件,让你能在 Storybook 中从小到大搭建组件库。

组件驱动开发

现代用户界面愈加复杂,在日新月异的技术发展下,人们愈加期望独特,个性的用户体验。但这也意味着前端和设计需要给 UI 界面嵌入更多的逻辑和设计,这让前端变得原来越重,界面也越加复杂难以维护和测试。
将 UI 模块化分离也成了趋势,分离页面逻辑到一个个独立的交互模块中,将复杂的业务拆分解构,每个模块易于测试,组合和复用。所以现在组件化框架成了绝对的主流。相似的,当前微服务和容器化也变得非常流行。

开发流程

那么组件驱动的开发又是如何呢?

  1. 一次编写一个组件:首先编写一个个独立组件,定义它们的输入属性和输出事件,始于微末(例如 Avatar、Button、Input、Tooltip)。没错,这就是咱们熟悉的一个个组件库所做的事情:Material、Antd、ElementUI……
  2. 组合组件:构成更复杂的业务模块,构成新的功能(例如 Forms、Header、List、Table)。
  3. 装配页面:将业务模块构建出页面,使用假数据来模拟页面的各种使用场景和边缘用例。(例如首页、设置页、个人主页)
  4. 集成页面到应用:将页面与后台数据接口,业务逻辑服务层对接,构成实际应用程序(例如 xx监管系统、xxx商城、xx官网)

组件驱动开发

所以 Storybook 初始化项目后,创建了 button、header、page 三个组件示例,它们就代表了这样的开发阶梯流程。

适用场景

没有万精油的开发模式,总是需要按需选择技术栈和开发模式的,我们重要去了解每个优劣场景,这个也不例外。

优势;

  • 高质量: 通过编写各种 stories 来验证组件的运行场景,测试用例越多,组件质量越发可靠和健壮。
  • 易测试: 在组件级别进行测试,将 debug 细化,比按业务页面测试更省力精确。
  • 开发和设计并行:讲 UI 按组件模块化开发,可以让设计和前端开发更紧密的合作,不同项目共享资源。

劣势:

  • 时间周期: 相比针对页面一把梭似的开发,针对组件严谨设计api,编写 story,考虑复用性和边缘场景。项目的开发时间周期必然会提高,这对外包的小伙伴来说,可就不友好了。
  • 页面类应用:整个应用在开发和设计角度来看,都是几个完整页面的集合,页面间没有太多可以复用的元素,整夜开发和划分组件差别不大(开发各种可视化大屏可能会有同感)

所以,组件驱动开发,更适合复用场景丰富的系列应用场景,项目生命周期充足,质量要求高,前端和设计合作紧密的团队(或者,咱们自己一个人包圆儿的个人项目😁)

总结

本篇简单介绍了 Storybook 这一组件开发工具,主要包含几点:

  • 在已有的 Angular 项目下,通过一行命令初始化了 Storybook 开发环境,它自动安装了依赖,创建了默认配置,并在项目里生成了三个示例。运行它设置好的命令行,即可看到组件测试界面。
  • 介绍了如何编写组件的 story。
    • 编写按键组件的三个 story 用例,实现用例的复用。
    • 用 parameter 配置背景插件。
    • 用 decorator 给组件添加额外的外层 DOM、传递组件的模块依赖。
  • 介绍组件驱动开发模式,分析其优劣场景。

纸上得来终觉浅,还是得把它融入咱们实际的开发场景,看看它是如何驱动项目的开发的。所以,一起来体验可视化的测试驱动开发,完成一个完整的业务页面吧,咱们下一篇见!😎