第 2 章 开发 Web 应用

本章中,我们先来了解 Web 应用的概念,然后一起实际开发一个简单的留言板应用,借此来了解 Web 应用开发的基本流程。至于留言板应用,它类似于观光景点的留言板,可以让访问网站的人在上面添加留言,说白了就是一个简单的网络留言板。正文中会涉及 HTML/CSS 和 Python 代码,但本书将省去对这些语法的说明。另外,本章的开发环境为 VirtualBox 虚拟机上的 Ubuntu 和 Python 2.7,同时还会用到 virtualenv。

2.1 了解Web 应用

要想开发 Web 应用,首先要知道 Web 应用是什么,它是怎样工作的。所以我们先来了解一下什么是 Web 应用。

2.1.1 Web 应用是什么

顾名思义,Web 应用就是可以通过网络使用的应用程序。比如 Google 的搜索服务、Gmail、Wikipedia、各种博客服务、Twitter 等迷你博客、GREE 和手机游戏、Facebook 等社交网站以及上面的社交游戏等,这些都是 Web 应用。人们使用 Web 应用时需要通过 Web 浏览器访问(连接)相应服务。

那么,同样需要通过 Web 浏览器阅览的 Web 站点又如何呢? Web 站点只是单纯地显示页面,它们能称作 Web 应用吗?

答案是不一定。某些 Web 站点或许可以称为 Web 应用。因为虽然有些东西在阅览者眼中像 Web 站点,但它实际上却是由 CMS(Content Management System,内容管理系统)等 Web 应用构成的。Web 浏览器会从访问对象的计算机(服务器)中下载 HTML、CSS、图片等各种内容,然后再把这些内容显示在我们的屏幕上。如果负责发送内容的服务器只是返回一些早已准备好的静态内容,那么这个 Web 站点就不能称作 Web 应用。只有能够动态生成并返回内容的系统(比如通过 Web 浏览器接收用户输入的数据,再根据这些数据生成内容)才能称作 Web 应用。

NOTE

CMS 是负责管理和发送文章、图片等内容的系统。Wiki 和博客系统也都属于 CMS。

2.1.2 Web 应用与桌面应用的区别

要通过 Web 浏览器连接服务器使用的应用叫 Web 应用;相对地,要将软件安装在计算机上才能使用的应用叫桌面应用。二者的区别如下。

比较项目 Web 应用 桌面应用
服务器 需要 可有可无
网络连接 需要 部分应用需要
使用 OS 具备的功能 不可能 可能
安装 不需要 需要
升级 不需要使用者专门注意 需要使用者亲自动手
跨平台使用 有 Web 浏览器就能用 需要各平台分别作处理
移动端支持 与 PC 端基本相同 需要各平台分别作处理
运行速度

从这张表上看,二者各有所长。但如果要开发一个让使用者通过网络互相交流信息的应用,那么由于桌面应用也同样需要用到服务器,所以使用 Web 应用开发起来会简单很多。不过,这个表中的内容也不是一成不变的。

比如 Web 应用本来离不开网络,但如今已有一部分应用可以离线运行。另外,某些桌面应用也开始具备自动升级新版本的功能(比如 Google Chrome),这让使用者不必再费心手动升级。可以说,Web 应用和桌面应用之间的距离正在逐渐缩小。

2.1.3 Web 应用的机制

接下来我们来了解一下 Web 应用的机制。

从我们在 Web 浏览器中输入 URL 到显示出页面,其间要进行下述处理。

① 用户在 Web 浏览器中输入 URL

② 向 DNS 服务器询问该 URL 中的域名,获取 IP 地址

③ Web 浏览器连接该 IP 地址的 Web 服务器,开始 HTTP 通信

④ Web 服务器根据 HTTP 发送来的信息运行 Web 应用,获取相应内容

⑤ Web 服务器响应,返回执行应用后得到的 HTML、CSS、JavaScript、图片文件等内容

⑥ Web 浏览器将收到的内容显示在页面中

下表是对上述处理流程中的术语进行的说明。

Web 浏览器通过 HTTP 等协议与 URL 所示的计算机通信,将 HTML、CSS、图片等内容显示在页面中的软件
URLUniform Resource Locator(统一资源定位符)的简称,其中包含域名。这个字符串用来表示计算机需要访问网络上的哪些内容
域名与 IP 地址挂钩的字符串(比如 URL 中包含的 www.beproud.jp 等字符串)
DNS 服务器可以通过域名查询 IP 地址的服务器
IP 地址这个数值用来在网络上识别我们想访问的计算机(以 IPv4 地址为例,IP 地址由 0 ~ 255 的数字和点组成,比如 192.168.0.1)
HTTPHypertext Transfer Protocol(超文本传输协议)的简称,是与被访问计算机之间的通信协议
HTML用来描述文档(包括文字和图片等)结构的语言
CSS描述 HTML 等语言描述的文档该如何显示的语言、机制
JavaScript在 Web 浏览器上运行的程序
Web 服务器通过 HTTP 进行通信的服务器程序 / 计算机
Web 应用在 Web 服务器上运行的程序
CGICommon Gateway Interface(通用网关接口)的简称,Web 应用的机制之一

上述流程可以用图 2.1 表示。

图像说明文字

图 2.1 Web 应用的处理流程

Web 应用与 CGI

CGI 是 Web 服务器运行 Web 应用的一种机制。Web 服务器执行 CGI 程序(CGI 脚本),然后将该程序的标准输出结果作为 HTTP 通信的响应返回给对方。最简单的 CGI 程序就是在控制台界面上显示字符串。

CGIHTTPServer 是 Python 标准模块中的 Web 服务器,它可以运行 CGI 程序。现在我们试着运行下面这个 CGI 程序(LIST 2.1)。

LIST 2.1 hello.py

  1. #!usrbin/env python
  2. print "200 OK"
  3. print "ContentType: text/plain"
  4. print ""
  5. print "Hello CGI!"

用 python 命令运行这个脚本,然后控制台界面中将显示如 LIST 2.2 所示的 4 行字符串。

LIST 2.2 用 python 命令运行 hello.py 的结果

  1. $ python hello.py
  2. 200 OK
  3. ContentType: text/plain
  4. Hello CGI!

用 CGIHTTPServer 运行 CGI 程序时,待运行文件必须位于 cgi-bin 目录下,所以我们要创建这个目录并将 hello.py 放进去。另外,必须具有运行权限才可以运行这些文件,因此还要用 chmod 命令赋予运行权限。配置好 CGI 程序之后,我们就可以按照下面的例子,用 python 命令的 -m 选项运行 CGIHTTPServer 了。

LIST 2.3 CGI 程序的配置与 CGIHTTPServer 的运行

  1. $ mkdir cgi-bin
  2. $ mv hello.py cgi-bin/
  3. $ chmod u+x cgi-bin/hello.py
  4. $ python -m CGIHTTPServer
  5. Serving HTTP on 0.0.0.0 port 8000 ...

执行 LIST 2.3 中的命令后,在该环境(这里是 VirtualBox 上的 Ubuntu)的 8000 端口等待请求的 Web 服务器将会启动。在终端按下 Ctrl + C 键可以关闭服务器。

要想通过本地环境(主 OS)的 Web 浏览器查看效果,我们需要先设置端口转发。设置方法与附录 B 中介绍的 SSH 端口的设置方法一样,这里我们给 VirtualBox 的端口转发添加如下设置:主、客端口均为 8000,协议为 TCP。

完成端口转发的设置后,我们只要通过 Web 浏览器访问 http://127.0.0.1:8000/cgi-bin/hello.py,即可看到 Hello CGI! 字样。

NOTE

127.0.0.1 代表的是自己的计算机的 IP 地址。设置过 VirtualBox 的端口转发之后,我们对自己的计算机(主OS)的 8000 端口的访问会被转发到客 OS 的 8000 端口。

本例中的 CGI 程序由 Python 代码编写而成。实际上,只要 CGI 程序能够进行标准输出,用任何语言都没有问题。

Web 应用与应用服务器

应用服务器指能运行 Web 应用的功能且能与 Web 服务器通信的服务器。处于启动状态的应用服务器会一直等待 Web 服务器发来请求,一旦接到请求便会运行 Web 应用并返回结果。由于待运行的程序一直放在内存里,所以它的速度通常要比 CGI 程序的速度更快。Web 服务器与应用服务器之间的通信协议通常为 HTTP 或 FCGI 等。如果一个应用服务器可以通过 HTTP 通信,那么我们就可以用 Web 浏览器访问它。

本章使用的 Flask 内置了用于开发的应用服务器(Web 服务器),因此不需要再另外准备 Web 服务器。

2.2 前置准备

接下来我们需要为开发 Web 应用做一些前置准备。这里需要使用 Python 和 virtualenv,二者的安装请参考第 1 章。

2.2.1 关于 Flask

开发留言板应用时,我们会用到 Flask1,因此需要事先安装。Flask 是一个用 Python 编写的 Web 应用框架,它整合了 Werkzeug(WSGI 实用工具)和 Jinja2(模板引擎)两个库。用它可以轻松完成小规模的应用程序开发。

1http://flask.pocoo.org/

2.2.2 安装Flask

安装 Flask 之前,我们先用 virtualenv 命令搭建开发应用所需的虚拟环境。用 LIST 2.4 中的命令可以搭建出名为 venv 的虚拟环境。

LIST 2.4 搭建名为 venv 的虚拟环境

  1. $ virtualenv venv

通过 source 命令执行 venv/bin 目录下的 activate 脚本,启动我们刚刚搭建好的虚拟环境(LIST 2.5)。执行完成后,命令提示符上会显示虚拟环境名 (venv)

LIST 2.5 启动虚拟环境 venv

  1. $ source venv/bin/activate

NOTE

Windows 需要使用 venv\Scripts\activate 命令启动虚拟环境。

接下来使用 pip 命令在虚拟环境上安装最新版本的 Flask。我们需要在 venv 虚拟环境处于激活状态时执行 LIST 2.6 中的命令。

LIST 2.6 用 pip 命令安装 Flask

  1. $ pip install -U Flask

指定 -U 选项之后,可以用新版本替换已经安装的旧版本。2014 年 12 月时,最新的 Flask 版本为 0.10.1。至此 Flask 安装完毕,前置准备结束。

2.3 Web 应用的开发流程

那么,接下来我们应该按什么顺序开发 Web 应用呢?

现在唯一确定下来的事情就是要开发一个留言板应用,除此之外毫无计划。首先我们要确定这个应用的需求,再以需求为出发点考虑如何实现页面和功能,然后开始敲代码。等编码完成后,还要测试该应用是否能够正常运行。

整个流程总结起来是下面这样的。

① 确认需求(确认要开发什么应用)

② 根据需求明确成品必备的功能

③ 根据功能明确成品必备的页面

④ 页面设计

⑤ 实现功能

⑥ 将功能植入到页面中

⑦ 确认是否能正常运行

⑧ 完成

关于上述各流程,我们将在实际开发过程中详细了解。

2.4 明确要开发什么应用

这里,我们来了解一下这个应用的需求以及必备功能和页面。

2.4.1 留言板应用的需求

首先,我们来确定留言板应用是个什么样的应用,需要实现些什么(即需求)。

如果不事先明确需求或规格,很容易在编码过程中遇到“原本想做什么来着”“这个功能有必要做吗”之类的问题,导致项目一团乱。因此我们需要先确定这个留言板应用的需求,其具体内容如下。

① 在 Web 浏览器上显示一个包含“提交留言”表单的页面

② 可以在提交留言表单中输入名字和留言正文

③ 通过提交留言表单发送的名字和留言内容会被保存

④ 已保存的名字、留言、提交日期会显示在页面中

⑤ 整个应用由一个页面构成,页面上部为提交留言表单,下部显示已提交的内容

⑥ 提交的内容按新旧顺序由上到下排列

⑦ 可经由网络(互联网)使用本系统

⑧ 可同时在多台计算机上显示已提交的内容

2.4.2 明确必备的功能

确定好该应用的需求后,我们来思考一下该用哪些功能来满足这些需求。

这次我们要开发的是一个 Web 应用,并且导入了专门开发 Web 应用的框架,因此能够轻松实现需求①在 Web 浏览器上显示、⑦经由网络使用以及⑧同时在多台计算机上显示。

从该应用的需求来看,我们需要的功能如下表所示。

功能 说明 需求编号
提交留言功能 显示可输入名字和留言的表单,保存该表单发送的数据 ①、②、③
留言显示功能 取出提交留言功能保存的数据(优先提交日期较新的数据)并显示在页面上 ④、⑥
Web 应用 在 Web 浏览器上显示,并且可让多台计算机同时经由网络使用该应用 ①、⑦、⑧

需求⑤是与页面相关的问题,我们在下一小节学习页面的时候再处理。这里,我们再确认一遍已明确的功能,看看是否能满足需求。

2.4.3 明确必备的页面

接下来我们根据应用的功能分析所需的页面,结果如下。

功能 必备的页面 说明
提交留言功能 可以输入名字和留言的表单 可供输入名字与留言的栏、提交按钮
留言显示功能 显示有名字和留言的列表 在页面上列表显示已提交的名字和留言

需求中提到整个应用由一个页面构成(需求⑤),所以我们要将这两个页面元素糅合到同一个页面中。于是,必备的页面就成了下面这样。

必备的页面 页面元素
提交和显示留言的页面 可以输入名字和留言的表单、显示有名字和留言的列表

至此,我们明确了必备的页面。接下来可以开始设计页面了。

2.5 页面设计

需要的功能和页面都已经敲定,接下来,我们就动手设计页面吧。

至于为什么先实现页面而非功能,是因为当页面完成后,我们可以更好地把握成品应用的整体印象。在把功能植入系统之前,即页面设计阶段,我们需要先写好 HTML 文件和 CSS 文件。

2.5.1 确定成品页面的形式

在开始编写 HTML 代码之前,还需先按捺住自己跃跃欲试的心情,把我们希望呈现的页面明确下来。所以,现在要做的就是把必备页面转化成图。这一步称为页面设计。

建议各位先在纸上画出页面的草图。如果希望后期修改起来方便,或者希望页面草图干净漂亮,还可以考虑用绘图软件或页面设计方面的专业软件来画。

这次我们没有使用工具,而是直接画了一张页面草图(图 2.2)。

图像说明文字

图 2.2 留言板应用的页面草图

按照需求,上面是表单,下面显示提交的评论内容。另外,这张草图还为各个显示元素(表单和文章)添加了注释。这样一张草图不但能在植入功能时为我们提供参考,还能随时提醒我们仍缺少哪些功能,这对开发来说意义重大。

2.5.2 编写 HTML 和 CSS

下面我们开始编写 HTML 文件和 CSS 文件。为了查看其在 Web 浏览器中的效果,我们将在本地环境(主 OS)中进行编写。编写完成的文件可以通过 scp 命令发送到服务器,以便后续使用。

首先我们来参考草图编写 HTML 文件。现阶段不必太在意页面布局,这些外观上的东西都可以留在编写 CSS 文件的时候进行调整。

LIST 2.7 是我们编写的 HTML 文件的源码。

LIST 2.7 index.html

  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="utf-8">
  5. <title> 留言板</title>
  6. </head>
  7. <body>
  8. <h1> 留言板</h1>
  9. <form action="post" method="post">
  10. <p> 请留言<p>
  11. <table>
  12. <tr>
  13. <th> 名字</th>
  14. <td>
  15. <input type="text" size="20" name="name">
  16. </td>
  17. </tr>
  18. <tr>
  19. <th> 留言</th>
  20. <td>
  21. <textarea rows="5" cols="40" name="comment"></textarea>
  22. </td>
  23. </tr>
  24. </table>
  25. <p><button type="submit"> 提交</button></p>
  26. </form>
  27. <div>
  28. <h2> 留言记录</h2>
  29. <h3> 游客 的留言(2014/10/31 10:00:00):</h3>
  30. <p>
  31. 留言内容<br>
  32. 留言内容
  33. </p>
  34. <h3> 游客 的留言(2014/10/31 09:00:00):</h3>
  35. <p>
  36. 留言内容<br>
  37. 留言内容
  38. </p>
  39. </div>
  40. </body>
  41. </html>

用 Web 浏览器打开这个 HTML 文件,结果如图 2.3 所示。

图像说明文字

图 2.3 只有 HTML 文件的页面

这样,HTML 文件就写好了,接下来开始写 CSS 文件。此时需要调整字体大小、颜色、显示位置等外观(风格)。

LIST 2.8 是写好的 CSS 文件。

LIST 2.8 main.css

  1. body {
  2. margin: 0;
  3. padding: 0;
  4. color: #000E41;
  5. background-color: #004080;
  6. }
  7. h1 {
  8. padding: 0 1em;
  9. color: #FFFFFF;
  10. }
  11. form {
  12. padding: 0.5em 2em;
  13. background-color: #78B8F8;
  14. }
  15. .main {
  16. padding: 0;
  17. }
  18. .entries-area {
  19. padding: 0.5em 2em;
  20. background-color: #FFFFFF;
  21. }
  22. .entries-area p {
  23. padding: 0.5em 1em;
  24. background-color: #DBDBFF;
  25. }

为了让这个样式表生效,我们需要在 HTML 文件中添加 link 标签读取 CSS,并在相应位置指定 class。修改后的 HTML 文件如 LIST 2.9 所示。

LIST 2.9 index.html

  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="utf-8">
  5. <title> 留言板</title>
  6. <link rel="stylesheet" href="main.css" type="text/css"> <!-- 添加link 标签 -->
  7. </head>
  8. <body>
  9. <h1> 留言板</h1>
  10. <form action="post" method="post">
  11. <p> 请留言<p>
  12. <table>
  13. <tr>
  14. <th> 名字</th>
  15. <td>
  16. <input type="text" size="20" name="name">
  17. </td>
  18. </tr>
  19. <tr>
  20. <th> 留言</th>
  21. <td>
  22. <textarea rows="5" cols="40" name="comment"></textarea>
  23. </td>
  24. </tr>
  25. </table>
  26. <p><button type="submit"> 提交</button></p>
  27. </form>
  28. <div class="entries-area"> <!-- 在div 标签中添加class 属性 -->
  29. <h2> 留言记录</h2>
  30. <h3> 游客 的留言(2014/10/31 10:00:00):</h3>
  31. <p>
  32. 留言内容<br>
  33. 留言内容
  34. </p>
  35. <h3> 游客 的留言(2014/10/31 09:00:00):</h3>
  36. <p>
  37. 留言内容<br>
  38. 留言内容
  39. </p>
  40. </div>
  41. </body>
  42. </html>

我们用 Web 浏览器打开套用 CSS 后的 HTML 文件,如图 2.4 所示。

图像说明文字

图 2.4 套用 CSS 后的页面

这样我们就写好了显示页面用的 HTML 文件和 CSS 文件。现在用 scp 命令将已写好的文件发送到 VirtualBox 上的 Ubuntu 环境下。发送文件的命令如 LIST 2.10 所示。

LIST 2.10 用 scp 命令发送文件

  1. $ scp -P 2222 index.html main.css bpbook@127.0.0.1:

NOTE

SSH 连接的设置等问题请参考附录 B。

下一步我们要实现功能,并将其植入页面中。

2.6 实现功能

终于到了编写 Python 程序的阶段。我们准备让服务器端的程序来实现这个应用的必备功能。这里将优先实现比较重要的功能,或者能提高其他部分实现速度的功能。

保存和读取用户提交的数据是这个应用的核心部分,所以我们先从它下手。

2.6.1 保存留言数据

提交功能不但要能保存表单传来的名字和留言,还得能将提交日期及时间存储下来,供显示时使用。

这里我们用 Python 的标准模块 shelve 来存储数据。shelve 能够像 Python 字典对象一样操作数据,将对象持久化。也就是说,shelve 会将提交的数据转换成字典对象,以列表形式保存多个字典,然后将这些字典保存在 shelve 中。

下面来编写留言板的脚本文件。guestbook.py 实现了负责保存数据的 save_data 函数,具体代码如 LIST 2.11 所示。

LIST 2.11 guestbook.py

  1. # coding: utf-8
  2. import shelve
  3. DATA_FILE = 'guestbook.dat'
  4. def save_data(name, comment, create_at):
  5. """ 保存提交的数据
  6. """
  7. # 通过shelve 模块打开数据库文件
  8. database = shelve.open(DATA_FILE)
  9. # 如果数据库中没有greeting_list,就新建一个表
  10. if 'greeting_list' not in database:
  11. greeting_list = []
  12. else:
  13. # 从数据库获取数据
  14. greeting_list = database['greeting_list']
  15. # 将提交的数据添加到表头
  16. greeting_list.insert(0, {
  17. 'name': name,
  18. 'comment': comment,
  19. 'create_at': create_at,
  20. })
  21. # 更新数据库
  22. database['greeting_list'] = greeting_list
  23. # 关闭数据库文件
  24. database.close()

然后再来看看 save_data 函数的运行情况。我们通过终端在 guestbook.py 文件所在的目录下启动 Python shell,然后像 LIST 2.12 这样通过 Python shell 加载并执行 guestbook 模块的 save_data 函数。

LIST 2.12 save_data 函数的运行测试

  1. $ ls # 确认guestbook.py 位于当前目录下
  2. guestbook.py
  3. $ python # 启动Python shell
  4. >>> import datetime
  5. >>> from guestbook import save_data
  6. >>> save_data('test', 'test comment',
  7. ... datetime.datetime(2014, 10, 31, 10, 0, 0))
  8. >>>

这里我们将 datetime 模块的日期时间对象传递给传值参数 create_at,以此来保存提交的日期和时间。在这个阶段,我们只能看出它的运行是否报错,至于数据是不是真的被保存下来了,还要等取出数据的功能实现之后才能知道。

2.6.2 获取已保存的留言列表

实际上,我们在保存数据的时候就从 shelve 模块中取出过数据,现在只把这部分代码单独拿出来做成函数即可。

在 guestbook.py 中添加 load_data 函数(LIST 2.13)。

LIST 2.13 guestbook.py

  1. def load_data():
  2. """ 返回已提交的数据
  3. """
  4. # 通过shelve 模块打开数据库文件
  5. database = shelve.open(DATA_FILE)
  6. # 返回greeting_list。如果没有数据则返回空表
  7. greeting_list = database.get('greeting_list', [])
  8. database.close()
  9. return greeting_list

这里同样通过 Python shell 查看其运行情况。启动 Python shell,加载函数并运行(LIST 2.14)。

LIST 2.14 load_data 函数的运行测试

  1. >>> from guestbook import load_data
  2. >>> load_data()
  3. [{'comment': 'test comment', 'name': 'test', 'create_at': datetime.datetime (2014, 10, 31, 10, 0)}]

如果运行正常,那就表示我们能获取 save_data 函数保存下来的数据。

2.6.3 用模板引擎显示页面

从文件中取出数据之后,为了将其显示到页面上,我们要使用模板引擎。模板引擎可以将模板(程序的雏形)与要植入模板内的数据合并输出。Flask 标准支持 Jinja2 模板引擎。

接下来创建 templates 目录,然后将前面已经写好的 HTML 文件放到该目录下(LIST 2.15)。这样一来,我们就可以以它为模板生成 HTML 了。

LIST 2.15 放置模板

  1. $ mkdir templates
  2. $ mv index.html templates/

下面从程序端入手,用这个 HTML 文件(模板)来完成页面的显示。先添加代码,让 guestbook.py 调用 Flask,然后再添加用来显示首页的函数以及用来启动 Web 服务器的代码(LIST 2.16)。

LIST 2.16 guestbook.py

  1. # coding: utf-8
  2. import shelve
  3. from flask import Flask, request, render_template, redirect, escape, Markup
  4. application = Flask(__name__)
  5. DATA_FILE = 'guestbook.dat'
  6. def save_data(name, comment, create_at):
  7. """ 保存提交的数据
  8. """
  9. # 省略
  10. def load_data():
  11. """ 返回已提交的数据
  12. """
  13. # 省略
  14. @application.route('/')
  15. def index():
  16. """ 首页
  17. 使用模板显示页面
  18. """
  19. return render_template('index.html')
  20. if __name__ == '__main__':
  21. # 在IP 地址127.0.0.1 的8000 端口运行应用程序
  22. application.run('127.0.0.1', 8000, debug=True)

我们将 Flask 类的实例赋给变量 application,传值参数指定为 __name__ 变量的模块名。方法 route 是一个装饰器,负责注册针对特定 URL 执行的函数。这里我们让主页的 URL 对应执行 index 函数。render_template 函数负责将指定文件用作模板,再通过模板引擎进行输出。

Flask 类的 run 方法用于启动 Web 服务器并执行应用程序,传值参数用来指定要绑定的 IP 地址及端口。另外,将 debug 选项指定为 True 时,一旦应用程序出错,Web 浏览器端就会启动可用的调试程序。

接下来用 python 命令运行 guestbook.py(LIST 2.17)。

LIST 2.17 运行已开发完毕的 Web 应用

  1. $ python guestbook.py
  2. Running on http://127.0.0.1:8000/
  3. Restarting with reloader

运行 guestbook.py 之后,在该环境的(这里是 VirtualBox 上的 Ubuntu)127.0.0.1 地址的 8000 端口等待请求的应用服务器(Web 服务器)就会启动。我们可以在终端同时按下 Ctrl 键和 C 键,关闭这个服务器。

现在我们需要设置好 8000 端口的端口转发,然后在 Web 浏览器中打开 http://127.0.0.1:8000/。如何,看到页面没有?

我们会发现 CSS 文件并没有生效,所以需要重新调整一下 CSS 文件的位置,让它能够被读取。Flask 会公开 static 目录下存放的静态文件。接下来我们创建一个 static 目录,把 main.css 文件放进去(LIST 2.18)。

LIST 2.18 放置静态文件

  1. $ mkdir static
  2. $ mv main.css static/

另外,还要将 templates/index.html 中的 CSS 文件引用位置改为 staticmain.css(LIST 2.19、LIST 2.20)。

LIST 2.19 templates/index.html(更改前)

  1. <link rel="stylesheet" href="main.css" type="text/css">

LIST 2.20 templates/index.html(更改后)

  1. <link rel="stylesheet" href="staticmain.css" type="text/css">

现在再用 Web 浏览器打开该页面,就会发现 CSS 文件这时已经生效了。

接下来,我们把从数据库中取出的内容显示在页面上。修改 guestbook.py 文件,让 index 函数调用 load_data 函数,同时使模板能够使用 load_data 函数取出来的数据(LIST 2.21)。

LIST 2.21 guestbook.py

  1. @application.route('/')
  2. def index():
  3. """ 首页
  4. 使用模板显示页面
  5. """
  6. # 读取已提交的数据
  7. greeting_list = load_data()
  8. return render_template('index.html', greeting_list=greeting_list)

render_template 函数可以将关键字传值参数所指定的值用作模板变量。比如本例就使用了名为 greeting_list 的模板变量。

此外,我们还要修改模板 templates/index.html,使其能够使用模板变量进行显示。现在对 HTML 中的显示留言部分作如下修改(LIST 2.22)。

LIST 2.22 templates/index.html

  1. <div class="entries-area">
  2. <h2> 留言记录</h2>
  3. {% for greeting in greeting_list %}
  4. <h3>{{ greeting.name }}
  5. 的留言({{ greeting.create_at }}):</h3>
  6. <p>{{ greeting.comment }}</p>
  7. {% endfor %}
  8. </div>

模板内可以使用一种特殊的描述方式,即模板语言。Jinja2 的模板可以通过 {%…%} 的形式使用 if 或 for 等控制语句。植入有模板变量的部分用 {{…}} 的形式描述。在上述模板中,程序会从 greeting_list 中逐一取值并赋给模板变量 greeting,然后使用从 for 到 endfor 之间的模板变量进行循环输出。

这样一来,应用就能将 save_data 函数保存的数据显示在页面上了。

2.6.4 准备评论接收方的 URL

下一步我们用 save_data 函数保存表单提交来的数据。由于模板文件中表单的 action 值为 /post,所以我们就做出这个 URL。

这里,我们将 post 函数添加到 guestbook.py 的 if __main__ … 前面(LIST 2.23)。

LIST 2.23 guestbook.py

  1. @application.route('/post', methods=['POST'])
  2. def post():
  3. """ 用于提交评论的URL
  4. """
  5. # 获取已提交的数据
  6. name = request.form.get('name') # 名字
  7. comment = request.form.get('comment') # 留言
  8. create_at = datetime.now() # 投稿时间(当前时间)
  9. # 保存数据
  10. save_data(name, comment, create_at)
  11. # 保存后重定向到首页
  12. return redirect('/')

在 Flask 中,可以用 request.form 引用表单发来的数据。此外,由于保存数据后需要重新显示首页,所以我们要返回 redirect 函数的结果并进行重定向。

post 函数使用了 datetime 模块,所以需要在文件开头添加如 LIST 2.24 所示代码,以导入该模块。

LIST 2.24 导入 datetime 模块(guestbook.py)

  1. # coding: utf-8
  2. import shelve
  3. from datetime import datetime # 添加此行

至此,保存数据的功能也实现了。应用的运行部分基本完工。

2.6.5 调整模板的输出

程序运行所需的功能现在已经基本上都实现了,但这里至少还有两点需要注意。

  • 表单提交多行留言时,无法正常显示留言

  • 显示的时间精确到了毫秒

要想解决这两个问题需创建一个模板过滤器。模板过滤器会对模板变量的值加以转换并输出。接下来,我们在 guestbook.py 的 if __main__ … 前添加如 LIST 2.25 所示代码,将模板过滤器导入模板。

LIST 2.25 guestbook.py

  1. @application.template_filter('nl2br')
  2. def nl2br_filter(s):
  3. """ 将换行符置换为br 标签的模板过滤器
  4. """
  5. return escape(s).replace('\n', Markup('<br>'))
  6. @application.template_filter('datetime_fmt')
  7. def datetime_fmt_filter(dt):
  8. """ 使datetime 对象更容易分辨的模板过滤器
  9. """
  10. return dt.strftime('%Y/%m/%d %H:%M:%S')

Flask 类的 template_filter 方法是一个装饰器,它负责将函数注册为指定名称的模板过滤器。程序运行时,指定了模板过滤器的模板变量会被作为传值参数传递给相应函数,而函数的返回值则为最后输出的值。在上述源码中,我们注册了 nl2br 和 datetime_fmt 两个模板过滤器。

接下来,我们修改一下 templates/index.html 文件,让模板能够使用这两个模板过滤器(LIST 2.26)。

LIST 2.26 templates/index.html

  1. <div class="entries-area">
  2. <h2> 留言记录</h2>
  3. {% for greeting in greeting_list %}
  4. <h3>{{ greeting.name }}
  5. 的留言({{ greeting.create_at|datetime_fmt }}):</h3>
  6. <p>{{ greeting.comment|nl2br }}</p>
  7. {% endfor %}
  8. </div>

在模板内指定模板过滤器的方法很简单,只需要在模板变量的名称后面加管道符“|”再加模板过滤器名即可。在本次的模板中,我们给 greeting.create_at 指定了 datetime_fmt 过滤器,给 greeting.comment 指定了 nl2br 过滤器。

至此,服务器端的功能、功能与页面的对接已经全部完工。从 Web 浏览器看到的效果如图 2.5 所示。显示留言部分 2 显示了我们已提交的内容。

2留言记录部分的 tokibito 为本章撰写者冈野真也先生的 Twitter 用户名。——编者注

{%}

图 2.5 植入功能后的页面

整个应用的开发过程到这里就结束了,接下来就是确认该应用的运行是否正常,以及看一看成品是否能满足我们当初定下的需求。

2.7 查看运行情况

虽然程序的编写已经结束,但我们还需要确认应用程序能不能按预想的样子运行。下面就来逐条确认刚刚开发完成的应用是否满足需求,以及运行上是否存在问题。到了这一步,可能会有人觉得“开发的时候就已经逐条确认过了,肯定不会有问题的”。这种心情可以理解。但要知道,我们在编码后期阶段所作的修改可能会导致之前的功能出现 Bug,而且也难保开发过程中不会遗漏某些需求。因此要想制作一个品质上乘的应用程序,这项确认工作必不可少。

好了,现在让我们回顾一下这个留言板应用的需求。

① 在 Web 浏览器上显示一个包含“提交留言”表单的页面

② 可以在提交留言表单中输入名字和留言正文

③ 通过提交留言表单发送的名字和留言内容会被保存

④ 已保存的名字、留言、提交日期会显示在页面中

⑤ 整个应用由一个页面构成,页面上部为提交留言表单,下部显示已提交的内容

⑥ 提交的内容按新旧顺序由上到下排列

⑦ 可经由网络(互联网)使用本系统

⑧ 可同时在多台计算机上显示已提交的内容

现在运行开发服务器,查看成品是否能满足这 8 项需求。

只要通过 Web 浏览器访问 http://127.0.0.1:8000/ 即可查看需求①是否得到了满足。与此同时,由于虚拟机和本地计算机之间经由网络连接,所以⑦和⑧也没有问题。至于②,只要我们能在页面中的表单里填写名字和留言内容,那就是过关了。接下来输入内容并点击提交按钮,随后页面被刷新,刚刚提交的内容显示在了页面上。于是⑤也搞定了。然后重启开发服务器,再次通过 Web 浏览器读取页面,如果之前提交的内容能够正常显示,就表示③和④也 OK。最后我们再进行一次输入和提交,只要后来提交的内容显示在最上方,那么需求⑥也就满足了。

另外,这次我们开发的应用没有对使用者输入的字符串做任何限制,而且输入的内容会直接以植入 HTML 的形式显示出来。因此,接下来需要确认是否存在跨站脚本攻击漏洞。

NOTE

跨站脚本攻击(Cross Site Scripting,XSS3)是一种常见的漏洞,用户能在某些以植入 HTML 的形式显示输入内容的应用中,故意植入一些攻击型的脚本(比如 HTML 标签或 JavaScript 等)。

XSS 漏洞可被用在会话劫持、钓鱼等恶意行为上。

3Cross Site Scripting 的缩写为 XSS,这是为了和层叠样式表(Cascading Style Sheet,CSS)有所区分。——编者注

要验证应用中是否含有 XSS 漏洞,只需要像 LIST 2.27 这样,在留言输入栏中键入带有 JavaScript 代码的 HTML 标签,然后点击提交即可。

LIST 2.27 验证跨站脚本攻击时需要输入的内容

  1. <script>alert('NG')</script>

显示提交内容的区域内是否显示出了我们输入的字符串呢?如果该应用含有 XSS 漏洞,那么 NG 字样将显示在浏览器的警告中。

我们在本次开发中使用的是 Jinja2 模板引擎,它在翻译字符串时会自动忽略 HTML 标签的“< 和 >”符号。因此,我们的应用能够成功避免 XSS 漏洞。

建议各位在最后查看运行情况时也检测一下其他可能造成安全问题的漏洞。

NOTE

与 XSS 同样恶名昭彰的漏洞还有跨站请求伪造(Cross Site Request Forgery,CSRF)。用户通过与目标应用程序无关的外部输入表单(这些输入表单通常用于攻击)发送数据,一旦目标应用程序处理了这些数据,就会引发使用者意料之外的操作。

CSRF 漏洞可被用来触发使用者意料之外的操作(比如在线购买商品、泄露个人信息等)。

本章中开发的应用并没有对 CSRF 进行防范。由于 XSS 和 CSRF 这两种攻击手法可以相互组合出新的攻击手法,所以建议各位务必做好防范工作。

如果上述过程全部顺利通过,我们的应用开发就可以宣告完工了。各位辛苦了。

最终的文件结构如下表所示。

文件路径 说明
guestbook.py 服务器程序
guestbook.dat 提交数据文件
static/main.css CSS 文件
templates/index.html 输出 HTML 的模板,用于显示提交 / 留言列表页面

Web 应用开发的第一步是确定自己要开发什么东西。要是对自己要做的东西没有概念,开发就不可能进行下去。另外,如果必备的页面和功能不够明确,那么编写代码的过程将是相当痛苦的。要想让开发如行云流水般流畅,那就必须将这些不确定的因素统统明确下来,确定一个开发流程。

在本章中,我们强调了 Web 应用是一个发送动态内容的程序,并且用户可以经由 Web 浏览器使用 Web 应用,同时还学习了 Web 浏览器 / 服务器 / 应用之间的基本通信机制。另外,我们还以留言板应用为例,从确定需求到最终完成,边学边练地一起了解了整个应用开发流程。本章学习的开发流程适用于多种应用的开发。