Notes

Webプログラミング

  • 今回は、サーバ上で実行するコードを記述することで、より高度なWebアプリケーションを作成します。
  • 前回、CS50 IDEのhttp-serverWebサーバとして使用しました。これは、接続と要求をリッスンし、Webページやその他のリソースで応答するプログラムです。
  • HTTPリクエストには、次のようなヘッダーがあります。
GET / HTTP/1.1
...
  • これらのヘッダは、ファイルやページを要求したり、ブラウザからサーバにデータを送り返したりできます。
  • http-serverは静的ページでのみ応答しますが、GET /search?q=cats HTTP/1.1のように、リクエストヘッダーを解析または分析する他のWebサーバーを使用し、ページを動的に返すことができます。

Flask

  • PythonとFlaskというライブラリを使用して、独自のWebサーバを作成し、追加機能を実装します。Flaskはフレームワークでもあり、コードのライブラリには、それをどのように使うべきかについての一連の規約も含まれています。たとえば、他のライブラリと同様に、Flaskにはリクエストを個別に解析するために使用できる関数が含まれていますが、フレームワークとして、プログラムのコードを特定の方法(フレームワークのルールに則った方法)で組む必要があります。
application.py
requirements.txt
static/
templates/
  • application.pyには、Webサーバ用のPythonコードがあります。
    • requirements.txtには、このアプリケーションに必要なライブラリのリストが含まれています。
    • staticは、CSSやJavaScriptファイルなどの静的ファイルのディレクトリです。
    • templates/は、最終的なHTMLの作成に使用されるファイルのディレクトリです。
  • 一般的な言語ごとに多くのWebサーバフレームワークがありますが、Flaskは今日私たちが使用する代表的なものと言えるでしょう。
  • Flaskはまた、特定のデザインパターン、つまりプログラムやコードの構成方法を実装します。Flaskの場合、デザインパターンは一般的にMVC (Model-view-controller) と呼ばれるものです。
  • コントローラ (controller) は、ユーザ入力を受けてアプリケーション全体を管理するロジックとコードです。Flaskでは、これがPythonコードになります。
    • ビュー (view) は、ユーザが表示して操作するHTMLやCSSなどのユーザ・インターフェースです。
    • モデル (model) は、SQLデータベースやCSVファイルなど、アプリケーションのデータです。
  • 最も単純なFlaskアプリケーションは次のようになります。application.pyというファイル名で保存しましょう。
from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "hello, world"
  • まず、flaskライブラリからFlaskをインポートします。Flaskライブラリでは、先頭大文字のメイン名が使用されています。
    • 次に、Flask変数にファイルの名前を指定して、app変数を作成します。
    • 次に、ルート/、URLとURL用の関数を@app.routeを使ってラベル付けします。Pythonの@マークはデコレーターと呼ばれ、ある関数を別の関数に紐付けするためのものです。
    • デフォルトページである/の要求に応答するため、@app.routeで紐付けられた関数indexを呼び出します。index関数は文字列を応答するだけです。
  • CS50 IDEでサーバーを起動するには、アプリケーションコードapplication.pyを含むディレクトリに移動し、flask runと入力して起動します。URLが表示されるので、そのURLを開いてhello, worldが表示されていることを確認します。
  • render_template関数を使用してHTMLを実際に返すようにコードを更新します。この関数は、指定されたファイルを検索し、その内容を返します。
from flask import Flask, render_template

app = Flask(__name__)


@app.route("/")
def index():
    return render_template("index.html")
  • templates/ディレクトリを作成し、その中にコンテンツを含むindex.htmlファイルを作成する必要があります。
    • flask runと入力すると、サーバのURLにアクセスしたときにHTMLファイルが返されます。
  • コントローラのコードに書いているrender_templateに引数を渡してみましょう。
from flask import Flask, render_template, request

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html", name=request.args.get("name", "world"))
  • render_templateには、nameのような任意の名前付き引数を与えることができ、テンプレートやHTMLファイルではプレースホルダで置き換えられます。
    • index.htmlでは、hello, worldhelloに置き換えて、Flaskに変数nameの置き換え先を指示します。
<!DOCTYPE html>

<html lang="en">
    <head>
        <title>hello</title>
    </head>
    <body>
        hello, {{ name }}
    </body>
</html>
  • Flaskライブラリのrequest変数を使用して、HTTPリクエストからパラメータ (この場合はnameも) を取得し、パラメータが指定されていない場合はデフォルトのworldに戻すことができます。
    • これらの変更を行った後でサーバを再起動し、デフォルトページのURLに/?name=Davidなどを加えてアクセスすることで、サーバで生成されたHTMLに引数(name=Davidの部分)と同じ文字列が埋め込まれて表示されます。
  • Googleの検索クエリである/search?q=catsも、何らかのコードによってqパラメータが解析され、関連するすべての結果を得るためにデータベースに渡されていると推測できます。そして、それらの結果をもとに、最終的なHTMLページが生成されます。

フォーム

  • 元のテンプレートをindex.htmlからgreet.htmlにファイル名を変更して、ユーザーの名前を表示します。新たに作るindex.htmlに下記のようなフォームを作成します。
<!DOCTYPE html>

<html lang="en">
    <head>
        <title>hello</title>
    </head>
    <body>
        <form action="/greet" method="get">
            <input name="name" type="text">
            <input type="submit">
        </form>
    </body>
</html>
  • @app.route("/greet")で設定したルート/greetにフォームを送信し、nameパラメータとsubmitボタンの入力項目が配置された上記のindex.htmlを作成します。
    • applications.pyコントローラでは、ルート/greet用の関数も追加する必要があります。これは、以前/に対して行ったものとほぼ同じです。
@app.route("/")
def index():
    return render_template("index.html")


@app.route("/greet")
def greet():
    return render_template("greet.html", name=request.args.get("name", "world"))
  • index.htmlのフォームは、毎回同じにすることができるので静的です。
    • これで、flask runを実行してサーバを起動し、デフォルトページ「/」のフォームを表示し、index.htmlのフォームを使ってgreet.htmlを表示することが出来ます。

POST

  • これまで作成してきた上記のフォームでは、データをURLに含むGETメソッドを使用していました。
  • HTMLのメソッドを<form action="/greet" method="post">として変更します。また、POSTメソッドを受け入れるようにapplications.pyコントローラを変更し、パラメータもrequest.argsからではなく別の場所を探す必要があります。
@app.route("/greet", methods=["POST"])
def greet():
    return render_template("greet.html", name=request.form.get("name", "world"))
  • request.argsはGETリクエストのパラメータ用ですが、POSTリクエストのパラメータにはFlaskのrequest.formを使用する必要があります。
  • これらの変更を行った後でアプリケーションを再起動すると、フォームのsubmitボタンを押すことで/greetページに移動しますが、ページ内容がURLのパラメータ(?name=Davidのようなもの)に含まれなくなります。

レイアウト

  • index.htmlgreet.htmlには、繰り返されるHTMLコードがあります。HTMLだけではファイル間でコードを共有することはできませんが、Flaskテンプレート (およびその他のWebフレームワーク) を使用すれば、このような共通のコンテンツを排除できます。
  • 別のテンプレートlayout.htmlを作成します。
<!DOCTYPE html>

<html lang="en">
    <head>
        <title>hello</title>
    </head>
    <body>
        {% block body %}{% endblock %}
    </body>
</html>
  • Flaskは、{% %}構文を使用してプレースホルダブロックや他のコードの塊・部品をインクルードするテンプレート言語であるJinjaをサポートします。ここでは、<body> 要素に含めるHTMLが含まれているブロックにbody と名前を付けました。
  • index.htmlでは、layout.htmlを配置図として使用し、bodyブロックを次のように定義します。
{% extends "layout.html" %}

{% block body %}

    <form action="/greet" method="post">
        <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
        <input type="submit">
    </form>

{% endblock %}
  • 同様に、greet.htmlでは、挨拶文だけのbodyブロックを定義しています。
{% extends "layout.html" %}

{% block body %}

    hello, {{ name }}

{% endblock %}
  • ここで、サーバを再起動し、サーバのURLを開いた後にHTMLのソースを表示する(ブラウザの表示部分で右クリックし、ページのソースを表示)と、Flaskによって生成されたHTMLファイル内にフォームを含む完全なページが表示されます。
  • GETメソッドとPOSTメソッドの両方をサポートするために、同じルートを再利用することもできます。
@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "POST":
        return render_template("greet.html", name=request.form.get("name", "world"))
    return render_template("index.html")
  • 最初に、リクエストrequestのメソッドmethodがPOSTリクエストかどうかを確認します。POSTリクエストの場合は、nameパラメータを検索し、greet.htmlテンプレートからHTMLを返します。それ以外の場合は、フォームを持つindex.htmlからHTMLを返します。
    • フォームのアクションactionもデフォルトのルート/に変更する必要があります。

Frosh IMs

  • Davidの最初のWebアプリケーションのひとつは、キャンパスの学生が校内スポーツである「frosh IM」に登録するためのものでした。
  • 以前と同様のlayout.htmlを使用します。
<!DOCTYPE html>

<html lang="en">
    <head>
        <meta name="viewport" content="initial-scale=1, width=device-width">
        <title>froshims</title>
    </head>
    <body>
        {% block body %}{% endblock %}
    </body>
</html>
  • <head>の中に<meta>タグを使用すると、ページにさらにメタデータを追加できます。この場合、ページのサイズとフォントをデバイスに自動的にスケーリングするようにブラウザに指示するために、viewportメタデータのcontent属性を追加します。
  • application.pyでは、デフォルトのルート/用のindex.htmlテンプレートを返します。
from flask import Flask, render_template, request

app = Flask(__name__)

SPORTS = [
    "Dodgeball",
    "Flag Football",
    "Soccer",
    "Volleyball",
    "Ultimate Frisbee"
]

@app.route("/")
def index():
    return render_template("index.html")
  • index.htmlテンプレートは次のようになります。
{% extends "layout.html" %}

{% block body %}
    <h1>Register</h1>

    <form action="/register" method="post">

        <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
        <select name="sport">
            <option disabled selected value="">Sport</option>
            <option value="Dodgeball">Dodgeball</option>
            <option value="Flag Football">Flag Football</option>
            <option value="Soccer">Soccer</option>
            <option value="Volleyball">Volleyball</option>
            <option value="Ultimate Frisbee">Ultimate Frisbee</option>
        </select>
        <input type="submit" value="Register">

    </form>
{% endblock %}
  • 以前のようなフォームを用意し、各スポーツのオプションを含む<select>メニューを用意します。
  • application.pyでは、/registerルートに対してPOSTを許可します。
@app.route("/register", methods=["POST"])
def register():

  if not request.form.get("name") or not request.form.get("sport"):
      return render_template("failure.html")

  return render_template("success.html")
  • フォームの値が有効であることを確認し、実際にはまだデータを処理していない場合でも、結果に応じてテンプレートを返します。
  • ただし、ユーザーはブラウザでフォームのHTMLを変更し、選択したオプションとして他のスポーツを含むリクエストを送信できます。
  • application.pyにリストを作成して、sportの値が有効であることを確認しましょう。
  • success.htmlは、https://cdn.cs50.net/2020/fall/lectures/9/src9/froshims5/templates/ にあるソースコードを使いましょう。
from flask import Flask, render_template, request

app = Flask(__name__)

SPORTS = [
    "Dodgeball",
    "Flag Football",
    "Soccer",
    "Volleyball",
    "Ultimate Frisbee"
]

@app.route("/")
def index():
    return render_template("index.html", sports=SPORTS)

...
  • そのリストをindex.htmlテンプレートに渡します。
  • テンプレートでは、ループを使用して、sportsとして渡される文字列のリストからオプションのリストを生成することもできます。
...
<select name="sport">
    <option disabled selected value="">Sport</option>
    {% for sport in sports %}
        <option value="{{ sport }}">{{ sport }}</option>
    {% endfor %}
</select>
...
  • 最後に、POSTリクエストで送信されたsportapplication.pyのSPORTSリストにあるかどうかを確認します。
...
@app.route("/register", methods=["POST"])
def register():

    if not request.form.get("name") or request.form.get("sport") not in SPORTS:
        return render_template("failure.html")

    return render_template("success.html")
  • フォームの選択メニューをチェックボックスに変更して、複数のスポーツの選択を許可できます。
{% extends "layout.html" %}

{% block body %}
    <h1>Register</h1>

    <form action="/register" method="post">

        <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
        {% for sport in sports %}
            <input name="sport" type="checkbox" value="{{ sport }}"> {{ sport }}
        {% endfor %}
        <input type="submit" value="Register">

    </form>
{% endblock %}
  • チェックボックスの値をapplication.pyで使う場合、register関数でrequest.form.getlistを呼び出して、チェックされたオプションのリストを取得します。(チェックボックスでは値が複数あるので、get()からgetlist()に書き直す必要があることに注意しましょう。)
  • ラジオボタンを使用して、一度に1つのオプションのみを選択することもできます。

データの保存

  • 登録された学生または登録者を、Webサーバのメモリ内の辞書に保存します。
from flask import Flask, redirect, render_template, request

app = Flask(__name__)

REGISTRANTS = {}

...

@app.route("/register", methods=["POST"])
def register():

    name = request.form.get("name")
    if not name:
        return render_template("error.html", message="Missing name")

    sport = request.form.get("sport")
    if not sport:
        return render_template("error.html", message="Missing sport")
    if sport not in SPORTS:
        return render_template("error.html", message="Invalid sport")

    REGISTRANTS[name] = sport

    return redirect("/registrants")
  • REGISTRANTSという辞書を作成し、registerでまず名前nameとスポーツsportをチェックし、それぞれの場合に異なるエラーメッセージを返します。そうすれば、名前とスポーツをREGISTRANTS辞書に安全に保存し、登録された学生を表示する別のルートにリダイレクトできます。
    • 一方、エラーメッセージテンプレートは単に次のメッセージを表示します。
{% extends "layout.html" %}

{% block body %}
    {{ message }}
{% endblock %}
  • 登録した学生を表示するために、/registrantsルートとテンプレートを追加してみましょう。
@app.route("/registrants")
def registrants():
    return render_template("registrants.html", registrants=REGISTRANTS)
  • ルートでは、REGISTRANTSディクショナリをregistrantsというパラメータとしてテンプレートに渡します。
{% extends "layout.html" %}

{% block body %}
    <h1>Registrants</h1>
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Sport</th>
            </tr>
        </thead>
        <tbody>
            {% for name in registrants %}
                <tr>
                    <td>{{ name }}</td>
                    <td>{{ registrants[name] }}</td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
{% endblock %}
  • テンプレートにはテーブルがあり、各キーと値の見出し行と行がregistrantsに保存されます。
  • Webサーバが停止すると、保存されているデータが失われるため、cs50のSQLライブラリでSQLiteデータベースを使用します。
from cs50 import SQL
from flask import Flask, redirect, render_template, request

app = Flask(__name__)

db = SQL("sqlite:///froshims.db")

...
  • IDEターミナルで、sqlite3 froshims.dbを実行してデータベースを開き、.schemaコマンドを使用して、事前に作成されたidname、およびsportの列を持つ表を表示できます。
  • このルートでは、SQLを使用して行を挿入および選択できます。
@app.route("/register", methods=["POST"])
def register():

    name = request.form.get("name")
    if not name:
        return render_template("error.html", message="Missing name")
    sport = request.form.get("sport")
    if not sport:
        return render_template("error.html", message="Missing sport")
    if sport not in SPORTS:
        return render_template("error.html", message="Invalid sport")

    db.execute("INSERT INTO registrants (name, sport) VALUES(?, ?)", name, sport)

    return redirect("/registrants")


@app.route("/registrants")
def registrants():
    registrants = db.execute("SELECT * FROM registrants")
    return render_template("registrants.html", registrants=registrants)
  • 要求を検証したら、INSERT INTOを使用して行を追加できます。同様に、registrants()では、すべての行をSELECTし、行のリストとしてテンプレートに渡すことができます。
  • db.executeから返される各行は辞書であるため、registrants.htmlテンプレートも調整する必要があります。したがって、registrant.nameおよびregistrant.sportを使用して、各行の各キーの値にアクセスできます。
<tbody>
    {% for registrant in registrants %}
        <tr>
            <td>{{ registrant.name }}</td>
            <td>{{ registrant.sport }}</td>
            <td>
                <form action="/deregister" method="post">
                    <input name="id" type="hidden" value="{{ registrant.id }}">
                    <input type="submit" value="Deregister">
                </form>
            </td>
        </tr>
    {% endfor %}
</tbody>
  • 別のライブラリflask_mailを使用してユーザに電子メールを送信することもできます。
import os
import re

from flask import Flask, render_template, request
from flask_mail import Mail, Message

app = Flask(__name__)
app.config["MAIL_DEFAULT_SENDER"] = os.getenv("MAIL_DEFAULT_SENDER")
app.config["MAIL_PASSWORD"] = os.getenv("MAIL_PASSWORD")
app.config["MAIL_PORT"] = 587
app.config["MAIL_SERVER"] = "smtp.gmail.com"
app.config["MAIL_USE_TLS"] = True
app.config["MAIL_USERNAME"] = os.getenv("MAIL_USERNAME")
mail = Mail(app)
  • 機密性の高い変数をコードの外部、つまりIDEの環境に設定し、コードに含めないようにします。
    • メールを送信する変数Mailに、ユーザ名、パスワード、メールサーバ (この場合はGmail) などの設定詳細を指定できます。
  • 最後に、ルートregisterでは、ユーザに電子メールを送信できます。
@app.route("/register", methods=["POST"])
def register():

    email = request.form.get("email")
    if not email:
        return render_template("error.html", message="Missing email")
    sport = request.form.get("sport")
    if not sport:
        return render_template("error.html", message="Missing sport")
    if sport not in SPORTS:
        return render_template("error.html", message="Invalid sport")

    message = Message("You are registered!", recipients=[email])
    mail.send(message)

    return render_template("success.html")
  • フォームでは、名前の代わりに電子メールも要求する必要があります。
<input autocomplete="off" name="email" placeholder="Email" type="email">
  • サーバを再起動し、フォームを使用してメールを送信すると、実際にメールが送信されます。

セッション

  • セッションとは、Webサーバが各ユーザに関する情報をサーバーに記憶する方法のことで、ユーザがログインしたままでいられるようにするなどの機能を可能にします。
  • その結果、サーバーはレスポンスの中にSet-Cookieと呼ばれる別のヘッダーを送信できることがわかりました。
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: session=value
...
  • クッキーは、ブラウザに保存するウェブサーバからの小さなデータです。多くの場合、これらは大きな乱数または文字列であり、訪問ごとにユーザを一意に識別し追跡するために使用されます。
    • この場合、サーバーは私たちのブラウザに、そのサーバー用のクッキーをsessionと言う名前でvalueという値を設定するように求めています。
  • その後、ブラウザが同じサーバに対して別のリクエストを行う時、同じサーバが以前に設定したCookieをブラウザからサーバーに送信します。
GET / HTTP/1.1
Host: gmail.com
Cookie: session=value
  • 現実の世界では、遊園地に行けばハンドスタンプを押してもらうことで、再入園することができます。同様に、私たちのブラウザは私たちのクッキをウェブサーバに返しているので、私たちが誰であるかを記憶することができます。
  • 広告会社は、多数のWebサイトからクッキーを設定して、すべてのWebサイトのユーザを追跡するでしょう。これとは対照的に、シークレット (Incognito) モードでは、ブラウザは以前に設定されたクッキーを送信しません。
  • Flaskでは、flask_sessionライブラリを使用してこれを管理できます。
from flask import Flask, redirect, render_template, request, session
from flask_session import Session

app = Flask(__name__)
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)


@app.route("/")
def index():
    if not session.get("name"):
        return redirect("/login")
    return render_template("index.html")


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        session["name"] = request.form.get("name")
        return redirect("/")
    return render_template("login.html")


@app.route("/logout")
def logout():
    session["name"] = None
    return redirect("/")
  • IDEのファイルシステムを使用するようにセッションライブラリを構成し、ユーザー名を格納するために辞書のようにセッションsessionを使用します。FlaskはHTTPクッキーを使用して、私たちのWebサーバにアクセスする各ユーザのセッション変数を維持することが分かりました。各訪問者は、コード内ではグローバルであるように見えても、各自のセッション変数を取得します。
    • デフォルトの / ルートでは、sessionでユーザーのnameが設定されていない場合は /login にリダイレクトし、それ以外の場合はデフォルトの index.html テンプレートを表示します。
    • /loginルートについては、POST経由で送信されるフォームの値をsession内のnameに設定し、デフォルトルート「/」にリダイレクトします。GET経由でルートにアクセスした場合は、login.htmlでログインフォームを表示します。
    • /logoutルートの場合、session内のnameの値をNoneに設定してクリアし、/に再度リダイレクトします。
    • また、一般的には使用したいライブラリの名前を記載したrequirements.txtが必要になりますが、ここで使用しているものはIDEにあらかじめインストールされています。
  • login.htmlには、名前のフォームだけがあります。
{% extends "layout.html" %}

{% block body %}

    <form action="/login" method="post">
        <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
        <input type="submit" value="Log In">
    </form>

{% endblock %}
  • index.htmlでは、session.nameが存在するかどうかをチェックし、異なるコンテンツを表示できます。
{% extends "layout.html" %}

{% block body %}

    {% if session.name %}
        You are logged in as {{ session.name }}. <a href="/logout">Log out</a>.
    {% else %}
        You are not logged in. <a href="/login">Log in</a>.
    {% endif %}

{% endblock %}
  • サーバーを再起動し、そのURLに移動してログインすると、 「Network」 タブにブラウザが実際にヘッダーCookie:をリクエスト時に送信していることを見ることができます。

格納、表示

  • 例としてstoreを見てみましょう。
    • application.pyは、データベースとセッションを使用するようにアプリケーションを初期化および構成します。index()では、デフォルトルートはデータベースに格納されている本(books)のリストをブラウザに描画します。
    • templates/books.htmlには、本booksのリストと、それぞれの本に対して 「Add to Cart (カートに追加)」 をクリックできるフォームが表示されます。
    • /cartルートは、POSTリクエストのidをリストのセッション変数に保存します。しかしながら、リクエストがGETメソッドであった場合、/cartルートはセッションに保存されているidのリストに一致するidを持つ本booksのリストを表示します。(セッションに「カートに追加 (Add to Cart)」ボタンが押された本のリストがあります。)
  • そのため、Webサイト上の 「ショッピングカート (shopping carts)」 は、ブラウザに保存されたクッキーとサーバに格納されたセッション変数を使用して実装できます。
  • デフォルトルート「/」で生成されたソースを表示すると、それぞれの本に独自の<form>要素があることがわかります。それぞれが異なるidを持つinputタグで、type="hidden"属性により非表示で生成されます。このidはサーバ上のSQLiteデータベースから取得され、/cartルートに送り返されます。
  • 別の例として、フロントエンド (ユーザ側) でJavaScriptを使用し、バックエンド (サーバ側)でPythonを使用する方法を示します。
  • application.pyにあるデータベースshows.dbを開きます。
  • shows.dbは、 https://cdn.cs50.net/2020/fall/lectures/9/src9/shows0/ にあります。
from cs50 import SQL
from flask import Flask, render_template, request, jsonify

app = Flask(__name__)

db = SQL("sqlite:///shows.db")


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/search")
def search():
    shows = db.execute("SELECT * FROM shows WHERE title LIKE ?", "%" + request.args.get("q") + "%")
    return render_template("search.html", shows=shows)
  • デフォルトのルート/はフォームを表示します。ここに検索語を入力します。
    • フォームはGETメソッドを使用して/searchに検索クエリを送信し、/searchはデータベースを使用して一致する番組のリストを検索します。最後に、search.htmlテンプレートが番組のリストを表示します。
  • JavaScriptを使用すると、入力時に結果の一部のリストを表示できます。まず、jsonifyという関数を使用して、JavaScriptで使用できる標準形式であるJSON形式で番組を返します。
@app.route("/search")
def search():
    shows = db.execute("SELECT * FROM shows WHERE title LIKE ?", "%" + request.args.get("q") + "%")
    return jsonify(shows)
  • 検索クエリを送信すると、辞書のリストが返されます。
  • 次に、index.htmlテンプレートでこのリストをDOMの要素に変換します。
<!DOCTYPE html>

<html lang="en">
    <head>
        <meta name="viewport" content="initial-scale=1, width=device-width">
        <title>shows</title>
    </head>
    <body>

        <input autocomplete="off" autofocus placeholder="Query" type="search">

        <ul></ul>

        <script crossorigin="anonymous" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
        <script>

            let input = document.querySelector('input');
            input.addEventListener('keyup', function() {
                $.get('/search?q=' + input.value, function(shows) {
                  let html = '';
                  for (let id in shows)
                  {
                      let title = shows[id].title;
                      html += '<li>' + title + '</li>';
                  }

                  document.querySelector('ul').innerHTML = html;
                });
            });

        </script>

    </body>
</html>
  • リクエストをより簡単に行うために、別のライブラリであるJQueryを使用します。
    • input要素の変更を監視し、$.getを使います。この関数はJQueryライブラリの関数を呼び出し、入力された値でGETリクエストを行います。そして、レスポンスは変数showsとして無名関数に渡され、レスポンスのデータに基づいて<li>要素でリストを生成してDOMに設定します。
  • $.getAJAXの呼び出しで、ページがロードされた後にJavaScriptを使って追加のHTTPリクエストを行い、より多くのデータを取得できるようにします。「Network」 タブをもう一度開くと、実際に、押した各キーが別のリクエストを行い、そのレスポンスが次のように表示されることがわかります。(ここで使われている$.getは単純なGETリクエストを送るjQueryの関数です。)
  • ネットワーク要求は遅いかもしれないので、$.getに渡す匿名関数はコールバック関数であり、サーバから応答を受け取った後にのみ呼び出されます。その間、ブラウザは他のJavaScriptコードを実行することができます。
  • 今回はここまでです!