【Nuxt.js / Storybook】インストールとaddonの導入まで(StoriesOf APIではなく最新ver推奨のCSF記法で)

**追記**
新しいバージョンでは、`.storybook/webpack.config.js`に設定を記述するのではなく`.storybook/main.js`や`.storybook/preview.js`を利用するようです。詳細は公式ドキュメントをご確認ください。

---

新規でNuxt(Vue)のプロジェクトを立ち上げる機会があったので、初めてStorybookを導入してみました。
現在Storybookのバージョンは5.2.xですが、公式が推奨するストーリーの記述方式が**Component Story Format (CSF)**に変更されたようです。
(v5.1以前は**StoriesOf API**が一般的。v5.2でも問題なく使用できる)
[Component Story Format (CSF)](https://storybook.js.org/docs/formats/component-story-format/)
[StoriesOf API](https://storybook.js.org/docs/formats/storiesof-api/)
初導入にあたり参考情報を探していたのですが、公式ドキュメントもネットの情報も**StoriesOf API**での記載例が多かったので(当たり前ですが)、良い機会だと思い基本的な情報ですが**CSF**での記法でまとめでみました。

# 1.インストールと基本設定
[Storybook for Vue](https://storybook.js.org/docs/guides/guide-vue/)
CLIを使用して半自動でインストールする方法と、必要なモジュールを自分で追加していく方法がありますが、CLIでインストールする方がやはり楽です。

## 1.1 NuxtプロジェクトにStorybookをCLIでインストール

CLIを使用すると雛形のStoryファイルが生成されるのでそれを見るだけでも基本的な使用方法はわかると思います。

```shell
$ npx -p @storybook/cli sb init
```
上記の書き方で、プロジェクトが何のフレームワークで構成されているか自動で判定して必要なモジュール群をインストールしてくれます(心配な場合はオプションでフレームワーク名を追加してください。`--type vue`)
作ったばかりのNuxtプロジェクトですが、以下のモジュールが追加されました。

```shell
+ @babel/core@7.7.2
+ babel-loader@8.0.6
+ babel-preset-vue@2.0.2
+ @storybook/addons@5.2.6
+ @storybook/addon-actions@5.2.6
+ @storybook/addon-links@5.2.6
+ @storybook/vue@5.2.6
```
さらに、storybookの設定ファイルなどを格納する`.storybook`とストーリーファイルを格納する`stories`の2つのディレクトリも同時に生成されます。

## 1.2 package.jsonの設定
実行する際のコマンドを追加します。が、CLIでインストールするとこの記述も追加されていますので、特に編集はいりません。Dockerなどを使用している関係でポート番号を変更したいなどありましたら編集してください。

```json
"scripts": {
  "storybook": "start-storybook -p 6006",
  "build-storybook": "build-storybook"
}
```
これで、下記コマンドによる実行ができるようになりました。

```shell
$ npm run story
```

## 1.3 Storybook用webpackの設定
StorybookはNuxtなどと実行環境が別(この言い方で良いのか..?)なので、専用にwebpackの設定も行う必要があります。以下のファイルを編集していきます。CLIによって自動生成されるはずですが、なかったら追加してください。
`.storybook/webpackconfig.js`

### 1.3.1 Nuxtプロジェクトで使用している`@`や`~`などのエイリアス設定
Nuxtプロジェクトからコンポーネントの読み込みをした際に、`@`や`~`などのエイリアスを使用している場合エラーになるので追記しましょう。
Nuxtのプロジェクトディレクトリを移動している場合は適宜`rootPath`の値を変更してください。

```　.storybook/webpack.config.js
const path = require('path')
const rootPath = path.resolve(__dirname, '../')
// Nuxtのプロジェクトをsrc以下に移動しいる場合
// const rootPath = path.resolve(__dirname, '../src/')

module.exports = ({ config }) => {

  config.resolve.alias['~'] = rootPath
  config.resolve.alias['@'] = rootPath
  
  return config
}
```

### 1.3.2 Typescriptの使用設定
Typescriptを使用する場合に。素のjsでプロジェクトを記述している場合は無視してください。

```　.storybook/webpack.config.js
config.module.rules.push({
  test: /\.ts/,
  use: [
    {
      loader: 'ts-loader',
      options: {
        appendTsSuffixTo: [/\.vue$/],
        transpileOnly: true
      },
    }
  ],
});
```

### 1.3.3 scssの使用設定、かつ変数やmixinを使用している場合
Storybookで表示するコンポーネント内で、scssの変数やmixinを使用している場合はここで指定します。
以下のモジュールが追加されていない場合にはインストールしてください。

```shell
$ npm install --save-dev style-loader css-loader sass-loader @nuxtjs/style-resources
```

```　.storybook/webpack.config.js
config.module.rules.push({
  test: /\.scss$/,
    use: [
      'style-loader', 'css-loader', 'sass-loader',
      {
        loader: 'sass-resources-loader',
        options: {
          resources: [ 
            './assets/scss/_variable.scss',
          ],
          rootPath  // path.resolve(__dirname, '../')
        }
      },
    ],
});
```

### 1.3.4 css内からassets以下のファイルにアクセスできるように設定
`assets`ディレクトリ以下の画像やフォントにcssからアクセスできるように設定します。

```　.storybook/webpack.config.js
config.resolve.modules = [
  ...(config.resolve.modules || []),
  rootPath
];
```

基本的には、以上で設定は終わりです。

## 1.4 簡単なストーリーの記述

`MyButton`コンポーネントを表示する、ごく基本的なストーリーの記述方を書きます。
**Component Story Format (CSF)**での書き方は、実際に記述してみれば結構簡単かと思います。
ストーリーは`.storybook/config.js`の中にある以下の記述によって、`/stories`以下の`.stories.js`がついたファイルが自動で読み込まれます。

```　.storybook/config.js
// automatically import all files ending in *.stories.js
configure(require.context('../stories/', true, /\.stories\.js$/), module)
```


```sample.stories.js
import MyButton from '@/components/MyButton'

// コンポーネントのメタデータを記述
// アドオンのでコレータやパラメータなどもここに書く
export default {
  // スラッシュで区切ると、Atoms>Buttonと階層構造で表示できます
  //　他にも記述方法はあるが割愛。公式ドキュメントを確認してください
  // タイトルは一意であること
  title: 'Atoms/Button',
  decorators: [ ... ],
  parameters: { ... }
}

// Common Link Btnがストーリーの名前となります。
export const CommonLinkBtn = () => ({
  components: MyButton,
  // 適宜htmlなりを記述
  template: `
  <div id="storybook-container">
    <div class='component-wrapper'>
      <h5>MyButton</h5>
      <Button>{{ label }}</ButtonLink>
    </div>
  </div>
  `
})

// 下記のように名称を変更できます
// CommonLinkBtn.story = { name: 'hoge' }

```

---

# 2. 代表的なaddonの追加

追加していくaddonは以下です。全てStorybookの[公式](https://github.com/storybookjs/storybook/blob/master/ADDONS_SUPPORT.md)です

|アドオン|機能|github|
|:--|:--|:--|
|addon-knobs|動的にpropsの値を変更して、コンポーネントの挙動を確認できる|[@storybook/knobs](https://github.com/storybookjs/storybook/tree/master/addons/knobs#with-vuejs)|
|actions|onClickなどのUIアクションに対してのログを確認できる|[@storybook/addon-actions](https://github.com/storybookjs/storybook/tree/master/addons/actions)|
|notes|text,html,マークダウンでコンポーネントの説明を追加できる|[@storybook/notes](https://github.com/storybookjs/storybook/tree/master/addons/notes)|
|viewport|表示サイズの変更|[@storybook/viewport](https://github.com/storybookjs/storybook/tree/master/addons/viewport)|
|a11y|コンポーネントのアクセシビリティチェック|[@storybook/addon-a11y](https://github.com/storybookjs/storybook/tree/master/addons/a11y)|
|backgrounds|ストーリーの背景色変更|[@storybook/addon-backgrounds](https://github.com/storybookjs/storybook/tree/master/addons/backgrounds)|

上記のアドオンをインストール&Storyに追加

```shell
$ npm i -D @storybook/knobs @storybook/addon-actions @storybook/notes @storybook/viewport @storybook/addon-a11y @storybook/addon-backgrounds
```

インストールしたアドオンをimportします。

```　.storybook/config.js
// actionsとlinksはCLIによるインストール時に追加される。
// linksについては省略
import '@storybook/addon-links/register'
import '@storybook/addon-knobs/register'
import '@storybook/addon-actions/register'
import '@storybook/addon-notes/register'
import '@storybook/addon-viewport/register'
import '@storybook/addon-a11y/register'
import '@storybook/addon-backgrounds/register'
```

## 2.1 knobs

propsを設定したコンポーネントを用意します。

```MyButton.vue
<template>
  <button :disabled="isDisabled">
    <slot></slot>
  </button>
</template>
 
<script>
export default {
  props: {
    isDisabled: {
      type: Boolean,
      default: false
    }
  }
}
</script>
```

ちなみにpropsだけでなく、slotの値の挿入などもOKです。

```MyButton.story.js
import { withKnobs, text, boolean } from '@storybook/addon-knobs';
import MyButton from '@/components/MyButton.vue';
 
export default {
  title: 'Storybook Knobs',
  decorators: [ withKnobs ]
}

export const Button = () => ({
  components: { MyButton },
  props: {
    isDisabled: {
      // default: 与えるデータの型('ラベル', 初期値)
      default: boolean('Disabled', false)
    },
    text: {
      default: text('Text', 'Hello Storybook')
    }
  },
  template: `<MyButton :isDisabled="isDisabled">{{ text }}</MyButton>`
})
```

## 2.2 actions

下記のようなカスタムイベントを設定したコンポーネントを準備します。

```MyButton.vue
<template>
  <button @click="sampleEvent">ボタン</button>
</template>

<script>
export default {
  methods: {
    sampleEvent (e) {
      this.$emit('clicked', e)
    }
  }
}
</script>
```

上記を`actions`を使用して表示するストーリーは以下。

```MyButton.stories.js
import { action } from '@storybook/addon-actions'
import MyButton from '@/components/MyButton.vue';

export default {
  title: 'Storybook Actions',
}

export const PostBtn = () => ({
  components: { Button },
  // カスタムイベントが発火すると、以下のactionが実行されます。
  methods: {
    log: action('Post!')
  },
  template: `<ButtonPost @clicked="log" />`
})
```
actionが実行された様子
![スクリーンショット 2019-11-26 19.55.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42337/de90f6ff-2f89-a1ac-fe7e-c4a48c6bb834.png)


## 2.3 notes

`parameters`にテキストなり、マークダウンファイルを指定すればOKです。

```sample.stories.js
import markdownNotes from './someMarkdownText.md';

export default {
  title: 'Storybook Notes',
  // テキストの場合
  parameters: { notes: 'Some Notes' },
  // マークダウンファイルの場合
  parameters: { notes: { markdown: markdownNotes } }
}

__(省略)__
```

## 2.4 viewport
Storybook全体に適用する場合は、`.storybook/config.js`に設定を追記します。
公式ドキュメントの例に沿って、Kindle端末の画面サイズを追加します。

```　.storybook/config.js
import { addParameters } from '@storybook/vue';
// viewportが初期設定しているデバイスのパラメータリストを読み込む
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';

// Kindleのパラメータを設定
const newViewports = {
  kindleFire2: {
    name: 'Kindle Fire 2',
    styles: {
      width: '600px',
      height: '963px'
    }
  },
  kindleFireHD: {
    name: 'Kindle Fire HD',
    styles: {
      width: '533px',
      height: '801px'
    }
  }
}

// 初期パラメータと上記で設定したパラメータオブジェクトを追加
// ...INITIAL_VIEWPORTS,を再度追加しているのは表示順を
// INITIAL_VIEWPORTS > newViewportsにするため
addParameters({
  viewport: {
    viewports: {
      ...INITIAL_VIEWPORTS,
      ...newViewports,
    }
  }
})

// 上記の設定はconfigure(require.context~より前に記載すること
configure(require.context('../stories/', true, /\.stories\.js$/), module)
```

## 2.5 a11y
チェックしたいコンポーネントに読み込み、`decolators`に追加すればOK

```sample.stories.js
import { withA11y } from '@storybook/addon-a11y'

export default {
  title: 'Sample',
  decorators: [ withA11y ]
}

__(省略)__
```

## 2.6 backgrounds

全体に適用する場合は`config.js`に記述追加

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

addParameters({
  backgrounds: [
    { name: 'Sample BG 1', value: '#CCCCCC', default: true },
    { name: 'Sample BG 2', value: '#000000' },
  ],
});

// 上記の設定はconfigure(require.context~より前に記載すること
configure(require.context('../stories/', true, /\.stories\.js$/), module)
```

---

以上、私もまだ深いところまで触っていない&導入したばかりで間違い等あるかもしれませんが、基本的なところは抑えられているかと思います:pushpin: