给angular配置storybook

2020-11-12 loading

近期加入了一个三期项目,项目业务已比较冗余。前人也抽离了很多公用组件,But

  • 对于项目不熟悉,不了解各个公共组件的样式
  • 在没人告知的情况下,自己写样式,造成代码重复,不好统一管理
  • 假如有小伙伴告知,有可用组件,但是找到该组件使用的地方,并去查看使用地方的样式,略显麻烦
  • 假如组件具有各种状态的不同展现形式,想要了解所有样貌,需要去尝试各种业务逻辑,更为麻烦
  • ……

So,定了个小目标,给项目配置storybook,实现组件可视化ヾ(◍°∇°◍)ノ゙ 但是,再配置storybook,发现它已经从安装、配置到写story的方式都发生了翻天覆地的变化🤷‍♀️

# 安装

在当前项目下执行

npx sb init

会自动完成,storybook的配置

  • package.json
  • 初始库的安装
  • .storybook 配置文件夹
  • src/stories 初始story案例

然后执行

npm run storybook

完成storybook的启动~ http://localhost:6006

# 小插曲

目前自动安装的storybook的版本是“6.0.28”(使用的corejs版本是3.7.0),但是angular版本是“8.0.0”(配置的corejs的版本系列是2),导致启动后,满屏爆红~ 解决方法:将corejs升级到了版本3系列

# 写story

# 1、常规

组件文件

// src/app/components/task.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-task',
  template: `
    <div class="list-item">
      <input type="text" [value]="task.title" readonly="true" />
    </div>
  `,
})
export class TaskComponent implements OnInit {
  title: string;
  @Input() task: any;

  // tslint:disable-next-line: no-output-on-prefix
  @Output() onPinTask: EventEmitter<any> = new EventEmitter();

  // tslint:disable-next-line: no-output-on-prefix
  @Output() onArchiveTask: EventEmitter<any> = new EventEmitter();

  constructor() {}

  ngOnInit() {}
}

story文件

// *.stories.ts
// src/app/components/task.stories.ts
import { action } from '@storybook/addon-actions';
import { TaskComponent } from './task.component';
export default {
  title: 'Task',
  excludeStories: /.*Data$/,
};

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

export const taskData = {
  id: '1',
  title: 'Test Task',
  state: 'Task_INBOX',
  updated_at: new Date(2019, 0, 1, 9, 0),
};
export const Default = () => ({
  component: TaskComponent,
  props: {
    task: taskData,
    onPinTask: actionsData.onPinTask,
    onArchiveTask: actionsData.onArchiveTask,
  },
});
// pinned task state
export const Pinned = () => ({
  component: TaskComponent,
  props: {
    task: {
      ...taskData,
      state: 'TASK_PINNED',
    },
    onPinTask: actionsData.onPinTask,
    onArchiveTask: actionsData.onArchiveTask,
  },
});
// archived task state
export const Archived = () => ({
  component: TaskComponent,
  props: {
    task: {
      ...taskData,
      state: 'TASK_ARCHIVED',
    },
    onPinTask: actionsData.onPinTask,
    onArchiveTask: actionsData.onArchiveTask,
  },
});

# 2、包含ng-content的组件

对于组件内部使用了 <ng-content/> (里面可以填充任何内容),在写story时,需要使用template写入代码

// *.component.html
<button class="button">
    <img src={{iconSrc}} *ngIf="iconSrc" class="icon" />
    <ng-content></ng-content>
</button>
// *.stories.ts

import {moduleMetadata} from '@storybook/angular'

export default {
  decorators: [
    moduleMetadata({
      declarations: [ButtonComponent],
    })
  ]
}

const Template = (args) => ({
  component: ButtonComponent,
  props: args,
  template: `<button-component> Put your content here </button-component>`,
});

export const Normal = Template.bind({})
Normal.args = {}

# 3、包含路由的组件的适配

// *.stories.ts
import {moduleMetadata} from '@storybook/angular'
import {RouterModule} from '@angular/router'

import {APP_BASE_HREF} from '@angular/common'

export default {
  decorators: [
    moduleMetadata({
      imports: [RouterModule.forRoot([],{useHash: true})],
      providers: [{provide:APP_BASE_HREF, useValue: '/}]
    })
  ]
}

# 4、可视化service和directive

对于公用的 service 和 directive 组件,如果想要进行可视化,可以针对使用这些工具的*.component.ts 文件写 *.stories.ts

# 5、使用了ng-zero中的service的适配

// *.stories.ts
import {NgZorroAntdModule} from 'ng-zerro-antd'

export default {
  decorators: [
    moduleMetadata({
      imports: [NgZorroAntdModule.forRoot()]
    })
  ]
}

# 6、controls的配置

对于组件传入的@Input 可以根据数据类型,设置到 controls里面,这样,修改controls里面的数据的值,可以实时看到 组件的样式变化。

前提,安装插件 @storybook/addon-controls

// *.stories.ts
export default {
  title: 'Gizmo',
  component: Gizmo,
  argTypes: {
    // 布尔
    isAgree: { 
      control: { type: 'boolean' },
    },
    // 数值
    score: { 
      control: { type: 'number' },
    },
    // 区间
    width: { 
      control: { type: 'range', min: 400, max: 1200, step: 50 },
    },
    // 单选
    loadingState: {
      control: {
        type: 'inline-radio',
        options: ['loading', 'error', 'ready'],
      },
    },
    // 多选
    icon: {
      control: {
        type: 'select',
        options: Object.keys(iconMap),
      },
    },
  },
};

# 7、配置mobile屏幕

针对具有自适应样式的组件,可以给它下面的每个story配置不同的屏幕大小,以便可视化各个屏幕大小下的样式。

前提,安装插件 @storybook/addon-viewport

在 .storybook/preview.js 中增加自定义屏幕大小设置

import { MINIMAL_VIEWPORTS} from '@storybook/addon-viewport';

const customViewports = {
  kindleFire2: {
    name: 'Kindle Fire 2',
    styles: {
      width: '600px',
      height: '963px',
    },
  },
  kindleFireHD: {
    name: 'Kindle Fire HD',
    styles: {
      width: '533px',
      height: '801px',
    },
  },
  mobile: {
    name: 'mobile',
    styles: {
      width: '375px',
      height: '801px',
    },
  },
};

export const parameters = {
  viewport: {
    viewports: {
       ...MINIMAL_VIEWPORTS,
      ...customViewports,
    },
  },
};

使用自定义的 mobile 屏幕

// *.stories.ts

export default {
  parameters: {
    viewport: {
      defaultViewport: 'mobile'
    }
  }
}

# 8、自定义组件外围样式

有些组件的宽高依赖于它的父组件时,则需要在组件的外面包括一层代码,做简单的样式限制,可以在默认模块写入 template

// *.stories.ts

export const Default = () => ({
  component: TaskListComponent,
  template: `
  <div style="padding: 3rem">
    <app-task-list [tasks]="tasks" (onPinTask)="onPinTask($event)" (onArchiveTask)="onArchiveTask($event)"></app-task-list>
  </div>
`,
  props: {
    tasks: defaultTasksData,
    onPinTask: actionsData.onPinTask,
    onArchiveTask: actionsData.onArchiveTask,
  },
});

# 9、注入常量值

# 10、一个完整的复杂story

🌰有一个组件——taks.component.ts,用于展示任务列表,包含的业务

  • 使用了路由跳转,进入详情页(配置路由module)
  • 使用了 ng-zerro-antd 的 MessageService提示功能 (配置组件 module)
  • 使用了另一个组件——展示空白时的样式(使用其他组件时,需要引入该组件,并在declarations中声明)
  • 内部 包含 ng-content,便于在 父组件中传入 列表的title(此时,需要declarations中声明该组件本身,并使用template写出如何使用的代码)
  • 接受外部传入数据@input(使用controls控制数据变化)
  • 在滚动加载时,需要告诉父组件@output (配置action,用于观察该事件是否在正确的时机被触发)
  • 该组件需要兼容手机端样式(配置viewports,用于显示mobile界面)
// *.stories.ts
import { action } from '@storybook/addon-actions';
import {moduleMetadata,Meta,Story} from '@storybook/angular'
import {RouterModule} from '@angular/router'
import {APP_BASE_HREF} from '@angular/common'
import {NgZorroAntdModule} from 'ng-zerro-antd'
import { TaskComponent } from './task.component';
import { EmptyComponent } from './empty.component'; //使用其他组件时,需要引入该组件,并在declarations中声明 
import {listMap} from './const'

// @output属性事件
const actionsData = {
  handleGetMore: action('handleGetMore'),
};

export default {
  title: 'Components/Task',
  component: TaskListComponent,
  decorators: [
    moduleMetadata({
      declarations:[EmptyComponent,TaskListComponent]
      imports: [RouterModule.forRoot([],{useHash: true}),NgZorroAntdModule.forRoot()],
      providers: [{provide:APP_BASE_HREF, useValue: '/}]
    })
  ],
  argTypes: {
    // 布尔
    isEmpty: { 
      control: { type: 'boolean' },
    },
    // 区间
    pageCount: { 
      control: { type: 'range', min: 400, max: 1200, step: 50 },
    },
    // 多选
    listType: {
      control: {
        type: 'select',
        options: Object.keys(listMap),
      },
    },
  },
} as Meta;

const Template:Story<TaskListComponent> = (args:TaskListComponent) => ({
  component:TaskListComponent,
  props: args,
  template: `
    <div style="padding: 3rem">
      <app-task-list [isEmpty]="isEmpty" [listType]="listType" (handleGetMore)="handleGetMore($event)">
        <h1>这里是title</h1>
      </app-task-list>
    </div>
  `
})

export const Init = Template.bind({})
Init.args = {
  isEmpty: true,
  pageCount: 30,
  handleGetMore: actionsData.handleGetMore
}

export const InitMobile = Template.bind({})
InitMobile.args = {
  isEmpty: false,
  pageCount: 30,
  handleGetMore: actionsData.handleGetMore
}
InitMobile.parameters = {
  viewport: {
    defaultViewport: 'mobile'
  }
}

# 常用插件

# 学习资料