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

はじめに

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

前提条件

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

node.js以下については、Homebrewを使ってインストールしました

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

create-nuxt-appコマンドを使います

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

# これは以下のコマンド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

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

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

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

開発サーバーを起動する

cd avocado
npm run dev

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

02.png

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

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

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

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.vuecontact.vueの2つのファイルをつくります

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-primaryis-infoに変更した程度でほとんど同じなので省略します

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

03.png

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

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>

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

layouts/default.vue
<template>
  <div>
    <navbar/>
    <nuxt/>
  </div>
</template>

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

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

04.png

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

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

05.png

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

Bulma公式サイトのNavbar解説ページにも、以下のような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要素に取り付けます

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

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

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

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

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

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

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

Vuexストアを設定する

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

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パートはざっくり削除です

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}">

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

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

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

07.png

最後に

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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.