可不要被名字迷惑,它可不是web网页的框架,而是服务器用来产生web网页时用到的工具。
Web应用框架(简称Web框架),是用来构建web支持下的应用程序的实践方式。从简单的博客到复杂的富Ajax应用,web上的每个页面都是通过代码构建起来的。最近我发现很多对web框架(如Flask、Django)感兴趣的开发者没有真正地理解什么是web框架——它们的目的是什么、它们怎么运行。因此,我将在本文中讨论web框架这个经常被忽略的基础话题。通读本文,你会对什么web框架、为什么它们一开始就存在等问题有深入的理解,这也会让你学习一个新的web框架以及使用哪个框架时的决定大为轻松。
Web如何工作
在我们讨论具体的框架以前,需要理解web如何工作,为此我们将深入探讨当你在浏览器中输入URL地址并按下回车键时到你的浏览器在呈现页面的过程中经过的步骤(不包括DNS查表)。
Web服务器及web提供的服务
每个页面都以HTML
文件发送到你的浏览器中,HTML
是一种浏览器用来描述内容和页面结构的一种语言,把HTML
发送到你的浏览器中的应用程序就是Web服务器。同时,这个应用程序所在的机器也叫做Web服务器。
HTTP
浏览器使用HTTP
协议(协议,在编程领域中是通信双方约定的数据格式和通信步骤)从Web服务器(或叫应用程序服务器)中下载页面,HTTP
协议基于请求—响应
模型,客户端(你的浏览器)向在运行在一台物理机器上的网页应用程序请求数据,web应用程序接着就用你浏览器请求的数据来响应这个请求。
有一点需要记住的是,通信总是由客户端(你的浏览器)发起的,服务器(这里是web服务器)没有任何方式发起连接或者主动给你的浏览器发送未请求的信息,如果你从一个网页服务器中收到了数据,那一定是因为你的浏览器发出了请求。
HTTP方法
在HTTP
协议中的每一个信息都有相关的方法(或动作),各不相同的HTTP
方法对应于客户端根据不同的需要而发出的不同逻辑的请求,比如请求一个网页的HTML就和提交一个表单在逻辑上不一样,所以处理这两种的请求就需要不同的方法。
HTTP GET
GET
方法做的事就跟它说的一样:从网页服务器要求(请求)数据,GET
请求是目前最常见的HTTP
请求,在一个GET
请求过程中,网页应用程序除了将所需要页面的HTML响应给这个请求外不做任何其他事情。这里特意指出,在处理GET
请求过程中网页应用程序不应改变自身任何状态(比如,它不能基于一个GET
请求就创建一个新的用户帐户),因为这个原因,GET
请求通常被认为是“安全”的,因为它们不会导致驱动网站的应用程序的任何变化。
HTTP POST
显然,除了单纯地看看页面意外还有更多与网站交互的方式,我们也可以向web应用程序发送数据,比如说一个表单,要完成这个工作,需要另一个不同的请求:POST
。POST
请求通常会携带用户输入的数据,继而会引起web应用采取一些行为。在网站的表单上输入你的信息来注册就是通过POST
请求把表单上的数据传递给web应用的。
与GET
请求不同,POST
请求通常会导致web应用的状态改变,在上面的例子中,当一个表单被POST
以后,就创建了一个新的用户帐户,其次,POST
请求也不总是会让一个新的HTML页面发送给客户端,客户端通过响应的响应码来决定服务器上的操作是否进行顺利。
HTTP 响应码
在最常见的情况中,web服务器会返回一个响应码200,意思是“我做了你要求做的,并且一切进行顺利”,响应码总是一个三位的数字。web应用必须为每个响应发送一个响应码来表明对一个请求的处理情况。200
意为“OK”,并且是响应一个GET
请求最常用到的,而一个POST
请求则有可能返回响应码204
(“没有内容”),意思是“一切事情都干得很顺利,但我没啥可以呈现给你看的”。
值得注意的是,POST
请求还会被发送到一个与提交表单的网页不同的URL,用上面那个注册用户的例子来说,就是表单所在的网址是www.foo.com/signup
,你在这个页面点击了提交
,而这个POST
请求会发送到www.foo.com/process_signup
,POST
请求被发送到的地址对于表单的HTML
是特定的。
Web应用程序
只要用到HTTP
GET
和POST
,你就能够做很多事情,因为它们是最常用的HTTP
方法。web应用就是用来接受HTTP
请求并且用通常包含HTML表示的请求页面的HTTP
响应进行回复。POST
请求引起web应用产生一些行为,如在数据库里添加一条新的记录。还有很多其他HTTP
方法,但目前我们仅聚焦于GET
和POST
方法。
最简单的web应用长什么样?我们可以写一个监听80
端口连接的应用,一旦它监听到了一个连接,它会等待客户端发送一个请求,然后它会用非常简单的HTML进行回复。
下面是这个应用的情况:
import socket
HOST = ''
PORT = 80
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.bind((HOST, PORT))
listen_socket.listen(1)
connection, address = listen_socket.accept()
request = connection.recv(1024)
connection.sendall(
"""
HTTP/1.1 200 OK
Content-type: text/html
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>
""")
connection.close()
(如果上述代码不工作,将PORT
改成8080
试试)
上述代码接收了一个简单连接和一个简单请求,不管请求什么URL,它会回复一个HTTP 200
的响应(它不是一个真正意义上的web服务器),Content-type: text/html
这一行代表header区域,header用来提供请求或者响应的元信息,在这个例子中,我们告诉客户端,发送过去的数据是HTML(而不是JSON)
对一个请求的分析
仔细观察我用来测试上述程序的HTTP
请求,我发现它和响应很相似,第一行的格式是
<HTTP Method> <URL> <HTTP version>
在本例中,是GET / HTTP/1.1
,在第一行接下来是诸如Accept: */*
(表示我们接收任何响应里面的内容)的头部,这是一个请求的基本情况。
我们发送的响应有着类似格式,如:
<HTTP version> <HTTP Status-Code> <Status-Code Reason-Phrase>
在本例中,是HTTP/1.1 200 OK
,接下来headers,格式跟请求的headers一样,最后包含了响应的实际内容。注意这些都可以用一个字符串或者二进制对象进行编码,Content-type
header让客户端知道如何解释响应。
web服务器的fatigue
如果我们接着上述的例子继续讲解web应用,随之而来有很多问题需要解决:
- 我们要怎么检测所需要的URL并且返回合适的网页?
- 除了简单的
GET
请求以外,我们如何处理POST
请求? - 怎么处理一些像sessions和cookies等更高级的概念?
- 如何描述能够处理数以千计并发连接的应用?
如你能想象,没有人愿意每次构建服务器时都要逐一对付这些问题,因此就有了能够处理HTTP
协议细节和统一解决上述问题的包。然而要记住,它们的核心就跟我们上述提到的例子一样:监听请求并且发送带有HTML的HTTP
响应。
注意客户端 web 框架(如前端当前流行的三大框架:React、Vue以及Angular)是另一个不同的庞然大物,与我们上述讲到的大不相同。
解决两个主要问题:路由与模版
在构建一个web应用涉及到的一切问题中,有两个是重中之重:
- 如何将一个被请求的URL定位到用于处理它的代码?
- 如何动态创建被请求的HTML,在其中加入从数据库读取的计算值或信息?
每个web框架都用某些方式来解决这些问题,并且有很多不同的方法。接下来我讨论了Django和Flask用来解决这些问题的方式,首先我们要简要讨论MVC架构。
Django中的MVC
Django遵从MVC架构并且要求使用该框架的代码也使用该架构,MVC即“模型—视图—控制 (Model-View-Controller)”的缩写,用来表示web应用需要负责的不同方面。与数据库有关的资源是用模型来表示(类似的,Python中常用class
来表示一些真实世界中的对象),控制包含应用的业务逻辑和对模型的操作,视图接受所有用来动态生成HTML页面所需要的信息。
令初学者疑惑的是,在Django中,MVC架构中的控制叫做视图,视图叫做模版,除去命名上的古怪,Django是非常典型的MVC架构的部署方式。
Django中的路由
路由是将被请求的URL定位到负责生成相关HTML的代码的过程,最简单的例子是所有请求都用同一个代码进行处理(如我们之前所举的例子),稍微复杂一点,每一个URL按照1:1地对应到视图函数
中,比如,我们可以在某个代码来实现如下功能,如果请求URLwww.foo.com/bar
,就让handle_bar()
函数来负责处理进行响应,以此类推,我们可以为所有web应用支持的URLs建立对应的处理函数。
但是,如果URLs中包含了有用的数据,比如某个资源的ID(按上面的例子来说,如果URL是www.foo.com/users/3/
),这种路由方法就会失败,那么我们怎么样将URL对应到一个视图函数的同时呈现ID为3
的用户页面呢?
Django的处理方式是用URL正则表达式定位到能够接受参数的视图函数,举例来说,我可以说符合^/users/(?P<id>\d+)/$
格式的URLs会调用display_user(id)
函数,函数中的id
变量会用正则表达式中的id
进行替换,通过这种处理,任何/users/<some number>/
格式的URL会定位到display_user
函数中,这些正则表达式可以写得非常复杂,并且同时包含键盘和位置参数。
Flask中的路由
Flask则用了不太一样的处理方式,它通过使用route()
修饰器将一个被请求的URL和函数连接起来。下面的Flask代码跟上面提到的正则表达式与函数的功能相同:
@app.route('/users/<id:int>/')
def display_user(id):
# ...
如你所见,修饰器简化了将URLs对应到处理函数的正则表达式写法中(用/
来分离),参数通过包括一个传递到route()
的URL中的<name:type>
命令被获取,路由到像/info/about_us.html
之类的静态URLs的方式也不难想到:
@app.route('/info/about_us.html')
通过模版生成HTML
继续上面的例子,我们一旦将合适的代码定位到正确的URL以后,我们怎样动态生成支持web开发者修改的HTML呢?Django和Flask的处理方式都是通过HTML 模版
HTML 模版类似于使用str.format()
,先通过占位符来写需要的输出,后面可以被替换成传入str.format()
函数的变量,想象一下将整个网页写成一个字符串,用括号标记动态数据,最后调用str.format()
,Django模版和Flask使用的模版引擎jinja2都是这么工作的。
然而,不是所有的模版引擎都进行相同的创建,Django对于模版编程提供了基本支持,而Jinja2基本上让你自由发挥(不一定准确,但基本上是这个意思)。Jinja2会缓存绘制模版的结果,这样接下来如果有相同变量的请求则会直接从缓存中返回,而不是重新绘制。
服务器交互
Django由于其“一条龙服务 batteries included”哲学,还包含了一个ORM
(object relational mapper, 对象关系映射器),ORM
的目的有两个:将Python类定位到数据库表、把各种不同数据库引擎的差别抽象掉(前者是其最基本的功能)。人们都不太喜欢ORMs(由于这种定位从来做不到完善),但是还是可以接受的。
Django是功能齐备,相比之下,Flask作为一个“微框架”则没有ORM(与Django最大也是唯一的竞争者SQLAlchemy相同)
由于囊括了一个ORM
,Django得以创建全功能CRUD
应用,CRUD
(Create Read Update Delete 增删改查)应用似乎是web框架的有效切入点(从服务器端来看)。Django(以及Flask-SQLAlchemy)为每个模型创建了很多不同的CRUD
操作。
Web框架总结
讲到这里,web框架的目的应该已经明确了:作为HTTP
请求响应和相应底层代码之间的接口,即把底层代码隐藏起来,藏到什么程度就看不同的框架如何处理了。Django和Flask代表了两种极端,Django几乎涉及到了每种情况,这都快成为它的包袱了,Flask将自己定位为一个“微框架”,它仅保留了web框架最核心的规模,而依赖于第三方包来处理那些web框架不太常用的任务。
记住,所有Python web框架工作的本质都是一样的:它们接收HTTP
请求,将之分配到生成HTML的代码中,并且用相应内容生成HTTP
响应,实际上,所有主流服务器端的框架都这样进行工作(包括Nodejs 的框架)。通过上文了解了它们的目的后,希望你现在对web框架进行选择时能心里有数。
原文地址:https://jeffknupp.com/blog/2014/03/03/what-is-a-web-framework/