Part1 https://qiita.com/senku/items/07c3e2859ac90c03867a
Part2 ここ
Part3 https://qiita.com/senku/items/08d547eda2c6ff818108
前回はVuex以外の主要なライブラリをStorybookに対応させる手順を紹介しました。
今回はVuexの話をします。ちょっと大変。
Summary
-
Vuex.Store
の引数のObjectをexportしておいて - モックに書き換えて
- Storybookに使わせる
Vuexとのたたかい
とりあえずvue add vuex
で入れます。
$ vue add vuex
src/store/index.js
にVuex Storeの定義が書かれています。
Vuex.Store()
の引数をexportしつつ、適当なストアにまるっと書き換えます。namespaceにも対応しておくよ。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = {
modules: {
test: {
namespaced: true,
state: {
name: ''
},
mutations: {
setName: (state, payload) => {
state.name = payload
}
},
actions: {
async setFoo({ commit }) {
commit('setName', 'Foo')
},
async setBar({ commit }) {
commit('setName', 'Bar')
}
},
getters: {
name: state => state.name
}
}
}
}
export default new Vuex.Store(store)
src/views/Home.vue
あたりでVuexを使えるようにしてみましょう。
diff --git a/src/views/Home.vue b/src/views/Home.vue
index fc2e940..807ed24 100644
--- a/src/views/Home.vue
+++ b/src/views/Home.vue
@@ -1,11 +1,13 @@
<template>
<div class="home">
+ <p>{{ name }}</p>
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
+import { mapGetters } from 'vuex'
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
@@ -13,6 +15,12 @@ export default {
name: 'home',
components: {
HelloWorld
+ },
+ computed: {
+ ...mapGetters('test', ['name']),
+ },
+ created() {
+ this.$store.dispatch('test/setFoo')
}
}
</script>
npm run serve
で確認するとFoo
が表示されます。バッチリ。
Home用のStoryも作っておきましょう。いつものパターン。
import { storiesOf } from "@storybook/vue";
import defaultDecorator from "@/stories/defaultDecorator";
import Home from "@/views/Home.vue";
storiesOf("Home", module)
.addDecorator(defaultDecorator)
.add("test", () => {
return {
components: { Home },
template: `
<home />
`
};
});
Vuex × Storybook
いつものようにこのStoryもタダでは動きません。
まずはVue Routerと同じようにconfig/storybook/config.js
でVue.use()
します。
diff --git a/config/storybook/config.js b/config/storybook/config.js
index d8dcd31..db6a520 100644
--- a/config/storybook/config.js
+++ b/config/storybook/config.js
@@ -2,8 +2,10 @@
import { configure } from '@storybook/vue'
import Vue from 'vue'
import Router from 'vue-router'
+import Vuex from 'vuex'
Vue.use(Router)
+Vue.use(Vuex)
const req = require.context('../../src/stories', true, /.stories.js$/)
decoratorでStoreを渡してもいいのですが、Storyごとに異なるモックデータを使いたいケースを考慮して、Storyで渡すようにしておきます。
diff --git a/src/stories/Home.stories.js b/src/stories/Home.stories.js
index 2902105..802e067 100644
--- a/src/stories/Home.stories.js
+++ b/src/stories/Home.stories.js
@@ -1,12 +1,16 @@
import { storiesOf } from "@storybook/vue";
import defaultDecorator from "@/stories/defaultDecorator";
import Home from "@/views/Home.vue";
+import Vuex from "vuex";
+import { store } from "@/store/index.js";
storiesOf("Home", module)
.addDecorator(defaultDecorator)
.add("test", () => {
+ const mockStore = new Vuex.Store(store);
return {
components: { Home },
+ store: mockStore,
template: `
<home />
`
StoryでもガッツリFoo
が表示されるようになります。
gettersのモック
getters で取得する値をストーリーによって変えたいことがありますよね。モックで対応しましょう。
Deep copyを利用したいので、とりあえずlodashを入れます。Deep copyの実装面倒だからね。
$ npm install --save-dev lodash
Vuex Storeをモックするための独自のクラスをひとつ作って、gettersを上書きするためのユーティリティ関数を作ってしまいます。後のことも考えて適当に共通化しておきます。
また、オリジナルのstoreオブジェクトを改変すると他のStoryで困る可能性があるので、cloneDeep
で複製したObjectを使います。
ここがこの記事の見どころですよ。それにしてもコードフォーマットバラバラだな。
import cloneDeep from "lodash/cloneDeep";
import Vuex from "vuex";
import { store as originalStore } from "@/store/index.js";
export default class mockVuex {
constructor() {
// モックするVuex StoreをDeep copy
this.mockStore = cloneDeep(originalStore);
}
/**
* Vuex Storeを生成して返却する
*
* @return {Vuex.Store} store
*/
getMockStore() {
return new Vuex.Store(this.mockStore);
}
/**
* mockStoreのプロパティを上書きする
*
* @param {string} field actions/getters
* @param {string} nameSpace 'foo/bar/baz' 形式
* @param {string} propertyName
* @param {function} newFunction
*/
mockProperty(field, nameSpace, propertyName, newFunction) {
let modules = this.mockStore.modules
// nameSpace に従ってモジュールの階層を掘る
const diggingNameSpaces = nameSpace.split('/')
const lastNameSpace = diggingNameSpaces.pop()
for (const diggingNameSpace of diggingNameSpaces) {
modules = modules[diggingNameSpace].modules
}
modules[lastNameSpace][field][propertyName] = newFunction
}
/**
* getters を上書きする
*
* @param {string} nameSpace 'foo/bar/baz' 形式
* @param {string} propertyName
* @param {function} newFunction
*/
mockGetters(nameSpace, propertyName, newFunction) {
this.mockProperty("getters", nameSpace, propertyName, newFunction);
}
}
Storyを修正します。VuexはmockVuex
に隠蔽したので参照不要になりました。
mockGetters()
関数を使って、gettersが返す値を変更します。
その後getMockStore()
で取得したVuex StoreをStoryで使います。
diff --git a/src/stories/Home.stories.js b/src/stories/Home.stories.js
index 802e067..d1e5e74 100644
--- a/src/stories/Home.stories.js
+++ b/src/stories/Home.stories.js
@@ -1,16 +1,16 @@
import { storiesOf } from "@storybook/vue";
import defaultDecorator from "@/stories/defaultDecorator";
+import mockVuex from "@/stories/mockVuex";
import Home from "@/views/Home.vue";
-import Vuex from "vuex";
-import { store } from "@/store/index.js";
storiesOf("Home", module)
.addDecorator(defaultDecorator)
.add("test", () => {
- const mockStore = new Vuex.Store(store);
+ const vuex = new mockVuex();
+ vuex.mockGetters('test', 'name', () => 'Baz')
return {
components: { Home },
- store: mockStore,
+ store: vuex.getMockStore(),
template: `
<home />
`
test/name
のgettersがモックされ、Baz
が表示されるようになりました。
実運用では、モック用のデータを別に用意してimportするのがいいと思います。モックサーバとかに使ってるデータがあればそれで。
actionsのモック
actionsもモックしたいですよね。
src/stories/mockVuex
にmockActions()
関数を追加します。共通化してるのでただのラッパー関数です。
/**
* actions を上書きする
*
* @param {string} nameSpace 'foo/bar/baz' 形式
* @param {string} propertyName
* @param {function} newFunction
*/
mockActions(nameSpace, propertyName, newFunction) {
this.mockProperty('actions', nameSpace, propertyName, newFunction)
}
Storyを編集して、Home.vue
のcreated()
で呼ばれていたtest/setFoo
アクションをモックして、qux
をセットしてみます。
diff --git a/src/stories/Home.stories.js b/src/stories/Home.stories.js
index d1e5e74..27a8fef 100644
--- a/src/stories/Home.stories.js
+++ b/src/stories/Home.stories.js
@@ -7,7 +7,7 @@ storiesOf("Home", module)
.addDecorator(defaultDecorator)
.add("test", () => {
const vuex = new mockVuex();
- vuex.mockGetters('test', 'name', () => 'Baz')
+ vuex.mockActions('test', 'setFoo', ({state}) => state.name = "qux")
return {
components: { Home },
store: vuex.getMockStore(),
ガッツリqux
が表示されます。やったね。
画面表示に必要なデータをモックするのはgettersのモックでだいたいなんとかなるので、actionsをモックするケースはHTTP通信をしたりStorybookで動作しない副作用を制御したい場合がほとんどだと思います。
その場合、actionsはmockVuexのコンストラクタでまとめて無効化してしまうのが楽ちんです。
constructor() {
// モックするVuex StoreをDeep copy
this.mockStore = cloneDeep(originalStore);
// 以下使わないActionを無効化
const NOOP = () => {}
this.mockActions('test', 'setBar', NOOP)
}
もうVuexのモックも怖くないですね。
明日からVue.jsコンポーネントのStoryを量産できるはずです。
おまけ:コンポーネントのdataを操作する
Storyから$refs
経由でコンポーネントにアクセスすればいいです。mounted()
のタイミングがよさげ。
.add("edit name", () => {
return {
components: { Home },
template: `
<home ref="foo"/>
`,
mounted() {
this.$refs.foo.bar = 'baz' // Homeコンポーネントのbarに'baz'をセット
}
};
});