この連載記事では、フロントエンドに 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 |
この章では写真の詳細ページを実装します。
基本的に写真一覧取得機能と実装パターンは似ているため、実装や説明の分量が多くありません。そこで Web API とフロントエンドの実装を本章でまとめて説明します。
ただし、いいねとコメント投稿は後続の章で実装します。
Web API
JSON レスポンス
JSON レスポンスは一覧の配列の一つ分と同じです。
{
"id": "abcd1234EFGH",
"url": "https://s3-ap-northeast-1.amazonaws.com/backet-name/abcd1234EFGH.jpeg",
"owner": {
"name": "John Lennon"
}
}
最終的にはいいね数やコメントなども含めますが、それらの機能は後続の章で実装しますので、本章では上記のフォーマットの JSON がレスポンスされる API 実装します。
テスト
まずテストを書きます。
$ php artisan make:test PhotoDetailApiTest
内容は一覧取得 API と基本的に同じパターンです。
<?php
namespace Tests\Feature;
use App\Photo;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class PhotoDetailApiTest extends TestCase
{
use RefreshDatabase;
/**
* @test
*/
public function should_正しい構造のJSONを返却する()
{
factory(Photo::class)->create();
$photo = Photo::first();
$response = $this->json('GET', route('photo.show', [
'id' => $photo->id,
]));
$response->assertStatus(200)
->assertJsonFragment([
'id' => $photo->id,
'url' => $photo->url,
'owner' => [
'name' => $photo->owner->name,
],
]);
}
}
assertJsonFragment
メソッドで JSON のフォーマットを確かめています。
ルート定義
ルート定義は以下の通りです。
// 写真詳細
Route::get('/photos/{id}', 'PhotoController@show')->name('photo.show');
パスパラメータ id
を定義しています。
コントローラー
写真詳細取得 API も認証なしでアクセスできる仕様にするので、コントローラーではまずコンストラクタの認証ミドルウェア指定の箇所で、except
の引数にメソッド名 show
を追加します。
public function __construct()
{
// 認証が必要
$this->middleware('auth')->except(['index', 'download', 'show']);
}
show
メソッドを追加します。
/**
* 写真詳細
* @param string $id
* @return Photo
*/
public function show(string $id)
{
$photo = Photo::where('id', $id)->with(['owner'])->first();
return $photo ?? abort(404);
}
index
メソッドとほとんど同じですね。
- 引数でパスパラメータ
id
を受け取っています。 - 写真データが見つからなかった場合は 404 を返却しています。
テスト実行
実装ができたらテストを実行しましょう。
./vendor/bin/phpunit --testdox
これで API の実装は完了です。
フロントエンド
PhotoDetail コンポーネント
<PhotoDetail>
コンポーネントを以下の通り編集してください。
<template>
<div v-if="photo" class="photo-detail">
<figure class="photo-detail__pane photo-detail__image">
<img :src="photo.url" alt="">
<figcaption>Posted by {{ photo.owner.name }}</figcaption>
</figure>
<div class="photo-detail__pane">
<button class="button button--like" title="Like photo">
<i class="icon ion-md-heart"></i>12
</button>
<a
:href="`/photos/${photo.id}/download`"
class="button"
title="Download photo"
>
<i class="icon ion-md-arrow-round-down"></i>Download
</a>
<h2 class="photo-detail__title">
<i class="icon ion-md-chatboxes"></i>Comments
</h2>
</div>
</div>
</template>
<script>
import { OK } from '../util'
export default {
props: {
id: {
type: String,
required: true
}
},
data () {
return {
photo: null
}
},
methods: {
async fetchPhoto () {
const response = await axios.get(`/api/photos/${this.id}`)
if (response.status !== OK) {
this.$store.commit('error/setCode', response.status)
return false
}
this.photo = response.data
}
},
watch: {
$route: {
async handler () {
await this.fetchPhoto()
},
immediate: true
}
}
}
</script>
紹介すべき点はほとんど前章の写真一覧実装で説明してしまいました。
ルートパラメータの取得箇所のみ説明します。
router.js
では、次のように写真詳細ページのルートを定義していました。
{
path: '/photos/:id',
component: PhotoDetail,
props: true
},
path
の :id
がパラメータとして定義されている部分です。写真IDがハマる箇所ですね。
さらに props
を true
に設定していますので、この :id
の値が <PhotoDetail>
コンポーネントに props
として渡されます。
props: {
id: {
type: String,
required: true
}
},
写真の幅を切り替える
写真をクリックすると、横幅いっぱいのサイズで表示されるようにします。
写真の右にあるコメント欄などは写真の下に配置します。
まず、data
に fullWidth
を追加します。
data () {
return {
photo: null,
fullWidth: false
}
},
次にテンプレートブロックを以下の通り編集します。
<template>
<div
v-if="photo"
class="photo-detail"
:class="{ 'photo-detail--column': fullWidth }"
>
<figure
class="photo-detail__pane photo-detail__image"
@click="fullWidth = ! fullWidth"
>
- 一番上の
<div>
要素に:class
を追加する。 <figure>
要素に@click
を追加する。
これで完成です。仕組みは以下の通りです。
- 写真(
<figure>
)をクリックするとfullWidth
の値がtrue
とfalse
に切り替わる。 -
fullWidth
の値が切り替わると<div>
要素のphoto-detail--column
クラスが付いたり外れたりする。- 具体的にいうと、
photo-detail--column
クラスが付いていなければflex-direction
がrow
になるので横並びになる。 photo-detail--column
クラスが付いていればflex-direction
がcolumn
になるので縦並びになる。
- 具体的にいうと、
ブラウザで動作を確認してみてください。
この章はこれでおしまいです。
本章までのソースコードはリポジトリの ch-13 ブランチに置いてあります。
次の章ではコメント投稿機能を実装します。
関連記事
連載記事(全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