qrcode

马上订阅,开启修仙之旅

Angular 工具篇之Storybook

Storybook 是一个 UI 组件的开发环境。它允许你能够浏览一个组件库,查看每个组件的不同状态,以及支持交互式的方式开发和测试组件。

Storybook 在你的应用程序之外运行。这允许你能够独立的开发 UI 组件,你可以提高组件的可重用性、可测试性和开发速度。你可以快速构建,而无需担心应用程序特定的依赖项。

这里有一些可以参考的特色示例,可以了解 Storybook 的工作原理。Storybook 这款工具很强大,它支持很多流行的框架,比如:

  • React
  • React Native
  • Vue
  • Angular
  • Polymer
  • Riot

接下来我们来介绍一下在 Angular 项目中如何使用 storybook。现在我们使用 Angular CLI 来创建一个新的演示项目:

1
2
$ ng new angular-storybook-demo
$ cd angular-storybook-demo

这里需要注意的是,本文使用的 CLI 版本为:

1
2
3
4
5
6
7
8
9
10
11
     _                      _                 ____ _     ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/

Angular CLI: 6.1.5
Node: 9.11.0
OS: darwin x64
Angular: 6.1.6

接下来安装 @storybook/cli

1
$ npm i -g @storybook/cli

成功安装以上依赖后,在命令行运行 getstorybook 命令初始化 storybook,该命令会为我们自动生成以下两个 npm script 命令:

1
2
3
4
"scripts": {
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
}

上面的 storybook 命令,通过 -p 参数用于指定 storybook 的端口。对于基础的 Storybook 配置文件,我们只需简单地告诉 Storybook 从哪里获取 stories。

getstorybook 命令运行后,会自动为我们创建一个 .storybook 目录。然后在该目录下分别创建两个文件:config.js 和 addons.js 文件。顾名思义 config.js 文件就是配置文件,该文件包含以下内容:

1
2
3
4
5
6
7
8
9
import { configure } from '@storybook/angular';

// automatically import all files ending in *.stories.ts
const req = require.context('../src/stories', true, /.stories.ts$/);
function loadStories() {
req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);

上面的代码支持从 ../src/stories 目录下自动导入以 *.stories.ts 结尾的文件。当然你也可以指定从其它目录加载。通过上面的两个步骤,我们已经完成 Storybook 的初始化工作。此外 getstorybook 命令还会在 src/stories 目录下创建一个 index.stories.ts 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { storiesOf } from '@storybook/angular';
import { withNotes } from '@storybook/addon-notes';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';

import { Welcome, Button } from '@storybook/angular/demo';

storiesOf('Welcome', module).add('to Storybook', () => ({
component: Welcome,
props: {},
}));

storiesOf('Button', module)
.add('with text', () => ({
component: Button,
props: {
text: 'Hello Button',
},
}))
.add(
'with some emoji',
withNotes({ text: 'My notes on a button with emojis' })(() => ({
component: Button,
props: {
text: '😀 😎 👍 💯',
},
}))
)
.add(
'with some emoji and action',
withNotes({ text: 'My notes on a button with emojis' })(() => ({
component: Button,
props: {
text: '😀 😎 👍 💯',
onClick: action('This was clicked OMG'),
},
}))
);

storiesOf('Another Button', module).add('button with link to another story', () => ({
component: Button,
props: {
text: 'Go to Welcome Story',
onClick: linkTo('Welcome'),
},
}));

在上面的示例中,我们通过调用 storiesOf() 方法后返回的对象的 add() 方法来创建故事。其中 add() 方法支持以下参数:

  • storyName: string —— 故事的名称;
  • getStory: IGetStory —— 一个函数对象,调用后返回一个配置对象,包含 component、props 等属性。这里 IGetStory 类型的定义如下:
1
2
3
4
5
6
export type IGetStory = () => {
props?: ICollection;
moduleMetadata?: Partial<NgModuleMetadata>;
component?: any;
template?: string;
};

通过 @storybook/addon-actions 库中导入的 action 方法,我们能够方便地记录用户触发的自定义事件。此外利用 @storybook/addon-notes 这个库导入的 withNotes() 方法,我们还可以为每个故事添加一个备注信息。

好的,这时一切看起来很顺利,但当我们运行 npm run storybook 命令时,控制台会抛出异常信息。

通过查看 Github 上 Storybook 项目中的 issue,我们发现了异常的原因。即对于 Angular CLI 6 创建的项目需要安装 @storybook/angular@storybook/addons 这两个库 4.0 以上的版本,实际测试发现还得手动安装 @babel/core 这个依赖库。

1
2
$ npm i @storybook/angular@4.0.0-alpha.20 @storybook/addons@4.0.0-alpha.20 --save-dev
$ npm i @babel/core@7.0.0 --save-dev

在成功安装完以上依赖后,我们再次运行 npm run storybook 命令,这时打开 http://localhost:6006/ 地址,你将会看到以下内容:

ng-storybook-demo

以上截图中所演示的 Button 组件的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
selector: 'storybook-button-component',
template: `
<button (click)="onClick.emit($event);">{{ text }}</button>
`,
styles: [
`
button {
border: 1px solid #eee;
border-radius: 3px;
background-color: #ffffff;
cursor: pointer;
font-size: 15px;
padding: 3px 10px;
margin: 10px;
}
`,
],
})
export default class ButtonComponent {
@Input() text = '';
@Output() onClick = new EventEmitter<any>();
}

上面的 ButtonComponent 组件很简单,而在实际的项目中我们的组件可能需要使用 Angular 内置的指令(如 ngIf 或 ngFor)或第三方库的组件。针对这种情况,我们就可以利用配置对象的 moduleMetadata 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { CommonModule } from '@angular/common';
import { storiesOf } from '@storybook/angular';
import { MyButtonComponent } from '../app/my-button/my-button.component';
import { MyPanelComponent } from '../app/my-panel/my-panel.component';
import { MyDataService } from '../app/my-data/my-data.service';

storiesOf('My Panel', module)
.add('Default', () => ({
component: MyPanelComponent,
moduleMetadata: {
imports: [CommonModule],
schemas: [],
declarations: [MyButtonComponent],
providers: [MyDataService],
}
}));

上面示例中,我们为每个 story 单独设置 moduleMetadata 属性。若每个 story 都使用同样的 Metadata 信息,我们就可以通过 addDecorator() 方法,统一设置 moduleMetadata 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { CommonModule } from '@angular/common';
import { storiesOf, moduleMetadata } from '@storybook/angular';
import { MyButtonComponent } from '../app/my-button/my-button.component';
import { MyPanelComponent } from '../app/my-panel/my-panel.component';
import { MyDataService } from '../app/my-data/my-data.service';

storiesOf('My Panel', module)
.addDecorator(
moduleMetadata({
imports: [CommonModule],
schemas: [],
declarations: [MyButtonComponent],
providers: [MyDataService],
})
)
.add('Default', () => ({
component: MyPanelComponent
}))
.add('with a title', () => ({
component: MyPanelComponent,
props: {
title: 'Foo',
}
}));

以上关于 moduleMetadata 的使用示例来源于 Storybook 官方的 guide-angular 文档,感兴趣的同学可以阅读一下该文档。


欢迎小伙伴们订阅前端修仙之路,及时阅读 Angular、RxJS、TypeScript 和 Node.js 最新文章。

qrcode