2019.01.12

Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (6) 認証機能とVuex


この連載記事では、フロントエンドに Vue.js + Vue Router + Vuex とサーバーサイドに Laravel を使用したシングルページ Web アプリケーションの開発方法を紹介します。実際に写真共有アプリを開発する手順を通して SPA 開発のエッセンスを学んでいただけるように書いていきます。

今回のチュートリアルで扱うツールなどのバージョンは以下の通りです。

Node npm Vue.js Vue Router Vuex PHP Laravel
10.7.0 6.4.1 2.5.21 3.0.2 3.0.1 7.2.10 5.7.19

この章では、状態管理ライブラリ Vuex を用いて認証の3つの機能「会員登録」「ログイン」「ログアウト」を実装します。

Vuex の導入

Vuex とは

Vuex とは、Vue のために開発された状態管理ライブラリです。状態管理というと難しく聞こえますが、簡単にいうと「コンポーネントをまたいで参照したいデータを入れておく場所」です。

例えばユーザーの認証状態も「コンポーネントをまたいで参照したいデータ」にあたるでしょう。前章で見た通り、ナビゲーションバーやフッターでは認証状態を参照して表示要素を切り替える必要がありました。

この「データを入れておく場所」をストアと呼びます。

Vuex のメリット

Vuex のメリットは、親コンポーネントと子コンポーネントの間でのデータのやりとりをショートカットできる点にあります。

今回の認証機能の例でいうと、ナビゲーションバーで表示要素を出し分けるためにはログインページコンポーネントからナビゲーションバーコンポーネントにユーザーデータを渡す必要がありますが、Vue コンポーネントの性質上、直接渡すことはできません。

Vue コンポーネント同士がデータをやりとりするためには、基本的に $emitprops を使用した親子間のコミュニケーションで実現する必要があります。これを図式化したのが下図です(緑の箱が Vue コンポーネントです)。

上のように子から親への $emit と親から子への props を組み合わせてコンポーネント間のデータのやりとりが実現されるわけですが、コンポーネントツリーの階層が深く複雑になるとこのやりとりが煩雑になってしまいます。

そこで Vuex を導入した場合の概要図は以下の通りです。

ストアの中に「ステート」「アクション」などさらに構成要素が入っていますがこれについては後述します。API との通信やデータの管理をストアに集中させることで、どのコンポーネントからでもデータを更新・参照できるようになっています。

親子間のやりとりをショートカットすることで、まずデータの流れが追いやすくなります。さらに親または子への依存を無くし疎結合なコンポーネントを作成できます。

Vuex ストアの構成要素

上の図の通り、Vuex ストアは「ステート」「ゲッター」「ミューテーション」「アクション」と4つの構成要素から成っています。具体的には実際にコードを書きながらの方が理解しやすいと思いますが、簡単にそれぞれを紹介しておきます。

ステート

ステートはデータの入れ物そのものです。ログイン中のユーザーデータなどが該当します。

ゲッター

ゲッターはステートの内容から算出される値です。ステートとゲッターの関係はコンポーネントでいうとデータ変数と算出プロパティの関係と同様でしょう。上の例を用いると「ユーザーがログイン中であるかどうか」をゲッターで表現することができます。

ミューテーション

ミューテーションはステートを更新するためのメソッドです。コンポーネントはステートを直接変更することができない仕組みになっていて、ミューテーションを介してステートを更新します。後述のアクションと異なり、ミューテーションは同期処理でなければいけません。

アクション

アクションもミューテーションと同様にステートを更新するメソッドですが、ミューテーションとの違いはアクションは非同期処理である点です。アクションは API との通信などの非同期処理を行った後にミューテーションを呼び出してステートを更新する、といった

インストール

説明はこれくらいにして、実際にコードを書いていきましょう。
まずは npm からライブラリをインストールします。

$ npm install --save-dev vuex

ストアの作成と読み込み

ストアの配置先として resources/js/store ディレクトリを作成してください。
そして store ディレクトリの中に以下の内容で auth.js を作成しましょう。

auth.js
const state = {}

const getters = {}

const mutations = {}

const actions = {}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}

まだ中身は空ですが、ステート・ゲッター・ミューテーション・アクションを定義してストアオブジェクトとしてエクスポートしています。これが認証関係のデータが入るストアになります。

次に store ディレクトリにもう一つ index.js を以下の内容で作成してください。

index.js
import Vue from 'vue'
import Vuex from 'vuex'

import auth from './auth'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    auth
  }
})

export default store

ストアを作成する際に、インポートした auth.js をモジュールとして登録しています。このように、ストアは種類に応じてモジュールとして分けて作成(認証関係のデータのストア、写真データのストア、などのように)することができます。

大きくない規模のアプリケーションでも種類の異なるデータがまとめて一つのファイルに書かれていると読みづらくなってしまうので、モジュールに分けて書くと良いですね。

モジュールに分けたときにステートやミューテーションの名前が被ってもモジュール名で区別できるように namespaced: true を指定しています。具体的には後ほどコードで見ていきます。

app.js でストアをインポートして Vue インスタンス作成時に読み込みます。

app.js
import Vue from 'vue'
import router from './router'
import store from './store' // ★ 追加
import App from './App.vue'

new Vue({
  el: '#app',
  router,
  store, // ★ 追加
  components: { App },
  template: '<App />'
})

これでストアを使用する準備は OK です。

CSRF 対策

会員登録やログインの機能を実装する前に、CSRF 対策を実装します。サーバサイドの API には GET 以外のメソッドでの通信時に CSRF トークンのチェックが入りますので、ストアを用いた API 呼び出し箇所を実装する前に用意しておく必要があります。

Laravel における CSRF 対策

Laravel でマルチページアプリを作成する際はフォームに CSRF トークンを含めます。

<form action="..." method="POST">
  @csrf

@csrf は HTML 生成時に以下のように <input> 要素に変換されます。

<form action="..." method="POST">
  <input type="hidden" name="_token" value="..." />

これに対し SPA における Web API の場合は <input> 要素を設置することができません。

実は CSRF トークンはレスポンスのたびにクッキーに入れて送信されています。開発者ツールを開いて Application タブからクッキーを確認してみてください。

Cookie

XSRF-TOKEN という名前の項目があるはずです。これが(暗号化された)CSRF トークンです。

フォームに設置する代わりにクッキーからトークンを取り出してヘッダーにそのトークンを含めてリクエストを送信しても CSRF チェックがかかりますので、今回はその方法を採用します。

CSRF 対策の実装

クッキーを取り出す関数

まずは特定の名前のクッキーの値を取り出す関数を用意します。
以下の内容で resources/js/util.js を作成してください。

util.js
/**
 * クッキーの値を取得する
 * @param {String} searchKey 検索するキー
 * @returns {String} キーに対応する値
 */
export function getCookieValue (searchKey) {
  if (typeof searchKey === 'undefined') {
    return ''
  }

  let val = ''

  document.cookie.split(';').forEach(cookie => {
    const [key, value] = cookie.split('=')
    if (key === searchKey) {
      return val = value
    }
  })

  return val
}

document.cookie によってクッキーは以下の形式で参照できます。

name=12345;token=67890;key=abcde

そこで、;split してさらに =split することで引数の searchKey と一致するものを探しています。

Axios の設定

次に resources/js/bootstrap.js を以下の内容に編集してください。

bootstrap.js
import { getCookieValue } from './util'

window.axios = require('axios')

// Ajaxリクエストであることを示すヘッダーを付与する
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'

window.axios.interceptors.request.use(config => {
  // クッキーからトークンを取り出してヘッダーに添付する
  config.headers['X-XSRF-TOKEN'] = getCookieValue('XSRF-TOKEN')

  return config
})

bootstrap.js では Ajax 通信で用いる Axios ライブラリの設定を記述しています。
Ajaxリクエストであることを示す X-Requested-With ヘッダーを付与し、トークンを X-XSRF-TOKEN ヘッダーに含めることで、Laravel はフォームではなくヘッダーを見て CSRF トークンチェックを行ってくれます。

上記のコードを読み込むために、app.js の先頭行に以下の記述を追加してください。

app.js
import './bootstrap'

これで Web API における CSRF 対策が実装できました。

会員登録

マイグレーション

ここでやっとデータベースにデータが入るので、マイグレーションを実行しておきましょう。

$ php artisan migrate

ストアの実装

ではストアから実装していきます。

ステート

まずはステートです。ログイン済みユーザーを保持する user を追加します。

auth.js
const state = {
  user: null
}

ミューテーション

ミューテーションには user ステートの値を更新する setUser を追加します。

auth.js
const mutations = {
  setUser (state, user) {
    state.user = user
  }
}

ミューテーションの第一引数は必ずステートです。ミューテーションを呼び出すときの実引数は仮引数では第二引数以降として渡されますので注意しましょう。

アクション

続いて会員登録 API を呼び出す register アクションを追加します。

auth.js
const actions = {
  async register (context, data) {
    const response = await axios.post('/api/register', data)
    context.commit('setUser', response.data)
  }
}

アクションの第一引数も決まっていて、コンテキストオブジェクトが渡されます。コンテキストオブジェクトにはミューテーションを呼び出すための commit メソッドなどが入っています。

上記ではまず会員登録 API を呼び出し、返却されたデータを渡して setUser ミューテーションを呼び出すことで(ここで commit メソッドを使っています)user ステートを更新しています。

最初はこの処理の流れが少し難しく感じるかもしれませんが、この「アクション→コミットでミューテーション呼び出し→ステート更新」というパターンはよく使うので慣れれば大丈夫です。

コンポーネントの実装

実装したストアを、コンポーネントで使用します。
ログインページを表す Login.vueregister メソッドを以下の通り編集してください。

Login.vue
async register () {
  // authストアのresigterアクションを呼び出す
  await this.$store.dispatch('auth/register', this.registerForm)

  // トップページに移動する
  this.$router.push('/')
}

stores/index.js にて Vue.use(Vuex) という記述で Vuex プラグインの使用を宣言したので、this.$store からストアを参照することができます。

アクションを呼び出すには dispatch メソッドを用います。

dispatch メソッドの第一引数はアクションの名前です。auth ストアを作成したときに namespaced: true として名前空間を有効化させたので、モジュール名を頭につけた 'auth/register' という名前でアクションを指定します。仮に他のストアモジュールにも register というアクションがあったとしてもこのように名前空間を有効にしておくと呼び出し時に区別できます。

dispatch メソッドの第二引数にはフォームの入力値を渡しています。これがアクションの第二引数になります。

await で非同期なアクションの処理が完了するのを待ってから(難しく言うと Promise の解決を待ってから)、トップページに遷移するために this.$routerpush メソッドを読んでいます。Vue Router の設定時に Vue.use(VueRouter) と記述して VueRouter プラグインの使用を宣言したため this にルーターオブジェクトを表す $router が追加されています。

動作確認

では動作を確認してみましょう。

ビルドコマンドを実行していなければ実行してください。

$ npm run watch

会員登録フォームに会員情報を入力して register ボタンをクリックします。

エラーがなければまずトップページに遷移するはずです。
また、Vue DevTools を確認してみましょう。

Vue.js DevTools

このように、state > auth > user にユーザー情報が保存されているはずです。
さらに、データベースの users テーブルにデータが登録されていることも確認しましょう。

いかがでしたか?うまくいっていたでしょうか。
これで Vuex を利用した会員登録機能が実装できました 🙌

ログイン

同じパターンでログイン機能も実装しましょう。

ストアの実装

アクション

ステートとミューテーションは会員登録機能で作成したので、アクションのみ追加します。

auth.js
const actions = {
  async register (context, data) {/* 中略 */},
  async login (context, data) {
    const response = await axios.post('/api/login', data)
    context.commit('setUser', response.data)
  }
}

register アクションと同じパターンですね。

コンポーネントの実装

コンポーネントの実装も会員登録と同じパターンです。

Login.vue
async login () {
  // authストアのloginアクションを呼び出す
  await this.$store.dispatch('auth/login', this.loginForm)

  // トップページに移動する
  this.$router.push('/')
},

動作確認

会員登録した時点でログイン状態なので、さらにログインしようとするとエラーになります。ログアウト機能はまだ実装できていないので、無理矢理ですが開発者ツールからクッキーをすべて削除してから Ctrl+R などで画面をリロードしてください。ログイン前の状態になります。

先ほど作成した会員情報でログインしてみてください。

  • 無事トップページに遷移したでしょうか。
  • Vue DevTools で user ステートが更新されていることが確認できたでしょうか。

うまくいっていればログイン機能も実装できています。

ログアウト

この章の最後にログアウト機能を実装します。
こちらもパターンは会員登録・ログインと同じです。

ストアの実装

アクション

auth.jslogout アクションを追加します。

auth.js
const actions = {
  async register (context, data) {/* 中略 */},
  async login (context, data) {/* 中略 */},
  async logout (context) {
    const response = await axios.post('/api/logout')
    context.commit('setUser', null)
  }
}

ログアウト処理が完了したあとは user ステートを null で更新しています。

コンポーネントの実装

Footer.vue にスクリプトブロックを追加します。

Footer.vue
<script>
export default {
  methods: {
    async logout () {
      await this.$store.dispatch('auth/logout')

      this.$router.push('/login')
    }
  }
}
</script>

同じく Footer.vue で、ログアウトボタンに @click="logout" を追加してクリックしたときに上記のメソッドを呼び出すようにします。

Footer.vue
<button class="button button--link" @click="logout">Logout</button>

動作確認

ログインした状態でフッターのログアウトボタンをクリックしてみましょう。

ログインページに遷移したでしょうか。
また、DevTools で user ステートが null に戻っていることが確認できたでしょうか。

👾 👾 👾

この章はこれでおしまいです。
本章までのソースコードはリポジトリの chapter-6 ブランチに置いてあります。

次の章では、認証状態によるヘッダーおよびフッターの要素の出し分けなどを実装し、認証機能をさらにブラッシュアップします。ストアの構成要素の一つであるゲッターも登場しますよ。

関連記事

連載記事(全16回)

その他


<!-- Font Awesome Free 5.0.13 by @fontawesome - https://fontawesome.com --><!-- License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->