はじめに
今流行りの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
create-nuxt-appコマンドがいくつか質問してきます。今回は以下のように答えました
- プロジェクト名は? => avocado
- プロジェクトの一言説明は? => My sublime Nuxt.js project
- サーバーフレームワークは? => 使わない
- UIフレームワークは? => Bulma
- レンダリングモードは? => ユニバーサルじゃなくてシングルページアプリケーション
- axiosモジュールは使う? => 使わない
- eslintは使う? => 使わない
- 作者名は? => Isamu Suzuki
- パッケージマネージャは? => npmじゃなくてyarn
本当は、Bulmaはここでインストールせずに、後で自分で組み込んだほうがカスタマイズできるのでベターなんですが、今回は端折って、後で読むべき記事を紹介するにとどめます => Nuxt.jsにBulmaを導入して変数を使ったカスタマイズを行う
開発サーバーを起動する
cd avocado
yarn install
npm run dev
ブラウザでlocalhost:3000
を開くと、この通りページを見ることができました
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.vue
とcontact.vue
の2つのファイルをつくります
<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
を開くとページを見ることができます
ナビゲーションバーのコンポーネントを作成する
<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>
このコンポーネントは、各ページにではなく、レイアウトファイルにとりつけます
<template>
<div>
<navbar/>
<nuxt/>
</div>
</template>
<script>
import Navbar from '~/components/Navbar.vue'
export default {
components: {
Navbar
}
}
</script>
ハンバーガーメニューをトグルさせる
見ているブラウザの横幅を小さくすると、ナビゲーションメニューが折りたたまれ、ハンバーガーメニューだけになります。しかし、これをクリックしても何も起こりません!
そう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
要素に取り付けます
<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>
これでナビゲーションバーはトグルするようになりました、めでたし、めでたし、
で、終わればよかったのですが、すぐに「ページ遷移した後もメニューが出っぱなし」という問題に気づきます
ページ遷移したらisMenuActive
プロパティはfalse
に戻って欲しいのですが、このコンポーネントは全ページに登場するため、リセットがかからず、同じ状態のままページ遷移が終わってしまうようです
Nuxt.jsアプリの動作を調べる
<nuxt-link>
経由で特定のページに移動したときのNuxt.jsアプリの動作は以下のようになっています(一部省略あり)
nuxt.config.js
ファイルを確認して、グローバルミドルウェアを実行する- レイアウトファイルのマッチングを確認して、親ページと子ページのミドルウェアを実行する
asyncData()
メソッドと呼び出す。サーバーサイドで取得しレンダリングするため用いるfetch()
メソッドを呼び出す。ページレンダリング前にデータを入れるために用いる- 適切なデータをすべて格納したページをレンダリングする
ページコンポーネントだけが持っているasyncData()
メソッドかfetch()
メソッドで、
ナビゲーションバーコンポーネントに「isMenuActiveプロパティをリセットしろ」と伝えられればいいのですが、間にレイアウトファイルを挟んでしまっています。各ページにナビゲーションバーコンポーネントを埋め込んで、親子コンポーネントにするのも面倒くさいです
isMenuActive
というたったひとつのプロパティを、どこからでも取得できて、どこからでも操作できるようにする方法はないのでしょうか? そう、ここでVuexストアが登場します
Vuexストアを設定する
Vuex入門ページの最初に登場するサンプルコードとほとんど変わらない以下のJSコードをstore
ディレクトリに書きます。Nuxt.jsアプリには最初からVuexがインストールされています。state
は状態を、mutations
はその状態を変更するメソッドを表します
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
パートはざっくり削除です
<!-- 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>
素晴らしい! ページ遷移後にトグルメニューが閉じるようになりました!
最後に
Vue.jsのシングルページコンポーネントに出会った時も「すごい! わかりやすい!」と感動しましたが、今年に入ってからNuxt.jsを知り「完璧! いろいろ作れそう!」と興奮しています。開発者・コミュニティの方々に感謝しつつ、自分もその一員になりたいなと思った次第です