Docker + Laravel6 + vue-cli3でホットリロードなフロントエンド開発環境を得る。

Laravelのフロントエンド開発にvue-cliを導入したい。
あると思います。

本記事では、Laravel6にvue-cliを利用して
`yarn serve`でフロントエンドの開発環境の導入をしてみたいと思います。

なお、本記事で使用したソースはこちらに置いております。
https://github.com/kyhei/qiita-laravuel6

## 開発環境
- macOS Mojave 10.14.4
- Docker Desktop for Mac 2.1.0.2
 - Engine 19.03.2
 - Compose 1.24.1
- Laravel 6.0
- vue-cli 3.11

## Dockerイメージの構成と構築

### Docker Composeするイメージ郡概要

![スクリーンショット 2019-09-09 11.34.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94505/8951e043-9240-185b-ed41-c563145f6757.png)
上記のような構成をDocker Composeで構築したいと思います。
MYSQL、phpMyAdmin,Mailhogは本記事の中では使用しませんのでお好みでお願いします。

要点としては

- Nginxコンテナで受けたHTTPリクエストをPHP-fpm上で処理する
- Laravelのインストールやvue-cliの実行はwork-spaceコンテナで行う

の２点です。

#### Laravel Serverについて
今回は以下のような構成でLaravelサーバを構築しました。
![スクリーンショット 2019-09-09 11.51.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94505/82fb2ce0-9805-43ec-d873-6a887ed41aa8.png)
Nginxコンテナで受けたリクエストをUNIXソケット経由でPHP-fpmに伝えるという構成です。
これらの３つのコンテナ郡を便宜上`Laravel Server`と呼ぶことにします。

#### Dev Serverについて
開発作業を行うwork-spaceコンテナ上で`yarn serve`するためこちらのコンテナはそのままポートを外部に開放しております。
![スクリーンショット 2019-09-09 12.01.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94505/93182563-3201-8043-6c2e-52674a00502b.png)
work-spaceコンテナの特徴は下記の点です。

- `composer create-project laravel/laravel`ができる。
- `php artisan migrate`等が実行できる。
- `yarn serve`等Node.jsの実行ができる。

VSCodeやシェルから中に入って作業を行うためのコンテナです。
ローカルのプロジェクトディレクトリは`/work`以下にバインドしております。

Docker及びDocker Composeの定義ファイルは下記をご参照くださいませ。
https://github.com/kyhei/qiita-laravuel6

## Laravelとvue-cliのインストール

ここからようやくコードを書いて参ります。
とりあえずDocker Composeをupしておきましょう。

```bash
$ git clone https://github.com/kyhei/qiita-laravuel6.git
$ cd qiita-laravuel6
$ docker-compose up -d
```
コンテナを確認するとこんな感じになるかと思います。
![スクリーンショット 2019-09-09 12.43.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94505/4e037978-cc24-9be3-3926-ebb6fafdaade.png)

workspaceコンテナに以下のコマンドで接続します。

```bash
$ docker-compose exec workspace bash
```

Composerはインストール済みですので早速Laravelをインストールしましょう。

```
/work# composer create-project --prefer-dist laravel/laravel laravuel
/work# ls
docker  docker-compose.yml  laravuel
```
カレントディレクトリ下にLaravelプロジェクトのフォルダが作成されていたらOKです。

続いてNginxコンテナのドキュメントルートを作成したLaravelプロジェクトのpublicディレクトリに設定します。

```./docker/nginx/default.conf
server {
    listen 80;
    root /work/laravuel/public; # <- ここに設定
〜後略〜
```

設定を反映させるためにNginxコンテナを再起動しましょう。

```bash
/work# exit
$ docker-compose restart nginx
```

コンテナの再起動を確認後、ブラウザからLaravelのHello world画面が表示されていればOKです。
http://localhost:8000

## vue-cliによるフロントエンド雛形作成

再びworkspaceコンテナにログインします。

```bash
$ docker-compose exec workspace bash 
/work#
```

まずはvue-cliをグローバルにインストールしましょう。

```bash
/work# yarn global add @vue/cli
/work# which vue
/usr/local/bin/vue
```

つづいてフロントエンドの雛形を作成します。

```bash
/work# cd laravuel
/work/laravuel# vue create frontend
```
何をインストールするか色々聞かれますが
vue routerとhistoryモードを有効化にしたらあとはお好みで構いません。
（本記事ではTypeScript + [vue-property-decorator](https://github.com/kaorun343/vue-property-decorator)を使用しております。）

`yarn serve`でvue-cliの初期画面を表示してみましょう。
まずは`vue.config.js`ファイルを作成しdevServerがリッスンするポートを指定します。

```js:laravuel/frontend/vue.config.js
module.exports = {
  devServer: {
    port: 9001
  }
}
```
次に`yarn serve`を実行します。

```bash
/work/laravuel# cd frontend
/work/laravuel/frontend# yarn serve
```
http://localhost:9001
で初期画面が表示されたらOKです。

## 本番用ビルドの設定

本項の`vue.config.js`及び`package.json`の設定内容は
Evan You氏の下記のリポジトリを参考にしております。
https://github.com/yyx990803/laravel-vue-cli-3

今回はフロントエンドをSPAで構築するため、Laravelはウェブページのリクエストが来たら
常に同じHTMLファイルを返す必要があります。
Laravelのルーティングを次のように書き換えましょう。

```php:laravuel/routes/web.php
Route::get('/{any?}', function () {
  return view('index');
})->where('any', '.+');
```
簡単のためコントローラは用意せずクロージャで対応しました。
これで任意のリクエストに対して`index.blade.php`を返すことができます。

つづいてvue-cliの本番ビルド時に
`laravuel/resources/views/index.blade.php`を作成するよう設定します。

```js:laravuel/frontend/vue.config.js
module.exports = {
  devServer: {
    port: 9001
  },
  outputDir: '../public',
  assetsDir: 'assets',
  indexPath: process.env.NODE_ENV === 'production'
    ? '../resources/views/index.blade.php'
    : 'index.html'
}
```
なお、`outputDir`オプションにてCSSやJSファイルが配置されるパスを設定しています。
`assetsDir`オプションと組み合わせることで、ビルド結果のファイル郡は全て
`public/assets`下に配置されます。

最後に`public`ディレクトリ下のindex.phpなど必要なファイルをビルド時に削除してしまわぬよう、package.jsonの内容を書き換えます。

```diff:laravuel/frontend/package.json
"scripts": {
  "serve": "vue-cli-service serve",
- "build": "vue-cli-service build",
+ "build": "rm -rf ../public/assets && vue-cli-service build --no-clean",
  "lint": "vue-cli-service lint"
},
```

`--no-clean`オプションで既存のビルドファイル削除を無効しているため、
rmコマンドで既存ファイルを削除しています。

ここまでの設定を終えたらフロントエンドをビルドしましょう。

```bash
/work/laravuel/frontend# yarn build
```
publicディレクトリ下にassetsフォルダが作成され、
resources/views下にはindex.blade.phpが作成されたかと思います。

http://localhost:8000
でLaravel Serverにアクセスし、vue-cliの初期画面が表示されたらOKです。

## Dev ServerからLaravel Serverへのプロキシ設定

vue-cliのDev serverとLaravel Serverはそれぞれ別々のコンテナ上で動作しているため、
Dev server上のvue-cliプロダクトからLaravelのAPIを叩くにはDev serverでプロキシする必要があります。
具体的には下記のような感じです。
![スクリーンショット 2019-09-09 15.37.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94505/45492e98-f26c-9502-777e-4e623bf96fcc.png)

vue.config.jsのdevServer項目にproxyの設定を追加しましょう。

```js:laravuel/frontend/vue.config.js
devServer: {
  port: 9001,
  proxy: {
    '^/api': {
      target: 'http://nginx'
    }
  }
}
```

workspaceコンテナとnginxコンテナはDockerの同一ネットワーク上に存在するため、
`nginx`というホスト名で名前解決が可能です。

環境構築及び設定については以上で完了です。
お疲れ様でした！

vue.config.jsの要点をまとめると、
開発環境に関わらず、

- 出力先のファイル、ディレクトリをLaravel用に変更する。
- フロント開発サーバとバックエンドのAPIサーバとのやり取りはproxyを設定する。

となります。

まとめとして、`vue.config.js`ファイル全文を載せておきます。

```js:laravuel/frontend/src/vue.config.js
module.exports = {
  devServer: {
    port: 9001,
    proxy: {
      '^/api': {
        target: 'http://nginx'
      }
    }
  },

  outputDir: '../public',
  assetsDir: 'assets',

  indexPath: process.env.NODE_ENV === 'production'
    ? '../resources/views/index.blade.php'
    : 'index.html'
}
```

## Dev ServerとLaravel Server間の通信ハンズオン

#### サンプルのAPIを作成
`/api/ping`とリクエストしたらJSONを返すシンプルなAPIを作成します。

```php:laravuel/routes/api.php
Route::get('/ping', function () {
  return response()->json([
    'message' => 'pong'
  ], 200);
});
```

ブラウザで
http://localhost:8000/api/ping
とリクエストしたらJSON文字列が表示されるかと思います。

#### フロントエンドにHTTPクライアントの導入

今回は[axios](https://github.com/axios/axios)を導入してAPIを叩いてみます。

```bash
$ docker-compose exec workspace bash
work/# cd laravuel/frontend
work/laravuel/frontend# yarn add axios
```
インストールが完了したらaxiosインスタンスを作成する処理を記述します。

```ts:laravuel/frontend/src/modules/axios.ts
import axios from 'axios'
const axiosInstance = axios.create()
export default axiosInstance
```

#### HelloWorldコンポーネントの修正

HelloWorldコンポーネントを修正し、
Laravel Serverからのレスポンスをブラウザで表示してみます。

```vue:laravuel/frontend/src/components/HelloWorld.vue
<template>
  <div class="hello">
    <h2>Request for Laravel Server</h2>
    <h3>/api/ping</h3>
    <h3>Response: {{apiText}}</h3>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import axiosInstance from '@/modules/axios'
import { AxiosResponse } from 'axios'

interface Ping {
  message: string
}

@Component({})
export default class HelloWorld extends Vue {
  apiText: string | null = null

  async initialize() {
    const res: AxiosResponse<Ping> = await axiosInstance.get('/api/ping')
    if (res.status === 200) {
      this.apiText = res.data.message
    }
  }

  created() {
    this.initialize()
  }
}
</script>
<style scoped>
〜後略〜
```

TSを使っていない等の環境の違いについては適宜読み替えていただけたらと思います。
`yarn serve`を実行しDev Serverにアクセスしてみましょう。
http://localhost:9001
![スクリーンショット 2019-09-09 17.37.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94505/e8895663-27c2-af29-b477-ec012240d849.png)

成功です！！！
もちろん`yarn serve`の実行下ではホットリロード等、開発サーバの恩恵を得られますので
いろいろいじってみるとリアルタイムで変更が反映されるかと思います。

最後に`yarn build`でフロントエンドをビルドし、
http://localhost:8000
で同じ結果を得られるか確認してみてください。

##終わりに
Dockerの構築からハンズオンまで盛りだくさんになってしまいました。

フレームワークに依らず、
**outputDirとdevServer.proxyの設定さえ上手いことできたら
後はいつものvue-cliで開発を進められる！！**
ということが感じられてたら嬉しいです。

##参考
https://github.com/yyx990803/laravel-vue-cli-3

##使用したDockerイメージ
https://github.com/kyhei/qiita-laravuel6