手把手教你快速搭建专属的storybook

15,623 阅读4分钟

什么是Storybook

Storybook是一个辅助UI控件开发的工具。通过story创建独立的控件,让每个控件开发都有一个独立的开发调试环境。 Storybook的运行不依赖于项目,开发人员不用担心由于开发环境、依赖问题导致不能开发控件。

Storybook支持的框架覆盖主流的框架(React、Vue、Angular)。 由于使用React作为技术栈,本文将介绍使用react的项目如何配置Storybook环境。

安装

  1. 全局安装Storybook
npm i -g storybook
  1. 执行以下命令安装@storybook/react
npm i --save-dev @storybook/react
  1. 在package.json文件中
{
  "scripts": {
    "storybook": "start-storybook -p 9001 -c .storybook"
  }
}
  1. 在工程根目录创建.storybook目录

  2. .storybook目录下创建config.js文件

import { configure } from '@storybook/react';
import 'index.scss';

function loadStories() {
  require('./stories/userStory');
}

configure(loadStories, module);
  1. 创建story
    虽然官方推荐在项目根目录下创建stories目录,但我比较喜欢在.storybook目录下创建一个stories目录。然后根据不同的业务模块创建不同的stories目录。 比如有个user模块,那么我会创建一个stories/userStory目录。
// stories/userStory/index.jsx
import React from 'react';
import { storiesOf } from '@storybook/react';
import BasicInfo from 'pages/clientDetail/components/BasicInfo';

storiesOf('用户信息', module)
  .add('基础信息', () => <BasicInfo />);

至此,根据Storybook React Guide,我们配置了一个简单的storybook环境。 其实这个环境已经可以用了,当然,如果还需要一些额外的功能,比如支持lessscss等,就需要自定义webpack配置。

storybook 配置

1. 自定义webpack配置

storybook基础webpack配置只包含以下几项:

  • babel
    • ES2016+ Support
    • .babelrc support
  • Webpack
    • CSS Support
    • Image and Static File Support
    • JSON Loader

有时候默认的webpack配置不能满足我们的项目,因此需要对webpack配置进行扩展。

通常我使用的是Full Control Mode对webpack配置进行修改。首先在.storybook目录下增加webpack.config.js文件

const path = require('path');

module.exports = (storybookBaseConfig, configType) => {
  // 现在应该很多项目会使用`less`或者`scss`等css预处理技术。
  // 这里使用了postcss-loader进行处理
  storybookBaseConfig.module.rules.push({
    test: /\.s?css$/,
    use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 1 } }, 'postcss-loader'],
    include: path.resolve(__dirname, '../'),
  });

  return storybookBaseConfig;
};

注意 1:默认配置失效

如果使用自定义的配置,默认配置就会失效,如果没有重新配置file-loader,storybook运行起来时候假如有控件引用了图片等文件会报错。

  // 默认配置会失效,处理文件需要配置相应的file-loader
  storybookBaseConfig.module.rules.push({
    test: /\.(gif|png|jpe?g|eot|woff|ttf|pdf)$/,
    loader: 'file-loader',
  });

注意 2:保留原配置上修改

使用Full Control Mode模式虽然可以最大限度修改storybook的webpack配置,但是以下配置修改时需要注意在原配置上进行扩展。

  • entry
  • output
  • first loader in the module.loaders (Babel loader for JS)
  • all existing plugins

比如需要添加一个loader,需要像面那样push一个loader到module.rules数组中。

storybookBaseConfig.module.rules.push({
  test: /\.s?css$/,
  use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 1 } }, 'postcss-loader'],
  include: path.resolve(__dirname, '../'),
});

2. storybook + Redux

其实组件可以分为2种,具体可以参照这篇文章

  • 展示类,不涉及逻辑只关注样式展示(Presentational)
  • 逻辑类,关注逻辑而不涉及样式展示(Container)

展示类

展示类的控件使用Storybook很简单,根据展示类的控件传入props即可。

逻辑类

在我的项目中通常为react-redux connect后的类。类中操作(如网络操作)都是通过redux进行的。 为了让这个类能正常测试运行,需要进行以下操作。(例子均为userStory

  1. 在story文件中引入react-redux的providerstore
import { provider } from 'react-redux';
import store from 'store';
  1. 扩展story
    在storiesOf中增加装饰器,装饰器作用就是把story包裹起来。 因此我们可以利用装饰器的特点把store引入到控件中。
// stories/userStory/index.jsx
import React from 'react';
import { provider } from 'react-redux'
import { storiesOf } from '@storybook/react';
// 引入store
import store from 'store';
import BasicInfo from 'pages/clientDetail/components/BasicInfo';

storiesOf('用户信息', module)
  .addDecorator(storyFn => <Provider store={store}>{storyFn()}</Provider>)
  .add('基础信息', () => <BasicInfo />);
  1. mock数据
    团队搭建的yapi平台负责mock数据生成。

  2. whistle
    由于mock数据和storybook不在同一个域,js调用mock数据会跨域,需要做请求转发代理。我们团队使用的是whistle。whistle是个好东西👍,墙裂推荐!!!

    以下是whistle配置的转发规则。

//yourproject.com resCors://*
//localhost:8888 resCors://enable
//yourproject.com http://127.0.0.1:8888/  weinre://
# 9001是storybook的端口
# https://myapi.xxx.com 是yapi所在域名。
^localhost:9001/cgi-bin/** //myapi.xxx.com/mock/3876/cgi-bin/$1

addons

什么是addons,其实可以理解扩展storybook功能的插件。

我使用到的addons有

  • addon-actions 用于展示事件处理函数接收到的数据
  • addon-console console输出(log、error、warning)
  • addon-info 这个最有用了,如果控件填写了proptype,直接就能显示到storybook中。
  • addon-viewport 其实就是chrome的device toolbar功能。

注意
addon-actions和addon-viewport都需要在addons.js中注册才能使用。

实践

实践代码

这个就是我的.storybook目录结构。

.
├── README.md
├── addons.js
├── config.js
├── stories
│   └── userStory
│       └── index.js
└── webpack.config.js
  • config.js
import { configure } from '@storybook/react';
import { setConsoleOptions } from '@storybook/addon-console';
import 'index.scss';

setConsoleOptions({
  panelExclude: [],
});

function loadStories() {
  require('./stories/userStory');
}

configure(loadStories, module);
  • webpack.config.js
const path = require('path');

module.exports = (storybookBaseConfig, configType) => {
  storybookBaseConfig.module.rules.push({
    test: /\.s?css$/,
    use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 1 } }, 'postcss-loader'],
    include: path.resolve(__dirname, '../'),
  });

  storybookBaseConfig.module.rules.push({
    test: /\.(gif|png|jpe?g|eot|woff|ttf|pdf)$/,
    loader: 'file-loader',
  });

  // 设置别名
  storybookBaseConfig.resolve.alias = {
    antd: path.resolve(__dirname, '..', `node_modules/antd/dist/antd.min.js`),
    antdcss: path.resolve(__dirname, '..', 'node_modules/antd/dist/antd.min.css'),
    antdzhCN: path.resolve(__dirname, '..', 'node_modules/antd/lib/locale-provider/zh_CN.js'),
  };

  // 增加src为绝对路径
  storybookBaseConfig.resolve.modules.push(path.resolve(__dirname, '..', 'src'));

  // 使用source-map
  storybookBaseConfig.devtool = 'source-map';

  storybookBaseConfig.mode = 'development';

  return storybookBaseConfig;
};
  • addons.js
import '@storybook/addon-actions/register';
import '@storybook/addon-viewport/register';

action和viewport addon均需要在addons.js中注册才能正常使用。

  • userStory
import React from 'react';
import { Provider } from 'react-redux';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withConsole } from '@storybook/addon-console';
import BasicInfo from 'pages/clientDetail/components/BasicInfo';
import KeyActionItem from 'pages/clientDetail/components/KeyAction/KeyActionItem';
import { withInfo } from '@storybook/addon-info';
import store from 'pages/clientDetail/store';
import zhCN from 'antdzhCN';
import { LocaleProvider } from 'antd';
import 'antdcss';

// redux结合
storiesOf('用户信息', module)
  .addDecorator(withInfo)
  .addDecorator((storyFn, context) => withConsole()(storyFn)(context))
  .addDecorator(storyFn => <Provider store={store}>{storyFn()}</Provider>)
  .addDecorator(storyFn => <LocaleProvider locale={zhCN}>{storyFn()}</LocaleProvider>)
  .add('基础信息', () => <BasicInfo />, {
    info: {
      text: `
        用户基础信息展示,可进行上下翻页。
      `,
    },
  });

storiesOf('行为轨迹item', module)
  .addDecorator(withInfo)
  .addDecorator((storyFn, context) => withConsole()(storyFn)(context))
  .add('行为轨迹item--课程顾问', () => (
    <div style={{ display: 'flex', justifyContent: 'center', paddingTop: '30px' }}>
      <KeyActionItem
        time="2018-12-12 12:12:12"
        user="testUser"
        role="课程顾问"
        type="saler"
        data={[{ title: '备注', content: '备注测试'}]}
        id={1}
        onClickDelete={action('onClickDelete')}
        canDelete
      />
    </div>
  ))
  .add('行为轨迹item--客户', () => (
    <div style={{ display: 'flex', justifyContent: 'center', paddingTop: '30px' }}>
      <KeyActionItem
        time="2018-12-12 12:12:12"
        user="testUser"
        role="家长"
        type="client"
        data={[{ title: '购买记录', content: '测试购买记录'}]}
      />
    </div>
  ))
  .add('行为轨迹item--admin', () => (
    <div style={{ display: 'flex', justifyContent: 'center', paddingTop: '30px' }}>
      <KeyActionItem
        time="2018-12-12 12:12:12"
        user="admin"
        type="admin"
        data={[{ title: '购买记录', content: '测试购买记录'}]}
      />
    </div>
  ));

实践截图

addons-info控件能把组件的prop信息展示出来

结语

我在本文中介绍了最基本的storybook使用。

  1. storybook配置
  2. webpack配置
  3. redux引入
  4. addons介绍

文章中介绍的使用方式实在是很简单,希望能安利更多人使用storybook。更多(高大上)storybook的实践可以参考这里 storybook实践

参考

  1. Storybook官网
  2. StoryBook meets redux