使用Docker构建Django+React的开发环境

目标

我希望未来能够在Docker环境中将Django和React协同工作,部署到Google Kubernetes Engine上并创建个人作品集。因为我完全是自学自进,所以若有任何错误,请不吝赐教。

环境

我正在使用Windows10 home上的Docker Toolbox。
我将使用DjangoRestFramework和React来创建一个超简单的Todo应用程序。
该应用程序的代码是参考了《Django for APIs》第三章。

首先在本地开始。

首先我们可以在本地创建一个 ToDo 应用程序,同时考虑 Django 和 React 的连接。

创建一个目录

Docker Toolbox使用Virtualbox创建Docker主机。
由于容器卷的挂载默认设置为C:Users/环境下,
所以建议在使用Docker Toolbox时使用User目录下的文件夹。

# プロジェクトフォルダの作成
mkdir gke-django
cd gke-djagno
# ディレクトリを作成する
mkdir backend
mkdir frontend

推进后端开发

首先,我们将使用Django-rest-framework来创建后端API。先从后端开始创建环境进行尝试。

配置文件.py

cd backend
# 仮想環境の作成
python -m venv venv
# 仮想環境の有効化
venv\Scripts\activate
# Pythonパッケージのインストール
python -m pip install --upgrade pip setuptools
python -m pip install django djangorestframework python-dotenv
# Djangoのプロジェクトを始める。
django-admin startproject config .

通过在backend目录下运行`django-admin startproject config .`命令,创建了一个名为config的项目文件夹在backend目录下。

我们将编辑config/settings.py。
首先只编辑基本事项。
因为SECRET_KEY将在.env中追加,所以请将其复制备份。

# config/settings.py

"""
Django settings for config project.

Generated by 'django-admin startproject' using Django 3.0.4.

For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""

import os
from dotenv import load_dotenv  # 追加

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.basename(BASE_DIR)

# .envの読み込み
load_dotenv(os.path.join(BASE_DIR, '.env'))  # 追加

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

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',
]

ROOT_URLCONF = 'config.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  # 変更
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'config.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/

STATIC_URL = '/static/'

# 開発環境下で静的ファイルを参照する先
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

# 本番環境で静的ファイルを参照する先
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# メディアファイルpath
MEDIA_URL = '/media/''

在settings.py文件中创建一个名为.env的文件来进行引用。

# .envファイルの作成
type nul > .env

# .envにコピペしておいたSECRET_KEYを追加する
SECRET_KEY = '+_f1u^*rb8+%cn-4o*kjn_(15*wspz0*!c)@=ll08odexo88a4'

开始使用待办事项应用

python manage.py startapp todos

将应用程序添加到settings.py文件中。

# conig/settings.py

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todos.app.TodosConfig'  # 追加
]

制作模型并进行迁移,然后将其注册到管理员界面上。

# todos/models.py
from django.db import models


class Todo(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()

    def __str__(self):
        return self.title
$ python manage.py makemigrations todos
Migrations for 'todos':
  todos\migrations\0001_initial.py
    - Create model Todo

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, todos
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying sessions.0001_initial... OK
  Applying todos.0001_initial... OK
# todos/admin.py
from django.contrib import admin
from .models import Todo


admin.site.register(Todo)

创建管理用户并登录为管理员,在待办事项中添加大约3个任务。

$ python manage.py createsuperuser
ユーザー名 (leave blank to use 'yourname'): yourname
メールアドレス: youraddress@mail.com
Password:
Password (again):
Superuser created successfully.

$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
March 10, 2020 - 23:41:26
Django version 3.0.4, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

当你访问 http://127.0.0.1:8000/admin 时,会打开django-admin的登录页面,因此请使用createsuperuser注册信息进行登录。我们先注册好3个待办事项。

开始使用Django REST framework。

需要更新 config/settings.py 文件,以便能够使用最初通过 pip 安装的 restframework。

# config/settings.py

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # 3rd party
    'rest_framework',

    # Local
    'todos.apps.TodosConfig',
]

# 追加
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

rest_framework.permissions.AllowAny 是为了解除 django-rest-framework 隐式设置的默认设置 ‘DEFAULT_PERMISSION_CLASSES’ 而存在的。
对于这个设置我还不是很清楚,但是我会先继续前进。

创建todos/urls.py,todos/views.py,todos/serializers.py。

网址

通过在config/urls.py中添加每个应用程序的urls.py文件来进行配置。

# config/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('todos.urls'))
]

我會添加 todos/urls.py 程式。

$ type nul > todos\urls.py
# todos/urls.py
from django.urls import path
from .views import ListTodo, DetailTodo

urlpatterns = [
    path('<int:pk>/', DetailTodo.as_view()),
    path('', ListTodo.as_view())
]
序列化器

添加序列化程序(serializers.py)以将模型实例转换为JSON格式。

type nul > todos\serializers.py
# todos/serializers.py
from rest_framework import serializers
from .models import Todo


class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = ('id', 'title', 'body')

如果在 fields = (‘id’, ‘title’, ‘text’) 中不指定 id 为 PrimaryKey,Django 会自动添加。

观点

在使用Django Rest Framework创建views.py时,需要继承rest_framework.generics中的APIView类。

# todos/views.py

from django.shortcuts import render
from rest_framework import generics
from .models import Todo
from .serializers import TodoSerializer


class ListTodo(generics.ListAPIView):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer


class DetailTodo(generics.RetrieveAPIView):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

虽然路由器等设备尚未进行配置,但现在可以通过 API 使用待办事项项了。您可以在开发服务器上访问 http://127.0.0.1:8000/api/ 来查看 API 视图。

这里是常见的Django本地开发环境。

CORS 的中文译名是跨域资源共享。

在React和Django协同工作时,当React启动在localhost:3000上时,需要让它与Django的API服务器localhost:8000进行JSON通信。请安装django-cors-headers插件。

python -m pip install django-cors-headers

只需要一种选项:

更新config/settings.py文件。

# config/settings.py

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # 3rd party
    'rest_framework',
    'corsheaders',

    # Local
    'todos.apps.TodosConfig',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMidddleware',  # 追加
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

##################
# rest_framework #
##################

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

CORS_ORIGIN_WHITELIST = (
    'http://localhost:3000',
)
考试 shì)

我要做考试。

# todos/test.py

from django.test import TestCase
from .models import Todo


class TodoModelTest(TestCase):

    @classmethod
    def setUpTestData(cls):
        Todo.objects.create(title="first todo", body="a body here")

    def test_title_content(self):
        todo = Todo.objects.get(id=1)
        excepted_object_name = f'{todo.title}'
        self.assertEqual(excepted_object_name, 'first todo')

    def test_body_content(self):
        todo = Todo.objects.get(id=1)
        excepted_object_name = f'{todo.body}'
        self.assertEqual(excepted_object_name, 'a body here')

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.007s

OK
Destroying test database for alias 'default'...

好像进展顺利。

推进前端开发

让我们预先安装Node.js吧。

$ cd frontend
$ npx create-react-app .
$ yarn start

yarn run v1.22.0
$ react-scripts start
i 「wds」: Project is running at http://192.168.56.1/
i 「wds」: webpack output is served from
i 「wds」: Content not from webpack is served from
C:\--you-path--\gke-django-tutorial\frontend\public
i 「wds」: 404s will fallback to /
Starting the development server...
Compiled successfully!

You can now view frontend in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.56.1:3000

Note that the development build is not optimized.
To create a production build, use yarn build.

我能够使用React开始前端项目。
当在浏览器中访问http:// localhost:3000时,您可以看到React的欢迎页面。

App.js 应用程序.js

api的终点会以以下的形式返回api,我们需要意识到这一点。
首先,我们将在模拟数据中进行尝试。

[
  {
    "id": 1,
    "title": "test_title",
    "body": "body of test_title"
  },
  {
    "id": 2,
    "title": "test_title2",
    "body": "body of test_title2"
  },
  {
    "id": 3,
    "title": "test_title3",
    "body": "body of test_title3"
  }
]
// src/App.js

import React, { Component } from "react";
import axios from "axiso";
import "./App.css";

const list = [
  {
    id: 1,
    title: "test_title",
    body: "body of test_title"
  },
  {
    id: 2,
    title: "test_title2",
    body: "body of test_title2"
  },
  {
    id: 3,
    title: "test_title3",
    body: "body of test_title3"
  }
];

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { list };
  }

  render() {
    return (
      <div>
        {this.state.list.map(item => (
          <div key={item.id}>
            <h1>{item.title}</h1>
            <p>{item.body}</p>
          </div>
        ))}
      </div>
    );
  }
}

export default App;

当访问 http://localhost:3000 时,显示了模拟数据。
我希望用从后端获取的数据来显示它。

axios 形就是「axios」

在前端进行请求时,可以使用内置的Fetch API或axios库来发送请求,但我们选择使用axios。

npm install axios --save
yarn start

我会修改 App.js。

// src/App.js

import React, { Component } from "react";
import axios from "axios";
import "./App.css";

class App extends Component {
  state = {
    todos: []
  };

  componentDidMount() {
    this.getTodos();
  }

  getTodos() {
    axios
      .get("http://127.0.0.1:8000/api/")
      .then(res => {
        this.setState({ todos: res.data });
      })
      .catch(err => {
        console.log(err);
      });
  }

  render() {
    return (
      <div>
        {this.state.todos.map(item => (
          <div key={item.id}>
            <h1>{item.title}</h1>
            <p>{item.body}</p>
          </div>
        ))}
      </div>
    );
  }
}

export default App;

我们已经成功地在本地环境中通过前端调用后端的API,实现了待办事项列表的显示。

虽然形式非常简单,但总算成功完成了Django和React的整合。

我希望接下来可以将这个项目进行Docker化。

推进Docker化

我们会为前端和后端分别创建 Dockerfile,并试着创建了一个后端容器和一个前端容器。

首先我们来考虑一下可以通过Docker-compose启动的部分。

将后端进行Docker化。

在编写 Dockerfile 之前,Django 方面有几件事可以先做。

# 静的ファイ用のディレクトリ
$ mkdir backend\static
# 静的ファイルを全部集めてstaticifilesディレクトリに集められ
$ python manage.py collectstatic

虽然通常情况下会将数据库内容和settings.py在本地和生产环境中进行分离,但首先我们要考虑将现有形式直接转换为Docker化的可能性。

在backend目录中创建一个名为Dockerfile的文件。

$ type nul > backend\Dockerfile
# backend/Dockerfile

# set base image
FROM python:3.7

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# set work directory
RUN mkdir /code
WORKDIR /code

# install dependencies
COPY requirements.txt /code/
RUN python3 -m pip install --upgrade pip setuptools
RUN pip install -r requirements.txt

# Copy project
COPY . /code/

EXPOSE 8000

然后将 docker-compose.yml 文件放置在项目目录中,以便使用 docker-compose up 命令启动后端容器。

# docker-compose.yml
version: "3.7"

services:
  backend:
    build: ./backend/.
    command: python /code/manage.py runserver 0.0.0.0:8000
    volumes:
      - ./backend:/code
    ports:
      - "8000:8000"
$ docker-compose up

使用本链接http://localhost:8000/api/,即可访问后端容器的 DRF 视图。
如果使用 DockerToolbox,请使用 Docker 主机的 IP 地址进行访问。

将前端代码容器化

接下来将前端部分进行Docker化。

参考页面:
Docker化React应用程序
使用Docker Compose、Django和Create React App创建应用程序
在开发和生产中使用Docker来运行Node.js

前端是用React构建的。如何将其Docker化比较好呢?
我们可以像后端一样,在前端目录中创建一个Dockerfile。

type nul > frontend\Dockerfile
# frontend/Dockerfile
FROM node:12.2.0-alpine

RUN mkdir /code
WORKDIR /code

# Install dependencies
COPY package.json /code/
COPY package-lock.json /code/
RUN npm install

# Add rest of the client code
COPY . /code/

EXPOSE 3000

您可以使用 package.json 在 node 容器内构建相同的环境。
将 frontend 服务添加到 docker-compose.yml 文件中。

# docker-compose.yml
version: "3.7"

services:
  backend:
    build: ./backend/.
    volumes:
      - ./backend:/code
    ports:
      - "8000:8000"
    stdin_open: true
    tty: true
    command: python /code/manage.py runserver 0.0.0.0:8000
    environment:
      - CHOKIDAR_USEPOLLING=true
  frontend:
    build: ./frontend/.
    volumes:
      - ./frontend:/code
      - /code/node_modules
    ports:
      - "3000:3000"
    command: npm start
    stdin_open: true
    tty: true
    environment:
      - CHOKIDAR_USEPOLLING=true
      - NODE_ENV=development
    depends_on:
      - backend

在将CHOKIDAR_USEPOLLING=true添加到环境变量之后,不需要重新构建图像,就可以实现热加载。

由于前端部分的 node_modules 目录非常庞大,因此在挂载或复制时会花费大量时间。
因此,我们将添加 .dockerignore 文件,以确保不在镜像构建中使用 node_modules。(对吗?)

$ type nul > frontend\.dockerignore
/node_modules

在执行 docker-compose up 之前

你现在可以使用docker-compose up来启动了,但是如果你正在使用docker-toolbox,那么你的端口转发主机名不是localhost。你需要将其更改为主机IP。使用docker-machine ls命令来查找你正在使用的主机IP。

后端/设置.py

在本地的浏览器上,为了从前端容器访问后端容器,需要将 Docker 主机的 IP 地址添加到 CORS_ORIGIN_WHITELIST。

# backend/settings.py

CORS_ORIGIN_WHITELIST = (
    'http://localhost:3000',
    'http://192.168.99.100:3000',  # 追加
)

前端/src/App.js

API的终端点将是Docker主机的IP地址。在这里我们使用192.168.99.100:8000作为示例。

// src/App.js

import React, { Component } from "react";
import axios from "axios";
import "./App.css";

class App extends Component {
  state = {
    todos: []
  };

  componentDidMount() {
    this.getTodos();
  }

  getTodos() {
    axios
      .get("http://192.168.99.100:8000/api/") //変更
      .then(res => {
        this.setState({ todos: res.data });
      })
      .catch(err => {
        console.log(err);
      });
  }

  render() {
    return (
      <div>
        <h1>mother fucker!!?? </h1>
        {this.state.todos.map(item => (
          <div key={item.id}>
            <h1>{item.title}</h1>
            <p>{item.body}</p>
          </div>
        ))}
      </div>
    );
  }
}

export default App;

运行docker-compose

在包含docker-compose.yml文件的目录下运行docker-compose up。

$ docker-compose up --build

React编译需要花费一定的时间。

如果能够启动的话,访问http://localhost:3000,应该可以在本地看到与之前显示的内容相同。

然后将GKE

如果可以的话,我会添加的。

广告
将在 10 秒后关闭
bannerAds