Nuxt.jsにBulmaを組み込んだら、Vuexストアが理解できた件

## はじめに

今流行りのJSフレームワークである[Nuxt.js](https://ja.nuxtjs.org/)（ベースは[Vue.js](https://jp.vuejs.org/index.html)）に、これまた今流行りのUIフレームワークである[Bulma](https://bulma.io/)を組み込む方法を試していたら、今までどんなに勉強してもイマイチよくわからなかった[Vuex](https://vuex.vuejs.org/ja/)ストアの役割がストンと腑に落ちたという**アハ体験**をしましたので、みなさんと共有したいと思います

## 前提条件

- macOS High Sierra v10.13.3
- node.js v9.7.1
- npm v5.7.1
- yarn v1.5.1

_node.js以下については、[Homebrew](https://brew.sh/index_ja)を使ってインストールしました_

## 新規プロジェクトを生成する

[create-nuxt-app](https://github.com/nuxt-community/create-nuxt-app)コマンドを使います

プロジェクト名は`avocado`とします。特に意味はありません

```bash
# これは以下のコマンド2つと同じ意味です
yarn create nuxt-app avocado

# 1/2 create-nuxt-appコマンドをグローバルにインストールする
yarn global add create-nuxt-app
# 2/2 create-nuxt-appコマンドを使って新規プロジェクトを生成する
create-nuxt-app avocado
```

![08.png](https://qiita-image-store.s3.amazonaws.com/0/48056/767f16af-46b7-1624-954b-f2112e4994aa.png)

create-nuxt-appコマンドがいくつか質問してきます。今回は以下のように答えました

> 1. プロジェクト名は？ => avocado
> 1. プロジェクトの一言説明は？ => My sublime Nuxt.js project
> 1. サーバーフレームワークは？ => 使わない
> 1. UIフレームワークは？ => Bulma
> 1. レンダリングモードは？ => ユニバーサルじゃなくてシングルページアプリケーション
> 1. axiosモジュールは使う？ => 使わない
> 1. eslintは使う？ => 使わない
> 1. 作者名は？ => Isamu Suzuki
> 1. パッケージマネージャは？ => npmじゃなくてyarn

_本当は、Bulmaはここでインストールせずに、後で自分で組み込んだほうがカスタマイズできるのでベターなんですが、今回は端折って、後で読むべき記事を紹介するにとどめます => [Nuxt.jsにBulmaを導入して変数を使ったカスタマイズを行う](https://qiita.com/mnmemo/items/5eb4fb8cbfe17670fd36)_

## 開発サーバーを起動する

```bash
cd avocado
yarn install
npm run dev
```

ブラウザで`localhost:3000`を開くと、この通りページを見ることができました

![02.png](https://qiita-image-store.s3.amazonaws.com/0/48056/f729ca8b-b548-7062-3568-f8c220f14d6e.png)

`Ctrl+C`で開発サーバーを止められますが、これから行うコーディング中も、いちいち開発サーバーを止める必要はなく、ファイルを保存した途端にブラウザがリロードしてくれます（ホットリローディング）

## Nuxt.jsアプリのディレクトリ構造

ざっくり説明すると、こんな感じになっています

```text
root
|--.nuxt/
|--assets/ ... SASS,JSのようなコンパイルされていないファイル
|--components/ ... Vue.jsのコンポーネントファイル
|--layouts/ ... レイアウトファイル
|--middleware/ ... アプリケーションのミドルウェア
|--node_modules/
|--pages/ ... アプリケーションのビュー及びルーティングファイル
|--plugins/ ... ルートのVue.jsアプリをインスタンス化する前に実行したいJSファイル
|--static/ ... 静的ファイルを置くとrootに配置される
|--store/ ... Vuexストアのファイル
`--nuxt.config.js ... Nuxt.jsのカスタム設定を記述する
```

今回使うディレクトリは、`pages`, `components`, `layouts`, `store`の4つです

最初にページを2つ追加し、次にナビゲーションバーをコンポーネントとしてつくり、そのナビゲーションバーをレイアウトファイルに組み込んで、最後にVuexストアファイルをつくります

## インデックスページ以外のページを作成する

`pages`ディレクトリに、`about.vue`と`contact.vue`の2つのファイルをつくります

```html:pages/about.vue
<template>
  <section class="hero is-primary is-bold">
    <div class="hero-body">
      <h1 class="title is-size-2">
        About
      </h1>
      <h2 class="subtitle is-size-4">
        Avocadoとは？
      </h2>
    </div>
  </section>
</template>
```

_`contact.vue`は、2行目の`is-primary`を`is-info`に変更した程度でほとんど同じなので省略します_

Nuxt.jsではファイルをつくるだけでページが出来上がります。ブラウザで、`localhost:3000/about`、`localhost:3000/contact`を開くとページを見ることができます

![03.png](https://qiita-image-store.s3.amazonaws.com/0/48056/a2853a08-c634-e66a-587d-a67659917c66.png)

## ナビゲーションバーのコンポーネントを作成する

```html:components/Navbar.vue
<template>
  <nav class="navbar is-white" role="navigation" aria-label="main navigation">
    <div class="navbar-brand">
      <div class="navbar-item">Avocado</div>
      <div class="navbar-burger" data-target="navMenu">
        <span></span>
        <span></span>
        <span></span>
      </div>
    </div><!-- navbar-brand END -->
    <div class="navbar-menu" id="navMenu">
      <div class="navbar-end">
        <nuxt-link to="/" class="navbar-item">トップ</nuxt-link>
        <nuxt-link to="/about" class="navbar-item">Avocadoとは？</nuxt-link>
        <nuxt-link to="/contact" class="navbar-item">お問い合わせ</nuxt-link>
      </div>
    </div><!-- navbar-menu END -->
  </nav>
</template>
```

このコンポーネントは、各ページにではなく、レイアウトファイルにとりつけます

```html:layouts/default.vue
<template>
  <div>
    <navbar/>
    <nuxt/>
  </div>
</template>

<script>
import Navbar from '~/components/Navbar.vue'

export default {
  components: {
    Navbar
  }
}
</script>
```

![04.png](https://qiita-image-store.s3.amazonaws.com/0/48056/fed3ac7b-e785-186c-d3ba-624aba4df17b.png)

## ハンバーガーメニューをトグルさせる

見ているブラウザの横幅を小さくすると、ナビゲーションメニューが折りたたまれ、ハンバーガーメニューだけになります。しかし、これをクリックしても何も起こりません！

![05.png](https://qiita-image-store.s3.amazonaws.com/0/48056/d2773269-b930-be45-4b6a-f701cb83c363.png)

そうBulmaはCSSファイルだけで出来ているUIフレームワークなので、トグルのような動きは範囲外なのです。自分で実装しないといけません

Bulma公式サイトの[Navbar解説ページ](https://bulma.io/documentation/components/navbar/)にも、以下のようなJSコードが実装例として載っています

```js
document.addEventListener('DOMContentLoaded', function () {

  // navbar-burger要素をすべて取得する
  var $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);

  // navbar-burger要素が1個以上あるならば
  if ($navbarBurgers.length > 0) {

    // それぞれにクリックイベントを追加する
    $navbarBurgers.forEach(function ($el) {
      $el.addEventListener('click', function () {

        // data-target属性からターゲット（navbar-menu）を取得し
        var target = $el.dataset.target;
        var $target = document.getElementById(target);

        // navbar-burger要素とnavbar-menuターゲットのクラスをトグルする
        $el.classList.toggle('is-active');
        $target.classList.toggle('is-active');

      });
    });
  }
});
```

これはとてもベーシックなJSコードですが、Vue.jsの流儀ではないので、どこに書いても動きませんし、どこに書けばいいかの答えもありません

Vue.jsの流儀で書くと、このようになります。ナビゲーションバーのコンポーネントのファイルで、`isMenuActive`というプロパティと`toggleMenu`というメソッドを定義して、`navbar-burger`要素と`navbar-menu`要素に取り付けます

```html:components/Navbar.vue
<template>
  <nav class="navbar is-white" role="navigation" aria-label="main navigation">
    <div class="navbar-brand">
      <div class="navbar-item">Avocado</div>
      <div class="navbar-burger" data-target="navMenu" @click="toggleMenu" :class="{'is-active': isMenuActive}">
        <span></span>
        <span></span>
        <span></span>
      </div>
    </div><!-- navbar-brand END -->
    <div class="navbar-menu" id="navMenu" :class="{'is-active': isMenuActive}">
      <div class="navbar-end">
        <nuxt-link to="/" class="navbar-item">トップ</nuxt-link>
        <nuxt-link to="/about" class="navbar-item">Avocadoとは？</nuxt-link>
        <nuxt-link to="/contact" class="navbar-item">お問い合わせ</nuxt-link>
      </div>
    </div><!-- navbar-menu END -->
  </nav>
</template>

<script>
export default {
  data: () => {
    return {isMenuActive: false}
  },
  methods: {
    toggleMenu () {
      this.isMenuActive = !this.isMenuActive
    }
  }
}
</script>
```

これでナビゲーションバーはトグルするようになりました、めでたし、めでたし、
で、終わればよかったのですが、すぐに「ページ遷移した後もメニューが出っぱなし」という問題に気づきます

![06.png](https://qiita-image-store.s3.amazonaws.com/0/48056/af43ac1a-64fb-ebdd-23c9-68b1c8794903.png)

ページ遷移したら`isMenuActive`プロパティは`false`に戻って欲しいのですが、このコンポーネントは全ページに登場するため、リセットがかからず、同じ状態のままページ遷移が終わってしまうようです

## Nuxt.jsアプリの動作を調べる

`<nuxt-link>`経由で特定のページに移動したときのNuxt.jsアプリの動作は以下のようになっています（一部省略あり）

> 1. `nuxt.config.js`ファイルを確認して、グローバルミドルウェアを実行する
> 1. レイアウトファイルのマッチングを確認して、親ページと子ページのミドルウェアを実行する
> 1. `asyncData()`メソッドと呼び出す。サーバーサイドで取得しレンダリングするため用いる
> 1. `fetch()`メソッドを呼び出す。ページレンダリング前にデータを入れるために用いる
> 1. 適切なデータをすべて格納したページをレンダリングする

ページコンポーネントだけが持っている`asyncData()`メソッドか`fetch()`メソッドで、
ナビゲーションバーコンポーネントに「isMenuActiveプロパティをリセットしろ」と伝えられればいいのですが、間にレイアウトファイルを挟んでしまっています。各ページにナビゲーションバーコンポーネントを埋め込んで、親子コンポーネントにするのも面倒くさいです

`isMenuActive`というたったひとつのプロパティを、どこからでも取得できて、どこからでも操作できるようにする方法はないのでしょうか？ そう、ここでVuexストアが登場します

## Vuexストアを設定する

[Vuex入門ページ](https://vuex.vuejs.org/ja/getting-started.html)の最初に登場するサンプルコードとほとんど変わらない以下のJSコードを`store`ディレクトリに書きます。Nuxt.jsアプリには最初からVuexがインストールされています。`state`は状態を、`mutations`はその状態を変更するメソッドを表します

```js:store/index.js
import Vuex from 'vuex'

const store = () => new Vuex.Store({
  state: {
    isMenuActive: false
  },
  mutations: {
    toggleMenu (state) {
      state.isMenuActive = !state.isMenuActive
    },
    resetMenu (state) {
      state.isMenuActive = false
    }
  }
})

export default store
```

ナビゲーションバーコンポーネントは以下のように書き換えます。`script`パートはざっくり削除です

```html:components/Navbar.vue
<!-- 5行目 before -->
<div class="navbar-burger" data-target="navMenu" @click="toggleMenu" :class="{'is-active': isMenuActive}">
<!-- 5行目 after -->
<div class="navbar-burger" data-target="navMenu" @click="$store.commit('toggleMenu')" :class="{'is-active': $store.state.isMenuActive}">

<!-- 11行目 before -->
<div class="navbar-menu" id="navMenu" :class="{'is-active': isMenuActive}">
<!-- 11行目 after -->
<div class="navbar-menu" id="navMenu" :class="{'is-active': $store.state.isMenuActive}">
```

そして、各ページコンポーネントに以下を追加します

```html
<script>
export default {
  fetch ({store}) {
    store.commit('resetMenu')
  }
}
</script>
```

素晴らしい！ ページ遷移後にトグルメニューが閉じるようになりました！

![07.png](https://qiita-image-store.s3.amazonaws.com/0/48056/b87ba6d3-7398-c43c-4632-2e487fd70978.png)

## 最後に

Vue.jsのシングルページコンポーネントに出会った時も「すごい！ わかりやすい！」と感動しましたが、今年に入ってからNuxt.jsを知り「完璧！ いろいろ作れそう！」と興奮しています。開発者・コミュニティの方々に感謝しつつ、自分もその一員になりたいなと思った次第です