Casual Developers Note

エンジニアやデザイナー向けの技術情報・英語学習情報・海外留学情報・海外旅行情報を提供中。世界を旅して人生を楽しもう。

  • ホーム
  • 技術 Tips & Tutorials
  • 技術塾
  • ライフハック
  • 海外留学
  • 英語学習
  • コラム
  • お問い合わせ
現在の場所:ホーム / アーカイブexpress

2018年4月27日 By KD コメントを書く

初級JavascriptフルスタックエンジニアのためのReactとExpressの同時開発チュートリアル(Dockerコンテナ化してHerokuにデプロイ編)

初級JavascriptフルスタックエンジニアのためのReactとExpressの同時開発チュートリアル(Dockerコンテナ化してHerokuにデプロイ編)

最近のアプリケーションはDocker上で稼働させるのが当たり前になりつつあります。そして、HerokuのようなPaaS環境にアプリケーションをリリースすることも小規模サービスでは一般的になっています。今回は、人気のHeroku上に人気のDockerコンテナをデプロイする方法をご紹介します。

Herokuとは?

Herokuとは、AmazonのAWSやMicrosoftのAzureと言ったIaaS上にアプリケーションを簡単にデプロイすることができるPaaSです。一回デプロイしてみると、そのデプロイの簡単さに驚くばかりです。

今回は、前回の「JavascriptフルスタックエンジニアのためのReactとExpressの同時開発チュートリアル(基本的なアプリ作成と同時開発環境構築編)」に引き続き、サンプルアプリケーションをDocker化し、Herokuにデプロイしていきましょう。

環境準備

Heroku CLIのインストール

$ brew install heroku
$ heroku --version
heroku-cli/6.15.30-e7b41cd (darwin-x64) node-v9.7.1

デプロイするアプリケーション

今回は前回作成した簡単なアプリケーションを使います。なので、サンプルアプリケーションで試したい人は、前回の記事からプロジェクトを持ってきて下さい。

初級JavascriptフルスタックエンジニアのためのReactとExpressの同時開発チュートリアル(基本的なアプリ作成と同時開発環境構築編)

Herokuにデプロイする時はGitを使うので、前回のプロジェクトをひとまずコミットしましょう。

$ cd full-stack-app/
$ git init
$ git add .
$ git commit -m 'initial commit'

Herokuの登録

まずはHerokuアカウントを作ってログインしましょう。

Herokuアカウントの作成

Herokuのサイトに言ってアカウントを作成して下さい。この時にクレジットカード情報の登録を行って下さい。なぜその必要があるかと言うと、後で使うHerokuのアドオンの使用条件になっているからです。クレジットカードの登録をしただけで課金されることは無いので安心して下さい。今回は無料枠だけでやります。

ログインとキー登録

環境準備でインストールしたHeroku CLIを使ってHerokuにコマンドラインからログインします。その後、キーを生成して登録します。

$ heroku login
Enter your Heroku credentials:
Email: your@email.com
Password: ***************
Logged in as your@email.com
$ heroku keys:add
Could not find an existing SSH key at ~/.ssh/id_rsa.pub
? Would you like to generate a new one? Yes
Generating public/private rsa key pair.
...
Uploading /Users/you/.ssh/id_rsa.pub SSH key... done

Herokuにデプロイする(通常のアプリケーション編)

それではDockerで、と言いたいところですが、まずは通常のHeroku上にデプロイする方法をやってみましょう。

package.jsonにスクリプトを追加する(NPM編)

まずはpackage.jsonにHerokuにビルドしてもらうためのスクリプトを追加します。このスクリプトにより、HerokuにPushするだけでReactがビルドされます。

  "scripts": {
    ...
    "heroku-postbuild":
      "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
  }

package.jsonにスクリプトを追加する(Yarn編)

Yarnの場合のスクリプトはこうなります。

  "scripts": {
    ...
    "heroku-postbuild":
      "NPM_CONFIG_PRODUCTION=false yarn --cwd client install && yarn --cwd client build"
  }

Mongooseの設定を変更する

「server.js」のMongooseの設定部分を以下のように書き換えます。

mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27018/sampledb');

すぐに分かりますが「MONGODB_URI」はHerokuが自動設定してくれるMongoDBの接続先の環境変数です。

Herokuにデプロイする

後は非常に簡単です。以下のようにコミットして、Herokuコマンドを実行するだけです。Herokuのアドオンを指定している「heroku addons:create mongolab:sandbox」では、mLabのSandbox(無料枠)にデータベースを作って自動で連携します。

$ cd full-stack-app/
$ git add .
$ git commit -m 'change to deploy to Heroku'
$ git push origin master #GitHubなどを使っている場合
$ heroku create
$ heroku addons:create mongolab:sandbox
Creating mongolab:sandbox on ⬢ xxxx-xxxx... free
Welcome to mLab.  Your new subscription is being created and will be available shortly.  Please consult the mLab Add-on Admin UI to check on its progress.
Created mongolab-xxxx-xxxx as MONGODB_URI
Use heroku addons:docs mongolab to view documentation
$ heroku config
MONGODB_URI: mongodb://heroku_xxxx:xxxx@xxxx.mlab.com:xxxx/heroku_xxxx
$ git push heroku master
$ heroku open

「heroku open」を実行してブラウザでうまく表示されない場合はログを確認しましょう。

$ heroku logs

よくあるのは環境変数の設定漏れや誤り、それに伴うデータベースとの接続失敗なので、確認してみて下さい。

アプリケーションをDockerコンテナ化する

ここからが本番です。HerokuにDockerコンテナをデプロイするためにはまずサンプルアプリケーションをDockerコンテナ上で起動するようにする必要があります。それではやっていきましょう。

必要なファイルの作成

まずはプロジェクト直下に「Dockerfile」と「docker-compose.yml」を作成します。

$ cd full-stack-app/
$ touch Dockerfile
$ touch docker-compose.yml
$ touch .dockerignore
$ touch .env

Dockerfileの作成(NPM編)

FROM node:9.7.1-alpine

RUN mkdir /app
WORKDIR /app

RUN npm install -g nodemon

COPY package.json package.json
COPY package-lock.json package-lock.json
RUN npm install && mv node_modules /node_modules

COPY . .

LABEL maintainer="You <your@email.com>"

CMD node server/server.js

Dockerfileの作成(Yarn編)

FROM node:9.7.1-alpine

RUN mkdir /app
WORKDIR /app

RUN npm install -g yarn nodemon

COPY package.json package.json
COPY yarn.lock yarn.lock
RUN yarn install && mv node_modules /node_modules

COPY . .

LABEL maintainer="You "

CMD node server/server.js

docker-compose.ymlの作成

「docker-compose.yml」は以下のようになります。

version: '3'

services:
  web:
    build: .
    ports:
      - '3001:3001'
    command: node server/server.js
    env_file:
      - .env
    depends_on:
      - 'db'
  db:
    image: mongo:latest
    ports:
      - '27018:27017'

今回はテスト用なので簡素にしてありますが、開発で使いたい場合はデータを共有したり保存したりするために「volumes」を設定すると良いでしょう。

.dockerignoreの作成

「.dockerignore」は以下のようになります。

.dockerignore
.git
node_modules
client/*
!client/build
db/

DockerコンテナにはReactをビルドしたファイルのみがあればよいのと、ローカルに保存してあるデータベースのデータも不要なので除外します。

.envの作成

ローカルでwebコンテナとdbコンテナを接続するために「MONGODB_URI」を設定します。

MONGODB_URI=mongodb://db:27017/sampledb

ローカルでビルドしてコンテナを起動する(NPM編)

$ npm run build --prefix client
$ docker-compose up -d

ローカルでビルドしてコンテナを起動する(Yarn編)

$ yarn --cwd client build
$ docker-compose up -d

以上で、サンプルアプリケーションのDocker化が完了しました。

Herokuにデプロイする(Dockerコンテナ編)

やっとここまできました。Docker化したサンプルアプリケーションをHerokuにデプロイしましょう。

環境変数の調整

「.env」ファイルの設定をコメントアウトします。「MONGODB_URI」の設定としてHerokuに設定されている環境変数を利用するためです。

# MONGODB_URI=mongodb://db:27017/sampledb

そして、Herokuの環境変数に「NODE_ENV」を設定します。

$ heroku config:set NODE_ENV=production
$ heroku config
=== xxxx-xxxx-xxxx Config Vars
MONGODB_URI: mongodb://heroku_xxxx:xxxx@xxxx.mlab.com:xxxx/heroku_xxxx
NODE_ENV:    production

これは、HerokuのNodeJS環境ではデフォルトで「NODE_ENV=production」と設定されていますが、Container Registryでは設定されていないためです。

Herokuにデプロイする(Herokuコマンド編)

それではHerokuにDockerコンテナをデプロイしましょう。「heroku container」コマンドで以下のようにするだけです。

$ cd full-stack-app/
$ npm run build --prefix client
$ heroku container:login
Login Succeeded
$ heroku container:push web
=== Building web (/Users/you/full-stack-app/Dockerfile)
Sending build context to Docker daemon   3.75MB
...
Successfully built 244a09e60313
Successfully tagged registry.heroku.com/xxxx-xxxx/web:latest
=== Pushing web (/Users/you/full-stack-app/Dockerfile)
The push refers to repository [registry.heroku.com/xxxx-xxxx-xxxx/web]
8ae4845af426: Pushed
...
$ heroku open

素晴らしい!簡単にできましたね。ローカルで自動的にDockerイメージが作成され、Herokuコンテナ環境にPushされているのが分かります。

Herokuにデプロイする(Dockerコマンド編)

Herokuコマンドを使ってやるのも良いのですが、Dockerイメージに好きな名前とバージョンを付けて自分でコントロールしたい場合は、dockerコマンドを使うと良いです。やってみましょう。

$ cd full-stack-app/
$ yarn --cwd client build
$ heroku container:login
$ docker-compose build --no-cache web
$ docker images
$ docker tag 0f4d3762ac35 registry.heroku.com/your-app-xxxxx/web:1.0.0
$ docker push registry.heroku.com/your-app-xxxxx/web:1.0.0
The push refers to repository [registry.heroku.com/your-app-xxxxx/web]
$ heroku open -a your-app-xxxxx

「your-app-xxxxx」には「heroku create」コマンド実行時にHerokuが自動で払い出したアプリ名を使用します。「heroku open」の時にブラウザに表示されるドメイン名ですね。

これで好きな名前やバージョンを付けたDockerイメージもHerokuにデプロイすることができました。

追記(container:release対応)

Container RegistryにDockerイメージをPushした後に、Releaseコマンドが必要になりました。詳しくは以下の記事を参照して下さい。

Heroku Container Registryのcontainer:push後のcontainer:releaseの対応方法

おまけ

dockerコマンドによるHerokuコンテナ環境へのログイン方法

Herokuコンテナ環境にログインするには「heroku container:login」コマンドを使ってましたが、dockerコマンドでもログインできます。

$ docker login --username=_ --password=$(heroku auth:token) registry.heroku.com
...
Login Succeeded

Herokuコンテナ環境でdocker-compose.ymlで必要なもの

今回作成した「docker-compose.yml」はローカル起動のためにPort設定やデータベース設定を記載しましたが、Herokuコンテナ環境ではPortは自動設定されますし、今回はデータベースはmLabを使用しているので設定は不要です。

そいういった贅肉を落とすと、結局以下の設定さえあればOKです。

version: '3'

services:
  web:
    build: .
    command: node server/server.js
    env_file:
      - .env

Dockerコマンドでイメージ名を後で変更しなくて良いように「image」を設定しても良いでしょう。

最後に

いかがでしたか?これでHerokuにDockerコンテナをデプロイする方法をマスターできたと思います。Herokuの独特の癖がありますが、それでもこのお手軽さは素晴らしいですね。それでは。

環境

  • PC : macOS High Sierra 10.13.3
  • heroku : heroku-cli/6.15.30-e7b41cd (darwin-x64) node-v9.7.1
  • docker : Docker version 17.12.0-ce, build c97c6d6

カテゴリ : 技術 Tips & Tutorials タグ : docker, express, heroku, react

2018年4月20日 By KD コメントを書く

初級JavascriptフルスタックエンジニアのためのReactとExpressの同時開発チュートリアル(基本的なアプリ作成と同時開発環境構築編)

初級JavascriptフルスタックエンジニアのためのReactとExpressの同時開発チュートリアル(基本的なアプリ作成と同時開発環境構築編)

フルスタックエンジニアの需要はスタートアップであればあるほど強いものです。そして、エンジニアにとってフロントエンドとバックエンドの両方を扱うことは楽しみでもあります。今回はよくあるReactとExpressを使ったアプリケーションを作成し、同時開発の方法を紹介します。

はじめに

スタートアップにおいて、フルスタックエンジニアの需要が高いのは理に適っています。なぜなら、フロントエンドエンジニアやバックエンドエンジニアという枠組みでエンジニアを採用してしまうと、単純に2倍のコストがかかるからです。そして、役割を分けるということはコミュニケーションコストの発生を防ぐことができません。仕事をしている人なら誰でも知っている話ですが、コミュニケーションコストが最も高く付きます。なぜ周りのマネージャーが仕事をしていないかのように見えるのか?なぜならコストの大半をコミュニケーションに費やしているからですね。私もマネジメントの仕事をしたことがありますが、正直、開発の仕事より何倍もストレスが多いです。

話が少し脱線してしまいましたが、今回は、JavascriptのフルスタックエンジニアとしてReactとExpressを同時に開発する事を想定して、基本的なサンプルアプリケーションを構築しながら、同時にフロントエンドとバックエンドを開発する環境構築方法を紹介していきます。IsomophicとかUniversalとかそういう話はしませんのあしからず。

MongoDBの準備

それでは、ReactとExpressの同時開発環境を作るために、まずは簡単ではありますが、フロントエンドをReact、バックエンドをExpressとMongoDBでサンプルアプリケーションを構築していきましょう。

MongoDBを起動する(Mac編)

今回は開発用のデータベースとしてMongoDBを使います。Macの場合は、Homebrewでインストールし、launchctlで常駐化させます。

$ brew install mongodb
$ ln -s /usr/local/opt/mongodb/homebrew.mxcl.mongodb.plist ~/Library/LaunchAgents
$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mongodb.plist
$ mongo --version
MongoDB shell version v3.6.3
...

これでPCを起動するとMongoDBが自動的に立ち上がります。デフォルトのポートは「27017」です。

MongoDBを起動する(Docker編)

最近はなんでもDockerにするのが主流ですし、MacでもWindowsでも起動できるので普通に楽ですね。

一応ですが、Dockerをインストールしていない場合は、HomebrewからCask経由で簡単にできます。

$ brew cask install docker
$ docker --version
Docker version 17.12.0-ce, build c97c6d6

続いて、以下のコマンドを実行すればOKです。

$ docker container run -d --rm --name mongo-docker -p 27018:27017 mongo:3.6.3
$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                      NAMES
75002cc641b5        mongo:3.6.3         "docker-entrypoint.s…"   12 seconds ago      Up 22 seconds       0.0.0.0:27018->27017/tcp   mongo-docker
$ docker exec -it mongo-docker mongo --version
MongoDB shell version v3.6.3
...

ポートは「27018」を指定しています。

Expressのサンプルアプリケーションの開発

フルスタックの経験が少ないエンジニアがSPAのアプリケーション開発しようとする場合、なぜかフロントエンドから作ろうとしがちですが、SPAのアプリケーションを作る場合はAPIから開発するのが基本です。(もちろん最低でもモックなどの見た目はあった方がよいですが)

Expressのプロジェクト作成

それでは、まずは必要なフォルダ作成やパッケージインストールからはじめましょう。

$ mkdir full-stack-app
$ cd full-stack-app/
$ brew install yarn
$ yarn global add nodemon
$ yarn init -y
$ yarn add express mongoose body-parser
$ mkdir server
$ touch server/server.js
$ mkdir server/models
$ touch server/models/User.js
$ tree server/
server/
├── models
│   └── User.js
└── server.js

MongooseでUserモデルを作る

MongoDBのスキーマを定義します。「User.js」は以下のようにします。

const mongoose = require('mongoose');
const { Schema } = mongoose;

const userSchema = new Schema({
  name: {
    type: String,
    required: true,
    trim: true,
    unique: 1
  },
  age: Number
});

const User = mongoose.model('users', userSchema);

module.exports = User;

ExpressでREST APIを作る

ここは、単純にUserオブジェクトをCRUDするREST APIを作りましょう。「server.js」は以下のようにしましょう。

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const PORT = process.env.PORT || 3001;
const User = require('./models/User');

mongoose.connect('mongodb://localhost:27018/sampledb');

const app = express();
app.use(bodyParser.json());

app.post('/users', (req, res) => {
  const user = new User(req.body);
  user.save((err, addedUser) => {
    if (err)
      return res.status(400).json({
        errorMessage: 'faild to add the user. User name must be unique.'
      });
    res.send(addedUser);
  });
});

app.get('/users', (req, res) => {
  User.find({}, (err, users) => {
    res.send(users);
  });
});

app.get('/users/:id', (req, res) => {
  const id = req.params.id;
  User.findById(id, (err, user) => {
    if (err)
      return res.status(400).json({
        errorMessage: 'the user not found.'
      });
    res.send(user);
  });
});

app.delete('/users/:id', (req, res) => {
  const id = req.params.id;
  User.findByIdAndRemove(id, (err, deletedUser) => {
    if (err)
      return res.status(400).json({
        errorMessage: 'faild to delete the user.'
      });
    res.send(deletedUser);
  });
});

app.delete('/users', (req, res) => {
  User.remove({}, err => {
    if (err)
      return res.status(400).json({
        errorMessage: 'faild to delete all users.'
      });
    res.send(true);
  });
});

app.listen(PORT, () => {
  console.log(`Server up on ${PORT}`);
});

簡単ではありますがバックエンド完成です。

Expressのサンプルアプリケーションの動作確認

出来上がったバックエンドの動作確認をしましょう。コンソールを2つ開いて下さい。

1つ目のコンソールでExpressのサンプルアプリケーションを起動します。

$ node server/server.js
Server up on 3001

2つ目のコンソールからcurlで確認していきます。

$ curl -H 'Content-Type:application/json' -d '{"name":"Tom","age":20}' http://localhost:3001/users
{"name":"Tom","age":20,"_id":"5aa1486997f7dd76ae3ddec1","__v":0}
$ curl -H 'Content-Type:application/json' -d '{"name":"John","age":30}' http://localhost:3001/users
{"name":"John","age":30,"_id":"5aa1487397f7dd76ae3ddec2","__v":0}
$ curl -H 'Content-Type:application/json' -d '{"name":"Mary","age":25}' http://localhost:3001/users
{"name":"Mary","age":25,"_id":"5aa1487b97f7dd76ae3ddec3","__v":0}
$ curl http://localhost:3001/users | jq
...
[
  {
    "_id": "5aa1486997f7dd76ae3ddec1",
    "name": "Tom",
    "age": 20,
    "__v": 0
  },
  {
    "_id": "5aa1487397f7dd76ae3ddec2",
    "name": "John",
    "age": 30,
    "__v": 0
  },
  {
    "_id": "5aa1487b97f7dd76ae3ddec3",
    "name": "Mary",
    "age": 25,
    "__v": 0
  }
]
$ curl http://localhost:3001/users/5aa1486997f7dd76ae3ddec1 | jq
...
{
  "_id": "5aa1486997f7dd76ae3ddec1",
  "name": "Tom",
  "age": 20,
  "__v": 0
}
$ curl http://localhost:3001/users/5aa1487397f7dd76ae3ddec2 | jq
...
{
  "_id": "5aa1487397f7dd76ae3ddec2",
  "name": "John",
  "age": 30,
  "__v": 0
}
$ curl http://localhost:3001/users/5aa1487b97f7dd76ae3ddec3 | jq
...
{
  "_id": "5aa1487b97f7dd76ae3ddec3",
  "name": "Mary",
  "age": 25,
  "__v": 0
}
$ curl -X DELETE http://localhost:3001/users/5aa1486997f7dd76ae3ddec1
{"_id":"5aa1486997f7dd76ae3ddec1","name":"Tom","age":20,"__v":0}
$ curl http://localhost:3001/users | jq
...
[
  {
    "_id": "5aa1487397f7dd76ae3ddec2",
    "name": "John",
    "age": 30,
    "__v": 0
  },
  {
    "_id": "5aa1487b97f7dd76ae3ddec3",
    "name": "Mary",
    "age": 25,
    "__v": 0
  }
]
$ curl -X DELETE http://localhost:3001/users
true
$ curl http://localhost:3001/users | jq
...
[]

ちゃんと動いていますね。

Reactの開発準備

フロントエンドはひとまず雛形だけ作ります。

プロジェクトの作成

流行りの「create-react-app」を使ってささっと作りましょう。

$ yarn global add create-react-app
$ create-react-app client
$ cd client
$ yarn add axios
$ rm src/*
$ touch src/index.js
$ touch src/App.js
$ tree src/
src/
├── App.js
└── index.js

index.jsの作成

ここは一般的な形にします。

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

App.jsの作成

ここも表示だけの最小限の形にします。

import React, { Component } from 'react';

class App extends Component {
  render() {
    return <div>Hello App!</div>;
  }
}

export default App;

余談ですが、ここでエディタのVisual Studio Codeを使っていると、Reactのスニペットで一瞬でここまでできあがります。使っていない方はVSCodeのプラグインを探してみて下さい。

開発のためにフロントエンドとバックエンドを同時に起動させる

ここでフロントエンドとバックエンドをつなげて開発するための設定を行います。そうしないとコンソールをフロントエンドとバックエンドで2つも開かなければなりません。めんどくさいです。なので、同時起動の方法を説明します。YarnとNPMの両方の設定を載せますが、このチュートリアルではYarnを使っているので、最後までやりたい方はYarnの設定だけして下さい。

プロキシ設定

フロントエンドをポート3000、バックエンドをポート3001で起動して接続させるために、フロントエンドの「package.json」に設定をする必要があります。

$ cd client

フロントエンドの「package.json」に以下のスクリプトを追加します。「package.json」がフロントエンドとバックエンドで2つあるので注意して下さい。

  "proxy": {
    "/users/*": {
      "target": "http://localhost:3001"
    }
  }

同時に起動させる方法(Yarn編)

プロジェクトのフォルダに移動します。

$ cd ..
$ pwd
/Users/you/full-stack-app

必要なパッケージをインストールします。

$ yarn add --dev npm-run-all react-scripts

バックエンドの「package.json」に以下のスクリプトを追加します。

  "scripts": {
    "start": "node server/server.js",
    "dev:server": "nodemon server/server.js",
    "dev:client": "yarn --cwd client start",
    "dev": "npm-run-all -p dev:*"
  },

あとは以下のコマンドで同時開発を開始できます。

$ yarn dev

同時に起動させる方法(NPM編)

ここまでYarnを使って開発していましたが、NPMでも似たような環境が作れます。

プロジェクトのフォルダに移動します。

$ cd ..
$ pwd
/Users/you/full-stack-app

必要なパッケージをインストールします。

$ npm install --save-dev concurrently

バックエンドの「package.json」に以下のスクリプトを追加します。

  "scripts": {
    "start": "node index.js",
    "server": "nodemon index.js",
    "client": "npm run start --prefix client",
    "dev": "concurrently \"npm run server\" \"npm run client\""
  },

同じく以下のコマンドで同時開発を開始できます。

$ npm run dev

起動すると以下の画面がブラウザで自動で立ち上がります。

スクリーンショット 2018 03 08 1 45 17

これで、フロントエンドとバックエンドを同時に開発する環境ができあがりました。

Reactのサンプルアプリケーションの開発

ここまできたら、Reactを仕上げてしまいましょう。

今回はユーザ情報を管理する簡単なアプリケーションとして作っていきましょう。

デザイン用のReactコンポーネントのインストール

Reactの良いところは、さまざまなフロントエンド用のデザインがReactコンポーネントとして提供されていることです。今回はReact Materializeを使うことにします。

$ cd client
$ yarn add react-materialize
$ cd ..
$ pwd
/Users/you/full-stack-app
$ tree client/public/
client/public/
├── favicon.ico
├── index.html
└── manifest.json
$ tree client/src/
client/src/
├── App.js
└── index.js
$ yarn dev

最後のコマンドでReactとExpressが両方起動して表示されました。

index.htmlの作成

「index.html」は以下のようにします。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="theme-color" content="#000000">
  <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
  <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">

  <!-- Import Google Icon Font -->
  <link href="http://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <!-- Import materialize.css -->
  <link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.0/css/materialize.min.css" rel="stylesheet">
  <title>User Management App</title>
</head>

<body>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <div id="root"></div>

  <!-- Import jQuery before materialize.js -->
  <script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.0/js/materialize.min.js"></script>
</body>

</html>

単にReact Materialize用の設定を追加しただけです。

App.jsの作成

これはReactのメインですね。以下のようにします。

import React, { Component } from 'react';
import axios from 'axios';
import { Button, Row, Input } from 'react-materialize';
class App extends Component {
state = {
name: '',
age: '',
users: [],
error: '',
message: ''
};
componentWillMount() {
axios.get(`/users`).then(response => {
this.setState({ users: response.data });
});
}
handleInputName = event => {
this.setState({ name: event.target.value });
};
handleInputAge = event => {
this.setState({ age: event.target.value });
};
submitForm = event => {
event.preventDefault();
axios
.post(`/users`, {
name: this.state.name,
age: this.state.age
})
.then(response => {
this.setState({
users: [...this.state.users, response.data],
error: '',
message: `Info: ${response.data.name} was added.`
});
})
.catch(error => {
this.setState({
error: `Error: ${error.response.data.errorMessage}`,
message: ''
});
});
};
deleteUser = id => {
axios
.delete(`/users/${id}`)
.then(response => {
this.setState({
users: this.state.users.filter(
user => user._id !== response.data._id
),
error: '',
message: `Info: ${response.data.name} was deleted.`
});
})
.catch(error => {
this.setState({ error: `Error: ${error.response.data.errorMessage}` });
});
};
deleteAllUsers = event => {
axios
.delete(`/users`)
.then(response => {
this.setState({
users: [],
error: '',
message: 'Info: all users were deleted.'
});
})
.catch(error => {
this.setState({
error: `Error: ${error.response.data.errorMessage}`,
message: ''
});
});
};
clearMessage = event => {
this.setState({ message: '' });
};
clearError = event => {
this.setState({ error: '' });
};
renderUsers() {
const users = this.state.users;
return users.map(user => (
<tr key={user._id}>
<td>{user.name}</td>
<td>{user.age}</td>
<td>
<Button onClick={user_id => this.deleteUser(user._id)}>Delete</Button>
</td>
</tr>
));
}
render() {
return (
<div>
<div
style={
this.state.error
? {
backgroundColor: '#f72e2e',
textAlign: 'center',
padding: '10px 0'
}
: null
}
onClick={this.clearError}
>
{this.state.error}
</div>
<div
style={
this.state.message
? {
backgroundColor: '#42a7f4',
textAlign: 'center',
padding: '10px 0'
}
: null
}
onClick={this.clearMessage}
>
{this.state.message}
</div>
<div style={{ width: '80%', margin: '0 auto', textAlign: 'center' }}>
<h2>User Management App</h2>
<h4>Add New User</h4>
<form onSubmit={this.submitForm}>
<Row>
<Input
type="text"
placeholder="Enter name"
s={5}
label="Name"
value={this.state.name}
onChange={this.handleInputName}
/>
<Input
type="number"
placeholder="Enter age"
s={5}
label="Age"
value={this.state.age}
onChange={this.handleInputAge}
/>
<Button type="submit" s={2}>
Add
</Button>
</Row>
</form>
<h4>Delete All Users</h4>
<div>
<Button onClick={this.deleteAllUsers}>Delete All</Button>
</div>
<div>
<h4>Users</h4>
<table>
<thead>
<tr>
<th style={{ width: '30%' }}>Name</th>
<th style={{ width: '30%' }}>Age</th>
<th style={{ width: '30%' }} />
</tr>
</thead>
<tbody>{this.renderUsers()}</tbody>
</table>
</div>
</div>
</div>
);
}
}
export default App;

これでフロントエンドの開発も完了です!

完成したサンプルアプリケーションで遊んでみる

それでは最後に、作ったサンプルアプリケーションで遊んでみましょう。

ユーザを追加してみる

「yarn dev」で表示して確認しましょう。

スクリーンショット 2018 03 08 5 35 18

名前と年齢を入力してADDボタンでユーザが追加されました。

スクリーンショット 2018 03 08 5 36 34

同じ名前でADDボタンを押すとエラーが表示されました。

スクリーンショット 2018 03 08 5 36 47

データベース上にもちゃんとデータが登録されています。

スクリーンショット 2018 03 08 5 37 02

ユーザを削除してみる

ユーザの横のDELETEボタンを押すと、ユーザが削除されました。

スクリーンショット 2018 03 08 5 37 31

データベース上からも削除させています。

スクリーンショット 2018 03 08 5 37 43

DELETE ALLボタンを押すとユーザ情報が全て削除されました。

スクリーンショット 2018 03 08 5 38 01

データベースも空です。

スクリーンショット 2018 03 08 5 38 05

少し動きで気になる点がありますが、サンプルアプリケーションはうまく動いているようです。せっかくなので、何か機能を追加してみて下さい。

最後に

いかがでしたか?これでReactとNodeJSでフロントエンドとバックエンドを同時に開発することができるようになったのではないでしょうか。それにしてもJavascriptでフロントエンドもバックエンドも開発できるのは本当に素晴らしいです。小さい規模の開発であればバックエンドでNodeJS以外を選択する理由はほとんどありませんし、フロントエンドはReactを選んでおけば今のところ間違いありません。それでは。

環境

  • PC : macOS High Sierra 10.13.3
  • docker : Docker version 17.12.0-ce, build c97c6d6
  • mongoイメージ : 3.6.3
  • NodeJS : v9.5.0
  • yarn : 1.3.2
  • create-react-app : 1.5.2
  • [server] express : 4.16.2
  • [server] mongoose : 5.0.7
  • [server] body-parser : 1.18.2
  • [server] npm-run-all : 4.1.2
  • [server] react-scripts : 1.1.1
  • [server] concurrently : 3.5.1
  • [client] axios : 0.18.0
  • [client] react : 16.2.0
  • [client] react-dom : 16.2.0
  • [client] react-scripts : 1.1.1
  • [client] react-materialize : 2.1.0

カテゴリ : 技術 Tips & Tutorials タグ : express, nodejs, react

2018年4月9日 By KD コメントを書く

Expressで「Cannot set headers after they are sent to the client」と怒られた時の対処法

Expressで「Cannot set headers after they are sent to the client」と怒られた時の対処法

Expressでコーディングしているとたまに「Cannot set headers after they are sent to the client」と怒られることがあります。原因はお決まりのアレなのですが、今回は簡単なTipsとして紹介します。

エラー事象

サーバーサイドに以下のエラーが出ています。

「(node:54370) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client」

内容としては「クライアント側へレスポンスを返却した後でなぜかヘッダーを設定するような処理をやろうとしているぞ」と怒られています。

問題のあるソースコード

例えば、ある1冊の本のデータを取得する場合に以下のようなソースコードを書いたとします。

  app.get('/api/books', auth, async (req, res) => {
const bookId = req.query.id;
try {
const book = await Book.findById(bookId);
if (!book) {
res.status(404).json({
error: {
message: 'Not the book found.'
}
});
}
res.send(book);
} catch (err) {
res.status(400).json({
error: {
message: err.message
}
});
}
});

この時に対象の本のデータが無かった場合、先程のエラー事象が再現します。何が原因なのでしょうか?

原因と対処法

先程のソースコードをよく見ると、本のデータが無かった場合に404でレスポンスを返していますが、実はその後に取得した本のデータ(空)をレスポンスとして返そうとしています。別の言い方をすると、クライアント側に404で返却した後に、空のレスポンスを返却しようとしています。気づきましたか?まさにエラーメッセージの通りで、if文の処理をした際に処理を止めていないことが原因です。

つまり、対象の本が無いことを判定しているif文がそこで処理が終わるようにすれば解決します。

ソースコードを以下のように修正すればエラーは解決できます。

  app.get('/api/books', auth, async (req, res) => {
const bookId = req.query.id;
try {
const book = await Book.findById(bookId);
if (!book) {
return res.status(404).json({
error: {
message: 'Not the book found.'
}
});
}
return res.send(book);
} catch (err) {
return res.status(400).json({
error: {
message: err.message
}
});
}
});

気づきにくいですが、分かってしまえば簡単な話ですね。これはExpressに限らず、NodeJSでコーディングしている場合、レスポンスを2回返そうとしていると類似のエラーメッセージが表示されますので、注意しましょう。eslintでは「consistent-return」というルールになっているので、eslintを使っていれば事前に注意してくれますよ。

最後に

いかがでしたか?今回は小さなTipsを紹介しました。Expressでコーディングするとたまに目にする事象なので覚えておくと対処が簡単になります。では。

カテゴリ : 技術 Tips & Tutorials タグ : express, nodejs

ブログ更新情報や海外の関連情報などを配信する無料メルマガ

Sponsored Links

About Author

KD

世界を旅し日本を愛するエンジニア。大学でコンピュータサイエンスの楽しさを学び、日本の大手IT企業で働く中で、新しい技術やスケールするビジネスが北米にある事に気づく。世界に挑戦するための最大の壁が英語であったため、フィリピン留学およびカナダ留学を経て英語を上達させた。現在は日本在住でエンジニアとして働きつつ、次の挑戦に備えて世界の動向を注視している。挑戦に終わりはない。このブログでは、エンジニアやデザイナー向けの技術情報から、海外に留学したい人向けの留学情報、海外に興味がある人向けの海外旅行情報など、有益な情報を提供しています。

https://casualdevelopers.com/

最近の投稿

  • 2020年JS周辺のバックエンド寄りの注目技術!ネクストNodeJSの「Deno」と分散型パッケージレジストリの「Entropic」の紹介

    2020年JS周辺のバックエンド寄りの注目技術!ネクストNodeJSの「Deno」と分散型パッケージレジストリの「Entropic」の紹介

    2020年1月13日
  • 今さら聞けないJavaによる関数型プログラミング入門 ~ラムダ式、ストリーム、関数型インターフェース~

    今さら聞けないJavaによる関数型プログラミング入門 ~ラムダ式、ストリーム、関数型インターフェース~

    2019年11月4日
  • ReactのためのEslintおよびPrettierの設定方法 ~Airbnb JavaScript Style Guideの適用~

    ReactのためのEslintおよびPrettierの設定方法 ~Airbnb JavaScript Style Guideの適用~

    2019年10月30日
  • BashからZshに移行する方法(Mac編)

    BashからZshに移行する方法(Mac編)

    2019年10月21日
  • Create React Appを使わないでゼロからReactの開発環境を構築する方法(Webpack/Docker編)

    Create React Appを使わないでゼロからReactの開発環境を構築する方法(Webpack/Docker編)

    2019年9月30日

カテゴリ

  • 技術 Tips & Tutorials (100)
  • 技術塾 (6)
  • ライフハック (26)
  • 海外留学 (12)
  • 英語学習 (3)
  • コラム (6)

アーカイブ

最高の学習のために

人気記事ランキング

  • MySQLで「ERROR 2003 (HY000): Can't connect to MySQL server」と怒られた時の対処法
    MySQLで「ERROR 2003 (HY000): Can't connect to MySQL server」と怒られた時の対処法
  • Jupyter Notebookで「The kernel appears to have died. It will restart automatically.」というエラーが出た場合の原因と対処法
    Jupyter Notebookで「The kernel appears to have died. It will restart automatically.」というエラーが出た場合の原因と対処法
  • Expressで「Cannot set headers after they are sent to the client」と怒られた時の対処法
    Expressで「Cannot set headers after they are sent to the client」と怒られた時の対処法
  • SAKURAのメールボックスで独自ドメインのメールを設定し、Gmail経由で送受信する方法
    SAKURAのメールボックスで独自ドメインのメールを設定し、Gmail経由で送受信する方法
  • SpringBootのProfile毎にプロパティを使い分ける3つの方法
    SpringBootのProfile毎にプロパティを使い分ける3つの方法
  • Amazon EC2インスタンスにSSHできなくなった時の対処法
    Amazon EC2インスタンスにSSHできなくなった時の対処法
  • SLF4JとLogbackによるJavaのロギング入門(SLF4J + Logback + Lombok)
    SLF4JとLogbackによるJavaのロギング入門(SLF4J + Logback + Lombok)
  • PythonでWebスクレイピング入門(Scrapy+Selenium編)
    PythonでWebスクレイピング入門(Scrapy+Selenium編)
  • 爆速でJenkinsをマスターしよう(GitHubアカウント統合編) ~ JenkinsのGitHub Organizationの設定方法 ~
    爆速でJenkinsをマスターしよう(GitHubアカウント統合編) ~ JenkinsのGitHub Organizationの設定方法 ~
  • バンクーバー留学豆知識:バンクーバーのATMで日本の銀行のキャッシュカードを使ってお得にお金を引き出す方法
    バンクーバー留学豆知識:バンクーバーのATMで日本の銀行のキャッシュカードを使ってお得にお金を引き出す方法

Bitcoin寄付 / BTC Donation

Bitcoinを寄付しよう

BTC
Select Payment Method
Personal Info

Donation Total: BTC 0.0010

このブログの運営のためにBitcoinでの寄付を募集しています。お気持ち程度の寄付を頂けると管理者の励みになります。

Bitcoin寄付について知りたい方はこちらの記事へ

ビットコイン取引ならここ

  • ホーム
  • 技術 Tips & Tutorials
  • 技術塾
  • ライフハック
  • 海外留学
  • 英語学習
  • コラム
  • サイトマップ
  • タグ一覧
  • プライバシーポリシー
  • お問い合わせ

Copyright © 2023 KD - Casual Developers Notes