NuxtプロジェクトにStorybookとmswを導入する
前提
Nuxt v2.15.8を利用しています。
Storybookの導入
NuxtプロジェクトへのStorybook導入は @nuxtjs/storybook
が便利です。
公式ドキュメント:https://storybook.nuxtjs.org/
以下のようなメリットがあります。
Zero configuration
Nuxt webpack configuration
Nuxt plugins support
Story discovery from nuxt modules
Nuxt components support
Storybook Generate
Hot reload support
インストール・設定
公式ドキュメントのinstallationに従い進めます。
yarn add --dev @nuxtjs/storybook
.gitignore
へ下記を追加
.nuxt-storybook
storybook-static
Storybookでは .storybook
ディレクトリ配下の main.js
preview.js
などで各種設定を行えますが、 @nuxtjs/storybook
では nuxt.config.js
の storybook
セクションで同様の設定を行えます。
export default {
storybook: {
// Options
}
}
起動
yarn nuxt storybook
余談:yarn buildに失敗する
筆者の環境では、 @nuxtjs/storybook
の導入後に yarn build
( nuxt-ts build
)に失敗するようになっていました。
ValidationError: Invalid options object. PostCSS Loader has been initialized using an options object that does not match the API schema.
- options has an unknown property 'order'. These properties are valid:
object { postcssOptions?, execute?, sourceMap?, implementation? }
これは postcss-loader
のバージョンに依存する問題で
@nuxtjs/storybook
では postcss-loader
の4系が入るが、Nuxt自体は3系を利用していることが原因でした。
4系ではオプションの指定方法が変わっています。 (参考)
今回は明示的に postcss-loader
の3系を入れることで対応しました。
"postcss-loader": "3.0.0",
mswの導入
msw(Mock Service Worker)はService Worker APIを利用して、APIリクエストをモックできるライブラリです。
内部で外部APIを叩いているコンポーネントをStorybookに追加する際、mswを利用することで、各Storyで任意のレスポンスをモックすることが可能になります。
筆者の環境ではREST, GraphQLでのリクエストをおこなっていますが、mswではどちらもモック可能です。
msw-storybook-addon
というStorybookプラグインが用意されています。
インストール・設定
公式ドキュメントの手順に従い進めます。
yarn add msw msw-storybook-addon -D
public
にservice workerを生成
npx msw init public/
次にaddonの設定を行いますが、 ./storybook/preview.js
を用意する必要があります。
しかし、今回は @nuxtjs/storybook
を利用しているので .storybook
ディレクトリがありません。
せっかくのzero-configですが、今回はejectをしてManual Setupを行います。
(筆者の環境では nuxt.config.js
を利用した設定はうまくいきませんでしたが、ejectせずに設定できる方法もあるかもしれません。)
yarn nuxt storybook eject
生成された ./storybook/preview.js
に以下を追記します。
import { initialize, mswDecorator } from 'msw-storybook-addon';
// Initialize MSW
initialize();
// Provide the MSW addon decorator globally
export const decorators = [mswDecorator];
storiesファイルでのAPIモック
サンプルコードは公式ドキュメントのものを利用しています。
以下のようにコンポーネント内でfetchしているファイルがあるとします。
<!-- YourPage.vue -->
<template>
<div v-if="!loading && data && data.subdocuments.length">
<PageLayout :user="data.user">
<DocumentHeader :document="data.document" />
<DocumentList :documents="data.subdocuments" />
</PageLayout>
</div>
<p v-if="loading">
Loading...
</p>
<p v-if="error">
There was an error fetching the data!
</p>
</template>
<script>
import { ref } from 'vue';
import PageLayout from './PageLayout';
import DocumentHeader from './DocumentHeader';
import DocumentList from './DocumentList';
export default {
name: 'DocumentScreen',
setup() {
const data = ref(null);
const loading = ref(true);
const error = ref(null);
fetch('https://your-restful-endpoint')
.then((res) => {
if (!res.ok) {
error.value = res.statusText;
}
return res;
})
.then((res) => res.json())
.then((requestData) => {
data.value = requestData;
loading.value = false;
})
.catch(() => {
error.value = 'error';
});
return {
error,
loading,
data,
};
},
};
</script>
この場合、storiesファイルでは以下のようにしてAPIをモックできます。
// YourPage.stories.js
import { rest } from 'msw';
import DocumentScreen from './YourPage.vue';
export default {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/vue/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'DocumentScreen',
component: DocumentScreen,
};
//👇The mocked data that will be used in the story
const TestData = {
user: {
userID: 1,
name: 'Someone',
},
document: {
id: 1,
userID: 1,
title: 'Something',
brief: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
status: 'approved',
},
subdocuments: [
{
id: 1,
userID: 1,
title: 'Something',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
status: 'approved',
},
{
id: 2,
userID: 1,
title: 'Something else',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
status: 'awaiting review',
},
{
id: 3,
userID: 2,
title: 'Another document',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
status: 'approved',
},
{
id: 4,
userID: 2,
title: 'Something',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
status: 'approved',
},
],
};
const PageTemplate = () => ({
components: { DocumentScreen },
template: '<DocumentScreen />',
});
export const MockedSuccess = PageTemplate.bind({});
MockedSuccess.parameters = {
msw: [
rest.get('https://your-restful-endpoint/', (_req, res, ctx) => {
return res(ctx.json(TestData));
}),
],
};
export const MockedError = PageTemplate.bind({});
MockedError.parameters = {
msw: [
rest.get('https://your-restful-endpoint/', (_req, res, ctx) => {
return res(ctx.delay(800), ctx.status(403));
}),
],
};
GraphQLの場合も同様です。
<!-- YourPage.vue -->
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">There was an error fetching the data!</div>
<div v-if="!loading && data && result.subdocuments.length">
<PageLayout :user="data.user">
<DocumentHeader :document="result.document" />
<DocumentList :documents="result.subdocuments" />
</PageLayout>
</div>
</template>
<script>
import PageLayout from './PageLayout';
import DocumentHeader from './DocumentHeader';
import DocumentList from './DocumentList';
import gql from 'graphql-tag';
import { useQuery } from '@vue/apollo-composable';
export default {
name: 'DocumentScreen',
setup() {
const { result, loading, error } = useQuery(gql``);
return {
result,
loading,
error,
};
},
};
</script>
// YourPage.stories.js
import DocumentScreen from './YourPage.vue';
import WrapperComponent from './ApolloWrapperClient.vue';
import { graphql } from 'msw';
export default {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/vue/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'DocumentScreen',
component: DocumentScreen,
};
//👇The mocked data that will be used in the story
const TestData = {
user: {
userID: 1,
name: 'Someone',
},
document: {
id: 1,
userID: 1,
title: 'Something',
brief: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
status: 'approved',
},
subdocuments: [
{
id: 1,
userID: 1,
title: 'Something',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
status: 'approved',
},
{
id: 2,
userID: 1,
title: 'Something else',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
status: 'awaiting review',
},
{
id: 3,
userID: 2,
title: 'Another document',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
status: 'approved',
},
{
id: 4,
userID: 2,
title: 'Something',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
status: 'approved',
},
],
};
const PageTemplate = () => ({
components: { DocumentScreen, WrapperComponent },
template: `
<WrapperComponent>
<SampleGraphqlComponent />
</WrapperComponent>
`,
});
export const MockedSuccess = PageTemplate.bind({});
MockedSuccess.parameters = {
msw: [
graphql.query('AllInfoQuery', (req, res, ctx) => {
return res(ctx.data(TestData));
}),
],
};
export const MockedError = PageTemplate.bind({});
MockedError.parameters = {
msw: [
graphql.query('AllInfoQuery', (req, res, ctx) => {
return res(
ctx.delay(800),
ctx.errors([
{
message: 'Access denied',
},
])
);
}),
],
};
起動
/public
にservice workerを用意しているので、 -s public
オプションが必要です。
npm run start-storybook -s public
参考資料など
Discussion