讲座 9

欢迎!

  • 在之前的几周里,你已经学习了众多编程语言、技术和策略。
  • 事实上,这门课远不只是一门C语言课Python课,而更是一门编程课,让你能够跟上未来的技术趋势。
  • 在过去的这几周里,你已经学会了如何学习编程。
  • 今天,我们将从 HTML 和 CSS 出发,结合 HTML、CSS、SQL、Python 和 JavaScript,这样你就可以创建自己的 Web 应用程序。
  • 你可以考虑运用本周学到的技能来创建你的最终项目。

http-server

  • 在此之前,你看到的所有 HTML 都是预先编写好的静态文件。
  • 过去,当你访问一个页面时,浏览器会下载一个 HTML 页面供你查看。这些被称为静态页面,即 HTML 中编写的内容就是用户所看到的内容,并在客户端下载到他们的互联网浏览器中。
  • 动态页面则指的是 Python 及类似语言能够实时创建 HTML 的能力。因此,你可以拥有由代码基于用户的输入或行为在服务器端生成的网页。
  • 你之前使用过 http-server 来提供网页服务。今天,我们将使用一种新的服务器,它可以解析网址并根据提供的 URL 执行操作。
  • 此外,上周你看到的 URL 格式如下:

    https://www.example.com/folder/file.html
    

    注意 file.html 是位于 example.comfolder 文件夹中的一个 HTML 文件。

Flask

  • 本周,我们将介绍使用路由的能力,例如 https://www.example.com/route?key=value,通过 URL 中提供的键和值,可以在服务器上生成特定的功能。
  • Flask 是一个第三方库,允许你使用 Flask 框架(或微框架)在 Python 中托管 Web 应用程序。
  • 你可以通过在 cs50.dev 的终端窗口中执行 flask run 来运行 Flask。
  • 要做到这一点,你需要一个名为 app.py 的文件和另一个名为 requirements.txt 的文件。app.py 包含告诉 Flask 如何运行你的 Web 应用程序的代码。requirements.txt 包含运行 Flask 应用程序所需库的列表。
  • 以下是一个 requirements.txt 的示例:

    Flask
    

    注意此文件中只出现了 Flask。这是因为运行 Flask 应用程序需要 Flask。

  • 以下是一个在 app.py 中非常简单的 Flask 应用程序:

    # 通过返回文本字符串向世界问好
    
    from flask import Flask
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        return "hello, world"
    

    注意 / 路由简单地返回文本 hello, world

  • 我们也可以创建实现 HTML 的代码:

    # 通过返回 HTML 字符串向世界问好
    
    from flask import Flask
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        return <!DOCTYPE html><html lang="en"><head><title>hello</title></head><body>hello, world</body></html>
    

    注意这个方法不再返回简单的文本,而是返回 HTML。

  • 进一步改进我们的应用程序,我们还可以通过创建名为 templates 的文件夹,并在其中创建名为 index.html 的文件(代码如下)来提供基于模板的 HTML:

    <!DOCTYPE html>
    
    <html lang="en">
    
        <head>
            <title>hello</title>
        </head>
    
        <body>
            hello, world
        </body>
    
    </html>
    
    
  • 然后,在与 templates 文件夹相同的目录中,创建名为 app.py 的文件并添加以下代码:

    # 使用 request.args.get
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        name = request.args.get("name", "world")
        return render_template("index.html", name=name)
    

    注意这段代码将 app 定义为 Flask 应用程序。然后,它将 app/ 路由定义为返回 index.html 的渲染结果,并带有 name 参数。默认情况下,request.args.get 函数会查找用户提供的 name。如果没有提供名字,则会默认为 world@app.route 也称为装饰器。

  • 你可以通过在终端窗口中输入 flask run 来运行这个 Web 应用程序。如果 Flask 无法运行,请确保上述每个文件中的语法都是正确的。此外,如果 Flask 无法运行,请确保你的文件组织如下:

    /templates
        index.html
    app.py
    requirements.txt
    
  • 成功运行后,系统会提示你点击一个链接。进入该网页后,尝试在浏览器地址栏的基础 URL 后添加 ?name=[你的名字]

表单

  • 为了改进我们的程序,我们知道大多数用户不会在地址栏中输入参数。相反,程序员依靠用户在网页上填写表单。因此,我们可以如下修改 index.html

    <!DOCTYPE html>
    
    <html lang="en">
    
        <head>
            <title>hello</title>
        </head>
    
        <body>
            <form action="/greet" method="get">
                <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
                <button type="submit">Greet</button>
            </form>
        </body>
    
    </html>
    

    注意现在创建了一个表单,它接收用户的名字并将其传递给名为 /greet 的路由。autocomplete 已关闭。此外,还包含一个带有文本 nameplaceholder

  • 此外,我们可以如下修改 app.py

    # 添加表单和第二个路由
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @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"))
    

    注意默认路径会显示一个表单让用户输入他们的名字。/greet 路由会将 name 传递给该网页。

  • 为了完成这个实现,你需要在 templates 文件夹中为 greet.html 创建另一个模板,内容如下:

    <!DOCTYPE html>
    
    <html lang="en">
    
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>hello</title>
        </head>
    
        <body>
            hello, {{ name }}
        </body>
    
    </html>
    

    注意这个路由现在会向用户显示问候语,后面跟着他们的名字。

模板

  • 我们的两个网页 index.htmlgreet.html 有很多相同的数据。如果能让主体内容保持独特,同时在不同页面间复用相同的布局,岂不是很棒?
  • 首先,创建一个名为 layout.html 的新模板,并编写如下代码:

    <!DOCTYPE html>
    
    <html lang="en">
    
        <head>
            <title>hello</title>
        </head>
    
        <body>
            {% block body %}{% endblock %}
        </body>
    
    </html>
    

    注意 {% block body %}{% endblock %} 允许从其他 HTML 文件中插入代码。

  • 然后,如下修改你的 index.html

    {% extends "layout.html" %}
    
    {% block body %}
    
        <form action="/greet" method="get">
            <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
            <button type="submit">Greet</button>
        </form>
    
    {% endblock %}
    

    注意 {% extends "layout.html" %} 这行告诉服务器从何处获取此页面的布局。然后,{% block body %}{% endblock %} 指明哪些代码将被插入到 layout.html 中。

  • 最后,如下修改 greet.html

    {% extends "layout.html" %}
    
    {% block body %}
        hello, {{ name }}
    {% endblock %}
    

    注意这段代码变得更短更简洁了。

请求方法

  • 你可以想象在某些场景下使用 get 是不安全的,因为用户名和密码会显示在 URL 中。
  • 我们可以使用 post 方法来解决这个问题,修改 app.py 如下:

    # 切换为 POST
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        return render_template("index.html")
    
    
    @app.route("/greet", methods=["POST"])
    def greet():
        return render_template("greet.html", name=request.form.get("name", "world"))
    

    注意 POST 被添加到 /greet 路由中,并且我们使用了 request.form.get 而不是 request.args.get

  • 这告诉服务器在虚拟信封中深入查找,并且不在 URL 中暴露 post 中的内容。
  • 不过,这段代码还可以进一步改进,即为 getpost 使用同一个路由。为此,修改 app.py 如下:

    # 使用单一路由
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @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")
    

    注意 getpost 都在同一个路由中处理。然而,request.method 被用来根据用户请求的路由类型进行正确的分发。

  • 相应地,你可以如下修改 index.html

    {% extends "layout.html" %}
    
    {% block body %}
    
        <form action="/" method="post">
            <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
            <button type="submit">Greet</button>
        </form>
    
    {% endblock %}
    

    注意表单的 action 已更改。

  • 不过,这段代码中还有一个 bug。在我们的新实现中,当有人在表单中没有输入名字时,会显示 Hello, 而没有名字。我们可以通过如下修改 app.py 来改进代码:

    # 将默认值移到模板中
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @app.route("/", methods=["GET", "POST"])
    def index():
        if request.method == "POST":
            return render_template("greet.html", name=request.form.get("name"))
        return render_template("index.html")
    

    注意 name=request.form.get("name")) 已更改。

  • 最后,如下修改 greet.html

    {% extends "layout.html" %}
    
    {% block body %}
    
        hello,
        {% if name -%}
            {{ name }}
        {%- else -%}
            world
        {%- endif %}
    
    {% endblock %}
    

    注意 hello, {{ name }} 被更改了,以便在未识别到名字时输出默认值。

  • 由于我们更改了许多文件,你可能希望将自己的最终代码与我们的最终代码进行比较。

Frosh IMs

  • Frosh IMs(或称 froshims)是一个允许学生注册校内体育运动的 Web 应用程序。
  • 关闭所有与 hello 相关的窗口,并在终端窗口中输入 mkdir froshims 创建一个文件夹。然后,输入 cd froshims 进入该文件夹。在其中,输入 mkdir templates 创建一个名为 templates 的目录。
  • 接下来,在 froshims 文件夹中,输入 code requirements.txt 并编写如下代码:

    Flask
    

    与之前一样,运行 Flask 应用程序需要 Flask。

  • 最后,输入 code app.py 并编写如下代码:

    # 使用选择菜单实现注册表单,在服务器端验证运动项目
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    SPORTS = [
        "篮球",
        "足球",
        "极限飞盘"
    ]
    
    
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS)
    
    
    @app.route("/注册", methods=["POST"])
    def 注册():
    
        # 验证提交
        if not request.form.get("name") or request.form.get("sport") not in SPORTS:
            return render_template("failure.html")
    
        # 确认注册
        return render_template("success.html")
    

    注意提供了一个 failure 选项,这样如果 namesport 字段未正确填写,将向用户显示失败消息。

  • 接下来,在 templates 文件夹中创建一个名为 index.html 的文件,输入 code templates/index.html 并编写如下代码:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>注册</h1>
        <form action="/注册" method="post">
            <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
            <select name="sport">
                <option selected value="">Sport</option>
                {% for sport in sports %}
                    <option value="{{ sport }}">{{ sport }}</option>
                {% endfor %}
            </select>
            <button type="submit">注册</button>
        </form>
    {% endblock %}
      
    
  • 接下来,通过输入 code templates/layout.html 创建一个名为 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>
    
  • 第四,在 templates 文件夹中创建一个名为 success.html 的文件,内容如下:

    {% extends "layout.html" %}
    
    {% block body %}
        你已注册成功!
    {% endblock %}
    
  • 最后,在 templates 文件夹中创建一个名为 failure.html 的文件,内容如下:

    {% extends "layout.html" %}
    
    {% block body %}
        你注册失败!
    {% endblock %}
    
  • 执行 flask run 并查看此阶段的应用程序。
  • 可以想象,我们可能想通过单选按钮来查看各种注册选项。我们可以如下改进 index.html

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>注册</h1>
        <form action="/注册" method="post">
            <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
            {% for sport in sports %}
                <input name="sport" type="radio" value="{{ sport }}"> {{ sport }}
            {% endfor %}
            <button type="submit">注册</button>
        </form>
    {% endblock %}
    

    注意 type 已更改为 radio

  • 再次执行 flask run,你可以看到界面已经发生了变化。
  • 可以想象,我们可能希望接受许多不同注册者的注册信息。我们可以如下改进 app.py

    # 实现注册表单,将注册者存储在字典中,并包含错误消息
    
    from flask import Flask, redirect, render_template, request
    
    app = Flask(__name__)
    
    REGISTRANTS = {}
    
    SPORTS = [
        "篮球",
        "足球",
        "极限飞盘"
    ]
    
    
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS)
    
    
    @app.route("/注册", methods=["POST"])
    def 注册():
    
        # 验证姓名
        name = request.form.get("name")
        if not name:
            return render_template("error.html", message="缺少姓名")
    
        # 验证运动项目
        sport = request.form.get("sport")
        if not sport:
            return render_template("error.html", message="缺少运动项目")
        if sport not in SPORTS:
            return render_template("error.html", message="无效的运动项目")
    
        # 记住注册者
        REGISTRANTS[name] = sport
    
        # 确认注册
        return redirect("/registrants")
    
    
    @app.route("/registrants")
    def registrants():
        return render_template("registrants.html", registrants=REGISTRANTS)
    

    注意一个名为 REGISTRANTS 的字典被用来记录 REGISTRANTS[name] 选择的 sport。同时,注意 registrants=REGISTRANTS 将字典传递给了这个模板。

  • 此外,我们可以实现 error.html

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>错误</h1>
        <p>{{ message }}</p>
        <img alt="暴躁猫" src="/static/cat.jpg">
    {% endblock %}
    
  • 此外,创建一个名为 registrants.html 的新模板,内容如下:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>注册者</h1>
        <table>
            <thead>
                <tr>
                    <th>姓名</th>
                    <th>运动项目</th>
                </tr>
            </thead>
            <tbody>
                {% for name in registrants %}
                    <tr>
                        <td>{{ name }}</td>
                        <td>{{ registrants[name] }}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% endblock %}
    

    注意 {% for name in registrants %}...{% endfor %} 会遍历每个注册者。能够在动态网页上进行迭代操作非常强大!

  • 最后,在与 app.py 相同的文件夹中创建一个名为 static 的文件夹。在那里上传以下的图片文件。
  • 执行 flask run 并体验该应用程序。
  • 你现在已经拥有一个 Web 应用程序了!然而,存在一些安全缺陷!因为所有数据都保存在内存中,攻击者可以更改 HTML 并黑客网站。此外,如果服务器关闭,这些数据将无法持久保存。有没有办法让我们的数据在服务器重新启动后仍然存在?

Flask 和 SQL

  • 正如我们看到 Python 可以与 SQL 数据库交互一样,我们可以结合 Flask、Python 和 SQL 的强大功能来创建一个数据持久化的 Web 应用程序!
  • 要实现这一点,你需要完成一系列步骤。
  • 首先,将以下 SQL 数据库 下载到你的 froshims 文件夹中。
  • 在终端中执行 sqlite3 froshims.db 并输入 .schema 来查看数据库文件的结构。进一步输入 SELECT * FROM registrants; 来了解内容。你会注意到文件中目前没有注册信息。
  • 接下来,修改 requirements.txt 如下:

    cs50
    Flask
    
  • Modify index.html as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>注册</h1>
        <form action="/注册" 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 %}
            <button type="submit">注册</button>
        </form>
    {% endblock %}
    
  • Modify layout.html as follows:

    <!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>
    
  • Ensure error.html appears as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>错误</h1>
        <p>{{ message }}</p>
        <img alt="暴躁猫" src="/static/cat.jpg">
    {% endblock %}
    
  • Modify registrants.html to appear as follows:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>注册者</h1>
        <table>
            <thead>
                <tr>
                    <th>姓名</th>
                    <th>运动项目</th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                {% for registrant in registrants %}
                    <tr>
                        <td>{{ registrant.name }}</td>
                        <td>{{ registrant.sport }}</td>
                        <td>
                            <form action="/de注册" method="post">
                                <input name="id" type="hidden" value="{{ registrant.id }}">
                                <button type="submit">取消注册</button>
                            </form>
                        </td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% endblock %}
    

    注意包含了一个隐藏值 registrant.id,这样以后就可以在 app.py 中使用这个 id

  • 最后,修改 app.py 如下:

    # 实现注册表单,将注册者存储在 SQLite 数据库中,支持多项运动及取消注册
    
    from cs50 import SQL
    from flask import Flask, redirect, render_template, request
    
    app = Flask(__name__)
    
    db = SQL("sqlite:///froshims.db")
    
    SPORTS = [
        "篮球",
        "足球",
        "极限飞盘"
    ]
    
    
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS)
    
    
    @app.route("/de注册", methods=["POST"])
    def de注册():
    
        # 忘记注册者
        id = request.form.get("id")
        if id:
            db.execute("DELETE FROM registrants WHERE id = ?", id)
        return redirect("/registrants")
    
    
    @app.route("/注册", methods=["POST"])
    def 注册():
    
        # 验证姓名
        name = request.form.get("name")
        if not name:
            return render_template("error.html", message="缺少姓名")
    
        # 验证运动项目
        sports = request.form.getlist("sport")
        if not sports:
            return render_template("error.html", message="缺少运动项目")
        for sport in sports:
            if sport not in SPORTS:
                return render_template("error.html", message="无效的运动项目")
    
        # 记住注册者
        for sport in sports:
            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)
    

    注意使用了 cs50 库。为 注册post 方法包含了一个路由。这个路由会从注册表单中获取名字和运动项目,并执行 SQL 查询将 namesport 添加到 registrants 表中。de注册 路由则执行一个 SQL 查询来获取用户的 id,并利用该信息取消此人的注册。

  • 你可以执行 flask run 并查看结果。
  • 如果你想下载我们的 froshims 实现,可以点击此处
  • 你可以在 Flask 文档中阅读更多关于 Flask 的内容。

Cookies 和 Session

  • app.py 被认为是控制器视图是用户看到的内容。模型是数据存储和操作的方式。这三者合称为 MVC(模型-视图-控制器)。
  • 虽然之前的 froshims 实现从管理角度来看很有用(后台管理员可以向数据库添加或删除人员),但可以想象这段代码放在公共服务器上是不安全的。
  • 例如,恶意行为者可以通过点击取消注册按钮来代表其他用户做出决定——有效地从服务器删除他们记录的答案。
  • 像 Google 这样的 Web 服务使用登录凭据来确保用户只能访问正确的数据。
  • 我们实际上可以使用 cookies 来实现这一点。Cookies 是存储在浏览器中的小数据片段,这样你的浏览器就可以与服务器通信,有效地说:”我是一个已经登录的授权用户。” 通过这个 cookie 进行的授权称为会话
  • Cookies 可能以如下方式存储:

    GET / HTTP/2
    Host: accounts.google.com
    Cookie: session=value
    

    在这里,一个 session id 与一个表示该会话的特定 value 一起存储。

  • 在最简单的形式中,我们可以通过创建一个名为 login 的文件夹并添加以下文件来实现这一点。
  • 首先,创建一个名为 requirements.txt 的文件,内容如下:

    Flask
    Flask-Session
    

    注意除了 Flask 之外,我们还包含了 Flask-Session,这是支持登录会话所必需的。

  • 其次,在 templates 文件夹中,创建一个名为 layout.html 的文件,内容如下:

    <!DOCTYPE html>
    
    <html lang="en">
    
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>login</title>
        </head>
    
        <body>
            {% block body %}{% endblock %}
        </body>
    
    </html>
    

    注意这提供了一个非常简单的布局,包含标题和主体。

  • 第三,在 templates 文件夹中创建一个名为 index.html 的文件,内容如下:

    {% extends "layout.html" %}
    
    {% block body %}
    
        {% if name -%}
            你已登录为 {{ name }}。 <a href="/logout">登出</a>。
        {%- else -%}
            你尚未登录。 <a href="/login">登录</a>。
        {%- endif %}
    
    {% endblock %}
    

    注意这个文件会检查 session["name"] 是否存在(在下面的 app.py 中有进一步阐述)。如果存在,将显示一条欢迎消息。如果不存在,将建议你浏览到登录页面。

  • 第四,创建一个名为 login.html 的文件,并添加以下代码:

    {% extends "layout.html" %}
    
    {% block body %}
    
        <form action="/login" method="post">
            <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
            <button type="submit">登录</button>
        </form>
    
    {% endblock %}
    

    注意这是一个基本登录页面的布局。

  • 最后,创建一个名为 app.py 的文件,并编写如下代码:

    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():
        return render_template("index.html", name=session.get("name"))
    
    
    @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.clear()
        return redirect("/")
    

    注意文件顶部修改后的导入,其中包括 session,这将允许你支持会话。最重要的是,注意 session["name"] 是如何在 loginlogout 路由中使用的。login 路由会分配提供的登录名字并将其赋给 session["name"]。而在 logout 路由中,登出是通过清除 session 的值来实现的。

  • session 抽象允许你确保只有特定用户才能访问我们应用中的特定数据和功能。它能确保没有人可以冒充其他用户行事,无论好坏!
  • 如果你愿意,可以下载我们的 login 实现。
  • 你可以在 Flask 文档中阅读更多关于会话的内容。

购物车

  • 接下来看一个利用 Flask 启用会话功能的最终示例。
  • 我们查看了 storeapp.py 中的以下代码。展示了以下代码:

    from cs50 import SQL
    from flask import Flask, redirect, render_template, request, session
    from flask_session import Session
    
    # 配置应用
    app = Flask(__name__)
    
    # 连接数据库
    db = SQL("sqlite:///store.db")
    
    # 配置会话
    app.config["SESSION_PERMANENT"] = False
    app.config["SESSION_TYPE"] = "filesystem"
    Session(app)
    
    
    @app.route("/")
    def index():
        books = db.execute("SELECT * FROM books")
        return render_template("books.html", books=books)
    
    
    @app.route("/cart", methods=["GET", "POST"])
    def cart():
    
        # 确保购物车存在
        if "cart" not in session:
            session["cart"] = []
    
        # POST
        if request.method == "POST":
            book_id = request.form.get("id")
            if book_id:
                session["cart"].append(book_id)
            return redirect("/cart")
    
        # GET
        books = db.execute("SELECT * FROM books WHERE id IN (?)", session["cart"])
        return render_template("cart.html", books=books)
    

    注意 cart 是使用列表实现的。可以通过 books.html 中的 Add to Cart 按钮将商品添加到这个列表中。点击这样的按钮时,会调用 post 方法,其中商品的 id 会被附加到 cart 中。查看购物车时,调用 get 方法,会执行 SQL 查询来显示购物车中的书籍列表。

  • 我们还看到了 books.html 的内容:

    {% extends "layout.html" %}
    
    {% block body %}
    
        <h1>教材</h1>
        {% for book in books %}
            <h2>{{ book["title"] }}</h2>
            <form action="/cart" method="post">
                <input name="id" type="hidden" value="{{ book[‘id’] }}">
                <button type="submit">加入购物车</button>
            </form>
        {% endfor %}
    
    {% endblock %}
    

    注意这通过 for book in books 为每本书创建了 加入购物车 的功能。

  • 你可以在源代码中看到这个 Flask 实现的其他文件。

Shows

  • 我们查看了一个名为 shows 的预设计程序,在 app.py 中:

    # 使用 LIKE 搜索节目
    
    from cs50 import SQL
    from flask import Flask, render_template, request
    
    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)
    

    注意 search 路由提供了一种搜索 show 的方式。此搜索会查找标题 LIKE 用户提供的标题。

  • 我们还查看了 index.html

    {% extends "layout.html" %}
    
    {% block body %}
    
        <form action="/search" method="get">
            <input autocomplete="off" autofocus name="q" placeholder="查询" type="search">
            <button type="submit">搜索</button>
        </form>
    
    {% endblock %}
    
    
  • 你可以在源代码中看到此实现的其他文件。

APIs

  • 应用程序接口或称 API 是一系列规范,允许你与另一个服务进行交互。例如,我们可以利用 IMDB 的 API 来与其数据库进行交互。我们甚至可以集成 API 来处理可从服务器下载的特定类型数据。
  • 进一步改进 shows,我们看到了 app.py 的改进版本:

    # 使用 Ajax 搜索节目
    
    from cs50 import SQL
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    db = SQL("sqlite:///shows.db")
    
    
    @app.route("/")
    def index():
        return render_template("index.html")
    
    
    @app.route("/search")
    def search():
        q = request.args.get("q")
        if q:
            shows = db.execute("SELECT * FROM shows WHERE title LIKE ? LIMIT 50", "%" + q + "%")
        else:
            shows = []
        return render_template("search.html", shows=shows)
    

    注意 search 路由执行了一个 SQL 查询。

  • 查看 search.html,你会发现它非常简单:

    {% for show in shows %}
        <li>{{ show["title"] }}</li>
    {% endfor %}
    

    注意它提供了一个项目列表。

  • 最后,查看 index.html,注意使用了 AJAX 代码来驱动搜索:

    <!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="查询" type="search">
    
            <ul></ul>
    
            <script>
                let input = document.querySelector(input);
                input.addEventListener(input, async function() {
                    let response = await fetch(/search?q= + input.value);
                    let shows = await response.text();
                    document.querySelector(ul).innerHTML = shows;
                });
            </script>
    
        </body>
    
    </html>
    

    注意使用了一个事件监听器来动态查询服务器,以提供与输入标题匹配的列表。这将定位 HTML 中的 ul 标签并相应地修改网页以包含匹配项的列表。

  • 你可以在 AJAX 文档中阅读更多内容。

JSON

  • JavaScript 对象表示法(或称 JSON)是一种包含键值对的文本文件。这是一种原始的、计算机友好的获取大量数据的方式。
  • JSON 是一种从服务器获取返回数据的非常有用的方式。
  • 你可以从我们一起查看的 index.html 中看到它的实际应用:

    <!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="查询" type="text">
    
            <ul></ul>
    
            <script>
                let input = document.querySelector(input);
                input.addEventListener(input, async function() {
                    let response = await fetch(/search?q= + input.value);
                    let shows = await response.json();
                    let html = ’’;
                    for (let i in shows) {
                        let title = shows[i].title.replace(<, &lt;).replace(&, &amp;);
                        html += <li> + title + </li>;
                    }
                    document.querySelector(ul).innerHTML = html;
                });
            </script>
    
        </body>
    
    </html>
    

    虽然以上内容可能有点晦涩难懂,但它为你自己研究 JSON 并在自己的 Web 应用程序中实现它提供了一个起点。

  • 此外,我们还查看了 app.py 来了解如何获取 JSON 响应:

    # 使用 JSON 通过 Ajax 搜索节目
    
    from cs50 import SQL
    from flask import Flask, jsonify, render_template, request
    
    app = Flask(__name__)
    
    db = SQL("sqlite:///shows.db")
    
    
    @app.route("/")
    def index():
        return render_template("index.html")
    
    
    @app.route("/search")
    def search():
        q = request.args.get("q")
        if q:
            shows = db.execute("SELECT * FROM shows WHERE title LIKE ? LIMIT 50", "%" + q + "%")
        else:
            shows = []
        return jsonify(shows)
    

    注意 jsonify 被用来将结果转换为现代 Web 应用程序可接受的格式。

  • 你可以在 JSON 文档中阅读更多内容。
  • 总之,你现在已经具备了使用 Python、Flask、HTML 和 SQL 完成自己的 Web 应用程序的能力。

总结

在这节课中,你学习了如何使用 Python、SQL 和 Flask 来创建 Web 应用程序。具体来说,我们讨论了……

  • Flask
  • 表单
  • 模板
  • 请求方法
  • Flask 和 SQL
  • Cookies 和 Session
  • API
  • JSON