Storybook 是一个 UI 组件的开发环境。它允许你能够浏览一个组件库,查看每个组件的不同状态,以及支持交互式的方式开发和测试组件。
Storybook 在你的应用程序之外运行。这允许你能够独立的开发 UI 组件,你可以提高组件的可重用性、可测试性和开发速度。你可以快速构建,而无需担心应用程序特定的依赖项。
这里有一些可以参考的特色示例,可以了解 Storybook 的工作原理。Storybook 这款工具很强大,它支持很多流行的框架,比如:
- React
- React Native
- Vue
- Angular
- Polymer
- Riot
接下来我们来介绍一下在 Angular 项目中如何使用 storybook。现在我们使用 Angular CLI 来创建一个新的演示项目:
代码语言:javascript复制$ ng new angular-storybook-demo
$ cd angular-storybook-demo
这里需要注意的是,本文使用的 CLI 版本为:
代码语言:javascript复制 _ _ ____ _ ___
/ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ | '_ / _` | | | | |/ _` | '__| | | | | | |
/ ___ | | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ __| |_|__, |__,_|_|__,_|_| ____|_____|___|
|___/
Angular CLI: 6.1.5
Node: 9.11.0
OS: darwin x64
Angular: 6.1.6
接下来安装 @storybook/cli
:
$ npm i -g @storybook/cli
成功安装以上依赖后,在命令行运行 getstorybook 命令初始化 storybook,该命令会为我们自动生成以下两个 npm script 命令:
代码语言:javascript复制"scripts": {
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
}
上面的 storybook 命令,通过 -p
参数用于指定 storybook 的端口。对于基础的 Storybook 配置文件,我们只需简单地告诉 Storybook 从哪里获取 stories。
getstorybook
命令运行后,会自动为我们创建一个 .storybook
目录。然后在该目录下分别创建两个文件:config.js 和 addons.js 文件。顾名思义 config.js
文件就是配置文件,该文件包含以下内容:
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
文件:
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 类型的定义如下:
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
这个依赖库。
$ 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/
地址,你将会看到以下内容:
以上截图中所演示的 Button 组件的定义如下:
代码语言:javascript复制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 属性:
代码语言:javascript复制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
属性:
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 文档,感兴趣的同学可以阅读一下该文档。