2019.01.12

Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (3) SPA開発環境とVue Router


UPDATED:2020.01.05
PHP 7.4 および Laravel 6 に対応しました 🎉

この連載記事では、フロントエンドに 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

この章からコードを書いていきます。
まずは SPA プロジェクトの作成から Vue Router の導入まで進みます。

Laravel プロジェクトを作成する

まずはプロジェクトを作成します。

私は Mac マシンを使っているので Laravel Valet で開発環境を作成しましたが、Homestead や Docker などの方法もあります。

ここでは、参考までに Docker を用いた開発環境の構築方法を紹介しておきます。

Docker で開発環境構築

以下のディレクトリ構成を作成します。

/vuesplash
├─ /database  # コンテナ内のデータ保存先
├─ /web       # アプリケーションコード
├─ Dockerfile
├─ docker-compose.yml
└─ install-composer.sh

Dockerfile

Dockerfile
FROM php:7.4.1-fpm

COPY install-composer.sh /
RUN apt-get update \
  && apt-get install -y wget git unzip libpq-dev \
  && : 'Install Node.js' \
  &&  curl -sL https://deb.nodesource.com/setup_12.x | bash - \
  && apt-get install -y nodejs \
  && : 'Install PHP Extensions' \
  && docker-php-ext-install -j$(nproc) pdo_pgsql \
  && : 'Install Composer' \
  && chmod 755 /install-composer.sh \
  && /install-composer.sh \
  && mv composer.phar /usr/local/bin/composer

WORKDIR /var/www/html/vuesplash

docker-compose.yml

docker-compose.yml
version: '3'
services:
  vuesplash_web:
    build: .
    volumes:
      - ./web:/var/www/html/vuesplash
    ports:
      - 8081:8081
      - 3000:3000
  vuesplash_database:
    image: postgres:11-alpine
    restart: always
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - ./database:/var/lib/postgresql/data
    ports:
      - 5434:5432

install-composer.sh

install-composer.sh
#!/bin/sh

EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_SIGNATURE="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"

if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
then
    >&2 echo 'ERROR: Invalid installer signature'
    rm composer-setup.php
    exit 1
fi

php composer-setup.php --quiet
RESULT=$?
rm composer-setup.php
exit $RESULT

起動

Docker を起動します。docker-compose.yml があるディレクトリで実行してください。

$ docker-compose up -d

コンテナ内にログインします。

$ docker-compose exec vuesplash_web bash

Laravel プロジェクトを作成します。

コンテナ内
$ composer create-project --prefer-dist laravel/laravel .

開発用サーバーを立ち上げます。

コンテナ内
$ php artisan serve --host 0.0.0.0 --port 8081

ホスト側から実行するのであれば、以下のコマンドになります。

$ docker-compose exec vuesplash_web php artisan serve --host 0.0.0.0 --port 8081

本章の後半で npm コマンドを実行しますが、その場合も artisan コマンドと同様です。

$ docker-compose exec vuesplash_web npm install
$ docker-compose exec vuesplash_web npm run watch

コンテナ停止

コンテナの停止コマンドは以下の通りです。

$ docker-compose stop

Laravel の初期設定

app.php

config/app.phplocale 設定を日本語にします。

app.php
'locale' => 'ja',

.env

アプリ名およびデータベース接続情報を書き換えます。

.env(変更点のみ)
APP_NAME=Vuesplash

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=vuesplash
DB_USERNAME=postgres
DB_PASSWORD=postgres

冒頭の設定を使用して Docker で開発している場合は、データベース接続情報は以下の通りになります。vuesplash_databasedocker-compose.yml で指定したサービス名です。

.env
DB_CONNECTION=pgsql
DB_HOST=vuesplash_database
DB_PORT=5432
DB_DATABASE=postgres
DB_USERNAME=postgres
DB_PASSWORD=secret

EditorConfig

EditorConfig の設定は好みによりますが、私は PHP 以外のファイルはインデントをスペース2つにしたいので、最後の2行の拡張子を以下のように書き換えました。

.editorconfig
[*.{yml,json,scss,html,js,vue,blade.php}]
indent_size = 2

ここまでの設定で http://vuesplash.test にアクセスし、Laravel の welcome 画面が表示されることを確認しておいてください。

フロントエンドの準備

次に JavaScript と SCSS のセッティングを行います。

JavaScript

次のコマンドでパッケージをインストールします。

$ npm install

Vue.js を追加でインストールします。

$ npm install -D vue

スタイルシート

イントロダクションで述べたとおり、スタイルシートのコーディングはこのチュートリアルの範疇外とします。S3 にアップロードした CSS をあとでテンプレートから読み込みます。

もとの Scss ファイルはこちら(sass.zip)に置いてあるので、関心がある方はダウンロードして見てみてください。

ちなみにレスポンシブには対応できていませんのであしからず 😅

Laravel Mix

Laravel では JavaScript や Vue コンポーネント、SCSS などをコンパイルする仕組みが最初から備わっています。そのため、Gulp や Webpack などの煩雑な設定作業を行う必要がありません。

具体的には Webpack でフロントエンドをビルドしますが、設定を容易にするために Laravel Mix というライブラリを用います。

例えば JavaScript をコンパイルするための設定はたったこれだけです 🤯

mix.js('resources/js/app.js', 'public/js')

ビルドの際に設定ファイル webpack.mix.js が参照され、Webpack の設定が動的に生成されます。自分で Webpack の設定ファイルを書くより遥かに簡単な記述で済んでしまいますね。

では webpack.mix.js を以下の通り編集してください(*1)

webpack.mix.js
const mix = require('laravel-mix')

mix.browserSync('vuesplash.test')
  .js('resources/js/app.js', 'public/js')
  .version()

一つずつ何をしているか紹介していきます。

browserSync

BrowserSync というツールを組み合わせて、JavaScript や PHP ファイルが変更されたときに自動的にブラウザがリロードされるようになります。

Docker コンテナで実行する場合は以下の引数を指定してください。

mix.browserSync({
  proxy: '0.0.0.0:8081', // アプリの起動アドレス
  open: false // ブラウザを自動で開かない
})

js

JavaScript と Vue コンポーネントをコンパイルします。
第一引数がコンパイル対象のファイル、第二引数がコンパイル結果の配置先です。

version

コンパイルしたファイルのバージョニングが有効になります。

普通ブラウザは一度取得したファイルをキャッシュに保存するので、ファイルの内容を変更してもキャッシュが効いてしまってページに反映されないことがあります。

バージョニングを有効にすると、ビルドするたびにコンパイルしたファイルの URL にランダムな文字列を付けてブラウザがキャッシュを読まないようにします。

この機能はテンプレート側で mix 関数と組み合わせて使います。

<script src="{{ mix('js/app.js') }}" defer></script>

上のテンプレートは、以下のような HTML に変換されます。

<script src="/js/app.js?id=87459a9d906e11120dd5" defer=""></script>

クエリパラメータ id が付与されていますね。id の値はランダムに決まるのですが、要するにコンパイルのたびに URL が変わるのでブラウザからは以前とは異なるリクエストと見なされてキャッシュを読まずにサーバとの通信を行います。

これによってキャッシュのせいで変更が反映されない!という現象が起こらなくなります。

Laravel Mix についてより詳しくはマニュアル(🇺🇸 公式 / 🇯🇵 日本語)を参照してください。


以上でセッティングは完了したので、いよいよコーディングに入ります。

画面(HTML)を返す

まずは画面をレスポンスするコードから書いていきましょう。
以下の図で言うと最初の「画面要求」〜「画面(HTML)返却」を実装することになります。

SPA

ルーティング

routes/web.php を以下の内容に編集します。

web.php
<?php

// APIのURL以外のリクエストに対してはindexテンプレートを返す
// 画面遷移はフロントエンドのVueRouterが制御する
Route::get('/{any?}', fn() => view('index'))->where('any', '.+');

※上記は PHP 7.4 から導入されたアロー関数の文法を用いています。

コメントにもありますが、前章で設計した API の URL 以外の URL へのリクエストに対しては常に特定の HTML を返却する必要があります。

例えば /login という URL にアクセスがあったときでも、サーバからは上述のコードの通り index テンプレートを返却します。/login というパスに対応したコンテンツを表示するのはあくまでフロントエンド(特に Vue Router)の役割です。

{any?} で任意のパスパラメータ any を受け入れています。次に where メソッドで正規表現を用いて any パスパラメータの文字列形式を定義していますが、.+ で「任意の文字が一文字以上」つまり「なんでもいい」という指定をしています。

まとめると、any パラメータはあってもなくてもいい(?)し、ある場合はどんな文字列でもいい(.+)ということになります。

この記述によって、すべての URL で index テンプレートを返すことができます。

ちなみに後の章で紹介しますが、API の URL はどうなるかというと、この記述より上に定義します。Laravel のルーティングは記述順にリクエストにマッチするか探索されるためです。

テンプレート

テンプレートは以下の内容で resources/views/index.blade.php を作成してください。

index.blade.html
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ config('app.name') }}</title>

  <!-- Scripts -->
  <script src="{{ mix('js/app.js') }}" defer></script>

  <!-- Fonts -->
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Merriweather|Roboto:400">
  <link rel="stylesheet" href="https://unpkg.com/ionicons@4.2.2/dist/css/ionicons.min.css">

  <!-- Styles -->
  <link rel="stylesheet" href="https://hypertext-candy.s3-ap-northeast-1.amazonaws.com/posts/vue-laravel-tutorial/app.css">
</head>
<body>
  <div id="app"></div>
</body>
</html>

コンテンツは空の <div id="app"></div> だけですね。ここに Vue アプリが描画されます。

また、元ある welcome.blade.php は使用しないので削除してください。

JavaScript

次に resources/js/app.js を編集します。

app.js
import Vue from 'vue'

new Vue({
  el: '#app',
  template: '<h1>Hello world</h1>'
})

まず先に記述したルーティングの働きを確かめるために、Hello world とだけ表示させます。

フロントエンドのビルド

ビルドコマンドは npm スクリプトにまとめられているので、以下のコマンドで監視モードのコンパイルが走ります。つまり一度コンパイルが走った後に監視モードに入り、ファイルの変更があるたびに自動的に再度コンパイルが実行されます。

$ npm run watch

ちなみに、上のコマンドを初回実行したとき、コンソールに以下のメッセージが出力されます。これは Laravel Mix が足りない npm パッケージを自動でインストールするよ、と言っています。エラーなどではないのでご安心ください。少し待てば終わるので、もう一度 watch コマンドを実行しましょう。

Additional dependencies must be installed. This will only take a moment.
Running: npm install vue-template-compiler --save-dev --production=false
Okay, done. The following packages have been installed and saved to your package.json dependencies list:
- vue-template-compiler
Additional dependencies must be installed. This will only take a moment.
Running: npm install browser-sync browser-sync-webpack-plugin@2.0.1 --save-dev --production=false
Okay, done. The following packages have been installed and saved to your package.json dependencies list:
- browser-sync
- browser-sync-webpack-plugin@2.0.1
Finished. Please run Mix again.

BrowserSync によって自動的にブラウザに http://localhost:3000 が開かれます。これは自動リロードが有効になったアドレスです。以降はこちらの URL でアクセスしましょう(http://vuesplash.test もアクセスできますが自動リロードがかかりません。)。


ここまでで一度ブラウザで動きを確認しましょう。

  • / にアクセスして「Hello world」が表示されているか確かめてください。
  • さらにアドレスバーから任意のパスにアクセスしてみて、必ず index テンプレートが返却されている(つまり「Hello world」が表示されている)ことを確認してください。

Vue Router

この章の最後に、Vue Router を導入します。

インストール

まずは npm でインストールします。

$ npm install -D vue-router

ルートコンポーネント

ここから Vue コンポーネントをいくつか作成します。

まずはルートコンポーネント resources/js/App.vue を作成します。

App.vue
<template>
  <div>
    <main>
      <div class="container">
        <RouterView />
      </div>
    </main>
  </div>
</template>

ルートコンポーネントとは、コンポーネントツリーの頂上に位置するコンポーネントです。<div id="app"></div> にこのコンポーネントが描画されます。その他のコンポーネントは直接または間接的にこのコンポーネントから呼び出されることになります。

<RouterView /> に注目してください。このコンポーネントは Vue Router によって提供されています。前章の SPA での「画面遷移」 についての説明を思い出して(もしくは読み返して)ください。「URL によって HTML 部品が切り替わる」と説明しましたが、<RouterView /> が切り替わる箇所を定義します。紙芝居の枠のようなものと考えればよいでしょう。つまり URL が変わるたびに、<RouterView /> の部分に、URL に対応する HTML 部品が入れ替わって描画されます。

ページコンポーネント

さて、次に「切り替わる HTML 部品」を作成します。Vue アプリでは HTML 部品はもちろん Vue コンポーネントとして表現されます。

まず resources/js ディレクトリの下に新たに pages ディレクトリを作成します。ここをページコンポーネントの置き場所とします。

それから pages の中に以下のふたつのコンポーネントを作成してください。

写真一覧ページを表す PhotoList.vue コンポーネントです。

PhotoList.vue
<template>
  <h1>Photo List</h1>
</template>

ログインページを表す Login.vue コンポーネントです。

Login.vue
<template>
  <h1>Login</h1>
</template>

まだ有意義な内容ではありませんが、中身は後々の章で作っていきます。今のところは Vue Router による画面の切り替わりが確認できれば十分です。

ルーティング

続いてルーティング定義を作成します。
以下の内容で resources/js/router.js を作成してください。

router.js
import Vue from 'vue'
import VueRouter from 'vue-router'

// ページコンポーネントをインポートする
import PhotoList from './pages/PhotoList.vue'
import Login from './pages/Login.vue'

// VueRouterプラグインを使用する
// これによって<RouterView />コンポーネントなどを使うことができる
Vue.use(VueRouter)

// パスとコンポーネントのマッピング
const routes = [
  {
    path: '/',
    component: PhotoList
  },
  {
    path: '/login',
    component: Login
  }
]

// VueRouterインスタンスを作成する
const router = new VueRouter({
  routes
})

// VueRouterインスタンスをエクスポートする
// app.jsでインポートするため
export default router

記述内容の意味はコメントの通りです。初見でも結構わかりやすいかと思います。

最後に app.js を編集します。

app.js
import Vue from 'vue'
// ルーティングの定義をインポートする
import router from './router'
// ルートコンポーネントをインポートする
import App from './App.vue'

new Vue({
  el: '#app',
  router, // ルーティングの定義を読み込む
  components: { App }, // ルートコンポーネントの使用を宣言する
  template: '<App />' // ルートコンポーネントを描画する
})

ここでブラウザで動作を確認してみましょう。npm run watch コマンドが実行されていればコンパイルは自動的に済んでいるはずなので、ブラウザを再読み込みしてください。

  • / にアクセスしてください。URL が /#/ に変わって「Photo List」と表示されるはずです。
  • 次に /#/login にアクセスします。「Login」と表示されるはずです。

history モード

ここまでで Vue Router を使ったフロントエンドの画面遷移が実装できました。しかしお気づきの通り、URL にハッシュ # がついてしまっています。Vue Router のデフォルトの挙動は、URL 文字列中のハッシュの変化では画面遷移が発生しないブラウザの仕様を利用しているためです。

本来の URL の形を再現するには、History モードを用います。
router.js で VueRouter インスタンスを生成する際のオプションに以下を追記してください。

router.js
const router = new VueRouter({
  mode: 'history', // ★ 追加
  routes
})

ブラウザを再読み込みして動作を確認しましょう。

  • / にアクセスしてください。URL はそのままで「Photo List」と表示されるはずです。
  • 次に /login にアクセスします。「Login」と表示されるはずです。

ちなみにこの段階で当てずっぽうなパスにアクセスすると何も表示されません。index テンプレートは返却されていますが対応するコンポーネントの定義がないため何も表示できないのです。

👾 👾 👾

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

Vue Router で画面を切り替えられる仕組みが手に入ったので、次の章から認証機能の実装に取り組みます。

関連記事

連載記事(全16回)

Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう

その他


  1. Homestead を利用していて、かつ npm コマンドをゲストOS側で実行したい場合は、browserSync の引数には仮想環境のIPを指定してください。

    mix.browserSync('192.168.10.10')

    ただしこれだけでは PHP ファイルが変更されたときに自動リロードがかかりませんでした。未検証ですが files オプションを指定する必要があると思います(こういうのが面倒くさいので npm コマンドはホストOSで実行することをお勧めします)。