CYDAS Developer's Blog

サイダス技術者ブログ

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

f:id:kfukuyama:20190225131307p:plain

こんにちは、福山健@kenfdev)です!

この記事は 【前編】Vue CLI + Laravel によるMSPA (Multi-Single Page Application) の続きになります。 前編 を読んでいることを前提としているのでご注意ください。

長すぎて読みたくない人へ

コードを見たほうが早い!って人は以下にこの記事に関連するリポジトリを公開しているので遊んでみてください。

github.com

後編でやること

後編ではMSPA(Multi-Single Page Application)の部分に着目します。

  • MSPAについて
  • app2 を作ってみる
  • まとめ

という内容でお届けします。

MSPA(Multi-Single Page Application)とは?

「MSPA MSPA」って言ってますけど、たぶんMSPAという正式な略語は無いと思います。SPA(Single Page Application)と違って、1ページ毎にサーバーがレンダリングするようなアプリケーションをMPA(Multi Page Application)と呼ぶことはあります。でも、この記事で作るものはただのMPAでは無く、 複数のページでそれぞれのSPAがある のでMSPA(Multi-Single Page Application)と呼ばせていただきます。

以下の悩みがある人にはMSPAがフィットするかもしれません。

  • 一つの巨大なSPAを作りたくない。
  • ドメインが明確に分かれている機能間の画面遷移までニュルニュル遷移する必要がないからSPAを分けたい。
  • 複数チームで分かりやすく、SPAを分担して開発したい

でもMSPAを作ろうと思った場合に悩みもあります:

  • 複数のSPA作りたいけど、できればパイプラインは統一したい(configの冗長管理したくない)
  • みんな同じ外部ライブラリ使うのに冗長なpackage管理したくない
  • 安易な共通化は危険。。。と理解しつつも共通利用できるものは複数のSPAから同じオレオレライブラリっぽいところから使いたい

通常であれば↑を真面目に考えないといけないのですが、Vue CLI を使うとこれらを意識せずにいい感じに管理してくれる pages という機能があります。 前編 ではこの機能を既に使わせていただいているのですが、単一のSPAしか作っていないという意味のない状態になっています。後編ではここにもう一つSPAを加えてMSPAにしましょう!

【余談】 上の「悩み」のところにもあるように、安易な共通化は長い目で見たときに負債になる可能性が高くなります。外部ライブラリの共通化も、本気でマイクロサービス化などでアプリを分けた場合には「独立したデプロイ」ができなくなるので、注意が必要です。ただし、それらを理解した上で、いざ別のアプリ(コードベース)に切り出すときには pages 機能でVueのアプリを切り出すのは比較的容易に行えるはずです。

構成

後半の構成は次の通り。Vueのアプリが増えています。MSPAのコンセプトを見せたいだけなので、2つ目のVueアプリは特に処理を入れません。(最初のアプリ同様、API連携は同じように作っていくことができます)

f:id:kfukuyama:20190224205558p:plain
構成

フロントエンド app2 の作成

名前が安易すぎますが、2個めのSPAを app2 とでもしましょう。中身というよりMSPAのコンセプトだけ見せたいので、ベースは app1 から作っちゃいます。

# frontendディレクトリ内で作業します
cd frontend

# app1をapp2としてコピー
cp -r src/app1 src/app2

app2 からはrouter機能を消しちゃいましょう。

rm src/app2/router.js

# 合わせてコンポーネントもApp以外消してしまう
rm src/app2/views/*
rm src/app2/components/*
// src/app2/main.js

import Vue from 'vue'
import App from './App.vue'
// import router from './router'  ←消してしまう

Vue.config.productionTip = false

new Vue({
  // router,  ←消してしまう
  render: h => h(App)
}).$mount('#app')

Vuetifyを適用

このまま app2 を表示しても面白くないので、無駄に Vuetify を使って見た目をドラスティックに変えてみちゃいましょう。まずはVuetifyインストールしましょう。

npm install vuetify

VuetifyをVueで使うように指示します。

import Vue from 'vue'
import App from './App.vue'
import Vuetify from 'vuetify'  // ←ここ
import 'vuetify/dist/vuetify.min.css'  // ←ここ

Vue.use(Vuetify)  // ←ここ

Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')

iconも使えるようにしておきたいので、 index.html<head> にiconへのリンクを追加したいのですが、せっかくなので templates/base.html ではなく、Vuetifyアプリ用のテンプレートを用意しましょう。仮に 前編 で作った app1 でも使っている templates/base.html に追加してしまうと、 app1 はMaterial Designのアイコンが必要ないのに取得しにいってしまうという無駄が生じてしまいます。

# もともとのbase.htmlからコピーして作る
cp templates/base.html templates/vuetify-base.html

<head> 内に次の1行を増やしておきましょう。

<link href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons' rel="stylesheet">

これでVuetifyが使える状態になっているはずです。でも見た目を何も変えていないので、 App.vue を下の内容にしちゃいましょう。

<template>
  <v-app id="inspire">
    <v-navigation-drawer v-model="drawer"
                         fixed
                         app>
      <v-list dense>
        <v-list-tile @click="onClick">
          <v-list-tile-action>
            <v-icon>home</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            <v-list-tile-title>App1</v-list-tile-title>
          </v-list-tile-content>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>
    <v-toolbar color="indigo"
               dark
               fixed
               app>
      <v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
      <v-toolbar-title>Application</v-toolbar-title>
    </v-toolbar>
    <v-content>
      <v-container fluid
                   fill-height>
        <v-layout justify-center
                  align-center>
          <v-flex text-xs-center>
            <v-tooltip left>
              <v-btn slot="activator"
                     :href="source"
                     icon
                     large
                     target="_blank">
                <v-icon large>code</v-icon>
              </v-btn>
              <span>Source</span>
            </v-tooltip>
          </v-flex>
        </v-layout>
      </v-container>
    </v-content>
    <v-footer color="indigo"
              app>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  data: () => ({
    drawer: null
  }),
  props: {
    source: String
  },
  methods: {
    onClick() {
      window.location.href = '/app1';
    }
  }

};
</script>

vue.config.jsの設定

app2 を作ったところで、 vue.config.js でちゃんとビルドができるように設定しましょう。 ほとんど app1 と同じ内容になるだけです( templateが違うので注意! )。

...
    pages: {
        app1: {
            entry: 'src/app1/main.js',
            template: 'templates/base.html',
            filename: `../../resources/views/spa/app1.blade.php`,
        },
        app2: {
            entry: 'src/app2/main.js',
            template: 'templates/vuetify-base.html',
            filename: `../../resources/views/spa/app2.blade.php`,
        },
    },
...

フロントエンドのビルド

上までの手順でフロントエンド側は完了です。ビルドしてLaravel側から使えるようにしましょう!

# frontendディレクトリで作業
npm run build

Laravel連携

では、アセットの準備ができたので残りはLaravelが app2 を配信できるようにすることです。前編SpaController を作ってるので、これのメソッドを増やすことと、 Router をつなげればOKです。

SpaControllerは次のようにメソッドを増やします。

<?php
// app/Http/Controllers/SpaController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SpaController extends Controller
{
    public function app1()
    {
        return view('spa/app1');
    }
    // ↓至ってシンプル
    public function app2()
    {
        return view('spa/app2');
    }
}

そして、ここに http://localhost/app2 のリクエストがフォワードされるように web.php を修正します。

<?php
// routes/web.php

Route::get('/app1{any}', 'SpaController@app1')->where('any', '(/?$|/.*)');
// app2用のルーティングを追加
Route::get('/app2{any}', 'SpaController@app2')->where('any', '(/?$|/.*)');

これで完成です!では ./vessel start していることを確認して http://localhost/app2 にアクセスしてみましょう。

f:id:kfukuyama:20190224212531g:plain

上図のような画面が見えたなら正常にMSPAが完成しています! app2 内ではVuetifyが動いていて、そこから app1 に遷移して、 app1前編 のときと同様に動作しているのがわかります。

まとめ

Laravel Mixをあえて使わずに、Vue CLIとLaravelを連携して、なるべくDX(Developer Experience)を損なわずに開発していく方法について前編・後編と見てきました。「でもLaravel Mixの方が簡単でしょ」って意見もたくさんあると思いますが、あえてフロントをLaravelに依存させない方法で攻めています。

  • バックエンドの言語が変わるかもしれない
  • フロントエンドのコードを別リポジトリで管理したくなるかもしれない
  • Vue CLIに超素晴らしい機能が追加されたけどLaravel Mixが対応するのが遅いかもしれない

など、理由はいくつか思い浮かびます。ただ、この手法をとった場合の「Hot Reload」が効かなくなる問題は結構デメリットとして大きいと思います。不可能なのかどうかという点も含めて引き続きDXを磨いていきたいと思います。