🐣

DjangoでFetch APIを使用する方法

13 min read

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 &copy; フルスタックチャンネル</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 のロードなしに画面の表示が変わります。

ぜひ試してみてください。

Fetch

Discussion

ログインするとコメントできます