Nuxt.js( + Vuetify) で pug, scss な環境に Storybook を入れた

Nuxt.js、便利ですね。生の Vue.js のようにいろいろな config を自分でやらなくてよいのが素敵です。
ただ、全てのツールに対応しているわけではないので、最終的には自力で解決する必要があり、 やはり Webpack から避けて通れない現実を突きつけてきます…。

Storybook も自力で解決する必要のあるツールの1つでした。

※たぶん Stylus や Less でも同じ問題が起きるのでそちらで設定辛い人も見てください。~~でも Stylus 使ってるような人はこの手の問題でハマらないイメージ。あくまでイメージ~~

# 前提条件

`@storybook/vue` と `@storybook/cli` を使う方法がありますが、この手順では CLI を使います（最新に追従できそうなので）。

# 1. Nuxt.js に pug と scss を導入

導入済みの人は**[本題](https://qiita.com/akagire/items/bc24ba13df4bf7145dd4#%E3%81%93%E3%81%93%E3%81%8B%E3%82%89%E6%9C%AC%E9%A1%8C)**以降を見てね。

## Nuxt.js

https://ja.nuxtjs.org/guide/installation/
このページを参考に即導入。このタイミングで Vuetify もインストールしています。

## pug

Vue ファイルのテンプレートで Ruby で言うところの slim みたいな記法で書けます。

```index.vue
<template lang="pug">
  .container
    .block Hello,
      span.block--colored World
</template>
```

インストールは以下のコマンドでOKですが、おすすめなインストール手順は、まず vue ファイルを書いて、ビルド時に表示されるエラーを見て足りないパッケージをインストールするのが間違いないです。（というのも、以前は `jade` という名前だったことと、ローダーの名前も以前は `pug-loader` でした）

``` bash
npm install --save-dev pug pug-plain-loader
```

## SCSS

CSS がネストでかけるようになります。

```index.vue
<style lang="scss" scoped>
.block {
  background-color: #fff;
  &__colored { /* .block__colored に変換されます */
    color: red;
  }
}
</style>
```

pug と同様に足りないパッケージをインストール。このへんも足りないパッケージが変わるかもしれないので適宜調整してください。

```bash
# 省略したくなっちゃった
npm i -D node-sass sass-loader
```

# ここから本題

早速 Storybook を導入します。[GitHub の Getting Started](https://github.com/storybooks/storybook#getting-started) にも記載されているように、インスコ不要でパッケージが実行できる `npx` で導入します。
[npx についてはこちらを参照](https://qiita.com/akagire/items/384eb60b6d0b284a7527)。だいぶ普及してきたのを感じます。

```bash
npx -p @storybook/cli sb init
```

導入終わりました。簡単でした。では起動しましょう。

```bash
npm run storybook
```

これでビルドが走って、 `localhost:xxxx`　に飛ばされて Storybook の　Welcome ページが表示されました。 Storybook は導入成功です。

## Vuetify を使ったコンポーネントを Storybook に追加

_注意:ここに記載の方法だけではうまくいきません。後に記載の方法との組み合わせが必要です_

具体例として、ヘッダーコンポーネントを `components/MyHeader.vue` に用意します。

```components/MyHeader.vue
<template lang="pug">
header
  v-toolbar(
    dark
    color="primary"
  )
    v-toolbar-side-icon
    v-spacer
    v-toolbar-title Title
    v-spacer
    v-btn(icon)
      v-icon search
</template>

<style lang="scss" scoped>
header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
}
</style>
```

これをストーリーに追加します。

```stories/index.js
import { storiesOf } from '@storybook/vue';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';

import MyButton from './MyButton';
import Welcome from './Welcome';
// 追記
import PartsHeader from '../components/MyHeader';

storiesOf('Components', module).add('Header', () => ({
  components: { MyHeader },
  template: '<v-app><my-header /></v-app>',
}));

// 以下、デフォルトのストーリー
```

これで Storybook を起動します。

```bash
npm run storybook

# ビルドが走って...
WARN Broken build, fix the error below.
```

これだと起動しません。

### Webpack の設定

_注意：ここからは順次、正常に動作するようになります_

起動しないのは冒頭でも触れたとおり Storybook は Nuxt の設定を引き継がないからです。なので、以下のドキュメントを参考に、 Storybook 用の `webpack.config.js` を作ります。
[Custom Webpack Config](https://storybook.js.org/docs/configurations/custom-webpack-config/)

Full Control Mode のほうが強そうですが、使い慣れた Extend Mode 記法で書きます…。

```.storybook/webpack.config.js
const path = require('path');

module.exports = {
  module: {
    rules: [
      // scss 関連のローダー設定
      {
        test: /\.s?css$/,
        loaders: ['style-loader', 'css-loader', 'sass-loader'],
      },
      // pug のローダー設定
      {
        test: /\.pug$/,
        loaders: 'pug-plain-loader',
      }
    ],
  },
};
```

これで `npm run storybook` すると、起動はしますが、「title」 とだけ表示されて見た目の確認になりません。

### storybook/config.js の設定

素の Vue.js でやるように、 Vue のインスタンスに Vuetify を注入する必要がありました。

```.storybook/config.js
import { configure } from '@storybook/vue';
// 追加
import Vue from 'vue';
import Vuetify from 'vuetify';
import 'vuetify/dist/vuetify.css'; // →これ重要

// 依存注入
Vue.use(Vuetify);

// 以下、デフォルトのまま
// automatically import all files ending in *.stories.js
const req = require.context('../stories', true, /.stories.js$/);
// 以下略
```

これで `npm run storybook` で起動…ほぼ表示されましたが、 `v-icon` がアイコンではなくテキスト表示されてしまっています。

### 対応フォントの読み込み

この記事を参考にさせていただきました。
[Storybook (Vue) で Vuetify を使う際にアイコン（<v-icon>）が表示されない件 - Qiita](https://qiita.com/sugasaki/items/e76f20384b484f6f2aa3)

```.storybook/preview-head.html
<link
  href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
  rel="stylesheet"
/>
```

~~まるパクリですｗ~~

これで再度 `npm run storybook` します。これで無事表示されました。

### 毎回 `<v-app>` で囲わなくていいようにする

表示するコンポーネントが1つだけならこのままでも良いですが、今後ストーリーに追加されるコンポーネントが増えると、

```stories/index.stories.js
storiesOf('Components', module).add('Header', () => ({
  components: { MyHeader },
  template: '<v-app><my-header /></v-app>', // ここが冗長
}));
```

**template** が冗長になりますね。上位レイヤーで常に `<v-app>` で囲われるように修正します。

```.storybook/config.js
// addDecorator を追加
import { configure, addDecorator } from '@storybook/vue';
import Vue from 'vue';
import Vuetify from 'vuetify';
import 'vuetify/dist/vuetify.css';

Vue.use(Vuetify);

// ここが重要
addDecorator(() => ({
  template: '<v-app><story /></v-app>',
}));

// automatically import all files ending in *.stories.js
const req = require.context('../stories', true, /.stories.js$/);
function loadStories() {
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);
```

で、ストーリーは下記のように書き換えられました。

```stories/index.stories.js
// 略
storiesOf('Components', module).add('Header', () => ({
  components: { PartsHeader },
  template: '<parts-header />',
}));
// 略
```

また `npm run storybook` で起動します。見た目に変化がないので、うまく設定できました〜！

# ハマったポイント

`webpack.config.js` を設定したのに、 `vuetify/dist/vuetify.css` を　import するとうまく動かず、`Module parse failed: Unexpected character '@' (1:0)` というエラーが出ており、頭を抱え、30分悩みました。

1. `css-loader` を入れてなかった
2. webpack.config.js 見直した

```.storybook/webpack.config.js
// 抜粋（修正前)
{
  test: /\.?scss$/, // ここ正規表現間違っとるやんけ😇
  loaders: ['style-loader', 'css-loader', 'sass-loader'],
}
```

以上です。快適な Vue ライフを！