勉強会用の資料です.
今回の記事ではユーザログイン情報の確認や復元を行えるようにしていきます.
ソースコードはgithubで管理してます.
https://github.com/usa-mimi/tutorial-spa
今回の記事から開始する人は tutorial6-start
のタグから開始してください.
はじめに
前回の記事までで,ログイン画面で正しいIDとパスワードを入力するとログイン状態になるところまで作成しました.
ユーザがログインしているかどうかの情報は axiosクライアント(this.$request
)にtokenがセットされているかどうかで一応の判断はできますが,
ユーザがログイン済みかどうか という情報の判断をAPI用に使用しているプラグインに問い合わせるのは好ましくありません.
また,今はまだないですが,ログイン中のユーザの名前などは多くのコンポーネントで使用することが想定されるため,
どのコンポーネントのdataで定義すべきなのか判断がつきません.
vue.jsには vuex と呼ばれるライブラリが提供されており,
これを使うことでコンポーネント間で共通して使用するデータを楽に共有することが可能になります.
上述の公式の図を見てわかる通り,vuexはvueのデータ処理部分を抜き出して共通化したライブラリと言えます.
vueコンポーネントでは通常,data
でデータを定義し, テンプレート内で記述したボタン等のhtmlに
methods
で定義したメソッドを紐付けます.
呼び出されたメソッド内ではデータを変更し,テンプレート内で使用されているデータに対して変更を反映します.
vuexではデータを state,メソッドを action として定義します.
また,vuex内で mutation と呼ばれるメソッドを持っており, stateを変更できるのはmutationのみ,と設定されています.
少し回りくどいように感じるかもしれませんが,このような設計によってAPIのような非同期な処理を同期的な処理に落とし込めるようになっています.
公式ドキュメントに詳しい思想やカウンタでのサンプルコードが記載されているのでぜひ一度目を通してください.
https://vuex.vuejs.org/ja/getting-started.html
ログイン情報の確認
vuexのインストール
ソース: b90f807
→ be10d0c
npmでさくっと入ります.
frontend/
ディレクトリに移動し,
$ npm install --save vuex
とコマンド実行してください.
vuexソースコードの記述
ソース: be10d0c
→ 0c90ee7
慣例的に store
としているのでそれに合わせます.
frontend/src/
以下に storeディレクトリを作りましょう.
公式のサンプルではactionやmutation用にjsファイルを作ってますが,
今回はとりあえず index.js
に全部いれちゃいます.
最初なのでまずはログインしているかどうかだけ保持し,状態を切り替えるためのactionとmutationを用意します.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
const state = {
isLoggedIn: false,
}
const mutations = {
loggedIn (state) {
state.isLoggedIn = true
},
loggedOut (state) {
state.isLoggedIn = false
},
}
const actions = {
login ({commit}) {
commit('loggedIn')
},
logout ({commit}) {
commit('loggedOut')
},
}
const getters = {
isLoggedIn: state => state.isLoggedIn,
}
export default new Vuex.Store({
strict: debug,
actions,
getters,
mutations,
state,
})
ここまで書けたら main.js
を修正し,Vueにセットします.
...
import store from './store' // <----- 追加
import api from './api'
...
new Vue({
el: '#app',
store, // <----- 追加
router,
components: { App },
template: '<App/>',
})
このままだと動作がわからないので適当な場所に入れて動作確認しましょう.
今回は App.vue
に書いていきます
template内にはログイン中かどうかの情報を表示させます.
また,ログイン用,ログアウト用のボタンを配置します.
vuex
には mapActions
,mapGetters
というヘルパーが用意されています.
これをそれぞれmethods, computedに記述することでメソッドや算術プロパティとして登録できます.
<template>
<v-app id="app">
<v-navigation-drawer app></v-navigation-drawer>
<v-toolbar app>
{{ isLoggedIn ? 'ログイン中' : 'ログアウト中' }}
<v-btn @click="login">ログイン</v-btn>
<v-btn @click="logout">ログアウト</v-btn>
</v-toolbar>
<v-content>
<v-container fluid>
<router-view></router-view>
</v-container>
</v-content>
<v-footer app></v-footer>
</v-app>
</template>
<script>
import {mapActions, mapGetters} from 'vuex'
export default {
name: 'App',
computed: {
...mapGetters(['isLoggedIn']),
},
methods: {
...mapActions(['login', 'logout']),
},
}
</script>
ブラウザで開いて動作確認しましょう.
今回はルートコンポーネントに差し込んだのでトップページでもログイン画面でもヘッダの位置に文字とボタンが表示されます.
ログイン処理の記述
ソース: 0c90ee7
→ ae560d3
ボタンを押すことでvuexのstate変更ができるようになりましたが,
この状態では本当のログイン操作は行えていません.
store/index.js
のloginアクションを拡張して実際のログイン処理を記述します.
現状ではLogin.vue内でログインボタン押下時に this.$request
からAPIを直接叩いていましたが,
ログイン状態のvuex側で保持する必要があるのでAPIの呼び出しをvuexのactionに移動させます.
以下修正した部分の抜粋です.
import Vuex from 'vuex'
import client from '@/api' // 追加
...
const mutations = {
loggedIn (state, token) { // 引数追加
state.isLoggedIn = true
client.defaults.headers.common['Authorization'] = `JWT ${token}` // ここでtokenセット
},
...
const actions = {
login ({commit}, [username, password]) { // 引数追加,配列で受け取るので展開して受け取る
return client.auth.login(username, password).then(res => {
commit('loggedIn', res.data.token)
return res
})
},
Login.vueでやっていたAPIの呼び出しや成功時の動作の一部をstoreに移動させただけです.
Vueコンポーネント内では this.$request
で参照していましたが,store内では client としてimportしてます.
actionsのloginメソッドの第2引数ですが,ここはvuexでmethodをmapする際に2つ以上の引数が上手く渡せないので
呼び出し元で配列で渡すようにしています.そのため,ここでは受け取った配列を展開するように記述しています.
続いてLogin.vue側です.
<script>
import {mapActions} from 'vuex' // 追加
export default {
name: 'Login',
...
methods: {
...mapActions(['login']),
submit () {
this.nonFieldErrors = []
this.login([this.username, this.password]).then(res => {
this.$router.push('/')
}, err => {
this.nonFieldErrors = err.response.data.nonFieldErrors
})
},
},
}
こちらはscriptの頭で mapActions
をimportしてloginアクションを使えるようにしています.
this.$request
の時もアクションのlogin
の時の戻り値はPromiseなので記述自体は殆ど変わりません.
tokenのセット処理をstore内に移動させているのでその部分だけ削除しています.
this.login
の引数が配列になっている点に注意してください.
ログアウト処理とAppの修正
ソース: ae560d3
→ 42f7864
ログイン処理を入れたので次はログアウト処理です.
ログイン時にはヘッダにtokenをセットしましたが,ログアウト時にはこれを消してあげましょう.
この後にページ遷移させたいならコンポーネント側に処理が必要ですが,取り敢えず今回はこれで.
const mutations = {
...
loggedOut (state) {
state.isLoggedIn = false
delete client.defaults.headers.common['Authorization']
},
}
ついでにApp.vueで記述したテスト用のヘッダも修正します.
今は常にログイン/ログアウトボタンがついていますが,ログアウト時にはログイン画面へのリンク,
ログイン時にはログアウトボタンを表示するようにします.
<v-toolbar app>
<router-link v-show="!isLoggedIn" :to="{name: 'Login'}">ログイン</router-link>
<v-btn v-show="isLoggedIn" @click="logout">ログアウト</v-btn>
</v-toolbar>
もうかなり慣れてきたと思いますが,コンポーネントの表示/非表示は v-show
を使います.
v-if でもいいです.
v-showの場合,非表示時にもDOMは存在するが,表示はされていない,という状態になります.
v-ifの場合は非表示時にはDOM自体が存在せず,表示状態になった時に作成されます.
公式を参考にどちらを使うか考えてみてください.
<router-link>
は vue-routerが持っているコンポーネントです.
to
としてnameを渡すと router/index.js
で定義した名前のコンポーネントからURLが逆引きできます.
ログイン情報の復元
ソース: 42f7864
→ c64c47c
今の状態だとブラウザのF5を押すとログアウト状態になってしまいます.
これはコンポーネントやvuexが持つデータはどこにも保存されず,javascript再読込時に初期化されるからです.
これを防ぐためには,ログイン成功時に取得したtokenをcookieかlocalStorageに保存する必要があります.
今回はlocalStorageに保存することにします.
まずはstoreのmutationを修正し,ログイン時にlocalStorageに保存,ログアウト時にlocalStorageから削除する処理を挟みます.
また,localStorageからの復元を試みるactionを追加します.
const mutations = {
loggedIn (state, token) {
state.isLoggedIn = true
client.defaults.headers.common['Authorization'] = `JWT ${token}`
localStorage.setItem('token', token) // <---- 追加
},
loggedOut (state) {
state.isLoggedIn = false
delete client.defaults.headers.common['Authorization']
localStorage.clear() // <--- 追加
},
}
const actions = {
...
tryLoggedIn ({commit}) {
const token = localStorage.getItem('token')
if (token) {
commit('loggedIn', token)
}
},
}
storeの修正ができたら今度は呼び出し処理です.
今回は一番TOPのコンポーネントであるApp.vueが作られるタイミングでローカルストレージからの復元処理を呼び出すようにします.
修正は以下のようになります.
created () { // イベント登録
this.tryLoggedIn()
},
methods: {
...mapActions(['login', 'logout', 'tryLoggedIn']), // 'tryLoggedIn' 追加
},
ここまでできたらブラウザで一度ログインし,ブラウザを更新してみましょう.
更新後もログイン状態が保持されていれば(ログアウトボタンがヘッダに表示されていれば)成功です.
tokenの検証処理を入れる
今はtokenがlocalStorageに保存されていればログイン状態になってしまいます.
しかし,tokenの有効期限が切れていたり,ユーザそのものが削除されていたりし,本当はログイン状態ではない可能性も考えられます.
そこで,localStorageからtokenを復元する際に検証を行い,tokenが正しい場合のみログイン処理を行うように修正します.
token検証APIの追加
ソース: c64c47c
→ 25731fa
まずはdjango側に手を入れます.
token発行用のAPIを作った時のように,token検証用のviewもすでにライブラリで提供されています.
従って,やることはurls.pyの記述だけです.
from rest_framework_jwt.views import obtain_jwt_token
from rest_framework_jwt.views import verify_jwt_token # <--- 追加
...
api_urlpatterns = [
path('auth/', obtain_jwt_token),
path('auth/verify/', verify_jwt_token), # <--- 追加
path('questions/', include(question_router.urls)),
...
これでtokenをpostして正しいかどうか調べるための準備ができました.
不正な値を入れると下記のようにステータスコード400が返ります.
検証用APIの呼び出し処理追加
ソース: 25731fa
→ de22002
まずクライアントから呼び出せるように検証用APIを追加します.
export default function (cli) {
return {
...
verify (token) {
return cli.post('auth/verify/', {token})
},
}
}
次に,storeのtryLoggedInアクションを修正し,APIの戻り値が正しい場合のみログイン処理を行うことにします.
APIの戻り値が正しくない場合,無効なtokenがlocalStorageに残ってることになるので削除しておきます.
tryLoggedIn ({commit}) {
const token = localStorage.getItem('token')
if (token) {
client.auth.verify(token).then(() => {
commit('loggedIn', token)
}, () => {
// 不正なtoken
localStorage.clear()
})
}
},
これで復元時にtokenの検証を行う処理完了です.
次回はログインしないと入れないページを作っていきます.