Flask 极致细节:1. 路由和请求响应规则,GET/POST,重定向

提示:此博客包含如下概念的介绍:路由(装饰器),请求与响应(Request&Response),GET/POST,重定向(Redirect)。
此章节可能会比较枯燥,之后我们会跟新真实的案例,但打好基础总是必须的。另外,我们会一如既往地分享所有代码,并附有非常详细的注解。
喜欢的朋友点个赞哦:)
代码链接:https://pan.baidu.com/s/1wH1BjtUKbT2xj1ctTThQvw
密码:1zyl



0. 准备工作

当你下载并打开代码后,里面的结构应该是这样的:

--项目名
	|---static (静态)
	|---templates (模板)
	|---app.py (运行/启动)
	|---venv1 (虚拟环境)
	|---requirements.txt (所有安装包以及版本)
	|---config.py (参数配置文件)
	|---readme.md (说明文档)

我们可以创建虚拟环境:virtualenv [venv],或直接使用已经存在的虚拟环境venv1。接下来进入虚拟环境:.\[venv]\Scripts\activate。如是自己创建的新虚拟环境,还需要安装依赖:pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。相关的配置以及解释请参见前一篇博文:Flask 极致细节:0. VS Code 手动新建Flask项目

1. 路由 Route

官方网站:网址

1.1. Route的正常使用

app.py中,我们使用了路由,比如:

@app.route('/test')                              # 路由
def hello_world2():                              # 在此路由下的视图(函数)
    return 'Wow!'

路由的请求和响应:浏览器地址输入的内容:http://0.0.0.0:8080/test ---->请求给到服务器 ----> 服务器去app找有没有这个路由 ----> 如果有,执行路由匹配函数 ----> return 返回值 ----> 通过Response对象返回到客户端的浏览器。

根据flask官方解释:使用 route() 装饰器来把函数绑定到 URL。也就是说,route()是一个装饰器,通过看源代码,我们发现,系统实际上运行的是add_url_rule函数。之所以做了一个装饰器,一方面是因为好用,另外一方面也是考虑代码的复用性。如下为源代码的一部分:

def route(self, rule: str, **options: t.Any) -> t.Callable:
    def decorator(f: t.Callable) -> t.Callable:
        endpoint = options.pop("endpoint", None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f
    return decorator

所以,这段代码

@app.route('/test')                                 # 路由
def hello_world2():                                 # 在此路由下的视图(函数)
    return 'Wow!'

也等价于

def hello_world2():                            
    return 'Wow!'
app.add_url_rule('/test',view_func=hello_world2)

1.2 装饰器

Python的装饰器本质上是一个嵌套函数,它接受被装饰的函数(func)作为参数,并返回一个包装过的函数。这样我们可以在不改变被装饰函数的代码的情况下给被装饰函数或程序添加新的功能。

试想你写了很多程序,一直运行也没啥问题。有一天老板突然让你统计每个程序都运行了多长时间并比较下运行效率。此时如果你去手动修改每个程序的代码一定会让你抓狂,而且还破坏了那些程序的重用性。此时你可以编写一个@time_it的装饰器(代码如下所示)。如果你想打印出某个函数或程序运行时间,只需在函数前面@一下,是不是很帅?

import time

def time_it(func):
    def inner():
        start = time.time()
        func()
        end = time.time()
        print('用时:{}秒'.format(end-start))
    return inner

@time_it
def func1():
    time.sleep(2)
    print("Func1 is running.")

if __name__ == '__main__':
    func1()

1.3 路由的变量规则

app.py文件中,我们已经包含了一个包含变量的路由例子:

# 路由的变量规则
testNameId = {"A":1, "B":2, "C":3}
@app.route('/test/<username>')
def hello_world3(username):
    return str(testNameId[username])

通过把 URL 的一部分标记为 <variable_name> 就可以在 URL 中添加变量。标记的分会作为关键字参数传递给函数。通过使用<converter:variable_name> ,可以选择性的加上一个转换器,为变量指定规则。

比如上面这段代码,当我们运行app.py后,在网页端输入http://127.0.0.1:8080/test/A, 你会看到网页显示1。在网页端
输入http://127.0.0.1:8080/test/B,你会看到网页显示2。当然,这些只是很简单的例子,但背后的概念和使用方式很重要。

2. 请求响应 Request&Response

假设用户通过前端网页的点击向服务器发出请求(通过URL的HTTP协议)。中间我们需要把请求的内容封装成一个Request对象,包含请求行,请求头,请求体(响应也同样如此,包含响应行,响应头,响应体)。请求发送到服务器,这里就涉及到路由,系统判断路由路径有没有,有的话就执行下面的执行函数,函数返回值,比如一些字符串。这些字符串会默认进行包装,转成Response对象。这个对象会返回到浏览器。浏览器真正展示的响应体里面的内容。【我觉得这段话挺重要的】

2.1 响应 Response

app.py中,我们展示了一个案例,在test1函数中。我们可以实例化一个返回的Response对象,定制返回的内容,返回头,等等。详细的信息可以直接在flask文档中搜索Response。

在运行app.py后,我们打开网页http://127.0.0.1:8080/test1并查看后台信息(Windows点击F12),在响应头这块我们会看到下图:

在这里插入图片描述
详细内容如下:

Content-Length: 194
Content-Type: text/html; charset=utf-8
Date: Sat, 01 Jan 2022 01:49:04 GMT
Server: Werkzeug/1.0.1 Python/3.8.5
Set-Cookie: test3="cookie for test1"; Path=/
test1: created in 2022.01.01.
test2: created in 9:30 a.m.

相应行以及状态返回:

请求 URL: http://127.0.0.1:8080/test1
请求方法: GET
状态代码: 200 OK
远程地址: 127.0.0.1:8080
引用站点策略: strict-origin-when-cross-origin

响应体就是我们发过去的内容:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
</head>
<body>

<h1>我的第一个标题</h1>
<p>我的第一个段落。</p>

</body>
</html>

重点:Response的响应可以包含:

  • str 自动转换成response对象
  • json.dumps(dict) json
  • response 对象
  • make_resaponse() response对象
  • redirect() 重定向,返回302状态码
  • render_template() 模板渲染+模板

我把上一段落加粗的文字中与Respnse有关的一句话再复制一遍:**函数返回值,比如一些字符串。这些字符串会默认进行包装,转成Response对象。**上面列举了6中最终可以作为Reponse对象传回浏览器的最常用方式,非常重要。

2.2 请求 Request

我们可以在flask中找到request。需要注意的是,request本身就是对象,所以可以直接访问属性,或调用方法。如下为源代码中对request的定义:

request: "Request" = LocalProxy(partial(_lookup_req_object, "request"))  # type: ignore

app.pytest5中,我们尝试了打印出request的一些属性。

2.3 Render Template

我们在test1函数中导入了html网页格式,如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
</head>
<body>

<h1>我的第一个标题</h1>
<p>我的第一个段落。</p>

</body>
</html>

但一个问题是,我们不可能把冗长的html文件都复制黏贴到这个app主程序中。一个更便利的解决办法是,单独为之新建一个template文件,然后在app中引用。

r = render_template("app_test6.html")

需要注意的是,模板文件都必须放在templates文件夹下,否则会报错。那么,为什么render_template可以做这种转换呢?这就涉及到与flask一起安装的jinjia模板引擎。render_template就是由模板引擎负责将html文件找到并转化成字符串。另外一个问题是,为什么render_template会直接去templates文件夹下找模板,为不是其他文件夹。当我们实例化Flask的时候:app = Flask(__name__),我们点进去Flask类里的__init__函数:

def __init__(
        self,
        import_name: str,
        static_url_path: t.Optional[str] = None,
        static_folder: t.Optional[t.Union[str, os.PathLike]] = "static",
        static_host: t.Optional[str] = None,
        host_matching: bool = False,
        subdomain_matching: bool = False,
        template_folder: t.Optional[str] = "templates",
        instance_path: t.Optional[str] = None,
        instance_relative_config: bool = False,
        root_path: t.Optional[str] = None,
    ):

我们看到,template folder 默认了templates这个文件夹。

2.4 捋一遍逻辑

在这里插入图片描述

首先,服务器收到请求,并查看请求中的路由是否在规则表中,有的话就找到匹配的函数,并执行里面的内容。上图的内容包含了render_template,是由模板引擎在底层来完成的。这个模板引擎的作用是网页转换成字符串,再利用return返回到客户浏览器。

3. GET/POST

3.1 概念

从上一章的描述中我们对于客户端与服务器之间的通信以及请求-应答协议有了一些基本的了解。在客户端和服务器之间的请求-响应中,两种最常被用到的方法是:GET和POST。

这个链接,GET和POST的定义如下:

  • GET: 从指定的资源请求数据。请注意,查询字符串(名称/值对)是在 GET 请求的 URL 中发送的:
/test/demo_form.php?name1=value1&name2=value2
  • POST: 向指定的资源提交要被处理的数据。请注意,查询字符串(名称/值对)是在 POST 请求的HTTP消息主体中发送的:
POST /test/demo_form.php HTTP/1.1
Host: runoob.com
name1=value1&name2=value2

所以,GET请求可以被缓存,存在于URL中,不应在处理敏感数据时使用。而POST请求不会被缓存。

3.2 几个例子

app_test6.html中,我们增加了两个输入框以及一个按钮。

<div>
    <!-- 如果是表单提交,则必须在表单元素上添加name这个属性 -->
    <form action="/testGET" method="get">
        <p><input type="text" name="username" placeholder="请输入用户名"></p>
        <p><input type="text" name="address" placeholder="请输入地址"></p>
        <p><input type="submit" placeholder="提交"></p>
    </form>
</div>

细节1:如果是表单提交,则必须在表单元素上添加name这个属性。

细节2:action一栏中,我们需要输入一个页面/路由路径。当我们点击了submit按钮后,系统会进入此页面。

细节3:method一栏中,我们可以选择getpost

首先我们选择gettestGET函数包含了对应的代码:

# 当我们点击了/test6 route之后,我们可以输入用户名和密码,那么服务器后台如何获得这些数据呢?
@app.route('/testGET', methods=['GET'])
def testGET():                                    # 获取页面提交的内容
    print(request.full_path)                    # /test7?username=yifan&address=NewYork
    print(request.path)                         # /test7
    print(request.args)                         # dict 类型 d = {'a':'aaa', 'b':'bbb'}
    # 这里的获得数据的方式只针对method="get"的方式。
    print("GET")
    username = request.args.get("username")     # 获取GET中username的方法
    address = request.args.get("address")
    print("username is ",username, "; address is ",address)
    return "OK"

细节4:这里的method可以填,也可以不填。但对于postmethod值必须填。

当我们运行app.pyapp.url_map打印的结果是:

Map([<Rule '/testPOST' (POST, OPTIONS) -> testPOST>,
 <Rule '/testGET' (OPTIONS, HEAD, GET) -> testGET>,
 <Rule '/test1' (OPTIONS, HEAD, GET) -> test1>,
 <Rule '/test5' (OPTIONS, HEAD, GET) -> test5>,
 <Rule '/test6' (OPTIONS, HEAD, GET) -> test6>,
 <Rule '/' (OPTIONS, HEAD, GET) -> hello_world>,
 <Rule '/static/<filename>' (OPTIONS, HEAD, GET) -> static>])

上面的第二行指的是testGET路由的规则。

在这里插入图片描述

我们进入链接127.0.1:8080/test6,输入用户名和密码,然后点击提交按钮。代码中打印的结果如下:

/testGET?username=yifan&address=NewYork
/testGET
ImmutableMultiDict([('username', 'yifan'), ('address', 'NewYork')])
GET
username is  yifan ; address is  NewYork

细节5:我们能看到,点击提交按钮之后的链接变成了http://127.0.0.1:8080/testGET?username=yifan&address=NewYork
这也就是之前在对GET介绍时提到的查询字符串(名称/值对)是在 GET 请求的 URL 中发送的

细节6:我们之前输入的用户名和密码以字典的形式存于request.args中。

然后我们选择posttestPOST函数包含了对应的代码:

# 当我们点击了/test6 route之后,我们可以输入用户名和密码,那么服务器后台如何获得这些数据呢?
@app.route('/testPOST', methods=['POST'])
def testPOST():                                    # 获取页面提交的内容
    print(request.full_path)                    # /test7?username=yifan&address=NewYork
    print(request.path)                         # /test7
    print(request.args)                         # dict 类型 d = {'a':'aaa', 'b':'bbb'}
    # 这里的获得数据的方式只针对method="post"的方式。
    print("POST")
    print(request.form)
    username = request.form.get('username')
    address = request.form.get('address')
    print("username is ",username, "; address is ",address)
    return "OK" 

app_test6.html中对应的修改:<form action="/testPOST method="post">

我们进入链接127.0.1:5051/test6,输入用户名和密码,然后点击提交按钮。代码中打印的结果如下:

/testPOST?
/testPOST
ImmutableMultiDict([])
POST
ImmutableMultiDict([('username', 'yifan'), ('address', 'NewYork')])
username is  yifan ; address is  NewYork

细节7:注意,之前的GET会导致缓存,我们需要清理一下,或者索性先换一个端口。

细节8:我们点击提交按钮后,链接变成了http://127.0.0.1:5051/testPOST。这也印证了
之前对POST的介绍。

细节6:我们之前输入的用户名和密码以字典的形式存于request.form中。

3.3 重定向 redirect

上一段落中,我们解释了GET和POST的使用,在test7中,我们将test6以及testPOST函数整合到了一起。

# 我们可以把上面的test6和testPOST函数合起来。
@app.route('/test7', methods = ['GET','POST'])
def test7():
    print(request.method)
    if request.method == "POST":                # 当我们点击了submit按钮后,系统又会进入这个页面,因为action上写的也是这个路由地址。
        username = request.form.get('username') # 但这个时候,request method 变成了POST
        address = request.form.get('address')
        user = {'username':username,'address':address}
        users.append(user)
        return redirect(location="/")           # 重定向。两次相应。1. 302状态码+location;2. 返回location请求地址内容。
        #return '注册成功!<a href ="/">返回首页</a>'
    return render_template('app_test6.html')    # 首次进入这个函数,我们会调用这个html页面。这个时候request method是GET

这里,我们使用到了redirect重定位。当我们在注册界面点击了submit按钮后,我们就会回到主菜单。

当我们运行代码,重定位的逻辑:

首先,浏览器发请求到服务器(/test7 GET请求),服务器render_template,我们在浏览器中可以看到表单。输入username和address信息后,点击submit按钮。这个时候浏览器(因为app_test6.html中的action /test7)发请求到服务器(/test7 POST请求)。服务器端通过request.form来获取我们刚才输入的数据。然后,服务器执行redirect,返回一个response对象,里面携带code:302以及location的相应头(response headers)到浏览器。浏览器先看状态码(302),然后会立即拿出location的值,去替换现有的值,并且发出请求到服务器。最后,页面切换到/(主页)。

所以,这就是为什么重定向有两次响应。

在这里插入图片描述
在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐