从一个小型Python Web框架发送头信息
我正在为Python写一个网页框架,目标是尽可能“简洁”(目前代码行数不到100行)。你可以在github上查看当前的代码。
基本上,这个框架的设计是为了让使用起来尽可能简单。比如,一个“Hello World”网站的例子:
from pyerweb import GET, runner
@GET("/")
def index():
return "<strong>This</strong> would be the output HTML for the URL / "
@GET("/view/([0-9]+?)$")
def view_something(id):
return "Viewing id %s" % (id) # URL /view/123 would output "Viewing id 123"
runner(url = "/", # url would be from a web server, in actual use
output_helper = "html_tidy" # run returned HTML though "HTML tidy"
简单来说,你有一个函数返回HTML内容,而GET装饰器则把这个函数和一个网址关联起来。
当调用runner()
时,会检查每个被装饰的函数,如果网址的格式符合请求的网址,就会执行这个函数,然后把结果发送到浏览器。
现在,问题来了——输出头信息。目前在开发阶段,我在runner()
调用之前加了一行代码,做了print Content-type:text/html\n
,这显然有点局限。
我最初的想法是让函数返回一个字典,类似于..
@GET("/")
def index():
return {
"html": "<html><body>...</body></html>",
"headers": {"Location":"http://google.com"}
}
我其实不太喜欢这样——返回一个特定键名的字典没有直接返回一个字符串来得方便。
我可以检查返回的数据是否是字典,如果是,就用returned_data['html']
作为输出;如果是字符串,就不需要发送自定义头信息……但这样一来,从没有头信息(大多数情况下都是这样)到有头信息,就得把返回函数从return my_html
改成return {'html':my_html}
,这也不太优雅。
写完这些后,我发现了“Sinatra”——一个用法类似的Ruby库,看看它是怎么处理头信息的:
get "/" do
content_type 'text/css', :charset => 'utf-8'
end
这在Python中似乎也能很好地实现:
@GET("/")
def index():
header("location", "http://google.com")
为了实现这个,我考虑改变函数的执行方式——不再简单地使用返回值,而是把sys.stdout
改成一个StringIO,这样你就可以这样做..
def index():
print "<html>"
print "<head><title>Something</title></head>"
print "<body>...</body>"
print "</html>
..而不需要担心把一堆字符串拼接在一起。这样做的好处是我可以为头信息设置一个单独的输出流,所以上面的header()
函数可以写入这个流……类似于:
def header(name, value):
pyerweb.header_stream.write("%s: %s" % (name, value))
基本上,我想问的是,你会如何从这个网页框架输出头信息(主要是从使用的角度,但实现方面也稍微提一下)?
3 个回答
你应该重新考虑返回 HTML 的想法,因为头部信息是 HTTP 的一部分。如果你是围绕 HTTP 流来构建你的框架,那么头部信息就是在 HTML 内容之前的一些行。
上面链接中的一个头部信息示例:
HTTP/1.1 200 OK
Date: Mon, 23 May 2005 22:38:34 GMT
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
Etag: "3f80f-1b6-3e1cb03b"
Accept-Ranges: bytes
Content-Length: 438
Connection: close
Content-Type: text/html; charset=UTF-8
如果想看 Python 的例子,可以参考 BaseHTTPRequestHandler.send_header(keyword, value
) 的实现。
你可以用返回字典或字符串的这个想法,但可以加一个新的装饰器,这样用户的“演变”就会是:
简单的HTML:
@GET("/")
def index():
return "<html><body>...</body></html>"
带有固定的头部(每个头部一个@HEADER,或者用一个字典把它们都放在一起):
@GET("/")
@HEADER("Location","http://google.com")
def index():
return "<html><body>...</body></html>"
带有复杂的、可能是计算出来的头部:
@GET("/")
def index():
return {
"html": "<html><body>...</body></html>",
"headers": {"Location":"http://google.com"}
}
@HEADER()这个装饰器会简单地改变返回的值,这样“框架”的代码就能保持简单。
可以看看 PEP 333,里面有一个非常轻量级的网页服务器设计模式。如果你的服务器遵循这个具体的接口(API),那么你可以在很多不同的场合和其他产品中重复使用它。
PEP 333(WSGI)建议你不要直接返回网页,而是把HTML页面交给一个叫“start_response”的可调用对象,这个对象会把你的HTML包装成正确的HTTP响应,并加上合适的头信息。