EJS de Storybook入門

おはようございます、こんにちは、こんばんわ。

今回はEJSでの`Storybook`導入と`Storybook`の簡単な使い方を書きます。
`Storybook`はUIコンポーネントとページを分離して構築するためのオープンソースツールです。
[Storybook公式サイト](https://storybook.js.org/)

##前置き
「Storybook 入門」とかで調べると`React`やら`Nuxt`やらへの導入を前提とした記事が溢れています。
コンポーネント化を行うならばもちろんそれらのフレームワークを使うのが1番ですが、`HTML`のテンプレートエンジンもフレームワークほどではないとはいえ、できない事もないです。
また、同時に2つのことを学ぶより1つのことを学ぶ方が楽だろうと思い、弊社で使われている「`EJS`」への導入を試みました。
`Storybook`はアドオンが強力なので、ぜひ最後まで読んで実践してみてください。

##ファイル構成
```html:ファイル構成
root/
　  ├ dest/ <!-- srcの出力ファイル -->
　  ├ src/ <!-- コンパイル・トランスパイル前提で編集するファイル -->
　  │  ├ ejs/ <!-- .ejsを格納 -->
　  │  └ scss/ <!-- .scssを格納 -->
　  │
　  ├ static/ <!-- コンパイルしない静的ファイル -->
　  ├ node_modules/
　  └ package.json

```

##Storybook導入
`Storybook`は`Vue.js`や`React`環境下だと`CLI`でバコッと導入できたりするのですが、`EJS`の環境下での導入なので手動で導入を進めます。

まずは`HTML`用の`Storybook`をインストールします。

```shell
$ npm install @storybook/html
```

次は`Storybook`用の設定ファイル・フォルダを用意します。
`root/`直下に`.storybook`フォルダを作成します。

```html:ファイル構成
root/
　  ├ .storybook/  <!-- new!! -->
　  ├ dest/ 
　  ├ src/ 
　  │  ├ ejs/ 
　  │  └ scss/ 
　  │
　  ├ static/ 
　  ├ node_modules/
　  └ package.json

```
作成した`.storybook`フォルダの中に設定ファイルを2ファイル(`main.js`,`preview.js`)を作成していきます。

```js:.storybook/main.js
const path = require('path');

module.exports = {
  webpackFinal: async (config, { configType }) => {

    config.module.rules.push();
    
    return config;
  },
  stories: ['../stories/**/*.stories.@(js|mdx)']
};
```
```js:.storybook/preview.js
import { addDecorator } from '@storybook/html'
```
もう一息です。
`package.json`に起動用の`script`を記述します。

```json:package.json
"scripts": {
    "storybook": "start-storybook -p 6006"
 }
```
(`6006`のポート番号だと都合の悪い方は自由な番号で大丈夫です)

あとは正常に起動すれば一旦導入は完了です。

```shell
$ npm run storybook
```
以下の画面が表示されれば成功です！
![スクリーンショット 2021-11-26 14.16.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177358/39644eb3-4b2d-68e3-e08c-42ce0b184a90.png)


##コンポーネントを作成
コンポーネントの作成場所は以下になります。

```html:ファイル構成
root/
　  ├ .storybook/
　  ├ dest/ 
　  ├ src/ 
　  │  ├ ejs/ 
　  │  │  └ _partials/ 
　  │  │       └ _components/   <!-- _components/配下に作成 -->
　  │  │
　  │  └ scss/ 
　  │
　  ├ static/ 
　  ├ node_modules/
　  └ package.json

```
試しにボタンコンポーネントを作成してみましょう。

```_components/_button.ejs
<% //初期値を設定%>
<% if (typeof disabled === 'undefined') { var disabled = false; } %>
<% if (typeof type === 'undefined') { var type = 'button'; } %>
<% if (typeof label === 'undefined') { var label = ''; } %>
<% if (typeof modefireClass === 'undefined') { var modefireClass = ''; } %>
<% if (typeof jsClass === 'undefined') { var jsClass = ''; } %>
<% //初期値を設定%>

<button <%= disabled ? 'disabled' : '' %> type=<%= type %> class="<%= modefireClass %> <%= jsClass %>" ><%= label %></button>

```
以下の5つの設定をできるように作成しました。

- disabled
- type
- label
- modefireClass
- jsClass

余談ですが、初期値を設定を設定してあげないと`EJS`に怒られてしまうので、初期値は必須になります。

##EJSを読み込めるようにする
`EJS`のローダーがあるので、インストールします。

```shell
$ npm install ejs-compiled-loader
```
インストール後は`.storybook/main.js`に`EJS`ローダー用の設定を記述するだけです。

```js:.storybook/main.js
const path = require('path');

module.exports = {
  webpackFinal: async (config, { configType }) => {

    config.module.rules.push(
      {
        test: /\.ejs$/,
        loaders: ['ejs-compiled-loader'],
        // 読み込む予定のEJSのディレクトリを指定する
        include: path.resolve(__dirname, '../src/ejs/_partials/_components/') 
      }
    );
    
    return config;
  },
  stories: ['../stories/**/*.stories.@(js|mdx)']
};
```
完了！

##storiesの作成
作成したボタンコンポーネントを`Storybook`に表示させるため、`storie/`に設定ファイル(`button.stories.js`)を作成します。
`xxx.stories.js`
`xxx`部分は自由ですが、わかりやすいように表示するコンポーネントと名前を統一します。

```js:storie/button.stories.js
import button from '../src/ejs/_partials/_components/_button.ejs'; // コンポーネントを読み込む

export default {
  title: 'button', // storybookでリストに表示されるタイトル名
  component: button // importしたコンポーネント
};

export const nomarl = () => {
  // コンポーネントに渡す変数を宣言
  const label = 'Click Here';
  const type = 'button';
  const disabled = false;
  const modefireClass = '';
  const jsClass = '';

  return button({ label, type, disabled, modefireClass, jsClass }); // 各変数を引数に入れてコンポーネントを表示
};
```
ここまで終わって`Storybook`を起動するとコンポーネントが表示されるはずです。
![スクリーンショット 2021-11-26 15.04.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177358/53937af8-3b09-4b52-029f-e42748c8e7cd.png)
これにて`stories`の作成は完了になります。
コンポーネントが複数ある場合は1ファイルで完結する事もできると思いますが、コンポーネントごとに設定ファイルを作成するのをお勧めします。

##StorybookにSCSSを読み込ませる
`CSS`を読み込んでもいいですが、コンポーネント用の`SCSS`は大体インポートして使われると思うので、そのまま読み込めるように`SCSS`に対応します。
まずは各ローダーをインストール

```shell
$ npm install style-loader@2.0.0 css-loader@5.2.6 sass-loader@10.0.0
```
各ローダーが最新バージョンだと動かないらしいので、こちらの`issue`を参考にバージョン指定しています。（鬼ハマりポイント）
https://github.com/storybookjs/presets/issues/195
`sass-loader`などバージョンを下げているので、すでに`node-sass`、`sass(dart-sass)`などをインストールされている方はバージョンの見直しをしてください。
今回は`node-sass`を入れます。

```shell
$ npm install node-sass@4.14.1
```
インストール後は`.storybook/main.js`にローダー用の設定を記述します。

```js:.storybook/main.js
const path = require('path');

module.exports = {
  webpackFinal: async (config, { configType }) => {

    config.module.rules.push(
      {
        test: /\.ejs$/,
        loaders: ['ejs-compiled-loader'],
        include: path.resolve(__dirname, '../src/ejs/_partials/_components/') 
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader'],
        // 読み込む予定のSCSSのディレクトリを指定する
        include: path.resolve(__dirname, '../src/scss/css/')
      }
    );
    
    return config;
  },
  stories: ['../stories/**/*.stories.@(js|mdx)']
};
```
`sass(dart-sass)`を使っている方は`sass-loader`で`option`の記述をする必要があるので、ご注意を。
これで`SCSS`をインポートできるようになりました！
各ファイルに必要な`SCSS`を読み込んでいきましょう。
ますは`storie/button.stories.js`

```js:storie/button.stories.js
import button from '../src/ejs/_partials/_components/_button.ejs';
import '../src/scss/css/_partials/_components/_button.scss'; // 必要SCSSを適宜読み込む

export default {
  title: 'button',
  component: button
};

export const nomarl = () => {
  const label = 'Click Here';
  const type = 'button';
  const disabled = false;
  const modefireClass = '';
  const jsClass = '';

  return button({ label, type, disabled, modefireClass, jsClass });
};
```
全てのコンポーネントへ共通で読み込みたいファイルがある場合は`.storybook/preview.js`で読み込めばOKです。

```js:.storybook/preview.js
import { addDecorator } from '@storybook/html';
import '../src/scss/css/bundle.scss';
```
`Storybook`を起動すると、`SCSS`が反映されているはずです。

##Storybookにアドオンを入れる
アドオンを入れてない`Storybook`はネタの乗っていない寿司みたいなものです。
アドオンを入れることでさらに強力なツールになります。
今回は以下4つのアドオンを追加します。

- [@storybook/addon-a11y](https://storybook.js.org/addons/@storybook/addon-a11y)
    - アクセシビリティチェックをしてくれます。
- [@storybook/addon-knobs](https://storybook.js.org/addons/storybook-addon-knobs-color-options)
    - コンポーネントに渡す値をUIを用いて変更できるようにしてくれます。
- [@storybook/addon-notes](https://storybook.js.org/addons/@storybook/addon-notes/)
    - マークダウン形式で説明文を記述できるようになります。
- [@storybook/addon-storysource](https://storybook.js.org/addons/@storybook/addon-storysource)
    - `stories.js`のソースコードをコンポーネントと合わせてブラウザから見ることができます。

まずはインストール

```shell
$ npm install @storybook/addon-a11y @storybook/addon-knob @storybook/addon-notes @storybook/addon-storysource
```
各ファイルに必要な記述をしていきます。

```js:.storybook/main.js
const path = require('path');

module.exports = {
  webpackFinal: async (config, { configType }) => {

    config.module.rules.push(
      {
        test: /\.ejs$/,
        loaders: ['ejs-compiled-loader'],
        include: path.resolve(__dirname, '../src/ejs/_partials/_components/') 
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader'],
        include: path.resolve(__dirname, '../src/scss/css/')
      }
    );
    
    return config;
  },
  stories: ['../stories/**/*.stories.@(js|mdx)'],
  addons: [ // アドオンは追加したらここに記述する
    '@storybook/addon-a11y', 
    '@storybook/addon-knobs',
    '@storybook/addon-storysource',
    '@storybook/addon-notes'
  ],
};
```
```js:.storybook/preview.js
import { addDecorator } from '@storybook/html';
import { withA11y } from '@storybook/addon-a11y'; //全てのコンポーネントで反映させるため、ここでimport
import '../src/scss/css/bundle.scss';

addDecorator(withA11y);
```

@storybook/addon-knobsは覚えることがちょっとあるので、公式ドキュメントを参照しながら触ってみてください。

```js:storie/button.stories.js
import { withKnobs, text, select, boolean } from '@storybook/addon-knobs'; // 渡す変数の指定方法によって記述が変わります。
import button from '../src/ejs/_partials/_components/_button.ejs';
import '../src/scss/css/_partials/_components/_button.scss';

const readme = ` // @storybook/addon-storysource用の記述
## Props
- label
  - 初期値: ''
- type
  - 初期値: 'button'
- disabled
  - 初期値: false
- modefireClass
  - modefire用のクラス付与
  - 初期値: ''
- jsClass
  - JavaScript用のクラス付与
  - 初期値: ''

## 関連コンポーネント
なし
`;

export default {
  title: 'button',
  component: button,
  decorators: [withKnobs],
  parameters: {
    notes: readme // readme読み込み
  }
};

export const nomarl = () => {
  const label = text('Text', 'Click Here'); // @storybook/addon-knobs用の記述
  const type = select('type', { // @storybook/addon-knobs用の記述
    button: 'button',
    submit: 'submit',
    reset: 'reset'
  });
  const disabled = boolean('disabled', false); // @storybook/addon-knobs用の記述
  const modefireClass = text('modefireClass', ''); // @storybook/addon-knobs用の記述
  const jsClass = text('jsClass', ''); // @storybook/addon-knobs用の記述

  return button({ label, type, disabled, modefireClass, jsClass });
};
```

これでStorybookの画面から設定変数の値を触れたり、コンポーネントの説明を参照することができるようになりました。
お気に入りのアドオンでカスタマイズして最強のStorybookを目指しましょう！