Nuxt3(RC版)にStorybookを導入する

36216 ワード

この記事の背景・目的

ついにNuxt3のRC1がリリースされました。
Stable Versionのリリースもあと一歩というところかと思いますが、先んじてNuxt3の実戦投入を行っております。

Nuxt3入門系のネタをなにか書こうと思っていたのですが、ちょうどプロジェクトでStorybookを導入してみたので、その手順を残しておこうと思います。(英語含め、ちゃんとした記事になっている資料は軽く探して見つからなかったため)

この記事の目的は、Nuxt3でStorybookを動かすための手順をNuxt3へのアーリーアダプタ且つ初心者向けに示すことです。Nuxt3も周辺のエコシステムも過渡期のためすぐに情報が古くなってしまうかもしれませんが、その場合はその旨追記したいと思います。(また、新しい情報にお気付きの方はコメント等で教えて頂けると助かります。)

今回のアウトプットとなるサンプルのリポジトリはこちらになります。

予備知識

NuxtにStorybookを入れるとなると、Nuxt2であれば公式のnuxt/storybookというモジュールを用いることで、特段なにもすることなくZero-ConfigurationでStorybookが動きます。

しかし、nuxt/storybookがまだNuxt3に対応していないとのことなので、今回はstorybook/vue3と、@storybook/builder-viteを使って対応していきます。

事前準備

NodeとNPM / Yarnの最新版を入れておきます。

Nuxt3のインストール

まず、Nuxt3のプロジェクトを作成していきます。

npx nuxi init {{ ProjectName }}

{{ ProjectName }}には任意の名前を入れて下さい。

必要なパッケージのインストール

次に、Storybookを動かすのに必要なパッケージをインストールします。
(Nuxt3のデフォルトのバンドラーはViteなので@storybook/builder-viteをインストールしますが、WebPackや他のバンドラーを使う場合は手順が異なります。)

yarn add -D @storybook/vue3 @storybook/addon-essentials @storybook/builder-vite

ちなみに@storybook/addon-essentialsは、Storybookのよく使われるアドオンをまとめたもので導入は必須ではないと思いますが、特に設定等必要なく有用なので入れておきます。(Storybookを使っている殆どのプロジェクトで入っていると思っています)

Storybook用設定ファイルの配置

Storybook用の設定ファイルを作っていきます。
プロジェクトルートに.storybookというディレクトリを作成して、その中にmain.jsという名前のファイルを置きます。

mkdir .storybook
touch .storybook/main.js

中身はこんな感じにしておきます。(後ほど追記します。)

// .storybook/main.js

module.exports = {
  stories: ['../components/stories/**/*.stories.mdx', '../components/stories/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-essentials'],
  framework: '@storybook/vue3',
  core: {
    builder: '@storybook/builder-vite',
  },
}

これでひとまず動くようになります。

とりあえず動かしてみる

package.jsonに以下のようにstorybookコマンドを追記します。

{
  "private": true,
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "storybook": "start-storybook" \\ 追記
  },
  "devDependencies": {
    "@storybook/addon-essentials": "^6.4.22",
    "@storybook/vue3": "^6.4.22",
    "nuxt": "3.0.0-rc.1",
    "storybook-builder-vite": "^0.1.23"
  }
}

コマンドを実行してStorybookを開きます(バンドルが完了すると、自動でブラウザにタブが開き、Storybookが開かれます。)

yarn storybook

Couldn't find any stories in your Storybook.と言われるはずですが、Storybookの起動が確認できました。
ちなみに、portを指定したいときはstart-storybook -p 6006, 静的に読み込みたいファイルがあるときはstart-storybook -s ./staticなどのオプションを付けて実行します。

グローバルSCSSを読み込めるようにする

プロジェクト内でグローバルな共通SCSSを読み込みたいというユースケースは非常に多いと思います。
SFC内でSCSS / SASSを使えるようにするため、以下のコマンドでsassをインストールします。

yarn add -D sass

これでSASS / SCSSが使えるようになります。

assetsディレクトリ配下にSCSSを配置します。

mkdir -p assets/styles
touch assets/styles/global.scss

中身はこんな感じにしておきます。

// assets/styles/global.scss
html {
  font-size: 62.5%;
}

$color__primary: #0079a5;
$color__white: #fff;

これだけではグローバルSCSSとして読み込めないため、nuxt.config.tsと、storybook/main.jsにそれぞれViteの設定を追記します。

// nuxt.config.ts

import { defineNuxtConfig } from 'nuxt'

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@import "@/assets/styles/global.scss";',
        },
      },
    },
  },
})
// .storybook/main.js

const { mergeConfig } = require('vite')

module.exports = {
  stories: ['../components/stories/**/*.stories.mdx', '../components/stories/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-essentials'],
  framework: '@storybook/vue3',
  core: {
    builder: 'storybook-builder-vite',
  },
  viteFinal: async (config) => {
    return mergeConfig(config, {
      css: {
        preprocessorOptions: {
          scss: {
            additionalData: '@import "../assets/styles/global.scss";',
          },
        },
      },
    })
  },
}

Nuxt2であれば@nuxtjs/style-resourcesを使えばよかったのですが、Nuxt3には対応していないため、Nuxtの設定側でもこのような対応になります。

サンプルのコンポーネントを用意する

既存プロジェクトに入れる場合や表示したいコンポーネントが既にある場合は読み飛ばしてください。
サンプル用にSampleButtonというコンポーネントを作っていきます。
componentsディレクトリとSampleButton.vueコンポーネントを作ります。

mkdir components
touch components/SampleButton.vue

中身は↓をコピペします。

// components/SampleButton.vue
<script setup lang="ts">
interface PropType {
  outlined?: boolean
  type?: 'button' | 'submit' | 'reset'
}
withDefaults(defineProps<PropType>(), {
  outlined: false,
  type: 'button',
})

type Emits = {
  (e: 'click'): void
}
const emit = defineEmits<Emits>()
const handleClick = () => {
  emit('click')
}
</script>

<template>
  <button class="sample-button" :class="{ outlined: outlined }" :type="type" @click="handleClick">
    <slot></slot>
  </button>
</template>

<style lang="scss" scoped>
.sample-button {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 8px 16px;
  border-radius: 8px;
  font-size: 1.4rem;
  line-height: 2.1rem;
  letter-spacing: 0.1em;
  border: none;
  background: $primary-cyan;
  color: $base-gray-000;
  cursor: pointer;

  &.outlined {
    background: $base-gray-000;
    color: $primary-cyan;
    border: 1px solid $primary-cyan;
    box-sizing: border-box;
    border-radius: 8px;
  }
}
</style>

また、このコンポーネント用のStoriesファイルを用意します。
今回はcomponents配下にstoriesというディレクトリを作って、ここにStoriesファイルを配置します。

mkdir components/stories
touch components/stories/SampleButton.stories.ts

中身はこんな感じにしておきます。

import SampleButton from '../SampleButton.vue'

export default {
  title: 'Button/SampleButton',
  component: SampleButton,
  argTypes: {
    click: {
      action: 'click',
    },
  },
}

const Template = (args) => ({
  components: { SampleButton },
  setup() {
    return {
      args,
    }
  },
  template: `
    <SampleButton v-bind="args" @click="click">Sample Button</SampleButton>
  `,
})

export const Primary = Template.bind({})
Primary.args = {
  outlined: false,
  type: 'button'
}

Storiesファイルの書き方の詳細は公式ドキュメントを読んでください。

早速表示させてみます。

yarn storybook

表示されてますね!SCSSの変数も読み込めているようです。
ただ、1つだけ違和感があります。
実は、global.scssの以下の記述が効いてません。

html {
  font-size: 62.5%;
}

なぜかというと、Storybookはコンポーネントをプレビュー用のiframe内でレンダリングしてるのですが、この指定がその中のhtmlタグでなく、親(StorybookのアプリケーションUI)側のhtmlタグに適用されてしまうからです。
これを防ぐためには、.storybook/配下にpreview-body.htmlというファイルを置き、その中にスタイルを記述してあげます。

<!-- .storybook/preview-body.html -->
<style>
  html {
    font-size: 62.5%;
  }
</style>

もう一度読み込んでみると、ボタンのフォントサイズが小さくなっていて、remが参照するルートのフォントサイズが正しく変更されていることが確認できるかと思います。

コンポーネントの絶対インポートを有効にする

これでほぼ完成ですが、このままではComponent内でimport @/components/SampleComponent.vueなど、絶対インポートを使っているところがエラーになってしまいます。
これを防ぐため、ViteのPath Aliasの設定を追加します。

// .storybook/main.js
const { mergeConfig } = require('vite')
const path = require('path') // 追加

module.exports = {
  stories: ['../components/stories/**/*.stories.mdx', '../components/stories/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-essentials'],
  framework: '@storybook/vue3',
  core: {
    builder: 'storybook-builder-vite',
  },
  viteFinal: async (config) => {
    return mergeConfig(config, {
      css: {
        preprocessorOptions: {
          scss: {
            additionalData: '@import "../assets/styles/global.scss";',
          },
        },
      },
      // ↓追加
      resolve: {
        alias: {
          '@': path.resolve(__dirname, '../'),
          '~': path.resolve(__dirname, '../')
        },
      },
    })
  },
}

おわり

これでStorybookの環境が完成しました。
もし記事に不備や「こういうケースはどうするの?」等々あればコメントで是非教えて下さい。
ありがとうございました。