30. Storybook. Налаштування Nuxt + Storybook. Frontend Workshop. Структура директорій у Nuxt

Відео версія
(YouTube - для зворотнього зв'язку)

TL;DR

Storybook корисний для великих проєктів, проєктів з A/B тестами та з багатьма темами.

Змініть структуру проєкту так, щоб увесь код розробників знаходився в окремій директорії. Це допоможе уникнути пошуку у кешах, білдах та node_modules.

pathPrefix — погана практика в іменуванні компонентів у Nuxt. Це дуже ускладнює рефакторинг.

nuxt.config.ts
export default defineNuxtConfig({
    components: [
        {
            path: '~/components',
            pathPrefix: false,
        },
    ],
})

Зберігання stories та тестів поруч із компонентом не вплине на продакшн-збірку.

Зберігайте всі mock-дані в окремих .mock файлах, що дозволить їх перевикористовувати та оновлювати без зайвих проблем.

Storybook змусить вас писати чисті компоненти, залежні лише від аргументів (властивостей). Це гарний підхід.

Налаштування Storybook для Nuxt

command line
npm i --save-dev storybook @nuxtjs/storybook @storybook/addon-essentials @storybook/addon-interactions @storybook/addon-links

Запускайте Storybook окремо від основної збірки, з конфігурацією для відокремлення його від основного проєкту.

package.json
{
  "scripts": {
    "storybook": "IS_STORYBOOK=true storybook dev --port 6006",
    "build-storybook": "IS_STORYBOOK=true storybook build"
  }
}

Змінюйте конфігурацію Nuxt залежно від наявності змінної IS_STORYBOOK:

nuxt.config.ts
export default defineNuxtConfig({
    pages: !process.env.IS_STORYBOOK,
    ogImage: {
        enabled: !process.env.IS_STORYBOOK,
    },
    vue: {
        runtimeCompiler: 'IS_STORYBOOK' in process.env,
    },
})
  • pages — виправляє проблему навігації Storybook з конфігурацією Nuxt маршрутизації.
  • ogImage — виправляє баг з SSR для пакета ogImage, де SSR = false за замовчуванням у @nuxtjs/storybook.
  • vue.runtimeCompiler — дозволяє рендеринг компонентів на сторінках Storybook. Вимикайте для основної збірки, якщо вона цього не потребує.

Entrypoint для запуску:

.storybook/main.ts
import type { StorybookConfig } from '@storybook-vue/nuxt'
import { mergeConfig } from 'vite'

const config: StorybookConfig = {
  stories: [
    '../app/**/*.mdx',
    '../app/components/**/*.stories.ts',
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-themes',
    '@storybook/addon-a11y',
  ],
  framework: {
    name: '@storybook-vue/nuxt',
    options: {},
  },
  docs: {},
  async viteFinal(config) {
    return mergeConfig(config, {
      optimizeDeps: {
        include: ['jsdoc-type-pratt-parser'],
      },
    })
  },
}
export default config
  • stories — директорії для пошуку файлів stories.
  • viteFinal — виправляє баг з рендерингом документації.

Файл конфігурації:

.storybook/preview.ts
import type { Preview } from '@storybook-vue/nuxt'
import { withThemeByClassName } from '@storybook/addon-themes'
import { h, Suspense } from 'vue'
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'
import './stubs/i18n.stub'
import './stubs/nuxt-link.stub'

const preview: Preview = {
  parameters: {
    layout: 'fullscreen',
    viewport: {
      viewports: {
        desktop: {
          name: 'Desktop',
          styles: {
            height: '100%',
            width: '100%',
            overflow: 'clip',
          },
          type: 'desktop',
        },
        ...MINIMAL_VIEWPORTS,
      },
      defaultViewport: 'desktop',
    },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
  decorators: [
    withThemeByClassName({
      themes: {
        light: 'light',
        dark: 'dark',
      },
      defaultTheme: 'dark',
    }),
    (story) => {
      return {
        setup() {
          return () => h(Suspense, {}, [h(story())])
        },
      }
    },
  ],
}

export default preview
  • layout — шаблон розміщення компонентів за замовчуванням для всіх історій.
  • viewport — налаштування доступних viewport та встановлення глобального.
  • controls — автоматичне визначення елементів контролю для історій.
  • withThemeByClassName — налаштування тем.
  • (story) => ... Suspense — дозволяє очікування динамічних компонентів (наприклад, Nuxt Icon).

У файлі .storybook/preview.ts також підключайте всі глобальні директиви, компоненти та методи.

Підключення i18n для локалізації:

.storybook/stubs/i18n.stub.ts
import { setup } from '@storybook/vue3'
import { createI18n } from 'vue-i18n'
import en from '../../app/locales/en.json'

setup((app) => {
  const i18n = createI18n({
    locale: 'en',
    messages: { en },
  })
  app.use(i18n)
})

Коригування компонента NuxtLink:

nuxt-link.stub.ts
import { setup } from '@storybook/vue3'
import { action } from '@storybook/addon-actions'
import { h } from 'vue'

setup((app) => {
  app.component('RouterLink', {
    props: ['to'],
    methods: {
      onClick(e) {
        e.preventDefault()
        action('switch-route')(this.to)
      },
    },
    render() {
      return h('a', { onClick: this.onClick, href: this.to }, this.$slots.default())
    },
  })
})

Також створіть stub для всіх глобальних методів.