This article was translated from Japanese by AI and may contain inaccuracies. For the most accurate content, please refer to the original Japanese version.
histoire

Vue 向けの Vite 製の UI コンポーネントカタログツール Histoire

Histoireはフランス語で「Story」という意味の単語であり、Storybook のように UI コンポーネントのカタログを作成するツールです。Vite にネイティブ対応、Vue の SFC 形式で Story を書けるといった特徴があります。

Histoire はフランス語で「Story」という意味の単語であり、Storybook のように UI コンポーネントのカタログを作成するツールです。

Histoire は以下のような特徴を謳っています。

  • Vite にネイティブ対応
    • Histoire は Vite 向けのツールであるので、vite.config.ts の設定を再利用できます。このあたりの特徴は Vitest と同様ですね
  • Story をフレームワークそのままの書き方で作成できる
    • Storybook の場合 Vue でコンポーネントを作成していたとしても Story を作成する場合には .stories.ts のような拡張子でファイルを作成して Storybook 向けのコンポーネントの記述をする必要があります。一方 Histoire は Story を作成する際にも .vue.svelte のような拡張子を使用でき、フレームワークの特徴に合わせた書き方ができます。
  • 早くて軽い
    • やはり Vite を使っているだけあってビルド速度は高速なようです
  • 拡張性が高い
  • すばらしい UX

Histoire をはじめる

それでは早速 Histoire をはじめてみましょう。Histoire は現在(2022/06/04)以下のフレームワークに対応しています。

Framework Versions Support Auto CodeGen Auto Docs
Vue 3.2+ Todo
Svelte - Planned - -
Solid - Planned - -
Angular - TBD - -
React - Alternative - -

React の代替として Ladle があげられています。Ladle もまた Vite ベースなので Histoire が Vue 向け、Ladle が React 向けという立ち位置のように感じます。

インストール

以下コマンドで Histoire をインストールします。

$ npm i -D histoire

続いて package.json に scripts を追加します。

{
  "scripts": {
    "story:dev": "histoire dev",
    "story:build": "histoire build",
    "story:preview": "histoire preview"
  }
}

プロジェクトで TypeScript を使用している場合、env.d.ts を作成して以下を記述します。

/// <reference types="histoire" />

グローバル CSS を設定する

グローバルに読み込む CSS がある場合 Histoire の設定ファイルを作成する必要があります。設定ファイルは vite.config.tshistoire プロパティとして記載するか、histoire.config.ts ファイルを新たに作成して設定を記載する 2 通りの方法があります。ここでは後者の方法を使用します。

// histoire.config.ts
import { defineConfig } from 'histoire'
 
export default defineConfig({
  setupFile: '/src/histoire.setup.ts'
})

setupFile オプションは各ストーリープレビューの設定時にデフォルトで実行されるセットアップファイルを指定します。src/histoire.setup.ts ファイルを作成してそこで CSS を読み込みます。

// src/histoire.setup.ts
import './index.css

Story を作成する

それでは実際に Story を作成しましょう。すべての Story は .story.vue 拡張子を使用します。

<script lang="ts" setup>
import AppButton from "./AppButton.vue";
</script>
 
<template>
  <Story title="buttons">
    <AppButton> I am a button </AppButton>
  </Story>
</template>

上記のように通常の Vue の SFC ファイルと全く同じスタイルで Story を記述できます。Story は <template> タグ内の <Story> タグの中に記述する必要があります。

<Story> タグは title Props を受け取ることができ、これを指定すると任意のタイトルを付与できます。

Story を作成したら開発サーバーを起動しましょう。

$ npm run story:dev

http:://localhost:3000 にアクセスすると作成した buttons ストーリーが表示されます。その他、Design System メニューも自動で作成されており、どうやら TailwindCSS の設定に基づき生成されているようです。

スクリーンショット 2022-06-04 20.08.32

スクリーンショット 2022-06-04 20.11.21

Storybook と同じように Controls タブが存在し、Props を変更できます。

controls

その他の機能としてはデフォルトで以下を設定できるようです。

  • ダークモードの切り替え
    • darkmode
  • レスポンスサイズの設定
    • responsive
  • 背景色の設定
    • background-color

Story を複数作成する

<Variants> タグを使用することで、1 つのコンポーネントに対して複数の表示を作成できます。<Variants> タグも <Story> タグと同様に title Props を与えることができます。

<script lang="ts" setup>
import AppButton from "./AppButton.vue";
</script>
 
<template>
  <Story title="buttons">
    <Variant title="default">
      <AppButton> I am a button </AppButton>
    </Variant>
    <Variant title="secondary">
      <AppButton color="secondary"> I am a button </AppButton>
    </Variant>
    <Variant title="disabled">
      <AppButton disabled> I am a button </AppButton>
    </Variant>
  </Story>
</template>

<Variant> タグを追加したことにより、buttons メニューの配下にサブメニューが追加されました。

スクリーンショット 2022-06-04 20.28.20

レイアウトの変更

デフォルトでは <Variant> タグを使用した場合、1 つの variant につき 1 つのメニュが追加され、それぞれ別のページに表示されます。ですが、時にはすべての variant を横に並べて表示して比較したいこともあるでしょう。

そのような場合には、<Story>layout Props を渡して { type: 'grid' } を指定することで、すべての variant を 1 つのページにグリッドレイアウトで表示できます。

  <script lang="ts" setup>
  import AppButton from "./AppButton.vue";
  </script>
 
  <template>
-   <Story title="buttons">
+   <Story title="buttons" :layout="{ type: 'grid' }">
      <Variant title="default">
        <AppButton> I am a button </AppButton>
      </Variant>
      <Variant title="secondary">
        <AppButton color="secondary"> I am a button </AppButton>
      </Variant>
      <Variant title="disabled">
        <AppButton disabled> I am a button </AppButton>
      </Variant>
    </Story>
  </template>

スクリーンショット 2022-06-04 20.39.19

コンポーネントに状態を渡す

デフォルトでは Controls タブで Props の状態を変更できますが、それ以外の状態をコンポーネントに渡して操作したいこともあるでしょう。例えば、次の例ではボタンコンポーネントのスロットに渡す文字列を状態として保持するようにしています。

<script lang="ts" setup>
import { ref } from "vue";
import AppButton from "./AppButton.vue";
 
const label = ref("Hello World");
</script>
 
<template>
  <Story title="buttons">
    <AppButton>{{ label }}</AppButton>
  </Story>
</template>

あまり難しいことは考えずに、普段の Vue で行っている方法と同じく ref で状態を定義しています。このままでは状態を更新できないので Controls タブに label を更新できるようにします。

<Story> タグまたは <Variant> タグの controls スロットを使用することで Controls タブにフォームを追加できます。<Story> タグ直下にスロットを配置した場合すべての variant の Controls タブにフォームが追加され、<Variant> タグ配下にスロットを配置した場合にはその variant の Controls タブのみにフォームが追加されます。

<HstText> は Histoire にあらかじめ用意されているコンポーネントで、 Controls タグ向けの UI のフォームを利用できます。

<script lang="ts" setup>
import { ref } from "vue";
import AppButton from "./AppButton.vue";
 
const label = ref("Hello World");
</script>
 
<template>
  <Story title="buttons">
    <AppButton>{{ label }}</AppButton>
 
    <template #controls>
      <HstText v-model="label" title="label" />
    </template>
  </Story>
</template>

Controls タブに label フォームが追加され、ボタン内部のテキストを操作できるようになりました。

controls-label

イベント

Histoire の hstEvent 関数を使用することでコンポーネントが emit するイベントの一覧を Events タブに表示できます。

<script lang="ts" setup>
import AppButton from "./AppButton.vue";
import { hstEvent } from "histoire/client";
</script>
 
<template>
  <Story title="buttons">
    <AppButton @click="hstEvent('Click', $event)">I am a button</AppButton>
  </Story>
</template>

events

ドキュメント

ファイルのルートレベルに <doc> タグを追加することでコンポーネントのドキュメントをマークダウン記法で記述できます。ドキュメントは Docs タグに表示されます。

<script lang="ts" setup>
import AppButton from "./AppButton.vue";
</script>
 
<template>
  <Story title="buttons">
    <AppButton>I am a button</AppButton>
  </Story>
</template>
 
<docs lang="md">
# Buttons
 
This is a button.
 
## Props
 
| Name     | Type                         | Default   | Description                    |
| -------- | ---------------------------- | --------- | ------------------------------ |
| color    | "primary" &#124; "secondary" | "primary" | The color of the button        |
| disabled | boolean                      | false     | Whether the button is disabled |
</docs>

スクリーンショット 2022-06-04 21.59.44

ソースコード

デフォルトでは Story からソースコードを自動で生成して表示しますが、<Story> または <Variant> タグに source Props を渡すか、source スロットを使用することで表示されるソースコードを上書きすることでできます。

<script lang="ts" setup>
import AppButton from "./AppButton.vue";
</script>
 
<template>
  <Story title="buttons">
    <AppButton>I am a button</AppButton>
    <template #source>custome source code</template>
  </Story>
</template>

スクリーンショット 2022-06-04 22.08.12

プラグイン

Storybook の Addons のようにプラグインにより機能を拡張できます。例としてスクリーンショットを撮影してビジュアルリグレッションテストを実施する https://histoire.dev/guide/plugins.html を使ってみましょう。

まずはパッケージをインストールします。

$ npm i -D @histoire/plugin-screenshot

Histoire の設定ファイル(この記事内では histoire.config.ts)においてプラグインを追加します。

  // histoire.config.ts
  import { defineConfig } from 'histoire'
+ import { HstScreenshot } from '@histoire/plugin-screenshot'
 
  export default defineConfig({
    setupFile: '/src/histoire.setup.ts',
+   plugins: [
+     HstScreenshot({})
+   ]
  })

ビルドコマンドを実行した際にスクリーンショットが撮影されるようになります。

$ npm run story:build

感想

Vite 製のツールはやっぱり早く動作するので良いですね。エコシステムはまだまだ充実してるとはいえないですが、一通りの機能は揃っているので試してみるには良さそうですね。

個人的には Vue の SFC ファイル形式で Story を記述できるところが気に入っています。Storybook 用の新しい書き方を学ぶコストが削減できていい感じです。

サンプルコードは以下のレポジトリにあります。