Vuetify2をStorybookで動かしてみた (前編)

こんにちは!フリージアの東山です。
前回の続きとして、今回はVuetify2Storybookで動かしてみようと思います。

今回のゴールは以下になります。

  • Storybookの導入 (※ 前編)
  • VueコンポーネントをStorybookに登録 (※ 前編)
  • Vuetify2のコンポーネントをStorybookに登録 (※ 後編)

本記事にて、Vuetify2のコンポーネントをStorybookに登録するところまで書ければと考えていたのですが、記事が長くなってしまうため、前編、後編に分けて投稿します。

Storybookの導入

Storybookとは?

Storybookとは、一言で言うとUIコンポーネントカタログです。 UIコンポーネントを一覧化し、各UIコンポーネントの仕様・挙動を確認できます。
Storybookを導入することで、UIコンポーネントに関してチームで共通の認識を持つことができるため、デザインの統一や開発・運用の効率化といったメリットを享受できます。
今回使用するVue.js以外にも、フロントエンド開発の主要なフレームワークであるReact、Angularなど幅広く対応しています。

Storybookのインストール

前回作成したリポジトリを拡張する形で進めていきます。 まずはリポジトリをクローンしてください。

$ git clone https://github.com/freegian/nuxtjs-template.git 
$ cd nuxtjs-template
$ git checkout feature/modify-sass-variables

$ yarn

次に、以下のコマンドを実行し、Storybook関連のパッケージを追加してください。

$ yarn add -D @storybook/vue @types/storybook__vue @babel/preset-env

@babel/preset-envは環境に基づいてBabelプラグインを自動で決定するパッケージです。 @storybook/vueの依存パッケージとなっているため、合わせて追加します。

Storybookの設定

Storybookが起動することを目標に設定します。
「.storybook」フォルダをプロジェクトルートに作成し、その配下に設定ファイルを配置します。

/
├ .storybook
│  ├ .babelrc       : Babelの設定ファイル
│  └ config.js      : Storybookの設定ファイル
├ stories           : ストーリーの格納フォルダ
// 省略
└ yarn.lock

ストーリーファイルを配置する「stories」フォルダもこのタイミングで作成してください。

各ファイルの内容は以下になります。

// .storybook/.babelrc

{
  "presets": ["@babel/preset-env"]
}
// .storybook/config.js

import { configure, addDecorator } from '@storybook/vue'

addDecorator(() => ({
  template: `
<div><story/><div>
`
}))

const req = require.context('../stories', true, /\.stories\.ts$/)
const loadStories = () => {
  req.keys().forEach(req)
}

configure(loadStories, module)

「.storybook/.babelrc」ではプリセットとして先程追加した@babel/preset-envを指定します。これによりBabelがECMAScriptのバージョンを下位にトランスパイルします。

「.storybook/config.js」では主に以下の2つの設定しています。

  • デコレーターの設定
  • ストーリーファイルの読み込み設定

前半部分では、addDecorator()でVueのtemplateを設定しています。ストーリーとして描画するコンポーネントがレンダリングされるstoryタグをdivタグで囲んでいます。
後半部分では、ストーリーファイルの一覧をloadStories変数に格納し、configure()の引数として設定しています。 configure()の引数として設定されたファイルが、Storybookの読み込み対象ファイルとなります。

以上で、Storybookの起動に最低限必要な設定は完了です。
なお、ここまで設定方法については公式ページでも説明されていますので、必要に応じてご参照ください。

Storybookの起動

Storybookの起動には「start-storybook」コマンドを使います。
「yarn start-storybook ~」と毎回コマンドを入力しても問題ありませんが、設定フォルダの場所やポートの指定など、オプションとして指定する値が多いため、起動コマンドをシェルスクリプトのエイリアスとしてpackage.jsonに追加します。

// package.json

  "scripts": {
    ...,
    "storybook": "start-storybook -c .storybook -p 9000"
  },

以下のコマンドを実行し、Storybookが起動することを確認してください。

$ yarn storybook

storybook 1

VueコンポーネントをStorybookに登録

Vueコンポーネントの作成

前章では、Storybookの初期設定を行い、Storybookが起動することを確認しました。
本章では、StorybookにVueコンポーネントを追加し、Vueコンポーネント毎の仕様や挙動を実際に確認します。

確認用のVueコンポーネントとして、TheParagraph.vueを「./src/components」フォルダ配下に作成します。

/
├ src
│  └ components
│     └ TheParagraph.vue 
├ stories
// 省略
└ yarn.lock

TheParagraph.vueは親コンポーネントからPropで渡された文字列を段落として描画するVueコンポーネントです。
TheParagraph.vueの内容は以下になります。

// TheParagraph.vue

<template>
  <p>{{ text }}</p>
</template>

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

@Component
export default class TheParagraph extends Vue {
  @Prop({ type: String, required: true })
  text: string
}
</script>

<style lang="scss" scoped>
p {
  font-size: 16px;
  font-weight: bold;
}
</style>

Vueコンポーネントの読み込み設定

VueコンポーネントをStorybookで読み込めるよう、以下の3つの設定を追加します。

  • Storybook addonパッケージの追加
  • Storybook addonパッケージの読み込み
  • Webpack設定のカスタマイズ

Storybook addonパッケージの追加

先に事実を述べますと、Storybook addonパッケージがない状態でもVueコンポーネントを読み込むことは可能です。
しかし、本記事序文で述べた「開発・運用の効率化」を図るため、Storybook addonパッケージの追加を推奨します。

以下のコマンドを実行し、Storybook addonパッケージを追加してください。

yarn add -D @storybook/addon-knobs @storybook/addon-viewport storybook-addon-vue-info @types/storybook__addon-knobs

各パッケージを導入することで、以下の機能が追加されます。

上記のパッケージ以外にも様々なStorybook addonパッケージが用意されていますので、興味がある方は「storybook addon」などでググってみてください。

Storybook addonパッケージの読み込み

Storybook addonパッケージを読み込むには、Storybook設定ディレクトリ配下に「addons.js」ファイルを作成する必要があります。

/
├ .storybook
│  └ addons.js
// 省略
└ yarn.lock

「addons.js」に以下の内容を記述ください。

// addons.js

import 'storybook-addon-vue-info/lib/register'

import '@storybook/addon-viewport/register'
import '@storybook/addon-knobs/register'

各Storybook addonパッケージで用意されているregister関数をインポートすることで、Storybookにパッケージが提供する機能が登録されます。
インポートするStorybook addonパッケージによっては、インポートする順序が決められているものもありますので、上記以外のパッケージを追加する際にはご注意ください。

@storybook/addon-knobs、storybook-addon-vue-infoに関しては、上記の設定のほか、addDecorator()での追加設定が必要です。
「.storybook/config.js」に以下の+行を追記してください (※ +は削除ください)。

// .storybook/config.js

import { configure, addDecorator } from '@storybook/vue'
+ import { withInfo } from 'storybook-addon-vue-info'
+ import { withKnobs } from '@storybook/addon-knobs'

+ addDecorator(withInfo)
+ addDecorator(withKnobs)
...

以上で、Storybook addonパッケージの読み込み設定は完了です。

Webpack設定のカスタマイズ

Vueコンポーネントを読み込むためには、Storybookに内包されているWebpackの設定をカスタマイズする必要があります。 Vueコンポーネントの拡張状況によって設定が異なりますので、ご注意ください。

本記事で作成したVueコンポーネントでは、次の拡張をしています。

  • Typescript
  • SASS(SCSS)

それぞれWebpack Loaderの設定を追加することで、正常にVueコンポーネントが読み込まれるようにします。
Webpackについて詳細を知りたい方は、Webpack Documentationをご参照ください。 本記事では、Webpackに関する詳細な説明は割愛します。

これまで同様、「.storybook」フォルダに設定ファイルを追加し、設定内容を記述ください。

/
├ .storybook
│  └ webpack.config.js
// 省略
└ yarn.lock
// .storybook/webpack.config.js

const path = require('path')

module.exports = ({ config }) => {
  config.module.rules.push({
    test: /\.ts$/,
    exclude: /node_modules/,
    use: [
      {
        loader: 'ts-loader',
        options: {
          appendTsSuffixTo: [/\.vue$/],
          transpileOnly: true
        }
      }
    ]
  })
  config.module.rules.push({
    test: /\.s(a|c)ss$/,
    use: ['style-loader', 'css-loader', 'sass-loader']
  })
  config.module.rules.push({
    test: /\.vue$/,
    loader: 'storybook-addon-vue-info/loader',
    enforce: 'post'
  })
  config.resolve.alias = {
    vue: 'vue/dist/vue.esm.js',
    '@': path.resolve(__dirname, '../src'),
    '~': path.resolve(__dirname, '../src'),
    '@components': path.resolve(__dirname, '../src/components')
  }

  return config
}

以上で、Webpack設定のカスタマイズは完了です。

VueコンポーネントをStorybookに登録

TheParagraphコンポーネントのストーリーファイルを作成し、Storybookに登録します。
初期設定において、読み込み対象のストーリーファイルを「./stories/*.stories.ts 」としましたので、その規則に従ってストーリーファイルを作成します。

/
├ stories
│  └ TheParagraph.stories.ts
// 省略
└ yarn.lock

次に「./stories/TheParagraph.stories.ts」を以下のように編集します。

// ./stories/TheParagraph.stories.ts

import { storiesOf } from '@storybook/vue'
// @ts-ignore
import TheParagraph from '@components/TheParagraph'

storiesOf('Components', module).add(
  'TheParagraph',
  () => ({
    components: { TheParagraph },
    template: `
<the-paragraph text="paragraph"></the-paragraph>
`
  }),
  {
    info: {}
  }
)

「./stories/TheParagraph.stories.ts」では、import文を除けば、ストーリーを追加する処理のみを行っています。

storiesOf('A', module).add('B', StoryFunction)

といった構文で記述されており、「Aという種別にBという名前のストーリーを追加する」処理を行っています。 ストーリーの具体的な内容はStoryFunction部分で定義されており、TheParagraphストーリーでは、TheParagraphコンポーネントを描画しています。

「yarn storybook」を実行し、TheParagraphコンポーネントがStorybookに登録されていることを確認してください。 storybook 2

また、メインペインのタブを「Info(Vue)」から「Knobs」に切り替えると、Propであるtextプロパティを編集可能なフォームが表示されます。 フォームの内容を適当な文字列に変更し、動的に段落として描画されている文字列が変更されることを確認してください。 storybook 3

以上の動作が確認できれば、Vueコンポーネントの登録は完了です。
ここまでのソースコードはfreegian/nuxtjs-templateにプッシュしています。 正常に動作しないなどありましたら、ご確認ください。

最後に

今回はVueコンポーネントをStorybookに登録し、動作を検証ししました。

簡単なコンポーネントの登録に留まりましたが、実運用では様々なコンポーネントを登録し、チームで共有することになります。
その際、Storybookに登録するコンポーネントは再利用可能なものであることが望ましいです。 別の言い方をすると、「再利用可能なコンポーネント設計」をし、それらを登録することが望ましいです。

「再利用可能なコンポーネント設計」手法として、弊社ではAtomic Designの考え方を取り入れています。 「デザインの統一性」や「行動を阻害しない操作性」といった関心事ベースでコンポーネントを分割するこの考え方は、フロントエンド開発において高い保守性をもたらします。

近いうちに、このAtomic DesignをNuxt.jsでどのように取り入れているかについての記事を投稿できれば考えています。
次回は、Vuetify2をStorybookで動かしてみた (後編) を投稿する予定です。

本記事を見て、ご意見やご指摘等ございましたら、「[email protected]」まで是非ご連絡ください。