Dockerで簡易版Aidemyをつくってみよう (その2) ~React.jsによるフロントエンド~

# 概要
<a href="https://qiita.com/advent-calendar/2018/aidemy">Aidemy アドベントカレンダー</a>12日目担当の @km42428 です。
今回は、Dockerで簡易版Aidemyをつくってみようシリーズのその2です。
<a href="https://qiita.com/km42428/items/4e4653aa0e813282986b" target="_blank">前回の記事</a>では全体の構成をお伝えしましたが、今回からは実際に開発したいと思います。

# 今回の目標
 今回は、React によるフロントエンドを作成し、`docker-compose`コマンドで一発で起動するのが目標です。
フロントエンドはユーザと直接やりとりする部分で、ユーザの入力を受けたり、その情報をバックエンドへ送信、バックエンドから情報を取得して表示する役割があります。Reactについては<a href="https://reactjs.org/">公式サイト</a>をご覧ください。
 作成するのは、いわゆるWebページです。ソースは以下に公開しています。
https://github.com/km42428/aidemy-simple-pack/tree/frontend

今回の記事は `frontend` ブランチで動作しています。
`master`ブランチは適宜更新しているので、この記事と同じ動作をさせたい場合は、
以下のコマンドを入力してください。
※`自分の作業ブランチ`は適宜書き換えてください。

```shell
# クローン
$ git clone https://github.com/km42428/aidemy-simple-pack.git 

# リモートのfrontendブランチにチェックアウト
$ git fetch && git checkout origin/frontend

# ローカルの自分の作業ブランチにチェックアウト
$ git checkout -b {自分の作業ブランチ} 
```

フロントエンドの機能としては、エディタ部分にコードが入力できるようにし、
送信ボタンを押すと、イベントが発火して今回はalertが表示されるようにします。
ただし、cssについてはあまり手を加えないことにします。
理由は、~~僕がデザインセンスが皆無なことと、~~
今回の趣旨はReactでの簡単なデータの扱い方を見るのが目的だからです。

# ページ構成 (ルーティング)
今回は以下のようなページ構成を考えます。
プレビューが小さくて見えない場合は、画像をクリックすれば大きく見えます。

|ルート|ページ|プレビュー|
|:--|:--|:--------------------------------------------------:|
| / | 問題一覧 |<img width="300" alt="スクリーンショット 2018-11-18 21.46.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/291512/b86e46be-d7bf-00c2-2e0b-e89369c93671.png">|
| /exercises/:exerciseId | 演習画面 |<img width="300" alt="スクリーンショット 2018-11-18 21.47.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/291512/faac315b-a851-f39e-7ab6-db1aa8b44df2.png"> |

ちなみに、 `/exercises/:exerciseId` のRUN押下時は下のようになります。
<img width="1072" alt="スクリーンショット 2018-12-11 15.03.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/291512/d57c93b5-5a9c-886d-d7dd-514023a9ea9f.png">

# 事前準備
　フロントエンドの挙動をローカルで確認するには、
`node` と `npm` を事前にインストールする必要があります。
インストール手順については
以下の記事を参照してください。
※ 今回の最終目的は`Docker`を使って環境開発不要で起動することです。
なので、環境開発なんてしたくないやいっ！という方は、
ソースをcloneした上で、<a href="#dockerによる操作" target="_blank">Dockerによる操作</a>からお読みください。

## Ubuntuの場合
https://qiita.com/seibe/items/36cef7df85fe2cefa3ea

## Macの場合
https://qiita.com/ksyunnnn/items/a7f1ffdd545cefbbcbd3

## Windowsの場合
https://blog.clock-up.jp/entry/2018/02/10/nodejs-manager-on-windows

nodeの管理にはバージョン管理ツールが必要なのですが、
個人的には `n` というツールが使いやすいです。

インストール後バージョン確認をしましょう。
僕の場合は以下のようになりました。

```
$ node -v
v8.6.0

$ npm -v
5.3.0
```


# ローカルでの開発
## create-react-app のインストール

```shell
$ npm install -g create-react-app
```

## create-react-app によるプロジェクト作成

`aidemy-frontend` という名前で作成します

```shell
$ create-react-app aidemy-frontend
```

## localhost:3000 で動作確認
以下を実行すると、 http://localhost:3000 で動作確認できると思います。

```shell
$ cd aidemy-frontend
$ npm start
```

![スクリーンショット 2018-11-15 1.33.06.png](https://qiita-image-store.s3.amazonaws.com/0/291512/332d70fc-23f2-0059-303d-b4328edea749.png)

## react-router-dom
ルーティングは react-router-dom を用いてやっていきます。
react-route-dom は react-router の一機能ですが、今回は事足りるということでこれでいきます。

react-router-dom をインストールします。
僕の場合はバージョンは `4.3.1`でした

```
$ npm install react-router-dom
```

## react-ace
エディター部分には `React Ace` を使用します。
こちらは、React用のエディターで、よしなにカスタマイズすることができます。

react-ace をインストールします。
僕の場合はバージョンは `6.2.0`でした

```
$ npm install react-ace
```


## App.jsの編集
フロントエンドのメイン箇所となるsrc/App.jsを編集していきます。

```jsx

import React, { Component } from "react";
import { BrowserRouter, Route, Link } from "react-router-dom";
import AceEditor from "react-ace";
// react-aceのテーマ情報
import "brace/mode/python";
import "brace/snippets/python";
import "brace/theme/tomorrow";

// マスターデータ
const exercises = [
  {
    exerciseId: "exercise1",
    title: "1. Hello world",
    script: "# Hello worldを出力しましょう\n"
  },
  {
    exerciseId: "exercise2",
    title: "2. コメントの入力",
    script: "# 3 + 5 の結果を出力しましょう\n"
  }
];

// exerciseIdからexerciseを取得する関数
function getExercise(exerciseId) {
  return exercises.find(el => {
    return el.exerciseId === exerciseId;
  });
}

// 表示される土台の部分
// Link: クリック時にtoで指定されたページに遷移
// Route: 特定のurlの時に表示するコンポーネントを定義
const App = () => (
  <BrowserRouter>
    <div>
      <nav>
        <Link to="/">Home</Link>
      </nav>

      <hr />
      <Route exact path="/" render={props => <Exercises />} />
      <Route
        path="/exercises/:id"
        render={props => <Exercise match={props.match} />}
      />
    </div>
  </BrowserRouter>
);

// exerciseの一覧
class Exercises extends Component {
  constructor() {
    super();
    this.state = {
      exercises: []
    };
  }

  // コンポーネントのレンダリングが終了したら呼び出される
  componentDidMount() {
    this.setState({
      exercises
    });
  }

  // 実際の表示部分
  render() {
    return (
      <div>
        <h2>Exercises</h2>
        <p>エクササイズの一覧です</p>
        <ul>
          {(this.state.exercises || []).map(exercise => {
            return (
              <li key={exercise.exerciseId}>
                <Link
                  to={"/exercises/" + exercise.exerciseId}
                  key={exercise.exerciseId}
                >
                  {exercise.title}
                </Link>
              </li>
            );
          })}
        </ul>
      </div>
    );
  }
}

// 特定のexerciseの演習部分
class Exercise extends Component {
  constructor() {
    super();
    this.state = {
      exerciseId: "",
      title: "",
      script: ""
    };
    this.onChange = this.onChange.bind(this); // はじめにonChangeをthisにbindしておく
  }

  // コンポーネントのレンダリングが終了したら呼び出される
  componentDidMount() {
    const exercise = getExercise(this.props.match.params.id);
    if (exercise) {
      this.setState({
        exerciseId: exercise.exerciseId,
        title: exercise.title,
        script: exercise.script
      });
      return;
    }
    this.setState({
      exerciseId: "",
      title: "",
      script: ""
    });
  }

  // エディタに入力があるたびに呼び出され、stateを変更する
  onChange(newValue) {
    this.setState({
      script: newValue
    });
  }

  // RUN押下時に呼び出される関数。現状alertを表示するのみ。
  run() {
    alert(`以下のコードが実行されます\n\n${this.state.script}`);
  }

  // 実際の表示部分
  render() {
    return (
      <div>
        <h2>Exercise</h2>
        <p>{this.state.title}</p>
        <AceEditor
          mode="python"
          onChange={this.onChange}
          editorProps={{ $blockScrolling: true }}
          editorSetting="editorLightTheme"
          theme="tomorrow"
          value={this.state.script}
        />
        <input type="button" value="RUN" onClick={this.run.bind(this)} />
      </div>
    );
  }
}

export default App;

```

## ※静的ファイルをホスティングの準備(本番モード)
これは好みによるのでやらなくてもいいです。
Reactの起動方法には以下の二つがあります。

```markdown
- 開発モード
- 本番モード
```

`開発モード` は文字通り開発に適したモードで、
WarningやErrorがコンソール画面でみれたり、ソースを書き換えるたびに再度レンダリングされます。
ただし、ソースの書き換えを見張っていたり、ログを多めに出すので、リソースはかなり食います。

`本番モード` は開発モードとは異なり、
WarningやErrorはあまりコンソールにはでません。
また、ビルドされた(静的なHTML, CSS, Javascript などに出力された)ファイルをホスティングするので、
元のソースが書き換わっても再レンダリングは発生しません。
なのでリソースを小さく抑えることができますが、変更があった場合は再度ビルドしてホスティングする必要があります。

今回はDockerで起動するときは`本番モード`で起動することにします。

### serveのinstall
静的ファイルのホスティングのために serveをinstallします。
ちなみに、 `npm install` は　省略して `npm i` でもOKです。

```shell

$ npm i serve
```

### package.jsonに以下を追加

```diff


{
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
+   "serve": "serve -s build",
    "eject": "react-scripts eject"
  }
}
```

### 静的ファイルホスティング
はじめにビルドして静的ファイルを生成します。

```
$ npm run build
```

静的ファイルをホスティングします。
問題なければ http://localhost:5000 で立ち上がります。

```
$ npm run serve
```

# Dockerによる操作

## docker-compose で動作するために
### aidemy-frontendフォルダ内にDockerfileを配置

```
　└ aidemy-frontend/
　　　 　└ Dockerfile
```

Dockerfileを記述

`本番モード`で起動

```Dockerfile
FROM node:8
WORKDIR /usr/src/app
COPY . .
RUN npm install
RUN npm run build
CMD npm run serve
```

`開発モード`で起動

```Dockerfile
FROM node:8
WORKDIR /usr/src/app
COPY . .
RUN npm install
CMD npm run start
```

### aidemy-frontendフォルダと同じ階層にdocker-compose.yml 配置

```
　├ aidemy-frontend/
　│　└ Dockerfile
　└ docker-compose.yml
```


docker-compose.ymlを記述

```yaml

version: "3"
services:
  aidemy-frontend:
    build: ./aidemy-frontend
    image: aidemy-frontend
    environment:
      - ENV
    ports:
      - 5000:5000
```

※もし、開発モードで起動したい場合は、ポートを `5000:5000` -> `2000:2000` へ変更

## docker-composeによる起動

### docker imageの作成
以下のコマンドを実行すると、aidemy-frontendのimageが作成されます。

```
$ docker-compose build
```

### imageの確認
`Docker`のimage確認は以下のコマンドで行います。

```
$ docker images
```
以下のような出力が得られればOKです。

```
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
aidemy-frontend     latest              948452abd4d5        30 minutes ago      1.12GB
node                8                   3b7ecd51ffe5        7 days ago          889MB
```

`aidemy-frontend` は1.12GBもあるんですね。。
元となった`node:8` が889MBなので仕方ないですね。。
PCの容量にはお気をつけください！

### フォアグラウンドでの起動

```
$ docker-compose up
```

http://localhost:5000 で動作すればOK
もし、ローカルでも起動していた場合バッティングするので、ローカルで起動したものを止めましょう。

### ※バックグラウンドでの起動

```
$ docker-compose up -d
```

### ※バックグラウンドで起動したプロセスの終了

```
$ docker-compose down
```

今回は以上になります。
Dockerの使い方には慣れてきたでしょうか？
次回は12/17にバックエンドを開発していきます！