Django+Nuxtの同一オリジンによるSPA開発
DjangoとNuxtを使ってSPAの開発を行うときの一つの形を考えていきたいと思います。 大抵の場合Nuxtはフロントエンド、DjangoはAPIサーバーとして開発を進める場合が多いかと思います。 このフロントエンドからAPIサーバーへの通信ですがクロスドメイン/オリジン通信になる場合、 クライアント・サーバー両方にCORSの対応をしなければなりません。 NuxtならSSRモードのasyncData内での通信や@nuxtjs/proxyを使用して対応している場合も多いかと思います。
ここでは上記とは異なる方法、Django・Nuxt間を同一オリジンで通信する形を考えていきたいと思います。
開発環境
開発環境として下記のもので動作確認していきます。
・CentOS: 7.8
・Python: 3.8.3
・Node.js: 14.4.0
・Django: 3.0.6
・Nuxt: 2.12.2
私はDockerで上記環境を構築してVSCodeで開発しています。
※Docker+VSCodeによる開発環境の構築(Django+Nuxt編)
Django、Nuxtのプロジェクトを作成します。以下のディレクトリ構成になるようにします。
django-admin startproject djanuxt
cd djanuxt
npx create-nuxt-app nuxt
Nuxtはプロジェクト名等を聞いてきますので、とりあえず下記の入力内容で動作確認していきます。(※Axios、Single Page Appは必須)
上記コマンドで作ったプロジェクトは以下のディレクトリ構成になります。
djanuxt
│
├ djanuxt
├ nuxt
└ manage.py
必要ならDjangoプロジェクトのマイグレートをしておきます。
(※SQLite使用の場合、バージョン注意:SQLite 3.8.3 or later is required)
※ djanuxt/manage.pyの位置で
python manage.py migrate
Django側の開発
Djangoの環境変数として.envファイルを作成して以下の環境変数を定義しておきます。
[djanuxt/.env]
+.env
NUXT_MODE=spa
NUXT_URL=http://localhost:3000
NUXT_BASE=/nuxt/
NUXT_DEBUG=true
Djangoのアプリケーション構成ファイルを作成します。
[djanuxt/djanuxt/apps.py]
+apps.py
from dotenv import load_dotenv
from django.apps import AppConfig
from . import settings
import os
load_dotenv(verbose=True)
class DjanuxtConfig(AppConfig):
name = "djanuxt"
# Nuxt
nuxt_dir = settings.BASE_DIR + "/nuxt"
nuxt_mode = os.environ.get('NUXT_MODE')
nuxt_url = os.environ.get('NUXT_URL')
nuxt_base = os.environ.get('NUXT_BASE')
nuxt_debug = True if os.environ.get('NUXT_DEBUG')=="true" else False
settings.pyファイルに以下の+部分の行を追加します。
[djanuxt/djanuxt/settings.py]
settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'sslserver',
+ 'djanuxt.apps.DjanuxtConfig',
]
・
・
urls.pyファイルに以下の+部分の行を追加もしくは変更します。
[djanuxt/djanuxt/urls.py]
urls.py
from django.contrib import admin
+ from django.urls import path, re_path # re_path追加
+ from . import views
+ from .nuxt import NuxtView
urlpatterns = [
path('admin/', admin.site.urls),
+ # Nuxt
+ re_path(r'^nuxt/$', NuxtView.as_view(), name='nuxt'),
+ re_path(r'^nuxt/(?P\w+)', NuxtView.as_view()),
]
DjangoとNuxtの連携部分となるnuxt.pyを作成します。
[djanuxt/djanuxt/nuxt.py]
+nuxt.py
from django.http import HttpResponse
from django.views.generic import View
from .apps import DjanuxtConfig as config
import os
import re
import urllib.request
import mimetypes
import json
class NuxtView(View):
# Constractor
def __init__(self):
pass
# GET Request
def get(self, request, *args, **kwargs):
response = self.nuxt(request, *args, **kwargs)
return response
# POST Request
def post(self, request, *args, **kwargs):
response = self.nuxt(request, *args, **kwargs)
return response
# Nuxt
def nuxt(self, request, *args, **kwargs):
content = None
# Dispatch
dispatch = None
if (config.nuxt_debug or config.nuxt_mode == "ssr" or config.nuxt_mode == "universal"):
dispatch = config.nuxt_url + config.nuxt_base
else:
dispatch = config.nuxt_dir + "/dist/"
# Path
path = None
if (request.path.startswith(config.nuxt_base)):
path = re.sub(rf"^{config.nuxt_base}", "", request.path)
if (len(path) != 0):
dispatch = re.sub(r"/$", "", dispatch)
dispatch = f"{dispatch}/{path}"
# Parameters
method = request.method
parameters = {}
request_method = getattr(request, method)
for key in request_method:
parameters[key] = request_method[key]
# Load Content
try:
if (re.match(r"^https?://", dispatch) is not None):
content = self.__redirect(method, dispatch, parameters)
else:
dispatch = re.sub(r"/$", "", dispatch)
if (os.path.isdir(dispatch)):
dispatch += "/index.html"
else:
dispatch = self.__vuefile(dispatch)
with open(dispatch, "r") as file:
content = file.read()
except Exception as ex:
content = ex
# Mime Type
mime_type = mimetypes.guess_type(dispatch)[0]
if (mime_type is None):
mime_type = "text/html"
if (hasattr(content, "object")):
content = content.object
return HttpResponse(content, content_type=mime_type)
# Redirect
def __redirect(self, method: str, url: str, params: dict):
content = None
# Build Request
request = None
if (method == "GET"):
request = urllib.request.Request("{0}?{1}".format(url, urllib.parse.urlencode(params)), method="GET")
elif (method == "POST"):
header = {"Content-Type": "application/json"}
parameters = json.dumps(params).encode()
request = urllib.request.Request(url, parameters, header, method="POST")
# Send & Recv
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor())
with opener.open(request) as response:
content = response.read().decode('utf-8')
return content
# Search Vue Index.html
def __vuefile(self, filename: str):
vuefile = filename
path, ext = os.path.splitext(vuefile)
if (len(ext) == 0):
basename = os.path.basename(vuefile)
indexfile = re.sub(rf"{basename}$", "index.html", vuefile)
if (os.path.isfile(indexfile)):
vuefile = indexfile
else:
upper_dir = re.sub(rf"/{basename}$", "", vuefile)
root_dir = config.nuxt_dir + "/dist"
if (len(root_dir) <= len(upper_dir)):
vuefile = self.__vuefile(upper_dir)
return vuefile
Nuxt側の開発
nuxt.config.jsを編集。以下の+箇所の行を追加します。
[djanuxt/nuxt/nuxt.config.js]
nuxt.config.js
export default {
mode: 'spa',
+ server: {
+ port: 3000,
+ host: '0.0.0.0'
+ },
+ router: {
+ base: '/nuxt/'
+ },
head: {
・
},
・
・
・
axios: {
+ baseURL: '',
},
・
・
}
以上でDjango・Nuxt連携の実装が出来たので試験用画面を作って動作確認をしてみます。
動作確認用画面の実装
Django側にトップページを作成します。 urls.pyファイルに以下の+部分の行を追加もしくは変更します。
[djanuxt/djanuxt/urls.py]
urls.py
from django.contrib import admin
from django.urls import path, re_path
from .nuxt import NuxtView
+ from . import views
urlpatterns = [
path('admin/', admin.site.urls),
# Nuxt
re_path(r'^nuxt/$', NuxtView.as_view(), name='nuxt'),
re_path(r'^nuxt/(?P\w+)', NuxtView.as_view()),
+ # Django Top Page
+ path('', views.index, name='index'),
]
Djangoのビューを作成します。
[djanuxt/djanuxt/views.py]
+views.py
from django.shortcuts import render
def index(request):
return render(request, "index.html")
Djangoのテンプレートを作成します。templatesフォルダ作成して以下のindex.html配置。
[djanuxt/djanuxt/templates/index.html]
+templates/index.html
<!DOCTYPE html>
<html>
<head lang="ja">
<meta charset="UTF-8">
<title>Djanuxt</title>
</head>
<body>
<h1>Django</h1>
<button id="id_get">Go To Nuxt By GET</button>
<button id="id_post">Go To Nuxt By POST</button>
<form name="form" method="POST">
{% csrf_token %}
</form>
<script>
(()=>{
document.getElementById("id_get").addEventListener("click", ()=>{
document.location.href = "/nuxt";
}, false);
document.getElementById("id_post").addEventListener("click", ()=>{
document.forms['form'].action = "/nuxt/";
document.forms['form'].submit();
}, false);
})();
</script>
</body>
</html>
この状態でDjangoからNuxtの呼び出しができます。DjangoとNuxtの開発サーバーを起動してみます。
※djanuxt/manage.pyの存在するディレクトリで
python manage.py runserver 0.0.0.0:8000
Nuxtの側は
※djanuxt/nuxt/package.jsonの存在するディレクトリで
yarn run dev
起動後、
http://localhost:8000にアクセスすれば作成したDjangoのページが開きます。
http://localhost:3000/nuxtでNuxtのページが表示されます。
Django・Nuxt間を同一オリジンで通信することが目標なので、http://localhost:8000/nuxtでNuxt側にアクセスできるか確認してみます。 作ったDjangoトップページの[Go Nuxt By GET]か[Go Nuxt By POST]ボタン押下してNuxt画面へ遷移させます。 [Go Nuxt By GET]ボタンはHTTP GETメソッドで、[Go Nuxt By POST]ボタンはHTTP POSTメソッド(※CSRFトークン送信)でNuxtに遷移するようにしています。
http://localhost:8000のトップ画面から[..GET]または[...POST]ボタンを押して...
http://localhost:8000/nuxtでNuxtのページへアクセスできました。
次はNuxt⇒DjangoへAxiosで同一オリジン通信してみます。これも試験用を画面を作ります。 Nuxtトップページに試験用画面へのリンクを作ります。nuxtのindex.vueに下記の+行を追加します。
[djanuxt/nuxt/pages/index.vue]
index.vue
<template>
・
・
<a
href="https://github.com/nuxt/nuxt.js"
target="_blank"
class="button--grey"
>
GitHub
</a>
+ <nuxt-link to="app" class="button--grey">Go to Nuxt App</nuxt-link>
</div>
</div>
</div>
</template>
・
・
遷移先のページを作ります。pagesフォルダにapp.vueファイルを作成します。
[djanuxt/nuxt/pages/app.vue]
+app.vue
<template>
<div>
<h1>Nuxt</h1>
<button @click="send('GET')">Axios GET</button>
<button @click="send('POST')">Axios POST</button>
<button @click="send('PUT')">Axios PUT</button>
<button @click="send('DELETE')">Axios DELETE</button>
<div>{{ response }}</div>
<div v-html="html"></div>
</div>
</template>
<script>
export default {
async asyncData({ $axios }) {
if (process.server) {
const response = await $axios.get("https://www.yahoo.co.jp");
return {
html: response.data,
}
}
},
data() {
return {
response: null,
html: null,
}
},
methods: {
async send(method) {
// CookieからCSRF Token取得
let csrftoken = null;
let cookies = document.cookie.split(";");
for (const cookie of cookies) {
const keyvalue = cookie.split("=");
if (keyvalue[0].trim() == "csrftoken") {
csrftoken = keyvalue[1];
break;
}
}
// CSRF Token
const headers = {
"X-CSRFToken": csrftoken
};
// 各メソッド別送信
if (method == "GET") {
const response = await this.$axios.get("/api/get");
this.response = response.data;
} else if (method == "POST") {
const response = await this.$axios.post("/api/add/", {}, { headers: headers });
this.response = response.data;
} else if (method == "PUT") {
const response = await this.$axios.put("/api/edit/", {}, { headers: headers });
this.response = response.data;
} else if (method == "DELETE") {
const response = await this.$axios.delete("/api/remove/", { headers: headers });
this.response = response.data;
}
}
}
}
</script>
Django側のAPIを実装します。先程作ったDjangoのビューファイル views.pyに以下の関数を追記します。 またindex関数にはセッションオブジェクトに値を挿入する処理を追加します。
[djanuxt/djanuxt/view.py]
views.py
from django.shortcuts import render
from django.http import JsonResponse
import uuid
def index(request):
UUID = str(uuid.uuid4())
request.session.flush()
request.session['UUID'] = UUID
return render(request, "index.html")
def get(request):
response = {
"title": "REST API",
"message": "GET処理",
"UUID": request.session['UUID'] if ("UUID" in request.session) else "Nothing",
"CSRF": request.COOKIES['csrftoken'] if ("csrftoken" in request.COOKIES) else "Nothing",
}
return JsonResponse(response, safe=False)
def add(request):
response = {
"title": "REST API",
"message": "POST処理",
"UUID": request.session['UUID'] if ("UUID" in request.session) else "Nothing",
"CSRF": request.COOKIES['csrftoken'] if ("csrftoken" in request.COOKIES) else "Nothing",
}
return JsonResponse(response, safe=False)
def edit(request):
response = {
"title": "REST API",
"message": "PUT処理",
"UUID": request.session['UUID'] if ("UUID" in request.session) else "Nothing",
"CSRF": request.COOKIES['csrftoken'] if ("csrftoken" in request.COOKIES) else "Nothing",
}
return JsonResponse(response, safe=False)
def remove(request):
response = {
"title": "REST API",
"message": "DELETE処理",
"UUID": request.session['UUID'] if ("UUID" in request.session) else "Nothing",
"CSRF": request.COOKIES['csrftoken'] if ("csrftoken" in request.COOKIES) else "Nothing",
}
return JsonResponse(response, safe=False)
そしてDjangoのルーティングの設定(urls.py)に以下のAPI用の+行を追加します。
[djanuxt/djanuxt/urls.py]
urls.py
urlpatterns = [
path('admin/', admin.site.urls),
# Nuxt
re_path(r'^nuxt/$', NuxtView.as_view(), name='nuxt'),
re_path(r'^nuxt/(?P\w+)', NuxtView.as_view()),
# Django Top Page
path('', views.index, name='index'),
+ # API
+ path('api/get/', views.get, name='get'),
+ path('api/add/', views.add, name='add'),
+ path('api/edit/', views.edit, name='edit'),
+ path('api/remove/', views.remove, name='remove'),
]
では動作確認します。
http://localhost:8000/nuxtを開いて今貼った[Go to Nuxt App]のリンクボタンを押します。
app.vueのページが開きます。
AxiosのGET, POST, PUT, DELETEメソッドでDjango APIに通信してみます。 POST, PUT, DELETEメソッドでの送信はCSRFトークンが必要なのでHTTPヘッダーにセットして送信するようにしています。
通信が成功すると、レスポンスの値が表示されます。
静的ビルドのSPAモード
最後にNuxtを静的ビルドしてNuxt開発サーバーを停止した状態で動かしてみます。
(※Nuxtの開発サーバーは開発時のみ必要になります。SPAモードでNuxtをビルドしているので本番ではNuxtのサーバーは必要なくなります。 開発時はvueファイル等更新が入ったら自動ビルドして欲しいので使用しています。)
.envファイルのNUXT_DEBUGをfalseにします。.envファイルを再読み込みさせるためDjango開発サーバーを再起動します。
[djanuxt/.env]
.env
NUXT_DEBUG=false
次にNuxtを静的ビルドします。
※djanuxt/nuxt/package.jsonの存在するディレクトリで
yarn run build
ビルドが完了したらdistフォルダが出来ています。
djanuxt/nuxt/dist
Django開発サーバーの再起動後、アクセスしてみます(http://localhost:8000)。
(※ http://locahost:3000はアクセスできない状態での確認)
一つのWebサーバー上で動くDjangoとNuxtの同一オリジン連携が出来ました。
【おまけ】 SSR確認
SSRでも動くか試してみます。
.envのNUXT_MODEをssrにします(universalでもよい)。
[djanuxt/.env]
.env
NUXT_MODE=ssr
nuxt.config.jsのmodeをuniversalにする。
[djanuxt/nuxt/nuxt.config.js]
nuxt.config.js
export default {
mode: 'univarsal',
・
・
Nuxtをビルドする。universalだとクライアント側とサーバー側のビルドが行われます。
※djanuxt/nuxt/package.jsonの存在するディレクトリで
yarn run build
Nuxtサーバーをプロダクションモードで起動する(yarn run devでもいけるけど)。
※djanuxt/nuxt/package.jsonの存在するディレクトリで
yarn run start
起動後Djangoページにアクセスして/nuxt/appまで遷移してください。 spaモードの時と変わらない状態ですが、http://localhost:8000/nuxt/appを直接入力してアクセスするか[F5]でリロードするとYahooのページが表示されます。
※または<nuxt-link to="app"> を <a href="/nuxt/app">に修正
これはapp.vueのasyncDataがサーバー側で実行されaxiosが走っているからみたいですね。
[djanuxt/nuxt/pages/app.vue]
app.vue
async asyncData({ $axios }) {
if (process.server) {
const response = await $axios.get("https://www.yahoo.co.jp");
return {
html: response.data,
}
}
},
SSRはこんな動きをするんですね。
以上、Django+Nuxtの連携の一つの形を考えてきましたがもっとシンプルな方法もあるかと思います。 簡単に静的ページの領域に静的ビルドしたファイルを配置するだけでも同一オリジンを実現できると思います。 私の場合はNuxtへのアクセスを色々制限したり、レスポンス返却時のHTMLのmetaに値を動的に埋め込んだりする必要があったのでこの形に落ち着きました。 また開発時にもCORSを気にせずにNuxtの開発サーバーを使いたかったということもあります。
もともと仕事の方でPHP+Laravel+Nuxtの案件があってこの形で実装していたのですが、 Python+Djangoの仕事も少なくないのでDjango+Nuxt版も作っておこうと思った次第です。 同じ形を必要とする人がいるか分かりませんが、多少なりとも参考になる部分もあるかなと思い技術情報として公開することにしました。 またDjango+Nuxtの連携を作るに当たって多くの情報を参考にさせて頂きました。 情報を提供してくださった方々に心より感謝いたします。