为何有如此多的python web frameworks?
当问起python的web framework为什么会这么多时,答案总是:用python来做实在是太容易了。这听起来似乎有一定的道理,因为web framework中的组件很多都实现了,如果能轻松地把它们连接起来,那么就可能降低创建一个新框架的门槛。让我们给出一个实例,我们将使用一些组件,花上一些时间,看看我们能怎样构建一个web framework,我们将把这个框架称之为"Robaccia".
说明:Robaccia是在3个小时内写成的,总共60行python代码。
[更新:在wsgi wiki中添加了连接,清理了一些拼写错误。如果我使用mimetypes模块的话,robaccia.py可能会更小。]
对于我们需要的每种类型的库,我只选一个,也必须只选一个。那是否意味选中的库更好,而其他的就不好呢?答案是否定。它仅仅说明我只能选择一个,不存在优劣问题。如果我没有选择你最爱的templating/routing/sql库,请不要惊奇。
Templating
在python社区模板库还是挺多的,诸如Myghty,Cheetah等。
我选择Kid,"它是一个简单的基于xml的模板语言"
SQL
数据库的接口,我选择sqlalchemy.其他的库有sqlobject.
Routing
我们对进口的http请求发送到正确的处理器上。为此,
我选择Selector。其他的选择有Routes.
WSGI
WSGI,是由PEP 333定义,它是一个概念上胶水层,对于WSGI,
最好是把它看成像Java servlet api的一样的层。它是位于web
servers和python web application 或 framework之间的一个标准接口,
用来增强web application在不同web servers上的移植性。在wsgi
wiki网站上学习到很多关于wsgi知识,并找到wsgi相关的server,
framework,middleware等等。
现在我们就使用上面提到的这些组件,开始把它们连接起来。
事实上,你现在可能想走一遍django的指南(如果你还没有读过它的话),它会给你一个大概的内容,也是我们现在想做的事情,可并不是要去完成django指南的所有内容。
我们将使用传统的 model/view/controller 模式,但是在web frameworkd中看起来更像是model/view/template/dispatcher,因此每个application都有4个必须的文件:model.py,view.py,urls.py,和一个templates目录。我们会再放一个文件,dbconfig.py,它是用来允许你建立访问你的数据库。
我们所要做的是通过各个程序片断来建立一个weblog application,要注意这个application的各部件,它是怎样变成framework的一部份的。我们最先需要做的是创建model,在model.py中使用sqlalchemy来建立。
model.py from sqlalchemy import Table, Column, String import dbconfig entry_table = Table('entry', dbconfig.metadata, Column('id', String(100), primary_key=True), Column('title', String(100)), Column('content', String(30000)), Column('updated', String(20), index=True) )
这是一个纯粹用python描述的model,dbconfig.py中配置同样简单。
dbconfig.py from sqlalchemy import * metadata = BoundMetaData('sqlite:///tutorial.db')
在django tutorial中开始做的一件事是使用这样一个model去实际创建数据库中的一个表。我们用manage.py做同样的事,它是Robaccia框架的第一个项目。
manage.py import os , sys def create(): from sqlalchemy import Table import model for (name, table) in vars(model).iteritems(): if isinstance(table,Table): table.create() if __name__=="__main__": if 'create' in sys.argv: create()
现在我们就可以创建数据库了。
$ python manage.py create $
进入python解释器就能创建数据库表了,通过'model'模块操作数据,注意我们可以进入解释器来创建表,但再继续的话,就要在表中加2行记录了。
$ python [GCC 4.0.3 (Ubuntu 4.0.3-1ubuntu5)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import model >>> i = model.entry_table.insert() >>> i.execute(id='first-post', title="Some Title", content="Some pithy text...", updated="2006-09-01T01:00:00Z") >>> i.execute(id='second-post', title="Moving On", content="Some not so pithy words...", updated="2006-09-01T01:01:00Z") >>>
现在我们有一个model了,其中还有些数据,是介绍URLs和views的时候了。urls.py文件包含了进口的requests怎样发送到views的信息,view.py包含所有view的目标。
urls.py import selector import view urls = selector.Selector() urls.add('/blog/', GET=view.list) urls.add('/blog/{id}/', GET=view.member_get) urls.add('/blog/;create_form', POST=view.create, GET=view.list) urls.add('/blog/{id}/;edit_form', GET=view.member_get, POST=view.member_update)
Selector把URIs映射到views上。如果一个进来的请求有一个URI匹配,那这个请求就被分派到相关联的处理器上。Selector和处理器是WSGI对象,它使得把这些内容连接起来变得非常容易。
view.py import robaccia import model def list(environ,start_response): rows = model.entry_table.select().execute() return robaccia.render(start_response,'list.html',locals()) def member_get(environ,start_response): id=environ['selector.vars']['id'] row=model.entry_table.select(model.entry_table.c.id==id).execute().fetchone() return robaccia.render(start_response,'entry.html',locals()) def create(environ,start_response): pass def create_form(environ, start_response): pass def member_edit_form(environ, start_response): pass def member_update(environ, start_response): pass
注意上面代码只实现了list()和member_get()。
在我的第一次实现中,view处理器直接作templates激活,然后把所有的东西放在一起以适合WSGI model,但是这样的话对每个view都会重复代码,因此,就有了Robaccia的第二个版本的代码。
robaccia.py import kid import os extensions = { 'html': 'text/html', 'atom': 'application/atom+xml' } def render(start_response, template_file, vars): ext = template_file.rsplit(".") contenttype = "text/html" if len(ext) > 1 and (ext[1] in extensions): contenttype = extensions[ext[1]] template = kid.Template(file=os.path.join('templates', template_file), **vars) body = template.serialize(encoding='utf-8') start_response("200 OK", [('Content-Type', contenttype)]) return [body]
render()函数会查看template的扩展类型,并使用它来决定content-type的内容。然后模板和变量被传入kid处理。整个过程处理之后,返回的是以WSGI的形式。这里是list.html模板:
list.html <?xml version="1.0" encoding="utf-8"?> <html xmlns:py="http://purl.org/kid/ns#>"> <head> <title>A Robaccia Blog</title> </head> <div py:for="row in rows.fetchall()"> <h2>${row.title}</h2> <div>${row.content}</div> <p><a href="./66634226/">${row.updated}</a></p> </div> </html>
现在,urls.urls是一个符合WSGI规范的application,它会监听进来的呼叫,分派到view.py中的WSGI application。其中的每一个都使用model.py的model,然后把结果传到templates目录下的模板,并产生responses.
现在我们需要一个总纲来运行这些代码。既然我们使用的是WSGI application,我们就使用wsgiref。让我们在manage.py中加入一个'run'选项。
manage.py import os, sys def create(): from sqlalchemy import Table import model for (name, table) in vars(model).iteritems(): if isinstance(table, Table): table.create() def run(): import urls if os.environ.get("REQUEST_METHOD", ""): from wsgiref.handlers import BaseCGIHandler BaseCGIHandler(sys.stdin, sys.stdout, sys.stderr, os.environ).run(urls.urls) else: from wsgiref.simple_server import WSGIServer, WSGIRequestHandler httpd = WSGIServer(('', 8080), WSGIRequestHandler) httpd.set_app(urls.urls) print "Serving HTTP on %s port %s ..." % httpd.socket.getsockname() httpd.serve_forever() if __name__ == "__main__": if 'create' in sys.argv: create() if 'run' in sys.argv: run()
run()函数先看环境变量来决定是否做一个CGI application来运行,否则就直接在自己的server 8080端口运行这个application。
$ python manage.py run Serving HTTP on 0.0.0.0 port 8080 ...
如果我们想通过CGI运行我们的application,怎么办呢?那文件只有简单的几行
main.cgi #!/usr/bin/python2.4 import manage manage.run()
总结
我们已经作了些什么呢?下面列出的就是在一个目录下的文件集合:
* model.py - 由sqlalchemy Table来创建的一个或多个models
* view.py - 一个或多个views,作为WSGI application实现
* urls.py - 一个selector对象单一实例,它把URIs映射到view.py中的
WSGI application
* templates - 一个Kid templates的目录,是用作格式化来自view application
的响应
* dbconfig.py - 在model.py中sqlalchemy Tables的配置文件
除了这些文件,还实现的有manage.py,main.cgi,和robaccia.py,这就是我们framework代码的全部内容,总共大约60行代码。就是这些不多的胶水代码把功能强大的库sqlalchemy,Kid,Selector,和WSGIref串在了一起。因为我们整个过程中都是使用WSGI,所以我们能容易通过加载WSGI的代码库,比如处理authentication,caching,logging等。
现在和django比较一下,也清楚地知道我们没有做的东西,我们没有一个admin界面,我们没有一个generic views,自动的form生成器,自动的form处理,django社区,bug跟踪,irc等等。
我想引起大家注意的是那些主要组件的关键点。
我们通过Kid模板来解析model数据写了多少代码?没有。
我们把WSGI views挂接到Selector中写了多少代码?没有。
我们从URLs提取出信息,同时从model提取信息来使用,写了多少代码?只有1行:
id = environ['selector.vars']['id'].
许许多多的组件中可以用来构建python web framework的,有很多好用的。他们只要求很少的一些胶水代码。如果我们选择SQLObject,Cheetah,Routes,我们这个小小framework的代码量也差不多。
哦,要我告诉你为什么要用Robaccia来命名?它的意思是意大利语的"垃圾桶"。它是一个要抛弃的东西。因此,我们要从这开始出发,去看看那些已经构建起来的许许多多的python web framework。