Storybook はインタラクティブに開発し、ユーザー インターフェイス コンポーネントをアプリケーションを実行しないでテストできます。Storybook は独自の Webpack 構成でコンポーネント ライブラリとしての役目を果たすので、プロジェクト依存関係や要件を気にせずに個別に開発できます。
本投稿では、チームメートの Steve Hobbs 氏が作成した人気のカンバンボード プログレッシブ Web アプリケーション(PWA)(GitHub で入手可能)を使って、Storybook を既存の Vue.js プロジェクトに統合する方法を学んでいきます。このプロセスは新しい Vue プロジェクトにも使えます。
カンバンボードによるプロジェクトを実行する
以下のコマンドを実行して、カンバンボードによるプロジェクトを起動してローカルで実行します。
git clone git@github.com:elkdanger/kanban-board-pwa.git
cd kanban-board-pwa/
npm install
npm run dev
アプリケーションが起動しているか確認するには、ブラウザーで http://localhost:8080
を開きます。
Storybook を使うためにアプリを実行する必要はありません。ご希望であれば、それを停止してブラウザー タブを閉じます。
最後に、kanban-board-pwa
プロジェクトをご希望の IDE またはコード エディタで開きます。
Storybook を Vue で設定する
現行の作業ディレクトリとして kanban-board-pwa
を次のコマンドで実行し、npm
を使って Storybook をインストールします。
npm i --save-dev @storybook/vue
Storybook は vue
と babel-core
がインストールされている必要があります。kanban-board-pwa
は Vue CLI を使って作成されたので、これら2つの依存関係はすでにインストールされています。
最後に、Storybook を簡単に始めて実行できる npm
スクリプトを作ります。package.json
ファイルの scripts
セクションの下に、次を追加します。
{
// ...
"scripts": {
// ...
"storybook": "start-storybook -p 9001 -c .storybook"
}
// ...
}
-p
コマンド引数は Storybook がローカルで実行するポート、この場合は 9001
を指定します。-c
コマンド引数は Storybook に構成設定の .storybook
ディレクトリを探すように伝えます。これは次で実行します。
Storybook を Vue で構成する
Storybook は多くの異なる方法で構成されます。ベストプラクティスとして、その構造は .storybook
と呼ばれるディレクトリに保存します。ルート フォルダーの下にそのディレクトリを作ります。
.
├── .babelrc
├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .git
├── .gitignore
├── .idea
├── .postcssrc.js
├── .storybook // Storybook config directory
├── Dockerfile
├── LICENSE
├── README.md
├── build
├── config
├── index.html
├── node_modules
├── package-lock.json
├── package.json
├── screenshots
├── src
└── static
.storybook
内にすべての構成設定を保留する config.js
ファイルを作ります。ここで、Storybook 対応の Vue アプリケーションを作る重要な要素を定義します。
Vue を定義する
src/main.js
ファイルと同様に、vue
をインポートする必要があります。config.js
を次のように更新します。
// .storybook/config.js
import Vue from 'vue';
Vue コンポーネントを定義する
丁度 Vue プロジェクトで行ったように、グローバル カスタムコンポーネントの Vue.component
でインポートしてグローバルに登録する必要があります。config.js
インポートを更新し、TaskLaneItem
コンポーネントを登録します。
// .storybook/config.js
import Vue from 'vue';
// カスタムコンポーネントをインポートします。
import TaskLaneItem from '../src/components/TaskLaneItem';
// カスタムコンポーネントを登録します。
Vue.component('item', TaskLaneItem);
これは、Storybook は Vue アプリケーションとは個別に実行するので、実行しなければなりません。ローカルに登録されたコンポーネントは自動的に持ち込まれるのでご注意ください。これらは Vue コンポーネント オブジェクトの components
プロパティを使って登録されたコンポーネントです。例えば:
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
});
ComponentA
および ComponentB
は #app
の下にローカルに登録されます。
TaskLaneItem
はグローバル カスタムコンポーネントなので Storybook 内に独立してインスタンスを作成するためにそれをインポートして Vue で登録しなければなりません。
ストーリーを構成し読み込む
@storybook/vue
から configure
メソッドをインポートして Storybook を実行し、ストーリー(ストーリーについては以下で学びます)を読み込むためにそれを実装します。
// .storybook/config.js
import { configure } from '@storybook/vue';
import Vue from 'vue';
// カスタムコンポーネントをインポートします。
import TaskLaneItem from '../src/components/TaskLaneItem';
// カスタムコンポーネントを登録します。
Vue.component('item', TaskLaneItem);
function loadStories() {
// 必要なだけのストーリーを要求できます。
}
configure(loadStories, module);
Storybook はツールをテストするのと同じように機能します。Config.js
ファイルが configure
メソッドを実行し、それによって loadStories
という関数と module
を引数として取ります。loadStories
はそのボディに定義されたストーリーがあります。
ストーリーは指定された状態でコンポーネントを説明します。コンポーネントには active
、inactive
、loading
などの各状態のストーリーを書き込みます。その後、Storybook ではインタラクティブなコンポーネントライブラリでその指定された状態のコンポーネントをプレビューできます。これは後ほど設定していきます。
より良いプロジェクト マネジメントのため、このストーリーはコンポーネントの隣りに保存するのが理想的です。src
内に stories.js
ファイルを生成して使用するすべてのストーリーをホストします。それから stories.js
ファイルを使って、次のように config.js
ファイルにそのストーリーを素早くロードします。
// .storybook/config.js
// ...
function loadStories() {
// 必要なだけのストーリーを要求できます。
require('../src/stories');
}
configure(loadStories, module);
loadStories
を実行するとき、Storybook は src/stories.js
にあるすべてのストーリーをインポートして実行します。このように Storybook の構成よりもその実装にとことん集中することで config.js
ファイルのメンテナンスがずっと簡単になります。現時点ではすべてのアクションが src/stories.js
ファイル内で発生します。
すべてのカスタムコンポーネントと Vue プラグインは configure() を呼び出す前に登録します。
"「Storybook はインタラクティブに開発し、ユーザー インターフェイス コンポーネントをアプリケーションを実行しないでテストできます。それと VueJS を統合する方法について学びましょう。」"
Tweet This
Vue の Storybook ストーリーを書き込む
Storybook ストーリーを書いて、コンポーネントライブラリを現実のものにしましょう。src/stories.js
に移動し、次のように始めます。
// src/stories.js
import { storiesOf } from '@storybook/vue';
storiesOf('TaskLaneItem', module);
ここまでで storiesOf
メソッドをインポートしました。このメソッドはコンポーネントのストーリーを作るのに役立ち、この場合、TaskLaneItem
コンポーネントを使ってその役割を果たします。TaskLaneItem
は Vue.component
ですでにグローバルに登録されているので、それを src/stories.js
にインポートする必要はありません。Using, Storybook の宣言型言語を使って TaskLaneItem
のストーリーを次のように伝えます。
storiesOf('TaskLaneItem', module);
このプロセスを実際の書籍で考えると、これは書籍の製本や表紙になります。では、これらストーリーをページに埋めます。これを宣言的に add
メソッドを使って行います。
// src/stories.js
import { storiesOf } from '@storybook/vue';
storiesOf('TaskLaneItem', module).add('Default TaskLaneItem', () => ({
template: '<item :item="{id: 10, text: \'This is a test\'}"></item>'
}));
add
はストーリーがある書籍に章を追加するのと同じような役目をします。各章にタイトルを付けます。この場合、Default TaskLaneItem
というタイトルのストーリーを作ります。add
はそのストーリータイトルと、段階的なコンポーネントをレンダリングする関数を引数として取ります。この場合、コンポーネントは template
オプションを指定した Vue コンポーネント定義オブジェクトです。
重要な注意事項:コンポーネントを表す、テンプレートで使用されるタグ名は Vue.component
で .storybook/config.js
にコンポーネントを登録したときに使用した名前です。
// カスタムコンポーネントを登録します。
Vue.component('item', TaskLaneItem);
この Vue コンポーネント定義オブジェクトは id
と text
を控えめな props
にしてモジュラーを増やすためにリファクタリングできます。
// src/stories.js
import { storiesOf } from '@storybook/vue';
storiesOf('TaskLaneItem', module).add('Default TaskLaneItem', () => ({
data() {
return {
item: { id: 10, text: 'This is a test' }
};
},
template: '<item :item="item"></item>'
}));
ストーリーを書く基盤ができました。では Storybook を実行してその機能を確認しましょう。
"「Storybook はコンポーネント ライブラリにコンパイルされたストーリーを通して個別のコンポーネントのプレゼンテーションと状態を説明します。VueJS の Storybook ストーリーを書く方法を学びましょう。」"
Tweet This
Storybook を実行する
コンピューターで次のコマンドを実行します。
npm run storybook
すべての実行が成功したら、コンソールに次のメッセージが表示されます。
info Storybook started on => http://localhost:9001/
ブラウザーで URL http://localhost:9001/
を開きます。ロードしたら、ご自分の Storybook をお楽しみください。
この状態では TaskLaneItem
コンポーネントはあまり良くありません。それはライブ アプリケーションで見たときと比較してテキストが読みにくいからです。
TaskLaneItem
コンポーネントの定義を保留する src/components/TaskLaneItem.vue
ファイルを開きます。背景色が定義されている以外はスタイル指定がされていないことが分かります。このコンポーネントの完全なスタイル指定は Bootstrap から来ます。よって、次のステップは Storybook に Bootstrap を使用させることです。
カスタムヘッドタグを Storybook に加える
index.html
を開くと、kanban-board-pwa
アプリは <head>
要素内で異なるタグを使っていることが分かります。Storybook で正確にコンポーネントをプレビューするための2つの関連タグは Bootstrap と FontAwesome をプロジェクトに紹介する <link>
タグです。
Storybook はそのアプリとは個別に実行するので、index.html
内で定義されたこれらタグを見たり使用したりすることはできません。ソリューションとして、.storybook
構成ディレクトリの下に preview-head.html
ファイルを作成して必要な <link>
タグを次のように追加します。
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/4.0.0-beta.2/superhero/bootstrap.min.css">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
それを停止して Storybook を再起動してから npm run storybook
を再び実行します。これで Storybook が更新した構成を使用できるようになります。再びブラウザーで [
を開くと、同じスタイル設定でずっと良い TaskLaneItem
をフルバージョンのアプリでプレビューすることができます。
コンポーネント変更を Storybook でプレビューする
これまで既存の定義と構成でコンポーネントを段階的に実行してきました。Storybook の最も強力な機能はアプリケーションを実行せずに、変更をライブで見ることができることです。TaskLaneItem
オレンジ色内のテキストの色にしたいとするとき、そのパディングを増やし、オレンジ色の境界線を追加します。いかがですか?TaskLaneItem.vue
にある <style>
タグ内で変更します。
// src/components/TaskLaneItem.vue
<template>
// ... Template definition
</template>
<script>
// ... Script definition
</script>
<style>
.card.task-lane-item {
background: #627180;
border: solid orange 5px;
}
.card-title {
color: orange;
}
.card-block {
padding: 20px;
}
</style>
そのファイルを保存すると、Storybook を更新したコンポーネントのプレビューをすぐに見ることができます!
個別のコンポーネントの外観や印象を実験することで時間の節約にもなります。UI パズル全体を組み立てる代わりに、ひとつのデータだけをプレビューできます。ただし Storybook はコンポジションの中でコンポーネントを実行できます。
次に進む前に、変更した TaskLaneItem
のスタイルをリバースするだけでかなり良くなります。
Storybook で Vuex を使用する
ここでは簡単に提示するコンポーネントをプレビューする方法を学びましょう。では、さらに複雑な構造で、データと一緒にハイドレートするために Vuex ストアを使って TaskLane
コンポーネントのストーリーを作りましょう。まず .storybook/config.js
を更新してTaskLane
をインポートして登録します。
// .storybook/config.js
// ...
// カスタムコンポーネントをインポートします。
import TaskLaneItem from '../src/components/TaskLaneItem';
import TaskLane from '../src/components/TaskLane';
// ...
// カスタムコンポーネントを登録します。
Vue.component('item', TaskLaneItem);
Vue.component('lane', TaskLane);
// ...
次に、src/stories.js
に戻って TaskLane
のストーリーを作ります。
// src/stories.js
// ... TaskLaneItem stories
storiesOf('TaskLane', module).add('Default TaskLane', () => ({
data() {
return {
doneItems: [
{ id: 10, text: 'This is a test' },
{ id: 12, text: 'This is another test' },
{ id: 14, text: 'This is yet another a test' },
{ id: 16, text: 'This is one more test' }
]
};
},
template: `
<div class="col-md">
<lane id="done" title="Done" :items="doneItems"></lane>
</div>
`
}));
これは前に TaskLaneItem
で実行した方法と同様で、主な違いは col-md クラスで div
内に lane
を折り返し doneItems
の配列を items
プロパティを通して TaskLane
にパスすることです。
前に、
Vue.component
でコンポーネントを登録するために使った名前なので、コンプレート内のコンポーネントタグ名としてlane
を使っていました。
src/components/TaskLane.vue
ファイルで見たように TaskLane
は TaskLaneItem
をローカルで登録するので、Storybook は自動的に TaskLaneItem
をこのストーリーの TaskLane
のスコープに持ち込みます。Default TaskLane
ストーリーのコンポーネント定義オブジェクト内に components
プロパティを作る必要はありません。
そのファイルを保存します。その変更はそのページをリフレッシュするまで Storybook に正しく表示されないかもしれないので、リフレッシュしてください。TaskLane
メニュー項目をクリックしてそれを拡大しそれから Default TaskLane
をクリックします。TaskLane
コンポーネントのプレビューが次のように表示されます。
TaskLane
はかんばんボードにタスクをリストするために TaskLaneItem
を使用します。これら TaskLaneItem
コンポーネントはこのアプリで構成されているように、TaskLane
コンポーネントの間にドラッグ アンド ドロップします。Storybook TaskLane
プレビューが完全にインタラクティブになったことが分かります。レーン内に表示されている項目はドラッグして動かすことができます。
ただし、レーン内にコンポーネントをドラッグすると、その位置は固定されません。開発者コンソールを開くと、次のようなエラーが見えます。
vue.esm.js:591 [Vue warn]: Property or method "$store" is not defined on the instance but referenced during render.
どうしたのでしょうか?src/components/TaskLane.vue
ファイルにある TaskLane
の定義に移動します。このコンポーネントはこのプロジェクトで作成された vuex
ストアを使用して項目を更新することが分かります。
// src/components/TaskLane.vue
// ... Template tag
<script>
// ... Script imports
export default {
// ... Other properties
computed: {
itemCount() {
if (!this.items) return '';
if (this.items.length === 1) return '1 task';
return `${this.items.length} tasks`;
},
draggables: {
get() {
return this.items;
},
set(items) {
this.$store.commit('updateItems', {
items,
id: this.id
});
}
}
}
};
</script>
// ... Style tag
vuex
ストアのインスタンスは $store
を通じて存在します。それはレーンをタスクレーン項目が属するものに更新する updateItems
変更をコミットするために使用されます。ただし、ストーリー内の TaskLane
のインスタンスはこのストアの存在に非対応です。さらに重要なのは、項目の配列をレンダリングするために手動で TaskLane
にパスしています。なぜこのような問題が発生するのでしょうか。
kanban-board-pwa
アプリケーションのアーキテクチャはアプリケーションの状態に対する信頼できる唯一の情報源として Vuex ストアを使用します。データをレンダリングする必要があるコンポーネントはストアから入手しなければなりません。Vuex ストアは再有効化されているので、ストアのストラクチャが変更されると、影響を受けたデータにサブスクライブされたコンポーネントは更新されます。
問題は Storybook サンドボックス内にあり、ストアはデータで初期化されていません。また、TaskLane
はその items
データをその親コンポーネントである KanbanBoard
から取得します。KanbanBoard
コンポーネント定義は src/components/KanbanBoard.vue
でご覧いただけます。この親コンポーネントは TaskLane
コンポーネントにパスした算定されたプロパティを作るストアをクエリします。
// src/components/KanbanBoard.vue
<template>
<div class="board">
<div class="row">
<div class="col-md">
<task-lane id="todo" title="Todo" :items="todoItems"></task-lane>
</div>
<div class="col-md">
<task-lane id="inProgress" title="In progress" :items="inProgressItems"></task-lane>
</div>
<div class="col-md">
<task-lane id="done" title="Done" :items="doneItems"></task-lane>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import TaskLane from './TaskLane'
export default {
name: 'KanbanBoard',
components: {
'task-lane': TaskLane
},
computed: mapState({
todoItems: s => s.items.todo,
inProgressItems: s => s.items.inProgress,
doneItems: s => s.items.done
})
};
</script>
個別に Tasklane
インスタンスを作成することが Storybook を使用する目的です。ストアからの項目プロパティを Tasklane
にパスできるために KanbanBoard
全体のインスタンスを作ることはソリューションでも、ストアがまだ空だからという理由でもありません。この問題を解決する最初のステップは Storybook プロジェクトが作成されたときに、項目をストアに追加することです。
.storybook/config.js
に移動し、次のようにファイルを更新します。
// ...その他のインポート
import store from '../src/store';
// ..カスタムコンポーネントをインポートします。
store.commit('addItem', { text: 'This is a test' });
store.commit('addItem', { text: 'This is another test' });
store.commit('addItem', { text: 'This is one more test' });
store.commit('addItem', { text: 'This is one more test' });
// ...カスタムコンポーネントを登録します。
// ...
ここで Storybook プロジェクトが構築されたら、ストアはこれら4つの項目で読み込まれます。ただし、Storybook プロジェクトのファイルに変更すると、その項目はブラウザーのローカル ストレージに保存されているので、重複の項目が表示されます。これは Storybook がホストされているブラウザー ウィンドウを再度読み込みして解決します。
各項目の id
はそのストアによって自動的に作成されます。次に、ストア項目をプロパティとして TaskLane
コンポーネントにパスするために src/stories.js
内の Default TaskLane
ストーリーを更新します。
// src/stories.js
import { mapState } from 'vuex';
import { storiesOf } from '@storybook/vue';
import store from '../src/store';
// ...TaskLaneItem ストーリー
storiesOf('TaskLane', module).add('Default TaskLane', () => ({
computed: mapState({
items: s => [...s.items.todo, ...s.items.inProgress, ...s.items.done]
}),
store,
template: `
<div class="col-md">
<lane id="todo" title="Todo" :items="items"></lane>
</div>
`
}));
ストアから計算されたゲッター関数を生成するために src/components/KanbanBoard.vue
内にあるロジックと同様に、[
を使います。
項目を追加するとき、kanban-board-pwa
アプリの機能を理解していることが重要です。KanbanBoard
テンプレートに戻ると、各レーンに id
値が与えられていることが分かります。
// src/components/KanbanBoard.vue
<template>
<div class="board">
<div class="row">
<div class="col-md">
<task-lane id="todo" title="Todo" :items="todoItems"></task-lane>
</div>
<div class="col-md">
<task-lane id="inProgress" title="In progress" :items="inProgressItems"></task-lane>
</div>
<div class="col-md">
<task-lane id="done" title="Done" :items="doneItems"></task-lane>
</div>
</div>
</div>
</template>
その id
はストアの配列記号と(意図的に)一致するので 、データのオーバーヘッドがあまりなく、リストを劇的に更新する簡易対応になり、このデモのスコープにはぴったりのソリューションです。
作業を保存して Storybook をリフレッシュします。次をご覧ください。
項目をレーン内に並べ替え、これらが新しい位置を覚えているかご覧ください。
vuex
ディレクトリを使う必要はありませんが、ストア内に一からストアを作成したい場合、vuex
をインポートしそれを次のようにインストールします。
// .storybook/config.js
import Vuex from 'vuex'; // Vue plugins
// Vue プラグインをインストールします。
Vue.use(Vuex);
それから新しいストアのインスタンスを次のようにストーリー内に作ります。
import Vuex from 'vuex';
storiesOf('Component', module)
.add('Default Component', () => ({
store: new Vuex.Store({...}),
template: `...`
}));
vuex
のような必須 Vue プラグインは Vue.use を使ってインストールする必要があります。
複合コンポーネントを Storybook に組み立てる
項目をレーン内にドラッグ アンド ドロップしてそれらがどのように並べ替えられるかを学ぶことはよかったですが、項目をレーンの間にドラッグ アンド ドロップできることはさらに素晴らしいことです。では、フルバージョンのアプリに存在する「Todo」、「In Progress」、「Done」の3つのレーンを表すストーリーを作りましょう。これは KanbanBoard
コンポーネントのインスタンスを作らないで完了します。 src/stories.js
に移動し、Three TaskLanes
ストーリーを作ります。
// src/stories.js
// ... インポートします
// ...TaskLaneItem ストーリー
storiesOf('TaskLane', module)
.add('Default TaskLane', () => ({
computed: mapState({
items: s => [...s.items.todo, ...s.items.inProgress, ...s.items.done]
}),
store,
template: `
<div class="col-md">
<lane id="todo" title="Todo" :items="items"></lane>
</div>
`
}))
.add('Three TaskLanes', () => ({
computed: mapState({
todoItems: s => s.items.todo,
inProgressItems: s => s.items.inProgress,
doneItems: s => s.items.done
}),
store,
template: `
<div class="row">
<div class="col-md">
<lane id="todo" title="Todo" :items="todoItems"></lane>
</div>
<div class="col-md">
<lane id="inProgress" title="In progress" :items="inProgressItems"></lane>
</div>
<div class="col-md">
<lane id="done" title="Done" :items="doneItems"></lane>
</div>
</div>
`
}));
ここでは、レーン項目の各カテゴリ用に算出されたゲッター関数を生成するために mapState
を使う KanbanBoard
に存在する同じ算出されたプロパティを使います。3つのレーンはストアの配列シンボルに一致する「Todo
」、「InProgress
」、「Done
」で id
値として作成されます。 この作業を保存し、Storybook に戻ります。TaskLane
メニューのタブをクリックし、それから Three Tasklanes
をクリックします。そうすると、次のようなものが表示されます。
重複項目があったら Storybook ウィンドウをリフレッシュします。
ここで、項目をレーン間にドラッグ アンド ドロップします。各項目はその新規レーンとレーンの新規位置を記憶します。良い機能ですね?
下部から上部にコンポーネントを作る練習をしましょう。小さなk提示可能なコンポーネントから始め、そのスタイルやコンテンツを調整し、それからプレゼンテーションのコンポーネント構成に依存する大きめのコンポーネントに移動します。Storybook では UI パズルの真にモジュラー非依存型データのような各コンポーネントを処理することに集中できます。
大規模なチームにとってのもうひとつの利点は、コンポーネント ライブラリを作ることでプロジェクト内だけでなく、効果的にブランディングを行うために一貫した外観と印象を必要とする組織のプロジェクト全体でコンポーネントを再利用できることです。
まとめ
vuex
のような Vue プラグインにアクセスしたり使用したりしながら Storybook のコンポーネント ライブラリを通してインタラクティブな方法で基本的なプレゼンテーションや複雑なコンポーネントをプレビューする方法を学びました。Storybook はボタンをクリックして機能を起動したり UI テストのためなどに、コンポーネントを通してトリガーされるアクションをプレビューするためにも使うことができます。これらは高度な使用事例です。イベント処理やテストを扱うために、Vue 用に Storybook を拡張することについてのブログ投稿についてご関心のある方は以下のコメント欄でお知らせください。
Auth0 では、Storybook を広範囲にわたって使ってきました。Storybook でコンポーネント ライブラリを作ることについてご覧になりたい方、私たちが見つけた利点を知りたい方は「React および Storybook でコンポーネント ライブラリを設定する」 についての投稿をご覧ください。
Auth0:絶対に ID で妥協しない
アプリケーションの構築についてご関心がありますか?そのためにはユーザー認証を設定する必要があります。一から実装するのは複雑で時間がかかるかもしれませんが、Auth0 を使うと数分でできます。詳細については、https://auth0.com をご覧いただくか、@auth0 on Twitter をフォローしてください。