Storybook6をVue.jsのプロジェクトに導入し、新機能であるStorybook Argsを活用してVue.jsのDXを最高にした話

# はじめに

最近[iCARE](https://www.icare.jpn.com/dev)さんの所で`Vue.js`を一緒にやらせていただいているのですが、フロントの技術スタックがかなりモダンであり、開発体験が良く、ノウハウをどんどん公開して良いと言っていただけたので、その内容を共有するシリーズです.

今回は2日前にリリースされた`Storybook6`を`Vue.js + TypeScript`のプロジェクトに導入した話をします(執筆時点).

## 前置き

[Storybook](https://storybook.js.org)自体はずっと使っていたのですが、6系に関してはbetaの頃からrfcとreleaseノートを全て追っかけて追っかけはじめ、使っていました.
そしてついに`rc`が取れたので今回はその内容と[iCARE](https://www.icare.jpn.com/dev)さんでの活用方法、なぜそうするのかについて共有しようと思います.

## Vue.jsのプロジェクトにStorybookを導入した経緯

以前からVue.js用のできが良くて活発なコミュニティを持つStorybookみたいな物を待っていました.
候補としては[vue-styleguidist](https://vue-styleguidist.github.io)があります.
しかし、[vue-styleguidist](https://vue-styleguidist.github.io)でComposition APIでコードを書いたところコンパイルエラーが発生し、それをきっかけににIssueとvue-styleguidistのparserのコードを読んだところ、標準的なparse方法を採用しておらず、また、コミュニティもあまり活発ではない状態でした.
Storybookは活発であり、コミュニティも大きく、関連ツールも多く、parserも標準的なものを採用しています.
結果StorybookをVue.jsのプロジェクトに組み込むことにしました.

## [Storybook](https://storybook.js.org)とは

以下[こちらのReadme](https://github.com/storybookjs/storybook/blob/next/README.md)をIntroまで和訳したものです.

> <p align="center">
  <a href="https://storybook.js.org/">
    <img src="https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png" alt="Storybook" width="400" />
  </a>
</p>

> <p align="center">防弾性のあるUIコンポーネントを高速に構築</p>

> <br/>

> Storybookは、UIコンポーネントの開発環境です。
コンポーネントライブラリを閲覧したり、各コンポーネントの異なる状態を表示したり、インタラクティブにコンポーネントを開発したりテストしたりすることができます。

> <center>
  <img src="https://raw.githubusercontent.com/storybookjs/storybook/master/media/storybook-intro.gif" width="100%" />
</center>

> ## 紹介

> Storybook はアプリの外で動作します。これにより、UI コンポーネントを分離して開発することができ、コンポーネントの再利用、テスト性、開発スピードを向上させることができます。アプリケーション固有の依存関係を気にすることなく、迅速にビルドすることができます。

> ここでは、Storybook がどのように動作するかを確認するために参照できる、いくつかの特徴的な例を紹介します。<https://storybook.js.org/examples/>

> Storybookには、コンポーネント設計、ドキュメント作成、テスト、インタラクティブ性などのための多くの[アドオン](https://storybook.js.org/addons/introduction/)が付属しています。StorybookのAPIを利用することで、様々な設定や拡張が可能になります。モバイル向けのReact Native開発にも対応するように拡張されています。

つまり、[Storybook](https://storybook.js.org)は、UIコンポーネントの開発環境です。
ただの開発環境でしたらチーム全員で使う理由が弱くなり陳腐化しがちですが、[Storybook](https://storybook.js.org)に色んなAddonを追加して行くことにより、Storybook上でUIコンポーネントを開発する理由を多くできます.

## StoryShots

[StoryShots + PuppeteerのAddon](https://www.npmjs.com/package/@storybook/addon-storyshots-puppeteer)はかなりおすすめで、これだけでStorybookを導入する価値があります.
StoryShotsでは実装した全てのStoryに対してjestのjsdom上でDOMのsnapshotテストをしてくれます.
また、imageSnapshotを使う事により、全てのStoryに対して画像のsnapshotテストをしてくれます.

これを導入することにより、domの構造と画像の構造を保存しておけるようになり、意図しない差分の検出ができるようになり、ライブラリのバージョンアップやリファクタリング等により起こる不具合を見逃す頻度も下がります.

色んなpropsを与えた際のコンポーネントの初期レンダリングの状態がどうなるかの単調なテストも多くの場合はunit-testで書きますが、それらは全てdom-snapshotsで代替えされます.

## [Storybook Args](https://medium.com/storybookjs/introducing-storybook-args-2dadcdb777cc)

Storybookのstoriesの書き方は色々あるのですが、その中でも指定したインターフェースを持つes6 moduleを作成することだけを求める代わりに[Storybook](https://storybook.js.org)のAPIへの依存を低くし、ロックインを弱められるStoryの書き方があり、それが[Component Story Format (CSF)](https://storybook.js.org/docs/react/api/csf)です.

また、[Storybook 6](https://storybook.js.org)ではpropsの値をこの様に注入できるようになっています.
https://storybook.js.org/docs/react/api/csf#args-story-inputs

以下は[Storybook](https://storybook.js.org)公式からの引用のReactを短縮した例ですが、雰囲気を掴んで頂くために出します。(執筆時点で公式にVue.jsの例が無い)

```typescript
export const PrimaryButton = (args) => <Button {...args} />

PrimaryButton.args = {
  primary: true,
  label: 'Button',
}
```

PrimaryはReactのコンポーネントに見えますが、[StoryFn](https://github.com/storybookjs/storybook/blob/3fe7d96fd8d289f40bd8a38ec836ed95e6afe865/lib/addons/src/types.ts#L96)というものです。
第1引数にpropsを取り、第2引数に[StoryContext](https://github.com/storybookjs/storybook/blob/3fe7d96fd8d289f40bd8a38ec836ed95e6afe865/lib/addons/src/types.ts#L55)を受け取り、ReactElementを生成します.
これはReactの[FunctionComponent](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L546)と同じインターフェースです.
この[StoryFn](https://github.com/storybookjs/storybook/blob/3fe7d96fd8d289f40bd8a38ec836ed95e6afe865/lib/addons/src/types.ts#L96)の[args](https://storybook.js.org/docs/vue/writing-stories/args)というプロパティにpropsのデフォルト値を与えると、それらが[args](https://storybook.js.org/docs/vue/writing-stories/args)という項目の初期値になり、その後StorybookのUI上でそれらpropsを操作できるようになります.

執筆時点のTypeScriptの型は以下の通りです.

![carbon (26).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141711/aea1a0bf-dc15-5fd7-2307-c2bd1bdcf9b7.png)

Vue.jsの場合は以下のような書き方になります.

```typescript
import MyButton from './my-button.vue'

const PrimaryButton = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { MyButton },
  template: '<my-button v-bind="$props" />',
})
PrimaryButton.args = {
  primary: true,
  label: 'Button',
}
```

この書式で書かれたstoriesはStorybook以外でも簡単に利用できるようになっています.
ですので、jest上での簡単な形式的なテストとかも実現可能ですね.

## [iCARE](https://www.icare.jpn.com/dev)のStorybook 6環境

これはNetlifyでホストされている[iCARE](https://www.icare.jpn.com/dev)のStorybookのGifです.

[![Image from Gyazo](https://i.gyazo.com/5b2c25bf3d67aff2d4706122febf56c2.gif)](https://gyazo.com/5b2c25bf3d67aff2d4706122febf56c2)

### [@icare-jp/vue-props-story-fn-factory](https://www.npmjs.com/package/@icare-jp/vue-props-story-fn-factory)でStoryFnを作成

Storybookの各[StoryFn](https://github.com/storybookjs/storybook/blob/3fe7d96fd8d289f40bd8a38ec836ed95e6afe865/lib/addons/src/types.ts#L96)は実行結果にVue.jsの[ComponentOptions](https://github.com/vuejs/vue/blob/46ae9525964ea475dc927031e165c2b209396ac8/types/options.d.ts#L67)を受け取る必要があります(Vue.js 3になるとReact likeなfunctional component記法とかのサポートも入るのかな🤔).
Vue.jsはoptionsに[props項目](https://jp.vuejs.org/v2/api/#props)を設定しないとpropsを受け取れないので、第2引数の[StoryContext](https://github.com/storybookjs/storybook/blob/3fe7d96fd8d289f40bd8a38ec836ed95e6afe865/lib/addons/src/types.ts#L55)の中に入っているargTypesを利用して、Vue.jsの[ComponentOptions](https://github.com/vuejs/vue/blob/46ae9525964ea475dc927031e165c2b209396ac8/types/options.d.ts#L67)のpropsの項目を作成します.
これを毎回書くのは面倒くさいので[iCARE](https://www.icare.jpn.com/dev)では[@icare-jp/vue-props-story-fn-factory](https://www.npmjs.com/package/@icare-jp/vue-props-story-fn-factory)からexportされている[PropsStoryFnFactory](https://github.com/icare-jp-oss/vue-props-story-fn-factory/blob/55bd42aac219346e3808fdfb667df52a8ef03e2f/src/index.ts#L5)を利用し、[props項目](https://jp.vuejs.org/v2/api/#props)を代わりに埋めさせて、[Args](https://storybook.js.org/docs/vue/writing-stories/args)をTypesafeに書けるようにしています.

```typescript
import MyButton, { MyButtonProps } from './my-button.vue'
import { PropsStoryFnFactory } from '@icare-jp/vue-props-story-fn-factory'

const PrimaryButton = PropsStoryFnFactory<MyButtonProps>(() => ({
  components: { MyButton },
  template: '<my-button v-bind="$props" />',
})
PrimaryButton.args = {
  primary: true,
  label: 'Button',
}
```

### [NetlifyのDeploy Previews](https://blog.gaji.jp/2020/03/25/3048)で各プルリクに最新コミットのStorybookのPreviewを付ける

これは[iCARE](https://www.icare.jpn.com/dev)で使っている[netlify.toml](https://docs.netlify.com/configure-builds/file-based-configuration/)です.

```netlify.toml
[build]
  publish = "storybook-static/"
  command = "yarn build-storybook"
```

Netlify連携し、GUI上でいくつかの操作をし、このファイルをプロジェクトに入れるだけで、pullしなくても各プルリクでStorybookのPreviewが見れるようになります.

これはコンポーネントに関するレビュー(確認・指摘)の体験を向上させます.

### [DOM Snapshots](https://www.npmjs.com/package/@storybook/addon-storyshots)

これは上記で説明したStoryShotsを利用しています.
その際にコンパイル結果を統一するために、Vue.jsとTypeScriptの変換は全てbabelに任せています.
TimeZone等を統一するためにテストはDocker内で行わせています.
テストの実行速度は許容範囲内です.

### [Image Snapshots](https://www.npmjs.com/package/@storybook/addon-storyshots-puppeteer)

これも上記で説明したStoryShotsを利用しています.
FontやTimeZone等を統一するためにテストはDocker内で行わせています.
StoryShotsは各storyでjestのtest項目をつくり、１つ１つアクセスし画像を撮っている為、かなり遅いです.
American Expressが作成しているjest-image-snapshot + puppeteerで自分で書いたノンブロッキングなテストだと全部のスクショとdiff確認合わせて8秒なところ、1分以上かかります.
このテストの時間は線形に伸びていき、いずれ許容できなくなる前に、[reg-suit](https://github.com/reg-viz/reg-suit)への移行を検討しています.

## 最後に

[iCARE](https://www.icare.jpn.com/dev)では最新の技術の恩恵を素早く、最大限に受けて、開発しています.