Docker上でRails&Vue.jsでのSPAの開発環境構築

# 目的
6月あたりから仕事でRails&Vueの開発をしているんですが参加した時点ではコーディングできる状態になっていて
自分ではプロジェクト作成や開発環境構築などを全くしなかったのでその辺の理解を深めるために
年末年始を使って自力で構築してみました

# 最終的なゴール
- Docker上で、Rails&Vue(webpackで単一ファイルコンポーネント)のSPAの開発環境を構築 ←今回やる
- swaggerを使ってAPI定義してバックエンドとフロントエンドを円滑に開発
- リポジトリへpush時にCIで自動テストしてAWS(Elastic Beanstalk)へデプロイされるようにする

# 今回のゴール
- Docker上でバックエンド(Rails)とフロントエンド(Vue.js)の開発ができる状態にする
- ローカルでの開発だけでなくデプロイして公開できる状態にする
    - webpackでビルドしたものをRailsから配信できる状態

# 技術スタック
・Docker
・Rails
・Vue.js
・Webpack(Webpackerもいずれ試す)

# Railsプロジェクト作成
## 必要なディレクトリとファイル作成
ディレクトリを作ってその中に
- `Dockerfile`
- `docker-compose.yml`
- `Gemfile`
を作る

```
rails_vue_spa/
　├ Dockerfile
　├ docker-compose.yml
　└ Gemfile
```
<br />

```:rails_vue_spa/Dockerfile
FROM ruby:2.5.1

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
    apt-get update -qq &&\
    apt-get install -y nodejs
ENV TZ=Asia/Tokyo

COPY Gemfile .
RUN bundle install

WORKDIR /usr/src/app
```
Ruby環境のイメージをビルドするためのDockerfile
<br />

```yml:rails_vue_spa/docker-compose.yml
version: '3'
volumes:
  bundle:
services:
  app:
    build: .
    ports:
      - 3000:3000
    volumes:
      - bundle:/usr/local/bundle
      - .:/usr/src/app:delegated
    stdin_open: true
    tty: true
    command: /bin/sh
```
そのうちDBのコンテナと繋げる&操作性がdocker-composeの方が好きなのでdocker-compose.ymlを用意
<br />

```ruby:rails_vue_spa/Gemfile
source 'https://rubygems.org'

gem 'rails', '~> 5.2.2'
```
とりあえず`rails`だけ
<br />

## Docker立ち上げてRailsプロジェクト作る
```shell-session:Host側
$ docker-compose build
$ docker-compose up -d
$ docker-compose exec app sh
```

```shell-session:Docker側
# rails new .
       exist
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
    conflict  Gemfile
Overwrite /usr/src/app/Gemfile? (enter "h" for help) [Ynaqdhm] 
```
`y`でエンター
<br />

```shell-session:Docker側
# rails s -b 0.0.0.0 -p 3000
```
で http://localhost:3000 でrailsサンプルページが表示されるか確認
<br />

```shell-session:Docker側
# ^C
# exit
```
`Ctrl+C`でrailsサーバ止めてDocker上のコンソール終了

```shell-session:Host側
$ docker-compose down
```
一度Docker止める

## Dockerの設定修正
```:rails_vue_spa/Dockerfile
FROM ruby:2.5.1

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
    apt-get update -qq &&\
    apt-get install -y nodejs
ENV TZ=Asia/Tokyo

COPY Gemfile .
COPY Gemfile.lock .
RUN bundle install

WORKDIR /usr/src/app
```
イメージビルド時にlockファイルからgemがインストールされるように`Dockerfile`を修正
<br />

```yml:rails_vue_spa/docker-compose.yml
version: '3'
volumes:
  bundle:
services:
  app:
    build: .
    ports:
      - 3000:3000
    volumes:
      - bundle:/usr/local/bundle
      - .:/usr/src/app:delegated
    stdin_open: true
    tty: true
    command: /bin/sh -c "bundle install; rm /usr/src/app/tmp/pids/server.pid; rails s -b 0.0.0.0 -p 3000"
```
コンテナ起動時にもlockファイルからgemがインストールされ、
railsサーバが起動するように`docker-compose.yml`修正

`rm /usr/src/app/tmp/pids/server.pid;`
はコンテナ止める時にpidファイルが残って再度サーバ立ち上げようとした時にエラーになることがあるため
<br />


```shell-session:Host側
$ docker-compose build --no-cache
$ docker-compose up -d
```
で http://localhost:3000 にアクセスしてサンプルページが表示されればok

# Railsプロジェクトの中にVueの開発環境構築
## `frontend/`配下にVueプロジェクト作成
```shell-session:Host側
$ docker-compose exec app sh
```
Dockerの中に入って

```shell-session:Docker側
# pwd
/usr/src/app
# mkdir frontend
# cd frontend
# npm install -g vue-cli
# vue init webpack

? Generate project in current directory? (Y/n) y
? Project name (frontend)
? Project description A Vue.js project
? Author
❯ Runtime + Compiler: recommended for most users
  Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed in .vue files - render functions are required elsewhere
? Install vue-router? (Y/n) y
? Use ESLint to lint your code? (Y/n) y
? Pick an ESLint preset
❯ Standard (https://github.com/standard/standard)
  Airbnb (https://github.com/airbnb/javascript)
  none (configure it yourself)
? Set up unit tests (Y/n) y
? Pick a test runner (Use arrow keys)
❯ Jest
  Karma and Mocha
  none (configure it yourself)
? Setup e2e tests with Nightwatch? (Y/n) y
? Should we run `npm install` for you after the project has been created? (recommended) (Use arrow keys)
❯ Yes, use NPM
  Yes, use Yarn
  No, I will handle that myself
```
- Railsプロジェクト直下に`frontend`ディレクトリ作成
- vue-cliをインストール
- vueプロジェクト作成(色々選択肢出るけど基本ずっとそのままエンターでok)

## Dockerの設定を修正
```:Dockerfile
FROM ruby:2.5.1

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
    apt-get update -qq &&\
    apt-get install -y nodejs
ENV TZ=Asia/Tokyo

WORKDIR /usr/src/app
COPY Gemfile .
COPY Gemfile.lock .
RUN bundle install

RUN mkdir frontend
COPY frontend/package.json frontend/.
COPY frontend/package-lock.json frontend/.
RUN cd frontend && npm install
```
- イメージビルド時に`frontend`直下に`node_modules`をインストールするように修正
<br />
<br />

```yml:docker-compose.yml
version: '3'
volumes:
  bundle:
services:
  app:
    build: .
    ports:
      - 3000:3000
      - 8080:8080
    volumes:
      - bundle:/usr/local/bundle
      - .:/usr/src/app:delegated
      - /usr/src/app/frontend/node_modules
    stdin_open: true
    tty: true
    command: /bin/sh -c "bundle install; rm /usr/src/app/tmp/pids/server.pid; rails s -b 0.0.0.0 -p 3000"
```
- webpack-dev-server用に8080ポート開ける
- ボリュームマウント時に`node_modules`が消えないようにボリュームトリックを使う(※[参考](https://shotat.hateblo.jp/entry/2016/12/01/221631))
<br />
<br />

```shell-session:Host側
$ docker-compose down
$ docker-compose build --no-cache
$ docker-compose up -d
```
Dockerイメージビルドからし直して立ち上げる

## webpack-dev-serverの設定修正
```js:frontend/config/index.js
host: 'localhost', // can be overwritten by process.env.HOST
```
を

```js:frontend/config/index.js
host: '0.0.0.0', // can be overwritten by process.env.HOST
```
に修正

## webpack-dev-server起動して確認
```shell-session:Host側
$ docker-compose exec app sh
```
でDockerに入って

```shell-session:Docker側
# cd frontend
# npm run dev
```
webpack-dev-server起動して
http://localhost:8080/#/ にアクセスにサンプルページが表示されればok
`Ctrl+C`で終了

# Vueプロジェクトをビルドしてみる
```shell-session:Docker側
# pwd
/usr/src/app/frontend
# npm run build
```
でフロントエンドをビルドすると`frontend/dist`の直下に
ビルドしたcssやjsとそれらを読み込んでいるindex.htmlが作成される

```
rails_vue_spa/
            └ frontend/
                     └ dist/
                          ├ index.html
                          └ static/
                                 ├ css/
                                 │   ├ app.30790115300ab27614ce176899523b62.css
                                 │   └ app.30790115300ab27614ce176899523b62.css.map
                                 └ js/
                                    ├ app.f79767c5ed5e2af21cfb.js
                                    ├ app.f79767c5ed5e2af21cfb.js.map
                                    ├ manifest.2ae2e69a05c33dfc65f8.js
                                    ├ manifest.2ae2e69a05c33dfc65f8.js.map
                                    ├ vendor.42fc6c515ccdfe89bd76.js
                                    └ vendor.42fc6c515ccdfe89bd76.js.map
```
そのままだとビルドしてできたものにハッシュがついてて扱いづらいのでハッシュがつかないようにする

## Vueプロジェクトのビルドの設定変更
```js:frontend/build/webpack.prod.conf.js
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
.
.
.
filename: utils.assetsPath('css/[name].[contenthash].css'),
```
を

```js:frontend/build/webpack.prod.conf.js
filename: utils.assetsPath('js/[name].js'),
chunkFilename: utils.assetsPath('js/[id].js')
.
.
.
filename: utils.assetsPath('css/[name].css'),
```
へ修正して

```shell-session:Docker側
# npm run build
```
ビルドしなおすと

```
rails_vue_spa/
            └ frontend/
                     └ dist/
                          ├ index.html
                          └ static/
                                 ├ css/
                                 │   ├ app.css
                                 │   └ app.css.map
                                 └ js/
                                    ├ app.js
                                    ├ app.js.map
                                    ├ manifest.js
                                    ├ manifest.js.map
                                    ├ vendor.js
                                    └ vendor.js.map
```
となる

# ビルドしたフロントエンドのファイルをRailsサーバから配信されるようにする
## Railsのルーティングとコントローラ準備
```ruby:config/routes.rb
root to: 'index#index'
```

```ruby:app/controllers/index_controller.rb
class IndexController < ApplicationController
  def index; end
end
```

## Railsのviews作成
```html:frontend/dist/index.html(実際は改行ないけど見やすいように整形した)
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
        <meta name=viewport content="width=device-width,initial-scale=1">
        <title>frontend</title>
        <link href=/static/css/app.css rel=stylesheet>
    </head>
    <body>
        <div id=app></div>
        <script type=text/javascript src=/static/js/manifest.js></script>
        <script type=text/javascript src=/static/js/vendor.js></script>
        <script type=text/javascript src=/static/js/app.js></script>
    </body>
</html>
```
を参考にcssとjs読み込み部分をassets以下を読み込むように修正して以下のerbを作る

```erb:app/views/index/index.html.erb
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
        <meta name=viewport content="width=device-width,initial-scale=1">
        <title>frontend</title>
        <%= stylesheet_link_tag 'application', media: 'all' %>
    </head>
    <body>
        <div id=app></div>
        <%= javascript_include_tag 'application' %>
    </body>
</html>
```
このままだとまだ読み込まれないので
`frontend/dist/static/css/*`を`app/assets/stylesheets/frontend/`配下へ、
`frontend/dist/static/js/*`を`app/assets/javascripts/frontend/`配下へ移動する

```js:app/assets/javascripts/application.js
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require_tree .
```
を

```js:app/assets/javascripts/application.js
//= require frontend/manifest
//= require frontend/vendor
//= require_tree .
```
へ変更
`//= require_tree .`だけで置いてあるjs全部読み込むが
`manifest`と`vendor`を先に読み込まないとエラーになるので上記のようにする必要がある

これでvueでビルドしたものがrailsから配信されるようになるので
http://localhost:3000 にアクセスするとvueのサンプルページが表示されるようになる

## 画像について
ロゴのpngとかはbase64にエンコードされた状態になるためビルドした時に画像は出力されないが
ぐるぐるするアニメーションのgifはエンコードされず`dist/static/img/`配下に出力される(なんでだろう？）

試しに`frontend/src/asstes/loader.gif`を置いて

```vuejs:frontend/src/App.vue
<img src="./assets/logo.png">
```
を

```vuejs:frontend/src/App.vue
<img src="./assets/loader.gif">
```
にしてビルドすると`dist/static/img/loader.78f8cb8.gif`も生成されるようになる

が、
画像のパス`static/img/loader.78f8cb8.gif`が`app.js`の中に書かれてしまうため
`frontend/dist/static/img/`配下を`app/assets/images/`ではなく`public/static/img/`配下に置く

## フロントエンドのビルドが楽になるように
上記のcss,js,imgの移動を含めたビルドの流れをスクリプトにまとめる

```sh:scripts/build-frontend.sh
#!/bin/bash

cd /usr/src/app/frontend
npm install
npm run build

cp -a /usr/src/app/frontend/dist/static/js/. /usr/src/app/app/assets/javascripts/frontend
cp -a /usr/src/app/frontend/dist/static/css/. /usr/src/app/app/assets/stylesheets/frontend

if [ -d /usr/src/app/frontend/dist/static/img/ ] ;then
  # ディレクトリないとcpでエラーになるからなければ作る
  if [ ! -d /usr/src/app/public/static/img ] ;then
    mkdir -p /usr/src/app/public/static/img
  fi
  cp -a /usr/src/app/frontend/dist/static/img/. /usr/src/app/public/static/img/
fi
```
<br　/>
ここまで準備すると、

```shell-session:Host側
docker-compose exec app scripts/build-frontend.sh
```
とか
<br　/>

```shell-session:Host側
$ docker-compose exec app sh
```

```shell-session:Docker側
# scripts/build-frontend.sh
```

ってするだけでフロントエンドがビルドされて最新状態のものが
http://localhost:3000
で見れるようになる

このスクリプトは後々のCIでのビルド&デプロイでも活用する

# 〆
ってことで今回のゴール

- Docker上でバックエンド(Rails)とフロントエンド(Vue.js)の開発ができる状態にする
- ローカルでの開発だけでなくデプロイして公開できる状態にする
    - → webpackでビルドしたものをRailsから配信する

までたどり着きました

次回は
swaggerを使ってAPI定義してバックエンドとフロントエンドを円滑に開発

次次回
リポジトリへのpush時にCIで自動テストしてAWS(Elastic Beanstalk)へデプロイされるようにする

をやってみたいと思います