Windows10HomeのLaradockでHMRをやってみる

## 仮想環境下でのLaravel MixでHMR
LaravelにはLaravel Mixというwebpackベースのビルドツールがあり、ReactやVueなどのフロントのビルドがかなり簡単に行えてしまう機能が実装されています。またあらかじめHMR（Hot Module Replacement）という動的にコンポーネントの内容を置き換えるためのコマンドも用意されています。
Reactなどのコンポーネント系のフロント開発の場合、変更が即座に反映されるHMRはかなり効率的に開発が出来るになるので、これをLaravelの環境下で動かすことに挑戦してみました。

LaradockというDockerで動作するLaravelのオールインワンパッケージ的なものを使ってHMRを実現してみようと思ったのですが、環境上の問題でいろいろハマったので方法をメモしておきます。

## 環境構成
### 開発
・Windows10 Home
・Docker for Windows
・Oracle Virtual Box(10HomeのためDockerはVirtualBoxで動作)

### サーバーサイド
・Laradock(Laravel Framework 5.6)

### フロントエンド
・React 16.2

## 初期設定
※ Laradockのインストールとプロジェクト作成周りの設定は既に終わっている前提とします。
ここでは例としてtestProjectというプロジェクトを作った想定で進めます。

まずlaravelの標準のフロントエンドフレームワークはVueになっているので、reactに切り替えます。

### npm周りの設定
```
# php artisan preset react

React scaffolding installed successfully.
Please run "npm install && npm run dev" to compile your fresh scaffolding.
```
package.jsonがReact用のものに切り替わります。便利。
「npm install && npm run devを実行してね」的なメッセージが出るのでとりあえずnpm installを実行してパッケージをインストールします。

```
# npm install --no-bin-links --no-optional
```
#### --no-bin-links：
ホストOSがWindowsの場合はシンボリックリンクが張れない都合上、これを付けないとエラーとなってしまうので、これを回避するためのオプションです。

詳しく知りたい方はこの辺りを参考に。
http://eiua-memo.tumblr.com/post/117361529158/npmvagrantvagrant%E3%81%AE%E5%85%B1%E6%9C%89%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E4%B8%8A%E3%81%A7npm

#### --no-optional：
fseventsのエラーを回避するためのオプション。


#### 大量のエラーが発生する場合
nodeとnpmのバージョンの組み合わせが合っていない場合があります。
例えばNodeのバージョンが8.11のときの最適なnpmは5.6なので、バージョンの組み合わせが異なるときはバージョン指定でインストールして調整します。

```
# node -v
v8.11.4
# npm -v
6.2

# npm install -g npm@5.6

# npm -v
5.6
```

### HTMLテンプレートの修正
/resources/views/welcome.blade.php を開いて、以下のように編集します。

```/testProject/resources/views/welcome.blade.php 
<!DOCTYPE html>
<html lang="ja" dir="ltr">
  <head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>test</title>
  </head>
  <body>
    <div id="content"></div>
    <script src="js/app.js"></script>
  </body>
</html>
```

### Reactのコードを記述
簡単なテストコードを/resources/assets/js/app.jsxに設置します。

```/testProject/resources/assets/js/app.jsx
import React from 'react';
import ReactDOM from 'react-dom'

ReactDOM.render(
  <div>
    <p>hello! World.</p>
  </div>
  ,document.getElementById("content")
)
```

### 通常モードでテスト実行
環境設定の確認で、試しにHMRではない普通モードで実行してみます。

```
# npm run dev

> @ dev /var/www/testProject
> npm run development


> @ development /var/www/testProject
> cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js

...

```

#### cross-envのエラーが出る場合
globalにcross-envをインストールします。

```
# npm install -g cross-env
```



完了したら、ホストOS側のブラウザで実行確認
http://192.168.11.100 （VirtualBox内のDockerで割り当てられるアドレス、もしくはホストOSのhostsで割り当てたドメイン）

hello! World.

おそらくこんな感じのものが表示されます。

## HMRを試す


package.jsonを見ると、

```/testProject/package.json
{
    "private": true,
    "scripts": {
        "dev": "npm run development",
        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
        "watch": "npm run development -- --watch --progress",
        "watch-poll": "npm run watch -- --watch-poll",
        "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
        "prod": "npm run production",
        "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
    },
--- 略 ---
```

npm run hotというコマンドが用意されているので、これをそのまま実行してみます。

```
# npm run hot
> @ hot /var/www/testProject
> cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js

 DONE  Compiled successfully in 20010ms
```

こんな感じで無事実行された感じになりますので、ブラウザを開いて確認。

http://192.168.11.100 (IPもしくはhostsで割り当てたドメイン)

hello! World.

コンポーネントを変更してみます。

```/testProject/resources/assets/js/app.jsx

ReactDOM.render(
  <div>
    <p>hello! Laravel World.</p>
  </div>
  ,document.getElementById("content")
)
```

すると…

hello! World.

何も起こりません。。笑

## 問題点

Laravel MixのMHRはそもそも自身のOS内（コマンドを実行した環境内）でプレビューする環境を想定されています。そのため仮想環境内で変更した結果がホストOSに伝搬されず、結果何も起こらないという状態になります。

またHMRでのビルド結果は（仮想環境内の）メモリ上に展開されるだけであり、実際のファイル書き出しを行わないため、そのビルド結果がファイルに出力されない > ファイルの書き換えが起こらない > 書き換え前の結果が表示される　という感じになります。


## 解決方法

### laradockのnginxを使わない
laradockではポート80をnginxに受け渡すようになっていますが、このままだとnginxを介してのPHPサーバーとしての結果が返ってしまいHMRが提供する仮想サーバー(localhost:8080)の結果が拾えません。
またlocalhost:8080の結果をホストOSに返すルートもないので、それを設定で作ってあげる必要があります。

### ポートフォワードの設定変更
一度Laradockを止めます。

```
# docker-compose stop 
```
8080のポートフォワーディングを追記します。
nginxで使っているポート80ではなく8080にアクセスすることでHMRの仮想サーバーを拾えるようにします。
具体的にはlaradock/docker-compose.ymlにて以下を追記します。

```/laradock/docker-compose.yml
# -- 略 --
services:

### Workspace Utilities ##################################
    workspace:
　　 # -- 略 --
      ports:
        - "${WORKSPACE_SSH_PORT}:22"
        - "8080:8080" # ←　追記
```

再度コンテナを起動

```
# docker-compose up -d nginx workspace
```

### webpack-dev-serverの設定変更
HMRでメモリ上に仮想で作られるサーバーの設定を変更します。サーバーはwebpack-dev-serverで作られるので、通常はこの設定を変更するためにwebpack.config.jsを変更しますが、Laravel Mixでは専用の設定ファイルであるwebpack.mix.jsを変更します。

先に結論から書いてしまいますが以下のように変更します。

```/testProject/webpack.mix.js
let mix = require('laravel-mix');

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.js('resources/assets/js/app.jsx', 'public/js')
  .webpackConfig({
    output: {
      publicPath: "http://192.168.99.100:8080/" // (ホストOSのhostsで割り当てたドメイン):8080 でもＯＫ
    },
    devServer: {
      disableHostCheck: true,
      contentBase: path.join(__dirname, "public"),
      publicPath: '/',
      host: '0.0.0.0',
      port: 8080,
      proxy: {
        '/': {
          target : 'http://nginx'
        }
      }
    },
    watchOptions: {
      poll: true,
      ignored: /node_modules/
    }
  })

```

### output:
publicPathで親ホストのブラウザで表示する際のアドレスを記載します。HMRで動的に更新した際にこのパスを基準に配信されます。

#### devServer:
hostに0.0.0.0を、portに8080を指定することで、ポートフォワーディングした8080へ接続します。
またdisableHostCheckのオプションをtrueにすることで、外部公開時のドメインチェックをスルーさせます。これでwebpackDevServerのlocalhost以外の外部からの接続が可能になります。

なおproxyのtargetで`http://nginx`を指定していますが、これはwebpack-dev-serverがHTMLサーバーの機能のみでPHPの実行が出来ないため、LaravelのPHPを実行させて結果を反映させるためにproxyを指定しています。

`http://nginx`の表記については、docker-composeの内部link機能でnginxコンテナが"nginx"というホスト名で登録されているため、nginxサーバーの出力結果を内部で参照して見に行くよ、ということになります。

#### watchOptions:
poll:trueを指定することで、ホストOSが仮想環境下の結果をポーリングして確認するようになります。
またignoredでwatch対象の除外設定をしています。

### Reactの変更
Reactは実はそれ自身ではHMRに対応しておらず、パッケージを追加することで対応できるようになります。

パッケージを追加して、

```
# npm install -D  --no-bin-links --no-optional react-hot-loader
```

下記のようにAppContainerのコンポーネントで囲む形にします。

```/testProject/resources/assets/js/app.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader'; // ←追記

ReactDOM.render(
  <AppContainer> {/* ←追記 */}
    <div>
      <p>hello! World.</p>
    </div>
  </AppContainer> {/* ←追記 */}
  ,document.getElementById("content")
)

// ↓追記
if(module.hot) {
  module.hot.accept();
}
```

### 実行確認

ホストOSのブラウザで以下URLを開いて確認します。
http://192.168.11.100:8080 (IPもしくはhostsで割り当てたドメイン。ポート8080に変更することでHMRの仮想サーバーへ接続させています)

hello! World.

再度コンポーネントを変更して保存してみます。

```/testProject/resources/assets/js/app.jsx

ReactDOM.render(
  <AppContainer>
    <div>
      <p>hello! Laravel World.</p> {/* hello! World. →　hello! Laravel World.*/}
    </div>
  </AppContainer>
  ,document.getElementById("content")
)
```

すると…

hello! Laravel World.

リロード無しでコンポーネントが変更されました！

## 所感
もっと簡単にできると思っていましたが、構成も複雑さもなんだか難易度高めになってしまいました。。
もっといい方法あるようという方（たぶんありますので）是非コメントください。