【Django入門21】JavaScriptの扱い方

Djangoで開発するウェブアプリでのJavaScriptの扱い方の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Django で開発するウェブアプリでの「JavaScript の扱い方」について解説していきます。

これまで20回にわたって Django 入門 の連載を続けてきましたが、これらの連載の中での解説内容で実現できるのは「静的なページ」のみとなります。動作のないページしか実現できなくてガッカリされていた方もおられるかもしれません。

ですが、今回紹介する JavaScript をウェブアプリで扱うことで「動的なページ」を実現することができるようになります。具体的には、JavaScript によって、ページ内のデータの動的更新・複雑なアニメーション・新たな操作方法の提供等を実現することが可能となります。

つまり、JavaScript を利用することで、見た目・使いやすさ・ユーザー体験の優れたウェブアプリを開発することができるようになります。また、ゲームアプリなども開発できるようになります。より魅力的なウェブアプリを開発するためには JavaScript の利用が必須となりますので、是非このページで Django で開発するウェブアプリでの JavaScript の扱い方について学んでいただければと思います!

MEMO

このページでは、Django で開発するウェブアプリでの「JavaScript の扱い方」に焦点を当てて解説していきます

JavaScript についての説明や、簡単な JavaScript のコード例の紹介は行いますが、JavaScript でのプログラム開発に焦点を当てた解説は行わないため、詳細な JavaScript でのコードの書き方等については別途別のサイトや参考書等で学んでいただければと思います

JavaScript とは

JavaScript のことをご存知ない方もおられるかも知れませんので、まずは簡単に JavaScript について簡単に説明しておきます。

JavaScript

JavaScript とはプログラミング言語の1つです。JavaScript の最大の特徴はウェブブラウザ上で動作するという点になります。簡単に言えば、ウェブブラウザ上で動作するスクリプト(プログラム)を開発するための言語で、このスクリプトを動作させることで、ページに表示される要素を動的に変化させたり、定期的に or クリック等のイベントが発生したタイミングで特定の処理を実行したりすることが可能となります。

HTML・CSS・JavaScript

ウェブページは、HTML と CSS、さらには JavaScript の3種類の言語から構成されていることが多く、HTML や CSS と同様に、JavaScript はウェブ開発にとって非常に重要な役割を持つ言語となります。ここで、これらの言語の関連性について整理しておきます。

まず HTML は「ページの構造」や「ページに表示する要素」を作成するためのマークアップ言語となります。例えば「見出し」「箇条書き」「段落」等に対応するタグを HTML ファイルに記述しておくことで、それらの要素がページに描画されることになります。

HTMLの説明図

また CSS は「ページや要素の見た目」を制御するためのスタイルシート言語となります。例えば「見出し」要素に対して「文字のサイズ・文字の色・背景色」等のスタイルを CSS ファイルに定義しておくことで、そのスタイルに応じた見た目の見出しがページに表示されるようになります。

CSSの説明図

そして、JavaScript は「ページに動的な動作」を追加するためのプログラミング言語となります。JavaScript ファイルをページ表示時にウェブブラウザから読み込ませれば、そのファイルに記述した処理をウェブブラウザが実行してくれるようになります。つまり、ページ表示時にプログラムが実行されるようになります。そして、それによって「ユーザーの操作に応じてコンテンツを変化させる」・「定期的に要素を変化させる」「外部と通信を行う」等の動的な処理を実行するページを実現することができます。

JavaScriptの説明図

HTML や CSS だけでも一部の動的な処理、例えばフォームを作成してボタンのクリックを受け付けるようなことは実現可能ですが、これらだけで実現できる動的な処理は限られているため、基本的には動的なページを開発するためには JavaScript が必須となります。そして、動的なページを開発することで、より魅力的でユーザー体験の優れたページを実現することができます。

また、JavaScript では HTML でタグとして定義される要素を更新・追加・削除したり、各要素に適用するスタイルを変化させることで動的なページを実現することも多いです。そのため、JavaScript を扱うのであれば HTML や CSS に関しても多少の理解が必要となります。

Django での動的なページ生成との違い

ここまでの説明を聞いて、「Django で開発したウェブアプリでも動的なページ生成が行われるんじゃないの?」と疑問に思った方もおられるかもしれません。確かに Django で開発したウェブアプリでは、テンプレートの仕組みを利用してデータベースのレコード等に応じたページを動的に生成することができます。ですが、ここで生成されるのは、あくまでも静的なページとなります。つまり、動的に「静的なページ」が生成されるだけです。なので、一度ウェブブラウザに表示された後は、ページの一部が自動的に更新されたりする等、動的にページが変化することはありません。

ウェブアプリが生成するページが静的であることを示す図

それに対し、JavaScript をウェブアプリから扱うようにすることで、ウェブブラウザに表示された後に、動的にページを変化させるようなことが可能となります。つまり、ページ自体が動的なものになります。

JavaScriptにより、ページ自体が動的なものになることを示す図

こういった違いがあるので、これらの動的の意味合いについては混同しないように注意してください。そして、動的の意味合いが異なるため、Django で開発したウェブアプリで JavaScript を扱うようにすることで、今まで実現できなかった新たな機能や UI を実現することが可能となります。

スポンサーリンク

Django ではフロントエンド開発に利用

また、元々この JavaScript は、主にウェブブラウザ上に表示されるページに動的な処理を追加するために利用されるプログラミング言語でした。つまり、ウェブアプリ開発においては、主にフロントエンドの開発で利用されるプログラミング言語でした。ですが、最近ではバックエンドの開発でも JavaScript が利用されるようになってきています。

JavaScriptがバックエンド開発にも利用可能であることを示す図

ただ、この Django 入門 は、タイトル通り Django でのウェブアプリ開発の解説を行う記事となります。そして、Django でウェブアプリを開発する場合は、少なくともバックエンドは全て Django で開発することになります。このページでも、ウェブアプリは基本的には Django で開発し、フロントエンドの一部、つまりページに動的な処理を追加することを目的に JavaScript を利用することを前提として説明していきますので、この点はご了承ください。

このページではバックエンドをDjangoで開発すること前提に解説することを説明する図

JavaScript フレームワーク

皆さんが現在学習されている Django も Python のフレームワークとなります。Python には、他にも Flask などといったフレームワークが存在します。

それと同様に、JavaScript にもフレームワークが存在しています。例えば下記のようなフレームワークについては、皆さんも耳にしたことがあるのではないでしょうか?

  • React
  • Vue.js
  • Angular

これらのフレームワークを利用することで、Django を利用する時と同様に、JavaScript でのウェブアプリ開発(フロントエンド開発)の効率化・品質向上を図ることが可能です。

このページでは、まずは Django で開発するウェブアプリでの JavaScript の扱い方や、JavaScript を扱うことで実現できるようになることを中心に解説を行うため、上記で紹介したようなフレームワークは利用しません。ですが、フレームワークを利用することで JavaScript の開発も効率化できるという点は是非覚えておいてください。

ウェブアプリで JavaScript を扱うメリット

続いて、Django で開発するウェブアプリで JavaScript を扱うメリットについて説明していきます。

スポンサーリンク

リッチな UI が実現可能

ウェブアプリは、基本的にはウェブブラウザから操作が行われることを前提に開発され、ウェブアプリの UI はウェブブラウザにページとして表示されることになります。JavaScript はウェブブラウザ上で動作するプログラムですので、JavaScript を扱うようにすることで、ウェブアプリの UI に動的な動作を追加することができるようになります。

そして、それにより、ウェブアプリでリッチな UI を簡単に実現できるようになります。

例えば、JavaScript を利用することで、ページ内の要素に対してフェードイン・フェードアウトやスライドイン・スライドアウト等のアニメーション効果を加えたりすることもできます。

JavaScriptで要素をフェードイン・フェードアウトさせる様子

また、マウスのドラッグ&ドロップ操作によって、ページ上に表示される要素の移動などを実現することもできます。

JavaScriptで要素をドラッグ&ドロップする様子

このように、JavaScript を利用することで、視覚的にリッチな UI が実現可能となります。簡単なアニメーションであれば CSS だけでも実現できますが、複雑なアニメーションを実現したい場合は JavaScript の利用が必要となります。

使いやすい UI が実現可能

また、JavaScript を利用することで、直感的で使いやすい UI を実現することもできます。

例えば、フォームのフィールドへの入力値が変化するたびにリアルタイムで値の妥当性の検証を実施し、エラーがあればエラーメッセージを表示したり、さらに入力を補完するようなことが JavaScript の利用で実現できます。これにより、ユーザーは入力値の妥当性を確認しながらフィールドへの入力を実施することができるようになり、使いやすさが向上します。

JavaScriptで入力値のリアルタイムチェックを実現する様子

さらに、JavaScript を利用することで、独自の操作方法を追加することも可能です。例えば、キーボードでの上下左右キーの入力を受け付け、入力されたキーに応じてページ内の要素を移動させるようなこともできます。また、前述でも紹介したドラッグ&ドロップ操作を受け付けるようにすることもできます。このように、JavaScript を利用することで、ユーザーに独自の操作方法を提供し、より使いやすい UI を実現することができます。

キーボードのキー入力でページ上の要素の操作を行う様子

動的なデータ更新が実現可能

JavaScript を利用することで、ページの更新操作なしでの動的なデータ更新も実現できるようなります。

例えば、データベースのテーブルのレコード数は、各ユーザーからの新規登録操作や削除操作に応じて変化することになります。ですが、JavaScript を利用しない場合、ページに表示されるレコード数は、手動でページの更新操作を行わないと更新されることはありません。それに対し、JavaScript を利用する場合は、テーブルのレコード数に応じて、ページに表示されるレコード数を動的に変化させることが可能となります。つまり、ページの更新操作なしでページ内のデータが動的に更新されるようになります。

この例であれば、JavaScript からウェブアプリ(バックエンド側)にレコード数を定期的に問い合わせ、その問い合わせ結果に応じてページに表示されるレコード数を変化させるようにすれば、動的な更新が実現できることになります。

動的なデータの更新をJavaScriptで実現する様子

また、JavaScript を利用することで無限スクロールを実現することもできます。無限スクロールとは、レコードの一覧表示時に、スクロールに応じて追加のデータが自動で読み込まれて表示されるようなスクロール技術のことを言います。この無限スクロールを採用することで、ユーザーがページを遷移・リロードすることなく、データを続けて表示することが可能になります。

JavaScriptで無限スクロールを実現する様子

ただし、ウェブブラウザ上で動作する JavaScript からは直接データベース操作を行ってレコードを取得するようなことはできません。データベース操作を行うのは、あくまでもウェブアプリ(バックエンド)となります。そのため、上記のような動的更新を実現するためには「JavaScript からレコードを取得する手段」が必要となります。そして、この手段として採用されることが多いのが、前々回及び前回の連載で解説した Web API となります。

例えばレコード一覧取得 API がウェブアプリ(バックエンド)で公開されていれば、それを利用して間接的にレコードを取得することができるようになり、取得した結果を利用した処理を Web API 側で実現することができるようになります。もちろん、上記で示したデータの動的更新や無限スクロールも実現できるようになります。

Web APIを実行して動的更新を実現する様子

このように、JavaScript からデータベース操作を行うためには Web API が必要となり、ウェブアプリで JavaScript を扱う場合は Web API がより重要になります。

スポンサーリンク

動的な要素の追加・削除も可能

また、JavaScript を利用することで、先ほど説明したような要素の更新だけでなく、ページに表示する要素を追加したり削除したりすることも可能となります。なので、ユーザーの操作に応じて画像を追加して表示したり、表示されている段落を削除するようなことも可能です。

さらに、JavaScript でページそのものを生成するようなことも可能です。HTML にはヘッダーや JavaScript を実行するためのタグのみを記述し、あとは JavaScript でページ内の全要素を追加するような処理を実行するようにすれば、ページそのものが JavaScript から生成されることになります。

ページそのものをJavaScriptから生成する様子

フロントエンド開発とバックエンド開発の分離

JavaScript でページそのものを生成することも可能ですので、JavaScript の実装方法によっては、フロントエンドを完全に JavaScript で開発することができるようになります。

この場合、Django でのフロントエンド開発、特にページ表示のためのビューやテンプレートファイルの開発が不要となり、Django はバックエンド機能(例えばデータベース操作や Web API の提供)の開発に専念できます。これにより、フロントエンド開発とバックエンド開発を分業して、並行して作業することが容易になります。また、フロントエンドの開発者は Python や Django の知識がなくても JavaScript や HTML・CSS だけで開発が可能になり、その逆も同様に、バックエンド開発者は JavaScript 等に関する知識なしで開発することが可能となります。このように、各専門領域に集中しやすい体制を整えることもできます。

つまり、JavaScript をウェブアプリで扱うようにすることは、ウェブアプリ全体の開発効率の向上にも繋がります。

JavaScriptの導入によってウェブアプリの開発効率も向上することを説明する図

で、このようにフロントエンド開発とバックエンド開発を分離させた場合に重要となるのが、 動的なデータ更新が実現可能 でも紹介した Web API となります。

今までは、データベースから取得したデータを埋め込んだページをバックエンド側で生成していたので、フロントエンド側ではデータベースの操作は不要でした。ですが、バックエンド開発とフロントエンド開発を切り離し、JavaScript でフロントエンドを開発する場合は、Web API を利用して JavaScript からのデータベース操作実施し、必要なレコードを取得して要素としてページに追加するような処理が必要となります。そして、この時に Web API が必要となります。

バックエンドにWeb APIが必要になることを説明する図

ということで、フロントエンドとバックエンドの開発を分離する意味合いでも Web API が重要となります。

今回は、ページ全体を JavaScript で生成するようなことまでは行わず、テンプレートファイルから生成されたページの一部の要素を JavaScript から操作する例を用いて解説を行っていきますが、上記の通り、フロントエンド全体を JavaScript で開発するようなことも可能であることは是非覚えておいてください。

Django での JavaScript の扱い方

ここからは、Django で開発したウェブアプリでの JavaScript の扱い方について解説していきます。

基本的には、これまでの Django 入門 で解説してきた内容を理解していれば JavaScript は難なく扱うことができると思います。ただ、忘れてしまったことも多いと思いますので、ここで JavaScript の扱い方について再度学んでいきましょう!

スポンサーリンク

静的ファイルとして配信する

まず、Django で開発したウェブアプリでは、JavaScript は静的ファイルとして扱うことになります。動的なページを実現するためのファイルではあるのですが、JavaScript のファイル自体は動的に変化するようなことはなく、いつ誰が取得しても同じファイルが得られるようになっているものなので、静的ファイルとして扱うことになります。

そして、静的ファイルをウェブアプリで扱うためには、ウェブブラウザが静的ファイルを取得できるよう、ウェブサーバーからの静的ファイルの配信が必要となります。なので、この配信のための準備が必要となります。といっても、開発用ウェブサーバーを使う場合は、特定のフォルダに静的ファイルを設置しておくことで、静的ファイルの配信が実現できます。

静的ファイルの扱い方の説明図1

この静的ファイルの扱い方に関しては下記のページで解説済みですので、静的ファイルの扱い方の詳細を知りたい方は、別途下記ページを読んでいただくことをオススメします。

Djangoでの静的ファイルの扱い方の解説ページアイキャッチ 【Django入門17】静的ファイルの扱い方(画像・JS・CSS)
MEMO

JavaScript はテンプレートファイル内に記述して扱うことも可能です

ですが、メンテナンス性等を考えると JavaScript はテンプレートファイル内ではなく、個別のファイルとして用意するのが一般的です

そのため、ここでも JavaScript は個別のファイルに開発し、それを静的ファイルとして扱うことを前提に解説を進めます

テンプレートファイルに <script> タグを追記する

また、その配信される JavaScript ファイルがウェブブラウザから実行されるようにするためには、それを指示するためのタグをテンプレートファイルに記述しておく必要があります。この JavaScript の実行を指示するタグは <script> になります。つまり、テンプレートファイルに <script> タグを記述しておく必要があります。これにより、ウェブブラウザが HTML 内に <script> タグを見つけた際に、src 属性で指定された JavaScript の取得と実行を実施してくれます。

静的ファイルの扱い方の説明図2

さらに、先ほど紹介したページでも解説している通り、<script> タグ のsrc 属性には下記のように static テンプレートタグを利用して取得するファイルのパスを指定する必要があります。また、static テンプレートタグを利用するためには、その static テンプレートタグの記述位置よりも上の位置に {% load static %} を記述しておく必要があります。

scriptタグ
{% load static %}

<script src="{% static '取得するファイルのパス' %}"></script>

<script> タグの記述位置によって JavaScript の実行タイミングが異なることになるので注意してください。<head> セクション内に <script> タグを記述することも可能なのですが、この場合、ページ内の要素が作成されないうちに JavaScript が実行される可能性があります。そのため、要素に対して操作を行うような JavaScript である場合、その操作を実施する際にエラーが発生する可能性があります。

全ての要素が作成されてから JavaScript を実行させた方が無難なことも多いので、早いタイミングで JavaScript を読み込みたい場合を除けば、<head> セクションではなく <body> セクションの最後に <script> タグを記述することをオススメします。

scriptタグの記述位置
<!DOCTYPE html>
<html>
<head>
    〜略〜
</head>
<body>
    〜略〜
    <script src="{% static 'js/example.js' %}""></script>
</body>
</html>

Web API を開発する

JavaScript を扱う場合には Web API も重要となります。

Web API は他のユーザーから利用してもらうことを目的に開発することも多いのですが、JavaScript からも Web API は実行可能で、Web API を利用することで JavaScript で実現することのできる機能の幅をさらに広げることができます。

JavaScriptからWeb APIを実行する様子

JavaScript はバックエンドではなくフロントエンドで動作するため、基本的にはバックエンドに存在するデータベースに対する操作を行うことはできません。

ですが、ウェブアプリがデータベース操作を行うための Web API を公開しておけば、その Web API を利用して JavaScript からデータベースを操作することができるようになります。そして、このデータベース操作を JavaScript から実施できるようにすることで、JavaScript で実現することのできる機能の幅が広がります。

JavaScriptからAPIの実行によってDB操作を行う様子

例えば 動的なデータ更新が実現可能 で紹介したような「無限スクロール」は、ページがスクロールされるたびに次のレコードをデータベースから取得する処理が必要となります。これは JavaScript 単体では実現できませんが、ウェブアプリが「追加でレコードを取得する API」を公開していれば実現可能です。具体的には、ページがスクロールされるたびに、その API を JavaScript から実行し、取得したレコードをページに追加表示することで実現できます。

無限スクロールをWeb APIの利用によって実現する様子

このように、ウェブアプリの Web API を利用することで、JavaScript で実現できる機能の幅を広げることができます。つまり、JavaScript を扱うウェブアプリにおいては、Web API の重要性がより高くなります。そのため、JavaScript をウェブアプリから扱うのであれば、Web API をウェブアプリに開発しておくことをオススメします。少なくとも、JavaScript で実現したい機能に必要となる Web API は開発しておきましょう!

Web API の開発に関しては下記ページで解説していますので、詳細を知りたい方は下記ページを参照していただければと思います。

DjangoでのWeb APIの開発の仕方の説明ページ愛卯キャッチ 【Django入門19】Web APIを開発 【Django入門20】Django REST FrameworkでのWeb APIの開発

スポンサーリンク

タグに id 属性や class 属性を設定する

また、JavaScript から操作したい要素のタグには id 属性や class 属性等の目印となる属性を設定しておいた方が良いです。JavaScript ではページ内の要素を取得し、その要素に対して操作(値の取得・変更や属性の追加等)を実施することが可能です。で、その要素を取得する時には、id 属性や class 属性でページ内を検索して取得するのが楽なので、必要に応じてテンプレートファイル内のタグには id 属性や class 属性を設定しておくようにしましょう。

idやclassを指定して操作対象の要素を特定する様子

ということで、Django で開発するウェブアプリでの JavaScript を扱い方は基本的には他の静的ファイルと同様となるのですが、必要に応じて Web API の開発や、テンプレートファイルのタグへの id 属性・class 属性の設定も必要となります。

JavaScript でスクリプトを開発する

これらを行えば、あとは JavaScript でスクリプトを作り込んで、より魅力的な UI を開発していけば良いだけです!

JavaScript の文法やコーディング手法等については別途参考書等で学習していただければと思いますが、ここでも JavaScript で実装する機会の多い簡単な処理を3つ紹介しておきます。これらを活用・応用するだけでも、魅力的なウェブアプリが開発していけると思いますので、これらについては是非覚えておいてください!

DOM 操作

1つが DOM (Document Object Model) 操作になります。DOM 操作とは、簡単に言えばページ内の要素に対する操作となります。要素の各種属性を変更したり、要素内の文字列を変更したり、新たな子要素を追加したりするようなことが可能です。動的なデータの更新を行う時には、この DOM 操作を実施することが多いですし、適用するスタイルを動的に変化させることで複雑なアニメーションを実現するようなことも可能です。また、単に要素内の文字列や要素の属性を取得するようなことも可能です。

DOM操作の説明図

DOM 操作を行う際には、タグに id 属性や class 属性を設定する でも解説したように、まずページ内から idclass 等で要素を検索して取得し、その取得した要素のデータ属性の変更や、取得した要素からのメソッドの実行によって実施することになります。

例えば下記を実行すれば、id 属性が introduction の要素内の文字列が変化することになります。

DOM操作の例
const elem = document.getElementById('introduction');
elem.innerText = 'このページではJavaScriptについて解説します。';

HTTP リクエストの送信

また、HTTP リクエストの送信を行うことも多いです。この HTTP リクエストの送信によって、Web API を実行することができます。

HTTPリクエストの送信の説明図

この HTTP リクエストの送信は、fetch 関数等の実行によって実施することが可能です。

下記は POST /api/login/ の HTTP リクエストを送信する例となります。fetch 関数の引数により、送信する HTTP リクエストのメソッドやヘッダー・ボディ等を設定することも可能です。

HTTPリクエストの送信
async function login() {
    const response = await fetch('/api/login/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ 'username', 'password' }),
    });
    // 略
}

ここまでの説明からも分かるように、JavaScript からデータベース操作を実施するためには Web API の実行が必要となります。そのため、HTTP リクエストの送信の仕方についてはしっかり理解しておきましょう。

MEMO

JavaScript では fetch 意外にも HTTP リクエストを送信する関数が用意されています

詳しく知りたい方は、別途ググってみてください

イベントリスナーの登録

また、JavaScript においては、要素に対してイベントリスナーを登録することが可能です。イベントリスナーとは「監視対象のイベント」と「関数」を設定する仕組みです。要素に対してイベントリスナーを登録しておくことで、その要素に対して「監視対象のイベント」が発生した時に「関数」が自動的に実行されるようになります。

例えば、特定の画像に対し、監視対象のイベントが「クリック」、関数が「関数 A」であるイベントリスナーを登録しておけば、その画像がクリックされた時に 関数 A が自動的に実行されるようになります。例えば 関数 A を画像を小さくする処理を実行する関数とすれば、クリックによって画像の表示サイズが小さくなることになります。

イベントリスナー登録の説明図

このように、要素に対してイベントリスナーを登録しておくことで、ユーザーの操作に対してインタラクティブなページを実現することができるようになります。例えばユーザーの操作に応じて DOM 操作を行ったり、Web API を実行したりするようなことが可能となります。

このイベントリスナーの登録は、登録先の要素に addEventListener メソッドを実行させることで実施することができます。例えば下記を実行すれば、id 属性が thumbnail の要素がクリックされたときに、func という関数が実行されるようになります。

イベントリスナーの登録例
const thumbnail = document.getElementById('thumbnail');
thumbnail.addEventListener('click', func);

検証ツールや開発者ツールでデバッグする

ついでに、JavaScript を利用するウェブアプリを開発するときに便利なツールを紹介しておきます。

ここまで説明してきた通り、JavaScript はウェブブラウザ上で動作します。そのため、JavaScript のデバッグは、Django を開発する時に利用しているような開発ツールでは実施できないことが多いです。そもそもプログラミング言語も異なりますからね…。

ウェブブラウザ標準のデバッグツール

ですが、ウェブブラウザ自体に JavaScript のデバッグに役立つツールが用意されていますので、それを利用して JavaScript のデバッグを効率的に実施することができます。具体的には、Google Chrome における「検証ツール」や Microsoft Edge における「開発者ツール」が JavaScript のデバッグツールとなります。これらは、HTML や CSS の解析・ネットワークパフォーマンスの解析・送受信した HTTP リクエスト・HTTP レスポンスの解析等の様々な機能を持つツールであるため、別に JavaScript のデバッグ専用のツールというわけではないのですが、JavaScript のステップ実行等を行うことも可能なので JavaScript のデバッグにも利用可能です。

ツールを使ってJavaScriptをデバッグする様子

デバッグツールの使い方

Google Chrome の場合は、右クリックメニューの 検証 を選択することで検証ツールが起動します。また、Microsoft Edge の場合は、右クリックメニューの 開発者ツールで調査する を選択することで開発者ツールが起動します。これらは名前は異なりますが、機能や使い方に関してはほとんど同じです。

ツールの起動方法

JavaScript のデバッグを行う際には、まずツール起動後に ソース タブを選択し、そのタブの左側のエクスプローラーからデバッグ対象の JavaScript ファイルを選択します。すると、右側のウィンドウに選択した JavaScript のソースコードが表示されます(事前に、デバッグ対象の JavaScript ファイルが読み込まれるページを表示しておく必要があります)。

JavaScriptのデバッグの仕方を説明する図

表示したソースコードにはブレークポイントを設定することが可能です。ブレークポイントに設定したい行の「行番号の左側」をクリックすれば、その行をブレークポイントに設定することができます。

これにより、そのブレークポイントに設定された行が実行される直前で JavaScript の処理が停止するようになります。処理を停止させた後は、下図のオレンジ枠内のボタンのクリック操作で処理を1行ずつ進めることもできますし、その時点の各変数の値も右側のウィンドウ(下図の緑枠内)から確認可能です。

JavaScriptのデバッグの仕方を説明する図2

また、ウィンドウ下部にはコンソールが表示され、このコンソールで console.log()console.error() によって出力されたログやエラーを確認することもできます。

これらを利用することでバグの原因調査が進めやすくなりますので、JavaScript を扱う場合は、これらのツールを積極的に活用していくようにしましょう!

スポンサーリンク

JavaScript を扱うウェブアプリの開発例

次は、簡単な JavaScript ファイルを用意し、それを扱うウェブアプリを開発していきたいと思います。

ここでは、JavaScript を利用して無限スクロールを実現する例を示していきたいと思います。開発するウェブアプリでは「コメント一覧表示ページ」を備えており、このページを表示した際には8件分のコメントのみが表示されるものとします。さらに、このページで下方向に対するスクロール操作が行われたタイミングで表示するコメントを8件分追加し、それを繰り返すことで、データベース内のコメントが8件分ずつ順々に追加で表示されるようにしていきます。

無限スクロールの実現の仕方

これを実現するためには、まずは通常のウェブアプリとしてコメントを管理するモデルクラスを定義し、さらにビュー・テンプレートを開発してコメント一覧を表示するページを実現していく必要があります。

また、JavaScript からウェブアプリで管理しているコメントが取得できるように Web API の追加も必要となります。ついでなので、CRUD 操作全てに対応する Web API を追加し、動作確認で必要になるコメントの新規登録も Web API から実行できるようにしていきたいと思います。 Web API は Django REST Framework を利用して開発していきますので、Django REST Framework が未インストールの方は、事前に下記コマンドを実行してインストールしておいてください。

python -m pip install djangorestframework

後は、先ほど説明したように、スクロール操作が行われたタイミングでコメントを追加表示する JavaScript ファイルを開発し、その JavaScript をウェブアプリで扱うようにすることで(ウェブブラウザから実行されるようにすることで)、無限スクロール機能を実現していくことになります。

(準備)JavaScript を扱わないウェブアプリの開発

まずは、JavaScript を扱わないウェブアプリをいつも通りの手順で開発していきたいと思います。その後、JavaScript をウェブアプリで扱うようにし、無限スクロールを実現していきます。

プロジェクトとアプリの作成

最初にプロジェクトとアプリの作成を行います。今回はプロジェクトの名称を js_project とし、そのプロジェクトに forum アプリと api アプリを作成していきたいと思います。

手順に関してはいつも通りで、まずは下記のコマンドを実行してプロジェクトを作成し、

% django-admin startproject js_project

さらに、上記コマンドの実行によって作成された js_project フォルダに移動後、下記の2つのコマンドを実行してください。

% python manage.py startapp forum
% python manage.py startapp api

続いて、js_project/settings.py を開き、INSTALLED_APPS のリストに 'forum''api'、さらには 'rest_framework' の追加を行い、それぞれのアプリの登録を行なってください。ちなみに、'rest_framework' を追加する必要があるのは、今回 Web API を Django REST Framework を利用して開発していくからになります。

INSTALLED_APPSの編集
INSTALLED_APPS = [
    'forum',
    'api',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
]

さらに、js_project/urls.py を下記のように編集し、js_project/urls.py から forum/urls.pyapi/urls.py が読み込まれるように設定します。

js_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('forum/', include('forum.urls')),
    path('api/', include('api.urls'))
]

forum/models.py

次は、コメントを管理するモデルクラスの定義を行います。

forum/models.py を開き、下記のように編集してください。

forum/models.py
from django.db import models

class Comment(models.Model):
    author = models.CharField(max_length=32)
    text = models.CharField(max_length=256)

forum/views.py

ここからは、コメントの一覧を表示するページを実現するための作業を行なっていきます。

まずは、先ほど定義した Comment のレコードの一覧を表示するビューを用意しましょう。

レコードの一覧を表示するビューは ListView のサブクラスを定義することで簡単に作成することができます。ただし、最初にページを表示した時にはレコードは 8 つのみを表示するようにしたいため、クラス変数 paginate_by = 8 を定義してページネーションが行われるようにする必要があります。

つまり、下記のように forum/views.py を変更すれば良いことになります。

forum/views.py
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    paginate_by = 8

この CommentList ではクラス変数 template_name を定義していないため、レスポンスのボディとなる HTML を生成するときには forum/comment_list.html のパスのテンプレートファイルが利用されることになります。したがって、forum/templates/forum/comment_list.html を作成しておく必要があります。

また、このビューがテーブルから取得したレコードの集合は、コンテキストの object_list キーにセットされることになるため、テンプレートファイルからは、変数 object_list からレコードを取得する必要があります。

MEMO

他にも、comment_listpage からもレコードを取得することが可能ですが、今回は object_list からレコードを取得することを前提に解説を進めます

この辺りの ListView の詳細については下記ページで解説していますので、詳しく知りたい方は下記ページを参照してください。

DjangoのListViewの解説ページアイキャッチ 【Django】ListViewの使い方(クラスベースビューでの一覧リストページの実現)

comment_list.html

次に、CommentList から利用されるテンプレートファイルを作成していきます。

先ほども説明したように、テンプレートファイルは forum/templates/forum/comment_list.html というファイル名で作成しておく必要があります。

なので、まずは forum 内に templates フォルダを作成し、さらに templates フォルダ内に forum フォルダを作成しましょう。その後、最後に作成した forum フォルダ内に comment_list.html を作成してください。

続いて、comment_list.html を開き、下記のように変更を加えてください。本来であれば、スタイルは別途 CSS ファイルに定義するべきですが、手間が増えるので今回は HTML 内にスタイルの定義も行っています。

comment_list.html
<!DOCTYPE html>
<html>
<head>
    <title>無限スクロール</title>
    <style>
        table {
            font-size: 30px;
            width: 100%;
        }
        th, td {
            padding: 15px;
        }
    </style>
</head>
<body>
    <h1>コメント一覧</h1>
    <table>
        <tr><th>ID</th><th>Author</th><th>Text</th></tr>
        <tbody id="comment-list">
            {% for comment in object_list %}
            <tr>
                <td>{{ comment.id }}</td>
                <td>{{ comment.author }}</td>
                <td>{{ comment.text }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>

基本的には、単に object_list の集合に含まれる Comment のインスタンスをテーブルの各行に出力するだけのテンプレートファイルになります。

ただ、このテンプレートファイルにもポイントがあって、それは <tbody> タグに id 属性を設定している点になります。タグに id 属性や class 属性を設定する でも解説したように、id 属性が設定されているタグの要素は JavaScript からの取得が楽になりますので、そのために id 属性を設定するようにしています。

今回は、下方向へのスクロール時に、<tbody> セクション内へ行を追加することで表示するコメントを追加することになるため、この <tbod>y タグの要素への操作が楽になるよう、id 属性を設定するようにしています。

forum/urls.py の作成

続いて、forum/urls.py を新規作成し、下記のように中身を変更して comments/CommentList とのマッピングを行います。これにより、/forum/comments/ の URL に対する HTTP リクエストを受け取った時に、コメント一覧ページが表示されるようになったことになります。

forum/urls.py
from django.urls import path
from .views import CommentList

urlpatterns = [
    path('comments/', CommentList.as_view()),
]

api/serializers.py

ここからは、Web API の開発を行っていきます。前述の通り、Web API は Django REST Framework を利用して開発していきます。以降では、Django REST Framework を DRF と略します。

DRF を利用して Web API を開発する場合、まずシリアライザーを定義する必要があります。今回は、api/serializers.py を新規作成し、下記のようにシリアライザーを定義したいと思います。

api/serializers.py
from rest_framework import serializers
from forum.models import Comment

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ['id', 'author', 'text']

api/views.py

次に、Web API 用のビューを作成します。

Web API 用のビューは、DRF の ModelViewSet のサブクラスを定義し、必要なクラス変数を定義するだけで作成することが可能です。しかも、CRUD の各操作用の Web API が開発できます。

今回は、api/views.py を下記のように変更し、ModelViewSet のサブクラスとして CommentViewSet を定義したいと思います。

api/views.py
from rest_framework import viewsets
from rest_framework.pagination import PageNumberPagination

from forum.models import Comment
from .serializers import CommentSerializer

class CommentPagination(PageNumberPagination):
    page_size = 8

class CommentViewSet(viewsets.ModelViewSet):
    queryset = Comment.objects.all().order_by('id')
    serializer_class = CommentSerializer
    pagination_class = CommentPagination

ポイントは、クラス変数 pagination_class を定義している点です。このクラス変数を定義することで、レコードの一覧取得 API によって取得できるレコードがページネーションされるようになります。今回は、pagination_class に指定している CommentPagination で  page_size = 8 を定義しているため、クエリパラメーター page で指定されたページに割り付けられた 8 個のレコードのみが取得できることになります。

したがって、JavaScript 側で、ページがスクロールされるたびにクエリパラメーター page1 増やしながらレコードの一覧取得 API を実行するようにすれば、次に表示すべきレコード 8 件を取得し、それをページに追加して表示することができることになります。つまり、スクロールされるたびに表示されるレコードを順次追加していくような処理が実現できることになります。

api/urls.py

あとは、Web API 用のビューと URL とのマッピングを行えば Web API の完成です。

DRF を利用する場合、DefaultRouter によって Web API 用の URL が自動的に割り当てられることになるので、この URL とビューとのマッピングも簡単に実施することができます。

ということで、api/views.py を新規作成し、さらにファイルに下記を記述してください。

api/views.py
from django.urls import path, include
from rest_framework import routers
from .views import CommentViewSet

router = routers.DefaultRouter()
router.register('comments', CommentViewSet, basename='comment')

urlpatterns = [
    path('', include(router.urls)),
]

ここまでのファイルの追加・変更によって、下記の API が開発できたことになります。ただし、今回 JavaScript から実行する API は「コメントの一覧取得 API」のみとなります(動作確認では、コメントのレコードの新規登録のために「コメントの投稿 API」も利用します)。

  • コメントの投稿 API:POST /api/comments/
  • コメントの一覧取得 API:GET /api/comments/
  • コメントの詳細取得 API:GET /api/comments/{id}/
  • コメントの一部更新 API:PATCH /api/comments/{id}/
  • コメントの全体更新 API:PUT /api/comments/{id}/
  • コメントの削除 API:DELETE /api/comments/{id}/
  • ヘッダー情報取得 API:HEAD /api/comments/HEAD /api/comments/{id}/
  • メタ情報取得 API:OPTIONS /api/comments/OPTIONS /api/comments/{id}/

動作確認

一旦、ここまでに開発してきたウェブアプリの動作確認を実施しておきましょう!

まずは、js_project フォルダ内 (manage.py が存在するフォルダ)で、下記コマンドを実行してマイグレーションを実施してください。

% python manage.py makemigrations
% python manage.py migrate

次に、下記コマンドを実行して開発用ウェブサーバーを起動しましょう。

% python manage.py runserver

今度は、ウェブブラウザを起動して下記 URL を開いてください。

http://localhost:8000/api/comments/

表示されたページの下側には、/api/comments/ の Web API 実行用のフォームが表示されるはずです。このフォームの Author フィールドにコメントの投稿者、Text フィールドにコメントの本文を入力して POST ボタンをクリックすれば、POST /api/comments/ の Web API、すなわちコメントの新規登録 API が実行されます。そして、この API の実行によって、Comment のレコードがデータベースに新規登録されることになります。

コメント投稿APIの実行フォーム

このフォームを利用して、コメントの新規登録 API を実行してコメントの新規登録を行なってください。新規登録するコメントは多ければ多いほど良いです。無限スクロールを機能させるためには、少なくとも9件のコメントが必要です。目安としては40件くらいコメントが登録されていると、無限スクロールの効果が確認しやすくなると思います。

ただし、コメントの投稿者やコメントの本文は同じものでも良いです。POST ボタンをクリックしてもフォームのフィールドの値はクリアされないようになっているので、適当な文字列をフィールドに入力したあと、POST ボタンをゆっくり連打して適当に多めのコメントを新規登録しておいてください。

コメントが登録できたら、次はページ上部の GET ボタンをクリックしてみてください。GET /api/comments/ の Web API、すなわちコメントの一覧取得 API が実行され、その結果として得られる JSON がページに表示されるはずです。

コメント一覧取得APIの実行結果

ここで注目していただきたいのが、next フィールドと results フィールドになります。

next フィールドの値には、次のページが存在するときに、次のページの URL が設定されるようになっています。逆に、次のページが存在しない時には値が null になるようになっています。つまり、次のページの存在の有無は next フィールドで判断可能です。

また、results フィールドは配列で、各要素が取得された Comment のレコードの各種フィールドの値が設定されたオブジェクトになっていることが確認できます。つまり、コメントの一覧取得 API で得られた Comment のレコードの情報は results フィールドの配列の要素として取得可能です。

これらのフィールドを利用して無限スクロールを実現していくことになりますので、これらのフィールドの意味合いは覚えておいてください。

次は、ウェブブラウザで下記 URL を開いてください。

http://localhost:8000/forum/comments/

すると、下の図のようにコメント一覧が表示されると思います。コメントが8件のみ表示されていれば動作確認は OK です。

コメント一覧表示ページ

現状では、ページを下方向にスクロールしてもコメントが追加で表示されるようなこともありませんが、ここから JavaScript を実装してスクロールするたびにコメントが追加で表示されるようにしていきます。

以上で、JavaScript を導入する前のウェブアプリの動作確認は完了となりますので、先ほど起動した開発用ウェブサーバーは終了させておいてください。

JavaScript を扱うウェブアプリの開発

では、ここから JavaScript をウェブアプリから扱えるようにし、無限スクロール機能を実現していきたいと思います。

静的ファイル配信用フォルダの作成

まずは、JavaScript ファイルを配信するための準備をしていきます。

Django の開発用ウェブサーバーでは、設定を変更しなくても アプリ名/static/ 以下に保存された静的ファイルが配信されるようになっています。アプリ名/static/ 以下のフォルダ構成は自由に変更しても問題ないため、今回は下記のフォルダから JavaScript のファイルを配信するようにしていきたいと思います。

forum/static/forum/js/

このフォルダは自動では作成されないため、ここで、上記のフォルダを作成しておいてください。static 以下のフォルダ全てを作成する必要があります。

comment_scroll.js

続いて、先ほど作成した下記フォルダに「配信する JavaScript ファイル」を作成していきます。

forum/static/forum/js/

まずは、上記フォルダの中に comment_scroll.js を作成し、ファイルの中に下記をコピペしてください。これが、コメント一覧表示に対して無限スクロール機能を追加するための JavaScript となります。

comment_scroll.js
// 次に表示するページの番号を取得する関数
function getNextPage() {
    const urlParams = new URLSearchParams(window.location.search); // 現在のURLを取得
    const page = urlParams.get(('page')) // クエリパラメーターpageを取得
    if (page === null) {
        return 2; //pageが指定されていない場合は2を返す
    } else {
        return Number(page) + 1; //pageを数値に変換して+1した値を返す
    }
}

// APIから新しいコメントを取得し、テーブルに追加する関数
async function addMoreItems() {
    if (!hasNext) return; // 次のページが存在しない場合、処理を終了

    try {
        // fetchを使用してAPIからデータを取得
        const response = await fetch(`/api/comments/?page=${nextPage}`);
        const data = await response.json(); // JSON形式でレスポンスをパース

        const tableBody = document.getElementById('comment-list'); // コメント追加先の要素を取得
        
        data.results.forEach(comment => { // 取得したコメントデータを1件ずつ処理
            const row = tableBody.insertRow(); // 新しい行をテーブルに追加
            row.insertCell(0).textContent = comment.id; // IDを1列目に追加
            row.insertCell(1).textContent = comment.author; // 名前を2列目に追加
            row.insertCell(2).textContent = comment.text; // コメント内容を3列目に追加
        });

        // nextがnullの場合は次のページは存在しない
        if (data.next === null) {
            hasNext = false;
        } else {
            hasNext = true;
        }
        
        nextPage += 1; // 次に取得するページ番号をインクリメント
    } catch (error) {
        // エラーが発生した場合にコンソールに出力
        console.error('Failed to load comments:', error);
    }
}

// イベント処理の設定
function initEvent() {
    // スクロールイベントを監視
    window.addEventListener('scroll', () => {
        // ページの下端50px以内に到達した場合に表示するコメントを追加
        if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 50) {
            addMoreItems(); // コメントの追加
        }
    });
}

// 次のページが存在するかを示すフラグ
let hasNext = true;

// 次に表示するページの番号を取得
let nextPage = getNextPage();

// イベント処理の初期化
initEvent();

何を実施しているかは、JavaScript 初心者の方でもコメントを読んでいただければ大体分かるのではないかと思います

ポイントだけ説明しておくと、まず initEvent 関数では、ページに対するスクロールイベントの監視の設定を行なっています。そして、スクロールイベントが発生した時に、スクロール位置がページの下端.50 px 以内に到達していれば addMoreItems を実行するように設定しています。

スクロール検知時にaddMoreItems関数が実行される様子

その addMoreItems 関数でポイントになるのが fetch(`/api/comments/?page=${nextPage}`) の部分で、この fetch 関数によって、引数で指定された URL に対する HTTP リクエストの送信を実施しています。fetch 関数の引数でメソッドを指定しない場合はメソッドが GET の HTTP リクエストが送信されることになるため、ここでは「コメントの一覧取得 API」が実行されることになります。また、nextPageaddMoreItems が実行されるたびにインクリメントされるようになっているため、addMoreItems が実行されるたびに次のページに割り付けられたコメントが含まれる HTTP レスポンスが取得されることになります。

addMoreItems関数からAPIが実行される様子

また、この HTTP レスポンスのボディのデータフォーマットは JSON であり、data = await response.json() を実行することで、ボディの JSON データを JavaScript のオブジェクトに変換することが可能です。そして、取得したコメントの配列は、data.results から参照可能です。

さらに、document.getElementById('comment-list') によって、ページ内の id 属性が comment-list であるタグの要素の取得を行なっています。comment_list.html で作成したテンプレートファイルでは <tbody> タグの id 属性を comment-list に設定していますので、<tbody> タグの要素が取得されることになります。

あとは、下記のように data.results に含まれる要素に対してループを実施し、ループの中で <tbody> への行の追加、および、各行への Comment の各種フィールドの値のセルとしての追加を行えば、Web API の実行によって取得された全コメントが表に追加されて表示されるようになります。

tbodyへの列の追加
data.results.forEach(comment => { // 取得したコメントデータを1件ずつ処理
    const row = tableBody.insertRow(); // 新しい行をテーブルに追加
    row.insertCell(0).textContent = comment.id; // IDを1列目に追加
    row.insertCell(1).textContent = comment.author; // 名前を2列目に追加
    row.insertCell(2).textContent = comment.text; // コメント内容を3列目に追加
});

また、受信した HTTP レスポンスの next フィールド、すなわち data.nextnull の場合は、次のページが存在しないということになるため、addMoreItems 関数内の処理をスキップするようにしています。

これだと、最後のページのコメントを表示した後に追加されたコメントが表示できないなど、作り込みが甘い部分もあるのですが、この例でも、JavaScript でイベントの監視Web API の実行を行うことができ、それによって動的なページが実現可能である点は理解していただけるのではないかと思います。

JavaScript 実行用のタグの追加

あとは、comment_list.html で作成したテンプレートファイルに <script> タグを追加すれば、作成した JavaScript が読み込まれて実行されるようになり、無限スクロールが実現できるようになります。

開発用ウェブサーバーが配信する JavaScript ファイルを利用するためには、テンプレートファイルを下記のように作成する必要がある点に注意してください。

  • <script> タグの src には static テンプレートタグを利用して URL を指定する
  • <static> テンプレートタグの引数には、利用する JavaScript ファイルの アプリ名/static からの相対パスを指定する
  • <static> テンプレートタグの記述箇所より上側に {% load static %} を記述する

今回は、comment_list.html で作成した comment_list.html を下記のように変更すれば良いことになります。

scriptタグの追記
{% load static %}
<!DOCTYPE html>
<html>
<head>
    <title>無限スクロール</title>
    <style>
        table {
            font-size: 30px;
            width: 100%;
        }
        th, td {
            padding: 15px;
        }
    </style>
</head>
<body>
    <h1>コメント一覧</h1>
    <table>
        <tr><th>ID</th><th>Author</th><th>Text</th></tr>
        <tbody id="comment-list">
            {% for comment in object_list %}
            <tr>
                <td>{{ comment.id }}</td>
                <td>{{ comment.author }}</td>
                <td>{{ comment.text }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
    <script src="{% static 'forum/js/comment_scroll.js' %}"></script>
</body>
</html>

動作確認

以上で、JavaScript がウェブアプリから利用されるようになり、JavaScript に実装した機能、すなわち無限スクロールがコメント一覧表示ページで動作するようになったことになります。

ということで、動作確認を実施していきましょう!

まずは、いつも通り、下記のコマンドで開発用ウェブサーバーを起動してください。開発用ウェブサーバー起動中に静的ファイルを追加した場合、そのファイルの配信が上手く行われないことがあるようなので、先ほどの動作確認時から開発用ウェブサーバーが起動しっぱなしという方も、ここで一度開発用ウェブサーバーを終了させてから再度開発用ウェブサーバーを起動するようにしてください。

% python manage.py runserver

続けて、ウェブブラウザを起動し、下記の URL を開いてください。

http://localhost:8000/forum/comments/

これにより、ページにコメント一覧が表示されると思います。ここで気をつけていただきたいのが、ブラウザのウィンドウサイズになります。このウィンドウサイズは、コメント一覧の下側に余白ができないようにサイズ調整をするようにしてください。コメントの一部が隠れてしまっていても問題ありませんし、ページを拡大して余白が無くなるようにするのでも問題ありません。サイズ調整が必要なのは、comment_scroll.js ではコメント一覧の下側に余白ができていると上手くスクロールが検知できなないからになります。

ウィンドウのサイズ調整が必要であることを説明する図

続いて、ページを下方向にスクロールしてみてください。すると、以前に動作確認した際には8件しか表示されなかったのに対し、今回はスクロールするたびに表示されるコメントが追加され、無限スクロールが実現できていることが確認できるはずです。

無限スクロールが実現できている様子

コメントが追加されていく様子が視覚的に分かりにくいかもしれませんが、開発用ウェブサーバーを起動したコンソールを確認してみれば、GET /api/comments/ のリクエストを複数回ウェブアプリが受信したことを確認できるはずです。これにより、スクロールのたびに Web API が実行されている様子が確認できます。

[07/Jan/2025 00:43:31] "GET /forum/comments/ HTTP/1.1" 200 1702
[07/Jan/2025 00:43:31] "GET /static/forum/js/comment_scroll.js HTTP/1.1" 304 0
[07/Jan/2025 00:43:32] "GET /api/comments/?page=2 HTTP/1.1" 200 556
[07/Jan/2025 00:43:32] "GET /api/comments/?page=3 HTTP/1.1" 200 564
[07/Jan/2025 00:43:32] "GET /api/comments/?page=4 HTTP/1.1" 200 564
[07/Jan/2025 00:43:33] "GET /api/comments/?page=5 HTTP/1.1" 200 524

そして、Web API の実行によって取得されたコメントのレコードを JavaScript によってページのコメント一覧表示箇所に追加することで、スクロールされるたびにコメント表示が増えていくという動作を実現しています。

ということで、JavaScript を利用した動的なページが実現できたことになります。ただ、現状だとデータが動的に更新されている様子が分かりにくいので、アニメーションを追加し、コメントが追加されていく様子が視覚的に分かりやすくなるようにしていきたいと思います。

アニメーションの追加

今回は、スクロールによってページに追加されたコメントが右方向からスライドインするようなアニメーションを追加することで、コメントが追加されていく様子が視覚的に分かりやすくなるようにしていきたいと思います。

JavaScript を利用すれば、HTML のタグへの class 属性を好きなタイミングで追加したり変更したりすることが可能です。また、class 属性に応じてスタイルが適用されることになるため、JavaScript を利用することで、各タグの要素のスタイル(見た目)を動的に変化させることができることになります。

これを利用し、追加表示するコメント(表の行)の class 属性を動的に変化させて適用されるスタイルを変化させることで、スライドインのアニメーションを実現していきます。

ということで、まずはスタイルの定義を行います。comment_list.html を下記のように変更し、クラス slide-in に適用するスタイルと slide-in.show に適用するスタイルを定義してください。

scriptタグの追記
{% load static %}
<!DOCTYPE html>
<html>
<head>
    <title>無限スクロール</title>
    <style>
        table {
            font-size: 30px;
            width: 100%;
        }
        th, td {
            padding: 15px;
        }
        .slide-in {
            transform: translateX(100%); /* ページ外右に移動させる */
            opacity: 0;
            transition: transform 1s ease-in-out, opacity 1s ease-in-out;
        }
        .slide-in.show {
            transform: translateX(0); /* 元の位置に戻す */
            opacity: 1;
        }
    </style>
</head>
<body>
    <h1>コメント一覧</h1>
    <table>
        <tr><th>ID</th><th>Author</th><th>Text</th></tr>
        <tbody id="comment-list">
            {% for comment in object_list %}
            <tr>
                <td>{{ comment.id }}</td>
                <td>{{ comment.author }}</td>
                <td>{{ comment.text }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
    <script src="{% static 'forum/js/comment_scroll.js' %}"></script>
</body>
</html>

まず、クラス slide-inclass 属性に指定されたタグの要素は下記のようなスタイルが適用されるため、ページ外の右側に透明な状態で表示されることになります。

  • transform: translateX(100%):ページ外の右側に移動
  • opacity: 0:不透明度 0 %

さらに、このタグの class 属性にクラス show が追加されれば、その要素に下記のスタイルが適用されることになり、元々の位置に不透明な状態で表示されることになります。

  • transform: translateX(0%):元々の配置に移動
  • opacity: 1:不透明度 100 %

ただし、slide-in の transition の指定により、show が追加されてから 1 秒かけて徐々に上記の位置・不透明度に変化していくことになるため、徐々にページ外の右側から元の位置に、さらに不透明度を上げながら表示が変化していくことになるため、要素がページ右側からスライドインするようなアニメーションが実現できることになります。

スタイル設定の変更によってスライドインを実現する様子

そのため、スクロールによって追加されたコメント(正確には表の行)の class 属性にクラス slide-in を追加し、さらにその後にクラス show を追加してやれば、スクロールによって追加されたコメントがスライドインしてくるようになります。

ということで、次は JavaScript 側で、上記のような class 属性へのクラスの追加を行うように変更を行います。上記のような class 属性の追加は、comment_scroll.js に下記の太字部分の処理を追加することで実現できます。

class属性の追加
// 次に表示するページの番号を取得する関数
function getNextPage() {
    const urlParams = new URLSearchParams(window.location.search); // 現在のURLを取得
    const page = urlParams.get(('page')) // クエリパラメーターpageを取得
    if (page === null) {
        return 2; //pageが指定されていない場合は2を返す
    } else {
        return Number(page) + 1; //pageを数値に変換して+1した値を返す
    }
}

// APIから新しいコメントを取得し、テーブルに追加する関数
async function addMoreItems() {
    if (!hasNext) return; // 次のページが存在しない場合、処理を終了

    try {
        // fetchを使用してAPIからデータを取得
        const response = await fetch(`/api/comments/?page=${nextPage}`);
        const data = await response.json(); // JSON形式でレスポンスをパース

        const tableBody = document.getElementById('comment-list'); // コメント追加先の要素を取得
        
        data.results.forEach(comment => { // 取得したコメントデータを1件ずつ処理
            const row = tableBody.insertRow(); // 新しい行をテーブルに追加
            row.insertCell(0).textContent = comment.id; // IDを1列目に追加
            row.insertCell(1).textContent = comment.author; // 名前を2列目に追加
            row.insertCell(2).textContent = comment.text; // コメント内容を3列目に追加

            row.classList.add('slide-in'); // 新しい行のclassにslide-inを追加
            setTimeout(() => row.classList.add('show'), 10); // 遅延をかけてclassにshowを追加
        });

        // nextがnullの場合は次のページは存在しない
        if (data.next === null) {
            hasNext = false;
        } else {
            hasNext = true;
        }
        
        nextPage += 1; // 次に取得するページ番号をインクリメント
    } catch (error) {
        // エラーが発生した場合にコンソールに出力
        console.error('Failed to load comments:', error);
    }
}

// イベント処理の設定
function initEvent() {
    // スクロールイベントを監視
    window.addEventListener('scroll', () => {
        // ページの下端50px以内に到達した場合に表示するコメントを追加
        if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 50) {
            addMoreItems(); // コメントの追加
        }
    });
}

// 次のページが存在するかを示すフラグ
let hasNext = true;

// 次に表示するページの番号を取得
let nextPage = getNextPage();

// イベント処理の初期化
initEvent();

ポイントは、クラス show の追加を “遅延させて” 実施する必要があるという点になります。この遅延を実現するために、setTimeout を利用しています。

遅延させずにクラス show を追加すると、クラス slide-in のみのスタイルが適用される前にクラス show が追加され、.slide-in.show 向けに定義したスタイルのみが適用されることになります。つまり、スライドインのアニメーションが行われず、単にコメントが表示されるのみとなります。これを避けるため、クラス slide-in のみのスタイルが適用された後にクラス show が追加されるよう、上記のような遅延が必要となります。

ということで、以上の変更により追加表示されるコメントがスライドインされるようになったことになります。

ウェブブラウザで下記 URL を再度表示し、ページを下方向にスクロールしてみてください。

http://localhost:8000/forum/comments/

すると、下図のように、スクロールするたびにページ外の右側からスライドインされてコメントが追加されていく様子が確認できると思います。

コメントがスクロールするたびにスライドインする様子

このように、JavaScript を利用することでアニメーションを実現することもでき、よりリッチで見た目の良いウェブアプリを開発していくことができます。

簡単な例でしたが、それでも JavaScript を利用することで開発可能なウェブアプリの幅が大きく広がることは実感していただけたのではないかと思います。

MEMO

もし、スライドインが上手く行われないという方は、comment_scroll.js がキャッシュに残っていて変更後のファイルが読み込まれていない可能性があるため、まずは shift キーを押しながらウェブブラウザの更新ボタンをクリックしてみてください

これでキャッシュを無視して最新の comment_scroll.js が読み込まれるはずです(それでも上手くいかない場合はコードが間違っている可能性が高いです)

スポンサーリンク

掲示板アプリで JavaScript を扱う

最後に、ここまで解説してきた内容を踏まえ、掲示板アプリを JavaScript が扱えるように変更していきたいと思います。

この Django 入門 の連載の中では簡単な掲示板アプリを開発してきており、前回の連載の下記ページの 掲示板アプリの API を DRF で開発する で掲示板アプリの Web API を Django REST Framework で開発しました。

【Django入門20】Django REST FrameworkでのWeb APIの開発

今回は、前回の連載で開発したアプリを JavaScript が扱えるように改良していきたいと思います。

掲示板アプリのプロジェクト一式の公開先

この Django 入門 の連載を通して開発している掲示板アプリのプロジェクトは GitHub の下記レポジトリで公開しています。

https://github.com/da-eu/django-introduction

また、前述のとおり、ここでは前回の連載の 掲示板アプリの API を DRF で開発する で作成したプロジェクトをベースに変更を加えていきます。このベースとなるプロジェクトは下記のリリースで公開していますので、必要に応じてこちらからプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-drf

さらに、ここから説明していく内容の変更を加えたプロジェクトも下記のリリースで公開しています。以降では、基本的には前回からの差分のみのコードを紹介していくことになるため、変更後のソースコードの全体を見たいという方は、下記からプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-javascript

変更内容の概要

まずは、今回の変更内容の概要について説明しておきます。

JavaScript を利用することで実現可能になることは非常に幅広いのですが、今回は簡単な例を示したいので、コメント一覧ページに表示されるコメントの総数の動的更新を実現していくようにしたいと思います。

掲示板アプリでJavaScriptを扱うことで実現することを示す図

具体的には、JavaScript から定期的に「コメントの一覧取得 API (前回の連載で開発した Web API)」を実行して全コメントを取得し、そのコメントの総数でページに表示されるコメントの総数を動的に更新していくようにしていきます。

コメントの総数の動的更新を実現する方法の説明図

JavaScript を扱うウェブアプリの開発例 で示した無限スクロールの例に比べて難易度が低くなった気もするかもしれませんが、実はそうではありません。前回の連載では、コメントを操作する各種 Web API はトークン認証を実施するように開発しました。そのため、今回利用する「コメントの一覧取得 API」を実行する時にもトークンが必要となります。

これは、単にコメント一覧ページでトークンを取得し、それをコメントの一覧取得 API を実行時に送信するようにすれば良いだけのようにも思えます。確かにその通りなのですが、トークンを取得するためにはユーザー名とパスワードが必要なので、コメント一覧ページにフォームを表示し、ユーザーからユーザー名とパスワードを入力してもらうようなことが必要となることになります。特に、この掲示板アプリを利用するためにはログインも必要なので、ログインだけでなくトークン取得のためにもユーザー名・パスワードの入力が必要となり、使い勝手が悪くなってしまいます…。

JavaScriptからAPIを実行するためにユーザー名とパスワードを入力する必要があることを説明する図

このように、コメント一覧ページで動作する JavaScript がコメントの一覧取得 API を実行できるようにするためには、まずトークンの取得が必要となり、そのトークンの取得の実現が難しいです。特に使い勝手を下げずに実現するのが難しいです。

使い勝手を下げずにトークンの取得を実施できるように、今回は、下記のようにしてコメント一覧ページで動作する JavaScript でトークンを取得できるようにしていきたいと思います。

  • ログインとトークン発行を実施する「ログイン API」を追加する
  • ログインページで JavaScript を実行するようにし、JavaScript で下記の処理を行う
    • ログインフォームで 送信 ボタンがクリックされた時にログイン API を実行してログインとトークン取得を実施
    • 取得したトークンをウェブブラウザに記録
  • コメント一覧ページで実行する JavaScript でウェブブラウザから記録されたトークンを取得

つまり、ウェブブラウザにトークンを記録させることで、ログインページでログインと同時に取得したトークンをコメント一覧ページで使い回せるようにウェブアプリ(の JavaScript)を開発していきます。これにより、ユーザー名・パスワードの入力がログイン時のみで済むようになるため、使い勝手を低下させることなくコメントの一覧取得 API の実行およびコメントの総数の動的更新を実現することができます。

受け取ったトークンをウェブブラウザに記録させておくことで、他のページでトークンを使いまわせるようになることを示す図

これを実現するためには、JavaScript の実装も必要となりますし、下記のようなログイン API・ログアウト API も必要となります。

  • ログイン API:ログインとトークン発行を実施する API
  • ログアウト API;ログアウトとトークン削除を実施する API

ということで、ここから上記のような API の追加や JavaScript の実装等を行って、コメントの総数の動的更新を実現していきたいと思います。

MEMO

ログイン成功後にログイン状態を維持するためには、ウェブアプリから発行されたセッション ID をウェブブラウザに記録させておくことも必要となります

ただし、ログイン成功時にウェブアプリから発行されたセッション ID は、自動的にウェブブラウザに保存されるようになっているため、セッション ID を記録するための処理は JavaScript に無くても問題ありません

スポンサーリンク

ログイン API とログアウト API の追加

最初に、ログイン API とログアウト API を追加していきます。

ログイン API ではログインだけでなくトークン発行も実施し、ログアウト API ではログアウトだけでなくトークン削除も実施するように API を開発していきます。

api/views.py の変更

まずは、api/views.py を変更し、ログイン API とログアウト API のビューを開発していきます。

前回の連載となる下記ページでは、Comment の操作を行う API のビューを ModelViewSet を継承して開発しました。

【Django入門20】Django REST FrameworkでのWeb APIの開発

ただし、ModelViewSet は名前の通り、モデルクラスのテーブルの操作を行う API のビューを開発するために利用するクラスであり、ログインやログアウトを実現する API のビューを開発するのには向いていません。

そのため、これらの API のビューは、APIView という、API のビュー全般で利用可能である汎用的なクラスを継承して開発していきたいと思います。この APIView に関しても DRF で定義されるクラスとなります。

ということで、api/views.py を下記のように変更し、ログイン API のビューとなる LoginAPI とログアウト API のビューとなる LogoutAPI を定義します。

api/views.py
from django.contrib.auth import authenticate, login, logout
from rest_framework import viewsets
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.authtoken.models import Token
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from django_filters.rest_framework import DjangoFilterBackend
from forum.models import Comment
from .serializers import CommentSerializer
from .filters import CommentFilter
from .permissions import IsAuthorOrReadOnly


class CommentViewSet(viewsets.ModelViewSet):
    """
    ### コメントの操作API
    #### 認証
    - トークン認証
    #### 権限
    - 認証に失敗した場合はAPIの実行は失敗します
    - 他のユーザーが作成したレコードの更新・削除はできません
    #### その他
    - コメントの投稿者にはコメントの新規登録APIを実行したユーザーが自動的に設定されます
    """
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = CommentFilter
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)


class LoginAPI(APIView):

    @swagger_auto_schema(
        operation_description="ログインして認証トークンを取得します。",
        request_body=openapi.Schema(
            type=openapi.TYPE_OBJECT,
            properties={
                'username': openapi.Schema(type=openapi.TYPE_STRING, description='ユーザー名'),
                'password': openapi.Schema(type=openapi.TYPE_STRING, description='パスワード'),
            },
            required=['username', 'password']
        ),
        responses={
            200: openapi.Schema(
                type=openapi.TYPE_OBJECT,
                properties={
                    'token': openapi.Schema(type=openapi.TYPE_STRING, description='認証トークン'),
                    'user_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ユーザーID'),
                    'username': openapi.Schema(type=openapi.TYPE_STRING, description='ユーザー名'),
                },
            ),
            400: "Invalid credentials",
        },
    )
    def post(self, request):
        username = request.data.get('username')
        password = request.data.get('password')

        # ユーザー認証
        user = authenticate(username=username, password=password)

        if user is not None:
            # 認証成功時にトークンを発行
            token, created = Token.objects.get_or_create(user=user)

            login(request, user)

            return Response({
                'token': token.key,
                'user_id': user.id,
                'username': user.username,
            }, 200)
        else:
            return Response({
                'detail': '提供された認証情報でログインできません',
            }, 400)



class LogoutAPI(APIView):

    @swagger_auto_schema(
        operation_description="ログアウトして認証トークンを削除します。",
        responses={
            200: openapi.Schema(
                type=openapi.TYPE_OBJECT,
                properties={
                    'message': openapi.Schema(type=openapi.TYPE_STRING, description='ログアウトに成功しました'),
                },
            ),
        },
    )
    def post(self, request):
        if request.user.is_authenticated:
            # ログイン中のユーザーに対してのみ下記を実施

            # 現在のユーザーに紐付いたトークンを削除
            try:
                token = Token.objects.get(user=request.user)
                token.delete()
            except Token.DoesNotExist:
                pass

            logout(request)

        return Response({
            'detail': 'ログアウトに成功しました',
        }, 200)

変更点の半分程度は、@swagger_auto_schema に関する変更となります。

@swagger_auto_schema は、自動生成される API 仕様書に仕様を追記するデコレーターです。上記では、ログイン API・ログアウト API それぞれの仕様(特にリクエスト・レスポンスのボディ)を API 仕様書に追記しています。詳細に関しては省略させていただきますが、@swagger_auto_schema を利用することでも API 仕様書の追記が可能であることは覚えておいてください。

@swagger_auto_schema の部分を除けば、後は LoginAPILogoutAPI も割とシンプルな作りになっていて、コードを読めば何をしているかは大体理解できるのではないかと思います。

ポイントは Token で、この Token はトークン自体やトークンの発行相手となるユーザーを管理するモデルクラスになります。

LoginAPI の場合は Token.objects.get_or_create でトークンを取得し(存在しない場合はトークンの新規登録も実施される)、それを返却することでトークンの発行を実現しています。それに対し、LogoutAPI の場合は Token.objects.get でトークンのインスタンスを取得した後に delete メソッドを実行することでトークンの削除を実現しています。

また、Web API においても受信したデータの妥当性の検証が重要となるのですが、authenticate 関数を実行することでユーザー名とパスワードの妥当性が検証できるため、LoginAPI では別途シリアライザーの利用や妥当性の検証の処理は実施しないようにしています。

api/urls.py

続いて、api/urls.py を変更し、先ほど定義したビューと URL とのマッピングを実施します。

ModelViewSet のサブクラスとしてビューを定義した場合は DefaultRouter を利用することで自動的に各種 API に対する URL を割り当てることができるのですが、今回は APIView のサブクラスとしてビューを定義しているため、自身で各種 API に割り当てる URL を決め、いつも通りの手順で URL とビューとのマッピングを行う必要があります。

今回は、ログイン API とログアウト API に対し、下記のように URL を割り当てることにしたいと思います。

  • ログイン API:/api/login/
  • ログアウト API:/api/logout/

このような URL の割り当ては、次のように api/urls.py を変更して、上記の URL とビューとをマッピングすることで実現できます。

api/urls.py
from django.urls import path, include
from rest_framework import routers
from rest_framework import permissions
from rest_framework.authtoken.views import obtain_auth_token
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from .views import CommentViewSet, LoginAPI, LogoutAPI

router = routers.DefaultRouter()
router.register(r'comments', CommentViewSet, basename='comment')

info = openapi.Info(
    title='Forum API',
    default_version='v1',
    description='掲示板アプリ向けAPI',
    contact=openapi.Contact(email='daeu@test.com'),
    license=openapi.License(name="Apache 2.0", url="http://www.apache.org/licenses/LICENSE-2.0.html"),
)

schema_view = get_schema_view(
    info,
    public=True,
    permission_classes=[permissions.AllowAny],
)

urlpatterns = [
    path('', include(router.urls)),
    path('swagger/', schema_view.with_ui('swagger'), name='schema-swagger-ui'),
    path('token/', obtain_auth_token),
    path('login/', LoginAPI.as_view()),
    path('logout/', LogoutAPI.as_view()),
]

以上で、ログイン API とログアウト API とが完成したことになります。

テンプレートファイルへの id 属性の追加

続いて、JavaScript からの操作対象となるテンプレートファイルのタグに id 属性を追加していきます。

タグに id 属性や class 属性を設定する でも解説したように、特定の要素に対して操作を行うためには、まずはその要素の取得が必要となります。そして、要素のタグに id 属性が設定されていると、この要素の取得が楽に実現できます。

そのため、テンプレートファイルを変更し、JavaScript からの操作対象となる要素のタグに id 属性を追加していきたいと思います。

forum/comments.html

まず、JavaScript からの操作対象となる要素として、forum/comments.html における、コメントの総数表示箇所が挙げられます。この部分を動的に更新できるように id 属性を追加していきます。

forum/comments.htmlへのid属性の追加箇所

この総数の値の箇所に対して id 属性を追加するため、forum/comments.html を下記のように変更します。{{ page_obj.paginator.count }} がコメント総数を表示する変数となっているため、これを span タグで囲い、さらにその span タグに id 属性を設定するようにしています。これにより、コメント総数の要素の ID が comment-count に設定されることになります。

forum/comments.html
{% extends "forum/base.html" %}

{% block title %}
コメント一覧
{% endblock %}

{% block main %}
<h1>コメント一覧(全<span id="comment-count">{{ page_obj.paginator.count }}</span>件)</h1>
<table class="table table-hover">
    <thead>
        <tr>
            <th>本文</th><th>投稿者</th>
        </tr>
    </thead>
    <tbody>
        {% for comment in page_obj %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
            {% if comment.user is not None %}
            <td>{{ comment.user.username }}</td>
            {% else %}
            <td>不明</td>
            {% endif %}
        </tr>
        {% endfor %}
    </tbody>
</table>
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?p=1">« first</a>
            <a href="?p={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}
        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>
        {% if page_obj.has_next %}
            <a href="?p={{ page_obj.next_page_number }}">next</a>
            <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
        {% endif %}
    </span>
</div>
{% endblock %}

forum/login.html

続いて、forum/login.html における「ログインフォーム」と「フィールドの表示テーブル」に id 属性を追加していきます。

forum/login.htmlへのid属性の追加箇所

今回の変更内容を実現するため、ログインフォームに対して送信操作が行われた際に、そのままフォームを送信するのではなく、先ほど開発したログイン API を実行するように変更します。ログインフォームに対して送信操作が行われた時の処理を JavaScript で変更するため、ログインフォームに対して id 属性を追加します。また、ログインに失敗した際にユーザーに失敗したことを通知するためのメッセージを「フィールドの表示テーブル」の最初の行に出力するようにするため、テーブルに対しても id 属性を追加します。

テーブルにエラーメッセージを表示する様子

また、JavaScript からログイン API を実行するために、各種フィールドに入力されたデータの取得が必要となります。そのため、各種フィールドにも id 属性が必要となるのですが、これらはテンプレートの仕組みによるフォームのテンプレートファイルへの埋め込み時に自動的に id 属性が設定されるようになっているため、id 属性の追加は不要となります。ちなみに、ユーザー名フィールドの id 属性には id_username、パスワードフィールドの id 属性には id_password が設定されています。

ということで、forum/login.html にはフォームとテーブルに対して id の追加が必要となります。

これらに対して id 属性を追加するため、forum/login.html を下記のように変更します。今回は単純に id 属性を追加しているだけなので、特に説明は不要だと思います。

forum/login.html
{% extends "forum/base.html" %}

{% block title %}
ログイン
{% endblock %}

{% block main %}
<h1>ログイン</h1>
<form id="login-form" action="{% url 'login' %}" method="post">
{% csrf_token %}
    <table id="login-table" class="table table-hover">{{ form.as_table }}</table>
    <p><input type="submit" class="btn btn-primary" value="送信"></p>
</form>
{% endblock %}

forum/logout.html

また、JavaScript からの操作対象となる要素として、forum/base.html における「ログアウトフォーム」が挙げられます。ログインフォーム同様に、ログアウトフォームに対して送信操作が行われた時の処理を JavaScript で変更するため、ログアウトフォームに対して id 属性を追加します。ちょっと見た目では分かりにくいですが、ナビゲーションバーの ログアウト の部分はフォームになっており、ここに id 属性を必要となります。

forum/base.htmlへのid属性の追加箇所

ただし、既にログアウトフォームには下記のように id="logout-form" が既に設定されていますので、id 属性に関する変更は今回不要となります。

forum/base.html
~略~
<li class="nav-item">
    <form id="logout-form" method="post" action="{% url 'logout' %}">
        {% csrf_token %}
        <button class="btn btn-link nav-link" type="submit">ログアウト</button>
    </form>
</li>
~略~

JavaScript の設置先フォルダの作成

次は、JavaScript 側の用意を行っていきます。

まずは、JavaScript ファイルの設置先フォルダを作成していきたいと思います。

すでに、forum アプリ用の静的ファイルの設置先として forum/static/forum/css/ を作成済みですが、このフォルダはフォルダ名からも分かるように CSS ファイルの設置先となっているため、この css フォルダと同じ階層に js フォルダを作成し、作成した js フォルダに JavaScript ファイルを設置するようにしていきたいと思います。

ということで、ここで、下図のようなフォルダ構成となるように forum/static/forum/ のフォルダ内に js フォルダを作成してください。

JavaScript設置フォルダの説明図

forum/static/ 以下のファイルは開発用ウェブサーバーから静的ファイルとして配信されるようになっているため、js フォルダ以下に設置した JavaScript ファイルも、静的ファイルとして配信されることになります。

スポンサーリンク

JavaScript ファイルの作成

続いて、JavaScript ファイルを作成していきたいと思います。ここから作成するファイルは、先ほど作成した下記フォルダに設置するようにしてください。

forum/static/forum/js/

login.js

まずは、ログインページで動作させる login.js を作成していきます。

login.js は、ログインフォームからの送信操作が行われた際に下記のような処理を実施するように作成していきます。基本的には、今までログインフォームで実現していたこと+トークンの取得・記録を JavaScript で実施するように JavaScript ファイルを作成することになります。

  • ログイン API を実行する
    • (ログインとトークン取得が実施される)
  • 取得したトークンをウェブブラウザに記録する
  • ログインしたユーザーの詳細ページへリダイレクトする

あとは、エラーが発生した時にはエラーメッセージの表示も必要となります。

結論を先に示すと、ログインページで動作させる login.js の例は下記のようなものになります。

login.js
async function login() {

    // フォームから各種データを取得
    const username = document.getElementById('id_username').value;
    const password = document.getElementById('id_password').value;
    const csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]').value;

    try {
        // APIリクエストを送信
        const response = await fetch('/api/login/', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRFToken': csrfToken // CSRFトークンを送信
            },
            body: JSON.stringify({ username, password }),
        });

        // レスポンスの成功判定
        if (!response.ok) {
            // 失敗した場合はエラーメッセージを返す
            const data = await response.json();
            throw new Error(data.detail || 'ログインに失敗しました');
        }

        const data = await response.json();

        // レスポンスを処理
        if (data.token) {
            // トークンを保存
            localStorage.setItem('access_token', data.token);

            // リダイレクトの処理
            const userId = data.user_id; // ユーザーIDを取得
            const redirectUrl = `/forum/user/${userId}`; // リダイレクトURLを生成
            window.location.href = redirectUrl; // 指定のURLにリダイレクト
        } else {
            throw new Error('トークンの取得に失敗しました');
        }
    } catch (error) {
        // エラーメッセージの処理
        console.error('Login failed:', error);

        const table = document.getElementById('login-table');

        // 既存のエラーメッセージ行がある場合は削除
        const existingErrorRow = table.querySelector('.error-row');
        if (existingErrorRow) {
            table.deleteRow(existingErrorRow.rowIndex);
        }

        // 新しいエラーメッセージ行を挿入
        const tr = table.insertRow(0);
        tr.classList.add('error-row'); // エラーメッセージ用のクラスを追加
        const td = tr.insertCell(0);
        td.colSpan = 2;
        td.textContent = error.message || '原因不明のエラーが発生しました';
    }
}

function initEvent() {
    // フォームの送信イベントに対するイベントハンドラーを設定
    document.getElementById('login-form').addEventListener('submit', function(event) {
        // ボタンクリックによるフォーム送信を無効化
        event.preventDefault();

        // ログインとトークンの記録を実施
        login()
    });
}

initEvent();

上記の login.js のポイントをいくつか説明しておきます。

まず、下記で行なっているのは「通常のフォーム送信」の無効化となります。送信ボタンのクリック等によってフォーム送信が実施された際には、デフォルトでは通常のフォーム送信も実施されることになります。なので、フォーム送信によるログインと API によるログインが同時に実行されることになります。もしフォーム送信によるログインが先に完了すると、トークンをウェブブラウザに記録する前にログイン後のリダイレクトが実施される可能性があるため、フォーム送信は無効化しておく必要があります。

フォーム送信の無効化
event.preventDefault();

さらに、CSRF トークンの送信が必要となる点もポイントになります。DRF で開発した Web API においては、トークン認証を実施するようにしている場合は CSRF 検証が無効化されるようになっています。 なので、トークン認証を実施する API を実行する際には CSRF トークンの送信は不要です。

ですが、ログイン API では(ログアウト API も同様)トークン認証は実施しないため、CSRF 検証をクリアするために CSRF トークンの送信が必要となります。login.js では、CSRF トークンをフォームの csrfmiddlewaretoken フィールドから取得し、これをヘッダーの X-CSRFToken にセットして送信するようにしています。

あとは、ログインに失敗した場合、つまりユーザー名やパスワードが不正な場合には、エラーメッセージを表示するという点もポイントになります。login.js では、try に対する catch 節の中でエラーメッセージの表示を行っています。フィールドを表示するテーブルの ID を login-table に設定していますので、この要素を取得し、そのテーブルに新たな行を追加してエラーメッセージを表示するようにしています。

また、ウェブブラウザにトークンを記録させているのは下記部分になります。下記が実行されることでウェブブラウザにトークンが記録されることになりますので、他のページで動作する JavaScript からトークンを取得することが可能となります。そして、それによってトークン認証が必要となる Web API も実行することができるようになります。

トークンのウェブブラウザへの記録
localStorage.setItem('access_token', data.token);

logout.js

次は、ログアウトを実施する logout.js の作り方について解説していきます。

基本的には、login.js と処理の流れは同様で、ログアウトフォームからの送信操作が行われた際に下記のような処理が実施されるように logout.js を作成していきます。ウェブアプリで管理されているトークンはログアウト API によって削除されることになり、そのトークンでのトークン認証が通らなくなって不正な Web API の実行を防止することができるようになります。また、認証が通らないトークンをウェブブラウザに記録しておくのも無駄なので、最初にウェブブラウザからのトークンの削除も実施したいと思います。

  • 記録されているトークンをウェブブラウザから削除する
  • ログアウト API を実行する
    • (ログアウトとトークン削除が実施される)
  • ログインページへリダイレクトする

あとは、これも login.js と同様に、フォーム送信の無効化や CSRF トークンの送信が必要であるあたりがポイントになると思います。また、ウェブブラウザからのデータの削除は localStorage.removeItem によって実行可能です。

この辺りを踏まえて作成した logout.js の例は下記のようになります。ログアウトの場合はエラーが発生することも少ないので、その分コード量も少なくなっています。

logout.js
async function logout() {

    // CSRFトークンを取得
    const csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]').value;
    
    // localStorage からトークンを削除
    localStorage.removeItem('access_token');
    
    try {
        // ログアウトAPIを実行
        response = await fetch('/api/logout/', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRFToken': csrfToken // CSRFトークンの送信
            },
        });

        // ログインページにリダイレクト
        window.location.href = '/forum/login/';
    } catch (error) {
        console.error('ログアウト中にエラーが発生しました:', error);
    }
}

function initEvent() {
    // フォームの送信イベントに対してイベントハンドラーを設定
    document.getElementById('logout-form').addEventListener('submit', function(event) {
        // ボタンクリックによるフォーム送信を無効化
        event.preventDefault();

        // ログアウトとトークンの削除を実施
        logout()
    });
}

initEvent();

comment_count.js

JavaScript ファイルの最後の1つとして、コメントの総数を動的更新する comment_count.js を作成していきます。

ここまでの JavaScript では、フォームからの送信操作が実施された際に API の実行等の処理が行われるにしてきましたが、comment_count.js の場合は定期的に API 実行等の処理を行うように作成していきます。

具体的には、下記のような処理が定期的に実行されるように実装していきます。

  • 記録されているトークンをウェブブラウザから取得する
  • コメント一覧取得 API を実行する
  • 取得したコメントの件数でコメント総数表示箇所(ID が comment-count の要素)を更新する

今回実行する「コメント一覧取得 API」ではトークン認証を実施するため、CSRF トークンの送信は不要となります。ですが、トークン認証を通すためのトークン、すなわちログイン時にウェブブラウザに記録させたトークンを代わりに送信する必要があります。また、ウェブブラウザからのデータの取得は localStorage.getItem によって実行可能です。

この辺りを踏まえて作成した comment_count.js の例は下記のようになります。2秒間隔で、コメントの総数を更新するようにしています。

comment_count.js
async function updateCommentCount() {
    try {

        // localStorageからトークンを取得
        const token = localStorage.getItem('access_token');
        if (!token) {
            console.error('再度ログインを実施してください');
            return;
        }

        // APIリクエストを送信してコメントリストを取得
        const response = await fetch('/api/comments/', {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Token ${token}`, // トークンをヘッダーに追加
            },
        });

        if (response.ok) {
            const data = await response.json();
            const commentCount = data.length; // コメントの総数を取得

            // id="comment-count" の要素の値を更新
            const commentCountElement = document.getElementById('comment-count');
            commentCountElement.textContent = commentCount;
        } else {
            console.error('APIの実行に失敗しました:', response.statusText);
        }
    } catch (error) {
        console.error('コメント総数の更新に失敗しました:', error);
    }
}

// 一定間隔でコメントの総数を更新(2秒ごと)
setInterval(updateCommentCount, 2000);

// ページロード時に一度初期更新
updateCommentCount();

テンプレートファイルへの <script> タグの追記

次は、先ほど作成した JavaScript ファイルがページ表示時に読み込まれて実行されるよう、テンプレートファイルに <script> タグを追記していきます。

基本的には、Django での JavaScript の扱い方 で説明した手順でテンプレートファイルに <script> タグを追記していくことになります。また、<script> タグは <body> セクションの最後に追記するようにしたいと思います。

ただ、掲示板アプリではテンプレートファイルの継承を行っており、<body> セクションは親テンプレートファイルで定義しているため、子テンプレート側で <body> セクションの最後に <script> タグを追記することが今のままではできません。そのため、まず親テンプレートファイルに「<script> タグ追記用のブロック」を <body> セクションの最後に追加し、子テンプレートファイル側でブロックの定義を行い、そこに <script> タグを追記するようにしていきたいと思います。

scriptタグ記述用のブロックを追加する様子

これにより、子テンプレートファイル側に記述した <script> タグも <body> セクションの最後に配置されるようになります。

テンプレートファイルの継承に関しては下記ページで解説していますので、詳しく知りたい方は下記ページを参照していただければと思います。

Djangoのテンプレートの解説ページアイキャッチ 【Django入門4】テンプレート(Template)の基本

forum/base.html

ということで、まずは親テンプレートファイルである  forum/base.html の変更を行なっていきます。

先ほど説明したように、子テンプレートファイルで追記する <script> タグが <body> セクションの最後に配置されるよう、forum/base.html<body> セクションの最後に scripts ブロックを追加します。また、ログアウトフォームは forum/base.html に存在するため、このファイルを継承する全テンプレートファイルのページから logout.js が実行できるよう、logout.js 実行用の <script> タグの追記も行いたいと思います。

具体的には、下記のように forum/base.html を変更します。

forum/base.html
{% load static %}
<!doctype html>
<html lang="ja">
<head>
    〜略〜
</head>
<body>
    〜略〜
    <footer>
        <div class="logo">
            <img class="mini" src="{% static 'img/logo.png' %}">
        </div>
    </footer>
    <script src="{% static 'forum/js/logout.js' %}"></script>
    {% block scripts %}{% endblock %}
</body>
</html>

forum/login.html

次は、子テンプレートファイル側の変更を行なっていきます。親テンプレートファイル側で <body> セクションの最後に scripts ブロックが配置されているため、後は子テンプレートファイル側で scripts ブロックを定義し、そのブロック内に <script> タグを追記すれば、その <script> タグは自動的に <body> セクションの最後に配置されることになります。

ただし、子テンプレートファイル側で JavaScript ファイル等の静的ファイルを扱う場合は、子テンプレートファイル側にも {% load static %} が必要になるという点に注意してください。

今回は、login.html で表示されるページで login.js が実行されるようにしたいため、login.html を下記のように変更すればよいことになります。

forum/login.html
{% extends "forum/base.html" %}
{% load static %}

{% block title %}
ログイン
{% endblock %}

{% block main %}
<h1>ログイン</h1>
<form id="login-form" action="{% url 'login' %}" method="post">
{% csrf_token %}
    <table id="login-table" class="table table-hover">{{ form.as_table }}</table>
    <p><input type="submit" class="btn btn-primary" value="送信"></p>
</form>
{% endblock %}

{% block scripts %}
<script src="{% static 'forum/js/login.js' %}"></script>
{% endblock %}

forum/comments.html

後は、先ほどと同様の手順により、forum/comments.htmlcomment_count.js が実行されるようにすればよいだけです。

具体的には、forum/comments.html を下記のように変更すればよいことになります。

forum/comments.html
{% extends "forum/base.html" %}
{% load static %}

{% block title %}
コメント一覧
{% endblock %}

{% block main %}
<h1>コメント一覧(全<span id="comment-count">{{ page_obj.paginator.count }}</span>件)</h1>
<table class="table table-hover">
    <thead>
        <tr>
            <th>本文</th><th>投稿者</th>
        </tr>
    </thead>
    <tbody>
        {% for comment in page_obj %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
            {% if comment.user is not None %}
            <td>{{ comment.user.username }}</td>
            {% else %}
            <td>不明</td>
            {% endif %}
        </tr>
        {% endfor %}
    </tbody>
</table>
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?p=1">« first</a>
            <a href="?p={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}
        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>
        {% if page_obj.has_next %}
            <a href="?p={{ page_obj.next_page_number }}">next</a>
            <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
        {% endif %}
    </span>
</div>
{% endblock %}

{% block scripts %}
<script src="{% static 'forum/js/comment_count.js' %}"></script>
{% endblock %}

ユーザー登録ページの仕様変更

ここまでの変更によって、掲示板アプリから JavaScript を扱うことができるようになり、コメント一覧ページにおけるコメント総数も動的に更新されるようになったことになります。なので、最初に示した変更内容は既に実現できたことになります。

ただし、ここまでの変更によって不整合が生じるようになってしまっているので、最後にその修正を行なっておきたいと思います。その不整合とは、ユーザー登録時のログインでトークンが取得できないという点になります。掲示板アプリではユーザー登録時に自動的にログインを実施するようになっています。下記は、ユーザー登録ページのビューである Register の現状の実装となります。

変更前のRegisterView
class Register(CreateView):
    model = User
    form_class = RegisterForm
    template_name = 'forum/register.html'

    def form_valid(self, form):   
        response = super().form_valid(form)
        user = self.object
        login(self.request, user)
        return response

    def get_success_url(self):
        return reverse('user', kwargs={'user_id':self.object.pk})

JavaScript を扱うことで、ログインページからログインが実施された際にはトークンが取得できるようになりましたが、ユーザー登録時には上記の form_valid メソッドで単にログインされるだけなのでトークンが取得できません。そのため、ユーザー登録によってログインが行われた場合、コメント一覧ページで動作する comment_count.js でトークンが取得できずにエラーが発生することになります。

もちろん、ユーザー登録時のトークン取得も実現可能ですが、ユーザー操作用の API の追加等が必要になって変更量が大きくなってしまうため、今回はユーザー登録時にログインを実施しないようにすることで、上記のような不整合を解消するようにしたいと思います。

具体的には、ユーザー登録時にログインを実施せずにログインページへリダイレクトするようにします。これは、forum/views.py で定義している Register を下記のように変更することで実現できます。

RegisterView
class Register(CreateView):
    model = User
    form_class = RegisterForm
    template_name = 'forum/register.html'
    success_url = reverse_lazy('login')

少し使い勝手が悪くなるのですが、トークンが取得できずにエラーが発生するよりはマシなので、今回はこの解決策で対応させていただきたいと思います。

スポンサーリンク

動作確認

最後に動作確認を実施していきましょう!

動作確認前の準備

今までの Django 入門 の連載で開発してきた掲示板アプリの動作確認を行ったことがある方であれば、動作確認前に必要となる準備は下記コマンドの実行による開発用ウェブサーバーの起動のみとなります(manage.py が存在するフォルダで実行してください)。

% python manage.py runserver

掲示板アプリの動作確認を行ったことのない方は、上記のコマンド実行前にマイグレーションが必要となります。マイグレーションに関しては下記の2つのコマンドで実施可能です。

% python manage.py makemigrations
% python manage.py migrate

また、ここからの動作確認では登録済みのユーザーのユーザー名とパスワードが必要になります。まだユーザーを登録していない方や、ユーザー名やパスワードを忘れてしまったという方は、開発用ウェブサーバー起動後に(もちろんマイグレーションも必要)、下記 URL をウェブブラウザで開き、ユーザー登録を事前に実施しておいてください。

http://localhost:8000/forum/register/

ログインの動作確認

続いて、ログインの動作確認を行いたいと思います。

まず、下記 URL をウェブブラウザで開いてください。

http://localhost:8000/forum/register/

そして、表示されるフォームにユーザー名とパスワードを入力し、送信 ボタンをクリックしてください。

動作確認手順の説明図1

送信 ボタンクリック後、ログインしたユーザーの詳細ページが表示されればログインの動作確認は OK です。

コメントの総数の動的更新の動作確認

次は、コメントの総数が動的に更新される様子を確認していきましょう!

まずは、ナビゲーションバーの コメント一覧 リンクをクリックしてコメントの一覧ページを開いてください。コメントの一覧ページでは、コメントの総数が表示されているはずです。まずは、この数を覚えておいてください。

動作確認手順の説明図2

続いて、コメント一覧のページを表示したまま、今度は同じウェブブラウザでタブをもう1つ開き、そのタブで下記 URL を開いてください。

http://localhost:8000/forum/post/

この URL を開くとコメントの新規登録フォームが表示されますので、ここでコメントの投稿を行なってください。

動作確認手順の説明図3

その後、もともと開いていたタブを再度表示してみてください。そのタブではコメント一覧のページが表示されているはずで、コメントの総数が先ほどに比べて1増えていることが確認できるはずです(まだ増えていない場合は最大2秒ほど待ってみてください)。

動作確認手順の説明図4

これが確認できれば、コメントの総数の動的更新が実現できていることになります!

今までの掲示板アプリでは、一度ページを表示すると、そのページの内容は手動でウェブブラウザの更新操作が行われないと変化することはありませんでした。ですが、JavaScript を扱うようになったことで、ページの動的更新が実現できるようになったことになります。

JavaScriptを扱うようにしたことで操作なしでの更新が実現されるようになったことを示す図

今回はコメントの総数のみを更新するという非常に地味な例となりますが、同様の手順で様々なデータを更新することができるようになりますので、応用していただくことで魅力的なウェブアプリの開発に役立てることができると思います。

ログアウトの動作確認

最後に、ログアウトの動作確認を実施しておきましょう!

先ほど2つ目のタブでコメントの投稿を実施したと思いますが、その2つ目のタブで、ナビゲーションバーの ログアウト リンク(実際にはボタン)をクリックしてください。そして、クリックすることでログインページに自動的にリダイレクトされ、さらにナビゲーションバーの コメント一覧 リンクをクリックしてもコメント一覧ページが表示されず、ログインページにリダイレクトされることができれば、ログアウトが正常に実施できていることが確認できたことになります。

次は、先ほどコメントの一覧ページを表示していたタブに再度表示し、検証ツールや開発者ツールでデバッグする で紹介した検証ツール or 開発者ツールを起動してみてください。すると、コンソールにエラーが出力されていることが確認できると思います(コンソールが表示されていない場合は esc キーを押して表示してください)。

コンソールにエラーが出力されている様子

これは、ウェブブラウザからのトークンの取得に失敗したために出力されているエラーになります。つまり、このエラーの出力によって、ログアウトの実施でウェブブラウザに記録されたトークンの削除も正常に実行されていることが確認できたことになります。

今回は、簡単な例としたかったため、トークンの取得に失敗した場合には単にコンソールにエラーを出力する処理のみを実施していますが、ログインが必要であることをページに表示してユーザーに通知したり、自動的にログインページに遷移したりするような処理を実施することで、より使いやすいウェブアプリに仕立てることができると思います!

まとめ

このページでは、Django で開発するウェブアプリでの JavaScript の扱い方について解説しました!

JavaScript を扱うようにすることで、よりインタラクティブで見た目の良いウェブアプリを開発することができるようになります。具体的には、ページ内の要素を動的に追加・削除・更新するようなことも可能となりますし、複雑なアニメーションを実現したり、イベント処理を追加するようなことも可能です。

基本的には、JavaScript は他の静的ファイルと同様の手順で扱うことが可能です。ただし、JavaScript を実装しやすくするためにテンプレートファイルのタグに id 属性や class 属性を追加したり、JavaScript からデータベース操作が実施できるように Web API を開発しておくようなことも必要となります。

特に、ウェブアプリの見た目・使いやすさ・ユーザー体験を重視したい場合は JavaScript の利用が必須となりますので、是非このページで解説した JavaScript の扱い方については理解しておきましょう!

同じカテゴリのページ一覧を表示