Storybook 7 + Vue 3 を試していく
Book 化完了したので、本スクラップは不要です。
2年前に書いた以下コンテンツが未だに一定のアクセスあるけど、明らかに古くなってきてるので最新化したいというモチベーション
SB7 は現状まだβで、リリースまで数ヶ月は掛かりそうなペースだし、リリースされるより先に Book を公開して基本的な使い方とか Tips を紹介できるといいな。
βだけど、SB7 + Vue の公式ドキュメントがあるのでそれに沿いながら進めてこ。
Vite でプロジェクト作る
$ yarn create vite vue-3-storybook-7 --template vue
$ cd vue-3-storybook-7
$ yarn install
一旦TSはナシで、そのうち触れるかな。
$ yarn dev
http://127.0.0.1:5173/
開いて Vue 3 アプリケーションが動いていればOK。
開発環境はもう二度と使わないのでこれで終了。
e1ccf8c2fe880abd361f41091bb5799c184b36c3
公式ドキュメントだと
npx storybook init
でセットアップしろというけど、個人的には推奨アドオン勝手に入ってくるし、いらんサンプルストーリーも作成されて好きじゃないので、手動セットアップをしていこうと思う。
そもそもスキャフォルド使うとまだ v6 がインストールされるっぽい。(オプションで v7 指定できるのかな?)
必要なパッケージはここから確認できるかな。
storybook/code/frameworks/vue3-vite at next · storybookjs/storybook
一旦コレで動かせるかな。 core-server
は初めて見たので本当に必要かまだわからん。
$ yarn add -D @storybook/vue3@7.0.0-beta.14 @storybook/core-server@7.0.0-beta.14 @storybook/builder-vite@7.0.0-beta.14
28666130f53847ec8c7a58cd46ff15f0f1b251ed
core-server はフレームワークに依存しない共通部分のパッケージっぽいのでまぁ必須と考えて良さそう。
@storybook/vue3-vite
を入れるだけでこの辺も Dependencies になってるから一発で済むっぽい。
バイナリコマンドがなくなって CLI からサーバー起動できるようになってるみたい。
SB6.x framework packages shipped binaries called start-storybook and build-storybook.
In SB7.0, we've removed these binaries and replaced them with new commands in Storybook's CLI: > storybook dev and storybook build
なのでそもそも CLI が必要。
$ yarn add -D storybook@7.0.0-beta.14
6d76035a65ce57562513a316af37555314e1a8f2
CLI が動く。 SB7 では CLI から色々できるっぽいな。 codemod 使ってマイグレーションもできそう。
Usage: storybook <command> [options]
Options:
--disable-telemetry disable sending telemetry data
--debug Get more logs in debug mode (default: false)
--enable-crash-reports enable sending crash reports to telemetry data
-V, --version output the version number
-h, --help display help for command
Commands:
init [options] Initialize Storybook into your project.
add [options] <addon> Add an addon to your Storybook
babelrc generate the default storybook babel config into your current working directory
upgrade [options] Upgrade your Storybook packages to the latest
info Prints debugging information about the local environment
migrate [options] [migration] Run a Storybook codemod migration on your source files
extract [location] [output] extract stories.json from a built version
repro [options] [outputDirectory] Create a reproduction from a set of possible templates
repro-next [options] [filterValue] Create a reproduction from a set of possible templates
link [options] <repo-url-or-directory> Pull down a repro from a URL (or a local directory), link it, and run storybook
automigrate [options] [fixId] Check storybook for known problems or migrations and apply fixes
dev [options]
build [options]
help [command] display help for command
とりあえずサーバー起動してみる。
$ yarn storybook dev
@storybook/cli v7.0.0-beta.14
ERR! Starting in 7.0, react and react-dom are now required peer dependencies of Storybook.
ERR! https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#react-peer-dependencies-required
ERR!
ERR! It seems that you haven't run Storybook's CLI to upgrade to the latest version.
ERR! The upgrade command will install the required peer dependencies for you and will take
ERR! care of other important auto migrations as well.
ERR!
はぁーん? SB 動かすための React が Dependencies から PeerDependencies に変わった臭いな。
Vue なのにこれ入れるのちょっと癪だけどほんと必要?
エラーメッセージに普通に案内書いてあるんだなコレが。
まぁ React が必要なのは事実なので入れようじゃないの。
$ yarn add -D react react-dom
d160a3181cabde94ebb2c9c81ddb9db786263510
React のエラーが出なくなった。次は設定ファイルがないよってエラー。これは期待通り。
$ yarn storybook dev
ERR! Error: No configuration files have been found in your configDir (/Users/shingo.sasaki/vue-3-storybook-7/.storybook).
ERR! Storybook needs either a "main" or "config" file.
設定ファイルを作る。とりあえずこんなんでいいかな。
module.exports = {
framework: "@storybook/vue3",
stories: ["../src/**/*.stories.@(js|mdx)"],
};
なんだとー
$ yarn storybook dev
ERR! Error: Invalid value of @storybook/vue3 in the 'framework' field of Storybook config.
ERR!
ERR! Please run 'npx sb@next automigrate'
ERR!
ERR! See the v7 Migration guide for more information:
ERR! https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#framework-field-mandatory
ERR! at validateFrameworkName (/Users/shingo.sasaki/vue-3-storybook-7/node_modules/@storybook/core-common/dist/index.js:18:3976)
ERR! at buildDevStandalone (/Users/shingo.sasaki/vue-3-storybook-7/node_modules/@storybook/core-server/dist/cjs/build-dev.js:74:41)
ERR! at async withTelemetry (/Users/shingo.sasaki/vue-3-storybook-7/node_modules/@storybook/core-server/dist/cjs/withTelemetry.js:65:5)
ERR! at async dev (/Users/shingo.sasaki/vue-3-storybook-7/node_modules/@storybook/cli/dist/generate.js:441:440)
ERR! Error: Invalid value of @storybook/vue3 in the 'framework' field of Storybook config.
ここに書いてあった。
@storybook/vue3-vite
が正しいっぽい。
うん?プリセットないじゃん。 @storybook/vue3
じゃ入ってこないのかな?
WARN Failed to load preset: "@storybook/vue3-vite/preset"
ERR! Error: Cannot find module '@storybook/vue3-vite/preset'
同名のパッケージあるけど、バージョン止まってるし違うような。
npm のサイトで見るとαで止まってるように見えるけど、普通に最新版インストールできた。
yarn add -D @storybook/vue3-vite@7.0.0-beta.14
@storybook/vue-3
のほうはいらないっぽいな。
0982561c63e3fa22addc45a630a1b21f761b7a5c
設定ファイルはこう
module.exports = {
framework: "@storybook/vue3-vite",
stories: ["../src/**/*.stories.@(js|mdx)"],
};
403363d9af11777e49c59421bccc55a2a0c955be
よっしゃ Storybook 起動したぞ。当然ストーリーを1個も用意してないのでエラーは想定どおり。
空だからなんとも言えないけど爆速差を感じる。
Storybook で使用するコンポーネントを作ろう。
HelloWorld
コンポーネントがあるから、これをシンプルなカウンターに書き換えてみる。
<script setup>
import { ref } from "vue";
const props = defineProps({
initialCount: {
type: Number,
default: 0,
},
});
const count = ref(props.initialCount);
</script>
<template>
<div class="hello-world">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<style scoped>
.hello-world {
text-align: center;
color: #2c3e50;
}
</style>
こんなん。
最初の例でだけ使うからシンプルに。
259366b31fdbd09ae3928f85a1859e89343f6d0a
Story を作る。どこに Story ファイルを作るかは派閥が分かれそうだけど、今回は自作のコンポーネントだけじゃなくて、外部UIライブラリのコンポーネントに対して試してみるってのをやりたいから、 src/stories
ってディレクトリを作ろうかな。
ドキュメントに Vue2/3 JS/TS それぞれの CSF での記法が書いてあるのでそれに追従する。
import HelloWorld from "../components/HelloWorld.vue";
const meta = {
title: "HelloWorld",
component: HelloWorld,
};
export default meta;
export const Default = {
render: () => ({
components: { HelloWorld },
template: "<HelloWorld />",
}),
};
一応できた。今更だけど、デフォルトでダークモードなのか。
Story に render 関数定義させるんだ。
同じレンダーで複数のストーリーを用意したいときのお作法みたいなのあるかな。
a5948b9f9449b48a4cdffe67424f9a3b082bc640
addon-controls 入れてく。
$ yarn add -D @storybook/addon-controls@7.0.0-beta.14
アドオンを有効化
module.exports = {
framework: "@storybook/vue3-vite",
stories: ["../src/**/*.stories.@(js|mdx)"],
addons: ["@storybook/addon-controls"],
};
ストーリー側で args の注入。
import HelloWorld from "../components/HelloWorld.vue";
const meta = {
title: "HelloWorld",
component: HelloWorld,
};
export default meta;
export const Default = {
render: (args) => ({
components: { HelloWorld },
setup() {
return { args };
},
template: '<HelloWorld v-bind="args" />',
}),
args: {
initialCount: 0,
},
};
Controls から注入できるように
一つのコンポーネントに対して複数のストーリーを定義する場合は、メタ側にコンポーネントを定義して、各ストーリーでオーバーライドすれば良いみたい。
これ CSF のどのバージョンから出来たんだろ。知らなかった。
import HelloWorld from "../components/HelloWorld.vue";
const meta = {
title: "HelloWorld",
component: HelloWorld,
render: (args) => ({
components: { HelloWorld },
setup() {
return { args };
},
template: '<HelloWorld v-bind="args" />',
}),
};
export default meta;
export const Default = {};
export const WithInitialValue = {
args: {
initialCount: 5,
},
};
最初から TS で書いたほうが良いなと薄々感じてきたので、TS のセットアップを先にやっちゃう。
多分 TS と vue-tsc さえ入れればあとは ボイラーがどうにかしてくれる。
$ yarn add -D typescript vue-tsc
tsconfig も vite が生成するやつに従っておく。
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
.vue の型定義も追加
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
ここまで、最初からやるなら yarn create vite [name] --template vue-ts
にすれば自動でセットアップされると思う。
コンポーネントもTSにする。
<script setup lang="ts">
import { ref } from "vue";
const props = defineProps({
initialCount: {
type: Number,
default: 0,
},
});
const count = ref(props.initialCount);
</script>
<template>
<div class="hello-world">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<style scoped>
.hello-world {
background-color: #fff;
text-align: center;
color: #2c3e50;
}
.hello-world span,
button {
font-size: 4rem;
}
</style>
Story はこんな感じに。
import HelloWorld from "../components/HelloWorld.vue";
import type { Meta, StoryObj } from "@storybook/vue3";
type Story = StoryObj<typeof HelloWorld>;
const meta: Meta = {
title: "HelloWorld",
component: HelloWorld,
render: (args) => ({
components: { HelloWorld },
setup() {
return { args };
},
template: '<HelloWorld v-bind="args" />',
}),
};
export default meta;
export const Default: Story = {};
export const WithInitialValue: Story = {
args: {
initialCount: 5,
},
};
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
のせいで props を型安全に指定できないなぁ。
SFC 間での型推論は Volar で出来るから十分といえば十分かもしれないけど。
typegen 的なのでコンポーネントから props の型を取り出す手段があった気がするけど、まぁそれはそのうち。
「ストーリー」とか CSFについてざっとおさらい。
- 「ストーリー」はある状態のコンポーネントのレンダリングしたもの
- 一つのコンポーネントに対して、複数のストーリーを定義することが可能
- CSF (Component Story Format) は ESM の仕組みを使ってストーリーを記述するためのフォーマットで、
.stories.js
.stories.ts
の形式で書かれる
新バージョンの CSF 3
- CSF はストーリーを定義するために推奨されたフォーマットだが、ESM の仕組みに閉じているため、
Storybook
を超えて使用することができる -
export deafult
で、ストーリーにおける対象コンポーネントのメタデータ(title, parameters, decorators)を定義する- タイトルは全体でユニークである必要がある
- 多くのフィールドはアドオンで使用されるため、すべて省略可能な場合が多い
-
export
でストーリーを定義する- 明示しない限りは named export したオブジェクトはすべてストーリーとして扱う
- ストーリー名は変数名をもとに機械的に決まる
- 明示することも可能
-
default export
で定義したメタデータを、ストーリーレベルで上書きできる
CSF 3 だとかなりシンプルに書けるようになってた。
import Counter from "../components/Counter.vue";
import type { Meta, StoryObj } from "@storybook/vue3";
type Story = StoryObj<typeof Counter>;
const meta: Meta<typeof Counter> = {
component: Counter,
};
export default meta;
export const Default: Story = {};
アドオンについて
- アドオンは Storybook 本体には組み込まれていないが統合して Storybook を拡張できるツール
- Storybook のほとんどの機能はアドオンによって提供される
- 主要機能でさえも
- ドキュメンテーション
- アクセシビリティテスト
- インタラクティブコントロール
- アドオンは公式提供のものから、サードパーティのものまで多数ある
- https://storybook.js.org/integrations
- 主要機能でさえも
docs 普段全然使ってないので、改めて確認してみよう。
ドキュメントは
- ストーリーから自動生成するもの
- MDX で自由に記述するもの
- Doc Blocks を使って Storybook に埋め込むもの
があるっぽいな。
メタデータに tags: ['autodocs'],
を付与するとドキュメントページが自動生成される。
これ SB7 初登場か?
Cypress に魂を売ったせいで Play function もよくわかってないから確認する
Storybook's play functions are small code snippets that run once the story finishes rendering.
Play Function はストーリーのレンダリング後に自動で実行される。
だからテストを回すってイメージよりは、操作後の状態を作るってほうが正しいか。
フォームなら入力後とか、バリデーションエラーを出した後みたいな。
testing-library 自体全然使ったことなかったけど、Role とか Label みたいなユーザー視点での DOM 操作をしやすくするライブラリかな。