使用storybook管理React组件

2019-12-03 18:24:48 浏览数 (1)

本文作者:IMWeb Terrance 原文出处:IMWeb社区 未经同意,禁止转载

Storybook is a development environment for UI components. It allows you to browse a component library, view the different states of each component, and interactively develop and test components.

2018年10月storybook发布了4.0版本,在UI层支持、构建、移动端、stroy参数等多个方面进行了升级优化。本文已React的UI组件为例,演示如何新建/集成Storybook到项目中,并对UI组件进行全方位的管理,包括发布、demo文档、测试等。

1. 新建一个Storybook React项目

  • 按照官方教程使用npx -p [@storybook](/user/storybook)/cli sb init安装,一直会报错:
代码语言:javascript复制
TypeError: Cannot create property 'dependencies' on boolean 'false'
  • 我采用的是手动创建的方式
    1. 首先在React项目中手动添加@storybook/react和babel依赖和运行脚本
代码语言:javascript复制
"scripts": {
    "storybook": "start-storybook -p 9001 -c .storybook"
 }
"devDependencies": {
    "[@storybook](/user/storybook)/addon-actions": "^4.0.11",
    "[@storybook](/user/storybook)/react": "^4.0.11",
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5"
},
"dependencies": {
    "react": "^16.6.3",
    "react-dom": "^16.6.3"
}

PS:由于babel-loader的最新版本是v8,需要babel版本是v7,所以按照官方教程直接安装babel-core(最高版本是v6)运行会失败,这里选择安装的是babel6。

  1. 添加storybook配置文件
代码语言:javascript复制
import { configure, addDecorator } from '[@storybook](/user/storybook)/react';
function loadStories() {
  require('../stories/index.js');
  // You can require as many stories as you need.
}
configure(loadStories, module);
  1. 添加story
代码语言:javascript复制
// /stories/index.js
import React from 'react';
import { storiesOf } from '[@storybook](/user/storybook)/react';
import { Button } from '[@storybook](/user/storybook)/react/demo';

storiesOf('Button', module)
  .addDecorator(story => <div style={{ textAlign: 'center' }}>{story()}</div>)
  .add('with text abc', () => <Button onClick={action('clicked')}>hello world!</Button>, {
    notes: { markdown: docs },
  })
  .add('with some emoji', () => (
    <Button onClick={action('clicked')}>
      <span role="img" aria-label="so cool">
        ? ? ? ?
      </span>
    </Button>
  ), {
    notes: { markdown: docs },
  });
  1. 运行npm run storybook,这时启动一个server,并自动打开一个storybook的页面

2. 使用storybook的插件功能

storybook的很多功能都是靠插件来实现的,大多数插件都需要提前注册,在页面中有一个单独的tab来对storybook进行增强。

下面介绍几款官方插件:

代码语言:javascript复制
// /.storybook/addons.js
import '[@storybook](/user/storybook)/addon-actions/register'; // 记录事件日志
import '[@storybook](/user/storybook)/addon-notes/register'; // story笔记文档,支持markdown
import '[@storybook](/user/storybook)/addon-options/register'; // storybook页面自定义
import '[@storybook](/user/storybook)/addon-links/register'; // storybook页面跳转
import '[@storybook](/user/storybook)/addon-knobs/register'; // 组件可视化配置

@storybook/addon-info插件比较特殊,不需要提前注册,它可以显示story的源码,并针对props提供一些文档。

3. 以一个分页组件为例

从团队的stoneUI组件库直接移植过来

  1. PaginationIconV组件源码放入components目录;
  2. 编写story:
代码语言:javascript复制
import React from 'react';
import { storiesOf } from '[@storybook](/user/storybook)/react';
import { withKnobs, number } from '[@storybook](/user/storybook)/addon-knobs';

import Pagination from '../components/Pagination';
import paginationDoc from '../components/Pagination/readme.md';

storiesOf('Stone UI', module)
  .addDecorator(story => <div style={{ marginTop: '50px' }}>{story()}</div>)
  .addDecorator(withKnobs)
  .add('Pagination', () => {
    const totalPage = number('totalPage', 100);
    const currentPage = number('currentPage', 45);
    const maxDisplayNumber = number('maxDisplayNumber', 4);

    return (<Pagination totalPage={totalPage} page={currentPage - 1} maxDisplayNumber={maxDisplayNumber} />);
  }, {
    notes: { markdown: paginationDoc },
  });
  1. 运行效果如下:

4. 测试UI组件

4.1 写测试用例的原因

  • 找到bug
  • 新修改没有改变已有的接口和功能
  • 将测试用例作为文档

4.2 测试结构

使用storyshots插件来实现,其核心是使用Jest,原理是每次生成一份DOM结构文档(类似于html源码),可以无痛集成到组件测试中。

对于React项目,需额外安装如下npm包:

代码语言:javascript复制
npm i -D [@storybook](/user/storybook)/addon-storyshots jest react-test-renderer

新建一个测试文件storyshots.test.js(路径随意,以.test.js结尾即可)

代码语言:javascript复制
import initStoryshots from '[@storybook](/user/storybook)/addon-storyshots';

initStoryshots({ /* configuration options */ });

在控制台运行npm test即可(在package.json中配置好scripts:"test": "jest"),测试完成后会在storyshots.test.js生成一个stories/index.js对应的DOM快照。

PS:下次运行Jest时,只有DOM结构与上次完全一致测试才会通过,通常会有两种方法来解决这种情况:

  1. 找到问题,修复不同;
  2. 用新的DOM结构替换旧的。

4.3 测试交互

storybook交互性测试可以使用 Enzyme来模拟用户输入,然后使用Mocha or Jest来进行结果测试,storybook又一个专门的插件帮助我们集成他们:specifications

首先,需要安装如下npm包:

代码语言:javascript复制
npm i -D enzyme enzyme-adapter-react-16 expect storybook-addon-specifications

storybook/config.js中配置enzyme

代码语言:javascript复制
import { configure as enzymeConfigure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

enzymeConfigure({ adapter: new Adapter() });

stories/test.js中编写测试用例:

代码语言:javascript复制
import React from 'react';
import { storiesOf } from '[@storybook](/user/storybook)/react';

import { specs, describe, it } from 'storybook-addon-specifications';
import { mount } from 'enzyme';
import expect from 'expect';

storiesOf('Interaction test', module)
  .add('Button test', () => {
    const story = (
      <button onClick={action('Hello World')}>
        Hello World
      </button>
    );

    specs(() => describe('Hello World', () => {
      it('Should have the Hello World label', () => {
        const output = mount(story);
        expect(output.text()).toContain('Hello World');
      });
    }));

    return story;
  });

在组件挂载后,通过断言来测试UI组件的属性,更多使用方法可以参考specifications插件的使用。

4.4 测试样式

样式测试这里采用PuppeteerJest来实现,其原理是利用Puppeteer的无头的chrome浏览器和storybook的url绑定组件特点,来渲染不同的UI组件,再进行图片快照的对比。

首先安装几个npm包:(puppeteer默认会下载Chromium,比较慢要耐心等候)

代码语言:javascript复制
npm install --save-dev jest puppeteer jest-puppeteer jest-image-snapshot start-server-and-test

然后添加一些文件到integration目录下:

代码语言:javascript复制
// integration/jest.config.js
module.exports = {
  preset: 'jest-puppeteer',
  testRegex: './*\.test\.js$',
  setupTestFrameworkScriptFile: './setupTests.js',
};

// integration/setupTests.js
import { toMatchImageSnapshot } from 'jest-image-snapshot';
expect.extend({ toMatchImageSnapshot });

// integration/Button.test.js
describe('Button', () => {
  it('visually looks correct', async () => {
    // APIs from jest-puppeteer
    await page.goto('http://localhost:9009/iframe.html?selectedKind=Button&selectedStory=with text');
    const image = await page.screenshot();

    // API from jest-image-snapshot
    expect(image).toMatchImageSnapshot();
  });
});

然后在package.json中添加两个scripts命令:

代码语言:javascript复制
"jest:integration": "jest -c integration/jest.config.js",
"test:integration": "start-server-and-test storybook http-get://localhost:9009 jest:integration",

第一次运行npm run test:integration可以生成UI组件渲染的一次快照,再次运行会将新旧快照进行对比,只有完全一致才能测试通过。

PS:测试不通过时,运行npm run jest:integration将强制更新原有快照。

4.5 手动测试

再好的自动化测试,都和人的体验存在差距,所以发布之前还是需要经过人眼测试,因为storybook活文档的特点,我们可以直接运行体验UI组件,通过交互操作、knobs插件等来进行全面体验。

5. 包管理

使用lerna进行包管理和发布。

6. 参考链接

  • Storybook 4.0 is here!
  • Storybook Github源码
  • Storybook 官方网站
  • Jest官方文档
  • puppeteer官方文档

7. 写在最后

本文是作者学习storybook的一些总结,总体感觉是接入成本不算高,但是模块包版本安装可能会有一些坑,但收获是给组件的管理、文档和测试提供了一个一体化的解决方案,还是很值得的。

PS:文中所涉及的demo已放入Github仓库storybook-react。

0 人点赞