vuetifyのstorybookをchromaticにホスティングする

# はじめに
vueなどで開発したWeb画面の部品を描画してデザイナーとの齟齬をなくしたり、見た目のテストにstorybookが便利だと言われています。
vueの部品をきれいにするvuetifyライブラリにsotrybookを適用する方法がほとんど書かれていなかったのでまとめました。
さらにstorybookはchromaticにホスティングすることを推奨しているため、chromaticにホスティングする方法もまとめます。

# 環境
 - node:v12.18.2
 - npm:6.14.5
 - yarn:1.22.4
 - vue/cli:4.5.4
 - npx:6.14.5

# 前提
Node.jsとVue CLIのインストールは多くのサイトで説明されているので
ここではインストール済みなことを前提とします。

# Vuetifyプロジェクトの作成
## Vueプロジェクトの作成
Vueプロジェクトをvue-cliを使用して作成します。
コマンドを打つと設定値の入力になります。設定値はstorybookには関係ないため自由で良いですが、例では以下のようにしています。
選択肢があるときはキーボードの上下で対象を選択してスペースで有効無効の切り替えエンターで決定です。

 - preset:Manually select freature
 - 設定:TypeScrit、Router、Vuex、Unit Testing
 - Vueバージョン:2.x
 - クラススタイル:Yes
 - BabelとTypeScriptの使用:Yes

```
vue create vuetify-book-ts
```

##### コンソール画面
![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481713/bb13b023-7496-20a4-31ea-aa5881440901.png)


## Vueプロジェクトの確認
コマンドが正常終了するとVueプロジェクトが実行できるようになっているため、試しに実行してみます。
実行が完了するとコマンドを実行したプロンプトにアドレスが出るためブラウザからアクセスします。

```
cd vuetify-book-ts
yarn serve
```

##### ブラウザ画面
![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481713/70fb6c1d-c342-4465-555f-0fac0d4eb298.png)

## vuetifyのインストール
作成したプロジェクトにvuetifyのインストールをします。
設定値はデフォルトで問題ないです。

```
vue add vuetify
```

## vuetifyプロジェクトの確認
Vueプロジェクトの作成と同様にvuetifyのプロジェクトのインストールが完了するとvuetifyが適用されてVueプロジェクトが実行できるようになっています。
試しに実行します。実行が完了するとコマンドを実行したプロンプトにアドレスが出るためブラウザからアクセスします。

```
yarn serve
```

##### ブラウザ画面
![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481713/19e18464-a82b-3692-0d44-e038ed71c69a.png)

## コンポーネントの作成
今はデフォルトのコンポーネントしかないため、自作のコンポーネントを作成します。
例なので単純なボタンです。

``` vue
<!-- src/components/MyButton.vue -->
<template>
    <v-btn depressed rounded :outlined="outlined" :color="color" :dark="dark">{{text}}</v-btn>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'

@Component
export default class SimpleButton extends Vue {
    @Prop({ type: String, default: ""}) text!: string
    @Prop({ type: String, default: "primary"}) color!: string
    @Prop({ type: String, default: "primary"}) type!: string

    outlined = false
    dark = false

    created() {
        switch(this.type){
            case "primary":
                this.outlined = false
                this.dark = true
                break
            case "inversion":
                this.outlined = true
                this.dark = false
                break
        }
    }
}
</script>
```

## ページの作成
コンポーネントだけでもstorybookに反映できるので特に必要はありませんが、せっかくなのでページも作成しておきます。

``` vue
<!-- src/view/MyPage.vue -->
<template>
  <v-container>
    {{ info }}
    <div>
      <my-button text="テスト"/>
    </div>
    <div>
      <my-button type="inversion" color="error" text="エラーテスト"/>
    </div>
  </v-container>
</template>

<script lang="ts">
import Vue from "vue";

import MyButton from '../components/MyButton.vue'
import axiosInstance from '../utils/axios'

export default Vue.extend({
  name: "HelloWorld",

  mounted () {
    axiosInstance
      .get('http://localhost/v1/apt/test')
      .then(response => (this.info = response.data))
  },
  data: () => ({
     info: null
  }),
  components: {
    MyButton,
  },
});
</script>
```

## axiosのクライアント作成
ページでaxiosを使用しているため、axiosのクライアントを作成するファイルを作成します。

``` ts
// src/utils/axios.ts
import axios from 'axios';

export default axios.create();
```

## リンクの追加
ページまで作成できたので、このページにアクセスするためのリンクを追加します。

``` ts
// src/router/index.ts
import Vue from "vue";
～～　略　～～
// 追加部分
import MyPage from "../views/MyPage.vue";

Vue.use(VueRouter);

const routes: Array<RouteConfig> = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
～～　略　～～
// 追加部分
  {
    path: "/myPage",
    name: "MyPage",
    component: MyPage,
  }
];

const router = new VueRouter({
  routes,
});

export default router;
```

## MyPageのチェック
これでページがリンクに追加されたので、再度Vueを起動してブラウザから``` localhost:8080/myPage ```にアクセスして
ボタンが2つあることを確かめます。

##### ブラウザ画面
![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481713/ad587e3a-9872-552c-e9ee-728013c792fe.png)


# storybookの作成
## storybookのインストール
storybookをインストールします。インストール対象は先ほど作成したプロジェクトですので上で使用していたプロンプトをそのまま使用します。

```
npx sb init
```

## storybookの実行
上のコマンドが終わるとstorybookのサンプルが実行できるため、確かめてみます。
storybookが起動できると自動的にブラウザが開かれてstorybookの画面が表示されます。
ここでは上で作成したコンポーネントとページは適用していないので、事前にあるサンプル以外は表示されていません。

```
yarn storybook
```

##### ブラウザ画面

![05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481713/bf6737b7-18e0-3dad-4e65-41b61882277c.png)


## コンポーネントのstoryの作成
storybookは描画したい部品やページごとにstoryという設定を記載する必要があります。
まずは、MyButtonのstoryを作成します。export defaultには描画するコンポーネントと画面に表示する内容の設定をします。
Templateに設定するのはprops, コンポーネント, html部分を設定します。export const XXXXは1画面に表示するコンポーネントのpropsの設定をします。

``` js
// src/stories/MyButton.stories.js

// 描画対象のインポート
import MyButton from '../components/MyButton';

export default {
  // 描画対象の設定
  component: MyButton,
  // 描画項目の設定
  title: 'Components/MyButton',
};

const Template = (args, { argTypes }) => ({
    // propsに下で与えた値を与える
    props: Object.keys(argTypes),
    // 描画対象の設定
    components: { MyButton },
    // 描画用のhtml
    template: `<my-button v-bind="$props" v-on="$props"/> `
})

// storybookに表示する画面1
export const Primary = Template.bind({})
// propsの設定
Primary.args = {
  type: "primary",
  text: 'プライマリーボタン'
}

// storybookに表示する画面2
export const Inversion = Template.bind({})
Inversion.args = {
  type: "inversion",
  text: 'インバーションボタン'
}

// storybookに表示する画面3
export const ErrorInversion = Template.bind({})
ErrorInversion.args = {
  type: "inversion",
  color: "error",
  text: '強調ボタン'
}
```

## storybookにvuetifyを適用する
storybookに対してvuetifyを使用するためには、.storybook/preview.jsにvuetifyのインストールを記載します。
さらに、必要であればここでcssやpropertyの追加等を行います。

``` js
// .storybook/preview.js
import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.css'
import colors from 'vuetify/es5/util/colors'

const vuetifyOptions = {}

// Vueに必要なものをインストールする
Vue.use(Vuetify, {})

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
}

// storybookが使用するパラメータと大本のhtmlテンプレート
export const decorators = [
  () => {
    return (
    { vuetify: new Vuetify(vuetifyOptions), template: '<v-app><story/></v-app>' }
  )}
]
```

## MyPageのstoryを作成する
ページを作成したのでページのstoryも作成します。今回、MyPageにはaxiosを使用した通信を行っているため、モック化しています。
モック化のためにはaxiosのインスタンスをインポートしてMockAdapterに食わせます。その後、urlとレスポンスを設定することでモック化できます。

``` js
// src/stories/MyPage.stories.js
import  MockAdapter from 'axios-mock-adapter';

import MyPage from '../views/MyPage.vue';

// axiosのインスタンスをインポートします。
import axiosInstance from '../utils/axios'
// axiosのモックアダプター作成
const mock = new MockAdapter(axiosInstance);
// axiosのモックにURLとレスポンスを設定する
mock.onGet('http://localhost/v1/apt/test').reply(200, {"sample": "OK"})

export default {
  component: MyPage,
  title: 'Page/MyPage',
};

const Template = (args, { argTypes }) => ({
    props: Object.keys(argTypes),
    components: { MyPage },
    template: `<my-page v-bind="$props"/> `
})


export const Page = Template.bind({})
Page.args = {}
```

## storybookの実行
ここまで設定が終わるとstorybookの画面にコンポーネントとページが追加されています。

```
yarn storybook
```

##### ブラウザ画面
ブラウザ画面を見るとCOMPONENTSにMyButton、PAGEにMyPageが追加されています。さらに、MyButtonには設定した3つの設定時の画面が描画できるようになっています。

![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481713/5cce366d-045e-47dd-6e35-2dc248312ad0.png)

# Chromaticへのアップロード
## GitHubリポジトリの作成
GitHubのリポジトリからChromaticへ連携されるため上で作成したプロジェクトをリポジトリに追加します。
Chromaticはコミットが2つ以上ないとダメなようなので適当なものをコミットしておきます。

## ChromaticとGitHubリポジトリの連携
[Chromaticのホームページ](https://www.chromatic.com/)を開いて会員登録を行います。会員登録後にChromaticのホームページに表示されるGitHubを選択してGitHubのリポジトリなど必要なことを入力します。

## プロジェクトのアップロード
必要なことを入力するとChromaticのページにChromaticのインストールコマンドとアップロードコマンドの2つが表示されるため作成したプロジェクトに対してコマンドを実行します。
コマンド実行後はChromaticのページの次ボタンが有効になっているはずなので次ボタンを押すとホスティングされたstorybookが表示されます。
※この後、Chromaticからチュートリアルのメールが届きました。

# 終わりに
storybookはWeb画面上でPropsを動的に変更できたり、いろいろ便利な機能があり触ってみると予想より良いものだと感じました。
さらに、ビジュアルテスト等に使用できるらしいのでより活用できればさらに便利になるのかなと思います。