DjangoでFetch APIを使用する方法
Fetch API を使用することで、画面を再描画する必要なく、画面を更新することができます。
アマゾンや Youtube を思い浮かべてみてください。
商品を検索するとメインのコンテンツの表示は変わりますが、サイドメニューなどはそのままだと思います。
これは画面すべてを再描画しているのではなく、一部の部分だけ再描画をしています。
この方が画面を描画するスピードは確実に早くなります。
Fetch API を使用して、画面の一部だけ更新する方法を学習しましょう。
GitHub 準備
GitHub のリポジトリを作成します。
.gitignore 作成
.gitignore ファイルを作成してください。
記述されたファイルは、git 管理下から除外されてコミットされなくなります。
.gitignore
myvenv
db.sqlite3
.vscode
__pycache__
*.pyc
.DS_Store
仮想環境の作成
myvenv
という名前で仮想環境を構築します。
$ python3 -m venv myvenv
仮想環境の実行
source
コマンドで仮想環境を実行します。
ターミナルを再起動したときなど、必ずこのコマンドを実行して、仮想環境に入って下さい。
仮想環境に入ると、ターミナルに(myvenv)
という印が付きます。
これがあると、仮想環境に入っていることになります。
$ source myvenv/bin/activate
requirements.txt 作成
requirements.txt ファイルを作成してください。
開発で必要なパッケージを記載します。
requirements.txt
Django~=3.1.4
django-widget-tweaks~=1.4.8
パッケージのインストール
このコマンドで、requirements.txt
に記載されたパッケージがインストールされます。
(myvenv) ~$ pip3 install -r requirements.txt
これで、Django で開発する準備ができました。
プロジェクト作成
プロジェクトを作成します。
(myvenv) ~$ django-admin startproject mysite .
環境設定変更
settings.py を修正してプロジェクトの設定を変更します。
mysite/settings.py
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
データベースのセットアップ
migrate コマンドをすることでデータベースがセットアップされます。
(myvenv) ~$ python3 manage.py migrate
Web サーバーを起動する
Django が起動できるか確認しましょう。
(myvenv) ~$ python3 manage.py runserver
アプリケーション作成
アプリケーションを作成します。
今回は、アプリケーションの名前をapp
とします。
(myvenv) ~$ python3 manage.py startapp app
アプリケーションを使えるように設定
アプリケーションを使えるようにするには、プロジェクト設定にアプリケーションを追加する必要があります。
widget_tweaks
パッケージも同時に設定します。
mysite/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'widget_tweaks', # 追加
'app', # 追加
]
モデル
ブログモデルを作成します。
app/models.py
from django.db import models
class Blog(models.Model):
title = models.CharField('タイトル', max_length=255)
def __str__(self):
return self.title
Admin
管理画面でデータを登録できるようにします。
app/admin.py
from django.contrib import admin
from .models import Blog
admin.site.register(Blog)
マイグレーション実行
モデルを追加したので、マイグレーションが必要になります。
(myvenv) ~$ python3 manage.py makemigrations
(myvenv) ~$ python3 manage.py migrate
プロジェクト URL
プロジェクト URL に app アプリケーションを指定します。
mysite/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('app.urls')),
]
アプリケーション URL
トップページと追加、検索の URL を追加します。
app/urls.py
from django.urls import path
from app import views
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('add/', views.AddView.as_view(), name='add'),
path('search/', views.SearchView.as_view(), name='search'),
]
ビュー
ビューを作成します。
app/views.py
from django.views.generic import View
from django.shortcuts import render
from .models import Blog
from django.http import JsonResponse
class IndexView(View):
def get(self, request, *args, **kwargs):
blog_data = Blog.objects.all()
return render(request, 'app/index.html', {
'blog_data': blog_data,
})
class AddView(View):
def post(self, request, *args, **kwargs):
title = request.POST.get('title')
blog = Blog()
blog.title = title
blog.save()
data = {
'title': title,
}
return JsonResponse(data)
class SearchView(View):
def post(self, request, *args, **kwargs):
title = request.POST.get('title')
blog_data = Blog.objects.all()
title_list = []
if title:
blog_data = blog_data.filter(title__icontains=title)
for blog in blog_data:
title_list.append(blog.title)
data = {
'title_list': title_list,
}
return JsonResponse(data)
コード解説
request.POST でテンプレートから送信されたタイトルを取得します。
title = request.POST.get('title')
送信ボタンが押されたら、タイトルを取得してデータベースに保存します。
blog = Blog()
blog.title = title
blog.save()
戻り値は JsonResponse を使用して json 形式で返します。
return JsonResponse(data)
取得したタイトルで、フィルターをかけます。
icontains は、中間一致(大文字小文字区別無し)でレコードを取得します。
if title:
blog_data = blog_data.filter(title__icontains=title)
テンプレート
base
いつも通りベースを作成します。
app/templates/app/base.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
/>
<link rel="stylesheet" href="{% static 'css/style.css' %}" />
<title>FetchAPIチュートリアル</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">FetchAPI</a>
</div>
</nav>
<main>
<div class="container">{% block content %} {% endblock %}</div>
</main>
<footer class="py-2 bg-dark">
<p class="m-0 text-center text-white">Copyright © フルスタックチャンネル</p>
</footer>
{% block extrajs %} {% endblock %}
</body>
</html>
index
このチュートリアルのメインです。
トップページを作成します。
app/templates/app/index.html
{% extends "app/base.html" %} {% block content %}
<div class="my-4 text-center">
<div class="row">
<div class="col-md-6 mb-4">
<h2>記事の追加</h2>
<form id="add_blog">
{% csrf_token %}
<input class="form-control" type="text" id="post_title" required />
<button class="btn btn-primary mt-2" type="submit">送信</button>
</form>
</div>
<div class="col-md-6 mb-4">
<h2>記事の検索</h2>
<form id="search_blog">
{% csrf_token %}
<input class="form-control" type="text" id="search_title" />
<button class="btn btn-warning mt-2" type="submit">検索</button>
</form>
</div>
</div>
</div>
<hr />
<div class="my-5 text-center">
<h2 class="mb-2">記事一覧</h2>
<div class="row" id="posts">
{% for blog in blog_data %}
<div class="col-4 mb-3">
<div class="card">
<div class="card-body">{{ blog.title }}</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %} {% block extrajs %}
<script>
// https://developer.mozilla.org/ja/docs/Learn/JavaScript/Client-side_web_APIs/Fetching_data
// CSRF対策
const getCookie = (name) => {
if (document.cookie && document.cookie !== '') {
for (const cookie of document.cookie.split(';')) {
const [key, value] = cookie.trim().split('=')
if (key === name) {
return decodeURIComponent(value)
}
}
}
}
const csrftoken = getCookie('csrftoken')
// 記事追加
const addBlog = document.getElementById('add_blog')
addBlog.addEventListener('submit', (e) => {
e.preventDefault()
const url = '{% url "add" %}'
const post_title = document.getElementById('post_title')
// URLのクエリパラメータを管理
const body = new URLSearchParams()
body.append('title', post_title.value)
fetch(url, {
method: 'POST',
body: body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'X-CSRFToken': csrftoken,
},
})
.then((response) => {
// JSON形式に変換
return response.json()
})
.then((response) => {
// フォームをクリア
post_title.value = ''
// 追加するエレメント
const postArea = document.getElementById('posts')
const element = Object.assign(document.createElement('div'), { className: 'col-4 mb-3' })
const element2 = Object.assign(document.createElement('div'), { className: 'card' })
const element3 = Object.assign(document.createElement('div'), {
className: 'card-body',
textContent: response.title,
})
element.appendChild(element2)
element2.appendChild(element3)
// 最後に追加
postArea.insertBefore(element, postArea.lastChild.nextSibling)
})
.catch((error) => {
console.log(error)
})
})
// 記事検索
const searchBlog = document.getElementById('search_blog')
searchBlog.addEventListener('submit', (e) => {
e.preventDefault()
const url = '{% url "search" %}'
const search_title = document.getElementById('search_title')
// URLのクエリパラメータを管理
const body = new URLSearchParams()
body.append('title', search_title.value)
fetch(url, {
method: 'POST',
body: body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'X-CSRFToken': csrftoken,
},
})
.then((response) => {
// JSON形式に変換
return response.json()
})
.then((response) => {
// フォームをクリア
search_title.value = ''
// 検索するエレメント
const postArea = document.getElementById('posts')
postArea.innerHTML = ''
for (const title of response.title_list) {
const element = Object.assign(document.createElement('div'), { className: 'col-4 mb-3' })
const element2 = Object.assign(document.createElement('div'), { className: 'card' })
const element3 = Object.assign(document.createElement('div'), {
className: 'card-body',
textContent: title,
})
element.appendChild(element2)
element2.appendChild(element3)
postArea.appendChild(element)
}
})
.catch((error) => {
console.log(error)
})
})
</script>
{% endblock %}
コード解説
FetchAPI を使用するときは、CSRF に関する処理が必要になります。
const getCookie = (name) => {
if (document.cookie && document.cookie !== '') {
for (const cookie of document.cookie.split(';')) {
const [key, value] = cookie.trim().split('=')
if (key === name) {
return decodeURIComponent(value)
}
}
}
}
const csrftoken = getCookie('csrftoken')
URLSearchParams を使用して URL のクエリパラメータを設定します。
appned でビューで処理したいデータを追加します。
const body = new URLSearchParams()
body.append('title', search_title.value)
FetchAPI を使用します。
body にビューに渡すデータを追加します。
headers は CSRF 対策で必要になります。
fetch(url, {
method: 'POST',
body: body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'X-CSRFToken': csrftoken,
}
ビューからのデータを json 形式に変換しています。
}).then(response => {
return response.json();
非同期でデータを追加します。
insertBefore は指定した要素を現在の要素の子要素として対象要素の前に挿入します。
postArea.lastChild.nextSibling を指定することによって、最後にデータを追加することができます。
}).then(response => {
// フォームをクリア
post_title.value = '';
// 追加するエレメント
const postArea = document.getElementById('posts');
const element = Object.assign(document.createElement('div'), {className: 'col-4 mb-3'});
const element2 = Object.assign(document.createElement('div'), {className: 'card'});
const element3 = Object.assign(document.createElement('div'), {className: 'card-body', textContent: response.title});
element.appendChild(element2);
element2.appendChild(element3);
// 最後に追加
postArea.insertBefore(element, postArea.lastChild.nextSibling);
もしエラーがあればエラーを出力します。
}).catch(error => {
console.log(error);
});
CSS
app/static/css/style.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #f1f1f1;
display: flex;
flex-flow: column;
min-height: 100vh;
}
main {
flex: 1;
}
確認
このように、投稿、検索しても URL のロードなしに画面の表示が変わります。
ぜひ試してみてください。
Discussion