最近は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について詳しくみていきましょう。
長すぎて読む時間のない人へ
サンプルのリポジトリが以下にあるので、遊んでみてください。
前提
- 読者はVue, Laravelの基本的な知識を持っている
- 読者はdockerコマンドを一度くらいは叩いたことがある
- 記事内のcomposerコマンドはDocker経由で行います(Docker使わない人は適宜読み替える必要があります)
- Laravelのローカルでの開発は Vessel を使います
- フロントエンド開発は(Dockerを使わず)ローカルで行います
- Vue CLI 3 がインストールされているものとします
構成
前編の構成としては至ってシンプルです。
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のトップ画面が表示されるはずです。
ダミーAPIの作成
Vueから使える 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 のリポジトリが参考になります。
このセクションでやろうとしていることの概要は次のようなイメージです:
Laravel Mixのファイルを削除
Laravel Mixを使わないので不要なファイルを削除しましょう。
rm -rf package.json webpack.mix.js yarn.lock resources/assets
新しいVueプロジェクトの作成
※ 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アプリが増えていくと仮定)
下図のような構成になるようにします:
# 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 serve
で webpack devServer
と Hot 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を作成
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 にアクセスしてみましょう。
上のような画面が出てきたら一応正解です。でもなんだかおかしいですね。
特に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.js
の publicPath
を /app
に指定しているので、VueRouterが base
を /app
だと勘違いしちゃっています。ちょっと不格好ですが、 app1
配下で配信されることを前提としてハードコードで直しちゃいましょう。
// frontend/src/app1/router.js export default new Router({ mode: 'history', base: '/app1/', // ←修正
再度 npm run build
して http://localhost/app1 にアクセスして、以下の画面になれば成功です!
API結合
前編の仕上げとして、最後にVueとLaravel間でAPIの結合をしましょう。VueアプリからLaravelへajaxリクエストを正常に行えるかチェックします。
修正した内容が継続的に build
されるように次のコマンドを実行しておきます:
npm run build -- --watch
上の画面の枠で囲った場所はハードコードされているのですが、ここを 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; } };
リフレッシュして開発者ツールのネットワークを確認してみます。
/api/ecosystems
に対してリクエストしていて、レスポンスもちゃんと返ってきています!Laravelへのリクエストも問題なくできてそうですね!
親コンポーネントから子コンポーネントへデータを渡す
最後に ajax
でとってきた情報を HelloWorld
コンポーネントに渡しましょう。
まずは props
で ecosystems
を受け取れるようにしておいて、
// src/app1/components/HelloWorld.vueの<script> export default { name: 'HelloWorld', props: { msg: String, // ecosystemsを受け取れるようにしときます ecosystems: Array, } }
template
で v-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の連携としては完成です!
ここまででいったんVueとLaravelの結合は完了となります。Vue CLIとLaravelのそれぞれの依存度を最小にして、DX(Developer Experience)を保つ手法として使えるのではないでしょうか。(
今はVueのアプリが一つしかないので、 後編 は複数のVueアプリを作る方法について見ていきましょう!