Skip to content

32. 第 12 版的 HTML 模式

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

  • 基于HTML应用程序的视图,我们将定义Web应用程序需要实现的操作。此处我们将使用实际视图,但也可以是纸面上的视图;
  • 基于这些操作,我们将定义应用程序 URL 的服务;
  • 我们将通过一个提供 jSON 的服务器来实现这些 URL 服务。这样可以定义 Web 服务器的框架,而无需考虑要提供的 HTML 页面。 我们将使用 Postman 测试这些 URL 服务;
  • 随后将使用控制台客户端测试我们的 jSON 服务器;
  • 一旦 jSON 服务器通过验证,我们将开始编写 HTML 应用程序;

目前 jSON 和 XML 服务器已正常运行。现在可以转到 HTML 服务器。 我们将看到,该服务器沿用了为 jSON / XML 服务器开发的所有架构,并为其增加了 HTML 视图管理功能。

32.1. MVC架构

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

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

  • 1 - 请求

请求的 URL 将采用 http://machine:port/action/param1/param2/… 的形式[Contrôleur principal] 将使用配置文件将请求“路由”到正确的控制器。 为此,它将使用 URL 中的 [action] 字段。URL 和 [param1/param2/…] 的其余部分由可选参数组成,这些参数将传递给操作。 此处的MVC中的C即为字符串[Contrôleur principal, Contrôleur / Action]。如果没有任何控制器能处理所请求的操作,Web服务器将返回“未找到所请求的URL”的响应。

  • 2 - 处理
  • 所选操作 [2a] 可以利用 [Contrôleur principal] 传递给它的 parami 参数。这些参数可能来自两个来源:
      • URL 中的 [/param1/param2/…] 路径,
      • 客户端请求正文中提交的参数;
    • 在处理用户请求时,该操作可能需要 [métier][2b] 层。一旦处理完客户端的请求,该操作可能会触发各种响应。一个典型的例子是:
      • 若请求未能正确处理,则返回错误响应;
      • 否则返回确认响应;
    • [Contrôleur / Action] 将向主控制器返回其响应 [2c] 以及一个状态码。这些状态码将唯一地表示应用程序当前的状态。该状态码要么是成功码,要么是错误码;
  • 3 - 响应
    • 根据客户端请求的响应类型(jSON、 XML 或 HTML,[Contrôleur principal] 将实例化 [3a] 以生成相应的响应类型,并要求其将响应发送给客户端。 [Contrôleur principal] 将向其传递由已执行的 [Contrôleur / Action] 提供的响应和状态码;
    • 如果所需的响应类型为 jSON 或 XML,则所选响应将对 [Contrôleur / Action] 提供的响应进行格式化,并通过 [3c] 发送出去。 能够处理此响应的客户端可以是 Python 控制台脚本,也可以是托管在 HTML 页面中的 JavaScript 脚本;
    • 如果期望的响应类型为 HTML,则所选响应将根据给定的状态码,从 HTML 到 [Vuei] 的视图中选择 [3b]。 这就是 MVC 中的 V。一个状态码对应一个唯一的视图。该视图 V 将显示已执行的 [Contrôleur / Action] 的响应。 它使用 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. 视图介绍

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

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

税款计算视图:

Image

  • [1] 中,调用显示该视图的操作 [/authentifier-utilisateur]
  • [2] 中,点击按钮 [Valider] 将触发操作 [/calculer-impot],并传递三个参数 [2-5]
  • 点击链接 [6] 将触发操作 [/lister-simulations](不带参数);
  • 点击链接 [7] 将触发操作 [/fin-session],且不带参数;

第三个视图是经过身份验证的用户进行的模拟:

Image

  • [1] ,[/lister-simulations] 操作会显示此视图;
  • [2] 中,点击链接 [Supprimer] 将触发带有一个参数的操作 [/supprimer-simulation],该参数为列表中要删除的模拟编号;
  • 点击链接 [3] 将触发操作 [/afficher-calcul-impot](无参数),重新显示税款计算视图;
  • 点击链接 [4] 将触发无参数的操作 [/fin-session]

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

  • [1] 中:用户手动输入了 URL。但在本例中并未进行模拟。因此会收到错误信息 [2]。 我们已知该错误信息。此前在 jSON / XML 中也出现过。 我们将此类错误称为“意外错误”,因为它在应用程序的正常使用过程中不会发生。只有当用户手动输入 URL 时,才会出现此类错误; Image
  • 若发生意外错误,可通过 [3-5] 链接返回其他三个视图之一;

回顾URL服务器的各项服务jSON / XML:

操作
角色
执行上下文
/init-session
用于设定所需响应的类型(json、xml、html)
请求 GET
可随时发出
/authentifier-utilisateur
授权或拒绝用户登录
请求 POST。
该请求必须包含两个POST参数 [user, password]
仅当会话类型(json、xml、html)已知时才可发出
/计算-税款
进行税款计算模拟
请求 POST。
该请求必须包含三个POST参数:[marié, enfants, salaire]
仅当会话类型(json、xml、html)已知且用户已通过身份验证时才可发出
/lister-simulations
请求查看自会话开始以来执行的模拟列表
请求 GET。
仅当会话类型(json、xml、html)已知且用户已通过身份验证时,方可发出
/删除-模拟/编号
从模拟列表中删除一个模拟
请求 GET。
仅当会话类型(json、xml、html)已知且用户已通过身份验证时,方可发出
/显示-税款计算
显示税款计算页面 HTML
请求 GET
仅当会话类型(json、xml、html)已知且用户已通过身份验证时才可发出
/结束-会话
结束模拟会话。
技术上,旧的 Web 会话将被删除,并创建一个新的会话
仅当会话类型(json、xml、html)已知且用户已通过身份验证时才可发出

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

32.4. 视图配置

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

  • [résultat] 是键值对字典 [action, état, réponse]
  • [status_code] 是将返回给客户端的响应 HTTP 的状态码;

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


        # 视图 HTML 及其模板取决于控制器返回的状态
        "views"[
            {
                # 身份验证视图
                "états": [
                    # /init-session 成功
                    700,
                    # /用户认证失败
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # 税款计算视图
                "états"[
                    # /用户认证成功
                    200,
                    # /计算-税款 成功
                    300,
                    # /计算-税款 失败
                    301,
                    # /显示-税款计算
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # 模拟列表视图
                "états"[
                    # /列出模拟
                    500,
                    # /删除-模拟
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],

        # 意外错误视图
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

        # 重定向
        "redirections"[
            {
                "états": [
                    400,  # /会话结束成功
                ],
                # 重定向至
                "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-bandeau.html] 生成;
  • 片段 [2] 由片段 [v-authentification.html] 生成;

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


<!-- 文档 HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- 必需的元标签 -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- BootstrapCSS -->
    <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">
    <!-- 横幅 -->
    {% include "fragments/v-bandeau.html" %}
    <!-- 双栏布局 -->
    <div class="row">
        <div class="col-md-9">
            {% include "fragments/v-authentification.html" %}
        </div>
    </div>
    <!-- 如果出现错误,则显示错误提示 -->
    {% 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'>标签设置了视口的初始显示方式:占据显示屏幕的整个宽度(width),保持原始比例(initial-scale),且不缩放以适应较小的屏幕尺寸(shrink-to-fit);
  • 第 9 行:<link rel='stylesheet'> 标签指定了控制视图外观的 CSS 文件。此处我们使用的是 CSS Bootstrap 4.4.1 [https://getbootstrap.com/docs/4.0/getting-started/introduction/] 框架 ;
  • 第 10 行:<title> 标签设定了页面的标题:

Image

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

Image

  • 第 28 行:一段文本;
  • 第 29 行:标签 HTML <ul>(无序列表)用于显示带项目符号的列表。列表中的每个元素必须采用 <li>元素</li> 的语法。此处显示的是 [modèle.erreurs] 的值。 该值由过滤器 [safe] 进行过滤(检测是否包含 |)。默认情况下,当需要向浏览器发送字符串时,Flask 会“中和”其中可能存在的所有 HTML 标签,以防止浏览器对其进行解析。 但有时我们需要让浏览器进行解析。例如,当字符串 [modèle.erreurs] 包含用于界定列表项的 HTML 标签 <li> 和 </li> 时,就属于这种情况。 此时,我们使用过滤器 [safe],它会告知 Flask 该显示的字符串是安全的(safe),因此它不应屏蔽其中发现的 HTML 标签;

请记住此代码中需要定义的动态元素:

  • [modèle.error]:用于显示错误消息;
  • [modèle.erreurs]:一个错误消息列表(按HTML中的定义);

32.5.2. 片段 [v-bandeau.html]

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

Image

片段 [v-bandeau.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 行:横幅被封装在一个名为 Jumbotron 的 Bootstrap 部分中([<div class="jumbotron">])。该 Bootstrap 类通过特殊样式处理显示内容,使其更加醒目;
  • 第3-12行:一行Bootstrap代码;
  • 第4-6行:一张[img]图片被放置在该行的前四列中;
  • 第 5 行:语法:
{{ url_for('static', filename='images/logo.jpg') }}

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

  • 第7-11行:该行的其余8列(需注意总共12列)将用于放置一段文本(第9行),并以大号字体显示(<h1>,第8-10行);

32.5.3. [v-authentification.html]片段

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

Image

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


<!-- 表单 HTML - 使用操作 [authentifier-utilisateur] 提交其值 -->
<form method="post" action="/authentifier-utilisateur">

    <!-- 标题 -->
    <div class="alert alert-primary" role="alert">
        <h4>Veuillez vous authentifier</h4>
    </div>

    <!-- Bootstrap 表单 -->
    <fieldset class="form-group">
        <!-- 第一行 -->
        <div class="form-group row">
            <!-- 标签 -->
            <label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
            <div class="col-md-4">
                <!-- 文本输入框 -->
                <input type="text" class="form-control" id="user" name="user"
                       placeholder="Nom d'utilisateur" value="{{ modèle.login }}" required>
            </div>
        </div>
        <!-- 第二行 -->
        <div class="form-group row">
            <!-- 标签 -->
            <label for="password" class="col-md-3 col-form-label">Mot de passe</label>
            <!-- 文本输入框 -->
            <div class="col-md-4">
                <input type="password" class="form-control" id="password" name="password"
                       placeholder="Mot de passe" required>
            </div>
        </div>
        <!-- 第三行上的 [submit] 类型按钮 -->
        <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]标签的[action]属性中指定的URL (第2行)中指定的URL。用于调用该URL的方法HTTP,在[form]标签(第2行)的[method]属性中进行了指定;
    • 在此,当用户点击 [Valider] 按钮(第 34 行)时,浏览器将向 URL [/authentifier-utilisateur](第 2 行)提交(第 2 行)表单中输入的值;
    • 提交的值即用户在第17行和第27行输入框中输入的值。这些值将作为请求主体,由浏览器以[x-www-forl-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’]:输入框的名称。浏览器将通过此名称提交用户输入的值;
    • [placeholder=’invite’]:当用户尚未输入任何内容时,输入框中显示的文本;

Image

  • (续)
    • [value=’valeur’]:输入框一经显示,即会显示文本“值”,即在用户输入其他内容之前。此机制用于在发生错误时显示导致错误的输入内容。 此处的值即为变量 [modèle.login] 的值;
    • [required]:要求用户输入值,表单才能发送至服务器:
  • 第21-30行:用于密码输入的类似代码;
  • 第27行:[type=’password’] 生成一个文本输入框(可输入任意内容),但输入的字符会被隐藏:

Image

  • 第 32-36 行:用于按钮 [Valider] 的第三行 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="/authentifier-utilisateur"] 中,我们有一个绝对的 URL,即从 URL 的根节点开始计算。 因此,POST 的完整 URL 即为 [http://machine:port/chemin/authentifier-utilisateur],这也是浏览器将使用的版本。

需要注意的是,该片段使用的是 [modèle.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 应用程序
app = Flask(__name__, template_folder="../templates", static_folder="../static")

# 首页 URL
@app.route('/')
def index():
    # 将页面数据封装到模板中
    modèle = {}
    # 用户 ID
    modèle["login"] = "albert"
    # 错误列表
    modèle["error"] = True
    erreurs = ["erreur1""erreur2"]
    # 构建错误列表 HTML
    content = ""
    for erreur in erreurs:
        content += f"<li>{erreur}</li>"
    modèle["erreurs"] = content
    # 显示页面
    return make_response(render_template("views/vue-authentification.html", modèle=modèle))

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

注释

  • 第 1-3 行:创建一个 Flask 应用程序,其唯一目的是显示视图 [vue-authentification.html](第 22 行);
  • 第 7 行:该应用程序仅有一个 URL 服务视图;
  • 第9-20行:身份验证视图包含由[modèle]对象控制的动态部分。 该对象被称为视图模型。根据MVC这一缩写所给出的两个定义之一,这里对应的是MVC中的M。在定义[vue-authentification.html]视图时,我们已确定了三个动态值:
    • [modèle.error]:布尔值,用于指示是否显示错误消息;
    • [modèle.erreurs]:包含错误消息的列表 HTML;
    • [modèle.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] 是操作控制器所使用的相同参数。因此,这些参数也会传递给视图模型;
    • 控制器生成了一个结果 [résultat],该结果同样被传递给视图模型。该结果包含一个重要元素 [état],用于指示当前操作的执行情况。视图模型将利用此信息;

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


        # 视图 HTML 及其模板取决于控制器返回的状态
        "views"[
            {
                # 身份验证视图
                "états": [
                    # /init-session 成功
                    700,
                    # /用户认证失败
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # 税款计算视图
                "états"[
                    # /用户认证成功
                    200,
                    # /计算-税款 成功
                    300,
                    # /计算-税款 失败
                    301,
                    # /显示-税款计算
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # 模拟列表视图
                "états"[
                    # /列出模拟
                    500,
                    # /删除-模拟
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],
        # 意外错误视图
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },
        # 重定向
        "redirections"[
            {
                "états": [
                    400,  # /会话结束成功
                ],
                # 重定向至
                "to""/init-session/html",
            }
        ],
    }

因此,正是状态代码 [700, 201](第 7 行和第 9 行)触发了身份验证视图的显示。 要了解这些代码的含义,可以参考在应用程序 jSON 上执行的 [Postman] 测试:

  • [init-session-json-700]:700是[init-session]操作成功后的状态码:此时将显示空白的身份验证表单;
  • [authentifier-utilisateur-201]201是[authentifier-utilisateur]操作失败(凭证不被识别)后的状态码:此时将显示身份验证表单以便用户更正;

既然我们已经知道在何时应显示身份验证表单,就可以在 [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:
        # 将页面数据封装到模板中
        modèle = {}
        # 应用程序状态
        état = résultat["état"]
        # 模板取决于状态
        if état == 700:
            # 显示空表单的情况
            modèle["login"] = ""
            # 无错误需显示
            modèle["error"] = False
        elif état == 201:
            # 身份验证错误
            # 重新显示最初输入的用户
            modèle["login"] = request.form.get("user")
            # 有错误需要显示
            modèle["error"] = True
            # 错误消息列表 HTML
            erreurs = ""
            for erreur in résultat["réponse"]:
                erreurs += f"<li>{erreur}</li>"
            modèle["erreurs"] = erreurs

        # 渲染模板
        return modèle

注释

  • 第 8 行:身份验证视图中的 [get_model_for_view] 方法需提供一个包含三个键 [error, erreurs, login] 的字典。该计算基于操作控制器返回的状态码进行;
  • 第 12 行:获取处理当前操作的控制器返回的状态码;
  • 第 14-29 行:模型依赖于该状态码;
  • 第15-18行:需显示空白身份验证表单的情况;
  • 第20-29行:认证失败的情况:显示用户输入的用户名并显示一条错误信息。此时用户可以通过键盘重新尝试认证;
  • 第22行:用户最初输入的用户名可在客户端请求中获取;
  • 第24行:提示存在需要显示的错误;
  • 第26-29行:若出现错误,结果[‘réponse’]中包含错误列表;

32.5.6. 生成响应 HTML

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

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

[3a] 中,选择了一种响应类型(jSON、XML、HTML)。 我们已经了解了 jSON 和 XML 响应的生成方式,但尚未涉及 HTML 响应。这些响应由类 [HtmlResponse] 生成:

Image

回顾一下在主脚本 [main] 中,如何确定向用户返回的响应类型:


        ….
        # 正在构建待发送的响应
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # 发送响应
        return response, status_code

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


        # 各种响应类型(json、xml、html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

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


# 根据结果中包含的状态生成的响应字典 HTML

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):
        # 响应 HTML 取决于控制器返回的状态码
        état = résultat["état"]

        # 是否需要进行重定向?
        for redirection in config["redirections"]:
            # 需要重定向的状态
            états = redirection["états"]
            if état in états:
                # 需要进行重定向
                return redirect(f"/{redirection['to']}"), status.HTTP_302_FOUND

        # 一个状态对应一个视图
        # 在视图列表中查找该视图
        views_configs = config["views"]
        trouvé = False
        i = 0
        # 遍历视图列表
        nb_views = len(views_configs)
        while not trouvé and i < nb_views:
            # 视图编号 i
            view_config = views_configs[i]
            # 与视图编号 i 关联的报表
            états = view_config["états"]
            # 所查找的报表是否位于视图编号 i 的关联报表中
            if état in états:
                trouvé = True
            else:
                # 下一视图
                i += 1
        # 找到?
        if not trouvé:
            # 如果应用程序当前状态下不存在任何视图
            # 渲染错误视图
            view_config = config["view-erreurs"]

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

        # 显示意外错误
        "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]操作成功后的状态码:此时将显示空的身份验证表单;
  • [authentifier-utilisateur-201]201是[authentifier-utilisateur]操作失败(凭证不被识别)后的状态码:此时将显示认证表单以便用户更正;

只需重新调用这些代码,并检查是否正确显示了认证视图。此处展示两种情况:

情况 1[init-session-html-700],HTML 会话的开始;

Image

响应如下:

Image

  • [5] 中,通过 [Preview] 模式可查看收到的 HTML 页面;
  • [6] 中,确实看到了预期的空表单;
  • [7] 中,Postman 未点击页面中的图片链接;
  • [8] 中,[Raw] 模式可访问收到的 HTML;

Image

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

情况 2:[authentifier-utilisateur-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] 生成:


<!-- 文档 HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- 必需的元标签 -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- BootstrapCSS -->
    <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">
    <!-- 横幅 -->
    {% include "fragments/v-bandeau.html" %}
    <!-- 双栏布局 -->
    <div class="row">
        <!-- 菜单 -->
        <div class="col-md-3">
            {% include "fragments/v-menu.html" %}
        </div>
        <!-- 计算表单 -->
        <div class="col-md-9">
            {% include "fragments/v-calcul-impot.html" %}
        </div>
    </div>
    <!-- 成功情况 -->

    {% if modèle.success %}
    <!-- 显示成功提示 -->
    <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 列错误列表 -->
    <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行);
  • 第30-46行:如果税额计算成功([modèle.success=True]),则将计算结果显示在绿色框中(第37-43行)。 该框位于视图的第三行 Bootstrap(第 32 行),占据三个空列(第 33-35 行)右侧的九个列(第 36 行)。因此,该框将位于税额计算表下方;
  • 第48-61行:如果税额计算失败([modèle.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


<!-- 已提交的表单 HTML -->
<form method="post" action="/calculer-impot">
    <!-- 12列蓝色背景消息 -->
    <div class="col-md-12">
        <div class="alert alert-primary" role="alert">
            <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
        </div>
    </div>
    <!-- 表单元素 -->
    <fieldset class="form-group">
        <!-- 9 列第一行 -->
        <div class="row">
            <!-- 4 列的标签 -->
            <legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
            <!-- 5列单选按钮-->
            <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>
        <!-- 9列中的第二行 -->
        <div class="form-group row">
            <!-- 4列标签 -->
            <label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
            <!-- 5列的子女数量数字输入区 -->
            <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>
        <!-- 9列中的第3行 -->
        <div class="form-group row">
            <!-- 4列标题 -->
            <label for="salaire" class="col-md-4 col-form-label">Salaire annuel net imposable</label>
            <!-- 5列的工资数值输入区 -->
            <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>
        <!-- 第四行,5列的[submit]按钮 -->
        <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 将被提交(属性 [method])至 URL [/calculer-impot] (属性 [action])。提交的值将是输入字段的值:
  • 已选中单选按钮的值,格式为:
      • 若单选按钮 [Oui] 被选中,则为 [marié=oui](第 17-22 行)。 [marié] 是第 18 行中 [name] 属性的值,[oui] 是第 18 行中 [value] 属性的值;
      • 若单选按钮 [Non] 被选中,则 [marié=non] 即为该值(第 23-28 行)。 [marié] 是第 24 行 [name] 属性的值,[non] 是第 24 行 [value] 属性的值;
    • 第37行数字输入框的值为[enfants=xx],其中[enfants]是第37行[name]属性的值, 而 [xx] 则是用户通过键盘输入的值;
    • 第46行数字输入框的值为[salaire=xx],其中[salaire]是第46行[name]属性的值, 而 [xx] 则是用户通过键盘输入的值;

最终,提交的值将呈现为 [marié=xx&enfants=yy&salaire=zz]

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

Image

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

  • 它们之所以是单选按钮,是因为具有 [type="radio"] 属性(第 18、24 行);
  • 在表单显示时(输入前),其中一个单选按钮必须处于选中状态:只需在相应的 <input type="radio"> 标签中添加 [checked=’checked’] 属性即可。这通过动态变量实现:
    • 第18行添加[modèle.checkedOui]
    • 第 24 行:[modèle->checkedNon]

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

  • 第 37 行:一个数字输入框 [type="number"],其最小值为 0 [min="0"]。 在现代浏览器中,这意味着用户只能输入大于等于0的数值。在这些现代浏览器中,用户可以通过点击滑块向上或向下调整数值。 第 37 行中的 [step="1"] 属性表明,该滑块将以 1 为增量进行操作。这意味着滑块仅接受从 0 到 n 之间、步长为 1 的整数值。对于手动输入而言,这意味着不接受带小数的数字;
  • 第37行:在某些显示场景下,子节点的输入框需预先填入该区域的上次输入值。为此使用属性[value]来设定输入框中显示的值。该值将由变量[modèle.enfants]动态生成; Image
  • 第37行:属性[required]强制要求用户输入数据,表单才能通过验证;
  • 第46行:关于工资输入的说明与子女信息输入的说明相同;
  • 第53行:类型为[submit]的按钮,该按钮会触发POST,并将输入的值传递给URL和[/calculer-impot](第2行);

Image

32.6.3. 片段 [v-menu.html]

该片段在税款计算表单的左侧显示一个菜单:

Image

该片段的代码如下:


<!-- Bootstrap 菜单 -->
<nav class="nav flex-column">
    <!-- 显示链接列表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/chemin] 测量的绝对路径 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 应用程序
app = Flask(__name__, template_folder="../templates", static_folder="../static")

# 首页URL
@app.route('/')
def index():
    # 将页面数据封装到模板中
    modèle = {}
    # 表单
    modèle["checkedOui"] = ""
    modèle["checkedNon"] = 'checked="checked"'
    modèle["enfants"] = 2
    modèle["salaire"] = 300000
    # 成功消息
    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 %"
    # 错误消息
    modèle["error"] = True
    erreurs = ["erreur1""erreur2"]
    # 构建错误列表 HTML
    content = ""
    for erreur in erreurs:
        content += f"<li>{erreur}</li>"
    modèle["erreurs"] = content
    # 菜单
    modèle["optionsMenu"] = [
        {"text"'Liste des simulations'"url"'/lister-simulations'},
        {"text"'Fin de session'"url"'/fin-session'}]
    # 显示页面
    return make_response(render_template("views/vue-calcul-impot.html", modèle=modèle))

# 主菜单
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. 视图模型的计算

确定视图的视觉效果后,即可在实际条件下计算视图模型。回顾一下通向该视图的状态代码。这些代码位于配置文件中:


            {
                # 税款计算视图
                "états"[
                    # /用户认证成功
                    200,
                    # /计算-税款 成功
                    300,
                    # /计算-税款 失败
                    301,
                    # /显示-税款计算
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },

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

  • [authentifier-utilisateur-200]200是[authentifier-utilisateur]操作成功后的状态代码:此时将显示空白的税款计算表单;
  • [calculer-impot-300]300是[calculer-impot]操作成功后的状态码。此时将显示包含已输入数据及税额的计算表单。用户随后可重新进行计算;
  • 状态代码 [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:
        # 将视图数据封装到模板中
        modèle = {}
        # 应用程序状态
        état = résultat["état"]
        # 模型取决于状态
        if état in [200800]:
            # 初始显示一个空表单
            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:
            # 计算成功 - 显示结果
            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} %"
            # 表单恢复并保留已输入的值
            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:
            # 发生错误 - 表单已恢复为已输入的值
            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")
            # 错误
            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>"

        # 菜单选项
        modèle["optionsMenu"] = [
            {"text"'Liste des simulations'"url"'/lister-simulations'},
            {"text"'Fin de session'"url"'/fin-session'}]
        # 返回表单
        return modèle

注释

  • 第 12 行:要显示的视图取决于控制器返回的状态代码;
  • 第14-21行:显示一个空表单;
  • 第22-35行:税额计算成功的情况。重新显示已输入的数值以及税额;
  • 第36-47行:税额计算失败的情况;
  • 第49-52行:计算菜单中的两个选项;

32.6.6. 测试 [Postman]

使用请求 [init-session-html-700] 初始化会话 HTML,然后使用请求 [authentifier-utilisateur-200] 进行身份验证。随后我们使用以下请求 [calculer-impot-300]

服务器的响应如下:

Image

Image

现在尝试以下请求 [calculer-impot-301]

Image

服务器的响应如下:

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

Image

Image

  • [6] 中,我们取消了 [marié] 请求中发送的参数;

服务器的响应如下:

Image

  • 变为 [3],即服务器的错误信息;

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

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

对于失败情况,我们可以细化状态码以实现更精细的错误管理。例如,我们可以设置:

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

32.7. 模拟列表视图

Image

32.7.1. 视图介绍

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

Image

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

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

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


<!-- 文档 HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- 必需的元标签 -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- BootstrapCSS -->
    <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">
    <!-- 横幅 -->
    {% include "fragments/v-bandeau.html" %}
    <!-- 双栏布局 -->
    <div class="row">
        <!-- 三栏菜单-->
        <div class="col-md-3">
            {% include "fragments/v-menu.html" %}

        </div>
        <!-- 9 列模拟列表-->
        <div class="col-md-9">
            {% include "fragments/v-liste-simulations.html" %}
        </div>
    </div>
</div>
</body>
</html>

注释

  • 第 16 行:包含应用程序标题栏 [1]
  • 第21行:插入菜单[2]。该菜单将以三列形式显示在横幅下方;
  • 第26行:引入模拟表[3]。该表将以九列形式显示在横幅下方、菜单右侧;

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

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


{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- 蓝色背景上的消息 -->
<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 %}
<!-- 蓝色背景上的消息 -->
<div class="alert alert-primary" role="alert">
    <h4>Liste de vos simulations</h4>
</div>

<!-- 模拟表格 -->
<table class="table table-sm table-hover table-striped">
    <!-- 表格中六个列的标题 -->
    <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>
    <!-- 表格主体(显示的数据) -->
    <tbody>
    <!-- 通过遍历模拟表来显示每项模拟 -->
    {% for simulation in modèle.simulations %}

    <!-- 显示包含 6 列的表格中的一行 - <tr> 标签 -->
    <!-- 第1列:行标题(模拟编号) - 标签 <th scope='row' -->
    <!-- 第2列:参数值 [marié] - 标签 <td> -->
    <!-- 第3列:参数值 [enfants] - 标签 <td> -->
    <!-- 第 4 列:参数值 [salaire] - 标签 <td> -->
    <!-- 第5列:参数值 [impôt](税款) - 标签 <td> -->
    <!-- 第6列:参数值 [surcôte] - 标签 <td> -->
    <!-- 第 7 列:参数值 [décôte] - 标签 <td> -->
    <!-- 第8列:参数值 [réduction] - 标签 <td> -->
    <!-- 第 9 列:参数值 [taux](税款) - 标签 <td> -->
    <!-- 第10列:模拟删除链接 - 标签 <td> -->
    <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 %}

注释

  • 一个名为 HTML 的表格使用 <table> 标签创建(第 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行:模拟列表位于模板[modèle.simulations]中,该模板是一个字典列表;
  • 第 57 行:用于删除模拟的链接。URL 模型使用该行中显示的模拟编号;

32.7.2. 视觉测试

我们为视图 [vue-liste-simulations.html] 创建一个测试脚本:

脚本 [test_vue_liste_simulations] 如下:

Image


from flask import Flask, make_response, render_template

# Flask 应用程序
app = Flask(__name__, template_folder="../templates", static_folder="../static")

# 首页 URL
@app.route('/')
def index():
    # 将页面数据封装到模板中
    modèle = {}
    # 将模拟结果转换为页面所需的格式
    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
        }
    ]
    # 菜单
    modèle["optionsMenu"] = [
        {"text""Calcul de l'impôt""url"'/afficher-calcul-impot'},
        {"text"'Fin de session'"url"'/fin-session'}]
    # 显示页面
    return make_response(render_template("views/vue-liste-simulations.html", modèle=modèle))

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

注释

  • 第12-35行:在模型中添加两个模拟
  • 第37-39行:菜单选项表;

通过运行此脚本显示该视图。结果如下:

Image

对该视图进行调整,直到视觉效果令人满意。随后即可将其集成到正在开发的Web应用程序中。

32.7.3. 视图模型的计算

确定视图的视觉效果后,即可在实际环境中计算视图模型。回顾一下引导至该视图的状态代码。这些代码位于配置文件中:

Image


            {
                # 模拟列表视图
                "états"[
                    # /列出-模拟
                    500,
                    # /删除-模拟
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }

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

  • [lister-simulations-500]500是[lister-simulations]操作成功后的状态码:此时将显示用户已执行的模拟列表;
  • [supprimer-simulation-600]600是[supprimer-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:
        # 将页面数据封装到模板中
        modèle = {}
        # 在执行该操作的控制器响应中找到模拟
        # 以字典数组的形式呈现TaxPayer
        modèle["simulations"] = résultat["réponse"]
        # 菜单
        modèle["optionsMenu"] = [
            {"text""Calcul de l'impôt""url"'/afficher-calcul-impot'},
            {"text"'Fin de session'"url"'/fin-session'}]
        # 生成模板
        return modèle

注释

  • 第 13 行:待显示的模拟数据位于 [résultat["réponse"]] 中;
  • 第15-17行:要显示的菜单选项;

32.7.4. 测试 [Postman]

正在

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

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

Image

服务器的响应如下:

Image

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

返回的结果是已减少一个模拟项的模拟列表:

Image

Image

32.8. 意外错误视图

此处所指的“意外错误”,是指在正常使用Web应用程序时本不应发生的错误。例如,在未经过身份验证的情况下请求计算税款。没有任何机制能阻止用户直接在浏览器中输入URL [/calcul-impot]。 此外,正如我们所见,用户还可以通过不发送预期的参数,对 URL [/calcul-impot] 进行 POST 操作。我们已经看到,我们的 Web 应用程序能够正确响应此请求。 我们将那些在 HTML 应用程序运行过程中不应出现的错误称为“意外错误”。如果发生此类错误,很可能是有人试图“入侵”该应用程序。出于教学目的,我们决定针对此类情况显示一个错误视图。 实际上,我们可以重新显示发送给客户端的上一页。只需将最后发送的响应 HTML 保存在会话中即可。一旦发生意外错误,我们就返回该响应。这样,由于显示的页面没有变化,用户会觉得服务器对错误没有响应。

32.8.1. 视图介绍

Image

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

Image

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

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

意外错误的显示由以下脚本 [vue-erreurs.html] 生成:


<!-- 文档 HTML -->
<!doctype html>
<html lang="fr">
<head>
    <!-- 必需的元标签 -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- BootstrapCSS -->
    <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 列横幅 -->
    {% include "fragments/v-bandeau.html" %}
    <!-- 两栏布局 -->
    <div class="row">
        <!-- 3 列菜单-->
        <div class="col-md-3">
            {% include "fragments/v-menu.html" %}
        </div>
        <!-- 9列错误列表 -->
        <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>标签包裹了一个项目符号列表。该列表由模板[modèle.erreurs]提供

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

  • [v-bandeau.html]:在链接段落中;
  • [v-menu.html]链接段落;

32.8.2. 视觉测试

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

Image


from flask import Flask, render_template, make_response

# Flask 应用程序
app = Flask(__name__, template_folder="../templates", static_folder="../static")

# 首页 URL
@app.route('/')
def index():
    # 将页面数据封装到模板中
    modèle = {}
    # 构建错误列表 HTML
    content = ""
    for erreur in ["erreur1""erreur2"]:
        content += f"<li>{erreur}</li>"
    modèle["erreurs"] = content
    # 菜单选项
    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'}]

    # 显示页面
    return make_response(render_template("views/vue-erreurs.html", modèle=modèle))

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

注释

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

运行该脚本。结果如下:

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

Image

32.8.3. 视图模型的计算

Image

确定视图的外观后,即可在实际环境中计算视图模型。回顾一下通往该视图的状态代码。这些代码位于配置文件中:


        # 视图 HTML 及其模板取决于控制器返回的状态
        "views"[
            {
                # 身份验证视图
                "états": [
                    # /init-session 成功
                    700,
                    # /结束会话
                    400,
                    # /用户认证失败
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # 税款计算视图
                "états"[
                    # /用户认证成功
                    200,
                    # /计算-税款 成功
                    300,
                    # /计算-税款 失败
                    301,
                    # /显示-税款计算
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # 模拟列表视图
                "états"[
                    # /列出模拟
                    500,
                    # /删除-模拟
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],
        # 意外错误视图
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

第3-41行中那些不指向视图HTML的状态代码,会触发意外错误视图的显示。

视图 [vue-erreurs.html] 的模型计算由以下类 [ModelForErreursView] 完成:


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:
        # 模型
        modèle = {}
        # 错误
        modèle["erreurs"] = ""
        for erreur in résultat['réponse']:
            modèle['erreurs'] += f"<li>{erreur}</li>"
        # 菜单
        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'}]
        # 生成模型
        return modèle

注释

  • 第 11-14 行:计算视图 [vue-erreurs.html] 所使用的模板 [modèle.erreurs]
  • 第 16-197 行:计算由片段 [v-menu.html] 使用的模板 [modèle.optionsMenu]

32.8.4. [Postman] 测试

执行:

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

此时响应 HTML 如下:

Image

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

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

视图
链接
目标
作用
计算税款
[Liste des simulations]
[/lister-simulations]
请求模拟列表
  
[Fin de session]
模拟列表
[Calcul de l’impôt]
[/afficher-calcul-impot]
显示税款计算视图
  
[Fin de session]
意外错误
[Calcul de l’impôt]
[/afficher-calcul-impot]
显示税额计算视图
  
[Liste des simulations]
  
[Fin de session]

需要注意的是,点击链接会触发 GET 跳转至链接目标。[/lister-simulations, /fin-session] 操作已通过 GET 操作实现,因此我们可以将其设置为链接目标。 当操作通过 POST 实现时,除非结合 JavaScript 使用,否则无法再使用链接。

32.9.1. 操作 [/afficher-calcul-impot]

从上述操作来看,[/afficher-calcul-impot] 操作似乎尚未实现。 这是一项在两个视图之间进行导航的操作:jSON或XML服务器没有理由实现它,因为它们不具备视图的概念。是HTML服务器引入了这一概念。

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

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

Image

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


    # 控制器
    from AfficherCalculImpotController import AfficherCalculImpotController
    from AuthentifierUtilisateurController import AuthentifierUtilisateurController
    from CalculerImpotController import CalculerImpotController
    from CalculerImpotsController import CalculerImpotsController
    from FinSessionController import FinSessionController
    from GetAdminDataController import GetAdminDataController
    


        # 允许的操作及其控制器
        "controllers": {
            # 计算会话的初始化
            "init-session": InitSessionController(),
            # 用户身份验证
            "authentifier-utilisateur": AuthentifierUtilisateurController(),
            # 个人模式下的税款计算
            "calculer-impot": CalculerImpotController(),
            # 批量模式下的税款计算
            "calculer-impots": CalculerImpotsController(),
            # 模拟列表
            "lister-simulations": ListerSimulationsController(),
            # 删除模拟
            "supprimer-simulation": SupprimerSimulationController(),
            # 结束计算会话
            "fin-session": FinSessionController(),
            # 显示税款计算视图
            "afficher-calcul-impot": AfficherCalculImpotController(),
            # 获取税务部门数据
            "get-admindata": GetAdminDataController(),
            # 主控制器
            "main-controller": MainController()
        },

       # 视图 HTML 及其模板取决于控制器返回的状态
        "views"[
            {
                # 身份验证视图
                
            },
            {
                # 税款计算视图
                "états": [
                    # /用户认证成功
                    200,
                    # /计算-税款 成功
                    300,
                    # /计算-税款 失败
                    301,
                    # /显示-税款-计算
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {…
            }
        ],
  • 第 2 行:新控制器;
  • 第28行:新操作及其控制器;
  • 第51行:新控制器将返回状态码800。在视图切换时,不应出现错误。当前显示的视图是[vue-calcul-impot.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):
        # 获取路径中的元素
        dummy, action = request.path.split('/')

        # 视图切换——只需设置状态码
        return {"action": action, "état"800"réponse"""}, status.HTTP_200_OK

注释

  • 第6行:与其他辅助控制器一样,新控制器实现了[InterfaceController]接口;
  • 第13行:视图切换的实现很简单:只需返回与目标视图关联的状态码,此处如前所述为代码800;

32.9.2. 操作 [/fin-session]

操作 [/fin-session] 较为特殊。它不会直接跳转到视图,而是进行重定向。需要提醒的是,重定向已在配置 [config] 中按以下方式配置:


        # 重定向
        "redirections"[
            {
                "états": [
                    400,  # /注销成功
                ],
                # 重定向至
                "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):
        # 响应HTML取决于控制器返回的状态码
        état = résultat["état"]

        # 是否需要进行重定向?
        for redirection in config["redirections"]:
            # 需要重定向的状态
            états = redirection["états"]
            if état in états:
                # 需要进行重定向
                return redirect(f"{redirection['to']}"), status.HTTP_302_FOUND

        # 每个状态对应一个视图
        # 在视图列表中查找该视图
 ..
  • 第 6-12 行处理重定向;
  • 第 7 行:config[‘redirections’] 是一个重定向列表。每个重定向都是一个包含以下键的字典:
    • [états]:导致重定向的控制器返回状态;
    • [to]:重定向地址;
  • 第 7-12 行:遍历重定向列表;
  • 第 9 行:对于每个重定向,获取导致该重定向的状态;
  • 第 10 行:如果被测试的状态在此列表中,则执行重定向,见第 12 行;
  • 第12行:需注意方法[build_http_response]必须返回一个包含两个元素的元组:
    • [response]:待生成的响应 HTTP。该响应由函数 [redirect] 构建,其参数为重定向地址;
    • [status_code]:响应 HTTP 的状态码,此处为 [status.HTTP_302_FOUND],该代码指示客户端进行重定向;

让我们进行一次测试 [Postman]。我们:

  • 初始化一个会话 HTML [init-session/html]
  • 进行身份验证 [/authentifier-utilisateur]
  • 结束会话 [/fin-session]

Image

服务器的响应如下:

Image

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

Image

  • [1] 中,操作 [/fin-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:
    # 重定向至 /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:
    # 执行与该操作关联的控制器
    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;
      • 第二个参数是发给客户端的响应状态码。状态码 [status.HTTP_302_FOUND] 表示重定向;

准备就绪。现在我们展示一些视图流。

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

Image

若观察客户端与服务器之间的网络通信:

Image

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

填写我们收到的表单;

Image

然后进行一些模拟操作:

Image

Image

请求模拟列表:

Image

删除第一个模拟:

Image

结束本次会话:

Image

欢迎读者进行其他测试。