知識の枝

"All is well"

DjangoでAjaxを使い非同期通信を行う

約132日前 2021年6月18日15:57
デジタル
Django JavaScript

改訂履歴


2021/6/18 投稿
2021/7/9 改訂1「5.3 - POSTメソッドを使う場合」を追記

1. 背景


Djangoを使った開発中に覚えたことを備忘録として残します。

今回はAjaxを使用して非同期通信を行う方法を解説します。


2. 目的


Ajaxを理解し使えるようになる。


3. 非同期通信とは?


非同期通信を語る前にまずは同期通信とは何かを知っておく必要があります。

同期通信とはまさにこのブログで使用されている通信方式のことです。

ブログの記事一覧ページで記事を1つ選んでクリックすると、個別記事のページに移動します。

ユーザーが「クリック」した瞬間に「ページの読み込み&表示」が連続して起こります。

リクエストとレスポンスがほぼ同じタイミングとなる通信方式なので「同期通信」と呼ばれます。



では非同期通信とは何か?

同期通信の反対だから

「リクエストとレスポンスのタイミングがズレてるだけでしょ」

と思うかもしれませんが、少々違います。

リクエストとレスポンスのタイミングは同期通信と同じようにほぼ同時です。

では何が異なるのでしょう?

それは「ページの読み込みの有無」です。

ページを移動する際やページの内容を読み込み直す際、一瞬画面が消えてから内容が画面に表示されますよね。

非同期通信ではこの「読み込み直し」の部分が無くなります。

ページを全部読み込み直すことはせず、「一部分のみ読み込んで」更新します。



一部分の更新だと何が嬉しいのでしょうか?

私が以前作った「プロジェクト管理アプリ」を例に挙げます。
(ページ上部「Apps/ProjectManager」で公開中、編集自由です)


このアプリは画面上のボタンからタスクの追加や削除が行えます。

試しにタスクを1つ削除してみます。




削除ボタンを押してからページが再表示されるまでの待ち時間が長いですね。

タスクが1個消された際にページを読み込み直し、タスクが無くなった画面を再表示させている為このように処理が長くなります。

使用上は問題無いのですが、利用者の立場で考えると「タスク管理」のように頻繁に内容の更新が発生するアプリで更新処理が長くなるのは避けたいところです。


この更新処理が非同期通信になるとページ全体の読み込みが無くなる為、処理が短くなり使い勝手が向上します。

非同期通信化の嬉しさは「使い勝手の向上」です。




4. Ajaxとは?


Ajax(エイジャックス)は「Asynchronous JavaScript and XML」の略です。


Asynchronous(エイスィンクロナス):2つ以上の物事が同時に起こらない、非同期のという意味です。

反対語は Synchronous(スィンクロナス):2つ以上の物事が同時に起こる、同期する

シンクロナイズドスイミングのような同期するというイメージが強いものの「反対」と考えれば覚えやすいですね。



JavaScript(ジャバスクリプト)はWeb系で盛んに使われるプログラミング言語です。
詳細はウィキペディアで。
ウィキペディア



XML(エックスエムエル)は「Extensible Markup Language」の略です。

Extensible(エクステンシブル):広げられる、伸ばせる、拡張(可能)といった意味です。

Markup Language(マークアップ ランゲージ)は、マークアップ言語と呼ばれる「文章をコンピュータにとって分かり易い形に構造化した言語」のことです。

ウェブページを書く際に使用するHTMLもマークアップ言語です。



どのあたりが拡張可能なのでしょうか?

HTMLと比較してみましょう。

HTMLでは<>で囲まれたタグと呼ばれるものがありますよね。

例えばリンクを表す<a>タグや、見出しを表す<h>タグ等です。

これらのタグは予めHMTLで定められたものであり、用意されているものしか使用することができません。

対してXMLはタグを新しく作成することができ、タグ自体に情報を付与することができます



Ajaxは「非同期通信のJavaScriptと拡張可能なマークアップ言語」の略と言えます。




5. DjangoでAjaxを使いたい


5.1 - どんな準備が必要?


何かをインストールしたりsettings.pyを編集する必要はありません。
<script>を読み込む程度なので導入自体は比較的お手軽です。


5.2 - GETメソッドを使う場合


用途によりますが、今回はGETメソッドを使ってページの表示内容を非同期で切り替える方法を解説します。

想定している使い方は下記の通りです。


料理のレシピを表示するページがあったとします。

デフォルトでは1人前の材料しか表示されません。

何人前作りたいかをドロップダウンメニューで選択すると、選択した人数分の材料が即座に表示されるようにします。


完成イメージ





まず表示しているhtmlの中身です。
本文を構成するのが<main>タグ。Ajaxが<script>部分です。
recipe.html
<main>
<div class="container">
<div class="row">
<p>必要な材料</p>
<form id="ajax_ch_servings" action="{% url 'cooking:ajax_ch_servings' recipe.pk %}" method="GET">
<select name="servings" id="id_servings" onclick='document.getElementById("ch_servings_submit").click();'>
{% for serve in for_range %}
<option value="{{ forloop.counter }}">{{ forloop.counter }}</option>
{% endfor %}
</select>人前
<input type="submit" value="送信" id="ch_servings_submit" style="display:none;">
</form>
<hr>
<div id="id_ingredient" style="margin-left: 50px;">
{% for ingredient in recipe.related_ingredient.all %}
<p>{{ingredient.name}}{{ingredient.amount}}{{ingredient.name.unit}}</p>
{% endfor %}
</div>
</div>
</div>
</main>


<script>
$('#ajax_ch_servings').on('submit', e => {
e.preventDefault();

$.ajax({
'url': '{% url "cooking:ajax_ch_servings" recipe.pk %}',
'type': 'GET',
'data': {
'servings': $('#id_servings').val(),
},
'dataType': 'json'
}).done(response => {
$('#id_ingredient').empty();

for (const ingredient of response.ingredient_list) {
const p = $('<p>', { text: ingredient });
$('#id_ingredient').append(p);
}
});
});
</script>

ここで使っているモデルは下記の通りです。

料理を表す「Menu」モデル

材料を表す「Ingredient」モデル

それらに関連する「食材モデル」「単位」モデル等があります。


models.py
class Menu(models.Model):
name = models.CharField(verbose_name='料理の名前', max_length=20)

def __str__(self):
return str(self.name)


class Ingredient(models.Model):
target_menu = models.ForeignKey(Menu,
verbose_name='対象の料理',
on_delete=models.CASCADE,
related_name='related_ingredient')
name = models.ForeignKey(Material,
verbose_name='品名',
on_delete=models.CASCADE)
amount = models.DecimalField(verbose_name='必要量',
max_digits=6,
decimal_places=1)

def __str__(self):
return str(self.target_menu) + " - " + str(self.name)
長くなっちゃうので最低限の部分だけ載せています。


続いてURLのルーティングです。

実際にはページ遷移が発生しませんがviews.py内で専用の処理を行う為、そのviewを呼び出すルーティングを行います。
urls.py
app_name = 'cooking'

urlpatterns = [
path('<pk:int>/recipe/', views.RecipeView, name='recipe'),
path('<pk:int>/recipe/ajax_ch_servings', views.Ajax_ch_servings, name='ajax_ch_servings'),
]
レシピを表示しているのが「RecipeView」、レシピページでAjaxを使うのが「Ajax_ch_servings」です。


views.pyの中身を見ましょう。
views.py
from django.http import JsonResponse

def RecipeView(request, pk):
"""レシピページ"""
"""基本情報"""
template = "cooking/recipe.html"
recipe = get_object_or_404(Menu, pk=pk)
for_range = [i for i in range(10)]


context = {
'recipe': recipe,
'for_range': for_range,
}

return render(request, template, context)


def Ajax_ch_servings(request, pk):
"""Ajax処理"""
"""必要材料の数を入力した人数に応じて変更する"""
num = request.GET.get('servings')
menu = get_object_or_404(Menu, pk=pk)

ingredient_list = [
ingredient.name.name + str(float(ingredient.amount) * float(num)) + ingredient.name.unit.name
for ingredient in menu.related_ingredient.all()
]

dict = {
'ingredient_list': ingredient_list,
}

return JsonResponse(dict)
必要なものは以上です。


流れを説明します。

まずユーザーがレシピページにアクセスすると「RecipeView」が実行されます。
path('<pk:int>/recipe/', views.RecipeView, name='recipe'),


recipe.htmlの内容が表示され、まずは1人分の材料が表示されます。
<div id="id_ingredient" style="margin-left: 50px;">
{% for ingredient in recipe.related_ingredient.all %}
<p>{{ingredient.name}}{{ingredient.amount}}{{ingredient.name.unit}}</p>
{% endfor %}
</div>


ドロップダウンで人数を選択すると自動的にsubmitボタンが押されます
submitボタンは「style="display:none;"」で見えなくしています。
<form id="ajax_ch_servings" action="{% url 'cooking:ajax_ch_servings' recipe.pk %}" method="GET">
<select name="servings" id="id_servings" onchange='document.getElementById("ch_servings_submit").click();'>
{% for serve in for_range %}
<option value="{{ forloop.counter }}">{{ forloop.counter }}</option>
{% endfor %}
</select>人前
<input type="submit" value="送信" id="ch_servings_submit" style="display:none;">
</form>


submitイベントをJavascriptでキャッチします。

そしてまずはそのイベントを中止し、submitによる画面遷移を止めます。
e.preventDefault();

イベントを止めないとurls.pyで定義したURLに移動してしまいます。

「e.preventDefault();」を無くした例
ページが切り替わっちゃいます。




続いてAjax処理に移ります。
$.ajax({
'url': '{% url "cooking:ajax_ch_servings" recipe.pk %}',
'type': 'GET',
'data': {
'servings': $('#id_servings').val(),
},
'dataType': 'json'
})
「url」には処理を行うパスを書きます。

urls.pyで定義したパスですね。
path('<pk:int>/recipe/ajax_ch_servings', views.Ajax_ch_servings, name='ajax_ch_servings'),


今回は「GETメソッド」を使っている為、「type」には「GET」と入力します。

「data」にはviews.pyに渡す変数を定義します。

今回は「プルダウンで選択した人数」をviews.pyに渡したいので、「servings」という変数に「id="id_servings"」が割り当てられたタグの値を格納します。
<select name="servings" id="id_servings" onclick='document.getElementById("ch_servings_submit").click();'>
セレクトタグにこのidが付いています。

最後に「dataType」には「json」と記載します。

Ajaxなのになぜ「xml」じゃないんだ と思うかもしれませんね。

昔はxmlを使っていたようですが、最近はデータをより軽量に扱うことが出来るjsonを使うようです。


この後、views.pyでAjaxを通して受け取ったデータを処理します。

まずデータを受け取ります。
num = request.GET.get('servings')
この「servings」には「プルダウンで選択した人数」が入っています。


料理の「材料の量」を変更する為、対象の料理モデルインスタンスを呼び出しておきます。
menu = get_object_or_404(Menu, pk=pk)


1人前の「材料の量」に先ほどの「プルダウンで選択した人数」を掛け算し、結果をリストに保存します。
ingredient_list = [
ingredient.name.name + str(float(ingredient.amount) * float(num)) + ingredient.name.unit.name
for ingredient in menu.related_ingredient.all()
]
リスト内包表記で書いています。


作成したリストを辞書形式でhtmlテンプレートに返します。
dict = {
'ingredient_list': ingredient_list,
}

return JsonResponse(dict)
この書き方はviews.pyのcontextと同じですね。


htmlにviews.pyからレスポンスが届きました。

届いたリストをfor文で取り出しながら<p>タグで囲っていきます。
.done(response => {
$('#id_ingredient').empty();

for (const ingredient of response.ingredient_list) {
const p = $('<p>', { text: ingredient });
$('#id_ingredient').append(p);
}
});
まず最初に「id="id_ingredient"」となっているタグの中身を空にします。
$('#id_ingredient').empty();


これをやらないと表示内容がリフレッシュされず、データが溜まってしまいます。

「$('#id_ingredient').empty();」を無くした例



リフレッシュした後、更新内容を表示することで冒頭に載せた完成イメージのように動きます。



GETメソッドを使った例は以上です。



5.3 - POSTメソッドを使う場合


開発の中で使用する機会があれば、その内容をここに反映させます。


改訂1
POSTを使った例を別記事で挙げました。

Django&Ajax チェックボックスを扱う




6. さいごに


Ajaxを用いた非同期通信が使えるようになると、アプリの使い勝手が格段に向上すると思います。

みなさんのアイデアをぜひアプリに反映してみてください。

お疲れ様でした。