JavaScript
stylus
vue.js
webpack
storybook
Vue.js #4Day 22

Storybookがなぜ必要か?(Vue.js編)

まさあき(@masaaakikunsan)です。
最近よく、「Storybookを導入しよう」「Storybookがいい」と言う話は聞きますが、意外となぜ必要なのか、どう使うのか、という記事がみつからなかったので、基本的な使い方をサンプルと共に紹介します。

TL;DR

  • StorybookでUIコンポーネントのカタログを作ることができる
  • カタログのおかげでデザイナーと認識の齟齬が生まれなくなる
  • アドオンを追加するとStorybookがかなり便利アイテムになる

Storybookとは

ざっくり言うとコンポーネントのカタログです。
コンポーネントライブラリの参照ができ、各コンポーネントの様々な状態の表示などができるものとなります。

また、アプリ外で実行されるため、UIコンポーネントを単独で開発でき、コンポネの再利用、テストの容易性、開発スピードを向上させることができるのが魅力です。

Storybookがなぜ必要か?

結論を先に述べますとデザイナーとの認識の齟齬をなくすためです。(※ スタイルガイドが絶対という場合のみ有効です)

Storybookがない場合、完成品をデザイナーに確認してもらい……という流れが多く、

デザイナー「ここデザインと違う」
デザイナー「こここうしたほうがよくない?」
エンジニア「はい。」

ということを経験したことがあるかたも多いでしょう。
この問題自体は双方に原因にあるのですが、完成後に起こった場合は手戻りなどがあり面倒です。
この問題を解決してくれるのがStorybookです。

Storybookを利用して、コンポーネントやページなどを適宜torybookに追加しておくことで、デザイナーがいつでも確認できる状況となります。

そうすることで、齟齬があれば早期に変更依頼をもらうことができ、かつパーツが公開されることで常に完成品のイメージができるため、認識の齟齬が生まれず無駄なコミュニケーションを減らすことができます。

これはデザイナーに限ったことではなく、他のエンジニアや非エンジニアでも確認することができるようになるため、非常に円滑な、手戻りの少ない作業が可能となります。

Storybookを導入する

では早速そんなStorybookを導入してみましょう。今回はVue.jsで作ってみます。
以下のコマンドを実行して、 Vue.js の基本セットを構築しましょう。

Vue.jsの環境を作る
$ vue init webpack my-project
$ cd my-project
$ yarn
$ yarn dev

次に、Storybookを導入し、Storybookの実行に必要なフォルダとファイルを追加しましょう。

Storybookをいれる
$ yarn add @storybook/vue
$ mkdir .storybook
$ mkdir src/stories
$ touch .storybook/webpack.config.js
$ touch .storybook/config.js
$ touch .storybook/addons.js
$ touch src/stories/index.js

次にStorybookの設定ファイルを実際に記述し、実行のための準備を行います。
storybookはアプリ外で実行されるのでwebpackの設定をしましょう。

今回は詳しい説明を省きますので、以下の通りに記述してください。

.storybook/webpack.config.js
const path = require('path')

module.exports = {
  module: {
    rules: [
      {
        test: /\.styl$/,
        loaders: ["style-loader", "css-loader", "stylus-loader",{
          loader: 'vuetify-loader',
          options: {
            theme: path.resolve(__dirname, '../src/stylus/')
          }
        }],
        include: path.resolve(__dirname, '../src')
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      vue: 'vue/dist/vue.esm.js',
      '@': path.resolve(__dirname, '../src/')
    }
  }
}

最後にメインとなるコンポーネントを定義するための、config.jsを記述します。
ここに、Storybookの一つ一つの画面の単位である「ストーリー」を登録します。
以下のように記述すると良いでしょう。

.storybook/config.js
import { configure } from '@storybook/vue'

const loadStories = () => {
  require("../src/stories/index")
}

configure(loadStories, module)

以上で、Storybookを実行する設定は終わりです。
実際に実行してみましょう。

$ yarn storybook

スクリーンショット 2017-12-19 4.37.06.png

実行し上記画像のような画面がでてきたら成功です。
まだストーリーを作成していないため、Storybookは真っ白の状態です。

次に、実際のコンポーネント表示のための画面、ストーリーを作成してみましょう。
ストーリーを表示するためには、まず、コンポーネントを作成します。
今回はpropsで見た目をカスタマイズすることができる、汎用性の高いボタンを作ってみました。

src/components/Buttons.vue
<template>
  <button
    @click="handleClick"
    :disabled="disabled||false"
    :class="[
    'button',
    'button-' + (disabled ? 'disabled' : kind),
    size  ? 'button-'+size : 'button-normal'
    ]"
  >
    <span>{{ text }}</span>
  </button>
</template>

<script>
export default {
  props: {
    text: {
      type: String
    },
    kind: {
      type: String
    },
    size: {
      type: String
    },
    disabled: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    onclick (e) {
      this.$emit('click', e)
    }
  }
}
</script>

<style scoped lang="stylus">
.button {
  font-size: 14px;
    font-weight: bold;
    border: solid 1px;
    border-radius: 5px;
    margin: 10px 10px 10px 0;
    padding: 2px 6px 3px;
    outline: none;
    cursor: pointer;
    transition: all 0.3s ease-out;
    -webkit-font-smoothing: antialiased;

  &:hover {
    opacity: 0.5;
  }

  &-minimum {
  width: 120px;
  height: 45px;
  }

  &-small {
    width: 140px;
    height: 45px;
  }

  &-normal {
    width: 180px;
    height: 50px;
  }


  &-large {
    width: 220px;
    height: 60px;
    font-size: 19px;
  }

  &-full {
    width: 100%;
    height: 45px;
  }

  &-default {
    color: #333;
    background-color: #fff;
    border-color: #ccc;
  }

  &-primary {
    background-color: #337ab7;
    border-color: #2e6da4;
    color: #fff;
  }

  &-success {
    background-color: #5cb85c;
    border-color: #4cae4c;
    color: #fff;
  }

  &-info {
    background-color: #5bc0de;
    border-color: #46b8da;
    color: #fff;
  }

  &-warning {
    background-color: #f0ad4e;
    border-color: #eea236;
    color: #fff;
  }

  &-danger {
    background-color: #d9534f;
    border-color: #d43f3a;
    color: #fff;
  }

  &-dark {
    color: #fff;
    background: #2A3C4D;
  }

  &-disabled {
    color: #aaa;
    background: #ddd;
    border-color: #ddd;
    cursor: default;
  }

  span {
    align-items: center;
    justify-content: center;
    display: flex;
  }
}
</style>

コンポーネントが用意できたら、ストーリーを定義します。
storiesOfでコンポーネントを指定し.add()でモジュールの状態を追加しておきます。
.addの第一引数にはストーリーのタイトルをつけてあげましょう。
今回はcolorもsizeもデフォルトの状態を表示させたいのでdefaultにします。

src/stories/index.js
import { storiesOf } from '@storybook/vue'
import Button from '../components/Button.vue'

storiesOf('Button', module)
  .add('default', () => ({
    components: { Button },
    template: `<Button text="default" />`
  }))

ここまでできたら、yarn storybook を実行しましょう。ビルド後、ボタンが表示されます。

Screenshot from Gyazo

Buttonの下にdefaultがあり、それを見るとdefaultというデフォルトのボタンが表示されているのがわかります。
ここからpropsを変更した見た目を表示したらカタログとしては完成です。
早速、storiesにpropsを変更した見た目を加えてみます。

今回はわかりやすさを重視し、Story名は愚直にsizeとcolorにしsizeとcolorのバリエーションを見られるようにします。

src/stories/index.js
import { storiesOf } from '@storybook/vue'
import Button from '../components/Button.vue'

storiesOf('Button', module)
  .add('default', () => ({
    components: { Button },
    template: `<Button text="default" />`
  }))
  .add('color', () => ({
    components: { Button },
    template: `
      <div style="display: flex;">
        <Button text='primary' kind='primary'/>
        <Button text='default' kind='default'/>
        <Button text='success' kind='success'/>
        <Button text='info' kind='info'/>
        <Button text='warning' kind='warning'/>
        <Button text='danger' kind='danger'/>
        <Button text='dark' kind='dark'/>
      </div>
    `
  }))
  .add('size', () => ({
    components: { Button },
    template: `
      <div>
        <div style="display: flex;">
          <Button text='minimum' kind='primary' size="minimum"/>
          <Button text='small' kind='primary' size="small"/>
          <Button text='normal' kind='primary' size="normal"/>
          <Button text='large' kind='primary' size="large"/>
        </div>
        <div style="display:flex;"><Button text='full' kind='primary' size="full"/></div>
      </div>
      `
  }))

Screenshot from Gyazo

storyにcolorとsizeが追加されました。
このようにコンポーネントを確認できるようにしておくと、デザイナー側も作業が行いやすくなるかと思います。

ちなみに、一つのストーリーには一つのコンポーネントのストーリーを記述するのが推奨されているので、複数コンポーネントがある場合はファイルを分割するようにしましょう。

Storybookを強化するアドオン

Storybookにさらに機能が欲しい場合に、アドオン機能を利用することができます。

個人におすすめのアドオンは以下の二つです。

  • addon-viewport(さまざまなサイズとレイアウトで確認できる)
  • storybook-addon-vue-info(コンポーネント情報を載せれる)

これを、実際に導入してみる例をご紹介します。

$ yarn add storybook-addon-vue-info
$ yarn add @storybook/addon-viewport

アドオンを入れたらaddons.jsでimportしてstoriesに必要な記述を追加したら完成です。

.storybook/addons.js
import '@storybook/addon-viewport/register'
src/stories/index.js
import { storiesOf } from '@storybook/vue'
import VueInfoAddon from 'storybook-addon-vue-info'
import Button from '../components/Button.vue'

storiesOf('Usage button', module)
  .addDecorator(VueInfoAddon)
  .add('default', () => ({
    components: { Button },
    template: `<Button text="default" />`
  }))

storiesOf('Button', module)
  .add('default', () => ({
    components: { Button },
    template: `<Button text="default" />`
  }))
  .add('color', () => ({
    components: { Button },
    template: `
      <div style="display: flex;">
        <Button text='primary' kind='primary'/>
        <Button text='default' kind='default'/>
        <Button text='success' kind='success'/>
        <Button text='info' kind='info'/>
        <Button text='warning' kind='warning'/>
        <Button text='danger' kind='danger'/>
        <Button text='dark' kind='dark'/>
      </div>
    `
  }))
  .add('size', () => ({
    components: { Button },
    template: `
      <div>
        <div style="display: flex;">
          <Button text='minimum' kind='primary' size="minimum"/>
          <Button text='small' kind='primary' size="small"/>
          <Button text='normal' kind='primary' size="normal"/>
          <Button text='large' kind='primary' size="large"/>
        </div>
        <div style="display:flex;"><Button text='full' kind='primary' size="full"/></div>
      </div>
      `
  }))

Screenshot from Gyazo

addon-viewportによりChromeのモバイルエミュレーターを使わずにデバイスを切り替えられ
storybook-addon-vue-infoによりコンポーネントの使い方・見た目・propsのName,Type,Defaultが確認できるようになりました。
addonsを追加することによりStorybookがただのカタログではないことがわかっていただけたと思います。

アドオンの組み合わせ技

これは今回の記事を書いていて、もしできたらいいなと思ってやってみたらできてしまったものです。
組み合わせに使用するアドオンは、以下の三つです。

  • addon-knobs
  • addon-notes
  • storybook-addon-vue-info

storybook-addon-vue-infoでコンポーネントの使い方・見た目・propsを確認できるようにし、
addon-notesにpropsの詳細を書きます。
そして、addon-knobsでコンポーネントのpropsをStorybook上で変更できるようにします。

これを実装することにより、二つのメリットがあります。

  1. 他エンジニアがコードを見ずにコンポーネントが使用できる
  2. デザイナーが画面上でpropsを変更し様々な状態を確認できる

では、実際に実装してみます。
これまでにStorybookの導入方法を説明しているので、コードの説明等は省かせていただきます。
以下を参考に記述してください。

$ yarn add @storybook/addon-knobs
$ yarn add @storybook/addon-notes
.storybook/addons.js
import '@storybook/addon-viewport/register'
import '@storybook/addon-knobs/register'
import '@storybook/addon-notes/register'
src/stories/index.js
import { storiesOf } from '@storybook/vue'

import VueInfoAddon from 'storybook-addon-vue-info'
import { withKnobs, text } from '@storybook/addon-knobs'
import { withNotes } from '@storybook/addon-notes'

import Button from '../components/Button.vue'

storiesOf('Usage button', module)
  .addDecorator(VueInfoAddon)
  .addDecorator(withKnobs)
  .add('default', withNotes(
    `
      sizeとcolorはここにあるやつを使ってください
      size: minimum, small, normal, large, full
      color: default, primary, success, info, warning, danger, dark
    `
  )(() => {
    const ButtonText = text('text', 'defaul')
    const size = text('size', 'large')
    const color = text('color', 'default')
    return {
      components: { Button },
      template: `<Button text="${ButtonText}" size="${size}" kind="${color}" />`
    }
  }))

Screenshot from Gyazo

notesに書いてあるprops通りにknobsのpropsを変更したら、UsageのコードとPreviewが変わりました。

まとめ

ここまでStorybookの使い方となんで必要かの説明をしてきましたが、いかがでしたか?

今回はエンジニアがコンポネを書く場合でのあったらいいよねでしたが、デザイナーがコード書く場合はストーリーを作るだけで、Storybookがそのままデザインになるため、コードがかけるデザイナーの場合はより楽に進めることができるでしょう。
デザイナーがStorybookでスタイルガイドを作る時代がきたら個人的には楽でいいなと思っています。

Storybookを追加することで平和な環境が生まれたらいいなと個人的には願ってます。
ぜひ会社で導入してみてください。

今回のサンプルコードをHerokuにて実際のデモとして動かせるようにしているので、興味があればそちらもご覧ください。
もっとこうした方がいいとかあればぜひPRを送ってください。
https://storybook-demo.herokuapp.com
https://github.com/masaakikunsan/storybook_demo

参考資料