👗

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

2022/06/05に公開
1

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

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

  • Vite にネイティブ対応
    • Histoire は Vite 向けのツールであるので、vite.config.ts の設定を再利用できます。このあたりの特徴は Vitest と同様ですね
  • Story をフレームワークそのままの書き方で作成できる
    • Storybook の場合 Vue で SFC ファイル形式のコンポーネントを作成していたとしても、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 - -

https://github.com/histoire-dev/histoire#supported-frameworks

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

インストール

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

$ npm i -D histoire

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

package.json
{
  "scripts": {
    "story:dev": "histoire dev",
    "story:build": "histoire build",
    "story:preview": "histoire preview"
  }
}

プロジェクトで TypeScript を使用している場合、env.d.ts を作成して以下を記述します。この記述により、Histoire の提供するコンポーネントの型が有効になります。

env.d.ts
/// <reference types="histoire" />

グローバル CSS を設定する

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

histoire.config.ts
// 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
// src/histoire.setup.ts
import './index.css'

Story を作成する

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

AppButton.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 を与えることができます。

AppButton.story.vue
<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 つのページにグリッドレイアウトで表示できます。

AppButton.story.vue
  <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>

すべての variant が 1 つのページに表示され、variant をクリックすることで選択できます。

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

状態をコントロールする

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

AppButton.story.vue
<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 タブのみにフォームが追加されます。

AppButton.story.vue
<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>

<HstText> は Histoire にあらかじめ用意されているコンポーネントで、 Controls タグ向けの UI のフォームを利用できます。Controls タブに label フォームが追加され、ボタン内部のテキストを操作できるようになりました。

controls-label

イベント

Histoire の hstEvent 関数を使用することでコンポーネントが emit するイベントの一覧を Events タブに表示できます。hstEventhistoire/client からインポートする必要があります。

AppButton.story.vue
<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>

hstEvent の第 1 引数はイベント名、第 2 引数はイベントにより発生したデータを指定します。これでクリックイベントが発生するたび、Events タブに Click イベントが表示されます。

events

ドキュメント

トップレベルに <doc> タグを追加することでコンポーネントのドキュメントをマークダウン記法で記述できます。デフォルトでは markdown-it により描画されますが、markdown 設定によりカスタマイズ可能です。

AppButton.story.vue
<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>

ドキュメントは Docs タグに表示されます。

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

ソースコード

デフォルトでは Story からソースコードを自動で生成して表示されます。

<Story> または <Variant> タグに source Props を渡すか、source スロットを使用することで表示されるソースコードを上書きできます。

AppButton.story.vue
<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 のようにプラグインにより機能を拡張できます。例としてスクリーンショットを撮影してビジュアルリグレッションテストを実施する @histoire/plugin-screenshot を使ってみましょう。

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

$ npm i -D @histoire/plugin-screenshot

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

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({})
+   ]
  })

ビルドコマンドを実行した際にスクリーンショットが撮影されるようになります。デフォルトの設定では撮影されたスクリーンショットは .histoire/screenshots に保存されます。

$ npm run story:build

感想

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

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

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

https://github.com/azukiazusa1/histoire-example

GitHubで編集を提案

Discussion