8.3 做个简单的网站
从现在开始做一个网站,当然,这个网站只能算是一个毛坯的,可能很简陋,但是网站的主要元素都会涉及,读者通过此学习,能够了解网站的开发基本结构和内容,并且对前面的知识可以有综合应用。
8.3.1 基本结构
如图8-3所示是一个网站的基本结构。
图8-3 网站的基本结构
1.前端
这是一个不很严格的说法,但是在日常开发中都这么说。在网站中,所谓前端就是指用浏览器打开之后看到的那部分,它呈现网站传过来的信息的界面,也是用户和网站之间进行信息交互的界面。撰写前端,一般使用HTML/CSS/JS,当然,非要用Python也不是不可以(例如第8.2节的例子就没有用HTML/CSS/JS),但这势必造成以后维护困难。
MVC模式是一个非常好的软件架构模式,在网站开发中,也常常要求遵守这个模式。请阅读维基百科的解释:
MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。
MVC模式最早由Trygve Reenskaug在1978年提出,是施乐帕罗奥多研究中心(Xerox PARC)在20世纪80年代为程序语言Smalltalk发明的一种软件设计模式。MVC模式的目的是实现一种动态的程式设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。软件系统通过对自身基本部分分离的同时也赋予了各个基本部分应有的功能。专业人员可以通过自身的专长分组。
- 控制器(Controller):负责转发请求,对请求进行处理。
- 视图(View):界面设计人员进行图形界面设计。
- 模型(Model):程序员编写程序应有的功能(实现算法等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。所谓“前端”,大概对应着View部分,之所以说是大概,因为MVC是站在一个软件系统的角度进行划分的,图8-2中的前后端,与其说是系统部分的划分,不如说是系统功能的划分。
前端所实现的功能主要有:
- 呈现内容。这些内容是根据URL,由后端从数据库中提取出来的,发送给前端,然后前端将其按照一定的样式呈现出来。另外,有一些内容不是后端数据库提供的,是写在前端的。
- 用户与网站交互。比如用户登录,这是很多网站都有的功能,当用户在指定的输入框中输入信息之后,该信息就是被前端提交给后端,后端对这个信息进行处理,一般情况下都要再反馈给前端一个处理结果,然后前端呈现给用户。2.后端
这里所说的后端对应着MVC中的Controller和Model的部分或者全部功能,因为在图8-2中,“后端”是一个狭隘的概念,没有把数据库放在其内。
不在这些术语上纠结。
后端就是用Python写的程序,主要任务是根据需要处理由前端发过来的各种请求,然后根据逻辑流程操作数据库(对数据库进行增删改查),或者把请求的处理结果反馈给前端,还可能二者兼有之。
3.数据库
工作比较单一,就是面对后端的Python程序,任其增删改查。
8.3.2 一个基本架势
我们已经制作了一个只显示一行字的网站,该网站由于功能太单一,把所有的东西都写到一个文件中。在真正的工程开发中,如果那么做,那么开发过程和后期维护会遇到麻烦,特别是不便于多人合作。所以,要做一个基本框架,以后网站就在这个框架中开发。
建立一个目录,在这个目录中建立一些子目录和文件。
这个结构建立好,就摆开了一个做网站的架势。有了这个架势,后面的事情就是在这个基础上添加具体内容。当然,还可以用另外一个更好听的名字——设计。
依次说明上面的架势中每个目录和文件的作用(当然,这个作用是我规定的,如果读者愿意,也可以根据自己的意愿来任意设计)。
- handlers:我准备在这个文件夹中放后端python程序,主要处理来自前端的请求,并且操作数据库。
- methods:这里准备放一些函数或者类,比如用得最多的读写数据库的函数,这些函数被handlers里面的程序使用。
- statics:这里准备放一些静态文件,比如图片、css和JavaScript文件等。
- templates:这里放模板文件,都以html为扩展名,它们将直接面对用户。另外,还有三个Python文件,依次写下如下内容。这些内容的功能,已经讲过,只是这里进行分门别类。
1.url.py文件
- #!/usr/bin/env python
- # coding=utf-8
- """
- the url structure of website
- """
- import sys
- reload(sys)
- sys.setdefaultencoding("utf-8")
- from handlers.index import IndexHandler
- url = [
- (r'/', IndexHandler),
- ]
url.py文件主要设置网站的目录结构。from handlers.index import IndexHandler,虽然在handlers文件夹还没有什么东西,为了演示如何建立网站的目录结构,假设在handlers文件夹里面已经有了一个文件index.py,它里面还有一个类IndexHandler。在url.py文件中,将其引用过来。
变量URL指向一个列表,在列表中列出所有目录和对应的处理类。比如(r'/',IndexHandler),就是约定网站根目录的处理类是IndexHandler,即来自这个目录的get()或者post()请求,均有IndexHandler类中相应的方法来处理。
如果还有别的目录,如法炮制。
2.application.py文件
- #!/usr/bin/env python
- # coding=utf-8
- from url import url
- import tornado.web
- import os
- settings = dict(
- template_path = os.path.join(os.path.dirname(__file__), "templates"),
- static_path = os.path.join(os.path.dirname(__file__), "statics")
- )
- application = tornado.web.Application(
- handlers = url,
- **settings
- )
从内容中可以看出,这个文件完成了对网站系统的基本配置,建立网站的请求处理集合。
from url import url是将url.py中设定的目录引用过来。
setting引用了一个字典对象,里面约定了模板和静态文件的路径,即声明已经建立的文件夹“templates”和“statics”分别为模板目录和静态文件目录。
接下来application就是一个请求处理集合对象。请注意tornado.web.Application()的参数设置:
- tornado.web.Application(handlers=None, default_host='', transforms=None, **settings)
关于settings的设置,不仅仅是文件中的两个参数,还可以有其他,比如,如果填上debug=True就表示处于调试模式。调试模式的好处是开发时调试方便,但是,在正式部署的时候,最好不要用调试模式。其他更多的settings可以参看官方文档:tornado.web-RequestHandler and Application classes(http://tornado.readthedocs.org/en/latest/web.html)。
3.server.py文件
这个文件的作用是将tornado服务器运行起来,并且囊括前面两个文件中的对象属性设置。
- #!/usr/bin/env python
- # coding=utf-8
- import tornado.ioloop
- import tornado.options
- import tornado.httpserver
- from application import application
- from tornado.options import define, options
- define("port", default = 8000, help = "run on the given port", type = int)
- def main():
- tornado.options.parse_command_line()
- http_server = tornado.httpserver.HTTPServer(application)
- http_server.listen(options.port)
- print "Development server is running at http://127.0.0.1:%s" % options.port
- print "Quit the server with Control-C"
- tornado.ioloop.IOLoop.instance().start()
- if __name__ == "__main__":
- main()
如此这般,就完成了网站架势的搭建,下面要做的是向里面添加内容。
8.3.3 连接数据库
网站不一定非要有数据库,但是如果做一个功能强悍的网站,数据库就是必需的了。
接下来的网站,暂且采用MySQL数据库。
在前面已经搭建的目录结构中,找到methods,并建立一个文件db.py,然后分别建立起连接对象和游标对象。代码如下:
- #!/usr/bin/env python
- # coding=utf-8
- import MySQLdb
- conn = MySQLdb.connect(host="localhost", user="root", passwd="123123", db="qiwsirtest", port=3306, charset="utf8")
- cur = conn.cursor()
8.3.4 登录界面
很多网站上都看到用户登录功能,这里做一个简单的登录,其功能描述为:
当用户输入网址,呈现在眼前的是一个登录界面。在用户名和密码两个输入框中分别输入正确的用户名和密码之后,单击确定按钮,登录网站,显示对该用户的欢迎信息。
用图示来说明,如图8-4所示。
图8-4 用户登录界面
用户单击“登录”按钮,经过验证是合法用户之后,就呈现如图8-5所示的界面。
图8-5 呈现界面
先用HTML写好第一个界面。进入到templates文件,建立名为index.html的文件:
- <!DOCTYPE html>
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Learning Python</title>
- </head>
- <body>
- <h2>Login</h2>
- <form method="POST">
- <p><span>UserName:</span><input type="text" id="username"/></p>
- <p><span>Password:</span><input type="password" id="password" /></p>
- <p><input type="BUTTON" value="LOGIN" id="login" /></p>
- </form>
- </body>
这是一个很简单的前端界面。要特别关注<meta name="viewport"content="width=device-width,initial-scale=1"/>,其目的在于将网页的默认宽度(viewport)设置为设备的屏幕宽度(width=device-width),并且原始缩放比例为1.0(initial-scale=1),即网页初始大小占屏幕面积的100%。这样做的目的是让其在电脑、手机等不同大小的屏幕上,都能很好地显示。
这种样式的网页是“自适应页面”。当然,自适应页面绝非是仅仅有这样一行代码就完全解决的。要设计自适应页面,就是要进行“响应式设计”,还需要对CSS、JS乃至于其他元素如表格、图片等进行设计,或者使用一些响应式设计的框架。
一提到能够在手机上显示,读者是否想到了HTML5呢,这个被一些人热捧、被另一些人蔑视的家伙,毋庸置疑,现在已经得到了越来越广泛的应用。
HTML5是HTML最新的修订版本,2014年10月由万维网联盟(W3C)完成标准制定。目标是取代1999年所制定的HTML 4.01和XHTML 1.0标准,以期能在互联网应用迅速发展的时候,使网络标准达到符合当代的网络需求。广义论及HTML5时,实际指的是包括HTML、CSS和JavaScript在内的一套技术组合。
响应式网页设计(英语:Responsive Web Design,通常缩写为RWD),又被称为自适应网页设计、回应式网页设计。是一种网页设计的技术做法,该设计可使网站在多种浏览设备(从桌面电脑显示器到移动电话或其他移动产品设备)上阅读和导航,同时减少缩放、平移和滚动。
如果要看效果,可以直接用浏览器打开网页,因为它是.html格式的文件。
虽然完成了视觉上的设计,但是,如果单击login按钮,没有任何反应。因为它还仅仅是一个孤立的页面,这时候需要一个前端交互利器——JavaScript。
对于JavaScript,不少人对它有误解,总认为它是从Java演化出来的。它们两个有相像的地方,但其关系就如同“雷峰塔”和“雷锋”一样。详细读一读来自维基百科的诠释。
JavaScript,一种直译式脚本语言,是一种动态类型、弱类型、基于原型的语言,内置支持类。它的解释器被称为JavaScript引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在HTML网页上使用,用来给HTML网页增加动态功能,然而现在也可以被用于网络服务器,如Node.js。
在1995年时,由网景公司的布兰登·艾克,在网景导航者浏览器上首次设计实现而成。因为网景公司与昇阳公司合作,网景公司管理层希望它外观看起来像Java,因此取名为JavaScript。但实际上它的语义与Self及Scheme较为接近。
为了获取技术优势,微软推出了JScript,与JavaScript同样可在浏览器上运行。为了统一规格,1997年,在ECMA(欧洲计算机制造商协会)的协调下,由网景、昇阳、微软和Borland公司组成的工作组确定统一标准:ECMA-262。因为JavaScript兼容于ECMA标准,因此也称为ECMAScript。
但是,我更喜欢用jQuery,因为它的确让我省了不少事。
jQuery是一套跨浏览器的JavaScript库,可以简化HTML与JavaScript之间的操作。由约翰·雷西格(John Resig)于2006年1月在BarCamp NYC上发布第一个版本。目前是由Dave Methvin领导的开发团队进行开发。在全球前10,000个访问最高的网站中,有65%使用了jQuery,是目前最受欢迎的JavaScript库。
在index.html文件中引入jQuery的方法有多种。
原则上,可以在HTML文件的任何地方引入jQuery库,但是通常放置的地方在html文件的开头<head>…</head>中,或者在文件的末尾</body>以内。若放在开头,如果所用的库比较大、比较多,在载入页面时的时间相对较长。
第一种引入方法是国际化的一种:
- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
这是直接从jQuery CDN(Content Delivery Network)上直接引用,好处在于如果这个库更新,你不用做任何操作,就直接使用最新的了。
当然,jQuery CDN不止一个,比如官方网站的:<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>。
第二种引入方法是将jQuery下载下来,放在指定地方(比如,与自己的网站在同一个存储器中,或者自己可以访问的另外服务器)。到官方网站(https://jqueryui.com/)下载最新的库,然后将它放在已经建立的statics目录内,为了更清楚地区分,可以在里面建立一个子目录js,jQuery库放在js子目录里面。下载的时候,建议下载以min.js结尾的文件,因为这个是经过压缩之后的,体积小。
我在statics/js目录中放置了下载的库,并且为了简短,更名为jquery.min.js。
可以用下面的方法引入:
- <script src="statics/js/jquery.min.js"></script>
如果这样写也是可以的,但是考虑到Tornado的特点,用下面的方法引入更具有灵活性:
- <script src="{{static_url("js/jquery.min.js")}}"></script>
不仅要引入jQuery,还需要引入自己写的js指令,所以要建立一个文件,我命名为script.js,也同时引用过来,虽然目前这个文件还是空的。
- <script src="{{static_url("js/script.js")}}"></script>
这里用的static_url()是Tornado模板提供的一个函数,用这个函数,能够制定静态文件。之所以用它,而不是用上面的那种直接调用的方法,主要原因是如果某一天,将静态文件目录statics修改了,即不指定statics为静态文件目录了,定义别的目录为静态文件目录。只需要在定义静态文件目录那里修改,而其他地方的代码不需要修改。
先写一个测试性质的东西。
用编辑器打开statics/js/script.js文件,如果没有就新建。输入的代码如下:
- $(document).ready(function(){
- alert("good");
- $("#login").click(function(){
- var user = $("#username").val();
- var pwd = $("#password").val();
- alert("username: "+user);
- });
- });
由于本书不是专门讲授JavaScript或者jQuery,所以,在js代码部分,只能一带而过,不详细解释。
上面的代码主要实现获取表单中的id值分别为username和password输入的值,alert函数的功能是把值以弹出菜单的方式显示出来。
是否还记得url.py文件?做这样的设置:
- from handlers.index import IndexHandler
- url = [
- (r'/', IndexHandler),
- ]
现在把假设有了的那个文件index.py建立起来,即在handlers里面建立index.py文件,并写入如下代码:
- #!/usr/bin/env python
- # coding=utf-8
- import tornado.web
- class IndexHandler(tornado.web.RequestHandler):
- def get(self):
- self.render("index.html")
当访问根目录的时候,就将相应的请求交给了handlers目录中index.py文件的IndexHandler类的get()方法来处理,它的处理结果呈现index.html模板内容。
render()函数的功能在于向请求者反馈网页模板,并且可以向模板中传递数值。
将上面的文件保存之后,回到handlers目录中。因为这里面的文件要在别处被当作模块引用,所以,需要在这里建立一个空文件,命名为init.py。这个文件非常重要。只要在目录中加入了这个文件,该目录中的其他.py文件就可以作为模块被Python引入了。
至此,一个带有表单的网站就建立起来了。读者可以回到上一级目录中,找到server.py文件,并运行它:
- $ python server.py
- Development server is running at http://127.0.0.1:8000
- Quit the server with Control-C
如果读者在前面的学习中,跟我的操作完全一致,就会在shell中看到上面的结果。
打开浏览器,输入http://localhost:8000或者http://127.0.0.1:8000,看到的应该如图8-6所示。
图8-6 弹出对话框
这就是script.js中的“alert("good");”开始起作用了,第一句是要弹出一个对话框。单击“确定”按钮之后如图8-7所示。
图8-7 单击“确定”按钮后的对话框
在这个页面输入用户名和密码,然后单击Login按钮,如图8-8所示。
图8-8 网站雏形
一个网站有了雏形。不过,提交表单的反应,还仅仅停留在客户端,且没有向后端传递客户端的数据信息,接下来就解决这个问题。
8.3.5 数据传输
在建立了前端表单之后,就要实现前端和后端之间的数据传递。在工程中,常用到一个被称之为Ajax()的方法。
关于Ajax的故事,需要浓墨重彩,因为它足够精彩。
Ajax是“Asynchronous Javascript and XML”(异步JavaScript和XML)的缩写,在它的发展历程中,汇集了众家贡献。比如微软的IE团队曾经将XHR(XML HttpRequest)用于Web浏览器和Web服务器间传输数据,并且被W3C标准采用。当然,也有其他公司为Ajax技术做出了贡献,虽然他们都被遗忘了,比如Oddpost,后来被Yahoo!收购并成为Yahoo!Mail的基础。但是,真正让Ajax大放异彩的Google是不能被忽视的,正是Google在Gmail、Suggest和Maps上大规模使用了Ajax,才使得人们看到了它的魅力,程序员由此而兴奋。
技术总是在不断进化的,进化的方向就是用着越来越方便。
回到jQuery,里面就有Ajax()方法,能够让程序员方便地调用。
Ajax()方法通过HTTP请求加载远程数据。
该方法是jQuery底层AJAX实现。简单易用的高层实现如$.get、$.post等。$.ajax()返回其创建的XMLHttpRequest对象。大多数情况下你无须直接操作该函数,除非你需要操作不常用的选项,以获得更多的灵活性。
最简单的情况下,$.ajax()可以不带任何参数直接使用。
在上文介绍Ajax的时候,用到了一个重要的术语——“异步”,与之相对应的叫作“同步”,对此引用来自阮一峰的网络日志中的通俗描述:
“同步模式”就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;“异步模式”则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
“异步模式”非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,“异步模式”甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。
看来,Ajax()是前后端进行数据传输的重要角色。
承接前面对简单网站的研发,接下来是用Ajax()方法实现前后端的数据传输,只需要修改script.js文件内容即可:
- $(document).ready(function(){
- $("#login").click(function(){
- var user = $("#username").val();
- var pwd = $("#password").val();
- var pd = {"username":user, "password":pwd};
- $.ajax({
- type:"post",
- url:"/",
- data:pd,
- cache:false,
- success:function(data){
- alert(data);
- },
- error:function(){
- alert("error!");
- },
- });
- });
- });
在这段代码中,“var pd={"username":user,"password":pwd};”是将得到的user和pwd值,放到一个json对象中。接下来就是利用Ajax()方法将这个json对象传给后端。
jQuery中的Ajax()方法使用比较简单,正如上面的代码所示,只需要$.ajax()即可,不过需要对里面的参数进行说明。
- type:是post还是get。
- url:post或者get的地址。
- data:传输的数据,包括三种,(1)html拼接的字符串;(2)json数据;(3)form表单经serialize()序列化的。本例中传输的就是json数据,这也是经常用到的一种方式。
- cache:默认为True,如果不允许缓存,设置为False。
- success:请求成功时执行回调函数。本例中,将返回的data用alert方式弹出来。读者是否注意到,我在很多地方都用了alert()这个东西,目的在于调试,走一步看一步,看看得到的数据是否是自己所要。
- error:请求失败所执行的函数。
8.3.6 数据处理
前端通过Ajax技术,将数据以json格式传给了后端,并且指明了对象目录"/",这个目录在url.py文件中已经做了配置,是由handlers目录中index.py文件的IndexHandler类来处理。因为是用post方法传的数据,那么在这个类中就要有post方法来接收数据。所以,要在IndexHandler类中增加post(),增加之后的完善代码是:
- #!/usr/bin/env python
- # coding=utf-8
- import tornado.web
- class IndexHandler(tornado.web.RequestHandler):
- def get(self):
- self.render("index.html")
- def post(self):
- username = self.get_argument("username")
- password = self.get_argument("password")
- self.write(username)
在post()方法中,使用get_argument()函数来接收前端传过来的数据,这个函数的完整格式是get_argument(name,default=[],strip=True),它能够获取name的值。在上面的代码中,name就是从前端传到后端的那个json对象的键的名字,是哪个键就获取哪个键的值。如果获取不到name的值,就返回default的值,但是这个值默认是没有的,如果真的没有就会抛出HTTP 400。特别注意,在get的时候,通过get_argument()函数获得url的参数,如果是多个参数,就获取最后一个的值。要想获取多个值,可以使用get_arguments(name,strip=True)。
上例中分别用get_argument()方法得到了username和password,并且它们都是unicode编码的数据。
tornado.web.RequestHandler的方法write(),即上例中的self.write(username),是后端向前端返回数据。这里返回的实际上是一个字符串,也可返回json字符串。
如果读者要查看修改代码之后的网站效果,最有效的方式是先停止网站(ctrl+c),再重新执行python server.py运行网站,然后刷新浏览器即可。这是一种较为笨拙的方法。一种灵巧的方法是开启调试模式。在设置setting的时候,写上debug=True就表示是调试模式了。但是,调试模式也不是十全十美,如果修改模板,就不会加载,还需要重启服务。
看看上面的代码效果,如图8-9所示。
图8-9 代码效果
前端输入了用户名和密码之后,单击login按钮,提交给后端,后端再向前端返回数据之后的效果。这就是我们想要的结果。
按照流程,用户在前端输入了用户名和密码,并通过Ajax提交到了后端,后端借助于get_argument()方法得到了所提交的数据(用户名和密码)。下面要做的事情就是验证这个用户名和密码是否合法,其体现在:
- 数据库中是否有这个用户。
- 密码和用户先前设定的密码(已经保存在数据库中)是否匹配。这个验证工作完成之后,才能允许用户登录,登录之后才能继续做某些事情。
首先,在methods目录中(已经有了一个db.py)创建一个文件,我将其命名为readdb.py,专门用来存储读数据用的函数(这种划分完全是为了明确和演示一些应用方法,读者也可以都写到db.py中)。这个文件的代码如下:
- #!/usr/bin/env python
- # coding=utf-8
- from db import *
- def select_table(table, column, condition, value ):
- sql = "select " + column + " from " + table + " where " + condition + "='" + value + "'"
- cur.execute(sql)
- lines = cur.fetchall()
- return lines
上面这段代码,建议读者写上注释,以检验自己是否能够将以往的知识融会贯通地应用。
有了这段代码之后,就进一步改写index.py中的post()方法。为了明了,将index.py的全部代码呈现如下:
- #!/usr/bin/env python
- # coding=utf-8
- import tornado.web
- import methods.readdb as mrd
- class IndexHandler(tornado.web.RequestHandler):
- def get(self):
- self.render("index.html")
- def post(self):
- username = self.get_argument("username")
- password = self.get_argument("password")
- user_infos = mrd.select_table(table="users",column="*",condition="username", value=username)
- if user_infos:
- db_pwd = user_infos[0][2]
- if db_pwd == password:
- self.write("welcome you: " + username)
- else:
- self.write("your password was not right.")
- else:
- self.write("There is no thi user.")
特别注意,在methods目录中,只有不缺少init.py文件,才能在index.py中实现import methods.readdb as mrd。
代码修改到这里,看到的结果如图8-10所示。
图8-10 修改代码的结果
如图8-11所示是正确输入用户名(所谓正确,就是输入的用户名和密码合法,即在数据库中有该用户名,且密码匹配),并提交数据后,反馈给前端的欢迎信息。
图8-11 欢迎信息
用户的输入是最不可靠的,或许会出现多种情况。
如图8-12所示是输入的密码错误了,前端反馈给用户提示的信息。
图 8-12
这是随意输入的结果,数据库中无此用户。
上述演示中,数据库中的用户密码并没有加密,这不是真实的开发行为,在真实的开发中,一定要加密传输。
8.3.7 模板
网站做到现在,突然发现,前端页面写得太难看了。俗话说“外行看热闹,内行看门道”。程序员写的网站,在更多时候是给“外行”看的,他们可没有耐心来看代码,他们看的就是界面,因此把界面做得漂亮一点点至关重要。
其实,也不仅仅是漂亮的原因,而且前端页面还要显示从后端读取出来的数据。
恰好,Tornado提供比较好用的前端模板(tornado.template),通过这个模板,能够让前端编写更方便。
- render()render()方法能够告诉Tornado读入哪个模板,插入其中的模板代码,并返回结果给浏览器。比如在IndexHandler类中get()方法里面的self.render("index.html"),就是让Tornado到templates目中找到名为index.html的文件,读出它的内容,返回给浏览器。这样用户就能看到index.html所规定的页面了。前面所写的index.html仅仅是html标记,没有显示出所谓“模板”的作用。为此,将index.html和index.py文件做如下改造。
- #!/usr/bin/env python
- # coding=utf-8
- import tornado.web
- import methods.readdb as mrd
- class IndexHandler(tornado.web.RequestHandler):
- def get(self):
- usernames = mrd.select_columns(table="users",column="username")
- one_user = usernames[0][0]
- self.render("index.html", user=one_user)
index.py文件中,只修改了get()方法,从数据库中读取用户名,并且提出用户(one_user),然后通过self.render("index.html",user=one_user)将这个用户名放到index.html中,其中user=one_user的作用就是传递对象到模板。
要提醒读者注意的是,在上面的代码中,我使用了mrd.select_columns(table="users",column="username"),也就是说必须要在methods目录中的readdb.py文件中有一个名为select_columns的函数。为了使读者能够理解,贴出已经修改的readdb.py文件代码,比上一节多了函数select_columns:
- #!/usr/bin/env python
- # coding=utf-8
- from db import *
- def select_table(table, column, condition, value ):
- sql = "select " + column + " from " + table + " where " + condition + "='" + value + "'"
- cur.execute(sql)
- lines = cur.fetchall()
- return lines
- def select_columns(table, column ):
- sql = "select " + column + " from " + table
- cur.execute(sql)
- lines = cur.fetchall()
- return lines
下面是index.html修改后的代码:
- <!DOCTYPE html>
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Learning Python</title>
- </head>
- <body>
- <h2>登录页面</h2>
- <p>用用户名为:{{user}}登录</p>
- <form method="POST">
- <p><span>UserName:</span><input type="text" id="username"/></p>
- <p><span>Password:</span><input type="password" id="password" /></p>
- <p><input type="BUTTON" value="登录" id="login" /></p>
- </form>
- <script src="{{static_url("js/jquery.min.js")}}"></script>
- <script src="{{static_url("js/script.js")}}"></script>
- </body>
“<p>用户名为:{{user}}登录</p>”,在这里用了“{{}}”方式接受对应的变量引用的对象。即在首页打开之后,用户应当看到有一行提示,如图8-13所示。
图8-13 首页提示
图中箭头所指就是从数据库中读取出来的用户名,借助于模板中的双大括号“{{}}”显示出来。
“{{}}”本质上是占位符,当这个html被执行的时候,这个位置会被一个具体的对象(例如上面就是字符串qiwsir)所替代。具体是哪个具体对象替代这个占位符,完全由render()方法中的关键词来指定,也就是render()中的关键词与模板中的占位符包裹着的关键词一致。
用这种方式,修改一下用户正确登录之后的效果。要求用户正确登录之后,跳转到另外一个页面,并且在那个页面中显示出用户的完整信息。
先修改url.py文件,在其中增加一些内容。完整代码如下:
- #!/usr/bin/env python
- # coding=utf-8
- """
- the url structure of website
- """
- import sys
- reload(sys)
- sys.setdefaultencoding("utf-8")
- from handlers.index import IndexHandler
- from handlers.user import UserHandler
- url = [
- (r'/', IndexHandler),
- (r'/user', UserHandler),
- ]
然后就建立handlers/user.py文件,内容如下:
- #!/usr/bin/env python
- # coding=utf-8
- import tornado.web
- import methods.readdb as mrd
- class UserHandler(tornado.web.RequestHandler):
- def get(self):
- username = self.get_argument("user")
- user_infos = mrd.select_table(table="users",column="*",condition="username",value=username)
- self.render("user.html", users = user_infos)
在get()中使用self.get_argument("user"),目的是要通过url获取参数user的值。因此,当用户登录后,得到正确的返回值,那么js应该用这样的方式载入新的页面。
注意,上述的user.py代码为了简单仅突出本将要说明的,没有对user_infos的结果进行判断,但在实际的编程中,需要进行判断或者使用try…except。
- $(document).ready(function(){
- $("#login").click(function(){
- var user = $("#username").val();
- var pwd = $("#password").val();
- var pd = {"username":user, "password":pwd};
- $.ajax({
- type:"post",
- url:"/",
- data:pd,
- cache:false,
- success:function(data){
- window.location.href = "/user?user="+data;
- },
- error:function(){
- alert("error!");
- },
- });
- });
- });
接下来是user.html模板。注意,上面的代码中,user_infos引用的对象不是一个字符串了,即传入模板的不是一个字符串,而是一个元组。对此,模板这样来处理它:
- <!DOCTYPE html>
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Learning Python</title>
- </head>
- <body>
- <h2>Your informations are:</h2>
- <ul>
- {% for one in users %}
- <li>username:{{one[1]}}</li>
- <li>password:{{one[2]}}</li>
- <li>email:{{one[3]}}</li>
- {% end %}
- </ul>
- </body>
显示的效果如图8-14所示。
图8-14 显示的效果
在上面的模板中,其实用到了模板语法。
在模板的双大括号中,可以写类似Python的语句或者表达式。比如:
- >>> from tornado.template import Template
- >>> print Template("{{ 3+4 }}").generate()
- 7
- >>> print Template("{{ 'python'[0:2] }}").generate()
- py
- >>> print Template("{{ '-'.join(str(i) for i in range(10)) }}").generate()
- 0-1-2-3-4-5-6-7-8-9
如果在模板中的某个地方写上{{3+4}},当那个模板被render()读入之后,在页面上该占位符的地方就显示7。这说明Tornado自动将双大括号内的表达式进行计算,并将其结果以字符串的形式返回到浏览器输出。
除了表达式之外,Python的语句也可以在表达式中使用,包括if、for、while和try。只不过要有一个语句做开始和结束的标记,用以区分哪里是语句、哪里是HTML标记符。
语句的形式:{{%语句%}}
例如:
- {{% if user=='qiwsir' %}}
- {{ user }}
- {{% end %}}
上面的举例中,第一行虽然是if语句,但是不要在后面写冒号了。最后一行一定不能缺少,表示语句块结束。将这一个语句块放到模板中,当被render读取此模板的时候,Tornado将执行结果返回给浏览器显示,跟前面的表达式一样。实际例子中可以看上图的输出结果和对应的循环语句。
8.3.8 转义字符
虽然读者已经对字符转义问题不陌生了,但是在网站开发中,它还是一个令人感到麻烦的问题。转义字符(Escape Sequence)也称为字符实体(Character Entity),它的存在是因为在网页中“<”、“>”之类的符号不能直接被输出,因为它们已经被用作了HTML标记符,如果在网页上用到它们,就要转义。另外,还有一些字符在ASCII字符集中没有定义(如版权符号“”),若这样的符号在HTML中出现,也需要转义字符(如“
”对应的转义字符是“&copy;”)。
上述是指前端页面的字符转义,在后端程序中,因为要读写数据库,也会遇到字符转义问题。
比如一个简单的查询语句“select username,password from usertable where username='qiwsir'”,如果在登录框中没有输入“qiwsir”,而是输入了“a;drop database;”,这个查询语句就变成了“select username,password from usertable where username=a;drop database;”,如果后端程序执行了这条语句会怎么样呢?后果很严重,因为会drop database,届时真的是欲哭无泪了。类似的情况还很多,比如还可以输入“<input type="text"/>”,结果出现了一个输入框,如果是“<form action="…"”,就会造成跨站攻击。这方面的问题还很多,读者有空可以到网上搜索一下。
所以,后端也要转义。转义是不是很麻烦呢?
Tornado为你着想了,因为存在以上转义问题,而且会有粗心的程序员忘记,于是在Tornado中,模板默认为自动转义,这是多么好的设计呀。于是所有表单输入,你就不用担心会遇到上述问题了。
为了能够体会自动转义,不妨在登录框中输入上面那样的字符,然后用print语句看看后台得到了什么(请读者自行完成)。
自动转义是一个好事情,但是,有时候不需要转义,比如想在模板中这样做:
- <!DOCTYPE html>
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Learning Python</title>
- </head>
- <body>
- <h2>登录页面</h2>
- <p>用用户名为:{{user}}登录</p>
- <form method="POST">
- <p><span>UserName:</span><input type="text" id="username"/></p>
- <p><span>Password:</span><input type="password" id="password" /></p>
- <p><input type="BUTTON" value="登录" id="login" /></p>
- </form>
- {% set website = "<a href='http://www.itdiffer.com'>welcome to my website</a>" %}
- {{ website }}
- <script src="{{static_url("js/jquery.min.js")}}"></script>
- <script src="{{static_url("js/script.js")}}"></script>
- </body>
这是index.html的代码,我增加了{%set website="<a href='http://www.itdiffer.com'>welcome to my website</a>"%},作用是设置一个变量,名字是website,它对应的内容是一个做了超链接的文字。然后在下面使用这个变量{{website}},本希望能够出现的是一行字“welcome to my website”,单击这行字,就可以打开对应链接的网站。可是,看到了如图8-15所示的页面。
图8-15 自动转义的结果
下面那一行把整个源码都显示出来了,这就是自动转义的结果。这里需要的是不转义。于是可以将{{website}}修改为:
- {% raw website %}
表示这一行不转义。但是别的地方还是转义的。这是一种最推荐的方法。
如果你要全转义,可以使用:
- {% autoescape None %}
- {{ website }}
貌似省事,但是并不推荐使用。
将下面几个函数放在这里备查,或许在某些时候会用到,都是可以使用在模板中的。
- escape(s):替换字符串s中的&、<、>为他们对应的HTML字符。
- url_escape(s):使用urllib.quote_plus替换字符串s中的字符为url编码形式。
- json_encode(val):将val编码成JSON格式。
- squeeze(s):过滤字符串s,把连续的多个空白字符替换成一个空格。
8.3.9 模板继承
用前面的方法已经能够很顺利地编写模板了。如果读者留心一下,会觉得每个模板都有相同的内容,遇到这种问题,作为程序员应该想到“继承”,它的作用之一就是能够让代码重用。
在Tornado的模板中,也能继承。
先建立一个文件,命名为base.html,代码如下:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Learning Python</title>
- </head>
- <body>
- <header>
- {% block header %}{% end %}
- </header>
- <content>
- {% block body %}{% end %}
- </content>
- <footer>
- {% set website = "<a href='http://www.itdiffer.com'>welcome to my website</a>" %}
- {% raw website %}
- </footer>
- <script src="{{static_url("js/jquery.min.js")}}"></script>
- <script src="{{static_url("js/script.js")}}"></script>
- </body>
- </html>
接下来就以base.html为父模板,依次改写index.html模板和user.html模板。
index.html代码如下:
- {% extends "base.html" %}
- {% block header %}
- <h2>登录页面</h2>
- <p>用用户名为:{{user}}登录</p>
- {% end %}
- {% block body %}
- <form method="POST">
- <p><span>UserName:</span><input type="text" id="username"/></p>
- <p><span>Password:</span><input type="password" id="password" /></p>
- <p><input type="BUTTON" value="登录" id="login" /></p>
- </form>
- {% end %}
user.html的代码如下:
- {% extends "base.html" %}
- {% block header %}
- <h2>Your informations are:</h2>
- {% end %}
- {% block body %}
- <ul>
- {% for one in users %}
- <li>username:{{one[1]}}</li>
- <li>password:{{one[2]}}</li>
- <li>email:{{one[3]}}</li>
- {% end %}
- </ul>
- {% end %}
以上代码已经没有以前重复的部分了。“{%extends"base.html"%}”意味着以base.html为父模板。在base.html中规定了形式如同“{%block header%}{%end%}”这样的块语句,在index.html和user.html中,分别对块语句中的内容进行了重写(或者说是填充)。这就相当于在base.html中做了一个结构,在子模板中按照这个结构填内容。
8.3.10 CSS
基本的流程已经差不多了,如果要美化前端,还需要使用css,它的使用方法跟js类似,也是在静态目录中建立文件即可。然后把下面这句加入到base.html的<head></head>中:
- <link rel="stylesheet" type="text/css" href="{{static_url("css/style.css")}}">
当然,要在style.css中写一个样式,比如:
- body {
- color:red;
- }
然后看看前端显示什么样子了,如图8-16所示。
图8-16 前端显示
至于其他关于CSS方面的内容,就不重点讲解了,读者可以参考关于CSS的资料。
至此,一个简单的基于Tornado的网站就做好了,虽然它很丑,但是它很有前途。因为读者只要按照上述的讨论,就可以在里面增加各种自己认为可以增加的内容。
建议读者在学习以上内容基础上,可以继续完成下面的几个功能:
- 用户注册。
- 用户发表文章。
- 用户文章列表,并根据文章标题查看文章内容。
- 用户重新编辑文章。
8.3.11 cookie和安全
cookie是现在网站重要的内容,特别是当有用户登录的时候,所以需要学习了解cookie。维基百科如是说:
cookie(复数形态cookies),中文名称为小型文本文件或小甜饼,指某些网站为了辨别用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)。定义于RFC2109。是网景公司的前雇员Lou Montulli在1993年3月发明的。
关于cookie的作用,维基百科说得非常详细:
因为HTTP协议是无状态的,即服务器不知道用户上一次做了什么,这严重阻碍了交互式Web应用程序的实现。在典型的网上购物场景中,用户浏览了几个页面,买了一盒饼干和两瓶饮料。最后结账时,由于HTTP的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么。所以cookie就是用来绕开HTTP的无状态性的“额外手段”之一。服务器可以设置或读取cookies中包含的信息,借此维护用户跟服务器会话中的状态。
在刚才的购物场景中,当用户选购了第一项商品,服务器在向用户发送网页的同时,还发送了一段cookie,记录着那项商品的信息。当用户访问另一个页面,浏览器会把cookie发送给服务器,于是服务器就知道他之前选购了什么。用户继续选购饮料,服务器就在原来那段cookie里追加新的商品信息。结账时,服务器读取发送来的cookie就行了。
cookie另一个典型的应用是,当登录一个网站时网站往往会请求用户输入用户名和密码,并且用户可以勾选“下次自动登录”。如果勾选了,那么下次访问同一网站时,用户会发现没输入用户名和密码就已经登录了。这正是因为前一次登录时,服务器发送了包含登录凭据(用户名加密码的某种加密形式)的cookie到用户的硬盘上。第二次登录时(如果该cookie尚未到期)浏览器会发送该cookie服务器验证凭据,于是不必输入用户名和密码就让用户登录了。
cookie也有缺陷,比如来自伟大的维基百科也列出了三条:
- cookie会被附加在每个HTTP请求中,所以无形中增加了流量。
- 由于在HTTP请求中的cookie是明文传递的,所以安全性成问题(除非用HTTPS)。
- cookie的大小限制在4KB左右,或许在某些情况下有点不够用。对于用户来说,可以通过改变浏览器的设置来禁用cookie,也可以删除历史的cookie。但就目前而言,大多数人都不再禁用cookie了。
cookie最让人担心的还是由于它存储了用户的个人信息,并且最终这些信息要发给服务器,那么它就会成为某些人的目标或者工具,比如有cookie盗贼,就是搜集用户cookie,然后利用这些信息进入用户账号,达到个人某种不可告人之目的;还有被称之为cookie投毒的说法,是利用客户端的cookie传给服务器的机会,修改传回去的值。这些行为常常是通过一种被称为“跨站指令脚本(Cross site scripting)”(或者跨站指令码)的行为方式实现的。伟大的维基百科这样解释了跨站脚本:
跨网站脚本(Cross-site scripting,通常简称为XSS或跨站脚本或跨站脚本攻击)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含HTML和用户端脚本语言。
XSS攻击通常指的是利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java、VBScript、ActiveX、Flash或者是普通的HTML。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。
对cookie的普遍使用,用户和网站都受益了,但也要防止有人用它作恶。
在Tornado中,也提供对cookie的读写函数,帮助我们管理和使用它。
set_cookie()和get_cookie()是默认提供的两个方法,但它是明文不加密传输的。
在index.py文件的IndexHandler类的post()方法中,当用户登录,验证用户名和密码后,将用户名和密码存入cookie,代码如下:
- def post(self):
- username = self.get_argument("username")
- password = self.get_argument("password")
- user_infos = mrd.select_table(table="users", column="*", condition="username", value=username)
- if user_infos:
- db_pwd = user_infos[0][2]
- if db_pwd == password:
- self.set_cookie(username,db_pwd) #设置cookie
- self.write(username)
- else:
- self.write("your password was not right.")
- else:
- self.write("There is no thi user.")
上面代码中,较以前只增加了一句“self.set_cookie(username,db_pwd)”,再回到登录页面,运行之后如图8-17所示。
看图8-17中箭头所指,从左开始的第一个是用户名,第二个是存储的该用户密码。将我在登录时输入的密码以明文的方式存储在cookie里面了。
明文存储,显然不安全。
Tornado提供另外一种安全的方法:set_secure_cookie()和get_secure_cookie(),之所以称其为安全cookie,是因为它以明文加密的方式传输。此外,跟set_cookie()的区别还在于,set_secure_cookie()执行后的cookie保存在磁盘中,直到它过期为止。也是因为这个原因,即使关闭浏览器,在失效时间以前,cookie都一直存在。
图8-17 回到登录页面
要是用set_secure_cookie()方法设置cookie,要先在application.py文件的setting中进行如下配置:
- setting = dict(
- template_path = os.path.join(os.path.dirname(__file__), "templates"),
- static_path = os.path.join(os.path.dirname(__file__), "statics"),
- cookie_secret = "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
- )
其中“cookie_secret="bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E="”是为此增加的,但是,它并不是真正的加密,仅仅是一个障眼法罢了。
因为tornado会将cookie值编码为Base-64字符串,并增加一个时间戳和一个cookie内容的HMAC签名。所以,cookie_secret的值,常常用下面的方式生成(这是一个随机的字符串):
- >>> import base64, uuid
- >>> base64.b64encode(uuid.uuid4().bytes)
- 'w8yZud+kRHiP9uABEXaQiA=='
如果嫌弃上面的签名短,可以用“base64.b64encode(uuid.uuid4().bytes+uuid.uuid4().bytes)”获取。这里得到的是一个随机字符串,用它作为cookie_secret值。
然后修改index.py中设置cookie那句话,变成:
- self.set_secure_cookie(username,db_pwd)
重新跑一个,效果如图8-18所示。
啊哈,果然“密”了很多。
如果要获取此cookie,用self.get_secure_cookie(username)即可。
图8-18 修改后效果图
这样是不是就安全了?如果这样就安全了,那你也太低估黑客们的技术实力了,甚至于用户自己也会修改cookie值。还要更安全,所以又有了httponly和secure属性,用来防范cookie投毒。设置方法是:
- self.set_secure_cookie(username, db_pwd, httponly=True, secure=True)
要获取cookie,可以使用self.set_secure_cookie(username)方法,将这句放在user.py中某个适合的位置,并且可以用print语句打印出结果,就能看到变量username对应的cookie了。这时候已经不是那个“密”过的,是明文显示。
用这样的方法,浏览器通过SSL连接传递cookie,能够在一定程度上防范跨站脚本攻击。
8.3.12 XSRF
XSRF的含义是Cross-site request forgery,即跨站请求伪造,也称之为“one click attack”,通常缩写成CSRF或者XSRF,可以读作“sea surf”。这种对网站的攻击方式跟上面的跨站脚本(XSS)似乎相像,但攻击方式不一样。XSS利用站点内的信任用户,而XSRF则通过伪装来自受信任用户的请求而利用受信任的网站。与XSS攻击相比,XSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。
还有一点需要提醒读者,即在开发应用时需要深谋远虑。任何会产生副作用的HTTP请求,比如单击购买按钮、编辑账户设置、改变密码或删除文档等都应该使用post()方法,这是良好的RESTful做法。
又一个新名词:REST。这是一种Web服务实现方案。伟大的维基百科中这样描述:
表征性状态传输(英文:Representational State Transfer,简称REST)是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。目前在三种主流的Web服务实现方案中,因为REST模式与复杂的SOAP和XML-RPC相比更加简洁,越来越多的Web服务开始采用REST风格设计和实现。例如,Amazon.com提供接近REST风格的Web服务进行图书查找;雅虎提供的Web服务也是REST风格的。
在Tornado中还提供了XSRF保护的方法。
在application.py文件中使用xsrf_cookies参数开启XSRF保护。
- setting = dict(
- template_path = os.path.join(os.path.dirname(__file__), "templates"),
- static_path = os.path.join(os.path.dirname(__file__), "statics"),
- cookie_secret = "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
- xsrf_cookies = True,
- )
这样设置之后,Tornado将拒绝请求参数中不包含正确的_xsrf值的post/put/delete请求。Tornado会在后面悄悄地处理_xsrf cookies,所以,在表单中也要包含XSRF令牌以确保请求合法。比如index.html的表单,修改如下:
- {% extends "base.html" %}
- {% block header %}
- <h2>登录页面</h2>
- <p>用用户名为:{{user}}登录</p>
- {% end %}
- {% block body %}
- <form method="POST">
- {% raw xsrf_form_html() %}
- <p><span>UserName:</span><input type="text" id="username"/></p>
- <p><span>Password:</span><input type="password" id="password" /></p>
- <p><input type="BUTTON" value="登录" id="login" /></p>
- </form>
- {% end %}
“{%raw xsrf_form_html()%}”是新增的,目的就在于实现上面所说的授权给前端以合法请求。
前端向后端发送的请求是通过Ajax(),所以,在Ajax请求中,需要一个_xsrf参数。
以下是script.js的代码:
- function getCookie(name){
- var x = document.cookie.match("\\b" + name + "=([^;]*)\\b");
- return x ? x[1]:undefined;
- }
- $(document).ready(function(){
- $("#login").click(function(){
- var user = $("#username").val();
- var pwd = $("#password").val();
- var pd = {"username":user, "password":pwd, "_xsrf":getCookie("_xsrf")};
- $.ajax({
- type:"post",
- url:"/",
- data:pd,
- cache:false,
- success:function(data){
- window.location.href = "/user?user="+data;
- },
- error:function(){
- alert("error!");
- },
- });
- });
- });
函数getCookie()的作用是得到cookie值,然后将这个值放到向后端post的数据中“var pd={"username":user,"password":pwd,"_xsrf":getCookie("_xsrf")};”。运行的结果如图8-19所示。
图8-19 运行结果
这是Tornado提供的XSRF防护方法。是不是这样做就高枕无忧了呢?世界是复杂的,要做好一个网站,需要考虑的事情还很多。
常常听到人说做个网站怎么简单,客户用这种说辞来压低价格,老板用这种说辞来缩短工时成本,从上面的简单叙述中,你还觉得网站是随便几个页面就完事儿的吗?除非那个网站不是给人看的,而是在那里摆着的。
8.3.13 用户验证
用户登录之后,当翻到别的网页中时,往往需要验证用户是否处于登录状态。当然,一种比较直接的方法,就是在转到每个目录时,都从Cookie中把用户信息传到后端,跟数据库验证。这不仅是直接的,也是基本的流程。但是,如果这个过程总让用户自己来做,框架的作用就显不出来了。Tornado就提供了一种用户验证方法。
为了后面更工程化地使用Tornado编程,需要将前面已经有的代码进行重新梳理。下面只是将有修改的文件代码写出来,不做过多解释,必要的有注释,相信读者在学习前面内容的基础上能够理解。
在handler目录中增加一个文件,名称是base.py,代码如下:
- #! /usr/bin/env python
- # coding=utf-8
- import tornado.web
- class BaseHandler(tornado.web.RequestHandler):
- def get_current_user(self):
- return self.get_secure_cookie("user")
在这个文件中,目前只做一件事情,就是建立一个名为BaseHandler的类,然后在里面放置一个方法,就是得到当前的Cookie。在这里要特别向读者说明,在这个类中,其实还可以写很多别的东西,比如你可以将数据库连接写到这个类的初始化init()方法中。因为在其他的类中,我们要继承这个类。所以,这样一个架势就为读者以后的扩展增加了冗余空间。
然后把index.py文件改写为:
- #!/usr/bin/env python
- # coding=utf-8
- import tornado.escape
- import methods.readdb as mrd
- from base import BaseHandler
- class IndexHandler(BaseHandler): #继承base.py中的类BaseHandler
- def get(self):
- usernames = mrd.select_columns(table="users",column="username")
- one_user = usernames[0][0]
- self.render("index.html", user=one_user)
- def post(self):
- username = self.get_argument("username")
- password = self.get_argument("password")
- user_infos = mrd.select_table(table="users", column="*", condition="username", value=username)
- if user_infos:
- db_pwd = user_infos[0][2]
- if db_pwd == password:
- self.set_current_user(username) #将当前用户名写入cookie
- self.write(username)
- else:
- self.write("-1")
- else:
- self.write("-1")
- def set_current_user(self, user):
- if user:
- #注意这里使用了tornado.escape.json_encode()方法
- self.set_secure_cookie('user', tornado.escape.json_encode(user))
- else:
- self.clear_cookie("user")
- class ErrorHandler(BaseHandler): #增加了一个专门用来显示错误的页面
- def get(self):
- self.render("error.html")
在index.py的类IndexHandler中,继承了BaseHandler类,并且增加了一个方法,set_current_user()用于将用户名写入Cookie。请读者特别注意tornado.escape.json_encode()方法,其功能是:
- tornado.escape.json_encode(value) JSON-encodes the given Python object.
如果要查看源码,可以阅读:http://www.tornadoweb.org/en/branch2.3/escape.html。
这样做的本质是把user转化为json,写入到了Cookie中。如果从Cookie中把它读出来,使用user的值时,还会用到:
- tornado.escape.json_decode(value) Returns Python objects for the given JSON string
它们与json模块中的dump()、load()功能相仿。
接下来要对user.py文件也进行重写:
- #!/usr/bin/env python
- # coding=utf-8
- import tornado.web
- import tornado.escape
- import methods.readdb as mrd
- from base import BaseHandler
- class UserHandler(BaseHandler):
- @tornado.web.authenticated
- def get(self):
- #username = self.get_argument("user")
- username = tornado.escape.json_decode(self.current_user)
- user_infos = mrd.select_table(table="users", column="*", condition="username", value=username)
- self.render("user.html", users = user_infos)
在get()方法前面添加@tornado.web.authenticated,这是一个装饰器,它的作用就是完成Tornado的认证功能,即能够得到当前合法用户。在原来的代码中,用username=self.get_argument("user")方法,从url中得到当前用户名,现在把它注释掉,改用self.current_user,这是和前面的装饰器配合使用的,如果它的值为假,就根据setting中的设置,寻找login_url所指定的目录(请关注下面对setting的配置)。
由于在index.py文件的set_current_user()方法中,是将user值转化为json写入Cookie的,这里就得用username=tornado.escape.json_decode(self.current_user)解码。得到的username值,可以被用于后一句中的数据库查询。
application.py中的setting也要做相应修改:
- #!/usr/bin/env python
- # coding=utf-8
- from url import url
- import tornado.web
- import os
- setting = dict(
- template_path = os.path.join(os.path.dirname(__file__), "templates"),
- static_path = os.path.join(os.path.dirname(__file__), "statics"),
- cookie_secret = "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
- xsrf_cookies = True,
- login_url = '/',
- )
- application = tornado.web.Application(
- handlers = url,
- **setting
- )
与以前代码的重要区别在于“login_url='/',”,如果用户不合法,根据这个设置,会返回到首页。当然,如果有单独的登录界面,比如/login,也可以login_url='/login'。
如此完成的是用户登录到网站之后,在页面转换的时候实现用户认证。
8.3.14 相关概念
1.同步和异步
有不少资料对这两个概念做了不同角度和层面的解释。在我来看,最典型的例子就是打电话和发短信。
打电话就是同步。张三给李四打电话,张三说:“是李四吗?”。当这个信息被张三发出,提交给李四,等待李四的响应(一般会听到“是”,或者“不是”),只有得到了李四返回的信息之后,才能进行后续的信息传送。
发短信是异步。张三给李四发短信,编辑了一句话“今晚一起看老齐的《零基础学python》”,发送给李四。李四或许马上回复,或许过一段时间才回复,这段时间有多长不一定。总之,李四不管什么时候回复,张三会以听到短信铃声为提示查看短信。
以上方式理解“同步”和“异步”不是很精准,有些地方或许有牵强。要严格理解,需要用严格一点的定义表述(以下表述参照了“知乎”上的回答):
同步和异步关注的是消息通信机制(synchronous communication/asynchronous communication)。
所谓同步,就是在发出一个“调用”时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。
而异步则相反,“调用”在发出之后,就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在“调用”发出后,“被调用者”通过状态、通知来通知调用者,或通过回调函数处理这个调用。
可能还是前面的打电话和发短信更好理解。
2.阻塞和非阻塞
“阻塞和非阻塞”与“同步和异步”常常被混为一谈,其实它们之间还是有差别的。如果按照一个“差不多”先生的思维方法,你也可以不那么深究它们之间学理上的差距,反正在你的程序中,会使用就可以了。不过,必要的严谨还是需要的,特别是本书中,要装扮的让别人看来自己懂,于是就再引用知乎上的说明:
阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态。
阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。非阻塞调用是指在不能立刻得到结果之前,该调用不会阻塞当前线程。
按照这个说明,发短信显然就是非阻塞,发出去一条短信之后,你利用手机还可以干别的,乃至于再发一条“老齐的课程没意思,还是看PHP刺激”也是可以的。
关于这两组基本概念的辨析,不是本教程的重点,读者可以参阅这篇文章:http://www.cppblog.com/converse/archive/2009/05/13/82879.html,文章作者做了细致入微的辨析。
8.3.15 Tornado的同步
此前,在Tornado基础上已经完成的Web就是同步的、阻塞的。为了更明显地感受这一点,不妨这样试一试。
在handlers文件夹中建立一个文件,命名为sleep.py。
- #!/usr/bin/env python
- # coding=utf-8
- from base import BaseHandler
- import time
- class SleepHandler(BaseHandler):
- def get(self):
- time.sleep(17)
- self.render("sleep.html")
- class SeeHandler(BaseHandler):
- def get(self):
- self.render("see.html")
sleep.html和see.html是两个简单的模板,内容可以自己写。别忘记修改url.py中的目录。
然后测试稍微复杂一点,打开浏览器之后,打开两个标签,分别在两个标签中输入localhost:8000/sleep(记为标签1)和localhost:8000/see(记为标签2),注意我用的是8000端口。输入之后先不要单击回车访问。做好准备,记住切换标签可以用“ctrl-tab”组合键。
1.执行标签1,让它访问网站。
2.马上切换到标签2,访问网址。
3.注意观察,两个标签页面,是不是都在显示“正在访问,请等待”。
4.当标签1不呈现等待提示(比如一个正在转的圆圈)时,标签2的表现如何?几乎同时也访问成功了。
建议读者修改sleep.py中的time.sleep(17)这个值,多试试。
当然,这是比较笨拙的方法,可以通过测试工具完成上述操作比较。
8.3.16 异步设置
Tornado本来就是一个异步的服务框架,体现在Tornado的服务器和客户端的网络交互的异步上,起作用的是tornado.ioloop.IOLoop。但是如果在客户端请求服务器之后,在执行某个方法的时候,比如上面的代码中执行get()方法的时候,遇到了time.sleep(17)这个需要执行时间比较长的操作,耗费时间,就会使整个Tornado服务器的性能受限。
为了解决这个问题,Tornado提供了一套异步机制,就是异步装饰器@tornado.web.asynchronous。
- #!/usr/bin/env python
- # coding=utf-8
- import tornado.web
- from base import BaseHandler
- import time
- class SleepHandler(BaseHandler):
- @tornado.web.asynchronous
- def get(self):
- tornado.ioloop.IOLoop.instance().add_timeout(time.time() + 17, callback=self. on_response)
- def on_response(self):
- self.render("sleep.html")
- self.finish()
将sleep.py的代码如上述一样改造,即在get()方法前面增加了装饰器@tornado.web.asynchronous,它的作用在于将Tornado服务器本身默认的设置_auto_fininsh值修改为False。如果不用这个装饰器,客户端访问服务器的get()方法并得到返回值之后,两者之间的连接就断开了,但是用了@tornado.web.asynchronous之后,这个连接就不关闭,直到执行了self.finish()才关闭这个连接。
tornado.ioloop.IOLoop.instance().add_timeout()也是一个实现异步的函数,time.time()+17给前面的函数提供一个参数,这样实现了相当于time.sleep(17)的功能,不过,还没有完成,当这个操作完成之后,就执行回调函数on_response()中的self.render("sleep.html"),并关闭连接self.finish()。
所谓异步,就是要解决原来的time.sleep(17)造成的服务器处理时间长、性能下降的问题。解决方法如上描述。
读者看这个代码,或许会感觉有点不舒服。如果有这个感觉是正常的,因为它里面除了装饰器之外,用到了一个回调函数,它让代码的逻辑不是平铺下去,而是被分割成了两段。第一段是tornado.ioloop.IOLoop.instance().add_timeout(time.time()+17,callback=self.on_response),用callback=self.on_response来使用回调函数,并没有如同改造之前直接self.render("sleep.html");第二段是回调函数on_response(self),要在这个函数里面执行self.render("sleep.html"),并且以self.finish()`结尾以关闭连接。
这还是执行简单逻辑,如果复杂了,要不断地进行“回调”,无法让逻辑顺利延续,就会“眩晕”了。这种现象被业界称为“代码逻辑拆分”,打破了原有逻辑的顺序性。为了让代码逻辑不至于被拆分的七零八落,于是就出现了另外一种常用的方法:
- #!/usr/bin/env python
- # coding=utf-8
- import tornado.web
- import tornado.gen
- from base import BaseHandler
- import time
- class SleepHandler(tornado.web.RequestHandler):
- @tornado.gen.coroutine
- def get(self):
- yield tornado.gen.Task(tornado.ioloop.IOLoop.instance().add_timeout, time.time() + 17)
- #yield tornado.gen.sleep(17)
- self.render("sleep.html")
从整体上看,这段代码避免了回调函数,看着顺利多了。
再看细节部分。
首先使用的是@tornado.gen.coroutine装饰器,所以要在前面有import tornado.gen。跟这个装饰器类似的是@tornado.gen.engine装饰器,两者功能类似,有一点细微差别。请阅读官方对此的解释:
This decorator(指engine)is similar to coroutine,except it does not return a Future and the callback argument is not treated specially.
@tornado.gen.engine是古时候用的,现在我们都使用@tornado.gen.corroutine了,这个是在tornado 3.0以后开始用的。在网上查阅资料的时候,会遇到一些使用@tornado.gen.engine的,但是在你使用或者借鉴代码的时候,可以勇敢地将其修改为@tornado.gen.coroutine。有了这个装饰器,就能够控制下面的生成器的流程了。
然后就看到get()方法里面的yield了,这是一个生成器。“yield tornado.gen.Task(tornado.ioloop.IOLoop.instance().add_timeout,time.time()+17)”的执行过程,先看括号里面,跟前面的一样是来替代time.sleep(17)的,然后是tornado.gen.Task()方法,其作用是“Adapts a callback-based asynchronous function for use in coroutines.”(由于怕翻译后遗漏信息,所以引用原文)。返回后,使用yield得到了一个生成器,先把流程挂起,等完全完毕再唤醒继续执行。要提醒读者,生成器都是异步的。
其实,上面啰嗦了一堆,可以用代码中注释的一句话来代替yield tornado.gen.sleep(17),之所以啰嗦,就是为了顺便看到tornado.gen.Task()方法,因为如果读者在看古老的代码时会遇到。但是,后面你写的时候,就不要那么啰嗦了,请用yield tornado.gen.sleep()。
至此,基本上对Tornado的异步设置有了概览,不过,上面的程序在实际中没有什么价值。在工程中,要让Tornado网站真正异步起来还要做很多事情,不仅仅是如上面的设置,因为其实很多东西都不是异步的。
在研发实践中,异步设置是比较复杂的,不是简单地完成上述流程就行了。比如以下各项中,尽管你已经完成了前面的设置,如果忽视了下面这些项目,那么tornado的非阻塞、异步优势削减了。
- 数据库的所有操作,不管你的数据是SQL还是noSQL、connect、insert、update等。
- 文件操作,打开、读取、写入等。
- time.sleep,在前面举例中已经看到了。
- smtplib,发邮件的操作。
- 一些网络操作,比如tornado的httpclient以及pycurl等。或许在编程实践中还会遇到其他的同步、阻塞实践。仅仅就上面几项,是编程实践中经常会遇到的,怎么解决?
聪明的大牛程序员帮我们做了扩展模块,专门用来实现异步/非阻塞。
- 在数据库方面,由于种类繁多,不能一一说明,比如MySQL,可以使用adb模块来实现Python的异步MySQL库;对于mongodb数据库,有一个非常优秀的模块,专门用于在Tornado和mongodb上实现异步操作,它就是motor。下面特别贴出它的Logo。
- 文件操作方面也没有替代模块,只能尽量控制好IO,或者使用内存型(Redis)及文档型(MongoDB)数据库。
- time.sleep()在Tornado中有替代:tornado.gen.sleep()或者tornado.ioloop.IOLoop.instance().add_timeout,这在前面代码已经显示了。
- smtp发送邮件,推荐改为tornado-smtp-client。
- 对于网络操作,要使用tornado.httpclient.AsyncHTTPClient。
其他的解决方法,只能看到问题具体说了,甚至没有很好的解决方法。不过,这里有一个列表,列出了足够多的库,供使用者选择:Async Client Libraries built on tornado.ioloop(https://github.com/tornadoweb/tornado/wiki/Links),同时这个页面里面还有很多别的链接,都是很好的资源,建议读者多看看。
到这里,请读者思考一个问题,既然对于mongodb有专门的motor库来实现异步,前面对于Tornado的异步,不管是哪个装饰器,都感觉麻烦,有没有专门的库来实现这种异步呢?这不是异想天开,还真有。也应该有,因为这才体现Python的特点。比如greenlet-tornado(https://github.com/mopub/greenlet-tornado)就是一个不错的库。读者可以浏览官方网站深入了解。
必须声明,前面演示如何在Tornado中设置异步的代码,仅仅是演示设置方法。在工程实践中,那个代码的意义不大,为此,应该有一个近似于实践的代码示例。是的,的确应该有。当我正要写这样的代码时,在网上发现一篇文章,这篇文章阻止了我写下去,因为我要写的内容那篇文章的作者早就写好了,而且我认为表述非常到位,示例也详细。所以,我不得不放弃,转而推荐给读者这篇好文章:http://emptysqua.re/blog/refactoring-tornado-coroutines/。