CYDAS Developer's Blog

サイダス技術者ブログ

【前編】Vue CLI + Laravel によるMSPA (Multi-Single Page Application)

f:id:kfukuyama:20190225131307p:plain

大阪からこんにちは、福山健@kenfdev)です!

最近はLaravelとVueを触ることが多く、開発のしやすい仕組みづくりについて考えてます。Laravelは最初からフロントエンドの開発がしやすいように Laravel Mix の仕組みが用意されています。これを使うとさくっとVueやReactなどのSPAのパイプラインもLaravelに組み込むことができます。

とはいえ、

  • バックエンドにLaravelを使い続けるかわからない
  • フロントエンドエンジニアにLaravelを極力意識させたくない
  • Vue CLIをそのまま使いたい
  • Vue CLIの中でも pages 機能を使ってMSPA(Multi-Single Page Application)を作りたい

というような思いがある人も一定数いるのではないでしょうか?

上記をふまえて、VueCLIを使ってフロントエンドを作り、バックエンドをLaravelで作成しつつ複数のSPA(Multi-Single Page Application)を作る方法について紹介したいと思います。前編はMSPA(Multi-Single Page Application)ではなく単一のSPAを作るところまでにして、 後編 はMSPAについて詳しくみていきましょう。

長すぎて読む時間のない人へ

サンプルのリポジトリが以下にあるので、遊んでみてください。

github.com

前提

  • 読者はVue, Laravelの基本的な知識を持っている
  • 読者はdockerコマンドを一度くらいは叩いたことがある
  • 記事内のcomposerコマンドはDocker経由で行います(Docker使わない人は適宜読み替える必要があります)
  • Laravelのローカルでの開発は Vessel を使います
  • フロントエンド開発は(Dockerを使わず)ローカルで行います
  • Vue CLI 3 がインストールされているものとします

構成

前編の構成としては至ってシンプルです。

f:id:kfukuyama:20190222133015p:plain
前編の構成

Laravelの準備

Laravelをscaffold

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

docker run --rm -it \
    -v $(pwd):/opt \
    -w /opt shippingdocker/php-composer:latest \
    composer create-project laravel/laravel laravel-with-vue-cli

サーバーをVesselで立ち上げる

Laravelの開発には Vessel を使います。(Docker使って便利に開発しましょうってツールです。)

Vesselの用意

# ディレクトリ移動
cd laravel-with-vue-cli

# Vesselのインストール
docker run --rm -it \
    -v $(pwd):/opt \
    -w /opt shippingdocker/php-composer:latest \
    composer require shipping-docker/vessel

# vesselコマンドが使えるようにする
docker run --rm -it \
    -v $(pwd):/opt \
    -w /opt shippingdocker/php-composer:latest \
    php artisan vendor:publish --provider="Vessel\VesselServiceProvider"

# vesselの初期化
bash vessel init

Vessel開始

次のコマンドでvessel経由でDockerのLaravel環境が立ち上がります

./vessel start

起動が完了したら http://localhost にアクセスしてみましょう。Laravelのトップ画面が表示されるはずです。

laravel-top
Laravel

ダミーAPIの作成

Vueから使える Rest API を用意しておきたいので、ダミー(固定値しか返さない)で作ります。

f:id:kfukuyama:20190222133711p:plain
Rest API部分の作成

Controller作成
docker run --rm -it \
    -v $(pwd):/opt \
    -w /opt shippingdocker/php-composer:latest \
    php artisan make:controller API/EcosystemController --api

app/Http/Controllers/API/EcosystemController.php の内容は次のようにします。

<?php

class EcosystemController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return response()->json([
            'vue-router' => 'https://router.vuejs.org/',
            'vuex' => 'https://vuex.vuejs.org/',
            'vue-devtools' => 'https://github.com/vuejs/vue-devtools#vue-devtools',
            'vue-loader' => 'https://vue-loader.vuejs.org/',
            'awesome-vue' => 'https://github.com/vuejs/awesome-vue'
        ]);
    }
}

routes/api.php に以下を追加して GET /api/ecosystems にAPIが公開されるようにします。

<?php
Route::get('/ecosystems', 'API\EcosystemController@index');

では、Postmanなど使って GET http://localhost/api/ecosystems を実行してみましょう。次のレスポンスが返ってくるはずです。

{
    "vue-router": "https://router.vuejs.org/",
    "vuex": "https://vuex.vuejs.org/",
    "vue-devtools": "https://github.com/vuejs/vue-devtools#vue-devtools",
    "vue-loader": "https://vue-loader.vuejs.org/",
    "awesome-vue": "https://github.com/vuejs/awesome-vue"
}

これでバックエンド側はいったん完成です。次にフロントエンド側も作っていきましょう。

Vue CLI プロジェクトとLaravelの連携

フロントとしてはLaravel Mixを使うではなくて、Vue CLIで作ったプロジェクトをLaravelと連携します!この手順は主にVueのEvan You氏の yyx990803/laravel-vue-cli-3 のリポジトリが参考になります。

このセクションでやろうとしていることの概要は次のようなイメージです:

f:id:kfukuyama:20190222152017p:plain
VueCLIとLaravel

Laravel Mixのファイルを削除

Laravel Mixを使わないので不要なファイルを削除しましょう。

rm -rf package.json webpack.mix.js yarn.lock resources/assets

新しいVueプロジェクトの作成

f:id:kfukuyama:20190222145006p:plain
Vue CLI

Vue CLI をインストールしておいてください

Vue CLIを使って新しいプロジェクトを作ります。このとき、 features の選択肢で Router を追加しておきましょう(SPAのページ遷移もほしいので)。

$ vue create frontend

Vue CLI v3.4.0
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files

Vueの Multi-Page Applicationの準備

前篇ではVueアプリは一つしか作りませんが、 後編 に向けて複数(MSPA)作っていくことを前提にしているので、 src 配下は1段下げて管理します。app1 とでもしておきましょう。( app2, app3 とVueアプリが増えていくと仮定)

下図のような構成になるようにします:

f:id:kfukuyama:20190222141543p:plain
srcの構成

# frontendディレクトリに移動
cd frontend

# 階層を1段下げるために今のsrcをapp1とする
mv src app1
# srcディレクトリを改めて作って
mkdir src
# その中に `app1` を入れる
mv app1 src/

階層が変わったので src/app1/views/Home.vue のコンポーネントを import しているところも忘れずに修正します。

...
// importのパスを修正
import HelloWorld from "@/app1/components/HelloWorld.vue";
...

Vue CLIには pages という便利な機能が備わっているので、簡単に複数のVueアプリを一括管理することができます。このあたりも詳細は 後編 で述べます。

frontend/vue.config.js というファイルを作成して、以下のような内容にしましょう。

module.exports = {
    // VueアプリをMSPA対応させる(↓の内容は次のセクションですぐ書き直します)
    pages: {
        app1: 'src/app1/main.js',
    },
};

いったんここまでとして、次に本格的にLaravel側との連携を行います。

VueアプリをLaravelで配信

では、今のところ個別のVueアプリとLaravelアプリがあるだけなので、2つがちゃんと連動するようにしましょう。具体的には:

  • npm run build したときにアセットがLaravel配下に作られる
  • Laravelから index.html にそうとうするものを配信する(アセットが配信できるように)

ようにします。

注意: 今から作る構成では、 npm run servewebpack devServerHot Code Replace (HCR) はできません。Laravelからアセットを配信したい(CSRFトークンなど、サーバーサイドで注入したいものがある)、ということとそうした場合にたぶんHCRはできないんじゃないかと思っているからです(違っていたら誰か教えてくださいー)。 なので、ソースを更新するとビルドは自動で走らせることができますが、ブラウザのリフレッシュは必要になります。

public/index.html の移動

frontend ディレクトリ内での作業になります

まず、 public/index.html は、LaravelのControllerに生成してもらうことになるので、 Vueとしての public 配下に置くのをやめます。代わりにVue CLIの pages では、アセット(js, cssなど)をテンプレートに自動注入する設定を pages.<アプリ名>.template で指定することができるため、ここに今までの index.html を、baseのテンプレートとして指定するようにします。

# ※frontendディレクトリ内で作業
# アセット注入用のテンプレートを入れておく場所を作成し
mkdir templates
# index.htmlをbase.htmlとして移動する
mv public/index.html templates/base.html

vue.config.jsの調整

では、 npm run build したときに意図したアセットが意図した場所に作られるために改めて vue.config.js を編集します。

module.exports = {
    // アセットはLaravelの `public` の `app` ディレクトリ配下に作成されるようにする.
    // appの中身はすべて自動生成されるものなので、バージョン管理からはずしておくことができます
    outputDir: '../public/app',

    // app配下にjs, cssなどが置かれるので、publicPathを調整する
    publicPath: '/app',

    // app1
    pages: {
        // app1のエントリポイント、テンプレート、出力先を調整
        app1: {
            entry: 'src/app1/main.js',
            template: 'templates/base.html',
            filename: `../../resources/views/spa/app1.blade.php`,
        },
    },
};

上の内容を行うことで、Laravelの resources/views/spa 配下に作られるようになります。 以下のディレクトリは自動生成したものしか作られないので、 .gitignore で無視しちゃいましょう(競合や最新版の生成忘れを防ぐために、CIで作らせておいたほうが安全)。

# .gitignore
/public/app
/resources/views/spa

LaravelにSpaControllerを作成

f:id:kfukuyama:20190222143424p:plain
SpaController

Vue CLI側で作ったアセットを配信する SpaController を作ります。

docker run --rm -it \
    -v $(pwd):/opt \
    -w /opt shippingdocker/php-composer:latest \
    php artisan make:controller SpaController

次の内容にして、Vue CLIが作ったアセットを配信できるようにしましょう。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SpaController extends Controller
{
    public function app1()
    {
        return view('spa/app1');
    }
}

そして、 routes/web.php を調整して /app1/<このあとはなんでもOK> にアクセスがきた場合にベースの HTML を配信できるようにします。

<?php

Route::get('/app1{any}', 'SpaController@app1')->where('any', '(/?$|/.*)');

結合!

ここまでで概ね連携が完了しました。それではまずはVueのアセットを作成しましょう!

アセットの配信
# frontendディレクトリにいること
npm run build

完了したら http://localhost/app1 にアクセスしてみましょう。

f:id:kfukuyama:20190217221244p:plain
http://localhost/app1にアクセスした画面

上のような画面が出てきたら一応正解です。でもなんだかおかしいですね。 特にURLが http://localhost/app/1 となって、なぜか app/1 と分割されているところも怪しいです。

これは frontend/src/app1/router.js が悪さをしちゃってます。

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL, // ←ここ

vue.config.jspublicPath/app に指定しているので、VueRouterが base/app だと勘違いしちゃっています。ちょっと不格好ですが、 app1 配下で配信されることを前提としてハードコードで直しちゃいましょう。

// frontend/src/app1/router.js
export default new Router({
  mode: 'history',
  base: '/app1/', // ←修正

再度 npm run build して http://localhost/app1 にアクセスして、以下の画面になれば成功です!

f:id:kfukuyama:20190217221923p:plain

API結合

f:id:kfukuyama:20190222152504p:plain
Vue + Laravel API

前編の仕上げとして、最後にVueとLaravel間でAPIの結合をしましょう。VueアプリからLaravelへajaxリクエストを正常に行えるかチェックします。

修正した内容が継続的に build されるように次のコマンドを実行しておきます:

npm run build -- --watch

f:id:kfukuyama:20190222112615p:plain

上の画面の枠で囲った場所はハードコードされているのですが、ここを ajax でLaravelの /api/ecosystems からとってくるようにして、Vueで動的に表示できるようにしてみましょう!

ajaxリクエスト

ajaxのリクエストをするために定番の axios を入れます。

npm install axios

そして、 src/app1/views/Home.vue でAPIを叩けるようにします。次のように修正しましょう。

// src/app1/views/Home.vue の <script> 内
import HelloWorld from "@/app1/components/HelloWorld.vue";
import axios from "axios";

export default {
  name: "home",
  components: {
    HelloWorld
  },
  data() {
    return {
      ecosystems: [],
    }
  },
  async mounted() {
    const res = await axios.get("/api/ecosystems");
    // 子コンポーネントが扱いやすい形に整形しときます
    const ecosystems = Object.keys(res.data).map(key => {
      return {
        name: key,
        link: res.data[key]
      };
    });
    this.ecosystems = ecosystems;
  }
};

リフレッシュして開発者ツールのネットワークを確認してみます。

f:id:kfukuyama:20190222115057p:plain
ajaxが成功しているログ

/api/ecosystems に対してリクエストしていて、レスポンスもちゃんと返ってきています!Laravelへのリクエストも問題なくできてそうですね!

親コンポーネントから子コンポーネントへデータを渡す

最後に ajax でとってきた情報を HelloWorld コンポーネントに渡しましょう。

まずは propsecosystems を受け取れるようにしておいて、

// src/app1/components/HelloWorld.vueの<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String,
    // ecosystemsを受け取れるようにしときます
    ecosystems: Array,
  }
}

templatev-for を使って表示できるようにします:

<!-- src/app1/components/HelloWorld.vueの<template> -->
...
    <h3>Ecosystem</h3>
    <template v-if="ecosystems.length > 0">
      <ul>
        <li :key="item.name" v-for="item in ecosystems">
          <a :href="item.link" target="_blank" rel="noopener">{{ item.name }}</a>
        </li>
      </ul>
    </template>
    <template v-else>
      Loading...
    </template>
...

そして、親(Home.vue)から ecosystems を渡しましょう!

<!-- src/app1/views/Home.vueの<template> -->
  <div class="home">
    <img alt="Vue logo"
         src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App" :ecosystems="ecosystems" />
  </div>

リフレッシュしてみて↓のようになっていればVueとLaravelの連携としては完成です!

f:id:kfukuyama:20190222120506g:plain
ajaxリクエストをしている様子

ここまででいったんVueとLaravelの結合は完了となります。Vue CLIとLaravelのそれぞれの依存度を最小にして、DX(Developer Experience)を保つ手法として使えるのではないでしょうか。(

今はVueのアプリが一つしかないので、 後編 は複数のVueアプリを作る方法について見ていきましょう!