2019.01.12

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


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

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

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

まずはプロジェクトを作成します。詳細な手順は別記事に書きましたし、他のサイトや書籍にもまとまっているかと思いますので、ポイントのみ記載します。

Laravel Valet を使う場合(Macのみ)

導入

こちらの記事を参考に必要なソフトウェアをインストールして Laravel Valet を導入しましょう。

データベース作成

$ psql -U postgres
Password for user postgres: 
psql (9.6.3)
Type "help" for help.

postgres=# CREATE DATABASE vuesplash;
CREATE DATABASE
postgres=# \q

プロジェクト作成

valet park コマンドを実行したディレクトリでプロジェクトを作成します。

$ composer create-project laravel/laravel vuesplash

Homestead を使う場合

Homestead を初めて使う場合はこちらの記事を参考にして下さい。

設定ファイル更新

Homestead.yml
sites:
    # sitesに以下の設定を追加する
    - map: vuesplash.test
      to: /home/vagrant/code/vuesplash/public

databases:
    # databasesに今回使用するDBを追加する
    - vuesplash

hosts ファイル更新

192.168.10.10  vuesplash.test

他のドメイン設定がすでにある場合は下記のように半角スペース区切りで記述します。

192.168.10.10  another-app.local vuesplash.test

プロジェクト作成

$ vagrant up
$ vagrant ssh
$ cd ~/code
$ composer create-project laravel/laravel vuesplash

npm コマンド

Homestead を使用する場合も、npm コマンドはホストOS側で実行することをお勧めします。

ゲストOSで npm コマンドを使用すると、Windows がホストの場合にたまに npm install に失敗しますし、後述しますが BrowserSync を期待通り動かすために設定の変更が必要でした。

ホストOSで npm コマンドを使用する場合、当然ですがホスト側に Node をインストールしておきましょう(npm は Node に添付されます)。

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

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

package.jsondevDependencies を以下の通り書き換えます。
bootstrap などいくつかのパッケージは使用しないので削除しました。

package.json
"devDependencies": {
  "axios": "^0.18",
  "browser-sync": "^2.26.3",
  "browser-sync-webpack-plugin": "2.0.1", 
  "cross-env": "^5.1",
  "laravel-mix": "^4.0.7",
  "resolve-url-loader": "^2.3.1",
  "sass": "^1.15.2",
  "sass-loader": "^7.1.0",
  "vue": "^2.5.17",
  "vue-template-compiler": "^2.5.21"
}

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

$ npm install

スタイルシート

イントロダクションで述べたとおり、スタイルシートのコーディングはこのチュートリアルの範疇外とします。このリンクから作成済みの SCSS をまとめた Zip ファイルをダウンロードして、内容を resources/sass ディレクトリに展開してください。元あった SCSS ファイルは使用しないので削除しましょう。ちなみにレスポンシブには対応できていませんのであしからず 😅

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')
  .sass('resources/sass/app.scss', 'public/css')
  .version()

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

browserSync

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

js

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

sass

Sass または Scss ファイルをコンパイルします。
第一引数がコンパイル対象のファイル、第二引数がコンパイル結果の配置先です。

version

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

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

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

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

<script src="{{ mix('js/app.js') }}" defer></script>
<link href="{{ mix('css/app.css') }}" rel="stylesheet">

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

<script src="/js/app.js?id=87459a9d906e11120dd5" defer=""></script>
<link href="/css/app.css?id=ef203d7a34fbe16e23c9" rel="stylesheet">

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

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

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


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

画面(HTML)を返す

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

SPA

ルーティング

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

web.php
<?php

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

コメントにもありますが、前章で設計した 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="dns-prefetch" href="//fonts.gstatic.com">
  <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 href="{{ mix('css/app.css') }}" rel="stylesheet">
</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

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章はこれでおしまいです。
ここまでのソースコードはリポジトリの chapter-3 ブランチに置いてあります。

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

関連記事

連載記事(全16回)

その他


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

    mix.browserSync('192.168.10.10')

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


<!-- 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) -->