Vue 向けの Vite 製の UI コンポーネントカタログツール Histoire
Histoire はフランス語で「Story」という意味の単語であり、Storybook のように UI コンポーネントのカタログを作成するツールです。
Histoire は以下のような特徴を謳っています。
- Vite にネイティブ対応
- Histoire は Vite 向けのツールであるので、
vite.config.ts
の設定を再利用できます。このあたりの特徴は Vitest と同様ですね
- Histoire は Vite 向けのツールであるので、
- Story をフレームワークそのままの書き方で作成できる
- Storybook の場合 Vue で SFC ファイル形式のコンポーネントを作成していたとしても、Story を作成する場合には
.stories.ts
のような拡張子でファイルを作成して Storybook 向けのコンポーネントの記述をする必要があります。一方 Histoire は Story を作成する際にも.vue
や.svelte
のような拡張子を使用でき、フレームワークの特徴に合わせた書き方ができます。
- Storybook の場合 Vue で SFC ファイル形式のコンポーネントを作成していたとしても、Story を作成する場合には
- 早くて軽い
- やはり 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
を作成して以下を記述します。この記述により、Histoire の提供するコンポーネントの型が有効になります。
/// <reference types="histoire" />
グローバル CSS を設定する
グローバルに読み込む CSS がある場合 Histoire の設定ファイルを作成する必要があります。設定ファイルは vite.config.ts
の histoire
プロパティとして記載するか、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 の設定に基づき生成されているようです。
Storybook と同じように Controls タブが存在し、Props を変更できます。
その他の機能としてはデフォルトで以下を設定できるようです。
- ダークモードの切り替え
- レスポンスサイズの設定
- 背景色の設定
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
メニューの配下にサブメニューが追加されました。
レイアウトの変更
デフォルトでは <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>
すべての variant
が 1 つのページに表示され、variant
をクリックすることで選択できます。
状態をコントロールする
デフォルトでは 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 タブのみにフォームが追加されます。
<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
フォームが追加され、ボタン内部のテキストを操作できるようになりました。
イベント
Histoire の hstEvent
関数を使用することでコンポーネントが emit するイベントの一覧を Events タブに表示できます。hstEvent
は histoire/client
からインポートする必要があります。
<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
イベントが表示されます。
ドキュメント
トップレベルに <doc>
タグを追加することでコンポーネントのドキュメントをマークダウン記法で記述できます。デフォルトでは markdown-it により描画されますが、markdown 設定によりカスタマイズ可能です。
<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" | "secondary" | "primary" | The color of the button |
| disabled | boolean | false | Whether the button is disabled |
</docs>
ドキュメントは Docs タグに表示されます。
ソースコード
デフォルトでは 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>
プラグイン
Storybook の Addons のようにプラグインにより機能を拡張できます。例としてスクリーンショットを撮影してビジュアルリグレッションテストを実施する @histoire/plugin-screenshot を使ってみましょう。
まずはパッケージをインストールします。
$ 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({})
+ ]
})
ビルドコマンドを実行した際にスクリーンショットが撮影されるようになります。デフォルトの設定では撮影されたスクリーンショットは .histoire/screenshots
に保存されます。
$ npm run story:build
感想
Vite 製のツールはやっぱり早く動作するので良いですね。エコシステムはまだまだ充実してるとはいえないですが、一通りの機能は揃っているので試してみるには良さそうですね。
個人的には Vue の SFC ファイル形式で Story を記述できるところが気に入っています。Storybook 用の新しい書き方を学ぶコストが削減できていい感じです。
サンプルコードは以下のレポジトリにあります。
Discussion
タイトルVueとVite反対では?