このページでは、Django で開発するウェブアプリでの「JavaScript の扱い方」について解説していきます。
これまで20回にわたって Django 入門 の連載を続けてきましたが、これらの連載の中での解説内容で実現できるのは「静的なページ」のみとなります。動作のないページしか実現できなくてガッカリされていた方もおられるかもしれません。
ですが、今回紹介する JavaScript をウェブアプリで扱うことで「動的なページ」を実現することができるようになります。具体的には、JavaScript によって、ページ内のデータの動的更新・複雑なアニメーション・新たな操作方法の提供等を実現することが可能となります。
つまり、JavaScript を利用することで、見た目・使いやすさ・ユーザー体験の優れたウェブアプリを開発することができるようになります。また、ゲームアプリなども開発できるようになります。より魅力的なウェブアプリを開発するためには JavaScript の利用が必須となりますので、是非このページで Django で開発するウェブアプリでの JavaScript の扱い方について学んでいただければと思います!
このページでは、Django で開発するウェブアプリでの「JavaScript の扱い方」に焦点を当てて解説していきます
JavaScript についての説明や、簡単な JavaScript のコード例の紹介は行いますが、JavaScript でのプログラム開発に焦点を当てた解説は行わないため、詳細な JavaScript でのコードの書き方等については別途別のサイトや参考書等で学んでいただければと思います
Contents
JavaScript とは
JavaScript のことをご存知ない方もおられるかも知れませんので、まずは簡単に JavaScript について簡単に説明しておきます。
JavaScript
JavaScript とはプログラミング言語の1つです。JavaScript の最大の特徴はウェブブラウザ上で動作するという点になります。簡単に言えば、ウェブブラウザ上で動作するスクリプト(プログラム)を開発するための言語で、このスクリプトを動作させることで、ページに表示される要素を動的に変化させたり、定期的に or クリック等のイベントが発生したタイミングで特定の処理を実行したりすることが可能となります。
HTML・CSS・JavaScript
ウェブページは、HTML と CSS、さらには JavaScript の3種類の言語から構成されていることが多く、HTML や CSS と同様に、JavaScript はウェブ開発にとって非常に重要な役割を持つ言語となります。ここで、これらの言語の関連性について整理しておきます。
まず HTML は「ページの構造」や「ページに表示する要素」を作成するためのマークアップ言語となります。例えば「見出し」「箇条書き」「段落」等に対応するタグを HTML ファイルに記述しておくことで、それらの要素がページに描画されることになります。
また CSS は「ページや要素の見た目」を制御するためのスタイルシート言語となります。例えば「見出し」要素に対して「文字のサイズ・文字の色・背景色」等のスタイルを CSS ファイルに定義しておくことで、そのスタイルに応じた見た目の見出しがページに表示されるようになります。
そして、JavaScript は「ページに動的な動作」を追加するためのプログラミング言語となります。JavaScript ファイルをページ表示時にウェブブラウザから読み込ませれば、そのファイルに記述した処理をウェブブラウザが実行してくれるようになります。つまり、ページ表示時にプログラムが実行されるようになります。そして、それによって「ユーザーの操作に応じてコンテンツを変化させる」・「定期的に要素を変化させる」「外部と通信を行う」等の動的な処理を実行するページを実現することができます。
HTML や CSS だけでも一部の動的な処理、例えばフォームを作成してボタンのクリックを受け付けるようなことは実現可能ですが、これらだけで実現できる動的な処理は限られているため、基本的には動的なページを開発するためには JavaScript が必須となります。そして、動的なページを開発することで、より魅力的でユーザー体験の優れたページを実現することができます。
また、JavaScript では HTML でタグとして定義される要素を更新・追加・削除したり、各要素に適用するスタイルを変化させることで動的なページを実現することも多いです。そのため、JavaScript を扱うのであれば HTML や CSS に関しても多少の理解が必要となります。
Django での動的なページ生成との違い
ここまでの説明を聞いて、「Django で開発したウェブアプリでも動的なページ生成が行われるんじゃないの?」と疑問に思った方もおられるかもしれません。確かに Django で開発したウェブアプリでは、テンプレートの仕組みを利用してデータベースのレコード等に応じたページを動的に生成することができます。ですが、ここで生成されるのは、あくまでも静的なページとなります。つまり、動的に「静的なページ」が生成されるだけです。なので、一度ウェブブラウザに表示された後は、ページの一部が自動的に更新されたりする等、動的にページが変化することはありません。
それに対し、JavaScript をウェブアプリから扱うようにすることで、ウェブブラウザに表示された後に、動的にページを変化させるようなことが可能となります。つまり、ページ自体が動的なものになります。
こういった違いがあるので、これらの動的の意味合いについては混同しないように注意してください。そして、動的の意味合いが異なるため、Django で開発したウェブアプリで JavaScript を扱うようにすることで、今まで実現できなかった新たな機能や UI を実現することが可能となります。
スポンサーリンク
Django ではフロントエンド開発に利用
また、元々この JavaScript は、主にウェブブラウザ上に表示されるページに動的な処理を追加するために利用されるプログラミング言語でした。つまり、ウェブアプリ開発においては、主にフロントエンドの開発で利用されるプログラミング言語でした。ですが、最近ではバックエンドの開発でも JavaScript が利用されるようになってきています。
ただ、この Django 入門 は、タイトル通り Django でのウェブアプリ開発の解説を行う記事となります。そして、Django でウェブアプリを開発する場合は、少なくともバックエンドは全て Django で開発することになります。このページでも、ウェブアプリは基本的には Django で開発し、フロントエンドの一部、つまりページに動的な処理を追加することを目的に JavaScript を利用することを前提として説明していきますので、この点はご了承ください。
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 を利用することで、視覚的にリッチな UI が実現可能となります。簡単なアニメーションであれば CSS だけでも実現できますが、複雑なアニメーションを実現したい場合は JavaScript の利用が必要となります。
使いやすい UI が実現可能
また、JavaScript を利用することで、直感的で使いやすい UI を実現することもできます。
例えば、フォームのフィールドへの入力値が変化するたびにリアルタイムで値の妥当性の検証を実施し、エラーがあればエラーメッセージを表示したり、さらに入力を補完するようなことが JavaScript の利用で実現できます。これにより、ユーザーは入力値の妥当性を確認しながらフィールドへの入力を実施することができるようになり、使いやすさが向上します。
さらに、JavaScript を利用することで、独自の操作方法を追加することも可能です。例えば、キーボードでの上下左右キーの入力を受け付け、入力されたキーに応じてページ内の要素を移動させるようなこともできます。また、前述でも紹介したドラッグ&ドロップ操作を受け付けるようにすることもできます。このように、JavaScript を利用することで、ユーザーに独自の操作方法を提供し、より使いやすい UI を実現することができます。
動的なデータ更新が実現可能
JavaScript を利用することで、ページの更新操作なしでの動的なデータ更新も実現できるようなります。
例えば、データベースのテーブルのレコード数は、各ユーザーからの新規登録操作や削除操作に応じて変化することになります。ですが、JavaScript を利用しない場合、ページに表示されるレコード数は、手動でページの更新操作を行わないと更新されることはありません。それに対し、JavaScript を利用する場合は、テーブルのレコード数に応じて、ページに表示されるレコード数を動的に変化させることが可能となります。つまり、ページの更新操作なしでページ内のデータが動的に更新されるようになります。
この例であれば、JavaScript からウェブアプリ(バックエンド側)にレコード数を定期的に問い合わせ、その問い合わせ結果に応じてページに表示されるレコード数を変化させるようにすれば、動的な更新が実現できることになります。
また、JavaScript を利用することで無限スクロールを実現することもできます。無限スクロールとは、レコードの一覧表示時に、スクロールに応じて追加のデータが自動で読み込まれて表示されるようなスクロール技術のことを言います。この無限スクロールを採用することで、ユーザーがページを遷移・リロードすることなく、データを続けて表示することが可能になります。
ただし、ウェブブラウザ上で動作する JavaScript からは直接データベース操作を行ってレコードを取得するようなことはできません。データベース操作を行うのは、あくまでもウェブアプリ(バックエンド)となります。そのため、上記のような動的更新を実現するためには「JavaScript からレコードを取得する手段」が必要となります。そして、この手段として採用されることが多いのが、前々回及び前回の連載で解説した Web API となります。
例えばレコード一覧取得 API がウェブアプリ(バックエンド)で公開されていれば、それを利用して間接的にレコードを取得することができるようになり、取得した結果を利用した処理を Web API 側で実現することができるようになります。もちろん、上記で示したデータの動的更新や無限スクロールも実現できるようになります。
このように、JavaScript からデータベース操作を行うためには Web API が必要となり、ウェブアプリで JavaScript を扱う場合は Web API がより重要になります。
スポンサーリンク
動的な要素の追加・削除も可能
また、JavaScript を利用することで、先ほど説明したような要素の更新だけでなく、ページに表示する要素を追加したり削除したりすることも可能となります。なので、ユーザーの操作に応じて画像を追加して表示したり、表示されている段落を削除するようなことも可能です。
さらに、JavaScript でページそのものを生成するようなことも可能です。HTML にはヘッダーや JavaScript を実行するためのタグのみを記述し、あとは JavaScript でページ内の全要素を追加するような処理を実行するようにすれば、ページそのものが JavaScript から生成されることになります。
フロントエンド開発とバックエンド開発の分離
JavaScript でページそのものを生成することも可能ですので、JavaScript の実装方法によっては、フロントエンドを完全に JavaScript で開発することができるようになります。
この場合、Django でのフロントエンド開発、特にページ表示のためのビューやテンプレートファイルの開発が不要となり、Django はバックエンド機能(例えばデータベース操作や Web API の提供)の開発に専念できます。これにより、フロントエンド開発とバックエンド開発を分業して、並行して作業することが容易になります。また、フロントエンドの開発者は Python や Django の知識がなくても JavaScript や HTML・CSS だけで開発が可能になり、その逆も同様に、バックエンド開発者は JavaScript 等に関する知識なしで開発することが可能となります。このように、各専門領域に集中しやすい体制を整えることもできます。
つまり、JavaScript をウェブアプリで扱うようにすることは、ウェブアプリ全体の開発効率の向上にも繋がります。
で、このようにフロントエンド開発とバックエンド開発を分離させた場合に重要となるのが、 動的なデータ更新が実現可能 でも紹介した Web API となります。
今までは、データベースから取得したデータを埋め込んだページをバックエンド側で生成していたので、フロントエンド側ではデータベースの操作は不要でした。ですが、バックエンド開発とフロントエンド開発を切り離し、JavaScript でフロントエンドを開発する場合は、Web API を利用して JavaScript からのデータベース操作実施し、必要なレコードを取得して要素としてページに追加するような処理が必要となります。そして、この時に Web API が必要となります。
ということで、フロントエンドとバックエンドの開発を分離する意味合いでも Web API が重要となります。
今回は、ページ全体を JavaScript で生成するようなことまでは行わず、テンプレートファイルから生成されたページの一部の要素を JavaScript から操作する例を用いて解説を行っていきますが、上記の通り、フロントエンド全体を JavaScript で開発するようなことも可能であることは是非覚えておいてください。
Django での JavaScript の扱い方
ここからは、Django で開発したウェブアプリでの JavaScript の扱い方について解説していきます。
基本的には、これまでの Django 入門 で解説してきた内容を理解していれば JavaScript は難なく扱うことができると思います。ただ、忘れてしまったことも多いと思いますので、ここで JavaScript の扱い方について再度学んでいきましょう!
スポンサーリンク
静的ファイルとして配信する
まず、Django で開発したウェブアプリでは、JavaScript は静的ファイルとして扱うことになります。動的なページを実現するためのファイルではあるのですが、JavaScript のファイル自体は動的に変化するようなことはなく、いつ誰が取得しても同じファイルが得られるようになっているものなので、静的ファイルとして扱うことになります。
そして、静的ファイルをウェブアプリで扱うためには、ウェブブラウザが静的ファイルを取得できるよう、ウェブサーバーからの静的ファイルの配信が必要となります。なので、この配信のための準備が必要となります。といっても、開発用ウェブサーバーを使う場合は、特定のフォルダに静的ファイルを設置しておくことで、静的ファイルの配信が実現できます。
この静的ファイルの扱い方に関しては下記のページで解説済みですので、静的ファイルの扱い方の詳細を知りたい方は、別途下記ページを読んでいただくことをオススメします。
【Django入門17】静的ファイルの扱い方(画像・JS・CSS)JavaScript はテンプレートファイル内に記述して扱うことも可能です
ですが、メンテナンス性等を考えると JavaScript はテンプレートファイル内ではなく、個別のファイルとして用意するのが一般的です
そのため、ここでも JavaScript は個別のファイルに開発し、それを静的ファイルとして扱うことを前提に解説を進めます
テンプレートファイルに <script>
タグを追記する
また、その配信される JavaScript ファイルがウェブブラウザから実行されるようにするためには、それを指示するためのタグをテンプレートファイルに記述しておく必要があります。この JavaScript の実行を指示するタグは <script>
になります。つまり、テンプレートファイルに <script>
タグを記述しておく必要があります。これにより、ウェブブラウザが HTML 内に <script>
タグを見つけた際に、src
属性で指定された JavaScript の取得と実行を実施してくれます。
さらに、先ほど紹介したページでも解説している通り、<script>
タグ のsrc
属性には下記のように static
テンプレートタグを利用して取得するファイルのパスを指定する必要があります。また、static
テンプレートタグを利用するためには、その static
テンプレートタグの記述位置よりも上の位置に {% load static %}
を記述しておく必要があります。
{% load static %}
<script src="{% static '取得するファイルのパス' %}"></script>
<script>
タグの記述位置によって JavaScript の実行タイミングが異なることになるので注意してください。<head>
セクション内に <script>
タグを記述することも可能なのですが、この場合、ページ内の要素が作成されないうちに JavaScript が実行される可能性があります。そのため、要素に対して操作を行うような JavaScript である場合、その操作を実施する際にエラーが発生する可能性があります。
全ての要素が作成されてから JavaScript を実行させた方が無難なことも多いので、早いタイミングで JavaScript を読み込みたい場合を除けば、<head>
セクションではなく <body>
セクションの最後に <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 を公開しておけば、その Web API を利用して JavaScript からデータベースを操作することができるようになります。そして、このデータベース操作を JavaScript から実施できるようにすることで、JavaScript で実現することのできる機能の幅が広がります。
例えば 動的なデータ更新が実現可能 で紹介したような「無限スクロール」は、ページがスクロールされるたびに次のレコードをデータベースから取得する処理が必要となります。これは JavaScript 単体では実現できませんが、ウェブアプリが「追加でレコードを取得する API」を公開していれば実現可能です。具体的には、ページがスクロールされるたびに、その API を JavaScript から実行し、取得したレコードをページに追加表示することで実現できます。
このように、ウェブアプリの Web API を利用することで、JavaScript で実現できる機能の幅を広げることができます。つまり、JavaScript を扱うウェブアプリにおいては、Web API の重要性がより高くなります。そのため、JavaScript をウェブアプリから扱うのであれば、Web API をウェブアプリに開発しておくことをオススメします。少なくとも、JavaScript で実現したい機能に必要となる Web API は開発しておきましょう!
Web API の開発に関しては下記ページで解説していますので、詳細を知りたい方は下記ページを参照していただければと思います。
【Django入門19】Web APIを開発 【Django入門20】Django REST FrameworkでのWeb APIの開発スポンサーリンク
タグに id
属性や class
属性を設定する
また、JavaScript から操作したい要素のタグには id
属性や class
属性等の目印となる属性を設定しておいた方が良いです。JavaScript ではページ内の要素を取得し、その要素に対して操作(値の取得・変更や属性の追加等)を実施することが可能です。で、その要素を取得する時には、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 操作を行う際には、タグに id 属性や class 属性を設定する でも解説したように、まずページ内から id
や class
等で要素を検索して取得し、その取得した要素のデータ属性の変更や、取得した要素からのメソッドの実行によって実施することになります。
例えば下記を実行すれば、id
属性が introduction
の要素内の文字列が変化することになります。
const elem = document.getElementById('introduction');
elem.innerText = 'このページではJavaScriptについて解説します。';
HTTP リクエストの送信
また、HTTP リクエストの送信を行うことも多いです。この HTTP リクエストの送信によって、Web API を実行することができます。
この HTTP リクエストの送信は、fetch
関数等の実行によって実施することが可能です。
下記は POST /api/login/
の HTTP リクエストを送信する例となります。fetch
関数の引数により、送信する 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 リクエストの送信の仕方についてはしっかり理解しておきましょう。
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 のデバッグにも利用可能です。
デバッグツールの使い方
Google Chrome の場合は、右クリックメニューの 検証
を選択することで検証ツールが起動します。また、Microsoft Edge の場合は、右クリックメニューの 開発者ツールで調査する
を選択することで開発者ツールが起動します。これらは名前は異なりますが、機能や使い方に関してはほとんど同じです。
JavaScript のデバッグを行う際には、まずツール起動後に ソース
タブを選択し、そのタブの左側のエクスプローラーからデバッグ対象の JavaScript ファイルを選択します。すると、右側のウィンドウに選択した JavaScript のソースコードが表示されます(事前に、デバッグ対象の JavaScript ファイルが読み込まれるページを表示しておく必要があります)。
表示したソースコードにはブレークポイントを設定することが可能です。ブレークポイントに設定したい行の「行番号の左側」をクリックすれば、その行をブレークポイントに設定することができます。
これにより、そのブレークポイントに設定された行が実行される直前で JavaScript の処理が停止するようになります。処理を停止させた後は、下図のオレンジ枠内のボタンのクリック操作で処理を1行ずつ進めることもできますし、その時点の各変数の値も右側のウィンドウ(下図の緑枠内)から確認可能です。
また、ウィンドウ下部にはコンソールが表示され、このコンソールで 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 = [
'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.py
と api/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
を開き、下記のように編集してください。
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
を変更すれば良いことになります。
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
からレコードを取得する必要があります。
他にも、comment_list
や page
からもレコードを取得することが可能ですが、今回は object_list
からレコードを取得することを前提に解説を進めます
この辺りの ListView
の詳細については下記ページで解説していますので、詳しく知りたい方は下記ページを参照してください。
comment_list.html
次に、CommentList
から利用されるテンプレートファイルを作成していきます。
先ほども説明したように、テンプレートファイルは forum/templates/forum/
に comment_list.html
というファイル名で作成しておく必要があります。
なので、まずは forum
内に templates
フォルダを作成し、さらに templates
フォルダ内に forum
フォルダを作成しましょう。その後、最後に作成した forum
フォルダ内に comment_list.html
を作成してください。
続いて、comment_list.html
を開き、下記のように変更を加えてください。本来であれば、スタイルは別途 CSS ファイルに定義するべきですが、手間が増えるので今回は 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 リクエストを受け取った時に、コメント一覧ページが表示されるようになったことになります。
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
を新規作成し、下記のようにシリアライザーを定義したいと思います。
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
を定義したいと思います。
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 側で、ページがスクロールされるたびにクエリパラメーター page
を 1
増やしながらレコードの一覧取得 API を実行するようにすれば、次に表示すべきレコード 8
件を取得し、それをページに追加して表示することができることになります。つまり、スクロールされるたびに表示されるレコードを順次追加していくような処理が実現できることになります。
api/urls.py
あとは、Web API 用のビューと URL とのマッピングを行えば Web API の完成です。
DRF を利用する場合、DefaultRouter
によって Web API 用の URL が自動的に割り当てられることになるので、この URL とビューとのマッピングも簡単に実施することができます。
ということで、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 を実行してコメントの新規登録を行なってください。新規登録するコメントは多ければ多いほど良いです。無限スクロールを機能させるためには、少なくとも9件のコメントが必要です。目安としては40件くらいコメントが登録されていると、無限スクロールの効果が確認しやすくなると思います。
ただし、コメントの投稿者やコメントの本文は同じものでも良いです。POST
ボタンをクリックしてもフォームのフィールドの値はクリアされないようになっているので、適当な文字列をフィールドに入力したあと、POST
ボタンをゆっくり連打して適当に多めのコメントを新規登録しておいてください。
コメントが登録できたら、次はページ上部の GET
ボタンをクリックしてみてください。GET /api/comments/
の Web API、すなわちコメントの一覧取得 API が実行され、その結果として得られる JSON がページに表示されるはずです。
ここで注目していただきたいのが、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 となります。
// 次に表示するページの番号を取得する関数
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
関数でポイントになるのが fetch(`/api/comments/?page=${nextPage}`)
の部分で、この fetch
関数によって、引数で指定された URL に対する HTTP リクエストの送信を実施しています。fetch
関数の引数でメソッドを指定しない場合はメソッドが GET
の HTTP リクエストが送信されることになるため、ここでは「コメントの一覧取得 API」が実行されることになります。また、nextPage
は addMoreItems
が実行されるたびにインクリメントされるようになっているため、addMoreItems
が実行されるたびに次のページに割り付けられたコメントが含まれる HTTP レスポンスが取得されることになります。
また、この 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 の実行によって取得された全コメントが表に追加されて表示されるようになります。
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.next
が null
の場合は、次のページが存在しないということになるため、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
を下記のように変更すれば良いことになります。
{% 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
に適用するスタイルを定義してください。
{% 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-in
が class
属性に指定されたタグの要素は下記のようなスタイルが適用されるため、ページ外の右側に透明な状態で表示されることになります。
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
に下記の太字部分の処理を追加することで実現できます。
// 次に表示するページの番号を取得する関数
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 を利用することで開発可能なウェブアプリの幅が大きく広がることは実感していただけたのではないかと思います。
もし、スライドインが上手く行われないという方は、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 から定期的に「コメントの一覧取得 API (前回の連載で開発した Web API)」を実行して全コメントを取得し、そのコメントの総数でページに表示されるコメントの総数を動的に更新していくようにしていきます。
JavaScript を扱うウェブアプリの開発例 で示した無限スクロールの例に比べて難易度が低くなった気もするかもしれませんが、実はそうではありません。前回の連載では、コメントを操作する各種 Web API はトークン認証を実施するように開発しました。そのため、今回利用する「コメントの一覧取得 API」を実行する時にもトークンが必要となります。
これは、単にコメント一覧ページでトークンを取得し、それをコメントの一覧取得 API を実行時に送信するようにすれば良いだけのようにも思えます。確かにその通りなのですが、トークンを取得するためにはユーザー名とパスワードが必要なので、コメント一覧ページにフォームを表示し、ユーザーからユーザー名とパスワードを入力してもらうようなことが必要となることになります。特に、この掲示板アプリを利用するためにはログインも必要なので、ログインだけでなくトークン取得のためにもユーザー名・パスワードの入力が必要となり、使い勝手が悪くなってしまいます…。
このように、コメント一覧ページで動作する JavaScript がコメントの一覧取得 API を実行できるようにするためには、まずトークンの取得が必要となり、そのトークンの取得の実現が難しいです。特に使い勝手を下げずに実現するのが難しいです。
使い勝手を下げずにトークンの取得を実施できるように、今回は、下記のようにしてコメント一覧ページで動作する JavaScript でトークンを取得できるようにしていきたいと思います。
- ログインとトークン発行を実施する「ログイン API」を追加する
- ログインページで JavaScript を実行するようにし、JavaScript で下記の処理を行う
- ログインフォームで
送信
ボタンがクリックされた時にログイン API を実行してログインとトークン取得を実施 - 取得したトークンをウェブブラウザに記録
- ログインフォームで
- コメント一覧ページで実行する JavaScript でウェブブラウザから記録されたトークンを取得
つまり、ウェブブラウザにトークンを記録させることで、ログインページでログインと同時に取得したトークンをコメント一覧ページで使い回せるようにウェブアプリ(の JavaScript)を開発していきます。これにより、ユーザー名・パスワードの入力がログイン時のみで済むようになるため、使い勝手を低下させることなくコメントの一覧取得 API の実行およびコメントの総数の動的更新を実現することができます。
これを実現するためには、JavaScript の実装も必要となりますし、下記のようなログイン API・ログアウト API も必要となります。
- ログイン API:ログインとトークン発行を実施する API
- ログアウト API;ログアウトとトークン削除を実施する API
ということで、ここから上記のような API の追加や JavaScript の実装等を行って、コメントの総数の動的更新を実現していきたいと思います。
ログイン成功後にログイン状態を維持するためには、ウェブアプリから発行されたセッション ID をウェブブラウザに記録させておくことも必要となります
ただし、ログイン成功時にウェブアプリから発行されたセッション ID は、自動的にウェブブラウザに保存されるようになっているため、セッション ID を記録するための処理は JavaScript に無くても問題ありません
スポンサーリンク
ログイン API とログアウト API の追加
最初に、ログイン API とログアウト API を追加していきます。
ログイン API ではログインだけでなくトークン発行も実施し、ログアウト API ではログアウトだけでなくトークン削除も実施するように API を開発していきます。
api/views.py
の変更
まずは、api/views.py
を変更し、ログイン API とログアウト API のビューを開発していきます。
前回の連載となる下記ページでは、Comment
の操作を行う API のビューを ModelViewSet
を継承して開発しました。
ただし、ModelViewSet
は名前の通り、モデルクラスのテーブルの操作を行う API のビューを開発するために利用するクラスであり、ログインやログアウトを実現する API のビューを開発するのには向いていません。
そのため、これらの API のビューは、APIView
という、API のビュー全般で利用可能である汎用的なクラスを継承して開発していきたいと思います。この APIView
に関しても DRF で定義されるクラスとなります。
ということで、api/views.py
を下記のように変更し、ログイン API のビューとなる LoginAPI
とログアウト API のビューとなる LogoutAPI
を定義します。
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
の部分を除けば、後は LoginAPI
も LogoutAPI
も割とシンプルな作りになっていて、コードを読めば何をしているかは大体理解できるのではないかと思います。
ポイントは 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 とビューとをマッピングすることで実現できます。
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
属性を追加していきます。
この総数の値の箇所に対して id
属性を追加するため、forum/comments.html
を下記のように変更します。{{ page_obj.paginator.count }}
がコメント総数を表示する変数となっているため、これを span
タグで囲い、さらにその span
タグに id
属性を設定するようにしています。これにより、コメント総数の要素の ID が comment-count
に設定されることになります。
{% 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
属性を追加していきます。
今回の変更内容を実現するため、ログインフォームに対して送信操作が行われた際に、そのままフォームを送信するのではなく、先ほど開発したログイン API を実行するように変更します。ログインフォームに対して送信操作が行われた時の処理を JavaScript で変更するため、ログインフォームに対して id
属性を追加します。また、ログインに失敗した際にユーザーに失敗したことを通知するためのメッセージを「フィールドの表示テーブル」の最初の行に出力するようにするため、テーブルに対しても id
属性を追加します。
また、JavaScript からログイン API を実行するために、各種フィールドに入力されたデータの取得が必要となります。そのため、各種フィールドにも id
属性が必要となるのですが、これらはテンプレートの仕組みによるフォームのテンプレートファイルへの埋め込み時に自動的に id
属性が設定されるようになっているため、id
属性の追加は不要となります。ちなみに、ユーザー名フィールドの id
属性には id_username
、パスワードフィールドの id
属性には id_password
が設定されています。
ということで、forum/login.html
にはフォームとテーブルに対して id
の追加が必要となります。
これらに対して id
属性を追加するため、forum/login.html
を下記のように変更します。今回は単純に id
属性を追加しているだけなので、特に説明は不要だと思います。
{% 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
属性を必要となります。
ただし、既にログアウトフォームには下記のように id="logout-form"
が既に設定されていますので、id
属性に関する変更は今回不要となります。
~略~
<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
フォルダを作成してください。
forum/static/
以下のファイルは開発用ウェブサーバーから静的ファイルとして配信されるようになっているため、js
フォルダ以下に設置した JavaScript ファイルも、静的ファイルとして配信されることになります。
スポンサーリンク
JavaScript ファイルの作成
続いて、JavaScript ファイルを作成していきたいと思います。ここから作成するファイルは、先ほど作成した下記フォルダに設置するようにしてください。
forum/static/forum/js/
login.js
まずは、ログインページで動作させる login.js
を作成していきます。
login.js
は、ログインフォームからの送信操作が行われた際に下記のような処理を実施するように作成していきます。基本的には、今までログインフォームで実現していたこと+トークンの取得・記録を JavaScript で実施するように JavaScript ファイルを作成することになります。
- ログイン API を実行する
- (ログインとトークン取得が実施される)
- 取得したトークンをウェブブラウザに記録する
- ログインしたユーザーの詳細ページへリダイレクトする
あとは、エラーが発生した時にはエラーメッセージの表示も必要となります。
結論を先に示すと、ログインページで動作させる 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
の例は下記のようになります。ログアウトの場合はエラーが発生することも少ないので、その分コード量も少なくなっています。
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秒間隔で、コメントの総数を更新するようにしています。
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>
タグも <body>
セクションの最後に配置されるようになります。
テンプレートファイルの継承に関しては下記ページで解説していますので、詳しく知りたい方は下記ページを参照していただければと思います。
【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
を変更します。
{% 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
を下記のように変更すればよいことになります。
{% 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.html
で comment_count.js
が実行されるようにすればよいだけです。
具体的には、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
の現状の実装となります。
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
を下記のように変更することで実現できます。
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/
そして、表示されるフォームにユーザー名とパスワードを入力し、送信
ボタンをクリックしてください。
送信
ボタンクリック後、ログインしたユーザーの詳細ページが表示されればログインの動作確認は OK です。
コメントの総数の動的更新の動作確認
次は、コメントの総数が動的に更新される様子を確認していきましょう!
まずは、ナビゲーションバーの コメント一覧
リンクをクリックしてコメントの一覧ページを開いてください。コメントの一覧ページでは、コメントの総数が表示されているはずです。まずは、この数を覚えておいてください。
続いて、コメント一覧のページを表示したまま、今度は同じウェブブラウザでタブをもう1つ開き、そのタブで下記 URL を開いてください。
http://localhost:8000/forum/post/
この URL を開くとコメントの新規登録フォームが表示されますので、ここでコメントの投稿を行なってください。
その後、もともと開いていたタブを再度表示してみてください。そのタブではコメント一覧のページが表示されているはずで、コメントの総数が先ほどに比べて1増えていることが確認できるはずです(まだ増えていない場合は最大2秒ほど待ってみてください)。
これが確認できれば、コメントの総数の動的更新が実現できていることになります!
今までの掲示板アプリでは、一度ページを表示すると、そのページの内容は手動でウェブブラウザの更新操作が行われないと変化することはありませんでした。ですが、JavaScript を扱うようになったことで、ページの動的更新が実現できるようになったことになります。
今回はコメントの総数のみを更新するという非常に地味な例となりますが、同様の手順で様々なデータを更新することができるようになりますので、応用していただくことで魅力的なウェブアプリの開発に役立てることができると思います。
ログアウトの動作確認
最後に、ログアウトの動作確認を実施しておきましょう!
先ほど2つ目のタブでコメントの投稿を実施したと思いますが、その2つ目のタブで、ナビゲーションバーの ログアウト
リンク(実際にはボタン)をクリックしてください。そして、クリックすることでログインページに自動的にリダイレクトされ、さらにナビゲーションバーの コメント一覧
リンクをクリックしてもコメント一覧ページが表示されず、ログインページにリダイレクトされることができれば、ログアウトが正常に実施できていることが確認できたことになります。
次は、先ほどコメントの一覧ページを表示していたタブに再度表示し、検証ツールや開発者ツールでデバッグする で紹介した検証ツール or 開発者ツールを起動してみてください。すると、コンソールにエラーが出力されていることが確認できると思います(コンソールが表示されていない場合は esc
キーを押して表示してください)。
これは、ウェブブラウザからのトークンの取得に失敗したために出力されているエラーになります。つまり、このエラーの出力によって、ログアウトの実施でウェブブラウザに記録されたトークンの削除も正常に実行されていることが確認できたことになります。
今回は、簡単な例としたかったため、トークンの取得に失敗した場合には単にコンソールにエラーを出力する処理のみを実施していますが、ログインが必要であることをページに表示してユーザーに通知したり、自動的にログインページに遷移したりするような処理を実施することで、より使いやすいウェブアプリに仕立てることができると思います!
まとめ
このページでは、Django で開発するウェブアプリでの JavaScript の扱い方について解説しました!
JavaScript を扱うようにすることで、よりインタラクティブで見た目の良いウェブアプリを開発することができるようになります。具体的には、ページ内の要素を動的に追加・削除・更新するようなことも可能となりますし、複雑なアニメーションを実現したり、イベント処理を追加するようなことも可能です。
基本的には、JavaScript は他の静的ファイルと同様の手順で扱うことが可能です。ただし、JavaScript を実装しやすくするためにテンプレートファイルのタグに id
属性や class
属性を追加したり、JavaScript からデータベース操作が実施できるように Web API を開発しておくようなことも必要となります。
特に、ウェブアプリの見た目・使いやすさ・ユーザー体験を重視したい場合は JavaScript の利用が必須となりますので、是非このページで解説した JavaScript の扱い方については理解しておきましょう!