この連載記事では、フロントエンドに Vue.js + Vue Router + Vuex とサーバーサイドに Laravel を使用したシングルページ Web アプリケーションの開発方法を紹介します。実際に写真共有アプリを開発する手順を通して SPA 開発のエッセンスを学ぶことができます。
今回のチュートリアルで扱うツールなどのバージョンは以下の通りです。
Node | npm | Vue.js | Vue Router | Vuex | PHP | Laravel |
---|---|---|---|---|---|---|
10.15 | 6.4 | 2.6 | 3.1 | 3.1 | 7.4 | 6.9 |
この章では、前章に引き続いて状態管理ライブラリ Vuex を用いて認証の3つの機能「会員登録」「ログイン」「ログアウト」を完成させます。
ステートの値による要素の出し分け
まず認証状態によってナビゲーションバーとフッターの要素を出し分ける処理を実装します。
ゲッターを追加する
user
ステートを参照して認証状態を確認してもいいのですが、ステートそのものではなくステートを元に演算した結果が欲しい場合はゲッターのちょうど良い使いどころです。
auth.js
に2つのゲッターを追加します。
const getters = {
check: state => !! state.user,
username: state => state.user ? state.user.name : ''
}
check
はログインチェックに使用します。確実に真偽値を返すために二重否定しています。
username
はログインユーザーの name
です。仮に user
が null
の場合に呼ばれてもエラーが発生しないように空文字を返すようにしています。
ナビゲーションバー
上記のゲッターを使用して、まずはナビゲーションバーの要素出し分けを実装しましょう。
Navbar.vue
に以下のスクリプトブロックを追加します。
<script>
export default {
computed: {
isLogin () {
return this.$store.getters['auth/check']
},
username () {
return this.$store.getters['auth/username']
}
}
}
</script>
ストアのステートまたはゲッターを参照する場合は、直接テンプレートで $store
を参照してもいいのですが、computed
から参照すれば冗長な記述を避けることができます。
あとは上記の算出プロパティを v-if
の条件に使えば要素を出し分けられます。
テンプレートを以下のように編集してください。
<div class="navbar__menu">
<div v-if="isLogin" class="navbar__item">
<!-- 中略 -->
</div>
<span v-if="isLogin" class="navbar__item">
{{ username }}
</span>
<div v-else class="navbar__item">
<!-- 中略 -->
</div>
</div>
- 写真投稿ボタンの外側の
<div>
にv-if="isLogin"
を追記 - ログインユーザー名の外側の
<span>
にv-if="isLogin"
を追記 - ログインユーザー名の箇所を
{{ username }}
に変更 - ログインリンクの外側の
<div>
にv-else
を追記
隣接した要素の v-if
と反対の条件で出し分けする場合は v-else
が使えます。
フッター
フッターもナビゲーションバーと同じパターンです。
まず Footer.vue
にログインチェックの算出プロパティを追加しましょう。
computed: {
isLogin () {
return this.$store.getters['auth/check']
}
},
そしてテンプレートを以下のように編集します。
<button v-if="isLogin" class="button button--link" @click="logout">
Logout
</button>
<RouterLink v-else class="button button--link" to="/login">
Login / Register
</RouterLink>
コードが書けたらブラウザで動作を確認してみましょう。
ログイン状態と非ログイン状態で意図通り要素が出し分けされているでしょうか。
ここからは現状の認証機能の実装が抱える2つの課題を解決していきます。
認証状態を維持する
課題
一つ目の課題は、ページをリロードしたときに認証状態が維持できない点です。
試しにログインした状態でページをリロードしてみてください。ヘッダーとフッターにログインリンクが表示され、ログアウト状態に戻ってしまうはずです。
ページをリロードすると Vue アプリケーションが再作成されるので、当然 user
ステートも初期値である null
に戻ってしまいます。それで見た目はログアウトしてしまうのです。
ただしサーバサイドのセッション的にはまだログインしているので、もう一度ログインしようとするとユーザーデータではなく /home
へのリダイレクトレスポンスが返却されます。そのためログインしているような見た目だがユーザー名が表示されないという中途半端な表示になります。
この挙動を修正するために、ログインユーザーを返却する API を追加して、最初にログインユーザーを取得してから Vue アプリケーションを生成するように起動スクリプトを変更しましょう。
また、一応 /home
へリダイレクトさせるミドルウェアも修正しておきます。
ユーザー取得 API
テスト
例によってテストコードを作成します。
$ php artisan make:test UserApiTest
雛形 tests/Feature/UserApiTest.php
を以下の通り編集してください。
<?php
namespace Tests\Feature;
use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserApiTest extends TestCase
{
use RefreshDatabase;
public function setUp(): void
{
parent::setUp();
// テストユーザー作成
$this->user = factory(User::class)->create();
}
/**
* @test
*/
public function should_ログイン中のユーザーを返却する()
{
$response = $this->actingAs($this->user)->json('GET', route('user'));
$response
->assertStatus(200)
->assertJson([
'name' => $this->user->name,
]);
}
/**
* @test
*/
public function should_ログインされていない場合は空文字を返却する()
{
$response = $this->json('GET', route('user'));
$response->assertStatus(200);
$this->assertEquals("", $response->content());
}
}
実装
API の実装は単純です。routes/api.php
に以下の記述を追加してください。
// ログインユーザー
Route::get('/user', fn() => Auth::user())->name('user');
ただログインユーザーを返すだけなので、コントローラーは作成しません。
ちなみにテストコードにもありましたが、ログインしていない場合は API は空文字(""
)を返却します。ログインしていないと Auth::user()
は null
を返しますが、HTTP レスポンスに変換されるときに null
は空文字に変わります。HTTP メッセージはただの文字列なので null
や false
などのプログラミング言語的な表現は存在しないためです。
コードが書けたらテストが通ることを確認しておきましょう。
$ ./vendor/bin/phpunit --testdox
起動時にログインチェック
API ができたのでアプリ起動時のログインチェックを実装します。
ストアにアクションを追加
auth
ストアに上記の API を呼び出す currentUser
アクションを追加します。
const actions = {
async register (context, data) {/* 中略 */},
async login (context, data) {/* 中略 */},
async logout (context) {/* 中略 */},
async currentUser (context) {
const response = await axios.get('/api/user')
const user = response.data || null
context.commit('setUser', user)
}
}
上で説明した通りログインしていなければ response.data
は空文字です。ログインしていないときの user
ステートは初期値の null
に揃えておいたほうが予期しやすいコードになると思ったのでそのまま setUser
しないで真偽値チェックで偽の場合は null
を入れるようにしました。
ログインチェックしてからアプリを生成する
アプリ起動時、Vue インスタンス生成前に currentUser
アクション呼び出します。
app.js
を以下の通り編集してください。
const createApp = async () => {
await store.dispatch('auth/currentUser')
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App />'
})
}
createApp()
すでに store
はインポートしているので、Vue インスタンス生成前でもアクションを dispatch
メソッドで呼び出すことはできます。
currentUser
アクションの非同期処理が終わってから Vue インスタンスが生成させるため、そして非同期処理を await
するためには async
メソッドの内部にいる必要があるため、起動処理を createApp
関数にまとめ、最後に呼び出す記述にしました。
ここまでできたらブラウザで挙動を確認しましょう。ログインしてからブラウザをリロードしてください。ログイン状態が維持されているはずです。
ミドルウェア
課題として挙げた内容についての対策としては上記までで OK なのですが、ミドルウェアもついでに直しておきます。
RedirectIfAuthenticated
ミドルウェアによって、ログイン状態で非ログイン状態でのみアクセスできる機能にリクエストを送信した場合に /home
へのリダイレクトが返却されています。SPA 的にはページへのリダイレクト(HTML)が返るのは相応しくないので、先ほど作成したログインユーザー返却 API にリダイレクトするように修正しましょう。
app/Http/Middleware/RedirectIfAuthenticated.php
を以下の通り編集してください。
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect()->route('user'); // ★ 変更
}
return $next($request);
}
ナビゲーションガード
課題
二つ目の課題は、ログインされている状態でもログインページにアクセスできる点です。
ログイン状態であればページ上にリンクは表示されませんが、アドレスバーに直接 URL を打ち込めばアクセスできてしまいます。
Vue Router のナビゲーションガード機能を使ってログイン状態でログインページへアクセスした際はトップページに移動させることにしましょう。
参考:グローバルガード
ルート定義にナビゲーションガードを追加
要素の切り替えをしたときと同様に auth
ストアの check
ゲッターを使用しますので、まずは router.js
でストアをインポートします。
import Vue from 'vue'
import VueRouter from 'vue-router'
import PhotoList from './pages/PhotoList.vue'
import Login from './pages/Login.vue'
import store from './store' // ★ 追加
次にログインページのルート定義に、以下の通り beforeEnter
を追加してください。
{
path: '/login',
component: Login,
beforeEnter (to, from, next) {
if (store.getters['auth/check']) {
next('/')
} else {
next()
}
}
}
beforeEnter
は定義されたルートにアクセスされてページコンポーネントが切り替わる直前に呼び出される関数です。beforeEnter
の第一引数 to
はアクセスされようとしているルートのルートオブジェクト、第二引数 from
はアクセス元のルート、そして第三引数 next
はページの移動先(切り替わり先)を決めるための関数です。
next()
を引数なしで呼ぶとそのままページコンポーネントが切り替わります。引数ありで next()
を呼ぶと切り替わるはずだったページコンポーネントは生成されず、引数のページに切り替わり、リダイレクトのような動きになります。
今回は auth/check
ゲッターでログイン状態をチェックし、ログインしていれば /
つまりトップページに切り替え、ログインしていなければそのままログインページを表示しています。
この章はこれでおしまいです。
本章までのソースコードはリポジトリの ch-7 ブランチに置いてあります。
次の章では、フォームのエラー処理を実装します。
関連記事
連載記事(全16回)
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう
- (1) イントロダクション
- (2) アプリケーションの設計
- (3) SPA開発環境とVue Router
- (4) 認証API
- (5) 認証ページ
- (6) 認証機能とVuex
- (7) 認証機能とVuex Part.2
- (8) エラーハンドリング
- (9) 写真投稿API
- (10) 写真投稿フォーム
- (11) 写真一覧取得API
- (12) 写真一覧ページ
- (13) 写真詳細ページ
- (14) コメント投稿機能
- (15) いいね機能
- (16) エラーハンドリング Part.2