株式会社HAMWORKS
🖌

Nuxt 3 + Vuetify 3 + Storybook 開発環境構築

2024/03/12に公開

本業でVueのアップデート周りを行っており、構成としてNuxt.js 3とVuetify 3を組み合わせて開発しています。
アップデートに伴って、いままでコンポーネントのドキュメントが整備されていなかったのでStorybookを利用して開発することにしました。

Vuetifyを利用するからStroybookは不要論はあるかもしれません。
しかし、コンポーネントを組み合わせたケースではStorybookがあると値の可視化ができるため、アンドキュメント部分の補完になります。

そこで Nuxt3 + Vuetify 3 + Storybook の開発環境を構築する方法を紹介します。
この構成での情報があまりなかったので覚書として記事にしました。

対象読者

  • Nuxt.js 3を使ってStorybookを利用したい人
  • Vuetify 3と組み合わせたい人

開発環境

  • Node.js 20.11.0
  • Nuxt.js 3以上
  • Vuetify 3以上
  • storybook 8系

検証リポジトリ

https://github.com/redamoon/nuxt3-storybook-vuetify-sample-code

Install Nuxt3

まずはじめに、Nuxtをインストールしていきます。
Nuxtをインストールしたいディレクトリで nuxi init でNuxtの雛形をインストールすることができます。

本記事のパッケージマネージャーは npm を利用します。

npx nuxi@latest init my-app
✔ Which package manager would you like to use? › npm
cd my-app  // プロジェクトディレクトリに移動
npm run dev  // Nuxt プロジェクトを起動

Install ESLint

ESLintをインストールしていきます。
Prettier も合わせていれておくのが鉄板でしたが、Nuxt 3では、No Prettier が推奨になっているので、今回はESLintのみでフォーマットさせておきます。

https://nuxt.com/docs/community/contribution#no-prettier

この記事では、ESLintのみでフォーマットを行っていきます。

npm install -D eslint eslint-plugin-nuxt @nuxtjs/eslint-config-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser

Config

.eslintrc
// .eslintrc
{
    "root": true,
    "env": {
      "browser": true,
      "es6": true
    },
    "extends": [
      "eslint:recommended",
      "@nuxtjs/eslint-config-typescript",
      "plugin:@typescript-eslint/recommended",
      "plugin:vue/vue3-recommended"
    ],
    "parser": "vue-eslint-parser",
    "parserOptions": {
      "parser": "@typescript-eslint/parser",
      "sourceType": "module"
    },
    "plugins": ["vue", "@typescript-eslint"],
    "overrides": [
      {
        "files": ["layouts/*.vue", "pages/**/*.vue"],
        "rules": { "vue/multi-word-component-names": "off" }
      }
    ]
}

Add Script

package.jsonにESLintの実行スクリプトを追加します。

package.json
{
  "script": {
    "lint": "eslint --fix './**/*.vue'"
  }
}

PhpStormやVSCodeの手元のエディタ・IDEで、保存時にフォーマットをかけておきます。

Vuetify 3

公式ドキュメントに Vuetify 3 をインストールしていきます。

  • vuetify
  • vite-plugin-vuetify
  • @mdi/font

Vuetifyの本体・Nuxtで利用するためにモジュールのインストール・Vuetifyで利用するアイコンをインストールしていきます。

https://vuetifyjs.com/en/getting-started/installation/#using-nuxt-3

npm i -D vuetify vite-plugin-vuetify
npm i @mdi/font

Config

次にNuxtでVuetifyを利用するための設定を行っていきます。
nuxt.config.ts に組み込みます。

ビルドはトランスパイルにVuetifyを追加します。
モジュールには、autoInportを許可できるように設定します。
最後にViteのテンプレートでアセットURLを参照できるようにします。

nuxt.config.ts
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  build: {
    transpile: ['vuetify']
  },
  plugins: [
    '~/plugins/vuetify.ts'
  ],
  modules: [
    (_options, nuxt) => {
      nuxt.hooks.hook('vite:extendConfig', (config) => {
        // @ts-expect-error
        config.plugins.push(vuetify({ autoImport: true }))
      })
    }
    // ...
  ],
  vite: {
    vue: {
      template: {
        transformAssetUrls
      }
    }
  }
})

Plugins

~/plugins/vuetify.ts を作成して プラグインで Vuetifyを呼び出すようにします。

plugins/vuetify.ts
// import this after install `@mdi/font` package
import '@mdi/font/css/materialdesignicons.css'

import 'vuetify/styles'
import { createVuetify } from 'vuetify'

export default defineNuxtPlugin((app) => {
  const vuetify = createVuetify({
    // ... your configuration
  })
  app.vueApp.use(vuetify)
})

公式では、defineNuxtPlugincreateVuetify を呼び出すようにしています。
この記事では、Storybookを利用するため、Vuetifyの設定値は Storybook の設定で呼び出すため別ファイルで管理しました。

utils/vuetify.ts を作成して、Vuetifyの設定を管理します。

Iconの設定を除けば、componentsとdirectivesのみでもVuetifyのコンポーネントを呼び出せます。
ここでは、アイコンを設定した場合も含めてコードをいれました。

utils/vuetify.ts
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
import 'vuetify/styles'

const vuetify = createVuetify({
  components,
  directives,
  icons: {
    defaultSet: 'mdi',
    aliases,
    sets: {
      mdi
    }
  }
})

export default vuetify

utils/vuetify.ts でファイルを退避させておくことで、Nuxtの開発環境・Storybook環境で利用することができます。

plugins/vuetify.ts
import vuetify from '~/utils/vuetify'

export default defineNuxtPlugin((app) => {
  app.vueApp.use(vuetify)
})

テンプレート部分にVuetifyのコンポーネントを配置

起動コマンドでVuetifyのコンポーネントが呼び出しができているか確認します。

npm run dev -- -o

app.vue or laypouts/defaluts.vue にVuetifyのコンポーネントを配置します。
ここでは簡易的にボタンをクリック後にコンテンツを表示するようにしました。

  • クリックでshowContentsの真偽値を切り替えしコンテンツを表示
app.vue
<script setup lang="ts">
import { ref } from 'vue'
const showContents = ref(false)
</script>
<template>
  <NuxtLayout>
    <v-app>
      <v-sheet class="pa-4 text-center mx-auto">
        <v-btn color="primary" @click="showContents = !showContents">
          <template v-if="!showContents">
            Open Content
          </template>
          <template v-if="showContents">
            Close Content
          </template>
        </v-btn>
      </v-sheet>
      <v-sheet v-if="showContents" class="pa-4 text-center mx-auto">
        コンテンツです。
      </v-sheet>
      <NuxtPage />
    </v-app>
  </NuxtLayout>
</template>

Nuxt環境でVuetifyのコンポーネントが利用できれば成功です。

Storybookの設定

ここからStorybookの環境構築をします。

https://storybook.js.org/docs/get-started/install

公式のドキュメントに沿ってインストールします。
npx storybook@latest init でStorybookの初期設定を行います。

初期設定では、利用するフレームワークを選択する画面が表示されます。Vue3を選択します。
ビルドツールはViteを選択します。

npx storybook@latest init
- Ok to proceed? (y) y
- ? Do you want to manually choose a Storybook project type to install? › y

- ? Please choose a project type from the following list: › - Use arrow-keys. Return to submit.
❯   react
    react_scripts
    react_native
    react_project
    webpack_react
    nextjs
  > vue3
    angular
    ember
  ↓ web_components

- We were not able to detect the right builder for your project. Please select one: › Vite
> Vite
- ? We have detected that you're using ESLint. Storybook provides a plugin that gives the best experience with Storybook and helps follow best practices: https://github.com/storybookjs/eslint-plugin-storybook#readme
> Yes

インストールが完了すると localhost:6006 でStorybookの画面が表示されます。

起動すると シンタックスエラーが発生します。
initで vite.config.jsが存在しないため vueファイルを読み込めないため起きます。(空ディレクトリで initする場合は、発生しません)

プロジェクトのルートに vite.config.js を作成して、Viteの設定を行います。

vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
})

設定ファイルの書き換え

初期インストールすると vite の設定なっているため stories のパスを変更をかけて .stories.ts といったファイルすべて対象にさせます。
また、初期インストールでは .storybook/main.js や .storybook/preview.js が存在するため、 tsファイル に変更します。

.storybook/main.ts を 修正します。

変更された部分は StorybookConfig 型をインポートして configの型定義します。

.storybook/main.ts
import type { StorybookConfig } from "@storybook/vue3-vite";

const config: StorybookConfig = {
  stories: ["../**/*.mdx", "../**/*.stories.@(js|jsx|mjs|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@chromatic-com/storybook",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/vue3-vite",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
};
export default config;

.storybook/preview.ts にVuetifyのコンポーネントを呼び出すように修正します。

Previewではスタイルの適用などコンポーネント実際に描画するため @storybook/vue3setup を利用して Vuetify を呼び出します。

また、Previewの型もmainと同様にインポートして型定義します。

.storybook/preview.ts
import type { Preview } from "@storybook/vue3";
import { setup } from '@storybook/vue3'
// Styles
import vuetify from "../utils/vuetify";

setup((app) => {
  if (app) {
    app.use(vuetify)
  }
})

export const decorators = [
  (story: any) => ({
    components: { story },
    template: '<v-app><story /></v-app>',
  }),
]

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

再度、storybookを起動するとStorybookが表示されます。

npm run storybook

StorybookにVuetifyのコンポーネントを追加

最後にStorybook上でVuetifyのコンポーネントを追加して表示されることを確認します。

components/Button.stories.tscomponents/Button.vue を配置します。

components/Button.vue
<script setup lang="ts">
defineProps<{
   /**
   * ボタンに表示するテキスト
   */
  label: string
  variant?: 'plain' | 'outlined' | 'tonal' | 'text'
  color: 'primary' | 'success' | 'error' | 'info'
  flat?: boolean
  exact?: boolean
  elevation?: string | number
  disabled?: boolean
  height?: string | number
  href?: string
  icon?: any
  loading?: string | boolean
}>()
const emits = defineEmits<{
  /**
   * @param label The label of the button
   */
  click: [label: string];
}>()
</script>

<template>
  <v-btn
    :label="label"
    :color="color"
    :variant="variant"
    :flat="flat"
    :exact="exact"
    :elevation="elevation"
    :disabled="disabled"
    @click="() => emits('click', 'Event')"
  >
    {{ label }}
  </v-btn>
</template>
components/Button.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import Button from './Button.vue'

type Story = StoryObj<typeof Button>

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    label: {
      control: {
        type: 'text'
      }
    },
    height: {
      control: {
        type: 'number'
      }
    },
    elevation: {
      control: {
        type: 'number'
      }
    },
    variant: {
      control: {
        type: 'select'
      },
      options: ['plain', 'outlined', 'tonal', 'text', undefined]
    },
    color: {
      control: {
        type: 'select'
      },
      options: ['primary', 'success', 'error', 'info']
    },
    href: {
      control: {
        type: 'text'
      }
    },
    onClick: { action: 'click' }
  },
  render: args => ({
    components: { Button },
    setup () {
      return { args }
    },
    template: '<Button v-bind="args" />'
  })
}

export const Default: Story = {
  args: {
    label: 'Button',
    variant: undefined,
    color: 'primary',
    flat: false,
    rounded: false,
    disabled: false,
    loading: true,
    exact: false,
    elevation: 1000,
    height: 1000
  }
}

export default meta

再度、起動することで配置したVuetifyのボタンを表示されます。

npm run storybook

表示されれば開発環境構築は以上です。
ドキュメントだけではいくつかハマりどころがあるので、参考になればと思います。

補足:検証用のリポジトリと表示用URL

以下の検証用リポジトリとURLはStorybook7系で動いています。
再度、開発環境構築試したところStorybook 8がリリースされていました。

記事自体は、8系として検証しています。
以下の検証用は Stoybook 8にアップデートする予定です。

https://github.com/redamoon/nuxt3-vuetify-storybook

https://nuxt3-vuetify-storybook.pages.dev/

株式会社HAMWORKS
株式会社HAMWORKS

Discussion