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


はじめに

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

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

前置き

Storybook自体はずっと使っていたのですが、6系に関してはbetaの頃からrfcとreleaseノートを全て追っかけて追っかけはじめ、使っていました.
そしてついにrcが取れたので今回はその内容とiCAREさんでの活用方法、なぜそうするのかについて共有しようと思います.

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

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

Storybookとは

以下こちらのReadmeをIntroまで和訳したものです.

防弾性のあるUIコンポーネントを高速に構築


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



紹介

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

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

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

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

StoryShots

StoryShots + PuppeteerのAddonはかなりおすすめで、これだけでStorybookを導入する価値があります.
StoryShotsでは実装した全てのStoryに対してjestのjsdom上でDOMのsnapshotテストをしてくれます.
また、imageSnapshotを使う事により、全てのStoryに対して画像のsnapshotテストをしてくれます.

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

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

Storybook Args

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

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

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

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

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

PrimaryはReactのコンポーネントに見えますが、StoryFnというものです。
第1引数にpropsを取り、第2引数にStoryContextを受け取り、ReactElementを生成します.
これはReactのFunctionComponentと同じインターフェースです.
このStoryFnargsというプロパティにpropsのデフォルト値を与えると、それらがargsという項目の初期値になり、その後StorybookのUI上でそれらpropsを操作できるようになります.

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

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

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のStorybook 6環境

これはNetlifyでホストされているiCAREのStorybookのGifです.

@icare-jp/vue-props-story-fn-factoryでStoryFnを作成

Storybookの各StoryFnは実行結果にVue.jsのComponentOptionsを受け取る必要があります(Vue.js 3になるとReact likeなfunctional component記法とかのサポートも入るのかな🤔).
Vue.jsはoptionsにprops項目を設定しないとpropsを受け取れないので、第2引数のStoryContextの中に入っているargTypesを利用して、Vue.jsのComponentOptionsのpropsの項目を作成します.
これを毎回書くのは面倒くさいのでiCAREでは@icare-jp/vue-props-story-fn-factoryからexportされているPropsStoryFnFactoryを利用し、props項目を代わりに埋めさせて、ArgsをTypesafeに書けるようにしています.

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で各プルリクに最新コミットのStorybookのPreviewを付ける

これはiCAREで使っているnetlify.tomlです.

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

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

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

DOM Snapshots

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

Image Snapshots

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

最後に

iCAREでは最新の技術の恩恵を素早く、最大限に受けて、開発しています.