DockerでReact + Django + Postgresの連携・SPA構築チュートリアル
ReactとDjango REST framework、Postgresで簡単なSPAアプリを作成しました。環境の準備からブラウザに表示させるまでをまとめます。
環境の用意
次のようなコンテナ環境を用意します。また、DockerとDocker Composeのインストールが必要です。
- Front-end:Reactが乗っかったNode.jsのコンテナ。表側はこのコンテナで用意したSPAを表示させます。
- Back-end:Django REST frameworkがインストールされたPythonのコンテナ。テンプレート機能は使わず、JSON形式のデータを返すWebAPIとして使います。
- DB:PostgreSQLのコンテナ。
事前準備-ディレクトリ構成
下記のような構成でDockerfileとYMLファイル、ディレクトリを用意しました。
.
├── code/
│ ├── django_rest_api/
│ │ └── requirements.txt
│ └── django_web_front/
├── DockerfileNode
├── DockerfilePython
└── docker-compose.yml
docker-compose.yml
Postgres(db)、Python(rest_api)、Node(web_front)のコンテナをDocker Composeで一元管理するためのファイルです。
version: "3"
services:
db:
container_name: django_db
image: postgres:11.2
volumes:
- django_data_volume:/var/lib/postgresql/data
rest_api:
container_name: django_rest_api
build:
context: .
dockerfile: DockerfilePython
volumes:
- ./code/django_rest_api:/code
tty: true
ports:
- 8000:8000
depends_on:
- db
web_front:
container_name: django_web_front
build:
context: .
dockerfile: DockerfileNode
volumes:
- ./code/django_web_front:/code
tty: true
ports:
- 3000:3000
depends_on:
- rest_api
volumes:
django_data_volume:
PostgresはDockerイメージから直でコンテナを起動し、PythonとNodeはDockerfileでビルドしてからコンテナを起動します。
Pythonのコンテナは8000番を、Nodeは3000番をポートフォワードし、両方とも localhost
でブラウザからアクセスできるようにします。
depends_onで、db→rest_api→web_frontという順に起動するよう指定しています。
また、Pythonのコンテナは ./code/django_rest_api
ディレクトリを、Nodeは ./code/django_web_front
をコンテナ側へマウントし、ファイルを共有しています。Postgresはデータが揮発しないようにData volumeをマウントしています。
DockerfileNode
NodeのイメージをビルドするためのDockerfileですが、作業ディレクトリを指定する以外はほとんど何もしていません。
FROM node:8
RUN mkdir /code
WORKDIR /code
DockerfilePython
PythonのイメージをビルドするためのDockerfileです。こちらもほぼ何もしていませんが、最終行のpipコマンドで、必要なライブラリをインストールしています。
FROM python:3.7
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
ADD . /code/
RUN pip install -r requirements.txt
requirements.txt
Pythonのライブラリをインストールするためのリストです。
Django==2.1.7
psycopg2==2.7.6
psycopg2-binary==2.7.7
djangorestframework==3.9.2
django-cors-headers==2.5.2
psycopg2
とpsycopg2-binary
: Postgresと連携するためのライブラリdjangorestframework
:Django REST frameworkのライブラリdjango-cors-headers
: CORS(Cross-Origin Resource Sharing)に必要なサーバーヘッダーを処理するためのDjango用ライブラリ
django-cors-headers
はクロスドメイン(異なるドメイン間)でのRequestを許可し、同一ドメインでのRequestのように処理できるようになるライブラリです。React(localhost:3000)からDjango(localhost:8000)のAPIを叩く際に必要となります。
コンテナの起動
↓docker-compose.ymlの置かれているディレクトリで docker-compose up
します。
docker-compose up -d
web_front
と rest_api
、 db
の3つのコンテナが起動します。
DBのセットアップ
ここから、db→rest_api→web_frontという順番でセットアップしていきます。まずはdbから。
↓dbのコンテナに psql -U postgres
コマンドを実行させます。
docker container exec -it django_db psql -U postgres
↓下記コマンドを順に投入し、DBを作成します。
CREATE DATABASE test_app;
CREATE USER admin WITH PASSWORD 'password';
GRANT ALL PRIVILEGES ON DATABASE test_app TO admin;
終わったら、 ctrl + d
でコンテナから抜けます。
Djangoのセットアップ
次はDjangoのrest_apiの設定です。
プロジェクト・アプリケーションディレクトリの作成
↓まずDjangoのプロジェクトを作成します。 django_rest_api
コンテナに対し、 django-admin startproject test_app
コマンドを実行させています。
docker container exec -it django_rest_api django-admin startproject test_app
↓続いてDjangoのアプリケーションファイルを作成。
docker container exec -it django_rest_api mkdir test_app/webapi
docker container exec -it django_rest_api django-admin startapp webapi test_app/webapi
すると code/django_rest_api/
ディレクトリの中身は、次のような構成になってるかと思います。
code/django_rest_api/
├── requirements.txt
└── test_app
├── manage.py
├── test_app
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── webapi
├── __init__.py
├── admin.py
├── apps.py
├── migrations
├── models.py
├── tests.py
└── views.py
環境変数の設定
次は環境変数を設定します。 test_app/test_app/settings.py
でいくつかのINSTALLED_APPSとMIDDLEWAREを有効にし、CORS_ORIGIN_WHITELISTを末尾に追加します。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'webapi',#←追加
'rest_framework',#←追加
'corsheaders',#←追加
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware',#←追加
]
.....
# ↓settings.pyの末尾に追加
CORS_ORIGIN_WHITELIST = (
'localhost:3000/',
'localhost:3000',
)
また、Postgresと接続できるように、DATABASESを次のように書き換えます。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'test_app',
'USER': 'postgres',
'HOST': 'db',
'PORT': 5432,
}
}
Vagrantなど、環境によってはホストの制限を変更する必要があります。
ALLOWED_HOSTS = ['*']
# 全てのアクセスを許可する設定。本番環境では使用しないこと。
モデルの作成
今回は名前とアドレス、メッセージ、作成日を記録できるテーブルを作成します。
↓ webapi/models.py
で下記のように定義します。
from django.db import models
class Profile(models.Model):
name = models.CharField(max_length=64)
email = models.EmailField()
message = models.CharField(max_length=256)
created_at = models.DateTimeField(auto_now_add=True)
↓次はシリアライザーを用意します。 webapi/serializers.py
を作成し、下記のように定義します。これはDjango REST frameworkの機能で、モデルのデータをPythonのデータ型に変換して、JSONまたはXMLなどの形式に変換してくれます。
from rest_framework import serializers
from webapi.models import Profile
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ('id', 'name', 'email', 'message', 'created_at')
ここで一旦、DBをマイグレートしておきます。
docker container exec -it django_rest_api python test_app/manage.py makemigrations
docker container exec -it django_rest_api python test_app/manage.py migrate
docker container exec -it django_rest_api python test_app/manage.py createsuperuser
ルーティング
localhost:8000/api/profile/
へアクセスした時に、ページが表示されるようにします。
↓ test_app/urls.py
を次のようにします。
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('webapi.urls')),
]
path('', include('webapi.urls')),
この行で webapi/urls.py
を読み込んでいます。
↓次は読み込み元の webapi/urls.py
を設定します。
from django.urls import path
from . import views
urlpatterns = [
path('api/profile/', views.ProfileListCreate.as_view() ),
]
↓最後に webapi/views.py
を設定します。
from webapi.models import Profile
from webapi.serializers import ProfileSerializer
from rest_framework import generics
class ProfileListCreate(generics.ListCreateAPIView):
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
データの挿入
fixtures機能でDBにデータを流し込みます。
↓ webapi/fixtures/profiles.json
を作成して次のように設定。
[
{
"model": "webapi.profile",
"pk": 1,
"fields": {
"name": "A子",
"email": "a-ko@gmail.com",
"message": "A子でーす",
"created_at": "2019-01-01 00:00:00"
}
},
{
"model": "webapi.profile",
"pk": 2,
"fields": {
"name": "B子",
"email": "b-ko@gmail.com",
"message": "B子でーす",
"created_at": "2019-02-01 00:00:00"
}
},
{
"model": "webapi.profile",
"pk": 3,
"fields": {
"name": "C子",
"email": "c-ko@gmail.com",
"message": "C子でーす",
"created_at": "2019-03-01 00:00:00"
}
}
]
↓下記コマンドでデータを挿入
docker container exec -it django_rest_api python test_app/manage.py loaddata profiles
動作確認
サーバーを起動してブラウザでアクセスし、レスポンスが帰ってくるか確認します。
docker container exec -it django_rest_api python manage.py runserver 0:8000
localhost:8000/api/profile
へアクセスすると、↓こんなページが返ってきます。
Reactのセットアップ
いよいよReactを使ってフロントサイドを作り込んでいきます。
↓それでは早速、Node.jsのコンテナでcreate-react-appを投入します。
docker container exec -it django_web_front npx create-react-app .
投入後、下記のようなファイル群が作成されます。
code/django_web_front/
├── README.md
├── node_modules/
├── package-lock.json
├── package.json
├── public/
├── src/
└── yarn.lock
↓次は必要なパッケージをインストールします。今回はAxiosとBulmaを使ってみました。
docker container exec -it django_web_front npm install axios bulma
- Axios…ブラウザとnode.js で動作する Promise ベースのHTTPクライアント。簡単に扱えるのでAJAXライブラリとして人気。
- Bulma…軽量なCSSフレームワーク。
public/index.html
↓ public/index.html
ファイルを下記のようにしました。最終的に <div class="container" id="root"></div>
へReactで構築したDOMが出力される予定です。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%!P(MISSING)UBLIC_URL%!/(MISSING)favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<link rel="manifest" href="%!P(MISSING)UBLIC_URL%!/(MISSING)manifest.json" />
<title>React App</title>
</head>
<body>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item" href="/">
MY REACT-DJANGO TEST APP
</a>
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
</div>
</nav>
<div class="container" id="root"></div>
</body>
</html>
src/index.js
↓ src/index.js
でBulmaをインポートします。
import 'bulma/css/bulma.min.css';
src/App.js
↓ src/App.js
を次のようにしました。Reactの習熟度が低いので大変でしたが、だんだんわかってきました。
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
class Form extends React.Component {
render() {
return (
<form id="post-data" onSubmit={this.props.handleSubmit}>
<div className="field">
<div className="control">
<input
className="input"
type="text" name="name"
id="name"
placeholder="Name"
value={this.props.name}
onChange={this.props.handleChange}
/>
</div>
</div>
<div className="field">
<div className="control">
<input
className="input"
type="text"
name="email"
id="email"
placeholder="Email"
value={this.props.email}
onChange={this.props.handleChange}
/>
</div>
</div>
<div className="field">
<div className="control">
<textarea
className="textarea"
name="message"
id="body"
cols="10"
rows="3"
placeholder="Message"
value={this.props.message}
onChange={this.props.handleChange}>
</textarea>
</div>
</div>
<input
className="button is-fullwidth is-primary is-outlined"
type="submit"
value="SEND POST"
/>
</form>
);
}
}
class UserList extends React.Component {
renderRow() {
const listItems = this.props.users.map((u) =>
<tr key={u.id}>
<td>{u.id}</td>
<td>{u.name}</td>
<td>{u.email}</td>
<td>{u.message}</td>
<td>{u.created_at}</td>
</tr>
);
return(
listItems
);
}
render() {
return (
<table className="table is-fullwidth">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Email</th>
<th>Message</th>
<th>Created at</th>
</tr>
</thead>
<tbody>
{this.renderRow()}
</tbody>
</table>
);
}
}
class App extends Component {
constructor(props) {
super(props);
this.state = {
users: [],
usersLength: 0,
name: "",
email: "",
message: "",
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
if (event.target.name === 'name') {
this.setState({name: event.target.value});
}
else if (event.target.name === 'email') {
this.setState({email: event.target.value});
}
else if (event.target.name === 'message') {
this.setState({message: event.target.value});
}
}
handleSubmit(event) {
event.preventDefault();
const { name, email, message } = this.state;
const conf = {
'name': name,
'email': email,
'message': message
};
axios.post("http://localhost:8000/api/profile/?format=json", conf)
.then(response => {
this.state.users.unshift(response.data);
this.setState({
users: this.state.users,
usersLength: this.state.users.length,
});
})
.catch((error) => {
console.error(error);
});
}
componentDidMount() {
axios.get('http://localhost:8000/api/profile/?format=json')
.then(response => {
this.setState({
users: response.data.reverse(),
usersLength: response.data.length,
});
})
.catch((error) => {
console.log(error);
});
}
render() {
return (
<div className="columns is-multiline">
<div className="column is-6">
<div className="notification">
This is my react-django app.
</div>
</div>
<div className="column is-6">
<Form
name={this.state.name}
email={this.state.email}
message={this.state.message}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
/>
</div>
<div className="column is-12" id="user-table">
<p>There are {this.state.usersLength} users.</p>
<UserList
users={this.state.users}
/>
</div>
</div>
);
}
}
export default App;
src/App.css
src/App.css
を次のようにしました。と言ってもテーブルのスタイルを少しいじっただけですが。
#user-table {
overflow: scroll;
}
最終動作確認
djangoとnode.jsのサーバーを起動し、アクセスできるか確認します。
↓djangoのサーバーを起動
docker container exec -it django_rest_api python test_app/manage.py runserver 0:8000
↓別のタブでターミナルを開いて、node.jsのサーバーを起動
docker container exec -it django_web_front npm start
localhost:3000
へアクセスすると。。!
まとめ
いい感じになりました。やったね!
ソースコードは下記GitHubリポジトリをご覧ください。dockerのエントリーポイントなども追加しています。