Django+Nuxtの同一オリジンによるSPA開発

DjangoとNuxtを使ってSPAの開発を行うときの一つの形を考えていきたいと思います。 大抵の場合Nuxtはフロントエンド、DjangoはAPIサーバーとして開発を進める場合が多いかと思います。 このフロントエンドからAPIサーバーへの通信ですがクロスドメイン/オリジン通信になる場合、 クライアント・サーバー両方にCORSの対応をしなければなりません。 NuxtならSSRモードのasyncData内での通信や@nuxtjs/proxyを使用して対応している場合も多いかと思います。

ここでは上記とは異なる方法、Django・Nuxt間を同一オリジンで通信する形を考えていきたいと思います。

開発環境

開発環境として下記のもので動作確認していきます。

CentOS: 7.8Python: 3.8.3Node.js: 14.4.0Django: 3.0.6Nuxt: 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は必須)

画像1

上記コマンドで作ったプロジェクトは以下のディレクトリ構成になります。

djanuxt
  │
  ├ djanuxtnuxtmanage.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のページが開きます。

画像2

http://localhost:3000/nuxtでNuxtのページが表示されます。

画像3

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]ボタンを押して...

画像4

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]のリンクボタンを押します。

画像5

app.vueのページが開きます。

画像6

AxiosのGET, POST, PUT, DELETEメソッドでDjango APIに通信してみます。 POST, PUT, DELETEメソッドでの送信はCSRFトークンが必要なのでHTTPヘッダーにセットして送信するようにしています。

通信が成功すると、レスポンスの値が表示されます。

画像7

静的ビルドの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はアクセスできない状態での確認)

画像8

一つのWebサーバー上で動くDjangoとNuxtの同一オリジン連携が出来ました。

画像9

【おまけ】 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">に修正

画像10

これは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の連携を作るに当たって多くの情報を参考にさせて頂きました。 情報を提供してくださった方々に心より感謝いたします。

この記事が気に入ったらサポートをしてみませんか?