見出し画像

FetchAPIチュートリアル

自己紹介

プログラミング講師の飼鳥晴康(@hathle)です。

エンジニア歴は10年以上で海外(カナダ)就労も経験しています。

カナダでは、映画を作っていました。

現在は、独立して、Python、Djangoをメインにオンラインでマンツーマンレッスンをしています。

お問い合わせなどは、Twitter(@hathle)からお願いいたします。

プログラミングは人生を変えることができます。

目標に向かって、一緒に頑張っていきましょう!!

はじめに

Fetch APIを使用することで、画面を再描画する必要なく、画面を更新することができます。

アマゾンやYoutubeを思い浮かべてみてください。

商品を検索するとメインのコンテンツの表示は変わりますが、サイドメニューなどはそのままだと思います。

これは画面すべてを再描画しているのではなく、一部の部分だけ再描画をしています。

この方が画面を描画するスピードは確実に早くなります。

Fetch APIを使用して、画面の一部だけ更新する方法を学習しましょう。

目標

下記を学習していきます。

・FetchAPIの使用方法

機能

・データを追加
・データを検索

前提知識

Djangoを始める前に、下記の言語は学習しておいて下さい。

・Python
・HTML
・CSS
・JavaScript

ブログ構成チュートリアルでDjangoを詳しく説明していますので、まだ実施していない方は、実施して下さい。

では、始めて行きましょう!!

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~=2.2.10
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; Django Startup 2020</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のロードなしに画面の表示が変わることを確認して下さい。

画像2

おわりに

チュートリアルを最後まで読んでいただき、誠にありがとうございました。

FetchAPIチュートリアルはここまでで終わりとなります。

Webアプリケーションを開発していて、非同期処理はよく使います。

FetchAPIはとても便利ですので、ぜひマスターして下さい。

Djangoは奥の深いフレームワークで、多くの機能を搭載することができます。

公式ドキュメントを参考にして、さらに理解を深めていきましょう。

エラーが発生した場合

もしエラーが発生したり、分かりにくい箇所は下記のGitHubにてソースコードを参考にして下さい。

これまでの知識で基本的なWebアプリケーションを開発できます。

ぜひオリジナルのアプリケーションを開発してみてください。

フィードバック

チュートリアルのフィードバックは、Twitter(@hathle)までお願いいたします。

どんどんDjangoチュートリアルを作成していきますので、楽しみにしていてください。

ではまた!!


最後まで読んでいただきありがとうございました😃 サポートは、プログラミングチュートリアル開発の手助けとなります。 シェアもして頂くと嬉しいです。 Twitterのフォローもお願いします。 https://twitter.com/hathle