Skip to content

32. 第 12 版的 HTML 模式

我们在第 12 版的开头提到,我们将分几个阶段开发该应用程序。我们写道:

  • 基于 HTML 应用程序的视图,我们将定义 Web 应用程序必须实现的操作。此处我们将使用实际的视图,但这些视图也可以只是纸面上的草图;
  • 基于这些操作,我们将定义 HTML 应用程序的服务 URL;
  • 我们将使用一个提供 JSON 的服务器来实现这些服务 URL。这使我们能够定义 Web 服务器的框架,而无需担心要交付的 HTML 页面。我们将使用 Postman 测试这些服务 URL;
  • 随后,我们将使用控制台客户端测试我们的 JSON 服务器;
  • 一旦 JSON 服务器通过验证,我们将着手编写 HTML 应用程序;

我们已拥有可运行的 JSON 和 XML 服务器。现在可以转向 HTML 服务器。我们将看到,它复用了为 JSON/XML 服务器开发的完整架构,并在此基础上增加了 HTML 视图管理功能。

32.1. MVC 架构

我们将按以下方式实现 MVC(模型-视图-控制器)架构模式:

Image

客户端请求的处理流程如下:

  • 1 - 请求

请求的 URL 将采用 http://machine:port/action/param1/param2/… 的形式。[主控制器] 将通过配置文件将请求“路由”到正确的控制器。为此,它将使用 URL 中的 [action] 字段。URL 的其余部分 [param1/param2/…] 由可选参数组成,这些参数将传递给该操作。 此处 MVC 中的“C”指代 [主控制器、控制器 / 操作] 这一链条。如果没有任何控制器能处理该请求的操作,Web 服务器将返回“请求的 URL 不存在”的响应。

  • 2 - 处理
  • 选定的操作 [2a] 可以使用 [主控制器] 传递给它的参数。这些参数可能来自两个来源:
      • URL 的路径 [/param1/param2/…]
      • 客户端请求主体中提交的参数;
    • 在处理用户请求时,该操作可能需要调用 [业务][2b]。一旦客户端的请求被处理完毕,可能会触发各种响应。一个典型的例子是:
      • 若请求无法正确处理,则返回错误响应;
      • 否则返回确认响应;
    • [控制器/操作] 将连同状态码一起将其响应 [2c] 返回给主控制器。这些状态码将唯一地表示应用程序的当前状态。它要么是成功码,要么是错误码;
  • 3 - 响应
    • 根据客户端请求的是 JSON、XML 还是 HTML 响应,[主控制器] 将实例化 [3a] 相应的响应类型,并指示其将响应发送给客户端。主控制器将同时向其传递响应以及由已执行的 [控制器/操作] 提供的状态码;
    • 如果所需的响应为 JSON 或 XML 类型,选定的响应组件将对 [控制器/操作] 提供的响应进行格式化并发送 [3c]。能够处理此响应的客户端可以是 Python 控制台脚本,也可以是嵌入在 HTML 页面中的 JavaScript 脚本;
    • 如果所需的响应是 HTML 类型,则选定的响应将根据收到的状态码选择 [3b] 其中一个 HTML 视图 [Vuei]。 这就是 MVC 中的 V。一个视图对应一个状态码。该视图 V 将显示已执行的 [控制器/操作] 返回的响应。它将该响应中的数据封装在 HTML、CSS 和 JavaScript 中。这些数据被称为视图模型。这就是 MVC 中的 M。客户端通常是浏览器;

32.2. HTML 服务器脚本的目录结构

Image

  • [1] 中,HTML 服务器的静态元素;
  • [2-3] 中,是 HTML 服务器的视图 V。片段 [2] 是视图 [3] 内的可复用元素;
  • [4] 中,用于静态测试视图的文件夹;
  • [5] 中,存放 V 视图的 M 模型的文件夹,即 MVC 中的 M;

32.3. 视图概述

该 HTML Web 应用程序使用四个视图。第一个视图是身份验证视图:

  • 引导至该首个视图的操作是 [/init-session] 操作 [1]Image
  • 点击 [Validate] 按钮将触发 [/authenticate-user] 操作,并提交两个参数 [2-3]

税费计算视图:

Image

  • [1] 中,调用此视图的操作 [/authenticate-user]
  • [2] 中,点击 [Validate] 按钮会触发 [/calculate-tax] 操作的执行,并提交三个参数 [2-5]
  • 点击链接 [6] 会触发不带参数的 [/list-simulations] 操作;
  • 点击链接 [7] 将触发不带参数的 [/end-session] 操作;

第三个视图显示已认证用户执行的模拟:

Image

  • [1] 中,调用此视图的操作 [/list-simulations]
  • [2] 中,点击 [删除] 链接会触发带有一个参数的 [/delete-simulation] 操作,该参数为要从列表中删除的模拟编号;
  • 点击 [3] 链接将触发不带参数的 [/display-tax-calculation] 操作,该操作将重新显示税费计算视图;
  • 点击 [4] 链接将触发不带参数的 [/end-session] 操作;

第四个视图将称为“意外错误视图”:

  • [1] 中:用户自行输入了 URL。但在本例中,列表中不存在任何模拟。因此我们收到错误信息 [2]。我们对这条信息并不陌生,此前在 JSON/XML 中也出现过。我们将此类错误称为“意外错误”,因为它在应用程序的正常使用过程中不会发生,仅当用户自行输入 URL 时才会出现; Image
  • 发生意外错误时,链接 [3-5] 可让您返回其他三个视图中的任意一个;

让我们回顾一下 JSON/XML 服务器的各种服务 URL:

操作
角色
执行上下文
/init-session
用于设置所需响应的类型(json、xml、html)
GET 请求
可在任何时候发送
/authenticate-user
授权或拒绝用户的登录
POST 请求
请求必须包含两个提交的参数 [user, password]
仅在已知会话类型(json、xml、html)时才能发出
/calculate-tax
执行税费计算模拟
POST 请求
请求必须包含三个提交参数 [married, children, salary]
仅当会话类型(json、xml、html)已知且用户已通过身份验证时才能发出
/list-simulations
请求查看自会话开始以来执行的模拟列表
GET 请求
只有在已知会话类型(json、xml、html)且用户已通过身份验证的情况下,才能发出此请求
/delete-simulation/number
从模拟列表中删除一个模拟
GET 请求
只有在已知会话类型(json、xml、html)且用户已通过身份验证的情况下,才能发出此请求
/display-tax-calculation
显示税费计算的 HTML 页面
GET 请求。
只有在已知会话类型(json、xml、html)且用户已通过身份验证的情况下才能发出
/end-session
结束模拟会话。
从技术上讲,旧的 Web 会话将被删除,并创建一个新的会话
仅当已知会话类型(json、xml、html)且用户已通过身份验证时,才可签发

这些不同的服务 URL 也将用于 HTML 服务器。

32.4. 配置视图

一个操作由控制器处理。该控制器返回一个元组 (result, status_code),其中:

  • [result] 是一个键值为 [action, status, response] 的字典;
  • [status_code] 是将发送给客户端的 HTTP 响应的状态码;

在 HTML 会话中,执行操作后显示的页面取决于控制器返回的状态码。这种依赖关系在 [config] 配置中体现如下:


        # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
        "views"[
            {
                # vue d'authentification
                "états": [
                    # /init-session réussite
                    700,
                    # /authentifier-utilisateur échec
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # vue du calcul de l'impôt
                "états"[
                    # /authentifier-utilisateur réussite
                    200,
                    # /calculer-impot réussite
                    300,
                    # /calculer-impot échec
                    301,
                    # /afficher-calcul-impot
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # vue de la liste des simulations
                "états"[
                    # /lister-simulations
                    500,
                    # /supprimer-simulation
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],
 
        # vue des erreurs inattendues
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },
 
        # redirections
        "redirections"[
            {
                "états": [
                    400,  # /fin-session réussite
                ],
                # redirection vers
                "to""/init-session/html",
            }
        ],
    }
  • 第 2–40 行:[views] 是一个视图列表。让我们来看一下第 3–13 行中的视图:
    • 第 11 行:显示的视图 V;
    • 第 12 行:负责为该视图生成模型 M 的类实例;
    • 第 5–10 行:导致此视图的状态;
  • 第 3–13 行:身份验证视图;
  • 第 14–28 行:税费计算视图;
  • 第 29–39 行:模拟列表视图;
  • 第 42–46 行:意外错误视图;
  • 第 49–57 行:某些状态会通过重定向跳转到某个视图。状态 400 即属于这种情况,它对应于成功的 [/fin-session] 操作。此时,客户端必须被重定向到 [http://machine:port/chemin/init-session/html] 操作;

接下来我们将介绍不同的视图。

32.5. 身份验证视图

Image

32.5.1. 视图概述

身份验证视图如下所示:

Image

该视图由两个元素组成,我们将它们称为片段:

  • 片段 [1] 由片段 [v-banner.html] 生成;
  • 片段 [2] 由片段 [v-authentication.html] 生成;

身份验证视图由以下页面 [vue-authentication.html] 生成:


<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <title>Application impôts</title>
</head>
 
<body>
<div class="container">
    <!-- headband -->
    {% include "fragments/v-bandeau.html" %}
    <!-- two-column line -->
    <div class="row">
        <div class="col-md-9">
            {% include "fragments/v-authentification.html" %}
        </div>
    </div>
    <!-- if error - displays an error alert -->
    {% if modèle.error %}
    <div class="row">
        <div class="col-md-9">
            <div class="alert alert-danger" role="alert">
                Les erreurs suivantes se sont produites :
                <ul>{{modèle.erreurs|safe}}</ul>
            </div>
        </div>
    </div>
    {% endif %}
</div>
</body>
</html>

评论

  • 第 2 行:HTML 文档以此行开头;
  • 第 3–36 行:HTML 页面由 <html> 和 </html> 标签包围;
  • 第 4–11 行:HTML 文档的头部(head);
  • 第 6 行:此处的 <meta charset> 标签表明该文档采用 UTF-8 编码;
  • 第 7 行:<meta name='viewport'> 标签设置初始视口显示方式:以初始大小(initial-scale)显示在屏幕的整个宽度(width)上,而不缩放以适应较小的屏幕(shrink-to-fit);
  • 第 9 行:<link rel='stylesheet'> 标签指定了控制视口外观的 CSS 文件。这里我们使用的是 Bootstrap 4.4.1 CSS 框架 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] ;
  • 第 10 行:title 标签设置页面标题:

Image

  • 第 13–35 行:网页的主体内容被 body 和 /body 标签所包围;
  • 第 14–34 行:<div> 标签界定了页面显示区域。视图中使用的 [class] 属性均指向 Bootstrap CSS 框架。<div class=’container’> 标签(第 14 行)界定了一个 Bootstrap 容器;
  • 第 26 行:引入了片段 [v-banner.html]。该片段生成页面的横幅 [1]。我们稍后将对此进行说明;
  • 第 18–22 行:<div class=’row’> 标签定义了一个 Bootstrap 行。这些行由 12 个列组成;
  • 第 19:该标签定义了一个 9 列的区域;
  • 第 20 行:我们引入片段 [v-authentification.html],该片段显示页面的身份验证表单 [2]。稍后我们将对此进行说明;
  • 第 24–33 行:这些行的 HTML 代码仅在 [model.error] 为 True 时才会被使用。我们将始终采用这种方式:HTML 视图的模型将被封装在一个字典 [model] 中;
  • 第 24–33 行:若用户输入的凭据不正确,认证将失败。此时,认证视图将重新显示并附带一条错误信息。属性 [model.error] 用于指示是否显示该错误信息;
  • 第 27–30 行:定义一个粉色背景的区域(class="alert alert-danger")(第 27 行);

Image

  • 第 28 行:部分文本;
  • 第 29 行:HTML 标签 <ul>(无序列表)用于显示带项目符号的列表。 每个列表项必须采用 <li>item</li> 的语法。此处,我们显示 [model.errors] 的值。该值由 [safe] 过滤器进行过滤(检查是否包含 |)。默认情况下,当字符串发送至浏览器时,Flask 会“转义”其中可能包含的任何 HTML 标签,以防止浏览器将其解释为有效标签。 但有时我们需要让它们被解释。本例即属此情况,字符串 [model.errors] 中包含用于分隔列表项的 HTML 标签 <li> 和 </li>。此时,我们使用 [safe] 过滤器,它会告知 Flask 待显示的字符串是安全的,因此不应对其中的任何 HTML 标签进行清理;

让我们注意一下此代码中需要定义的动态元素:

  • [model.error]:用于显示错误消息;
  • [model.errors]:错误消息的列表(HTML 意义上的列表);

32.5.2. [v-banner.html] 片段

[v-banner.html] 片段用于显示 Web 应用程序中所有视图的顶部横幅:

Image

[v-banner.html] 片段的代码如下:


<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
    <div class="row">
        <div class="col-md-4">
            <img src="{{ url_for('static', filename='images/logo.jpg') }}" alt="Cerisier en fleurs"/>
        </div>
        <div class="col-md-8">
            <h1>
                Calculez votre impôt
            </h1>
        </div>
    </div>
</div>

评论

  • 第 2–13 行:横幅被包裹在一个 Bootstrap Jumbotron 区域 [<div class="jumbotron">] 中。该 Bootstrap 类会以特定方式对显示内容进行样式设置,使其脱颖而出;
  • 第 3–12 行:一个 Bootstrap 行;
  • 第 4–6 行:在该行的前四列中放置了一张图片 [img]
  • 第 5 行:语法:
{{ url_for('static', filename='images/logo.jpg') }}

使用了 Flask 的 [url_for] 函数。此处的参数值是 [static] 文件夹中 [images/logo.jpg] 文件的 URL;

  • 第 7–11 行:该行其余 8 列(请记住总共有 12 列)将用于显示文本(第 9 行),并采用大字号(<h1>,第 8–10 行);

32.5.3. [v-authentification.html] 片段

[v-authentification.html] 片段用于显示 Web 应用程序的身份验证表单:

Image

[v-authentification.html] 片段的代码如下:


<!-- form HTML - post its values with the [authenticate-user] action -->
<form method="post" action="/authentifier-utilisateur">
 
    <!-- title -->
    <div class="alert alert-primary" role="alert">
        <h4>Veuillez vous authentifier</h4>
    </div>
 
    <!-- bootstrap form -->
    <fieldset class="form-group">
        <!-- 1st line -->
        <div class="form-group row">
            <!-- wording -->
            <label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
            <div class="col-md-4">
                <!-- text input field -->
                <input type="text" class="form-control" id="user" name="user"
                       placeholder="Nom d'utilisateur" value="{{ modèle.login }}" required>
            </div>
        </div>
        <!-- 2nd line -->
        <div class="form-group row">
            <!-- wording -->
            <label for="password" class="col-md-3 col-form-label">Mot de passe</label>
            <!-- text input field -->
            <div class="col-md-4">
                <input type="password" class="form-control" id="password" name="password"
                       placeholder="Mot de passe" required>
            </div>
        </div>
        <!-- submit] button on a 3rd line -->
        <div class="form-group row">
            <div class="col-md-2">
                <button type="submit" class="btn btn-primary">Valider</button>
            </div>
        </div>
    </fieldset>
 
</form>

评论

  • 第 2–39 行:<form> 标签定义了一个 HTML 表单。该表单通常具有以下特征:
    • 它定义了输入字段(第 17 行和第 27 行的 <input> 标签);
    • 它有一个 [submit] 按钮(第 34 行),用于将输入的值发送至 [form] 标签(第 2 行)中 [action] 属性指定的 URL。请求该 URL 所使用的 HTTP 方法在 [form] 标签(第 2 行)的 [method] 属性中指定;
    • 在此,当用户点击 [Submit] 按钮(第 34 行)时,浏览器将使用 POST 方法(第 2 行)将表单中输入的值提交至 URL [/authenticate-user](第 2 行);
    • POST 数据即用户在第 17 行和第 27 行输入框中输入的值。这些值将以 [x-www-form-urlencoded] 格式包含在浏览器发送的 HTTP 请求正文中。参数名称 [user, password] 对应于第 17 行和第 27 行输入框的 [name] 属性;
  • 第 5–7 行:一个 Bootstrap 部分,用于在蓝色背景上显示标题:
  • 第 10–37 行:一个 Bootstrap 表单。所有表单元素随后将采用特定样式; Image
  • 第 12–20 行:定义表单的第一行 Bootstrap 布局:

Image

  • 第 14 行将标签 [1] 设置为三列布局。[label] 标签的 [for] 属性将该标签与第 17 行输入字段的 [id] 属性建立关联;
  • 第 15–19 行:将输入字段放置在四列布局中;
  • 第 17–18 行:HTML [input] 标签定义了一个输入框。它具有以下属性:
    • [type='text']:这是一个文本输入框。您可以在其中输入任何内容;
    • [class='form-control']:输入字段的 Bootstrap 样式;
    • [id='user']: 输入字段的标识符。该标识符通常由 CSS 和 JavaScript 代码使用;
    • [name='user']: 输入字段的名称。用户输入的值将由浏览器以该名称提交 [user=xx]
    • [placeholder='prompt']: 用户尚未输入任何内容时,输入框中显示的提示文本;

Image

  • (待续)
    • [value='value']: 输入框一出现时,即会显示文本 'value',此时用户尚未输入任何内容。此机制通常用于在发生错误时显示导致错误的输入内容。在此处,该值将取自变量 [model.login]
    • [required]:要求用户输入值,以便表单能提交至服务器:
  • 第 21–30 行:针对密码输入字段的类似代码;
  • 第 27 行:[type='password'] 创建了一个文本输入框(可输入任意内容),但输入的字符会被隐藏:

Image

  • 第 32–36 行:用于 [Submit] 按钮的第三行 Bootstrap 代码;
  • 第 34 行:由于该按钮具有 [type="submit"] 属性,点击此按钮会触发浏览器将输入的值发送至服务器,如前所述。CSS 属性 [class="btn btn-primary"] 会显示一个蓝色按钮: Image

还有最后一点需要说明。在第 2 行中,[action="/authentifier-utilisateur"] 属性定义了一个不完整的 URL(它没有以 http://machine:port/chemin 开头)。 在本示例中,所有应用程序 URL 均采用 [http://machine:port/chemin/action/param1/param2/..] 的形式,其中 [http://machine:port/chemin] 是服务 URL 的根目录。在 [action="/authenticate-user"] 中,我们使用的是绝对 URL,即从 URL 根目录开始计算的 URL。 因此,POST 请求的完整 URL 为 [http://machine:port/chemin/authentifier-utilisateur],这也是浏览器将使用的 URL。

请注意,此片段使用了 [model.login] 模板。

32.5.4. 视觉测试

在将视图集成到应用程序之前,我们可以对其进行充分测试。此处的目的是测试其视觉外观。我们将把所有测试视图收集在项目的 [tests_views] 文件夹中:

Image

要测试视图 V [vue-authentification.html],我们需要创建它将要显示的数据模型 M。我们通过脚本 [test_vue_authentification.py] 来实现这一点:

from flask import Flask, render_template, make_response

#  flask application
app = Flask(__name__, template_folder="../templates", static_folder="../static")

#  Home URL
@app.route('/')
def index():
    #  we encapsulate the paged data in the model
    modèle = {}
    #  user code
    modèle["login"] = "albert"
    #  error list
    modèle["error"] = True
    erreurs = ["erreur1", "erreur2"]
    #  build a HTML list of errors
    content = ""
    for erreur in erreurs:
        content += f"<li>{erreur}</li>"
    modèle["erreurs"] = content
    #  page display
    return make_response(render_template("views/vue-authentification.html", modèle=modèle))

#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

注释

  • 第 1-3 行:我们创建了一个 Flask 应用程序,其唯一目的是显示 [authentication-view.html] 视图(第 22 行);
  • 第 7 行:该应用程序仅有一个服务 URL;
  • 第 9–20 行:身份验证视图包含由 [model] 对象控制的动态部分。该对象被称为视图模型。根据 MVC 缩写词的两种定义之一,这里对应 MVC 中的 M(模型)。在定义 [authentication-view.html] 视图时,我们确定了三个动态值:
    • [model.error]:一个布尔值,用于指示是否显示错误消息;
    • [model.errors]:一个包含错误消息的 HTML 列表;
    • [model.login]:用户的登录名;

因此,我们需要定义这三个动态值。

  • 第 9–20 行:我们定义了身份验证视图的三个动态元素;

要运行测试,我们启动脚本 [tests_views/test_vue_authentification.py] 并请求 URL [/localhost:5000/]

我们将持续进行这些视觉测试,直到对结果满意为止。

Image

32.5.5. 计算视图模型

一旦确定了视图的外观,我们就可以在实际条件下计算视图模型。视图模型将由位于 [models_for_views] 文件夹中的类生成:

Image

每个生成视图模型的类都将实现以下 [InterfaceModelForView] 接口:


from abc import ABC, abstractmethod
 
from flask import Request
from werkzeug.local import LocalProxy
 
class InterfaceModelForView(ABC):
 
    @abstractmethod
    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        pass
  • 第 8–10 行:[get_model_for_view] 方法负责生成一个封装在字典中的视图模型。为此,它接收以下信息:
    • [request, session, config] 是操作控制器使用的相同参数。因此,它们也会传递给模型;
    • 控制器已生成一个结果 [result],该结果也会传递给模型。该结果包含一个重要元素 [status],用于指示当前操作的执行情况。模型将使用此信息;

我们已经看到,在应用程序的配置 [config] 中,控制器返回的状态码被用于指定要显示的 HTML 视图:


        # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
        "views"[
            {
                # vue d'authentification
                "états": [
                    # /init-session réussite
                    700,
                    # /authentifier-utilisateur échec
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # vue du calcul de l'impôt
                "états"[
                    # /authentifier-utilisateur réussite
                    200,
                    # /calculer-impot réussite
                    300,
                    # /calculer-impot échec
                    301,
                    # /afficher-calcul-impot
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # vue de la liste des simulations
                "états"[
                    # /lister-simulations
                    500,
                    # /supprimer-simulation
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],
        # vue des erreurs inattendues
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },
        # redirections
        "redirections"[
            {
                "états": [
                    400,  # /fin-session réussite
                ],
                # redirection vers
                "to""/init-session/html",
            }
        ],
    }

因此,正是状态码 [700, 201](第 7 行和第 9 行)导致了身份验证页面的显示。要理解这些代码的含义,我们可以参考在 JSON 应用程序上进行的 [Postman] 测试:

  • [init-session-json-700]:700是[init-session]操作成功后的状态码:随后会显示空白的身份验证表单;
  • [authenticate-user-201]:201是[authenticate-user]操作失败(凭据不正确)后的状态码:此时会显示身份验证表单,以便用户修正凭据;

既然我们已知认证表单应在何时显示,即可在 [ModelForAuthentificationView](第 12 行)中定义其模型:

from flask import Request
from werkzeug.local import LocalProxy

from InterfaceModelForView import InterfaceModelForView

class ModelForAuthentificationView(InterfaceModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        #  we encapsulate the paged data in the model
        modèle = {}
        #  application status
        état = résultat["état"]
        #  the model depends on the state
        if état == 700:
            #  case of empty form display
            modèle["login"] = ""
            #  no error to display
            modèle["error"] = False
        elif état == 201:
            #  false authentication
            #  the user initially entered is redisplayed
            modèle["login"] = request.form.get("user")
            #  there is an error to display
            modèle["error"] = True
            #  list HTML of error msg
            erreurs = ""
            for erreur in résultat["réponse"]:
                erreurs += f"<li>{erreur}</li>"
            modèle["erreurs"] = erreurs

        #  we render the model
        return modèle

注释

  • 第 8 行:身份验证视图的 [get_model_for_view] 方法必须返回一个包含三个键 [error, errors, login] 的字典。该计算基于操作控制器返回的状态码;
  • 第 12 行:我们获取处理当前操作的控制器返回的状态码;
  • 第 14–29 行:模型取决于此状态码;
  • 第 15–18 行:需要显示空白认证表单的情况;
  • 第 20–29 行:认证失败的情况:我们显示用户输入的用户名并显示一条错误消息。用户随后可以尝试再次认证;
  • 第 22 行:可从客户端请求中获取用户最初输入的用户名;
  • 第 24 行:表明有错误需要显示;
  • 第 26–29 行:若发生错误,result[‘response’] 将包含一个错误列表;

32.5.6. 生成 HTML 响应

让我们回到 HTML 应用程序的 MVC 模型:

  • 在步骤 2(2a、2b):控制器执行一个操作; Image
  • 在 3(3a、3b、3c):选择一个视图并将其发送给客户端;

[3a] 中,会选择响应类型(JSON、XML、HTML)。我们已经了解过如何生成 JSON 和 XML 响应,但尚未涉及 HTML 响应。这些响应由 [HtmlResponse] 类生成:

Image

让我们回顾一下,在主脚本 [main] 中是如何确定要发送给用户的响应类型的:


        ….
        # on construit la réponse à envoyer
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # on envoie la réponse
        return response, status_code

其中,第 3 行中的 config['responses'] 是以下字典:


        # les différents types de réponse (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

因此,生成 HTML 响应的是 [HtmlResponse] 类。其代码如下:

#  dictionary of HTML responses according to the status contained in the result

from flask import make_response, render_template
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse

class HtmlResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        #  the HTML response depends on the status code returned by the controller
        état = résultat["état"]

        #  do I need to redirect?
        for redirection in config["redirections"]:
            #  conditions requiring redirection
            états = redirection["états"]
            if état in états:
                #  you need to redirect
                return redirect(f"/{redirection['to']}"), status.HTTP_302_FOUND

        #  a state corresponds to a view
        #  search for it in the list of views
        views_configs = config["views"]
        trouvé = False
        i = 0
        #  browse the list of views
        nb_views = len(views_configs)
        while not trouvé and i < nb_views:
            #  view n° i
            view_config = views_configs[i]
            #  states associated with view n° i
            états = view_config["états"]
            #  is the state you're looking for in the states associated with view n° i?
            if état in états:
                trouvé = True
            else:
                #  next view
                i += 1
        #  found?
        if not trouvé:
            #  if no view exists for the current state of the application
            #  render error view
            view_config = config["view-erreurs"]

        #  calculate the view model to be displayed
        model_for_view = view_config["model_for_view"]
        modèle = model_for_view.get_model_for_view(request, session, config, résultat)
        #  generate the HTML response code
        html = render_template(view_config["view_name"], modèle=modèle)
        #  build the HTTP response
        response = make_response(html)
        response.headers['Content-Type'] = 'text/html; charset=utf-8'
        #  we return the result
        return response, status_code
  • 第 11 行:负责生成 HTML 响应的 [build_http_response] 方法接收以下参数:
    • [request, session, dict]:这些是控制器用于处理当前操作的参数;
    • [status_code, result] 是该控制器生成的两个结果;
  • 第 14 行:如前所述,服务器的 HTML 响应取决于 [result] 字典中包含的状态码;
  • 第 16–22 行:首先处理重定向。目前我们暂且忽略这一部分,直到遇到重定向的示例。请注意,重定向通常是 HTML 服务器的典型用例。JSON 或 XML 服务器中不会出现这种情况;
  • 第 24–41 行:我们在视图中搜索其 [states] 列表中包含所需状态的视图;
  • 第 42–46 行:若未找到视图,则视为意外错误。让我们看一个示例。在应用程序的正常运行中,[/delete-simulation] 操作绝不会失败。事实上,我们将看到,模拟的删除是通过代码生成的链接来执行的。这些链接是有效的,不会引发错误。 然而,正如我们所见,用户可以直接输入 URL [/delete-simulation/id],从而触发错误。在此情况下,[SupprimerSimulationController] 会返回状态码 601。但该状态码并不在触发 HTML 页面显示的状态码列表中。因此,将显示错误视图。其在配置中定义如下:

        # vue des erreurs inattendues
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },
  • 第 49 行:一旦确定要显示的视图,我们就获取生成其模板的类。该类同样位于 [config] 配置中;
  • 第 50 行:找到该类后,生成视图的模型;
  • 第 52 行:一旦视图 V 的模型 M 计算完成,我们就可以生成该视图的 HTML 代码;
  • 第 54–55 行:我们构建包含 HTML 正文的 HTTP 响应;
  • 第 56–57 行:返回包含状态码的 HTTP 响应;

32.5.7. 测试 [Postman]

我们将执行返回代码 [700, 201] 的请求,这些请求会显示身份验证页面:

  • [init-session-html-700]:700 是 [init-session] 操作成功后的状态码;随后将显示空的身份验证表单;
  • [authenticate-user-201]:201 是 [authenticate-user] 操作失败(凭据不正确)后的状态码;随后将显示身份验证表单以便进行修正;

只需复用这些视图,并检查它们是否正确显示了身份验证视图。以下是两个示例:

案例 1:[init-session-html-700],HTML 会话的开始;

Image

响应如下:

Image

  • [5] 中,[预览] 模式允许您查看收到的 HTML 页面;
  • [6] 中,我们可以看到预期的空表单;
  • [7] 中,Postman 未点击页面图片的链接;
  • [8] 中,[Raw] 模式提供了对接收到的 HTML 的访问;

Image

  • [3] 中,是 Postman 未能加载的链接。它显示了 [alt=alternative] 属性的值,该值会在图片无法加载时显示。这里的情况更像是 Postman 故意不加载它。您可以在 上通过 Postman 请求 URL [http://localhost:5000/static/images.logo.jpg] 来验证这一点:

情况 2[user-authentication-201],身份验证错误

Image

现在,让我们在成功初始化 HTML 会话后执行一次错误的身份验证:

Image

上文:

  • [4,7] 处:请求发送字符串 [user=bernard&password=thibault]

响应如下:

Image

  • [4] 处,显示了一条错误信息;
  • [3] 处,再次显示了错误的用户;

32.5.8. 结论

我们在未编写其他视图的情况下成功测试了视图 [vue-authentification.html]。这是因为:

  • 所有控制器均已编写完成;
  • [Postman] 允许我们在无需所有视图的情况下向服务器发送请求。编写控制器时,必须做好处理任何视图都无法处理的请求的准备。切勿预先断定“此请求不可能发生”,必须进行验证;

32.6. 税费计算视图

Image

32.6.1. 视图概述

税费计算视图如下:

Image

该视图由三部分组成:

  • 1:顶部横幅由之前介绍过的片段 [v-bandeau.html] 生成;
  • 2:由片段 [v-calcul-impot.html] 生成的税费计算表单;
  • 3:由片段 [v-menu.html] 生成的包含两个链接的菜单;

税费计算视图由以下代码 [vue-calcul-impot.html] 生成:


<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <title>Application impôts</title>
</head>
 
<body>
<div class="container">
    <!-- headband -->
    {% include "fragments/v-bandeau.html" %}
    <!-- two-column line -->
    <div class="row">
        <!-- the menu -->
        <div class="col-md-3">
            {% include "fragments/v-menu.html" %}
        </div>
        <!-- calculation form -->
        <div class="col-md-9">
            {% include "fragments/v-calcul-impot.html" %}
        </div>
    </div>
    <!-- success stories -->
 
    {% if modèle.success %}
    <!-- a success alert is displayed -->
    <div class="row">
        <div class="col-md-3">
 
        </div>
        <div class="col-md-9">
            <div class="alert alert-success" role="alert">
                {{modèle.impôt}}</br>
                {{modèle.décôte}}</br>
                {{modèle.réduction}}</br>
                {{modèle.surcôte}}</br>
                {{modèle.taux}}</br>
            </div>
        </div>
    </div>
    {% endif %}
 
    {% if modèle.error %}
    <!-- 9-column error list -->
    <div class="row">
        <div class="col-md-3">
 
        </div>
        <div class="col-md-9">
            <div class="alert alert-danger" role="alert">
                Les erreurs suivantes se sont produites :
                <ul>{{modèle.erreurs | safe}}</ul>
            </div>
        </div>
    </div>
    {% endif %}
</div>
</body>
</html>

评论

  • 我们仅将尚未遇到的新功能注释掉;
  • 第 16 行:将视图的顶部横幅包含在视图的第一行 Bootstrap 行中;
  • 第 21 行:包含菜单,该菜单将占据视图第二行 Bootstrap 布局中的三列(第 18、20 行);
  • 第 25 行:包含税费计算表单,该表单将占据视图第二行 Bootstrap 布局中的九列(第 24 行)(第 18 行);
  • 第 30–46 行:如果税费计算成功 [model.success=True],则将税费计算结果显示在绿色框中(第 37–43 行)。 该框位于视图的第三行 Bootstrap 行(第 32 行),占据九列(第 36 行),位于三列空白区域(第 33–35 行)的右侧。因此,该框将位于税费计算表下方;
  • 第 48–61 行:如果税费计算失败 [model.error=True],则在粉色容器中显示错误信息(第 55–58 行)。 该框架位于视图的第三行 Bootstrap 区域(第 50 行),占据九个列(第 54 行),位于三个空列(第 51–53 行)的右侧。因此,该框架也将位于税费计算表单下方;

32.6.2. 片段 [v-calcul-impot.html]

片段 [v-calcul-impot.html] 显示了 Web 应用程序的税费计算表单:

[v-calcul-impot.html] 片段的代码如下:

Image


<!-- form HTML posted -->
<form method="post" action="/calculer-impot">
    <!-- 12-column message on blue background -->
    <div class="col-md-12">
        <div class="alert alert-primary" role="alert">
            <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
        </div>
    </div>
    <!-- form elements -->
    <fieldset class="form-group">
        <!-- first row of 9 columns -->
        <div class="row">
            <!-- 4-column wording -->
            <legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
            <!-- 5-column radio buttons-->
            <div class="col-md-5">
                <div class="form-check">
                    <input class="form-check-input" type="radio" name="marié" id="gridRadios1" value="oui" {{modèle.checkedOui}}>
                    <label class="form-check-label" for="gridRadios1">
                        Oui
                    </label>
                </div>
                <div class="form-check">
                    <input class="form-check-input" type="radio" name="marié" id="gridRadios2" value="non" {{modèle.checkedNon}}>
                    <label class="form-check-label" for="gridRadios2">
                        Non
                    </label>
                </div>
            </div>
        </div>
        <!-- second row of 9 columns -->
        <div class="form-group row">
            <!-- 4-column wording -->
            <label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
            <!-- 5-column numerical entry field for number of children -->
            <div class="col-md-5">
                <input type="number" min="0" step="1" class="form-control" id="enfants" name="enfants" placeholder="Nombre d'enfants à charge" value="{{modèle.enfants}}" required>
            </div>
        </div>
        <!-- third row of 9 columns -->
        <div class="form-group row">
            <!-- 4-column wording -->
            <label for="salaire" class="col-md-4 col-form-label">Salaire annuel net imposable</label>
            <!-- 5-column numeric input field for wages -->
            <div class="col-md-5">
                <input type="number" min="0" step="1" class="form-control" id="salaire" name="salaire" placeholder="Salaire annuel net imposable" aria-describedby="salaireHelp" value="{{modèle.salaire}}" required>
                <small id="salaireHelp" class="form-text text-muted">Arrondissez à l'euro inférieur</small>
            </div>
        </div>
        <!-- fourth row, [submit] button on 5 columns -->
        <div class="form-group row">
            <div class="col-md-5">
                <button type="submit" class="btn btn-primary">Valider</button>
            </div>
        </div>
    </fieldset>
 
</form>

评论

  • 第 2 行:该 HTML 表单将通过 [/calculer-impot] 网址(action 属性)提交(method 属性)。提交的值将是输入字段的值:
  • 表单中选中单选按钮的值:
      • 若选中 [Yes] 单选按钮,则 [married=yes](第 17–22 行)。其中 [married] 是第 18 行 [name] 属性的值,[yes] 是第 18 行 [value] 属性的值;
      • 若选中 [No] 单选按钮,则为 [married=no](第 23–28 行)。其中 [married] 是第 24 行 [name] 属性的值,[no] 是第 24 行 [value] 属性的值;
    • 表单第37行数字输入字段的值,形式为 [children=xx],其中 [children] 是第37行 [name] 属性的值,[xx] 是用户通过键盘输入的值;
    • 第46行数字输入字段的值,形式为 [salary=xx],其中 [salary] 是第46行 [name] 属性的值,[xx] 是用户通过键盘输入的值;

最后,提交的值将呈现为 [married=xx&children=yy&salary=zz] 的形式。

  • (待续)
    • 当用户点击第53行的[submit]按钮时,输入的值将被提交;
  • 第16–30行:两个单选按钮:

Image

这两个单选按钮属于同一个单选按钮组,因为它们具有相同的 [name] 属性(第 18、24 行)。浏览器确保在单选按钮组内,任何时候都只能选中一个。因此,点击其中一个会取消之前选中的那个;

  • 这些是单选按钮,是因为它们具有 [type="radio"] 属性(第 18、24 行);
  • 当表单显示时(在输入数据之前),必须选中其中一个单选按钮:要实现这一点,只需在相应的 <input type="radio"> 标签中添加 [checked='checked'] 属性。这可以通过动态变量来实现:
    • 第 18 行中的 [model.checkedYes]
    • 第 24 行的 [model->checkedNo]

这些变量将成为视图模板的一部分。

  • 第 37 行:一个数值输入字段 [type="number"],最小值为 0 [min="0"]。在现代浏览器中,这意味着用户只能输入 >=0 的数字。在这些现代浏览器中,可以通过点击上下滑块进行输入。 第 37 行中的 [step="1"] 属性表示滑块将以 1 为增量进行操作。因此,滑块仅接受从 0 到 n 之间以 1 为步长的整数值。对于手动输入,这意味着不接受带小数的数字;
  • 第 37 行:在某些视图中,children 输入字段必须预先填入该字段的上次输入内容。为此,我们使用 [value] 属性,该属性用于设置输入字段中显示的值。该值将是动态的,由 [model.children] 变量生成; Image
  • 第 37 行:[required] 属性强制用户输入数据,以便表单通过验证;
  • 第 46 行:薪资字段的说明与子女字段相同;
  • 第 53 行:[submit] 按钮会触发将输入的值通过 POST 请求发送至 URL [/calculer-impot](第 2 行);

Image

32.6.3. [v-menu.html] 片段

此片段会在税费计算表的左侧显示一个菜单:

Image

该片段的代码如下:


<!-- bootstrap menu -->
<nav class="nav flex-column">
    <!-- display a list of links HTML -->
    {% for optionMenu in modèle.optionsMenu %}
      <a class="nav-link" href="{{optionMenu.url}}">{{optionMenu.text}}</a>
    {% endfor %}
</nav>

评论

  • 第2–7行:HTML标签 [nav] 包裹着HTML文档中包含指向其他文档的导航链接的部分;
  • 第 5 行:HTML 标签 [a] 引入了一个导航链接:
  • [optionMenu.url]:是用户点击 [optionMenu.text] 链接时被引导到的 URL。随后浏览器将执行 [GET optionMenu.url] 操作。[optionMenu.url] 将是一个相对于应用程序根目录 [http://machine:port/path] 的绝对 URL。因此,在 [1] 中,我们将创建如下链接:
<a href=’/lister-simulations’>Liste des simulations</a>
  • 第 5 行:片段的 [modèle.optionsMenu] 模板将是一个列表,格式如下:
[‘Liste des simulations’:’/liste-simulations’,
‘Fin de session’:’/fin-session’]
  • 第 2、7 行:CSS 类 [nav, flex-column, nav-link] 是 Bootstrap 类,用于定义菜单的外观;

32.6.4. 视觉测试

我们将这些各种元素收集在 [Tests] 文件夹中,并为视图 [vue-calcul-impot.html] 创建一个测试模板:

Image

测试脚本 [test_vue_calcul_impot] 如下所示:

from flask import Flask, render_template, make_response

#  flask application
app = Flask(__name__, template_folder="../templates", static_folder="../static")

#  Home URL
@app.route('/')
def index():
    #  we encapsulate the paged data in the model
    modèle = {}
    #  form
    modèle["checkedOui"] = ""
    modèle["checkedNon"] = 'checked="checked"'
    modèle["enfants"] = 2
    modèle["salaire"] = 300000
    #  message of success
    modèle["success"] = True
    modèle["impôt"] = "Montant de l'impôt : 1000 euros"
    modèle["décôte"] = "Décôte : 15 euros"
    modèle["réduction"] = "Réduction : 20 euros"
    modèle["surcôte"] = "Surcôte : 0 euros"
    modèle["taux"] = "Taux d'imposition : 14 %"
    #  error message
    modèle["error"] = True
    erreurs = ["erreur1", "erreur2"]
    #  build a HTML list of errors
    content = ""
    for erreur in erreurs:
        content += f"<li>{erreur}</li>"
    modèle["erreurs"] = content
    #  menu
    modèle["optionsMenu"] = [
        {"text": 'Liste des simulations', "url": '/lister-simulations'},
        {"text": 'Fin de session', "url": '/fin-session'}]
    #  page display
    return make_response(render_template("views/vue-calcul-impot.html", modèle=modèle))

#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

注释

  • 第 9–34 行:初始化视图 [vue-calcul-impot.html] 中的所有动态部分,以及片段 [v-calcul-impot.html] [v-menu.html]
  • 第 36 行:显示视图 [vue-calcul-impot.html]

运行测试脚本 [test_vue_calcul_impot] 时,我们将得到以下结果:

我们对该视图进行调整,直到对视觉效果满意为止。随后,我们可以将该视图集成到当前正在开发的 Web 应用程序中。

Image

32.6.5. 计算视图模型

一旦确定了视图的外观,我们就可以在实际条件下计算视图模型。让我们回顾一下导致此视图的状态代码。这些代码可以在配置文件中找到:


            {
                # vue du calcul de l'impôt
                "états"[
                    # /authentifier-utilisateur réussite
                    200,
                    # /calculer-impot réussite
                    300,
                    # /calculer-impot échec
                    301,
                    # /afficher-calcul-impot
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },

因此,正是状态码 [200, 300, 301, 800] 触发了税费计算视图的显示。要理解这些代码的含义,我们可以参考在 JSON 应用程序上执行的 [Postman] 测试:

  • [authenticate-user-200]:200 是 [authenticate-user] 操作成功后的状态码;随后将显示空白的税费计算表单;
  • [calculate-tax-300]:300 是 [calculate-tax] 操作成功后返回的状态码。随后将显示计算表单,其中包含已输入的数据和税额。用户随后可以进行另一项计算;
  • 状态码 [301] 表示税额计算错误;
  • 状态码 [800] 将在后文讨论。目前我们尚未遇到该情况;

既然我们已经知道何时应显示税费计算表单,就可以在 [ModelForCalculImpotView] 类中定义其模型:

Image

from flask import Request
from werkzeug.local import LocalProxy

from InterfaceModelForView import InterfaceModelForView

class ModelForCalculImpotView(InterfaceModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        #  encapsulate view data in model
        modèle = {}
        #  application status
        état = résultat["état"]
        #  the model depends on the state
        if état in [200, 800]:
            #  initial display of an empty form
            modèle["success"] = False
            modèle["error"] = False
            modèle["checkedNon"] = 'checked="checked"'
            modèle["checkedOui"] = ""
            modèle["enfants"] = ""
            modèle["salaire"] = ""
        elif état == 300:
            #  successful calculation - result display
            modèle["success"] = True
            modèle["error"] = False
            modèle["impôt"] = f"Montant de l'impôt : {résultat['réponse']['impôt']} euros"
            modèle["décôte"] = f'Décôte : {résultat["réponse"]["décôte"]} euros'
            modèle["réduction"] = f"Réduction : {résultat['réponse']['réduction']} euros"
            modèle["surcôte"] = f'Surcôte : {résultat["réponse"]["surcôte"]} euros'
            modèle["taux"] = f"Taux d'imposition :  {résultat['réponse']['taux'] * 100} %"
            #  form restored with values entered
            modèle["checkedOui"] = 'checked="checked"' if request.form.get("marié") == "oui" else ""
            modèle["checkedNon"] = 'checked="checked"' if request.form.get("marié") == "non" else ""
            modèle["enfants"] = request.form.get("enfants")
            modèle["salaire"] = request.form.get("salaire")
        elif état == 301:
            #  error encountered - form restored with values entered
            modèle["checkedOui"] = 'checked="checked"' if request.form.get("marié") == "oui" else ""
            modèle["checkedNon"] = 'checked="checked"' if request.form.get("marié") == "non" else ""
            modèle["enfants"] = request.form.get("enfants")
            modèle["salaire"] = request.form.get("salaire")
            #  error
            modèle["success"] = False
            modèle["error"] = True
            modèle["erreurs"] = ""
            for erreur in résultat['réponse']:
                modèle['erreurs'] += f"<li>{erreur}</li>"

        #  menu options
        modèle["optionsMenu"] = [
            {"text": 'Liste des simulations', "url": '/lister-simulations'},
            {"text": 'Fin de session', "url": '/fin-session'}]
        #  we render the model
        return modèle

注释

  • 第 12 行:要显示的视图取决于控制器返回的状态码;
  • 第14–21行:显示一个空表单;
  • 第22–35行:税费计算成功。再次显示输入的数值和税费金额;
  • 第36–47行:税费计算失败的情况;
  • 第 49–52 行:计算两个菜单选项;

32.6.6. 测试 [Postman]

我们使用 [init-session-html-700] 请求初始化一个 HTML 会话,然后通过 [authenticate-user-200] 请求进行身份验证。接下来,我们使用以下 [calculate-tax-300] 请求:

服务器响应如下:

Image

Image

现在让我们尝试以下请求 [calculate-tax-301]

Image

服务器的响应如下:

现在我们来尝试一个意外的情况:POST请求中缺少参数。这种情况在应用程序的正常运行中是不可能发生的。但任何人都可以像我们现在这样“篡改”HTTP请求:

Image

Image

  • [6] 中,我们取消勾选了 POST 参数 [married]

服务器的响应如下:

Image

  • [3] 中,服务器的错误信息;

在此应用中,我们有两种选择。我们可以为该错误情况分配一个状态码,使其重定向至意外错误页面。在此应用中,我们为每个控制器选择了两个状态码:

  • [xx0]:表示成功;
  • [xx1]:表示失败;

对于失败情况,我们可以使用不同的状态码来实现更精细的错误处理。例如,我们可以使用:

  • [xx1]: 用于在引发错误的页面上显示错误;
  • [xx2]: 用于应用程序正常使用过程中出现的意外错误;

32.7. 模拟列表视图

Image

32.7.1. 视图概述

显示模拟列表的视图如下:

Image

由代码 [vue-liste-simulations.html] 生成的视图包含三个部分:

  • 1:顶部横幅由前文已介绍的片段 [v-banner.html] 生成;
  • 3:由片段 [v-simulation-list.html] 生成的模拟列表;
  • 2:一个包含两个链接的菜单,由之前介绍过的片段 [v-menu.html] 生成;

模拟视图由以下代码 [vue-liste-simulations.html] 生成:


<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <title>Application impôts</title>
</head>
 
<body>
<div class="container">
    <!-- headband -->
    {% include "fragments/v-bandeau.html" %}
    <!-- two-column line -->
    <div class="row">
        <!-- three-column menu-->
        <div class="col-md-3">
            {% include "fragments/v-menu.html" %}
 
        </div>
        <!-- list of simulations on 9 columns-->
        <div class="col-md-9">
            {% include "fragments/v-liste-simulations.html" %}
        </div>
    </div>
</div>
</body>
</html>

评论

  • 第16行:添加应用程序横幅 [1]
  • 第21行:添加菜单 [2]。它将以三列形式显示在横幅下方;
  • 第26行:插入模拟表格 [3]。它将显示在横幅下方、菜单右侧,呈九列布局;

我们已经对该视图的三个片段中的两个进行了说明:

  • [v-banner.html]:在链接部分;
  • [v-menu.html]:在链接部分;

片段 [v-liste-simulations.html] 如下所示:


{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
    <h4>Votre liste de simulations est vide</h4>
</div>
{% endif %}
 
{% if modèle.simulations is defined and modèle.simulations|length!=0 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
    <h4>Liste de vos simulations</h4>
</div>
 
<!-- simulation table -->
<table class="table table-sm table-hover table-striped">
    <!-- headers of the six table columns -->
    <thead>
    <tr>
        <th scope="col">#</th>
        <th scope="col">Marié</th>
        <th scope="col">Nombre d'enfants</th>
        <th scope="col">Salaire annuel</th>
        <th scope="col">Montant impôt</th>
        <th scope="col">Surcôte</th>
        <th scope="col">Décôte</th>
        <th scope="col">Réduction</th>
        <th scope="col">Taux</th>
        <th scope="col"></th>
    </tr>
    </thead>
    <!-- table body (data displayed) -->
    <tbody>
    <!-- display each simulation by browsing the simulation table -->
    {% for simulation in modèle.simulations %}
 
    <!-- display a table row with 6 columns - <tr> tag -->
    <!-- column 1: row header (simulation no.) - <th scope='row' tag -->
    <!-- column 2: parameter value [married] - <td> tag -->
    <!-- column 3: parameter value [children] - <td> tag -->
    <!-- column 4: parameter value [salary] - <td> tag -->
    <!-- column 5: [tax] parameter value - <td> tag -->
    <!-- column 6: parameter value [surcôte] - <td> tag -->
    <!-- column 7: parameter value [discount] - <td> tag -->
    <!-- column 8: parameter value [reduction] - <td> tag -->
    <!-- column 9: parameter value [rate] (of tax) - <td> tag -->
    <!-- column 10: link to delete simulation - <td> tag -->
    <tr>
        <th scope="row">{{simulation.id}}</th>
        <td>{{simulation.marié}}</td>
        <td>{{simulation.enfants}}</td>
        <td>{{simulation.salaire}}</td>
        <td>{{simulation.impôt}}</td>
        <td>{{simulation.surcôte}}</td>
        <td>{{simulation.décôte}}</td>
        <td>{{simulation.réduction}}</td>
        <td>{{simulation.taux}}</td>
        <td><a href="/supprimer-simulation/{{simulation.id}}">Supprimer</a></td>
    </tr>
    {% endfor %}
    </tr>
    </tbody>
</table>
{% endif %}

评论

  • 使用 <table> 标签创建了一个 HTML 表格(第 15 行和第 62 行);
  • 表格的列标题在 <thead> 标签(表格头,第 17 行和第 30 行)中定义。 <tr> 标签(表格行,第 18 行和第 29 行)定义一行。第 19–28 行:<th> 标签(表格标题)定义列标题。共有十个。[scope="col"] 表示该标题适用于列。[scope="row"] 表示该标题适用于行;
  • 第 32–61 行:<tbody> 标签包围了表格显示的数据;
  • 第 47–58 行:<tr> 标签框定表格的一行;
  • 第 48 行:<th scope=’row’> 标签定义行标题。浏览器会突出显示该标题;
  • 第 49–57 行:每个 td 标签(表格数据)定义该行的一列;
  • 第 34 行:模拟列表位于 [model.simulations] 模型中,该模型是一个字典列表;
  • 第 57 行:用于删除模拟的链接。URL 使用了该行中显示的模拟编号;

32.7.2. 可视化测试

我们为视图 [view-simulation-list.html] 创建一个测试脚本:

脚本 [test_simulation_list_view] 如下:

Image

from flask import Flask, make_response, render_template

#  flask application
app = Flask(__name__, template_folder="../templates", static_folder="../static")

#  Home URL
@app.route('/')
def index():
    #  we encapsulate the paged data in the model
    modèle = {}
    #  put the simulations in the format expected by the page
    modèle["simulations"] = [
        {
            "id": 7,
            "marié": "oui",
            "enfants": 2,
            "salaire": 60000,
            "impôt": 448,
            "décôte": 100,
            "réduction": 20,
            "surcôte": 0,
            "taux": 0.14
        },
        {
            "id": 19,
            "marié": "non",
            "enfants": 2,
            "salaire": 200000,
            "impôt": 25600,
            "décôte": 0,
            "réduction": 0,
            "surcôte": 8400,
            "taux": 0.45
        }
    ]
    #  menu
    modèle["optionsMenu"] = [
        {"text": "Calcul de l'impôt", "url": '/afficher-calcul-impot'},
        {"text": 'Fin de session', "url": '/fin-session'}]
    #  page display
    return make_response(render_template("views/vue-liste-simulations.html", modèle=modèle))

#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

注释

  • 第 12–35 行:向模型中添加了两个仿真
  • 第37–39行:菜单选项表;

让我们运行此脚本来显示该视图。我们将得到以下结果:

Image

我们将继续完善此视图,直到对其外观感到满意。随后,我们可以将其集成到当前正在开发的 Web 应用程序中。

32.7.3. 计算视图模型

确定视图的外观后,我们可以继续在实际条件下计算视图模型。让我们回顾一下导致此视图显示的状态代码。这些代码可在配置文件中找到:

Image


            {
                # vue de la liste des simulations
                "états"[
                    # /lister-simulations
                    500,
                    # /supprimer-simulation
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }

因此,正是状态码 [500, 600] 触发了模拟视图的显示。要理解这些代码的含义,我们可以参考在 JSON 应用程序上执行的 [Postman] 测试:

  • [list-simulations-500]: 500 是 [list-simulations] 操作成功后的状态码:随后将显示用户执行的模拟列表;
  • [delete-simulation-600]: 600 是 [delete-simulation] 操作成功后的状态码。随后将显示删除操作后生成的新的模拟列表;

既然我们已经知道何时应显示模拟列表,就可以在 [ModelForListeSimulationsView] 类中定义其模型:

from flask import Request
from werkzeug.local import LocalProxy

from InterfaceModelForView import InterfaceModelForView

class ModelForListeSimulationsView(InterfaceModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        #  we encapsulate the paged data in the model
        modèle = {}
        #  simulations are found in the response of the controller that executed the action
        #  as an array of TaxPayer dictionaries
        modèle["simulations"] = résultat["réponse"]
        #  menu
        modèle["optionsMenu"] = [
            {"text": "Calcul de l'impôt", "url": '/afficher-calcul-impot'},
            {"text": 'Fin de session', "url": '/fin-session'}]
        #  we render the model
        return modèle

注释

  • 第 13 行:要显示的模拟结果位于 [result["response"]] 中;
  • 第 15–17 行:要显示的菜单选项;

32.7.4. [Postman] 测试

我们

  • 初始化一个 HTML 会话;
  • 进行身份验证;
  • 执行三项税费计算;

[lister-simulations-500] 测试使我们能够获取 500 状态码。它对应于查看模拟结果的请求:

Image

服务器响应如下:

Image

[delete-simulation-600] 测试返回 600 状态码。在此,我们将删除第 2 号模拟。

返回的结果是一个模拟列表,其中缺少一个模拟:

Image

Image

32.8. 查看意外错误

在此,我们将“意外错误”定义为在 Web 应用程序正常使用过程中不应发生的错误。例如,在未经过身份验证的情况下请求税费计算。没有任何机制能阻止用户直接在浏览器中输入 URL [/tax-calculation]。此外,正如我们所见,用户还可以向 URL [/tax-calculation] 发送 POST 请求,而无需包含预期的参数。 我们已看到,我们的 Web 应用程序知道如何正确响应此请求。我们将“意外错误”定义为 HTML 应用程序内部不应发生的错误。如果它确实发生,则很可能有人正在试图“入侵”该应用程序。 出于教学目的,我们决定在这些情况下显示一个错误页面。实际上,我们也可以重新显示之前发送给客户端的页面。要实现这一点,我们只需将上次发送的 HTML 响应存储在会话中。一旦发生意外错误,我们就返回该响应。这样,由于显示的页面没有变化,用户会觉得服务器对他们的错误没有做出响应。

32.8.1. 视图概述

Image

显示意外错误的视图如下:

Image

由代码 [vue-erreurs.html] 生成的视图包含三个部分:

  • 1:顶部横幅由已展示的片段 [v-banner.html] 生成;
  • 2:意外错误;
  • 3:一个包含三个链接的菜单,由已展示的片段 [v-menu.html] 生成;

意外错误的视图由以下脚本 [error-view.html] 生成:


<!-- document HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <title>Application impôts</title>
</head>
 
<body>
<div class="container">
    <!-- 12-column banner -->
    {% include "fragments/v-bandeau.html" %}
    <!-- two-section line -->
    <div class="row">
        <!-- 3-column menu-->
        <div class="col-md-3">
            {% include "fragments/v-menu.html" %}
        </div>
        <!-- 9-column error list -->
        <div class="col-md-9">
            <div class="alert alert-danger" role="alert">
                Les erreurs inattendues suivantes se sont produites :
                <ul>{{modèle.erreurs|safe}}</ul>
            </div>
        </div>
    </div>
</div>
</body>
</html>

评论

  • 第16行:添加应用程序横幅 [1]
  • 第 21 行:添加菜单 [3]。它将以三列形式显示在横幅下方;
  • 第24–29行:将错误区域以九列布局显示;
  • 第 25 行:该显示区域将位于一个粉色背景的 Bootstrap 容器中;
  • 第 26 行:介绍性文本;
  • 第 27 行:<ul> 标签包裹着一个项目符号列表。该项目符号列表由模板 [template.errors] 提供;

我们已经对该视图的两个片段进行了说明:

  • [v-banner.html]:在链接部分;
  • [v-menu.html]:位于链接部分;

32.8.2. 视觉测试

我们为 [vue-erreurs.html] 视图编写了一个测试脚本:

Image

from flask import Flask, render_template, make_response

#  flask application
app = Flask(__name__, template_folder="../templates", static_folder="../static")

#  Home URL
@app.route('/')
def index():
    #  we encapsulate the paged data in the model
    modèle = {}
    #  build a HTML list of errors
    content = ""
    for erreur in ["erreur1", "erreur2"]:
        content += f"<li>{erreur}</li>"
    modèle["erreurs"] = content
    #  menu options
    modèle["optionsMenu"] = [
        {"text": "Calcul de l'impôt", "url": '/calculer-impot'},
        {"text": 'Liste des simulations', "url": '/lister-simulations'},
        {"text": 'Fin de session', "url": '/fin-session'}]

    #  page display
    return make_response(render_template("views/vue-erreurs.html", modèle=modèle))

#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

注释

  • 第 11–15 行:构建错误的 HTML 列表;
  • 第17–20行:菜单选项数组;

让我们运行这个脚本。我们会得到以下结果:

我们不断调整此视图,直到对视觉效果满意为止。随后,我们可以将其集成到当前正在开发的 Web 应用程序中。

Image

32.8.3. 计算视图模型

Image

一旦确定了视图的外观,我们就可以在实际条件下计算视图模型。让我们回顾一下导致此视图的状态代码。这些代码可以在配置文件中找到:


        # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
        "views"[
            {
                # vue d'authentification
                "états": [
                    # /init-session réussite
                    700,
                    # /fin-session
                    400,
                    # /authentifier-utilisateur échec
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # vue du calcul de l'impôt
                "états"[
                    # /authentifier-utilisateur réussite
                    200,
                    # /calculer-impot réussite
                    300,
                    # /calculer-impot échec
                    301,
                    # /afficher-calcul-impot
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # vue de la liste des simulations
                "états"[
                    # /lister-simulations
                    500,
                    # /supprimer-simulation
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],
        # vue des erreurs inattendues
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

以下是第 3–41 行中不会导致显示 HTML 视图的状态码,这些状态码会导致显示意外错误视图。

视图模型 [view-errors.html] 由以下 [ModelForErrorsView] 类计算得出:

from flask import Request
from werkzeug.local import LocalProxy

from InterfaceModelForView import InterfaceModelForView

class ModelForErreursView(InterfaceModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        #  the model
        modèle = {}
        #  errors
        modèle["erreurs"] = ""
        for erreur in résultat['réponse']:
            modèle['erreurs'] += f"<li>{erreur}</li>"
        #  menu
        modèle["optionsMenu"] = [
            {"text": "Calcul de l'impôt", "url": '/afficher-calcul-impot'},
            {"text": 'Liste des simulations', "url": '/lister-simulations'},
            {"text": 'Fin de session', "url": '/fin-session'}]
        #  we render the model
        return modèle

注释

  • 第 11-14 行:计算视图 [view-errors.html] 所使用的模型 [model.errors]
  • 第 16-197 行:计算片段 [v-menu.html] 所使用的模型 [model.optionsMenu]

32.8.4. 测试 [Postman]

我们执行:

  • 执行操作 [/init-session/html]
  • 随后执行操作 [/init-session/x]

随后生成的 HTML 响应如下:

Image

32.9. 应用程序菜单操作的实现

在此我们将讨论菜单操作的实现。让我们回顾一下之前遇到的链接的含义

查看
链接
目标
角色
税款计算
[模拟列表]
[/list-simulations]
请求模拟列表
  
[会话结束]
模拟列表
[税费计算]
[/显示税费计算]
查看税费计算
  
[结束会话]
意外错误
[税费计算]
[/显示税费计算]
查看税费计算
  
[模拟列表]
  
[结束会话]

需要注意的是,点击链接会向链接的目标发起一个 GET 请求。动作 [/lister-simulations、/end-session] 是通过 GET 操作实现的,这使得我们可以将其用作链接目标。当通过 POST 请求执行该动作时,除非结合 JavaScript 使用,否则将无法通过链接进行操作。

32.9.1. [/display-tax-calculation] 操作

从上述列出的操作来看,[/display-tax-calculation] 操作似乎尚未实现。这属于两个视图之间的导航操作:JSON 或 XML 服务器没有理由实现它,因为它们不具备视图的概念。引入这一概念的是 HTML 服务器。

因此,我们需要实现 [/display-tax-calculation] 操作。这将使我们能够回顾在服务器内实现操作的流程。

首先,我们需要添加一个新的二级控制器。我们将它命名为 [AfficherCalculImpotController]

Image

该控制器必须添加到 [config] 配置文件中:

    #  controllers
    from AfficherCalculImpotController import AfficherCalculImpotController
    from AuthentifierUtilisateurController import AuthentifierUtilisateurController
    from CalculerImpotController import CalculerImpotController
    from CalculerImpotsController import CalculerImpotsController
    from FinSessionController import FinSessionController
    from GetAdminDataController import GetAdminDataController
    


        #  authorized shares and their controllers
        "controllers": {
            #  initialization of a calculation session
            "init-session": InitSessionController(),
            #  user authentication
            "authentifier-utilisateur": AuthentifierUtilisateurController(),
            #  tax calculation in individual mode
            "calculer-impot": CalculerImpotController(),
            #  batch mode tax calculation
            "calculer-impots": CalculerImpotsController(),
            #  list of simulations
            "lister-simulations": ListerSimulationsController(),
            #  deleting a simulation
            "supprimer-simulation": SupprimerSimulationController(),
            #  end of calculation session
            "fin-session": FinSessionController(),
            #  display tax calculation view
            "afficher-calcul-impot": AfficherCalculImpotController(),
            #  obtaining data from tax authorities
            "get-admindata": GetAdminDataController(),
            #  main controller
            "main-controller": MainController()
        },

       #  HTML views and their models depend on the state rendered by the controller
        "views": [
            {
                #  authentication view
                
            },
            {
                #  tax calculation
                "états": [
                    #  /authentifier-user success
                    200,
                    #  /calculate-tax-success
                    300,
                    #  /calculate-tax failure
                    301,
                    #  /show-tax-calculation
                    800
                ],
                "view_name": "views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
            }
        ],
  • 第 2 行:新的控制器;
  • 第 28 行:新的操作及其控制器;
  • 第 51 行:新控制器将返回状态码 800。切换视图时不应出现错误。显示的视图是我们已经学习、讲解并测试过的 [vue-calcul-import.html] 视图;

[AfficherCalculImpotController] 控制器将如下所示:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class AfficherCalculImpotController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  change of view - just a status code to set
        return {"action": action, "état": 800, "réponse": ""}, status.HTTP_200_OK

评论

  • 第 6 行:与其他辅助控制器一样,新控制器实现了 [InterfaceController] 接口;
  • 第 13 行:视图更改的实现很简单:只需返回与目标视图关联的状态码,如上所示,这里是 800;

32.9.2. [/end-session] 操作

[/end-session] 操作具有特殊性。它不会直接跳转到视图,而是触发重定向。请注意,重定向在 [config] 文件中的配置如下:


        # redirections
        "redirections"[
            {
                "états": [
                    400,  # /fin-session réussi
                ],
                # redirection vers
                "to""/init-session/html",
            }
        ],

该应用程序中仅有一个重定向:

  • 当控制器返回状态码 [400](第 5 行)时,客户端必须被重定向到 URL [http://machine:port/chemin/init-session/html](第 8 行);

状态码 [400][/fin-session] 操作成功后返回的代码。那么,为什么必须将客户端重定向到 URL [/init-session/html] 呢?因为 [/fin-session] 操作会从 Web 会话中移除会话类型。此时我们已无法识别当前处于 HTML 会话中,因此需要进行重定向。我们通过 [/init-session/html] 操作来实现这一点。

HTML 重定向由 [HtmlResponse] 类处理:

   def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        #  the HTML response depends on the status code returned by the controller
        état = résultat["état"]

        #  do I need to redirect?
        for redirection in config["redirections"]:
            #  conditions requiring redirection
            états = redirection["états"]
            if état in états:
                #  you need to redirect
                return redirect(f"{redirection['to']}"), status.HTTP_302_FOUND

        #  a state corresponds to a view
        #  search for it in the list of views
 ..
  • 第 6–12 行处理重定向;
  • 第 7 行:config['redirections'] 是一个重定向列表。每个重定向都是一个包含以下键的字典:
    • [states]:导致重定向的控制器返回的状态;
    • [to]:重定向的 URL;
  • 第 7–12 行:遍历重定向列表;
  • 第 9 行:对于每个重定向,我们获取导致该重定向的状态码;
  • 第 10 行:如果检测到的状态码在此列表中,则执行重定向(见第 12 行);
  • 第 12 行:请注意,[build_http_response] 方法必须返回一个包含两个元素的元组:
    • [response]:要发送的 HTTP 响应。该响应通过 [redirect] 函数构建,其参数为重定向 URL;
    • [status_code]:HTTP 响应状态码,此处为 [status.HTTP_302_FOUND],该代码指示客户端进行重定向;

让我们运行一个 [Postman] 测试。我们:

  • 初始化一个 HTML 会话 [init-session/html]
  • 进行身份验证 [/authenticate-user]
  • 结束会话 [/end-session];

Image

服务器的响应如下:

Image

我们已经获取到了身份验证视图。这正是我们预期的结果。现在,让我们看看它是如何获取到的。切换到 [Postman] 控制台(Ctrl-Alt-C):

Image

  • [1] 中,执行了 [/end-session] 操作;
  • [2-3] 中,服务器返回的 HTTP 状态码 302 告知客户端正在进行重定向;
  • [4] 中,客户端 [Postman] 执行了重定向;

32.10. 在真实环境中测试 HTML 应用程序

代码已编写完毕,每个操作也已通过 [Postman] 进行了测试。我们仍需在实际场景中测试视图流。我们需要一种方法来初始化 HTML 会话。我们知道需要向服务器发送 [/init-session/html] 请求。但这并不是一个很实用的 URL。我们更希望从 [/] URL 开始。

我们在主脚本 [main] 中编写了以下路由:

from flask import request, Flask, session, url_for, redirect


@app.route('/', methods=['GET'])
def index() -> tuple:
    #  redirect to /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

#  init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()
  • 第 4–7 行:处理 [/] 路由。Web 应用程序的入口点将是 URL [/init-session/html](第 10 行)。此外,在第 7 行,我们将客户端重定向到此 URL:
  • [url_for] 函数在第 1 行被导入。此处它有两个参数(第 7 行):
      • 第一个参数是某个路由函数的名称,本例中即第 11 行中的函数。我们可以看到,该函数期望接收一个名为 [type_response] 的参数,即客户端请求的响应类型(json、xml、html);
      • 第二个参数接收第 11 行中的参数名 [type_response],并为其赋值。如果存在其他参数,我们将对每个参数重复此过程;
      • 它返回与通过两个参数指定的函数关联的 URL。在此,这将返回第 10 行中的 URL,其中参数被其值 [/init-session/html] 替换;
    • [redirect] 函数在第 1 行被导入。其作用是向客户端发送一个 HTTP 重定向头部:
      • 第一个参数是客户端应被重定向到的 URL;
      • 第二个参数是发送给客户端的 HTTP 响应状态码。代码 [status.HTTP_302_FOUND] 对应于 HTTP 重定向;

准备就绪。现在让我们看几个视图序列。

在浏览器中,我们启用开发者工具(Chrome、Firefox、Edge 中按 F12),并请求启动 URL [http://localhost:5000/]。服务器的响应如下:

Image

如果查看客户端与服务器之间的网络流量:

Image

  • 可以看到,在 [4, 5] 处,浏览器接收到了一条重定向请求,指向 URL [/init-session/html]

让我们填写收到的表单;

Image

然后我们进行几次模拟:

Image

Image

让我们请求模拟列表:

Image

删除第一个模拟:

Image

结束本次会话:

Image

欢迎读者尝试其他测试。