17
17

More than 3 years have passed since last update.

DockerでMySQL・Node.jsコンテナを用いたToDoリストアプリを作成する

Posted at

はじめに

Dcokerの使い方を学んだので、Docker上でNode.jsアプリケーションを作ってみました。
非常に多くの記事やサイトを参考にして作ったので、自分用のまとめ的な側面もあります。
作成したのは簡単なToDoリストアプリです。

初心者なので色々おかしな点があると思いますが、気づいたらコメントで教えていただけるとありがたいです。

完成イメージ

CRUD機能を搭載した簡単なToDoリストを作ります。
完成イメージは以下の通りです。

ToDoリストアプリの完成形.png

一番上のテキストボックスにタスクを入力し、右にある「add!」ボタンを押すことでタスクを追加できます。(Create)
その下には、既にデータベースに保存されているタスクが表示されます。(Read)
これらのタスクは、チェックボックスやテキストボックスの値を書き換えることで、自動的に更新されます。(Update)
また、タスク右の「delete」ボタンを押すことで、タスクを削除できます。(Delete)

※テキストボックスの値は、Enterキーを押すかフォーカスが外れたときに入力が確定します。

主な参考サイト

基本的には以下のサイトを参考にしていますが、一部自分なりにアレンジしています。

使用したソフトウェア等

DockerホストのPCはWindows10を使用しました。WSL2でUbuntuを実行し、Ubuntu上で作業を行いました。

  • Windows10 バージョン 2004
  • Ubuntu 20.04.2 LTS (Focal Fossa)
  • Docker 20.10.2
  • Node.js 14.16.0
  • express 4.16.1
  • sequelize 6.5.0
  • sequelize-cli 6.2.0
  • VueCLI 4.5.0
  • Vue.js 3.0.0
  • MySQL 8.0.23

システムの全体構成

システムは、3つのコンテナで構成されています。

  • dbコンテナ(MySQLイメージを使用)
    • ToDoリストアプリのデータを保管するデータベースサーバーです。
  • vueコンテナ(Node.jsイメージを使用)
    • ユーザーからのリクエストを受けるサーバーです。今回はフロントエンドにVue.jsを使うため、Vue CLIをインストールします。
  • apiコンテナ(Node.jsイメージを使用)
    • MySQLサーバーを操作するAPIを提供するサーバーです。vueコンテナは本サーバーが提供するAPIを利用してデータベースを操作します。Sequelizeをインストールします。

Docker①.jpg

Docker②.jpg

プロジェクトのディレクトリを作成

ディレクトリの構成は以下のようになります。

  • todo_app
    • docker-compose.yml
    • .env
    • api
      • (expressの雛型ファイル群が入る)
    • vue
      • (Vueの雛型ファイル群が入る)
    • db
      • logs
        • mysql-error.log
        • mysql-query.log
        • mysql-slow.log
      • conf
        • my.cnf

以下のコマンドを適当なディレクトリで実行してください。

mkdir -p todo_app/api
mkdir -p todo_app/vue
mkdir -p todo_app/db/logs
mkdir -p todo_app/db/conf
touch todo_app/docker-compose.yml
touch todo_app/.env
touch todo_app/db/logs/mysql-error.log
touch todo_app/db/logs/mysql-query.log
touch todo_app/db/logs/mysql-slow.log
touch todo_app/db/conf/my.cnf

docker-compose.ymlファイルを編集

todo_app/docker-compose.ymlファイルを以下のように編集します。

todo_app/docker-compose.yml
version: '3'
services:
  db:
    # 起動するイメージを指定
    image: mysql:8.0.23

    # 環境変数を設定
    environment:
      - MYSQL_ROOT_HOST=${DB_ROOT_HOST}
      - MYSQL_DATABASE=${DB_NAME}
      - MYSQL_USER=${DB_USER}
      - MYSQL_PASSWORD=${DB_PASS}
      - MYSQL_ROOT_PASSWORD=${DB_PASS}
      - TZ=${TZ}

    # ホスト側のポート:コンテナのポート
    ports:
      - '3306:3306'

    # ボリュームバインド
    volumes:
      - ./db/conf:/etc/mysql/conf.d/:ro
      - mysqldata:/var/lib/mysql
      - ./db/logs:/var/log/mysql

    #使用するネットワーク
    networks:
      - backend

  api:
    image: node:14.16.0-buster
    environment:
      - MYSQL_SERVER=db
      - MYSQL_USER=${DB_USER}
      - MYSQL_PASSWORD=${DB_PASS}
      - MYSQL_DATABASE=${DB_NAME}
      - TZ=${TZ}
      - CHOKIDAR_USEPOLLING=true

    #コンテナを起動させ続けるよう設定
    tty: true

    ports:
      - '3000:3000'

    # ソースコードを格納するフォルダをマウント
    #(ホスト側の./apiをコンテナの/appにマウント)
    volumes:
      - ./api:/app

    # 起動時のカレントフォルダを指定
    working_dir: /app

    # 起動後に実行するコマンドを指定
    command: npm run dev

    networks:
      - backend

    #依存関係(apiコンテナより先にdbコンテナが起動するように設定)
    depends_on:
      - db

  vue:
    image: node:14.16.0-buster
    environment:
      - CHOKIDAR_USEPOLLING=true
    tty: true
    ports:
      - '8080:8080'
    volumes:
      - ./vue:/app
    working_dir: /app
    command: npm run serve
    networks:
      - backend
    depends_on:
      - api

networks:
  backend:

volumes:
  mysqldata:

dbapivueの3つのサービスを定義しています。

イメージimage:

イメージはMySQLとNode.jsの公式イメージをそのまま使用しています。(今回はDockerfileを使いません。)Node.jsイメージはnode:14.16.0-busterを使用しています。なお、busterというのはDebianのVer.10のことです。ちなみにDebianのバージョン名は映画「トイ・ストーリー」の登場キャラクターが元になっているそうです。バージョン14.16.0は記事執筆時点での最新のLTS版を使用しています。

環境変数environment:

dbコンテナとapiコンテナには、MySQLを使用(MySQLに接続)するための環境変数を設定していますが、後述する.envファイルで定義したものを取り込むようにしています。直接docker-compose.ymlファイルに記述しなかったのは、データベースの接続情報をソースに含めるのはよろしくないからです。Githubに公開するときは.gitignore.envファイルを追加し、ソース管理から除外しましょう。
apiコンテナとvueコンテナの環境変数には- CHOKIDAR_USEPOLLING=trueを設定しています。これを書かないと、apiコンテナに入れるソースコードの変更を検知して自動でアプリを再起動するライブラリ「nodemon」が正常に動作しません。一応vueコンテナにも書きました。
参考:Docker 環境で nodemon が watch してくれない問題と対処方法 - Qiita

ポートports:

ユーザーからリクエストを受けるvueコンテナの8080番ポートと、ホスト側の8080番ポートを対応させています。
残りの2つのコンテナはコンテナ間のみで通信できればいいので、Dockerホストにマッピングする必要はないですが、apiコンテナもホストから動作を確認したいので設定しています。apiコンテナの3000番ポートと、ホスト側の3000番ポートを対応させています。(なお、dbコンテナも対応付けていますが、これは意味ないです。)

ボリュームvolumes:

dbコンテナでは、ホスト側のtodo_app/db/confディレクトリをコンテナの/etc/mysql/conf.d/ディレクトリにバインドマウントして、MySQLのデフォルト設定を記述したmy.cnfファイル(後述)を上書きしています。
また、MySQLのデータはコンテナが削除されてもデータが消えないように、mysqldataというボリュームを作成し、コンテナの/var/lib/mysqlディレクトリにボリュームマウントしています。(Windows環境ではバインドマウントだと上手くいかないそうです。)
さらに、MySQLのログデータを格納しているディレクトリもバインドマウントすることで、ホストからログを確認できるようにしています。
apiコンテナでは、ホスト側のtodo_app/apiをapiコンテナの/appにバインドマウントすることで、アプリケーションのファイル群をホスト側から編集可能にしています。
vueコンテナでも同様です。(ホスト側のtodo_app/vueをvueコンテナの/appにバインドマウント)

コマンドcommand:

apiコンテナおよびvueコンテナでは、起動時に実行するコマンドを設定しています。しかし、現時点ではこれらのコマンドを動作させるために必要なライブラリがインストールされていないため、コンテナを起動してもすぐに終了してしまいます。

ネットワークnetworks:

コンテナ名でコンテナ間の通信を行うために、backendという名前のDockerネットワークを作り、dbコンテナ、apiコンテナ、vueコンテナで設定しています。

依存関係depends_on:

依存関係を設定しました。dbapivueコンテナの順で起動します。停止するときは逆順になります。vueコンテナ(Todoリストアプリ)はapiコンテナの提供するAPIを使用しないとデータのCRUD(作成・取得・更新・削除)ができません。つまり、vueコンテナはapiコンテナに依存していると言えます。そのapiコンテナも、データをdbコンテナに取りにいかなければなりません。apiコンテナはdbコンテナに依存していると言えます。

my.cnfの編集

todo_app/db/conf/my.cnfを以下のように編集します。

todo_app/db/conf/my.cnf
# MySQLサーバーへの設定
[mysqld]
# 文字コード/照合順序の設定
character-set-server = utf8mb4
collation-server = utf8mb4_bin

# タイムゾーンの設定
default-time-zone = SYSTEM
log_timestamps = SYSTEM

# デフォルト認証プラグインの設定
default-authentication-plugin = mysql_native_password

# エラーログの設定
log-error = /var/log/mysql/mysql-error.log

# スロークエリログの設定
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 5.0
log_queries_not_using_indexes = 0

# 実行ログの設定
general_log = 1
general_log_file = /var/log/mysql/mysql-query.log

# mysqlオプションの設定
[mysql]
# 文字コードの設定
default-character-set = utf8mb4

# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default-character-set = utf8mb4

ここでは、文字コードの設定や、ログの出力先設定などを行っています。
また、デフォルトの認証プラグインを変更しています。

なお、DockerホストがWindowsの場合、todo_app/db/conf/my.cnfは読み取り専用にしておきます。
参考:[Docker+Windows]mysqlのdockerイメージがmy.cnfのマウントのエラーで起動しない時の対処法 - Qiita

my.cnfを読み取り専用にする①.png

my.cnfを読み取り専用にする②.png

.envファイルの編集

todo.app/.envファイルを以下のように編集します。
docker-compose.ymlでMySQLの環境変数を設定しますが、このファイルから具体的な値を取得しています。
参考:docker-composeのenv_fileと.envファイルの違い - Qiita

todo_app/.env
DB_ROOT_HOST=%
DB_NAME=todo
DB_USER=username
DB_PASS=mypassword
TZ=Asia/Tokyo

apiコンテナを起動してコンテナ内でExpress.jsアプリケーションを作成する

apiコンテナを起動して、express.jsアプリケーションの雛型を作成していきます。
docker-compose.ymlが置いてあるディレクトリ(todo_app/)で次のコマンドを実行します。

#docker-compose.ymlが置いてあるディレクトリに移動
cd todo_app

#apiコンテナを一時的に起動
$ docker-compose run --rm --no-deps api /bin/bash

このコマンドは、apiコンテナを実行(run)し、シェルを起動する(/bin/bash)という意味になります。
--rmは、実行後に自動でコンテナを削除するオプションです。
--no-depsは、リンクしたサービスを起動しないようにするオプションです。docker-compose.ymlでコンテナの依存関係を定義したため、本来ならばapiコンテナが起動する前にdbコンテナが立ち上がるはずですが、このオプションを設定することでapiコンテナのみ起動するようにしています。

lsコマンドで/appディレクトリ内に何もないことを確認しておきましょう。

==== ここからapiコンテナ内 ====
# lsコマンドで、/appディレクトリ(=ホスト側のtodo_app/api)内にまだ何もないことを確認
ls -al
total 4
drwxrwxrwx 1 node node 4096 Feb 15 11:48 .
drwxr-xr-x 1 root root 4096 Feb 15 11:46 ..

私の環境ではnpmのバージョンを上げておかないとライブラリのインストールが失敗してしまうことがあったので、npmをアップデートしておきます。(今後も何度かこの操作を行うのでDockerfileに書いておくべきだったかもしれません…。)

==== apiコンテナ内 ====
# npmのバージョンを確認
npm -v
6.14.11

# npmのアップデート
npm install -g npm

# npmがアップデートされたことを確認
npm -v
7.6.1

npm installでexpress-generatorをインストールして実行します。
(なぜかnpx express-generatorでは上手くいきませんでした。npmのバージョンを上げなければうまくいくのですが…)

==== apiコンテナ内 ====
# express-generatorをインストールして実行
npm install -g express-generator
express .

nodemonをインストール。ソースコードを変更したときに、自動でサーバーを再起動してくれる便利なツールです。開発環境でのみ使用するので-D--save-dev)オプションを付けておきます。

==== apiコンテナ内 ====
# nodemonをインストール
npm install -D nodemon

exitでコンテナを抜けます。

==== apiコンテナ内 ====
# exitでコンテナを抜ける
exit
==== ここまでapiコンテナ内 ====

todo_app/api/package.jsonを編集して、"scripts""dev"を追加します。

todo_app/api/package.json
{
    "name": "app",
    "version": "0.0.0",
    "private": true,
    "scripts": {
        "dev": "nodemon ./bin/www",
        "start": "node ./bin/www"
    },
    "dependencies": {
        "cookie-parser": "~1.4.4",
        "debug": "~2.6.9",
        "express": "~4.16.1",
        "http-errors": "~1.6.3",
        "jade": "~1.11.0",
        "morgan": "~1.9.1",
        "mysql2": "^2.2.5",
        "sequelize": "^6.5.0",
        "sequelize-cli": "^6.2.0"
    },
    "devDependencies": {
        "nodemon": "^2.0.7"
    }
}

以下のコマンドで3つのコンテナをまとめて起動します。

$ docker-compose up -d

起動後、apiコンテナはnpm run devが自動的に実行されます。(docker-compose.ymlで定義しました。)
http://localhost:3000/に接続して、Expressの初期画面が表示されることを確認します。

Expressの初期画面.png

ここで、docker-compose psコマンドでコンテナの起動状況を確認しておきます。

$ docker-compose ps

vueコンテナだけ状態(State)がExitになっています。(停止状態になっています。)

vueコンテナだけExitになっている.png

これは、vueコンテナを起動時に実行されるコマンド(npm run serve)の実行に失敗したためです。後ほどvueコンテナに入りVue CLIをインストールすることで、このコマンドを実行できるようになります。

dbコンテナを起動して正常に設定されていることを確認

起動済みのdbコンテナに入り、MySQLにログインする。

以下のコマンドを実行し、dbコンテナのシェルを起動してMySQLにログインします。

$ docker-compose exec db /bin/bash

==== ここからdbコンテナ内 ====
mysql -u root -p

MySQLログイン時のパスワードは、.envファイルで指定したものを入力してください。(今回はmypassword

MySQLの文字コードを確認

MySQLにログイン出来たら、念のため文字コードの設定を確認します。

==== dbコンテナ内 ====
mysql> show variables like 'char%';

MySQL.png

todo_app/db/conf/my.cnfで指定した通りの設定になっていればOKです。

データベースを確認

データベースの状態を確認します。

==== dbコンテナ内 ====
mysql> show databases;

データベースの確認.png

環境変数で指定したtodoというデータベースが存在しているのがわかります。
todoデータベースの中身はどうなっているでしょうか?

==== dbコンテナ内 ====
mysql> use todo;
mysql> show tables;

データベース切り替え&テーブル確認.png

todoデータベースの中身は空です。(テーブルは1つも存在しません。)

テーブルの作成は手作業でもできますが、今回はapiコンテナからSequelizeというライブラリのDBマイグレーション機能を使用して行います。
quitでMySQLを抜け、exitでdbコンテナも抜けてください。

==== dbコンテナ内 ====
mysql> quit
exit
==== ここまでdbコンテナ内 ====

apiコンテナ内で、sequelizeとその依存パッケージをインストール

今度はapiサーバーを設定していきます。
apiコンテナのシェルを立ち上げ、npmのバージョンをアップデートしておきます。

$ docker-compose exec api /bin/bash
==== apiコンテナ内 ====
# npmのバージョンを確認
npm -v
6.14.11

# npmのアップデート
npm install -g npm

# npmがアップデートされたことを確認
npm -v
7.6.1

まず、APIコンテナ内でSequelizeとその依存パッケージをインストールします。

==== apiコンテナ内 ====
npm install mysql2 sequelize sequelize-cli

次に、sequelize-cliを使用してSequelizeの初期化を行います。

==== apiコンテナ内 ====
npx sequelize-cli init

これによって、apiフォルダ内にconfigmigrationsmodelsseedersの4つのディレクトリが作成されます。

次に、以下のコマンドでモデルクラスを生成します。

==== apiコンテナ内 ====
npx sequelize-cli model:generate --name Task --attributes name:string,done:boolean

上記の例では、string型のnameと、boolean型のdoneという2つのプロパティを持つ、Taskというモデルクラスを作成しています。

カラム名 説明
name string タスク名
done boolean タスクが終了したか否か

実行すると、todo_app/api/modelsディレクトリに、task.jsファイルが出来ているのがわかります。

todo_app/api/models/task.js
'use strict';
const {
    Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
    class Task extends Model {
        /**
         * Helper method for defining associations.
         * This method is not a part of Sequelize lifecycle.
         * The `models/index` file will call this method automatically.
         */
        static associate(models) {
            // define association here
        }
    };
    Task.init({
        name: DataTypes.STRING,
        done: DataTypes.BOOLEAN
    }, {
        sequelize,
        modelName: 'Task',
    });
    return Task;
};

また、同時にtodo_app/api/migrationsディレクトリに、{日時}-create-task.jsというファイルも出来ています。

todo_app/api/migrations/[yyyyMMddHHmmss]-create-task.js
'use strict';
module.exports = {
    up: async (queryInterface, Sequelize) => {
        await queryInterface.createTable('Tasks', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
            },
            name: {
                type: Sequelize.STRING
            },
            done: {
                type: Sequelize.BOOLEAN
            },
            createdAt: {
                allowNull: false,
                type: Sequelize.DATE
            },
            updatedAt: {
                allowNull: false,
                type: Sequelize.DATE
            }
        });
    },
    down: async (queryInterface, Sequelize) => {
        await queryInterface.dropTable('Tasks');
    }
};

この時点のフォルダ構成は次の通りです。

フォルダ構成.png

sequelize-cliでDBマイグレーションを実行

次は、DBマイグレーションを実行してtodoデータベースにテーブルを作成します。
しかし、その前にDB接続情報を正しく設定する必要があります。

先程npx sequelize-cli initでSequelizeの初期化を行った時に、todo_app/api/configディレクトリにconfig.jsonというJSONファイルが出来ているはずですが、それをconfig.jsにしてJavascriptファイルにします。
そして内容を次のように書き換えます。
これは、docker-compose.ymlで設定した環境変数の値をそのままDB接続情報として使うためです。

todo_app/api/config/config.js
module.exports = {
    development: {
        username: process.env.MYSQL_USER,
        password: process.env.MYSQL_PASSWORD,
        database: process.env.MYSQL_DATABASE,
        host: process.env.MYSQL_SERVER,
        dialect: 'mysql',
    },
    test: {
        username: process.env.MYSQL_USER,
        password: process.env.MYSQL_PASSWORD,
        database: process.env.MYSQL_DATABASE,
        host: process.env.MYSQL_SERVER,
        dialect: 'mysql',
    },
    production: {
        username: process.env.MYSQL_USER,
        password: process.env.MYSQL_PASSWORD,
        database: process.env.MYSQL_DATABASE,
        host: process.env.MYSQL_SERVER,
        dialect: 'mysql',
    },
};

また、todo_app/api/models/index.jsファイルを開き、config.jsonとなっている部分をconfig.jsに変更します。

index.jsの修正.png

この状態で、以下のコマンドを実行するとSequelizeによるDBマイグレーションが実行されます。
つまり、何もなかったtodoデータベースにTasksテーブルが作成されます。

==== apiコンテナ内 ====
npx sequelize-cli db:migrate

最後に、exitでapiコンテナを抜けましょう。

==== apiコンテナ内 ====
exit
==== ここまでapiコンテナ内 ====

Tasksテーブルが作成されたことを確認

todoデータベースにTasksテーブルが作成されたことを確認しておきます。
以下のコマンドでdbコンテナのシェルを実行してください。

docker-compose exec db /bin/bash

先程と同様に、MySQLにログイン後、todoデータベースに切り替え、テーブルを見てみます。

==== ここからdbコンテナ内 ====
mysql -u root -p
mysql> use todo
mysql> show tables;

Tasksテーブルの確認.png

結果、Tasksテーブルが出来ていることが確認できました。なお、初回はマイグレーション管理用にSequelizeMetaテーブルも作られます。

ではMySQLとdbコンテナを抜け、ホストに戻ります。

==== dbコンテナ内 ====
mysql> quit
exit
==== ここまでdbコンテナ内 ====

vueコンテナの準備

ここで、ユーザーからのリクエストを受け取るvueコンテナの準備をしていきます。以前も触れましたが、vueコンテナは起動時に実行されるはずのnpm run serveコマンドが実行できなかったため、停止しています。(docker-compose psでもう一度確認してみましょう。)
ここでは、Vue CLIをインストールし、npm run serveコマンドを使えるようにしていきます。
vueコンテナは停止しているため、docker-compose execは使えません。ここでは、docker-compose runを使ってvueコンテナを一時的に起動します。

docker-compose run --rm --no-deps vue /bin/bash

例によってnpmのバージョンをアップデートしておきます。

==== vueコンテナ内 ====
# npmのバージョンを確認
npm -v
6.14.11

# npmのアップデート
npm install -g npm

# npmがアップデートされたことを確認
npm -v
7.6.1

npmのアップデート後、Vue CLIをインストールします。

==== vueコンテナ内 ====
npm install -g @vue/cli

インストール完了後、現在のディレクトリ.を対象にvue createを行います。

==== vueコンテナ内 ====
vue create .

以下の画像を参考に進めていきます。

vue create①.png

↑デフォルトで参照しているレジストリでは接続が遅く、「こっち(https://registry.npm.taobao.org)の方が早いからこっちを使おうよ!」と聞いているようですが、よくわからないのでNo(n)を選択しました。ちなみにtaobaoというのは中国のサーバーらしいです。

vue create②.png

↑カレントディレクトリに作っていいのか聞いています。Yes(Y)で大丈夫です。

vue create③.png

↑プリセットを選択します。Default (Vue 3 Preview) ([Vue 3] babel, eslint)を選びました。

vue create④.png

↑パッケージマネージャーを選択します。NPMを選びました。
その後、待っていればvue createが終わります。(私の環境だと結構時間がかかりました…)

次に、apiコンテナ(APIサーバー)からデータの取得や更新を行うために、axiosをインストールします。

==== vueコンテナ内 ====
npm install axios

インストールが完了したら、exitでvueコンテナを抜けます。

==== vueコンテナ内 ====
exit
==== ここまでvueコンテナ内 ====

そして、docker-compose downで全てのコンテナを停止&削除した後、docker-compose up -dで再び起動し直します。

docker-compose down
docker-compose up -d

ここで、docker-compose psで各コンテナのステータスを確認してみると、今度はvueコンテナも稼働状態になっていることがわかります。Vue CLiをインストールしたことで、npm run serveコマンドが実行できるようになりました。

docker-compose ps

vueコンテナもUpになっている.png

http://localhost:8080/に接続し、Vue.jsの初期画面が表示されることを確認します。

Vue.jsの初期画面.png

APIの実装

ここからは、apiコンテナ内のプログラムを編集し、ToDoリストアプリのCRUDのAPIを実装していきます。
まず、todo_app/api/routes/index.jsに2行追加します。
/taskルートの処理をtaskRoutes.jsに投げるようにします。

todo_app/api/routes/index.js
var express = require('express');
var router = express.Router();

//この2行を追加する
const taskRoutes = require("./taskRoutes");
router.use("/task", taskRoutes);

/* GET home page. */
router.get('/', function (req, res, next) {
    res.render('index', { title: 'Express' });
});

module.exports = router;

次に、todo_app/api/routes/に、ファイルtaskRoutes.jsを作成して、以下のように編集します。

todo_app/api/routes/taskRoutes.js
"use strict";

const router = require("express").Router(),
    taskController = require("../controllers/taskController");

router.get("/", taskController.read);
router.post("/", taskController.create);
router.put("/:id", taskController.update);
router.delete("/:id", taskController.delete);

module.exports = router;

ここでは、4つのhttpメソッド(get、post、put、delete)毎に違う処理を割り当てています。
処理の内容は、todo_app/api/controllers/taskController.js内に記述していきます。

todo_app/api/に、controllersという名前のディレクトリを作成し、その中にtaskController.jsを作成します。
todo_app/api/controllers/taskController.jsは、以下のように編集します。

todo_app/api/controllers/taskController.js
"use strict";

const db = require("../models/index");

module.exports = {
    read: async(req, res, next) => {
        try{
            const result = await db.Task.findAll();
            res.send(result);
        }catch(err){
            res.status(500).send(err);
        }
    },
    create: async(req, res, next) => {
        try{
            const result = await db.Task.create({
                name: req.body.name,
                done: false
            });
            res.send(result);
        }catch(err){
            res.status(500).send(err);
        }
    },
    update: async(req, res, next) => {
        try{
            const result = await db.Task.update(
                {
                    name: req.body.name,
                    done: req.body.done
                },
                {
                    where: {
                        id: req.params.id
                    }
                }
            );
            res.send(result);
        }catch(err){
            res.status(500).send(err);
        }
    },
    delete: async(req, res, next) => {
        try{
            const result = await db.Task.destroy({
                where: {
                    id: req.params.id
                }
            });
            res.send({
                result: result
            });
        }catch(err){
            res.status(500).send(err);
        }
    }
}

これで、APIコンテナの実装は完了です。

Vue.jsの実装

最後に、フロントエンドの実装を行います。
デフォルトで存在するtodo_app/vue/src/components/HelloWorld.vueというコンポーネントを以下のように書き換えます。

todo_app/vue/src/components/HelloWorld.vue
<template>
  <div class="hello">
    <form>
      <input type="text" style="display: none" />
      <input v-model="currentTask" type="text" />
      <input type="button" value="add!" @click="taskCreate" />
    </form>
    <table align="center" border="0">
      <tr>
        <th>done</th>
        <th>task</th>
        <th>delete</th>
      </tr>
      <tr v-for="(task, index) in tasks" :key="task.id">
        <td>
          <input
            type="checkbox"
            v-model="task.done"
            @change="taskUpdate(task.id, task.name, task.done)"
          />
        </td>
        <td>
          <input
            type="text"
            v-model="task.name"
            @change="taskUpdate(task.id, task.name, task.done)"
          />
        </td>
        <td>
          <input type="button" value="delete" @click="taskDelete(task.id, index)" />
        </td>
      </tr>
    </table>
  </div>
</template>
<script>
import axios from "axios";
export default {
  name: "HelloWorld",
  data: () => ({
    tasks: [],
    currentTask: "",
  }),
  created: async function () {
    try {
      const result = await axios.get("/task/");
      this.tasks = result.data;
    } catch (err) {
      alert(JSON.stringify(err));
    }
  },
  methods: {
    taskCreate: async function () {
      try {
        const result = await axios.post("/task/", {
          name: this.currentTask,
        });
        this.tasks.push(result.data);
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
    taskDelete: async function (id, index) {
      try {
        await axios.delete("/task/" + id);
        this.currentTask = "";
        this.tasks.splice(index, 1);
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
    taskUpdate: async function (id, val, done) {
      try {
        await axios.put("/task/" + id, {
          name: val,
          done: done,
        });
        this.currentTask = "";
      } catch (err) {
        alert(JSON.stringify(err));
      }
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
.table {
  height: 100%;
  text-align: center;
}
</style>

次に、todo_app/vue/にファイルvue.config.jsを作成し、以下のように編集します。
参考:Vue.jsとAPIサーバとのaxiosでCORSに引っかかった時のProxyを使った回避方法 - Qiita

todo_app/vue/vue.config.js
module.exports = {
    devServer: {
        proxy: 'http://api:3000'
    }
};

編集し終わったら、ここで各コンテナを再起動しましょう。

docker-compose down
docker-compose up -d

これで、ToDoリストアプリが完成しているはずです。http://localhost:8080/にアクセスして操作してみましょう。

ToDoリストアプリの完成形.png

出来ました!

参考書籍

主な参考サイト

17
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
17