Vue.js + Vue Router + VuexでシンプルなSPAテンプレートを作ってみる

Vue.js + Vue Router + Vuexで、シンプルなSPAサイトを構築するテンプレートを作ってみたので、その時の作業メモです。

下準備

プロジェクトを追加

まずは、Vue.jsのプロジェクトを作成します。cdnで読み出す方法もありますが、今回はvue-cliを使ってコマンドラインで作成しました。

vue create vue-project

Vue CLI v3.5.5
? Generate project in current directory? Yes


Vue CLI v3.5.5
┌───────────────────────────┐
│  Update available: 3.6.3  │
└───────────────────────────┘
? Please pick a preset: default (babel, eslint)


Vue CLI v3.5.5
.
.
.

エラーがおきなければOKです。

vue-routerとvuexを追加する

SPAでWebページのようなルーティングをするには、Vue Routerが必須なのでインストールします。

また、共通状態管理機能があったほうが後々便利なので、Vuexもインストールしておきます。

npm install --save-dev vue-router vuex
もしくは
yarn add vue-router vuex --dev

自分は、面倒なのでVue UIからボタンでポチポチインストールしました。

とりあえずサーバーアップ

ここで一旦nodeサーバーで状態確認しておきます。

vue-cli-service serve
もしくは
yarn serve

ポート変更とか楽なので、Vue UIからサーブすると楽です。

こんな画面が出てくればOKです。

デフォルトでHOME、ABOUTというリンクができていて、ここをクリックするとページ遷移ができます。

Google Botにしっかり認識されるかが心配な場合は、この段階でURL検査などでGoogle Botがどう認識するのかをチェックしてもいいでしょう。

作り込み

ここからは、実際にSPAにするためのベース作りをしていきます。

目標は、

  1. アプリのような高速動作
  2. URLはしっかりWEBサイト
  3. titleやmeta、ogpなどにも対応
  4. Google Botにもしっかり認識されるように

というところを目指していきます。

index.htmlの改修

まずは、ベースとなるindex.htmlをSEOに対応したものにしておきます。

vue createで作成された後のデフォルトでは、

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>テストサイト</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but sportsstyle.net doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

と、とてもシンプルなものなので、まずはHTMLヘッダを改修します。

  <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# website: http://ogp.me/ns/websaite#">
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <meta name="description" content="">
    <link rel="icon" href="https://cdn.example.com/favicon.ico">
    <meta property="og:type" content="website" />
    <meta property="og:title" content="" />
    <meta property="og:description" content="" />
    <meta property="og:site_name" content="" />
    <meta property="og:image" content="" />
    <meta property="og:image:width" content="" />
    <meta property="og:image:height" content="" />
    <meta property="og:url" content="" />
    <title>テストサイト</title>
  </head>

これで、SEOに最低限必要なmeta要素とogpタグが追加されます。デフォルト値は入れても構いませんが、今回はすべてVue.js側で制御するようにしました。

faviconについては、SPAとしてホーム画面に追加してもらうことを考えるともっと細かくする必要がありますが、今回は一旦省略します。

ページのパーツ(コンポーネント)化

続いて、Vue.jsらしく、サイト内をパーツに分割していきます。

大きな枠組みとしては、

  • header
  • main
  • sub
  • footer

という4ブロックにわけます。

componentsディレクトリに、パーツのmainを除く3ファイルを用意します(例:Header.vue)。mainの部分は、ルーティング時に中身のviewテンプレートを呼び出します。

中身は一旦vueテンプレートの空ファイルにしておきます。

<template>
  <div>
  </div>
</template>

<script>
export default {
}
</script>

<style scoped>
</style>

パーツの読み込み

rootであるApp.vueで、ヘッダーなどのパーツを呼び出します。

App.vueは、初期状態では下記のようになっているかと思います。

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

<router-view/>の部分は、ルーティングで指定したViewが入るので、その前後にヘッダーなどのパーツを読み込ませます。

<template>
  <div id="app">
    <Header />
    <router-view/>
    <Sub />
    <Footer />
  </div>
</template>

router-link部分はまるっと、Header.vueに移動させておきます。

コンポーネントは、テンプレート内(<template>の中)では、.vueを除いたファイル名で呼び出しができます。ただし、ファイル名がキャメルケースの場合は、ハイフンに変換します。

各コンポーネントはスクリプトで事前に呼び出しをする必要があるので、<script>内にコンポーネントの登録をします。

<script>
import Header from '@/components/Header.vue'
import Sub from '@/components/Sub.vue'
import Footer from '@/components/Footer.vue'

export default {
  name: 'app',
  components: {
    Header,
    Sub,
    Footer,
  }
}
</script>

これで、どのルートでも、ヘッダーなどの共有パーツが読み込まれるようになりました。

ルーティングの修正

続いて、ルーティングを修正していきます。

Hisotryモード

デフォルトではURLが#で切り替えされるようになっているので、これを通常のURLのような挙動にします。

const router  = new Router({
  mode: 'history',
  routes: [

mode: 'history',を追加するだけです。

これで、URL的にはページ遷移が発生しているように見えますが、実際にはVue.jsで切り替えをするだけの状態になります。これでぐっとSPAっぽくなりましたね。

titleとmeta、ogpを動的に変更させる

続いて、titleとmetaをルーティング時に変更する設定を追加します。

各ルートのViewコンポーネントに追加していきます。下記のはHome.vueの例です。

<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'home',
  components: {
    HelloWorld
  },
  mounted: function(){
    const title = "サンプルサイト"
    const description = "サンプルサイトの説明文です。ここに説明文を書いていきます。"
    document.title = title
    document.querySelector("meta[property='og:title']").setAttribute('content', title)
    document.querySelector("meta[name='description']").setAttribute('content', description)
    document.querySelector("meta[property='og:description']").setAttribute('content', description)
  }  
}
</script>

これで、titleとmeta、ogpが、各ルートの親コンポーネントに記載された設定に変更されます。

Vuexなどを使ってstateで変更させる実装もありますが、今回はシンプルでわかりやすいようにすっぴんのJavascriptで記述しています。

注意点は、コンポーネントごとに書き換えるようになっているので、ページ遷移時に中身がリセットされません。きちんと親コンポーネントごとに設定をしないと、前のページのtitleやmetaをそのまま引き継いでしまうので、SEO的にも注意が必要です。

共通stateを使う

Vue.jsでは、親コンポーネントから子コンポーネントへの変数の参照ができます。また、その逆も可能です。

しかし、記述が煩雑になったり、親変数の場所がわかりづらかったりするため、共通状態変数を設定すると管理も記述も非常に楽になります。

先程インストールしたVuexはまさにそのためのプラグインで、ルートに共通変数を格納して、各コンポーネントから参照や書き換えができるようにしてくれます。

今回は、SPAっぽく、ヘッダのタイトルを動的に変更する機能を実装します。

router.jsの改変

routerがstateを引き継げるように微修正を加えます。

export default new Router({

})

この部分を、

const router  = new Router({
})
export default router;

とします。これで、routerクラスに処理を追加できるようになります。

そのすぐ下で、routerのafterEach関数を実行します。

const router  = new Router({
.
.
.
})
export default router;

router.afterEach((to,from) =>{
  console.log('ページ変更処理!')
})

ブラウザの開発ツールのコンソールで確認すると、ページを変更するたびに「ページ変更処理!」と表示されるはずです。

これで、ルート切替時にアクションを追加できるようになりました。

storeディレクトリの作成とファイル移動

storeの設定ファイルは、デフォルトのstore.jsでも構わないのですが、将来拡張する時を考慮してstoreディレクトリに格納します。ここは任意です。

storeディレクトリを作成して、その中にstore.jsをindex.jsにリネームして格納します。

stateの定義

Vuexのstateは、

  1. state
  2. mutation
  3. action
  4. getter

の4つのパートに別れます。

大雑把にいえば、

  1. stateはデフォルト変数
  2. mutationは、stateを変更する関数
  3. actionはmutationをstateを書き換えるために呼び出すための関数
  4. getterはstate変数を読み込むための関数

というイメージです。

今回は、pageTitleという共通変数を持つことにします。

store/index.jsは下記のようになります。

export const store = new Vuex.Store({
  state: {
    pageTitle: 'Home',
  },
  mutations: {
    chagePageTitle(state, title) {
      state.pageTitle = title
    },
  },
  actions: {
    changePage({ commit }, title) {
      commit('chagePageTitle', title)
    }
  },
  getters: {
    getTitle(state) {
      return state.pageTitle;
    },
  }
})
export default store;

mutationはコンポーネント側で「mapMutations」をimpoertすることで呼び出せるため、actionは設定しなくても問題ないのですが、router側でアクションを呼びたいケースのために登録しています。

stateをrouterで使う

次に、stateをrouterで変更してみます。

router.jsのroute設定部分を下記のようにします。

import store from './store'; //追記

const router  = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
      meta: 'ホーム'
    },
    {
      path: '/about',
      name: 'about',
      component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
    }
  ]
})
export default router;

HomeルートにあるmetaにpageTitleに格納するページタイトルを設定しておきます。metaは配列が使えるので、配列にしてもOKです。

逆に、Aboutは、About.vue側からpageTitleを書き換えるため、metaの設定はしていません。

実際に変更処理をかけるのは「router.afterEach」部分で、ここにstoreのactionを追加します。先程、console.log(‘ページ変更処理!’)とした部分です。

router.afterEach((to,from) =>{
  store.dispatch('changePage', to.meta)
  // console.log('ページ変更処理!')
})

storeのactionは、store.dispatchで呼び出すことができます。引数は、アクション名で、第2引数でアクションが求める引数を渡せます。

これで、Homeに関しては、pageTitleが設定できました。

コンポーネントからstateを書き換える

続いて、コンポーネントからstateを書き換えてみます。

About.vueのスクリプトを書き換えます。

<script>
import { mapMutations } from 'vuex'

export default {
  name: 'about',
  mounted: function(){
    this.$store.commit('chagePageTitle', 'あばうと')
  }
}
</script>

最初の部分で、「mapMutations」を使って、store/index.jsのmutationを読み込む設定をしています。

その後、mountedで実際にstore/index.jsのmutationを呼び出して実行しています。

mutationは

this.$store.commit(メソッド名)

が基本で、

this.$store.commit(メソッド名、引数)

メソッド名が求める引数も渡せます。

これで、About.vue側でpageTitleを書き換える設定ができました。記事ページなど、ページタイトルが動的になる場合は、このような使い方をすることになるでしょう。

ついでに先程見たtitleとmetaなどの書き換えも追加しておきます。

  mounted: function(){
    this.$store.commit('chagePageTitle', 'あばうと')
    const title = "このサイトについて"
    const description = "サンプルサイトについての説明ページです。運営者情報や免責事項など。"
    document.title = title
    document.querySelector("meta[property='og:title']").setAttribute('content', title)
    document.querySelector("meta[name='description']").setAttribute('content', description)
    document.querySelector("meta[property='og:description']").setAttribute('content', description)
  }

Header.vueでpageTitleを呼び出す

最後に、Header.vueでstoreの変数・pageTitleを呼び出せば完成です。

<template>
  <div id="header">
    Header
    <h1>{{getTitle}}</h1>
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
  </div>
</template>

<script>
import { mapGetters } from 'vuex';

export default {
  computed: {
    ...mapGetters([
      'getTitle'
    ])

  }
}
</script>

<style scoped>
</style>

scriptの最初、

import { mapGetters } from 'vuex';

で、stateのgetterを取り込みます。

テンプレート側で使うためには、computedの…mapGettersで変数をテンプレート側に格納します。注意する点は、ここで呼び出す引数は、store/index.jsのgettersで設定した関数名になります。

mapGettersで取り込むと、このAbout.vueコンポーネントのtemplete内で「getTitle」がそのまま変数として格納されます。

<h1>{{getTitle}}</h1>

この部分ですね。

実際に、ページを切り替えるとページタイトルが切り替わるはずです。


Vue.jsでSPAのベースになる部分を作ってみました。

Vue RouterとVuexを使うことで、アプリのような操作性のSPAが簡単に作ることができますね。