Why so many Python web frameworks?

为何有如此多的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 ...

在你的浏览器中指向http://localhost:8080/blog/,你将得到blog的主页,模板会把两个记录填写进前面的list.html中。就是这些了,我们的application运行起来了,我们的framework开始发挥作用了。

如果我们想通过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。

你可能感兴趣的:(Why so many Python web frameworks?)