.../articles/

NuxtプロジェクトへのStorybookの導入

StorybookのNuxtを使っているプロジェクトへの導入と所感をまとめました。 * 2020/04/14 @storybook/vue 5.3.0以降のアップデートを追記しました。

最近フロントエンドのチーム開発環境の整備の一環として、Storybookの導入を行いました。

Storybook 5.3の公開により、設定方法が変更されましたので、追記・修正いたしました。
参考: https://medium.com/storybookjs/declarative-storybook-configuration-49912f77b78

導入背景

背景としては、現在弊社チームとしてはデザイナー、フロントエンドエンジニアの他に、インターン的にマークアップを手伝ってくれている人がいます。

デザイナー、エンジニア間では過去に何度もAtomic Designのコンポーネントの粒度であったり、Vue(Nuxt)を使う上でのHTML, CSS(SCSS)コーディング規約などを話し合っていて、「この議論N回目、3週間ぶり、、、」みたいなのがあります。

それにより色々と双方の認識が取れてきている部分もあるのですが、さてマークアップをインターン含めてやるかとなった時に、

  • VueファイルのComponent粒度をどう伝えるか
  • 実際のデータがない状態でも出来るだけHTMLの中身をベタがきせず、Propsで受け渡しながらComponentに切り出した開発ができるか
  • デザイナーへのマークアップしたもののレビュー
    などという問題にぶち当たります。

VueファイルのComponent粒度をどう伝えるか

こちらの問題は、デザインファイルの時点でパーツ、ページごとにコンポーネント名をつけてしまえば、解決という部分もあるのですが、そこまで用意できないプロジェクトもあったりという(言い訳)部分があり、ある程度マークアップする人に委ねられがちというところもあります。

そこでStorybookを作ることで、他のプロジェクトを参考にしながらマークアップすることで、温度感を合わせていくということができました。

この辺の開発フローは改善の余地ありということで、チームでも議論を重ねているポイントです。

実際のデータがない状態でも出来るだけHTMLの中身をベタがきせず、Propsで受け渡しながらComponentに切り出した開発ができるか

マークアップする際にNuxtを立ち上げず、StorybookのみでVueファイルを確認しながら開発が可能というのは、APIへのアクセス部分などを気にせずマークアップが可能なので、JSがあまり分からない人でもマークアップが可能というメリットがありました。
あと、動的に変わる部分もPropsで受け渡せるように書いてもらうことで、後々書き換える手間が減ったりというメリットも感じています。
Propsの型はある程度適当でも後から変えるくらいの工数は見逃せるかなという印象です。

デザイナーへのマークアップしたもののレビュー

Pull Requestを送った際に、CIでstorybookをbuildしているので、プルリクを確認する際のデザインが想定通りかという部分をわざわざローカルでビルドして確認する手間が省けます。
HTML, CSSのコーディング内容はコードを見ただけではわかりづらいという部分もあるので、パッと画面で見れるのは良いなと思っています。
個人的には、ここは一番大きい気がしているのですが、デザイナーにはまだ100%納得をしてもらえていません笑

Nuxtへの導入

1. packageの追加

$ yarn add -D @storybook/vue \
  @storybook/addons \
  @storybook/addon-viewport \
  @storybook/addon-notes \
  @storybook/addon-links \
  @storybook/addon-knobs \
  @storybook/addon-actions \
  storybook-addon-vue-info

2. .storybookディレクトリの作成とファイルの設置

次にstorybookの設定ファイルを設置します。
.storybookを作成し、treeで表示しているように、下記のようにファイルを設置します。


@storybook/vue 5.3.0以降の場合

$ tree .storybook

.storybook
├── main.js
├── preview.js
└── webpack.config.js

main.js

main.jsにはimportするpackageの記載します。

module.exports = {
  stories: ['../src/components/*.stories.js'],
  addons: [
    '@storybook/addon-actions',
    '@storybook/addon-links',
    '@storybook/addon-viewport',
    '@storybook/addon-notes',
    '@storybook/addon-knobs',
    'storybook-addon-vue-info/lib/register',
  ],
};

preview.js

preview.jsにはstorybookのファイル読み込み設定を記載します。
KnobsとInfoの設定もここで行なっています。

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

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// automatically import all files ending in *.stories.js
const req = require.context('../src/components', true, /.stories.js$/)
function loadStories() {
  req.keys().forEach(filename => req(filename))
}

configure(loadStories, module)
addDecorator(withKnobs)
addDecorator(withInfo)

webpack.config.js

storybook用のwebpackの設定を記載します。
正直ここは記載内容に少し自信ないところ。
tsを使っている場合は、ここでts-loaderの設定も行います。

const path = require('path')
const rootPath = path.resolve(__dirname, '../src/')

module.exports = async ({ config, mode }) => {
  mode = "development"

  config.module.rules.push({
    test: /\.(otf|eot|svg|ttf|woff|woff2)(\?.+)?$/,
    loader: 'url-loader',
  });

  config.module.rules.push({
    test: /\.css/,
    use: [
      'style-loader',
      { loader: 'css-loader', options: { url: false } },
    ],
  });

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

  config.module.rules.push({
    test: /\.vue$/,
    loader: 'storybook-addon-vue-info/loader',
    enforce: 'post'
  });

  config.module.rules.push({
    test: /\.scss$/,
    use: [
      'style-loader',
      'css-loader',
      {
        loader: 'sass-loader',
      },
      {
        loader: 'sass-resources-loader',
        options: {
          resources: [
            path.resolve(__dirname, './../src/assets/scss/_variables.scss'),
            path.resolve(__dirname, './../src/assets/scss/common.scss'),
          ],
        }
      }
    ]
  });

  config.resolve.extensions = ['.js', '.vue', '.json']
  config.resolve.alias['~'] = rootPath
  config.resolve.alias['@'] = rootPath

  return config;
}

@storybook/vue 5.3.0未満の場合

$ tree .storybook

.storybook
├── addons.js
├── config.js
└── webpack.config.js

addons.js

addons.jsにはimportするpackageの記載します。

import '@storybook/addon-actions/register'
import '@storybook/addon-links/register'
import '@storybook/addon-notes/register'
import '@storybook/addon-viewport/register'
import '@storybook/addon-knobs/register'
import 'storybook-addon-vue-info/lib/register'

config.js

config.jsにはstorybookのファイル読み込み設定を記載します。
KnobsとInfoの設定もここで行なっています。

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

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// Import all files ending in *.stories.js
const req = require.context('../app/components', true, /.stories.js$/)
function loadStories() {
  req.keys().forEach(filename => req(filename))
}

configure(loadStories, module)
addDecorator(withKnobs)
addDecorator(withInfo)

webpack.config.js

storybook用のwebpackの設定を記載します。
正直ここは記載内容に少し自信ないところ。
tsを使っている場合は、ここでts-loaderの設定も行います。

const path = require('path')
const rootPath = path.resolve(__dirname, '../app/')

module.exports = async ({ config, mode }) => {
  mode = "development"

  config.module.rules.push({
    test: /\.(otf|eot|svg|ttf|woff|woff2)(\?.+)?$/,
    loader: 'url-loader',
  });

  config.module.rules.push({
    test: /\.css/,
    use: [
      'style-loader',
      { loader: 'css-loader', options: { url: false } },
    ],
  });

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

  config.module.rules.push({
    test: /\.vue$/,
    loader: 'storybook-addon-vue-info/loader',
    enforce: 'post'
  });

  config.module.rules.push({
    test: /\.scss$/,
    use: [
      'style-loader',
      'css-loader',
      {
        loader: 'sass-loader',
      },
      {
        loader: 'sass-resources-loader',
        options: {
          resources: [
            path.resolve(__dirname, './../app/assets/scss/_variables.scss'),
            path.resolve(__dirname, './../app/assets/scss/common.scss'),
          ],
        }
      }
    ]
  });

  config.resolve.extensions = ['.js', '.vue', '.json']
  config.resolve.alias['~'] = rootPath
  config.resolve.alias['@'] = rootPath

  return config;
}

3. vueファイル、story.jsファイルの作成

次はやっと、vueファイル、storybook用のファイルを作成します。
弊社では、componentのフォルダに、.vue, story.js, .spec.jsを入れてます。
同じフォルダに入れることで、放置される危険性?を少しでも減らせればという思いです。

├── atoms
│   ├── buttons
│   │   ├── CommonButton.spec.js
│   │   ├── CommonButton.story.js
│   │   └── CommonButton.vue
│   ├── forms
│   │   ├── InputForm.story.js
│   │   ├── InputForm.vue
~
~
├── index.stories.js

index.stories.js

ここでimportするstorybookのファイルを記載します。
Componentの数が多くなればatom.stories.jsとかって分けたら良いのだろうか?

/** atoms */
import '@/components/atoms/forms/InputForm.story'

今回は例として、InputFormの部分を見てみます。

InputForm.vue

<template>
  <input
    v-model="$attrs.value"
    class="input"
    :type="type"
    @input="$emit('input', $event.target.value)"
  >
</template>

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

export default Vue.extend({
  name: 'AtomsFormsInputForm',
  props: {
    type: {
      type: String,
      default: 'text',
    },
  },
})
</script>

<style lang="scss" scoped>
  .input {
    width: 100%;
    height: 50px;
    background-color: #eceff1;
    border: solid 1px #cfd8dc;
    border-radius: 5px;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
  }
</style>

InputForm.story.js

addon-knobsの使い方が分からず、dataに書いたりして微妙につまりましたが、propsに記載してやればいいようです。

実際のファイルは下記です。infoとかを表示できるのは嬉しいですね!

import { storiesOf } from '@storybook/vue'
import { text } from '@storybook/addon-knobs/vue'
import InputForm from './InputForm.vue'

storiesOf('Atoms/forms', module)
  .add(
    'InputForm',
    () => ({
      components: { InputForm },
      template: `<InputForm
        :type="type"
      />`,
      props: {
        type: {
          type: String,
          default: text('type', 'password'),
        },
      },
      description: {
        InputForm: {
          props: {
            type: 'typeは text, password, date などは利用可能. 詳細はNotesに記載.',
          },
          events: {
            input: '入力内容を親コンポーネントに受け渡す',
          },
        },
      },
    }),
    {
      info: true,
      notes: `
        # Input Form

        ## Props
        * type
          * string
            * typeは text, password, date などは利用可能
            * file, image は別コンポーネントで管理する
      `,
    })

4. package.jsonへの記載とビルド

package.jsonのscriptsに下記のように追記し、$yarn storybookで実行するとstorybookが起動します。あとは、localhost:6006(ポートは自分で設定した番号)の画面をみながら開発を進めればOKです!

  "scripts": {
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook",
  },

実際にビルドされた画面はこんな感じで動かせます。

まだまだガッツリ活用できている段階ではないですが、模索しながらフロントエンド開発の効率を高めていければと思います!

参考

.../articles/

Articles

記事

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

monorepo管理しているNext.jsをAmplifyにデプロイしようとした際にいくつか躓く内容があったのでまとめておきます。

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

コロナ禍の影響により、リモートワークの導入をおこなっている制作会社も多く、実際に弊社でも導入しています。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトのデザインは、「この形式」という決まりはありません。ECサイトで売り上げを上げるなら、しっかりとしたコンセプトと、コンセプトを決定するまでのリサーチが必要です。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

新型コロナウイルス感染拡大の影響で、リモートワークが主流になり、弊社でも週のほとんどは各自宅で作業をしています。

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaを利用するWebデザイナーの助けになれると嬉しいです。使い方は本記事をご覧ください。

ECの構築方法、おすすめのECサービス。

ECの構築方法、おすすめのECサービス。

ファッションや家電、スーパーの買い物でさえもECサイトを利用することが当たり前になりました。加えて新型コロナウイルスの影響もあり、弊社にも「どんなプラットフォームを利用したら良いか」「どれくらいコストがかかるのか」などECに関するさまざまなご相談を頂きます。

FastAPIのスキーマクラスをOpenAPIから生成する方法

FastAPIのスキーマクラスをOpenAPIから生成する方法

PythonでAPIを構築する要件があり、フレームワークに比較的モダンなFastAPIを採用しました。FastAPIはバックエンドの開発を行えば自動でOepnApi定義を生成する機能が備わっていますが、今回はこれを使わず、事前に用意したOepnApi定義からFastAPIで利用するスキーマクラスを生成する方法を紹介します。

Laravel 日本一解りやすい全文検索のマイグレーション記載方法解説

Laravel 日本一解りやすい全文検索のマイグレーション記載方法解説

Laravel + MySQLで全文検索を実装する

とあるPythonのソースで sys.path.append としたく無かった話

とあるPythonのソースで sys.path.append としたく無かった話

とあるプロジェクトのとあるソースコードのレビューをしてた時、「ソースコードの参照がうまくいってなかったので修正しました」とレビュー依頼がきました。 ディレクトリ構造 ``` module L __init__.py L main.py L tests L __init__.py L test_main.py ``` ソースコード ``` python tests/test_main.py sys.path.append(os.path.abspath("..")) from main import fuga ``` 今まで案件でPythonに触れる機会も結構ありましたが、なんとなく使ってきた部分も多く、この書き方が良いのか悪いのか判別できなかったので、改めてPythonのモジュールのインポートに関して調べてみたのでブログにしました。普段PHPを書いている事が多くPythonに関して何も分からないので初心者向けの内容になっていると思います。

GiFT1号目新卒デザイナーの2021年振り返り

GiFT1号目新卒デザイナーの2021年振り返り

いつの間に、年末ですね。入社してもう、9ヶ月も立っていたようです。2021年の振り返りを記事にしました。

すべての記事

お問い合わせ