35. 应用练习:第15版
35.1. 简介
本版本旨在解决与在浏览器中刷新应用程序页面(F5)相关的问题。让我们举个例子。用户刚刚删除了 id=3 的模拟:

删除后,浏览器中的 URL 为 [/delete-simulation/3/…]. 如果用户刷新页面(F5),URL [1] 会被重新发送。因此,我们再次请求删除 id=3 的模拟。结果如下:

再看另一个例子。用户刚刚计算了一笔税款:
- 在 [1] 中,URL [/calculate-tax] 刚刚通过 POST 请求被查询;
如果用户刷新页面(F5),将收到一条警告信息:

页面刷新操作会重新执行浏览器发出的最后一次请求,本例中即 [POST /calculate-tax]。当被要求重新执行 POST 请求时,浏览器会显示类似上文的警告。该警告提示正在重新执行已完成的操作。假设此 POST 请求完成了购买操作,重复执行将造成不必要的后果。
此外,我们将限制用户在浏览器中输入 URL 的能力。让我们以之前的视图之一为例:

该页面提供的链接包括:
- [模拟列表],对应 URL [/lister-simulations];
- [结束会话],对应 URL [/end-session];
- [提交],对应 URL [/calculate-tax](上文未显示);
当显示税费计算视图时,我们仅接受 [/list-simulations、/end-session、/calculate-tax] 这三个操作。如果用户在浏览器中输入其他操作,系统将抛出错误。我们将对应用程序的全部四个视图执行此类验证。
我们建议按以下方式解决页面刷新问题:
- 我们将区分两种类型的操作:
- ADS(执行操作)操作:会修改应用程序状态。此类操作通常在 URL 或请求正文中包含参数;
- ASV(Action Show View)操作,用于显示视图而不改变应用程序的状态。ASV操作的数量与视图V的数量相同。ASV操作不包含参数;
- 此前,ADS 操作在执行后,会先准备视图 V 的模型 M,然后通过显示该视图 V 来完成操作。从现在起,它们将把视图的模型 M 放入会话中,并指示浏览器重定向至负责显示视图 V 的 ASV 操作;
- 视图 V 仅会在 ASV 操作之后显示。它们将从会话中检索其模型;
此方法的优势在于,浏览器地址栏将显示 ASV 的 URL。 刷新页面将重新执行 ASV 操作。该操作不会修改应用程序的状态,且使用会话中的模型( )。因此,同一页面将无任何副作用地重新显示。最后,由于重定向机制,用户在浏览器中仅会看到 ASV 操作的 URL,从而产生在页面间流畅导航的体验;
35.2. 实现

[impots/http-servers/10] 目录最初是通过复制 [impots/http-servers/09] 目录创建的。随后对其进行了修改。
35.2.1. 新路由

在 [routes_with_csrftoken] 和 [routes_without_csrftoken] 这两个文件中,我们需要为显示四个视图的四个 ASV 操作创建相应的四条路由。其余路由保持不变。
在 [routes_with_csrftoken] 中:
| # afficher-vue-calcul-impot
@app.route('/afficher-vue-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_vue_calcul_impot(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# display-view-authentication
@app.route('/afficher-vue-authentification/<string:csrf_token>', methods=['GET'])
def afficher_vue_authentification(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# display-view-list-simulations
@app.route('/afficher-vue-liste-simulations/<string:csrf_token>', methods=['GET'])
def afficher_vue_liste_simulations(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# display-view-error_list
@app.route('/afficher-vue-liste-erreurs/<string:csrf_token>', methods=['GET'])
def afficher_vue_liste_erreurs(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
|
第 1–23 行:我们为四个 ASV 操作创建了四个路由:
- [/display-authentication-view],第 8 行,显示身份验证视图;
- [/display-tax-calculation-view],第 2 行,显示税费计算视图;
- [/display-simulation-list-view],第 14 行,显示模拟视图;
- [/display-error-list-view],第 20 行,显示意外错误视图;
对于没有令牌的路由,我们在 [routes_with_csrftoken] 文件中也做了同样的处理:
| # routes ASV -------------------------
# afficher-vue-calcul-impot
@app.route('/afficher-vue-calcul-impot', methods=['GET'])
def afficher_vue_calcul_impot() -> tuple:
# execute the controller associated with the action
return front_controller()
# display-view-authentication
@app.route('/afficher-vue-authentification', methods=['GET'])
def afficher_vue_authentification() -> tuple:
# execute the controller associated with the action
return front_controller()
# display-view-list-simulations
@app.route('/afficher-vue-liste-simulations', methods=['GET'])
def afficher_vue_liste_simulations() -> tuple:
# execute the controller associated with the action
return front_controller()
# display-view-error_list
@app.route('/afficher-vue-liste-erreurs', methods=['GET'])
def afficher_vue_liste_erreurs() -> tuple:
# execute the controller associated with the action
return front_controller()
|
35.2.2. 新的控制器

[AuthenticationViewController] 执行 ASV 操作 [/display-authentication-view]:
| from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class AfficherVueAuthentificationController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# path elements are retrieved
params = request.path.split('/')
action = params[1]
# change of view - just a status code to set
return {"action": action, "état": 1100, "réponse": ""}, status.HTTP_200_OK
|
我们提到过,ASV 操作没有参数,也不会修改应用程序的状态。我们只需在第 14 行设置一个状态码,即可显示所需的视图。
[AfficherVueCalculImpotController] 控制器执行 ASV 操作 [/afficher-vue-calcul-impot]:
| from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class AfficherVueCalculImpotController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# path elements are retrieved
params = request.path.split('/')
action = params[1]
# view change - just a status code to set
return {"action": action, "état": 1400, "réponse": ""}, status.HTTP_200_OK
|
[AfficherVueListeSimulationsController] 控制器执行 ASV 操作 [/afficher-vue-liste-simulations]:
| from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class AfficherVueListeSimulationsController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# path elements are retrieved
params = request.path.split('/')
action = params[1]
# view change - just a status code to set
return {"action": action, "état": 1200, "réponse": ""}, status.HTTP_200_OK
|
[AfficherVueListeErreursController] 控制器执行 ASV 操作 [/afficher-vue-liste-erreurs]:
| from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class AfficherVueListeErreursController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# path elements are retrieved
params = request.path.split('/')
action = params[1]
# view change - just a status code to set
return {"action": action, "état": 1300, "réponse": ""}, status.HTTP_200_OK
|
让我们总结一下状态码:
- 代码 1100 应显示身份验证视图;
- 代码 1400 应显示税费计算视图;
- 代码 1200 应显示模拟视图;
- 代码 1300 应显示意外错误视图;
35.2.3. 新的 MVC 配置
由于规模过大,MVC 配置已拆分为四个文件:
- [controllers]:MVC 应用程序的 C 控制器列表;
- [ads_actions]:列出 ADS(Action Do Something)操作;
- [asv_actions]:列出 ASV(Action Show View)操作;
- [responses]:列出了应用程序的 HTTP 响应类;
让我们从最简单的开始,即 HTTP 响应文件 [responses]:
| def configure(config: dict) -> dict:
# application configuration MVC
# answers HTTP
from HtmlResponse import HtmlResponse
from JsonResponse import JsonResponse
from XmlResponse import XmlResponse
# different response types (json, xml, html)
responses = {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
}
# return response dictionary HTTP
return {
# answers HTTP
"responses": responses,
}
|
这没什么意外。
[controllers] 文件的内容也毫不意外。我们只是为 ASV 操作添加了新的控制器。
| def configure(config: dict) -> dict:
# application configuration MVC
# the main controller
from MainController import MainController
# action controllers ADS
from AuthentifierUtilisateurController import AuthentifierUtilisateurController
from CalculerImpotController import CalculerImpotController
from CalculerImpotsController import CalculerImpotsController
from FinSessionController import FinSessionController
from GetAdminDataController import GetAdminDataController
from InitSessionController import InitSessionController
from ListerSimulationsController import ListerSimulationsController
from SupprimerSimulationController import SupprimerSimulationController
from AfficherCalculImpotController import AfficherCalculImpotController
# action controllers ASV
from AfficherVueCalculImpotController import AfficherVueCalculImpotController
from AfficherVueAuthentificationController import AfficherVueAuthentificationController
from AfficherVueListeErreursController import AfficherVueListeErreursController
from AfficherVueListeSimulationsController import AfficherVueListeSimulationsController
# authorized shares and their controllers
controllers = {
# initialization of a calculation session
"init-session": InitSessionController(),
# user authentication
"authentifier-utilisateur": AuthentifierUtilisateurController(),
# link to tax calculation view
"afficher-calcul-impot": AfficherCalculImpotController(),
# 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(),
# obtaining data from tax authorities
"get-admindata": GetAdminDataController(),
# main controller
"main-controller": MainController(),
# display authentication view
"afficher-vue-authentification": AfficherVueAuthentificationController(),
# display tax calculation view
"afficher-vue-calcul-impot": AfficherVueCalculImpotController(),
# displaying the simulation view
"afficher-vue-liste-simulations": AfficherVueListeSimulationsController(),
# displaying the error view
"afficher-vue-liste-erreurs": AfficherVueListeErreursController()
}
# make the controller configuration
return {
# controllers
"controllers": controllers,
}
|
ASV 操作的 [asv_actions] 配置如下:
| def configure(config: dict) -> dict:
# application configuration MVC
# HTML views and their models depend on the state rendered by the controller
# actions ASV (Action Show view)
asv = [
{
# authentication view
"états": [
1100, # /show-view-authentication
],
"view_name": "views/vue-authentification.html",
},
{
# tax calculation
"états": [
1400, # /display-view-tax-calculation
],
"view_name": "views/vue-calcul-impot.html",
},
{
# view of simulation list
"états": [
1200, # /display-view-list-simulations
],
"view_name": "views/vue-liste-simulations.html",
},
{
# view of error list
"états": [
1300, # /display-view-error-list
],
"view_name": "views/vue-erreurs.html",
},
]
# return the ASV configuration
return {
# views and models
"asv": asv,
}
|
- [asv_actions] 文件包含四个新操作,其功能总结如下:
- 它们不带参数;
- 它们会显示一个特定视图,其模板位于会话中;
- 第 6–35 行中的 [asv] 列表将每个 ASV 操作与一个视图相关联;
[ads_actions] 文件包含 ADS 操作:
| def configure(config: dict) -> dict:
# application configuration MVC
# view models
from ModelForAuthentificationView import ModelForAuthentificationView
from ModelForCalculImpotView import ModelForCalculImpotView
from ModelForErreursView import ModelForErreursView
from ModelForListeSimulationsView import ModelForListeSimulationsView
# actions ADS (Action Do Something)
ads = [
{
"états": [
400, # /end-session success
],
# redirection to action ADS
"to": "/init-session/html",
},
{
"états": [
700, # /init-session - success
201, # /authentifier-user failure
],
# redirection to action ASV
"to": "/afficher-vue-authentification",
# model of the following view
"model_for_view": ModelForAuthentificationView()
},
{
"états": [
200, # /authentifier-user success
300, # /calculate-tax-success
301, # /calculate-tax failure
800, # /display-tax-calculation link
],
# redirection to action ASV
"to": "/afficher-vue-calcul-impot",
# model of the following view
"model_for_view": ModelForCalculImpotView()
},
{
"états": [
500, # /lister-simulations success
600, # /suppress-simulation success
],
# redirection to action ASV
"to": "/afficher-vue-liste-simulations",
# model of the following view
"model_for_view": ModelForListeSimulationsView()
},
]
# view of unexpected errors
view_erreurs = {
# redirection to action ASV
"to": "/afficher-vue-liste-erreurs",
# model of the following view
"model_for_view": ModelForErreursView()
}
# return the MVC configuration
return {
# shares ADS
"ads": ads,
# the sight of unexpected errors
"view_erreurs": view_erreurs,
}
|
- 第 11–51 行:ADS(Action Do Something)操作的列表。包含所有旧版本中的操作。不过,它们的行为已发生变化:
- 它们不再直接显示视图 V,而是仅为该视图 V 准备模型 M;
- 它们通过重定向至与视图 V 关联的 ASV 操作来请求显示视图 V;
- 并非所有 ADS 操作都会重定向到 ASV 操作:第 12–18 行,ADS 操作 [/fin-session] 会重定向到 ADS 操作 [/init-session/html]。为了区分 ADS -> ADS 和 ADS -> ASV 重定向,我们可以使用 [model_for_view] 模板。对于 ADS -> ADS 重定向,该模板不存在;
包含所有配置的 [main/config] 文件将发生如下变化:
| def configure(config: dict) -> dict:
# syspath configuration
import syspath
config['syspath'] = syspath.configure(config)
# application setup
import parameters
config['parameters'] = parameters.configure(config)
# database configuration
import database
config["database"] = database.configure(config)
# instantiation of application layers
import layers
config['layers'] = layers.configure(config)
# configuration MVC of the [web] layer
config['mvc'] = {}
# web] layer controller configuration
import controllers
config['mvc'].update(controllers.configure(config))
# actions ASV (Action Show View)
import asv_actions
config['mvc'].update(asv_actions.configure(config))
# actions ADS (Action Do Something)
import ads_actions
config['mvc'].update(ads_actions.configure(config))
# response configuration HTTP
import responses
config['mvc'].update(responses.configure(config))
# we return the configuration
return config
|
35.2.4. 新模型

模型将生成新的信息。以身份验证视图模型为例:
| from flask import Request
from werkzeug.local import LocalProxy
from AbstractBaseModelForView import AbstractBaseModelForView
class ModelForAuthentificationView(AbstractBaseModelForView):
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 = {}
…
# csrf token
modèle['csrf_token'] = super().get_csrftoken(config)
# possible actions from the view
modèle['actions_possibles'] = ["afficher-vue-authentification", "authentifier-utilisateur"]
# we render the model
return modèle
|
- 新功能位于第 17 行:当显示视图 V(该视图对应的模型即为 M)时,每个模型都会生成一个可能操作的列表。要查看这些操作,我们需要返回视图 V。以身份验证视图为例:

- 我们可以看到,认证视图仅提供一个操作,即 [Validate] 按钮的操作。该操作为 [/authenticate-user];
我们编写了:
# actions possibles à partir de la vue
modèle['actions_possibles'] = ["afficher-vue-authentification", "authentifier-utilisateur"]
在上方的视图中,我们可以看到,如果用户刷新视图,操作 [1] [/display-authentication-view] 将被重新执行。因此,该操作必须经过授权。我们本可以选择不对其进行授权,但在这种情况下,用户每次刷新页面时都会遇到错误。我们认为这种情况是不理想的。
可能的操作被放置在视图模型中。我们知道该模型将被存储在会话中。
我们对四个视图中的每一个都进行此操作。此时,可能的操作如下:
身份验证视图
modèle['actions_possibles'] = ["afficher-vue-authentification", "authentifier-utilisateur"]
税费计算视图
# actions possibles à partir de la vue
modèle['actions_possibles'] = ["afficher-vue-calcul-impot", "calculer-impot", "lister-simulations","fin-session"]
模拟列表视图
# possible actions from the view
actions_possibles = ["afficher-vue-liste-simulations", "afficher-calcul-impot", "fin-session"]
if len(modèle['simulations']) != 0:
actions_possibles.append("supprimer-simulation")
modèle['actions_possibles'] = actions_possibles
错误列表视图
# actions possibles à partir de la vue
modèle['actions_possibles'] = ["afficher-vue-liste-erreurs", "afficher-calcul-impot", "lister-simulations", "fin-session"]
35.2.5. 新的主控制器

[MainController] 进行了若干改动:
| # import dependencies
import threading
import time
from random import randint
from flask_api import status
from flask_wtf.csrf import generate_csrf, validate_csrf
from werkzeug.local import LocalProxy
from wtforms import ValidationError
from InterfaceController import InterfaceController
from Logger import Logger
from SendAdminMail import SendAdminMail
def send_adminmail(config: dict, message: str):
# send an e-mail to the application administrator
config_mail = config['parameters']['adminMail']
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# main application controller
class MainController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# we process the request
type_response1 = None
logger = None
try:
# path elements are retrieved
params = request.path.split('/')
# action is the 1st element
action = params[1]
# logger
logger = Logger(config['parameters']['logsFilename'])
…
# the /display-view-error-list action is special
# we don't check anything, otherwise we risk entering an infinite loop of redirects
erreur = False
if action != "afficher-vue-liste-erreurs":
# if error, (result] is the result to be sent to the customer
(erreur, résultat, type_response1) = MainController.check_action(params, session, config)
# if no error - action executed
if not erreur:
# execute the controller associated with the action
controller = config['mvc']['controllers'][action]
résultat, status_code = controller.execute(request, session, config)
except BaseException as exception:
# (unexpected) exceptions
résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
erreur = True
finally:
pass
# runtime error
if erreur:
# erroneous request
status_code = status.HTTP_400_BAD_REQUEST
if config['parameters']['with_csrftoken']:
# add the csrf_token to the result
résultat['csrf_token'] = generate_csrf()
….
# we send the HTTP response
return response, status_code
@staticmethod
def check_action(params: list, session: LocalProxy, config: dict) -> (bool, dict, str):
…
# result of the method
return erreur, résultat, type_response1
|
- 第 39–44 行:对操作进行首次检查。我们稍后会再讨论这一点。静态方法 [MainController.check_action] 返回一个包含三个元素的元组:
- [error]:若检测到错误则为 True,否则为 False;
- [result]:若 (error == True) 则为错误结果,否则为 None;
- [response_type1]:会话的类型(json、xml、html 或 None),取自会话对象;
- 第 39 行:若操作为 ASV 操作 [display-error-list-view](该操作用于显示错误列表),则不进行任何检查。事实上,若在此操作过程中发现错误,系统将重定向回 [/display-error-list-view] 操作,从而陷入无限重定向循环;
- 静态方法 [check_action] 如下:
| @staticmethod
def check_action(params: list, session: LocalProxy, config: dict) -> (bool, dict, str):
# retrieve the current action
action = params[1]
# no errors and no results to start with
erreur = False
résultat = None
# session type must be known before certain actions ADS
type_response1 = session.get('typeResponse')
if type_response1 is None and action != "init-session":
# we note the error
résultat = {"action": action, "état": 101,
"réponse": ["pas de session en cours. Commencer par action [init-session]"]}
erreur = True
# some ADS actions require authentication
user = session.get('user')
if user is None and action not in ["init-session",
"authentifier-utilisateur",
"afficher-vue-authentification"]:
# we note the error
résultat = {"action": action, "état": 101,
"réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
erreur = True
# from a view, only certain actions are possible
if not erreur and action != "init-session":
# for the moment no action possible
actions_possibles = None
# retrieve the model of the future session view, if it exists
modèle = session.get('modèle')
# if a model has been found, its possible actions are retrieved
if modèle:
actions_possibles = modèle.get('actions_possibles')
# if you have a list of possible actions, check that the current action is one of them
if actions_possibles and action not in actions_possibles:
# we note the error
résultat = {"action": action, "état": 151,
"réponse": [f"action [{action}] incorrecte dans l'environnement actuel"]}
erreur = True
# mistake?
if not erreur and config['parameters']['with_csrftoken']:
# check the validity of the csrf token
# the csrf_token is the last element of the path
csrf_token = params.pop()
try:
# an exception will be thrown if csrf_token is invalid
validate_csrf(csrf_token)
except ValidationError as exception:
# csrf token invalid
résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
# we note the error
erreur = True
# result of the method
return erreur, résultat, type_response1
|
- 第 2 行:[check_action] 方法对当前操作的有效性进行多项检查;
- 第 6–26 行、第 45–57 行:这些检查在之前的版本中已经存在;
- 第 28–42 行:新增了一项检查。我们验证在应用程序当前状态下当前操作是否可行。如果当前操作不可行,我们将生成状态码 151(第 40 行),这确保当前操作会被重定向到意外错误视图;
35.2.6. 新的 HTML 响应

当前更改仅适用于 HTML 会话。JSON 或 XML 会话不受影响。[HtmlResponse] 类更新如下:
| # dependencies
from flask import make_response, redirect, render_template
from flask.wrappers import Response
from flask_api import status
from flask_wtf.csrf import generate_csrf
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"]
# find out if the state was produced by a ASV action
# in which case a
asv_configs = config['mvc']["asv"]
trouvé = False
i = 0
# browse the list of views
nb_views = len(asv_configs)
while not trouvé and i < nb_views:
# view n° i
asv_config = asv_configs[i]
# states associated with view n° i
états = asv_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 trouvé:
# this is a ASV action - you need to display a view whose model is already in session
# generate the HTML response code
html = render_template(asv_config["view_name"], modèle=session['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
# not found - this is an action status code ADS
# this will be followed by a redirection
redirected = False
for ads in config['mvc']['ads']:
# conditions requiring redirection
états = ads["états"]
if état in états:
# redirection
redirected = True
break
# redirection dictionary for unexpected errors
if not redirected:
ads = config['mvc']['view_erreurs']
# is it a redirect to a ASD or ASV action?
# if there's a template, then it's a redirection to a ASV action
# then calculate the model of view V to be displayed by action ASV
model_for_view = ads.get("model_for_view")
if model_for_view:
# calculation of the next view model
modèle = model_for_view.get_model_for_view(request, session, config, résultat)
# the model is sessioned for the following view
session['modèle'] = modèle
# now it's time to generate the URL redirection, not forgetting the CSRF token if requested
if config['parameters']['with_csrftoken']:
csrf_token = f"/{generate_csrf()}"
else:
csrf_token = ""
# redirect response
return redirect(f"{ads['to']}{csrf_token}"), status.HTTP_302_FOUND
|
- 第 18–35 行:检查上次执行的操作返回的状态是否为 ASV 操作的状态;
- 第 36–46 行:如果是,则使用会话中与 [‘model’] 键关联的模型,显示与该 ASV 操作关联的 V 视图;
- 第 48–60 行:到达此处时,我们已知最后执行的操作返回的状态码属于 ADS 操作。随后将触发重定向。我们会在配置文件中查找其定义;
- 第 62 行:到达此处时,我们已获取待执行的重定向配置。有两种情况:
- 重定向至另一个 ADS 操作。在此情况下,无需计算视图模型;
- 重定向至 ASV 操作。此时必须计算视图模型(第 67–68 行)。随后将该模型添加到会话中(第 70 行);
- 第 72–76 行:计算重定向 URL;
- 第 78–79 行:将重定向响应发送给客户端;
35.3. 测试
使用浏览器执行以下测试:
- 如常使用应用程序。验证浏览器显示的唯一 URL 是否为 ASV URL [/display-view-view_name];
- 刷新页面(F5),并验证是否再次显示同一页面。不存在任何副作用;
此外,请使用客户端 [impots/http-clients/09]。由于所做的更改仅影响 HTML 会话,因此客户端 [main, main2, main3, Test1HttpClientDaoWithSession, Test2HttpClientDaoWithSession] 应能继续正常运行。
现在我们来看一个操作无法执行的情况。显示如下视图:

请不要输入 [1],而是输入 URL [/delete-simulation/1]。操作 [/delete-simulation] 不在视图提供的操作(即操作 1–4)之列,因此将被拒绝。服务器响应如下:
