django-vite を使って、Django の template 上で React を動かしてみた

これは Djangoのカレンダー | Advent Calendar 2022 - Qiita 12/6の記事です。

フロントエンドのビルドツール Vite を使って、ReactをDjangoで動かす方法を調べたところ、awesome-viteのページに django-vite の記載がありました。

 
そこで、フロントエンドとして React + Vite 、バックエンドとして Django + Django REST framework な構成でTODOアプリを作って試してみたため、メモを残します。

なお、記事が長いため、記事ではソースコードを省略している部分が多々あります。もしソースコードの詳細を確認したい場合はGithubを参照ください。

 
目次

 

環境

  • WSL2
  • Django環境
  • React 環境
    • React 18.2.0
    • React Router 6.4.4
    • Vite 3.2.3
    • TypeScript 4.6.4

 
なお、 django-vite を使った場合のシステム構成は書籍「 現場で使える Django REST Framework の教科書 (Django の教科書シリーズ) | 横瀬 明仁 」のチュートリアル同様、

  • Django
  • Django REST framework (DRF)
    • フロントエンド向けWeb API
  • React + React Router + axios
    • フロントエンドのルーティングやDRFのWeb APIへのリクエス

となります*1

 

バックエンド(Django + DRF)のセットアップ

まずはバックエンドから作成していきます。なお、ソースコードは後述の場所で公開していますので、バックエンドの作成では省略します。

モデルを含む task アプリと、APIapi アプリを作成します。

$ pip install django djangorestframework django-vite

$ django-admin startproject config .

$ python manage.py startapp api
$ python manage.py startapp task

 
taskアプリを実装します。今回はadminサイトでデータ登録するため、 admin.py も実装しておきます。

  • models.py
  • admin.py

 
apiアプリも実装します。

  • serializers.py
  • views.py
  • urls.py

 
configディレクトリにも修正を加えます。

  • urls.py

 
ファイルの実装が終わったところでマイグレーションを実行します。

$ python manage.py makemigrations
$ python manage.py migrate

 
adminサイトでデータ登録するためのユーザーを作成します。

$ python manage.py createsuperuser
Username (leave blank to use 'thinkami'): admin
Email address: admin@example.com
Password: pass12345
Password (again): pass12345
Superuser created successfully.

 
Djangoを起動し、adminサイトでデータを登録します。

$ python manage.py runserver

# http://localhost:8000/admin/ にアクセスして、データ登録

 
データ登録後、curlで動作確認を行います。レスポンスがあればOKです。

$ curl http://localhost:8000/api/tasks/
[{"id":1,"content":"買い物","updated_at":"2022-11-28T20:36:19.668279+09:00"}]

 

フロントエンドのセットアップ

Viteを使ってReactをセットアップ

django-viteのREADMEによると、Viteを使ってReactをセットアップするところまでは、通常のVite + Reactのセットアップで良いようです。
https://github.com/MrBin99/django-vite#vitejs

 
そこで、Viteを使って frontend ディレクトリの中にReactアプリをセットアップしていきます。

$ npm create vite@latest
Need to install the following packages:
  create-vite@3.2.1
Ok to proceed? (y) y
✔ Project name: … frontend
✔ Select a framework: › React
✔ Select a variant: › TypeScript

Scaffolding project in path/to/django_vite/static...

Done. Now run:

  cd static
  npm install
  npm run dev

 
あとは画面にあるように、 npm installnpm run dev を実行した後、ターミナルに表示されたURLへアクセスして、Vite + React 画面が出ればOKです。

 

frontendディレクトリの整理

以下の設定ファイルを、 frontend ディレクトリからプロジェクトルートディレクトリへと移動します。

  • package.json
  • package-lock.json
  • tsconfig.json
  • tsconfig.node.json
  • vite.config.ts

 
また、以下の不要なディレクトリファイルは削除します。

  • node_modules/
  • public/
  • index.html

 
整理後、ルートディレクトリで npm install しておきます。

 

DjangoとReactを連携して、開発環境で動かす

ここまでで一通りセットアップできたので、DjangoとReactが連携するアプリを作っていきます。

 

vite.config.tsでNode.jsの path モジュールを import できるようにする

以下を参考に、Node.jsの path モジュールを import できるようにします。
TypeScriptのモジュールのインポートには import を使う|まくろぐ

$ npm install --save-dev @types/node

 

Djangoと連携できるよう vite.config.ts を設定

Viteの設定ファイルである vite.config.ts を定義します。

 
設定ファイル全体は以下のとおりです。

import { defineConfig } from 'vite'
import { resolve } from 'path'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],

  // 開発環境のmain.tsxが置いてある場所
  root: resolve('./frontend/src'),

  // Djangoでの静的ファイル配信設定である STATIC_URL と同じになるよう設定
  base: '/static/',

  server: {
    host: 'localhost',
    port: 5173,
    open: false,
    watch: {
      usePolling: true,
      disableGlobbing: false,
    },
  },
})

 

Vite側と連携できるよう、Djangoの設定ファイルを修正

Djangoの設定ファイルは開発環境向けと本番環境向けとで分けます。

今回は開発環境で動作を確認してから本番環境の設定を行うことにします。

そこで、まずは開発環境向けの設定のみを行います。

 

開発環境向け

django-viteまわりの設定

django-viteのREADMEに従い設定します。

いくつか設定項目はありますが、開発環境向けとしては以下の項目を settings.py に追加すれば良さそうです。

ちなみに、Viteは3系から開発サーバの起動するポートが 5173 へと変更になりましたが、django-viteでのデフォルトポートは今のところ 3000 のままのようです。
Vite 3.0 no longer defaults to port 3000 · Issue #51 · MrBin99/django-vite

そのため、 DJANGO_VITE_DEV_SERVER_PORT の設定も必要です。

# 開発環境の場合、この値は参照されないので設定不要
# ただ、キーや中身がないと以下のエラーになるため、中身は適当な値を設定しておく
# AttributeError: 'Settings' object has no attribute 'DJANGO_VITE_ASSETS_PATH'
DJANGO_VITE_ASSETS_PATH = BASE_DIR

# HMRするためDebugと同じにしておく
DJANGO_VITE_DEV_MODE = DEBUG

# Vite.jsがv3系からポートが変更になったので対応
DJANGO_VITE_DEV_SERVER_PORT = 5173

 

CORSの設定

開発環境では、ReactとDjangoでは別オリジンになります。

 
そのため、 django-cors-headers をインストールしてCORSの設定を追加します。
adamchainz/django-cors-headers: Django app for handling the server headers required for Cross-Origin Resource Sharing (CORS)

INSTALLED_APPS = [
    ...
    'corsheaders',
    ...
]

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',  # 追加
    'django.middleware.common.CommonMiddleware',
    ...
]

CORS_ALLOWED_ORIGINS = (
    'http://localhost:5173',
    'http://127.0.0.1:5173',
)

 

templateで if debug をするための設定

後述しますが、templateでは開発環境のみ設定するタグやスクリプトがあります。

そのためには、Djangoのtemplateで if debug を使うことで、開発環境のみレンダリングするようにします。

 
また、templateで debug の値を使えるようにするには、

  • template の context_processors'django.template.context_processors.debug' を追加
  • INTERNAL_IPS で使用するIPアドレスを定義

すれば良さそうです。
django.template.context_processors.debug | The Django template language: for Python programmers | Django ドキュメント | Django

 
なお、ルートディレクトリの templates ディレクトリに React を render するテンプレートを入れておくことから、 DIRS への追加も合わせて行っておきます。

TEMPLATES = [
    {
        ...
        'DIRS': [BASE_DIR / 'templates'],  # 追加
        ...
        'OPTIONS': {
            'context_processors': [
                ...
                'django.contrib.messages.context_processors.messages',
                'django.template.context_processors.debug',  # 追加
            ],
        },
    },
]

 

フロントエンドを実装

今回は frontend/src ディレクトリの中に、Reactアプリを実装していきます。

ライブラリのインストール

まずは必要なライブラリとして

  • React Router
    • フロントエンドでのルーティングを行うため
  • axios
    • バックエンドとの連携を行うため

をインストールします。

$ npm install axios
$ npm install react-router-dom

 

Reactアプリを実装

frontend/src/ ディレクトリの中に、以下の役割を持ったファイルを用意します。

なお、各ファイルの実装については、 django-vite だからといって特別なことをしていません。

そのため、ここでは省略します。詳細は公開したソースコードを参照してください。

ファイルパス 主な役割
main.tsx エントリポイントであり、App.tsxを読み込む
App.tsx pages/ 以下のコンポーネントに対するルーティングを定義
pages/Index.tsx 一覧ページ
pages/TaskDetail.tsx 詳細ページ
pages/TaskForm.tsx 登録ページ

 

Djangoの実装

Django template

django-viteのテンプレートタグを使う

今回は templates/index.html の上で django-vite のテンプレートタグ

  • {% load django_vite %}
  • {% vite_hmr_client %}
  • {% vite_asset '<path to your asset>' %}

を使ってReactアプリを動かすことにします。
MrBin99/django-vite: Integration of ViteJS in a Django project.

 
ちなみに、 vite_asset に指定する path to your asset は、Reactのエントリポイントのファイルを指定します。今回は main.tsx を指定します。

また、 main.tsx までのパス frontend/src については、 vite.config.ts にて root で設定しています。

export default defineConfig({
  // 開発環境のmain.tsxが置いてある場所
  root: resolve('./frontend/src'),
//...
}

 

@vitejs/plugin-react can't detect preamble に対応するコードを追加

Reactの場合、 django-vite のテンプレートタグを書いただけでは

Uncaught Error: @vitejs/plugin-react can't detect preamble. Something is wrong. See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201

のようなエラーが表示されて動作しません。

そのため、 django-vite のissueにあるコードを追加する必要があります。
Add tag to render the react-refresh script · Issue #15 · MrBin99/django-vite

なお、この問題に対するプルリクも存在しましたが、現時点ではマージされていません。
Add template tag to enable react-refresh by BrandonShar · Pull Request #53 · MrBin99/django-vite

 

templateのコード全体

上記の対応を踏まえたtemplateのコード全体は以下となります。

{% load django_vite %}
{% load static %}

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

      {% if debug %}
          <script type="module">
            import RefreshRuntime from 'http://localhost:5173/@react-refresh'
            RefreshRuntime.injectIntoGlobalHook(window)
            window.$RefreshReg$ = () => {}
            window.$RefreshSig$ = () => (type) => type
            window.__vite_plugin_react_preamble_installed__ = true
          </script>
          {% vite_hmr_client %}
          {% vite_asset 'main.tsx' %}

      {% else %}
          {% vite_asset 'main.tsx' %}
      {% endif %}

    <title>Django + Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

 

urls.pyへの追加

上記テンプレートをTemplateViewで描画するよう、config/urls.pyに追加します。

なお、 re_path を使い、Djangoでルーティングできない場合はフロントエンドにルーティングを任せています。

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
    re_path('', TemplateView.as_view(template_name='index.html')),  # 追加
]

 

動作確認

DjangoとViteの開発サーバをそれぞれ起動します。

# Django
python manage.py runserer

# Vite
npm run dev

 
その後、 http://localhost:8000/tasks にアクセスするとTODOアプリが表示されます。

 

DjangoとReactを連携して、本番環境で動かす

開発環境での動作が確認できたので、次は本番環境で動作させてみます。

 

Vite側での作業

vite.config.ts にビルド設定を追加

開発環境ではReactアプリをViteの開発サーバから配信していました。

一方、 django-vite のREADMEにあるように、本番環境ではDjangoで配信することになります。

In production mode, assets are included as standard assets (no ViteJS webserver and HMR) like default Django static files. This means that your assets must be compiled with ViteJS before.

Configuration | Usage | MrBin99/django-vite: Integration of ViteJS in a Django project.

 
そこで、ViteでReactアプリをビルドするための設定を vite.config.ts に追加します。

rollupOptions.input にて、Reactのエントリポイントのファイルを指定します。
build.rollupOptions | ビルドオプション | Vite

また、 outDirコンパイル後のファイルの出力先を指定します。
build.outDir | ビルドオプション | Vite

全体はこんな感じです。

export default defineConfig({
  // ...
  build: {
    // コンパイル後の出力先。DJANGO_VITE_ASSET_PATHと一致させる
    outDir: resolve('./frontend/dist'),
    assetsDir: '',
    manifest: true,
    emptyOutDir: true,
    rollupOptions: {
      input: {
        main: resolve('./frontend/src/main.tsx'),
      },
      output: {
        chunkFileNames: undefined,
      },
    }
  }
})

 

ViteでReactアプリをビルド

ビルドの設定ができたため、Viteでビルドします。

$ npm run build

> static@0.0.0 build
> tsc && vite build

vite v3.2.4 building for production...
✓ 80 modules transformed.
../dist/manifest.json       0.21 KiB
../dist/main.3fce1f81.css   1.37 KiB / gzip: 0.71 KiB
../dist/main.f792177d.js    186.01 KiB / gzip: 61.81 KiB

 
すると、 build.outDir で指定したディレクトリにファイルが出力されます。

 
Vite側での作業は以上です。

 

Djangoの設定

settings_production.py の設定

今回は config/settings_production.py に本番環境用の設定を追加することにします。

まずは、本番環境なので以下の環境変数を設定します。

from .settings import *

ALLOWED_HOSTS = [
    'localhost',
    '127.0.0.1',
]

# 本番環境化
DEBUG = False
DJANGO_VITE_DEV_MODE = False

 
また、 django-cors-headers 向けの設定は不要なので空配列を指定しておきます。

CORS_ALLOWED_ORIGINS = []

 
本番環境では collectstatic コマンドを使って静的ファイルを収集するため、収集対象のディレクトリを設定します。

DJANGO_VITE_ASSETS_PATH でViteでビルドした後のファイルが含まれるディレクトリ(Viteの build.outDir で指定したディレクトリ) を指定します。
Configuration | MrBin99/django-vite: Integration of ViteJS in a Django project.

また、ビルドしたファイルを collectstatic の収集対象とするため、 STATICFILES_DIRSDJANGO_VITE_ASSETS_PATH を含めます。
STATICFILES_DIRS | 設定 | Django ドキュメント | Django

他に、 collectstatic の結果を入れておくディレクトリも STATIC_ROOT にて指定します。今回はルート直下の collected_static ディレクトリに入れるようにします。
STATIC_ROOT | 設定 | Django ドキュメント | Django

ちなみに、静的ファイルに関する設定の全体像は、以下の記事の図がわかりやすかったです。
Djangoにおける静的ファイル(static file)の取り扱い - Qiita

# collectstaticの結果を格納するディレクトリ
STATIC_ROOT = BASE_DIR / 'collected_static'

# 本番環境の場合、build.outDir と同じパスを指定する
DJANGO_VITE_ASSETS_PATH = BASE_DIR / 'frontend' / 'dist'
STATICFILES_DIRS = [DJANGO_VITE_ASSETS_PATH]

 

本番環境でも静的ファイルをDjangoの開発サーバから配信するよう urls.py に設定

色々準備するのが手間なので、今回はDjangoの開発サーバから静的ファイルを配信することにします。

(注意) 通常想定している本番運用とは異なるため、今回の記事を参考にして環境を構築する場合は、ここの設定はあるべき姿にしてください。

 
settings.pyで DEBUG=True であればDjangoの開発サーバから配信するため、今回はその設定を無理やり移植することで、 DEBUG=False のときもDjangoの開発サーバから配信できるようにします。

ソースコードを読むと django/conf/urls/static.py のこのあたりの実装を移植・修正すれば良さそうです。
https://github.com/django/django/blob/4.1.3/django/conf/urls/static.py#L10

なお、ルーティングの仕様によりtemplateへルーティングされるのを防ぐため、ルーティングの先頭に静的ファイルのルーティングを追加します。

static_patterns = [
    re_path(r"^%s(?P<path>.*)$" % re.escape(settings.STATIC_URL.lstrip("/")),
            serve,
            {'document_root': settings.STATIC_ROOT}),
] if not settings.DEBUG else []

urlpatterns = static_patterns + [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
    re_path('', TemplateView.as_view(template_name='index.html')),
]

 

collectstatic の実行

設定が終わったので、 collectstatic を実行します。

なお、本番環境の設定ファイルを使って実行するよう --settings config.settings_production オプションを付けて実行します。

$ python manage.py collectstatic --settings config.settings_production

You have requested to collect static files at the destination
location as specified in your settings:

    path/to/todo_app_with_django_vite/collected_static

This will overwrite existing files!
Are you sure you want to do this?

Type 'yes' to continue, or 'no' to cancel: yes

3 static files copied to 'path/to/todo_app_with_django_vite/collected_static', 165 unmodified.

 

本番環境を起動して動作確認

ViteやDjangoの開発サーバを停止した上で、Djangoの本番環境を起動します。

$ python manage.py runserver --settings config.settings_production
Performing system checks...

System check identified no issues (0 silenced).
December 06, 2022 - 22:46:40
Django version 4.1.3, using settings 'config.settings_production'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

 
http://localhost:8001/tasks にアクセスすると、画面が表示されます。

React developer tools の色が青くなっており、ビルドしたReactで動いていることが分かります。

 

また、Djangoのログを見ると、Djangoから静的ファイルが配信されていることも分かります。

[06/Dec/2022 22:47:59] "GET /tasks HTTP/1.1" 200 422
[06/Dec/2022 22:47:59] "GET /static/main.3fce1f81.css HTTP/1.1" 200 1405
[06/Dec/2022 22:47:59] "GET /static/main.f792177d.js HTTP/1.1" 200 190476
[06/Dec/2022 22:47:59] "GET /api/tasks/ HTTP/1.1" 200 311
[06/Dec/2022 22:47:59] "GET /favicon.ico HTTP/1.1" 200 422

 
以上で、django-viteを使って、Djangoの template 上で React を動かすことができました。

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/react_todo_app_with_django_vite

今回のプルリクはこちらです。
https://github.com/thinkAmi-sandbox/react_todo_app_with_django_vite/pull/1

*1:akiyokoさんの記事 https://akiyoko.hatenablog.jp/entry/2022/12/01/064746 によると、DRFの本は今月改訂になるようです