Vue + Storybook 实现组件驱动开发

为什么要使用Storybook?

在前端业务的开发中,我们可能会封装很多公共组件。大部分时候因为忙于业务开发,并不会为这些公共组件进行文档编写、测试。在进行复用的时候,需要在项目中查看源码去查看该组件的作用、参数和事件。这种组件开发的方式在前端项目规模不大的时候还能接受,但当项目到达一定规模后,一般会存在以下问题:

一个普通标题 一个普通标题 一个普通标题
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
短文本 中等文本 稍微长一点的文本
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
  1. 重复造轮子:因为没有统一的组件展示,其他开发者会不清楚组件已经在项目中实现,出现重复造轮子的现象。
  2. 组件通用性不强:组件和项目逻辑强耦合,导致重复利用率不高。
  3. 不知道如何使用:没有组件文档的存在,其他开发者需要去查看源码弄明白组件有哪些 eventprops。增加了组件的使用难度
image
image

这些问题可以通过在项目中引入Storybook来解决,它是一个用于组件开发、测试和文档编写的开源工具,并支持Vue、React、Angular等主流框架。

在项目中引入Storybook可以帮助我们设计出通用性更强的组件,并轻松实现像Element、AntDesign那样的结构化组件文档。

如何在Vue项目中安装Storybook?

笔者通常使用Vue进行前端UI层开发,这里只介绍如何在Vue项目中安装Storybook,如果你是其他JS框架的使用者,我相信Storybook的官方文档提供的丰富教程一定能够满足你。

这里使用的示例代码是一个基础的VueCLI项目。有过Vue项目经验的一定不会陌生:

├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│   ├── favicon.ico
│   └── index.html
└── src
    ├── App.vue
    ├── assets
    │   └── logo.png
    ├── components
    │   └── HelloWorld.vue
    └── main.js

1. 自动安装

Storybook官方提供了npx的自动安装模式,你只需要在命令行中输入下面的脚本(安装时间较长):

npx -p @storybook/cli sb init --type vue

在安装完成后,项目中多出了两个文件夹 .storybookstories

├── .storybook
│   └── main.js
└── stories
    ├── 0-Welcome.stories.js
    ├── 1-Button.stories.js
    ├── MyButton.js
    └── Welcome.js

在命令行执行 npm run storybook ,Storybook默认会在localhost: 6006端口打开

image
image

2. 手动安装

  1. 安装 @storybook/vue
npm install @storybook/vue 
  1. 安装相关依赖
npm install vue-loader vue-template-compiler @babel/core babel-loader babel-preset-vue --save-dev 
  1. package.json中添加启动脚本
{
  "scripts": {
    "storybook": "start-storybook"
  }
}
  1. 添加入口文件.storybook/main.js
module.exports = {
  stories: ['../src/components/**/*.stories.js'],
  addons: [],
};
  1. 启动Storybook,默认会在localhost: 6006
npm run storybook
image
image

Storybook的基础知识

在完成安装后,我们还需要了解一下Storybook的一些基础知识,比如项目的配置文件在哪里?项目中如何添加插件?什么是CSF?如何写组件的故事?如何添加自定义的Webpack配置?

1. 配置文件 - main.js

module.exports = {
  stories: ['../stories/**/*.stories.js'],
  addons: ['@storybook/addon-actions', '@storybook/addon-links'],
};

.storybook/main.js 是Storybook的配置文件,主要的配置是storiesaddons。更多的配置你可以查看文档 Configuration overview

  • stories 用来描述你的故事相对于配置文件的位置,从默认的配置可以知道,故事的文件名称格式为:*.stories.js。
  • addons 用来描述你需要引入的插件。storybook提供了丰富的插件

2. 插件-Addons

image
image

上图storybook官方提供的插件,例如@storybook/addon-links 用来创建storybook中的链接关系,@storybook/addon-docs 提供了开箱即用的组件文档。在社区里面也有很多其非官方的插件。

要使用Addons首先要进行安装,以@storybook/addon-links为例:

npm install -D @storybook/addon-links

.storybook/main.js中添加插件

module.exports = {
  stories: ['../stories/**/*.stories.js'],
  addons: ['@storybook/addon-links'],
};

在你的stories中使用插件

import { linkTo } from '@storybook/addon-links'

export default {
  title: 'Button',
};

export const first = () => (
  <button onClick={linkTo('Button', 'second')}>Go to "Second"</button>
);
export const second = () => (
  <button onClick={linkTo('Button', 'first')}>Go to "First"</button>
);

3. 组件故事格式 - CSF

要熟练使用新版本Storybook,一定要理解一个术语:Component Story Format(CSF),这是Storybook在5.2版本引入的一种采用ES6 modules去编写stories的方式,在官方文档中,CSF被反复的提及。为了更好的理解story和CSF的关系,我们来看一下官方提供的 1-Button.stories.js

import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';

import MyButton from './MyButton';

export default {
  title: 'Button',
  component: MyButton,
};

export const Text = () => ({
  components: { MyButton },
  template: '<my-button @click="action">Hello Button</my-button>',
  methods: { action: action('clicked') },
});

export const Jsx = () => ({
  components: { MyButton },
  render(h) {
    return <my-button onClick={this.action}>With JSX</my-button>;
  },
  methods: { action: linkTo('clicked') },
});

export const Emoji = () => ({
  components: { MyButton },
  template: '<my-button @click="action">😀 😎 👍 💯</my-button>',
  methods: { action: action('clicked') },
});

这是一个十分典型的stories.jsexport default定义了这是一个名为Button,展示Mybutton 组件的故事文件。需要注意的是,该对象的****title**** 是侧边栏中的标题,组件内title不能重复****。

export default {
  title: 'Button',
  component: MyButton,
};

三个export const定了三个story,分别对应的是侧边栏中的Text、Jsx、Emoji。

image
image

看到这里你大概明白到底什么是story了:story是一个代码片段,它已特定状态呈现该组件的示例。而CSF,就是ES6的Modules语法去编写 ****.stories.js****的一种格式

4. 添加自定义webpack配置

如果我们在组件中使用 less、sass等css语法,在启动storybook后就会出现报错,因为storybook的并不能使用项目中的webpack配置,我们需要在.storybook/main.js中添加scss-loader的配置。

.storybook/main.js中添加自定义webpack配置的字段是webpackFinal。例如你想添加对于sass的支持,代码如下:

module.exports = {
  stories: ['../**/*.stories.js'],
  addons: [],
  webpackFinal: async (config, { configType }) => {
    config.module.rules.push({
      test: /\.scss$/,
      use: ['style-loader', 'css-loader', 'sass-loader'],
    });

    return config;
  },
};

Storybook的Addons

我们可以使用addons来让storybook更加的强大,这里我介绍几个常用的插件,使用他们可以满足我CDD的开发模式,生成良好、可交互的组件文档。
**
注意:每个插件都需要npm install @storybook/addon-xxxx 安装,并在 .storyboo/``main.js中配置,方法见上文,下面的示例中会略过这部分操作。

1.@storybook/addon-actions

image
image

@storybook/addon-actions 可以用来在页面上打印组件 @emit 的事件和其事件参数,上面的story中,组件触发了btnClick 事件,打印出了参数['点我']

  • MyButton.vue 组件:
<template>
  <div>
    <button @click="btnClick">{{ text }}</button>
  </div>
</template>

<script>
export default {
  name: "MyButton",
  props: {
    text: String,
  },
  methods: {
    btnClick() {
      this.$emit('btnClick', this.text);
    }
  },
}
</script>
  • Button.stories.js
import { action } from '@storybook/addon-actions';
import MyButton from './MyButton.vue';

export default {
  title: 'MyButton',
  component: MyButton,
};


export const 使用actions插件 = () => ({
    components: {
        MyButton,
    },
    template: `
    <my-button text="点我" @btnClick="btnClick"/>
    `,
  methods: {
    btnClick: action('btnClick')
  }
});

2.@storybook/addon-viewport

image
image

@storybook/addon-viewport 能够实现viewport的改变,这在移动端组件文档中是十分必要的。

  • Button.stories.js
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
import MyButton from './MyButton.vue';

export default {
  title: 'MyButton',
  component: MyButton,
  parameters: {
    viewport: { 
      viewports: INITIAL_VIEWPORTS,
      defaultViewport: 'iphone6' 
    },
  }
};

export const 使用viewport插件 = () => ({
    components: {
        MyButton,
    },
    template: `
    <my-button text="点我"/>
    `,
})

使用viewport插件.story = {
  parameters: {
    viewport: { 
      defaultViewport: 'iphonex' 
    },
  },
}

但是使用该插件时,控制台会有一个warning信息,在笔者写这篇文章的时候,这个issue依然是open状态,暂时就忽略这个warning吧:

Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
    in Unknown (created by Context.Consumer)
    in WithTheme(Component)

3.@storybook/addon-docs

image
image

一旦在.storybook/main.js中安装了``@storybook/addon-docs,它可以默认列出组件的所有propsevents,不需要额外的配置。该插件也提供了 .mdx的语法支持,允许你使用markdown的语法之定义文档。但是我更喜欢使用下面的一个组件去抒写文档。

4.@storybook/addon-notes

image
image

@storybook/addon-notes能够完全支持.md的语法,相比较于``,我更加喜欢使用他去写组件的相关文档。

  • Button.md
# Storybook教程

### CSF格式的优点

- 💎很简单。编写故事就像从您的故事文件中以您知道和喜欢的干净标准格式导出ES6功能一样容易。
- 🚚 便携式。可以轻松地在ES6模块存在的任何地方使用组件故事,包括您最喜欢的测试工具(如Jest和Cypress)。
- 🔥优化。组件故事除了您的组件之外不需要任何库。而且因为它们是ES6模块,所以它们甚至可以Tree Shaking!
- ☝️声明式的。声明性语法与更高级别的格式(如MDX)同构,从而实现了清晰可验证的转换。- 
  • Button.stories.js
import ButtonMd from './Button.md';

export const 基础使用 = () => ({
    components: {
        MyButton,
    },
    template: `
    <my-button text="点我" @btnClick="btnClick"/>
    `,
  methods: {
    btnClick: action('btnClick')
  }
});

基础使用.story = {
  parameters: {
    notes: { ButtonMd },
  }
};

Github代码地址: https://github.com/Lee-Tanghui/vue-storybook-tutorial
语雀地址:https://www.yuque.com/docs/share/fcfa9ccf-e54f-4197-9ff9-0d512bf2b024?#

推荐阅读更多精彩内容