Cloud Run上でLaravel×React(Mix使用)のサイトを爆速で動かす - ReactをCDN(Cloud Storage)にデプロイする

[前回](https://qiita.com/joolen_official/items/9624f35e98d5c0287892)は、Laravelを最適化した上で`Cloud Run`にデプロイする方法について説明しました。
これにより、バックエンドは高速に動作するようになったハズです。
次はフロント側(React)について考えていきます。

# バンドルしたjsファイルをCloud Storageにアップしてパフォーマンスを改善する

'Laravel-mix'はとても優れたライブラリだと思っています。通常であれば煩わしいTypescriptやReactのコンパイルなどの設定をラップして、最適化までしてくれるからです。しかし、デフォルトでは、`Laravel-mix` でコンパイルしたassetファイルはプロジェクト内に保管されます。
`minify`されているとはいえ、Webサイトにアクセスすする度にバンドルされた`app.js`や`app.css`を`Cloud Run`からダウンロードすることになり、最終的にはフロントの規模が大きくなった時に思ったようなパフォーマンスを出すことはできなくなります。
この部分を解消するため、`Laravel-mix`で生成したassetファイルは是非とも、CDNにおきたいところです。

ではこのために、どのようにすれば良いか説明していきます。

# laravel-mixを使ってReactを有効にする

Cloud Runに関する説明とは逸れてしまいますが、一応、説明しておきます。

## laravel/uiをインストールし、必要な設定ファイルを追加・更新する
まずはローカルで動作確認をしていきましょう。必要に応じて、ボリュームをマウントしたり、VSCodeのリモートコンテナを使ったりすると、どのようにファイルが更新されるかわかりやすいと思います。

(例)ホストのディレクトリをマウントして実行する

```sh
docker run --volume $PWD/app:/var/www/html  -p 8080:80 -d cloud-run-demo
```

では、前回の記事で作成した**Dockerコンテナ内で**下記のコマンドを実行していきます。

```sh
/var/www/html# composer require laravel/ui
/var/www/html# php artisan ui react
/var/www/html# npm install
```

これで基本的に`React`はインストールされるのですが、せっかくなので`Typescript`も導入します！
型の定義をいちいちしなければならないなど、ちょっとしんどいですが変数に何が入っているか、何を入れれば良いかが明確にわかるので弊社では重宝しています！

まずは同じくDockerコンテナ内で、下記のコマンドを実行します。

```sh
/var/www/html# npm add -D typescript @types/node @types/react @types/react-dom
```

appディレクトリの配下に、`tsconfig.json`を作成します。弊社の場合は下記のように書いています。

```json
{
    "compilerOptions": {
        "jsx": "react",
        "outDir": "./built/",
        "sourceMap": true,
        "strict": true,
        "noImplicitReturns": true,
        "noImplicitAny": true,
        "module": "es2015",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "moduleResolution": "node",
        "target": "es5",
        "allowSyntheticDefaultImports": true,
        "lib": [
            "es2016",
            "dom"
        ]
    },
    "include": [
        "resources/assets/ts/**/*"
    ]
}
```

仕上げに`webpack.mix.js`を下記の通りに書き換えてあげましょう。

```javascript
const 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.ts("resources/assets/ts/app.tsx", "public/js")
    .sass("resources/assets/sass/app.scss", "public/css")
    .options({
        processCssUrls: false
    })
    .webpackConfig({
        module: {
            rules: [
                {
                    test: /\.tsx?$/,
                    loader: "ts-loader",
                    exclude: /node_modules/
                },
                {
                    test: /\.js?$/,
                    exclude: /node_modules/,
                    use: {
                        loader: "babel-loader",
                        options: {
                            presets: ["@babel/preset-env", "@babel/react"]
                        }
                    }
                },
                {
                    test: /\.s[ac]ss$/i,
                    use: [
                        {
                            loader: "resolve-url-loader",
                            options: {}
                        },
                        {
                            loader: "sass-loader",
                            options: {
                                sourceMap: true
                            }
                        }
                    ]
                }
            ]
        },
        resolve: {
            extensions: ["*", ".js", ".jsx", ".ts", ".tsx"],
            alias: {
                react: path.resolve("./node_modules/react")
            }
        }
    });
```

ここまでで設定ファイルの変更は完了です。次からは実際に表示をさせてみましょう！

## サンプル画面を表示する。

弊社の場合、ディレクトリ構造が複雑になりそうだったので、`resources`配下に`assets`というディレクトリを作ってその下にファイルを作成しています。手順は以下の通りです。

1. デフォルトでは`resources/js`配下にあったファイルを`resources/assets/ts`に移動する。
2. 同じくデフォルトでは`resources/sass`配下にあったファイルを`resources/assets/sass`に移動する。
3. Example.jsの名前をExample.tsxに変更する
4. app.jsの名前をapp.tsxに変更する

対応はこの程度です。今回、ファイル内容の変更は行っていません。ディレクトリ の見た目は下記のようになるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/71f70ce7-135e-fce6-9288-d21c4d727898.png)

最後に、bladeの記述を変更します。

```html
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Laravel</title>
        <link rel="stylesheet" href="{{mix('css/app.css') }}">
    </head>
    <body>
        <div class="flex-center position-ref full-height">
            <div id="example"></div>
        </div>
        <script src="{{ mix('js/app.js')}} "></script>
    </body>
</html>
```

ポイントは

```html
<link rel="stylesheet" href="{{mix('css/app.css') }}">
```

であったり、

```html
<script src="{{ mix('js/app.js')}} "></script>
```

でバンドルした`css`や`js`を読み込むようにしていることです。

では、早速コンパイルして表示してみましょう！！コマンドは下記の通りです。

```sh
npm install && npm run dev
```

:bangbang:**うまくいかない場合は、下記のコマンドでキャッシュしたviewファイルをクリアしてください。**

```sh
/var/www/html# php artisan view:clear
```

エラーなくコンパイされていれば`app/public/js`や`app/public/css`配下にファイルが作られているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/8154a1a3-9c2f-4f2a-c5ee-228f43fb9f28.png)


そしてローカルで下記の画面が表示されればOKです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/2092425b-aac8-c2b5-ad08-f057b8634196.png)

長くなってしまいましたが、Laravel-mixを使ってReactをインストールすることができました！

# Cloud Strageにアップするためのwebpackの書き方 

次はいよいよ本題、生成した`js`や`css`をCloud Strageにアップする手順を紹介します。

## webpack-google-cloud-storage-pluginをインストールする

Dockerコンテナ内で下記のコマンドを実行すればOKです。

```sh
npm install --save webpack-google-cloud-storage-plugin
```

## webpack.mix.jsを修正する

`webpack-google-cloud-storage-plugin`に関する処理を追加します。`mix.inProduction(）`の判定がありますが、これは

```sh
npm run prod
```
の時だけ、このプラグインを使いたいための判定です。(ローカルでのテストの時にはもちろん実行したくないですからね。)

```js
const 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.
 |
 */

 //ここを追加！
const WebpackGoogleCloudStoragePlugin = require("webpack-google-cloud-storage-plugin");

const plugins = [];
if (mix.inProduction()) {
    mix.version();
    plugins.push(
        new WebpackGoogleCloudStoragePlugin({
            directory: "public",
            include: ["app.js","app.css"],
            exclude: ["images"],
            storageOptions: {
                keyFilename: process.env.MIX_CLOUD_STORAGE_CREDENTIAL_FILE
            },
            uploadOptions: {
                bucketName: process.env.MIX_PUBLIC_BUCKET_NAME,
                destinationNameFn: file => path.join("", file.path),
                gzip: true,
                makePublic: true,
                resumable: true,
                concurrency: 5
            }
        })
    );
}
//追加ここまで

mix.ts("resources/assets/ts/app.tsx", "public/js")
    .sass("resources/assets/sass/app.scss", "public/css")
    .options({
        processCssUrls: false
    })
    .webpackConfig({
        module: {
            rules: [
                {
                    test: /\.tsx?$/,
                    loader: "ts-loader",
                    exclude: /node_modules/
                },
                {
                    test: /\.js?$/,
                    exclude: /node_modules/,
                    use: {
                        loader: "babel-loader",
                        options: {
                            presets: ["@babel/preset-env", "@babel/react"]
                        }
                    }
                },
                {
                    test: /\.s[ac]ss$/i,
                    use: [
                        {
                            loader: "resolve-url-loader",
                            options: {}
                        },
                        {
                            loader: "sass-loader",
                            options: {
                                sourceMap: true
                            }
                        }
                    ]
                }
            ]
        },
        resolve: {
            extensions: ["*", ".js", ".jsx", ".ts", ".tsx"],
            alias: {
                react: path.resolve("./node_modules/react")
            }
        },
        plugins: plugins//ここも追加しています。
    });
```

## Cloud Storageにバケットを作る
もちろん、これがないと始まりませんね。GCPでバケットを作ったら、その名前を控えておきましょう。
また、サービスアカウントキーも必要となりますので、[こちらのページ](https://cloud.google.com/iam/docs/creating-managing-service-account-keys?hl=ja#creating_service_account_keys)を参考にJSONファイルをダウンロードして`app`直下に保存してください。

## アップするための情報を`.env`に追加する

`.env`ファイルに下記の記述を追記します。

```
# Bundleされたファイルの配置バケット
MIX_PUBLIC_BUCKET_NAME="joolen-cloud-run-example"
# デプロイする時に利用するCloud StorageのCredentialファイル名
MIX_CLOUD_STORAGE_CREDENTIAL_FILE="your-service-account.json"
```

## ローカルでnpm run prod!

ここまで対応したら、

```sh
npm run prod
```

を実行しましょう。エラーなく処理が完了したら`Cloud Storage`には下記のようなディレクトリやファイルができるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/a889b509-68ae-4ef1-0ba4-70f9ee84de9a.png)

[公式ページ](https://cloud.google.com/storage/docs/access-control/making-data-public?hl=ja)を参考にして`app.js`と`app.css`を公開しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/25a8fbf6-0039-092a-48fa-8a2240b326a3.png)

これで、`laravel-mix`で作成した`js`や`css`の準備もできるようになりました:tada:
ちなみに、[公式ページ](https://cloud.google.com/storage/docs/static-website?hl=ja#tip-dynamic)に記載がありますが、
> 一般公開で閲覧可能なオブジェクトはデフォルトで Cloud Storage ネットワークにキャッシュされるので、特に設定しなくても、Cloud Storage は基本的にコンテンツ配信ネットワーク（CDN）のように機能します。

とのことです。これはありがたいですね。

## Cloud RunにデプロイするためのDockerfileを修正する。

ここまで準備が整ったら、`Dokcerfile`の修正は簡単です。下記の１行を追記しましょう。

```docker
RUN npm install && npm run prod
```

最終的なDockerfileは下記の通りになります。

```dockerfile
FROM php:7.4-apache
WORKDIR /var/www/html

# 必要最低限のPHP拡張とNode.jsのインストール
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
RUN apt-get update \
  && apt-get install -y libzip-dev libpq-dev mariadb-client unzip git libfreetype6-dev libjpeg62-turbo-dev libpng-dev \
  && docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ \
  && docker-php-ext-install zip pdo_mysql mysqli gd exif opcache \
  && a2enmod rewrite \
  && apt-get -y install vim \
  && apt-get install -y nodejs \
  && git clone https://github.com/phpredis/phpredis.git /usr/src/php/ext/redis \
  && docker-php-ext-install redis 

EXPOSE 80

ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /composer
ENV PATH $PATH:/composer/vendor/bin
ENV APACHE_LOG_DIR /var/log/apache2

# composerをインストール
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY ./apache/*.conf /etc/apache2/sites-enabled/
COPY ./php/php.ini /usr/local/etc/php/php.ini
COPY ./app /var/www/html

# composer install を高速化するためのライブラリをグローバルインストール
RUN composer global require hirak/prestissimo
RUN composer install --optimize-autoloader --no-dev
RUN php artisan key:generate  \
  && php artisan config:cache \
  && php artisan view:cache

# ReactのモジュールをコンパイルしてCloud Storageにアップロード
RUN npm install && npm run prod

RUN chmod 777 -R /var/www/html/storage/framework/
```

あとは前回と同様に

```sh
gcloud builds submit --config cloudbuild.json --timeout="30m" --ignore-file ".gcloudignore"
```

を実行してコンテナイメージを作成してコンテナレジストリに`push`できます。

## Cloud Runの環境変数を修正して`js`や`css`はCloud Storageを参照するように変更する

### Laravelのコンフィグファイルを修正する。
`app/config/app.php`に下記の１行を追加します。(場所はどこでも大丈夫です)
これにより、mix関数が生成するベースURLを変更することができます。

```php
'mix_url' => env('MIX_ASSET_URL', null),
```

### .envの環境変数にAsset_URLを追記する

下記のように`MIX_ASSET_URL`追加します。URLには、バケットの公開URLの`public`までです。(スラッシュを付けない)

```
# CDNに配置されたBundleファイルのURL
MIX_ASSET_URL="https://storage.googleapis.com/{your_bucket_name}/public"
```

`view:cache`をしないのであれば、下記のように`Cloud Run`の環境変数に設定をすることもできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651593/cdd18b16-a624-8ffe-453f-676c2e1a1dd4.png)

また、ローカルでReactをビルドしたときの名残でpublic配下に`mix-manifest.json`があると思います。これも消してしましましょう！これがあると、参照すべきファイルを間違えてしまう可能性があります。

これで、`app.js`や`app.css`はCloud Storageにあるファイルを参照するようになります。
様々なライブラリを使うようになったりして、バンドルしたファイルが大きくなればなるほど、非常に効果のある対応です。
こちらも、弊社プロジェクトでは数百ms単位での効果があり、体感できるほどのパフォーマンス向上につながりました:tada:

今回も長文にお付き合い頂きまして、ありがとうございました！
サーバレスなLaravel & Reactで快適開発体験を体験してみてくださいね。:+1:

[次回](https://qiita.com/joolen_official/items/f24847a5f9c9203e5cc8)はこれらの仕組みの実現に必要な設定やコストについて触れていきたいと思います。