用Twisted实现一个简单但是完整的Web应用框架

mallon 发布于 2011/05/17 10:41
阅读 6K+
收藏 10

学了一段时间Python,也看了很多的Web开发框架,轻量级、中量级的、重量级的...感觉都不尽如人意,原因有二:

1、任何开发者都有自己独特的思路,适应现成的框架是很痛苦的,相比完整但是死板的大框架,我更喜欢灵活的小工具,况且Python开发一个框架并不是想象的那么难

2、大部分框架依旧需要依附于独立的Web服务器运行,太麻烦。除了老牌的Twisted和新秀Tornado。在比较了这两个东西之后,我决定拿Twisted开刀。

以前的实践让我很反感那些用滥了的MVC、ORM等思路,没有必要把这些硬和前端Web页面紧绑定。我的想法是要尽可能地简单清晰,而且要实现“Designer Friendly”,也就是说美工不会对着嵌入一大堆符号完全走样的页面叹气。借鉴ASP.NET的一部分思想,我是这样设计的:

1、Web应用的根目录指定为项目源代码的根目录

2、尝试使用HTML页面对应的Python模块渲染页面,失败则直接返回HTML页面

例如/a/b/c/test.html如果对应a.b.c.test模块(很显然在项目中test.html和test.py是位于同一个目录的),规定该模块中必须包含render方法,可选包含master属性,render方法返回一个字典,使用python内置的string.Template对页面进行替换(我认为模板完成简单替换功能就足够了,更多业务逻辑应该交由后台或者Ajax完成),master指定该HTML页面使用的母版页

3、“母版页”的名字源自ASP.NET,思路同SiteMesh,母版页本身也是HTML页面,其中可以包含两个特殊占位符用来替换“被母版页”的Head和Body部分,SiteMesh设计中还有一个Title占位符,我给去掉了,一来加上Title后,势必要把Head中的Title扣掉,太麻烦,二来Title完全没有必要 - 母版页中不设置Title不就是了?

4、母版页中的引用路径应该为绝对路径,建议从根开始,这个原因很简单的,就不多说了

5、母版页也可以有对应的包含render方法的模块,但是母版页不能再有母版页了(实现起来感觉有些麻烦,而且也没什么必要)

6、其它参见源代码

我认为对于一个应用系统来讲安全性是必须的,而且我也不喜欢有状态的Session、Cookie等机制,于是后来又加上了HTTP基本认证和HTTPS的功能,当然也就一两行语句啦。

当然了,严格讲也不能算框架了,放在这里只是想抛砖引玉,和大家共同探讨吧。

/main.py

#!/usr/bin/env python
#coding: utf-8
'''
简单的Web框架
扩展名统一为.html
母版页中的引用路径应该为绝对路径,建议从根开始
统一用utf-8编码
'''
import os
from twisted.internet import reactor, ssl
from twisted.web.resource import Resource, ForbiddenResource
from twisted.web.server import Site
from twisted.web.static import File
from twisted.web.resource import ErrorPage
import importlib
from string import Template
import re
import traceback

class DynamicPage(File):
    def directoryListing(self):
        # 禁止目录浏览
        return ForbiddenResource()
    
    def getMaster(self, request, path):
        try:
            masterName = os.path.dirname(__file__).replace('\\', '/') + path
            masterFile = open(masterName)
            masterContent = masterFile.read()
        except Exception:
            # 找不到母版页则返回空
            return None
        finally:
            masterFile.close()
            
        # 母版页也可以对应.py模块
        # 但是母版页不能再有母版页
        try:
            # 尝试查找对应的py模块
            moduleName = path.replace('.html', '').replace('/', '.')[1:]
            # importlib只有2.7以上版本才有!
            module = importlib.import_module(moduleName)
            renderer = getattr(module, 'render')(request)
            return Template(masterContent).substitute(renderer)
        except Exception:
            # 如果没有对应的py模块,则直接返回该页面
            return masterContent
    
    def render(self, request):
        # HTTP基本认证
        user = request.getUser()
        password = request.getPassword()
        if not (user == 'aaa' and password == 'aaa'):
            request.setHeader('WWW-Authenticate', 'Basic realm=\"Secure Area\"')
            return ErrorPage(401, 'Unauthorized', 'Authentication required.').render(request)
        
        # 禁止浏览的扩展名
        ext = re.search('[^\\.]*\\.([^\\.]*)', request.path).group(1)
        if not ext:
            return ForbiddenResource().render(request)
        if ext in ['py', 'pyc', 'pem']:
            return ForbiddenResource().render(request)
        
        try:
            # 尝试查找对应的py模块
            moduleName = request.path.replace('.html', '').replace('/', '.')[1:]
            # importlib只有2.7以上版本才有!
            module = importlib.import_module(moduleName)
            master = getattr(module, 'master')
            renderer = getattr(module, 'render')(request)
        except Exception as e:
            # 如果没有对应的py模块,则直接返回该页面
            return File.render(self, request)

        try:
            # 打开html文件进行替换
            fileName = os.path.dirname(__file__).replace('\\', '/') + request.path
            file = open(fileName)
            content = file.read()
            rendered = Template(content).substitute(renderer)
            # 处理母版页
            masterContent = DynamicPage.getMaster(self, request, master)
            if not masterContent:
                return rendered
            else:
                head = re.search('<head>(.*)</head>', rendered, re.DOTALL).group(1)
                body = re.search('<body>(.*)</body>', rendered, re.DOTALL).group(1)
                return masterContent.replace('<!--head-->', head).replace('<!--body-->', body)
        except Exception as e:
            return ErrorPage(500, e, traceback.format_exc().replace('\n', '<br/>')).render(request)
        finally:
            file.close()

if __name__ == "__main__":
    root = DynamicPage(os.path.dirname(__file__).replace('\\', '/'))
    site = Site(root)
    sslContext = ssl.DefaultOpenSSLContextFactory(
        os.path.join(os.path.dirname(__file__), 'privkey.pem').replace('\\', '/'),
        os.path.join(os.path.dirname(__file__), 'cacert.pem').replace('\\', '/')
        )
    reactor.listenSSL(443, site, contextFactory=sslContext)
    reactor.run()

 

/a/b/c/test.html

<html>
	<head>
		<title>test</title>
	</head>
	<body>
		<h2>你好: $name</h2>
		<img src="hint.png" alt="hint"/>
	</body>
</html>

 

/a/b/c/test.py

#coding: utf-8
'''
Created on 2011-5-14

@author: User
'''

master = '/master.html'

def render(request):
    return {'name': '张三'}

 

/master.html

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">

<html lang="en">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
		<meta name="generator" content="Studio 3 http://aptana.com/">
		<meta name="author" content="User">
		<!--head-->
	</head>
	<body>
		<h1>这是母版页,你好,$name</h1>
		<!--body-->
	</body>
</html>

 

/master.py

#coding: utf-8
'''
Created on 2011-5-17

@author: User
'''

def render(request):
    return dict(name=request.getUser())
加载中
1
mallon
mallon

引用来自#2楼“Inside”的帖子

简单是够简单了,但是离完整还差非常远。

别的不说,就只提模板,只有替换功能的显然不是个合格的模板,至少还需要加上分支判断和循环。像你这样,如果我需要输出一个列表呢,怎么替换?难道要后台预先处理好html标签?那后台除了提供数据之外岂不是还要知道数据是怎么用的?

有这么多好用的模板不用非要自己造轮子非智者所为。

你这些代码最多叫简单实现了接收和处理http请求,离“应用框架”这几个字还差非常非常远。

慢慢完善吧,不过判断和循环应该不会加了,否则我会用第三方模板的,列表等动态功能基本打算用成熟的JavaScript框架实现,如果需要预制HTML标签,写一些简单的工具类就可以了

1
mallon
mallon

引用来自“LinkerLin”的答案

放到GitHub上吧。方便大家提交补丁。
现在已经改用tornado了
1
mallon
mallon

引用来自“晓骏”的答案

觉得 Django 怎么样
Django应该也有它的使用场合吧,看各人喜好了,我不太喜欢 Django这种大一统的方式。
mallon
mallon
回复 @晓骏 : 我也没仔细用过啊...你可以尝试一下,喜欢就用,我就是这么做的
首席安全砖家
首席安全砖家
tornado 没接触过,最近开始 在玩Django ,对一个python 新手 有什么建议吗?
0
Inside
Inside

简单是够简单了,但是离完整还差非常远。

别的不说,就只提模板,只有替换功能的显然不是个合格的模板,至少还需要加上分支判断和循环。像你这样,如果我需要输出一个列表呢,怎么替换?难道要后台预先处理好html标签?那后台除了提供数据之外岂不是还要知道数据是怎么用的?

有这么多好用的模板不用非要自己造轮子非智者所为。

你这些代码最多叫简单实现了接收和处理http请求,离“应用框架”这几个字还差非常非常远。

0
LinkerLin
LinkerLin
放到GitHub上吧。方便大家提交补丁。
0
LinkerLin
LinkerLin

引用来自“Mallon”的答案

引用来自“LinkerLin”的答案

放到GitHub上吧。方便大家提交补丁。
现在已经改用tornado了
何不改用GEvent?
0
0xFE
0xFE
Mallon觉得Tornado和Twisted相比如何?我也在考虑使用自带Web server的框架实现我的应用,给点意见吧!
0xFE
0xFE
回复 @Mallon : 貌似Tornado架构简单,但是自己要做的事情较多,不过我倒也觉得挺好的。就是不知道Twisted在DIY上面的空间多不多,倾向于用新出的框架~
mallon
mallon
tornado简单
0
首席安全砖家
首席安全砖家
觉得 Django 怎么样
返回顶部
顶部