0%

Storybook初探 (Part 1)

Storybook 是一个 ui 组件开发管理的工具,我们可以通过 story 独立创建组件,并且每个组件都有一个独立开发调试环境。Storybook 是运行在主应用程序之外,不依赖于项目,因此我们不必担心开发环境、依赖等问题导致不能开发组件。
Storybook 支持多个主流框架(React, Vue, Angular, Mithril, Ember)等,由于目前笔者使用的是 react 技术栈,本文将介绍 react 项目如何配置使用 Storybook。

一. 操作指南

  1. 首先,安装@storybook/react,react, react-dom, babel-core 等项目所需基础库。在 package.json 文件中添加 “storybook”: “start-storybook -p 9001 -c .storybook”
  2. 新建文件,先看下项目目录结构

  • .storybook 文件夹存放 storybook 配置,及插件文件。
  • src/components 存放 UI 组件。
  • src/markdown 存放组件显示的文档等介绍。
  • src/stories 存放 story,story 可绑定相应的组件,一个组件可以有多个不同状态的 story。
  1. 编写 story
  • 添加 storybook 配置文件
    • .config 配置文件
      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
      import { configure, addDecorator  } from '@storybook/react';
      import { withOptions } from '@storybook/addon-options';
      import { withNotes } from '@storybook/addon-notes';
      import { themes } from '@storybook/components';
      // 全局加载装饰器
      // addDecorator(story => <div style={{ textAlign: 'center' }}>{story()}</div>);
      addDecorator(
      withOptions({
      // name: 'Foo',
      // theme: themes.dark
      showSearchBox: false,
      showAddonsPanel: true,
      addonPanelInRight : true,
      enableShortcuts : true,
      })
      )
      addDecorator(withNotes);
      /**
      * 动态加载所有stories
      */
      const req = require.context('../src/stories', true, /\.stories\.js$/);
      function loadStories() {
      req.keys().forEach(fileName => req(fileName));
      }

      configure(loadStories, module);

这里为了动态加载 stories,Storybook 使用了 Webpack 的 require.context 动态加载模块。

  • 添加 UI 组件(src/components/circle)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import React from 'react'
    import './style.scss'

    export function Circle (props) {
    const defaultClass = 'circle_default';
    const { className, ...other } = props;
    return (
    <div
    className={`${defaultClass} ${className} `}
    {...other}
    >
    {props.children || ''}
    </div>
    )
    }
  • 添加 story(src/stories/circle.stories.js)

    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
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    import React from 'react';
    import { storiesOf } from '@storybook/react';
    import { action } from '@storybook/addon-actions';

    import { withKnobs, text, boolean, number, object } from '@storybook/addon-knobs';
    import { Circle } from '../components/circle';
    import '../components/circle/circle-color.scss';
    import circleText from '../markdown/circle.md';

    const stories = storiesOf('Circle', module);
    // 动态修改react组件属性(测试)
    // 可在config文件中全局配置
    stories.addDecorator(withKnobs);
    stories.add('圆点', () => (
    <div>
    <div>
    <Circle className='status_running' onClick={action('clicked')}></Circle>&nbsp;
    运行中
    </div>
    <div>
    <Circle className='status_finished'></Circle>&nbsp;
    成功
    </div>
    <div>
    <Circle className='status_stoped'></Circle>&nbsp;
    取消
    </div>
    <div>
    <Circle className='status_run_fail'></Circle>&nbsp;
    运行失败
    </div>
    <div>
    <Circle className='status_submitting'></Circle>&nbsp;
    提交中
    </div>
    <div>
    <Circle className='status_submit_failed'></Circle>&nbsp;
    提交失败
    </div>
    <div>
    <Circle className='status_submit_failed'></Circle>&nbsp;
    上游失败
    </div>
    <div>
    <Circle className='status_frozen'></Circle>&nbsp;
    冻结
    </div>
    <div>
    <Circle className='status_killed'></Circle>&nbsp;
    已停止
    </div>
    <div>
    <Circle className='status_restarting'></Circle>&nbsp;
    重试中
    </div>
    <div>
    <Circle className='status_wait_submit'></Circle>&nbsp;
    等待提交
    </div>
    </div>
    ), {
    notes: {
    markdown: circleText
    }
    });

    stories.add('动态修改style', () =>{
    const groupId = 'circle';
    const defaultStyle = {
    background: 'black'
    }
    const style = object('Style', defaultStyle, groupId);

    return <Circle style={style} ></Circle>
    }
    )
注意:

1. storybook 插件都需要在 ./stroybook/addons.js 中注册才能使用,注册完之后需要重新运行项目。

2. 这里有一个问题,在引入 markdown 作为组件文档时,其中表格渲染 border 无法显示。

3. storybook 内部集成的 webpack 不支持 scss 类型文件。需要自定义 webpack,这里我们在原来的配置基础上实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require("path");
// 保留storybook原有默认配置 + 扩展配置
module.exports = function (baseConfig, env, defaultConfig) {
defaultConfig.module.rules.push({
test: /\.stories\.js$/,
loaders: [require.resolve('@storybook/addon-storysource/loader')],
enforce: 'pre',
}, {
test: /.(scss|sass)$/,
loaders: ["style-loader", "css-loader", "sass-loader"],
include: path.resolve(__dirname, "../")
});

return defaultConfig;
};

npm run storybook 查看效果

二. 插件

storybook 的插件有许多,基本上有两种类型(装饰器 Decorators 和原生插件 Native Addons)
Decorators 通过封装 react 组件实现,比如 storybook-router。
Native Addons 主要是以面板的形式注入,便于管理使用组件,比如 addon-actions 等。
本文主要介绍几个常用的 addons,目前项目中使用到的 addons 有:

1
2
3
4
5
6
import '@storybook/addon-storysource/register';
import '@storybook/addon-notes/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';
import '@storybook/addon-options/register';
1. addon-storysource

此插件主要是在插件面板中显示 story 源代码

安装插件之后需要在 webpack 中定义,让其在每个 story 都生成一个装饰者调用。

1
2
3
4
5
6
7
8
9
module.exports = function (baseConfig, env, defaultConfig) {
defaultConfig.module.rules.push({
test: /\.stories\.jsx?$/,
loaders: [require.resolve('@storybook/addon-storysource/loader')],
enforce: 'pre',
});

return defaultConfig;
};

并且可以在 options 选项中定义 printWidth 等更多优化配置格式化面板。

2. addon-knobs
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
import React from 'react';
import { storiesOf } from '@storybook/react';
import SlidePanel from '../components/slidePanel';
import { boolean, object, text } from '@storybook/addon-knobs';
storiesOf('SlidePanel', module)
.add('基本用法', () =>{
const groupId = 'slidepanel'
const defaultStyle = {
right:'-20px',
width:'80%',
minHeight: '600px',
height: '100%'
}
const style = object('style', defaultStyle, groupId);
const defayltText = 'hello slidePanel'
const children = text('children', defayltText, groupId)
return <SlidePanel
visible={boolean('visible', true, groupId)}
style={style}
onClose={()=>{
console.log('click')
}}
>
<div>{children}</div>
</SlidePanel>
})

我们在这个 story 中定义了 style、visible、children 三个动态变量传入 SlidePanel 组件,其变量类型分别对应的是 boolean、object、text。在 knob 中还可以导入许多的类型约束(number, color, array)等。

注意:

在定义动态变量时,可以通过 groupId 来分类不同组件的变量,在选项卡中将会以 groupId 在组中过滤,没有参入 groupId,将会自动分类到 All 组中。

3. addon-actions

actions 可以用于显示 storybook 事件处理程序接收的数据(多个事件触发可以写成对象形式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action, actions, configureActions , decorate } from '@storybook/addon-actions';
import { Button } from '../components/button';


// 多个动作触发
const events = actions({
onClick: 'clicked',
onMouseOver: 'hovered'
})
// const firstArg = decorate([args => args.slice(0, 1)]);
storiesOf('Button', module)
.addDecorator(story => <div style={{ textAlign: 'center' }}>{story()}</div>) // 当前组件加载装饰器
.add('基本用法', () => (
<Button {...events}>123</Button>
))
4. addon-notes

可以在面板中添加 story 注释文本信息。

addon-notes 只允许在面板中显示信息,有时候不方便我们查看组件 api 等详细信息。如果我们想要在组件旁边显示用法以及其他类型的文档介绍等信息,可以安装 addon-info(组件文档管理必备)

storybook 提供了许多 addons。

  • storybook-state: 添加 state 面板,展示或更新 state,重绘视图。
  • storyShots: 快照测试。
  • storybook-readme: 将 markdown 导入为 story 等。

更多插件介绍详情官网

三. 写在后面

这篇文章简单介绍了如何基础使用 storybook 工具集成显示 ui 组件,总体感觉是学习成本不算高。为了后期有便于更好的维护组件,有信心的提交代码,那测试功能必不可少。并且目前不能直接在 story 中修改 react 组件 state。下篇文章我们将集成 redux 便于管理数据,详细叙述 storybook 是如何利用基于 jest 的 addons 进行一系列的自动化测试以及手动测试。

-------------本文结束, 感谢您的阅读-------------