Flask Web开发实战:入门、进阶与原理解析
上QQ阅读APP看书,第一时间看更新

第1章 初识Flask

这一切开始于2010年4月1日,Armin Ronacher在网上发布了一篇关于“下一代Python微框架”的介绍文章,文章里称这个Denied框架不依赖Python标准库,只需要复制一份deny. py放到你的项目文件夹就可以开始编程。伴随着一本正经的介绍、名人推荐语、示例代码和演示视频,这个“虚假”的项目让不少人都信以为真。5天后,Flask(http://flask.pocoo.org/)就从这么一个愚人节玩笑诞生了。

Flask是使用Python编写的Web微框架。Web框架可以让我们不用关心底层的请求响应处理,更方便高效地编写Web程序。因为Flask核心简单且易于扩展,所以被称作微框架(micro framework)。Flask有两个主要依赖,一个是WSGI(Web Server Gateway Interface,Web服务器网关接口)工具集——Werkzeug(http://werkzeug.pocoo.org/),另一个是Jinja2模板引擎(http://jinja.pocoo.org/)。Flask只保留了Web开发的核心功能,其他的功能都由外部扩展来实现,比如数据库集成、表单认证、文件上传等。如果没有合适的扩展,你甚至可以自己动手开发。Flask不会替你做决定,也不会限制你的选择。总之,Flask可以变成任何你想要的东西,一切都由你做主。

附注

Flask(瓶子,烧瓶)的命名据说是对另一个Python Web框架——Bottle的双关语/调侃,即另一种容器(另一个Python Web框架)。Werkzeug是德语单词“工具(tool)”,而Jinja指日本神社,因为神社(庙)的英文temple与template(模板)相近而得名。

附注

WSGI(Web Server Gateway Interface)是Python中用来规定Web服务器如何与Python Web程序进行沟通的标准,在本书的第三部分将进行详细介绍。

本章将会对Flask的主要基础概念进行一些介绍,并通过一个最简单的Flask程序来了解一些核心概念。如果你对某些概念感到疑惑,不用担心,我们会在后面深入学习这些内容。

在本书的第一部分,每一章都有一个对应的示例程序,章节中的大部分示例代码均可以在示例程序中找到。首先,请打开命令行窗口,切换到合适的目录,然后使用下面的命令把本书的示例程序仓库复制到本地,并切换进项目根目录:

        $ git clone https://github.com/greyli/helloflask.git
        $ cd helloflask

提示

如果你在Hello Flask的Git Hub页面(https://github.com/greyli/helloflask)单击了Fork按钮,那么可以使用你自己的Git Hub用户名来替换掉上面的greyli,这将复制一份派生仓库,你可以自由地修改和提交代码。

本章新涉及的Python包如下所示:

❑ Flask(1.0.2)

主页:http://flask.pocoo.org/

源码:http://github.com/pallets/flask

文档:http://flask.pocoo.org/docs/

❑ pip(10.0.1)

主页:https://github.com/pypa/pip

文档:https://pip.pypa.io

❑ Pipenv(v2018.05.18)

主页:https://github.com/pypa/pipenv

文档:http://pipenv.readthedocs.io/

❑ Virtualenv(15.1.0)

主页:https://github.com/pypa/virtualenv

文档:https://virtualenv.pypa.io

❑ Pipfile(0.0.2)

主页:https://github.com/pypa/pipfile

❑ python-dotenv(0.8.2)

主页:https://github.com/theskumar/python-dotenv

❑ Watchdog(0.8.3)

主页:https://github.com/gorakhargosh/watchdog

文档:https://pythonhosted.org/watchdog/

1.1 搭建开发环境

在前言中,我们已经简单介绍了阅读本书所需要的基础知识,现在我们开始正式的搭建开发环境。

1.1.1 Pipenv工作流

Pipenv是基于pip的Python包管理工具,它和pip的用法非常相似,可以看作pip的加强版,它的出现解决了旧的pip+virtualenv+requirements.txt的工作方式的弊端。具体来说,它是pip、Pipfile和Virtualenv的结合体,它让包安装、包依赖管理和虚拟环境管理更加方便,使用它可以实现高效的Python项目开发工作流。如果你还不熟悉这些工具,不用担心,我们会在下面逐一进行介绍。

1.安装pip和Pipenv

pip是用来安装Python包的工具。如果你使用Python 2.7.9及以上版本或Python 3.4及以上版本,那么pip已经安装好了。可以使用下面的命令检查pip是否已经安装:

        $ pip --version

如果报错,那么你需要自己安装pip。最简单的方式是下载并使用Python执行get-pip.py文件(https://bootstrap.pypa.io/get-pip.py)。

下面这条命令你经常在各种文档中见到:

        $ pip install <某个包的名称>

这会从Py PI(Python Package Index,Python包索引)上下载并安装指定的包。

附注

Py PI(https://pypi.org)是一个Python包的在线仓库,截至2018年5月,共有13万多个包存储在这里。后面我们会学习如何编写自己的Flask扩展,并把它上传到Py PI上。到时你就可以使用上面这条命令安装自己编写的包。

现在使用pip安装Pipenv:

        $ pip install pipenv

在Linux或mac OS系统中使用sudo以全局安装:

        $ sudo pip install pipenv

附注

如果你不想全局安装,可以添加--user选项执行用户安装(即pip install--user pipenv),并手动将用户基础二进制目录添加到PATH环境变量中,具体可参考https://docs.pipenv.org/install/#installing-pipenv

提示

Py PI中的包名称不区分大小写。出于方便的考虑,后面的安装命令都将使用小写名称。

可以使用下面的命令检查Pipenv是否已经安装:

        $ pipenv --version
        pipenv, version 11.10.4
2.创建虚拟环境

在Python中,虚拟环境(virtual enviroment)就是隔离的Python解释器环境。通过创建虚拟环境,你可以拥有一个独立的Python解释器环境。这样做的好处是可以为每一个项目创建独立的Python解释器环境,因为不同的项目常常会依赖不同版本的库或Python版本。使用虚拟环境可以保持全局Python解释器环境的干净,避免包和版本的混乱,并且可以方便地区分和记录每个项目的依赖,以便在新环境下复现依赖环境。

虚拟环境通常使用Virtualenv来创建,但是为了更方便地管理虚拟环境和依赖包,我们将会使用集成了Virtualenv的Pipenv。首先确保我们当前工作目录在示例程序项目的根目录,即helloflask文件夹中,然后使用pipenv install命令为当前的项目创建虚拟环境:

        $ pipenv install
        Creating a virtualenv for this project…
        ...
        Virtualenv location: /path/to/virtualenv/helloflask-5Pa0Zf Zw
        ...

这会为当前项目创建一个文件夹,其中包含隔离的Python解释器环境,并且安装pip、wheel、setuptools等基本的包。因为示例程序仓库里包含Pipfile文件,所以这个文件中列出的依赖包也会一并被安装,下面会具体介绍。

附注

默认情况下,Pipenv会统一管理所有虚拟环境。在Windows系统中,虚拟环境文件夹会在C:\Users\Administrator\.virtualenvs\目录下创建,而Linux或mac OS会在~/.local/share/virtualenvs/目录下创建。如果你想在项目目录内创建虚拟环境文件夹,可以设置环境变量PIPENV_VENV_IN_PROJECT,这时名为.venv的虚拟环境文件夹将在项目根目录被创建。

虚拟环境文件夹的目录名称的形式为“当前项目目录名+一串随机字符”,比如helloflask-5Pa0Zf Zw。

提示

你可以通过--three和--two选项来声明虚拟环境中使用的Python版本(分别对应Python3和Python2),或是使用--python选项指定具体的版本号。同时要确保对应版本的Python已经安装在电脑中。

在单独使用Virtualenv时,我们通常会显式地激活虚拟环境。在Pipenv中,可以使用pipenv shell命令显式地激活虚拟环境:

        $ pipenv shell
        Loading .env environment variables…
        Launching subshell in virtual environment. Type 'exit' to return.

提示

当执行pipenv shell或pipenv run命令时,Pipenv会自动从项目目录下的.env文件中加载环境变量。

Pipenv会启动一个激活虚拟环境的子shell,现在你会发现命令行提示符前添加了虚拟环境名“(虚拟环境名称)$”,比如:

        (helloflask-5Pa0Zf Zw) $

这说明我们已经成功激活了虚拟环境,现在你的所有命令都会在虚拟环境中执行。当你需要退出虚拟环境时,使用exit命令。

注意

在Windows系统中使用pipenv shell激活虚拟环境时,虽然激活成功,但是命令行提示符前不会显示虚拟环境名称。

除了显式地激活虚拟环境,Pipenv还提供了一个pipenv run命令,这个命令允许你不显式激活虚拟环境即可在当前项目的虚拟环境中执行命令,比如:

        $ pipenv run python hello.py

这会使用虚拟环境中的Python解释器,而不是全局的Python解释器。事实上,和显式激活/关闭虚拟环境的传统方式相比,pipenv run是更推荐的做法,因为这个命令可以让你在执行操作时不用关心自己是否激活了虚拟环境。当然,你可以自由选择你偏爱的用法。

注意

为了方便书写,本书后面涉及的诸多命令会直接写出,省略前面的虚拟环境名称。在实际执行时,你需要使用pipenv shell激活虚拟环境后执行命令,或是在命令前加入pipenv run,后面不再提示。

3.管理依赖

一个程序通常会使用很多的Python包,即依赖(dependency)。而程序不仅仅会在一台电脑上运行,程序部署上线时需要安装到远程服务器上,而你也许会把它分享给朋友。如果你打算开源的话,就可能会有更多的人需要在他们的电脑上运行。为了能顺利运行程序,他们不得不记下所有依赖包,然后使用pip或Pipenv安装,这些重复无用的工作当然应该避免。在以前我们通常使用pip搭配一个requirements.txt文件来记录依赖。但requirements.txt需要手动维护,在使用上不够灵活。Pipfile的出现就是为了替代难于管理的requirements.txt。

在创建虚拟环境时,如果项目根目录下没有Pipfile文件,pipenv install命令还会在项目文件夹根目录下创建Pipfile和Pipfile.lock文件,前者用来记录项目依赖包列表,而后者记录了固定版本的详细依赖包列表。当我们使用Pipenv安装/删除/更新依赖包时,Pipfile以及Pipfile. lock会自动更新。

附注

你可以使用pipenv graph命令查看当前环境下的依赖情况,或是在虚拟环境中使用pip list命令查看依赖列表。

当需要在一个新的环境运行程序时,只需要执行pipenv install命令。Pipenv就会创建一个新的虚拟环境,然后自动从Pipfile中读取依赖并安装到新创建的虚拟环境中。

提示

在本书撰写时,Pipfile项目还处于活跃的开发阶段,有很多东西还没有固定,所以这里不会过多介绍,具体请访问Pipfile主页了解。

1.1.2 安装Flask

下面使用pipenv install命令在我们刚刚创建的虚拟环境里安装Flask:

        $ pipenv install flask
        Installing flask...
        ...
        Successfully  installed  Jinja2-2.10  Markup Safe-1.0  Werkzeug-0.14.1  click-6.7
            flask-1.0.2 itsdangerous-0.24

提示

Pipenv会自动帮我们管理虚拟环境,所以在执行pipenv install安装Python包时,无论是否激活虚拟环境,包都会安装到虚拟环境中。后面我们都将使用Pipenv安装包,这相当于在激活虚拟环境的情况下使用pip安装包。只有需要在全局环境下安装/更新/删除包,我们才会使用pip。

从上面成功安装的输出内容可以看出,除了Flask包外,同时被安装的还有5个依赖包,它们的主要介绍如表1-1所示。

表1-1 Flask的依赖包

在大部分情况下,为了方便表述,我会直接称Flask使用这些包提供的功能为Flask提供的功能,必要时则会具体说明。这里仅仅是打个照面,后面我们会慢慢熟悉这些包。

附注

包括Flask在内,Flask的5个依赖包都由Pocoo团队(http://www.pocoo.org/)开发,主要作者均为Armin Ronacher(http://lucumr.pocoo.org/),这些项目均隶属于Pallets项目(https://www.palletsprojects.com/)。

本书使用了最新版本的Flask(1.0.2),如果你还在使用旧版本,请使用下面的命令进行更新:

        $ pipenv update flask

另外,本书涉及的所有Python包都将使用当前发布的最新版本,在每一章的开始我们都会列出新涉及的Python包的版本及Git Hub主页。如果你使用旧版本,请使用pipenv update命令更新版本。

提示

如果你手动使用pip和virtualenv管理包和虚拟环境,可以使用--upgrade或-U选项(简写时U为大写)来更新包版本:pip install-U <包名称>

1.1.3 集成开发环境

如果你还没有顺手的文本编辑器,那么可以尝试一下IDE(Integrated Development Enviroment,集成开发环境)。对于新手来说,IDE的强大和完善会帮助你高效开发Flask程序,等到你熟悉了整个开发流程,可以换用更加轻量的编辑器以避免过度依赖IDE。下面我们将介绍使用Py Charm开发Flask程序的主要准备步骤。

步骤1 下载并安装Py Charm

打开Py Charm的下载页面(http://jetbrains.com/pycharm/download/),单击你使用的操作系统选项卡,然后单击下载按钮。你可以选择试用专业版(Professional Edition),或是选择免费的社区版(Community Edition),如图1-1所示。

图1-1 下载Py Charm

附注

专业版有一个月的免费试用时间。如果你是学生,可以申请专业版的免费授权(https://www.jetbrains.com/student/)。专业版提供了更多针对Flask开发的功能,比如创建Flask项目模板,Jinja2语法高亮,与Flask命令行功能集成等。

Py Charm的安装过程比较简单,这里不再详细说明,具体可以参考https://www.jetbrains.com/help/pycharm/requirements-installation-and-launching.html

步骤2 创建项目

安装成功后,初始界面提供了多种方式创建新项目。这里可以单击“Open”,选择我们的helloflask文件夹。打开项目后的界面如图1-2所示,左边是项目目录树,右边是代码编辑区域。单击左下角的方形图标可以隐藏和显示工具栏,显示工具栏后,可以看到常用的Python交互控制台(Python Console)和终端(Terminal,即命令行工具)。

图1-2 Py Charm主界面

步骤3 设置Python解释器

因为Py Charm目前还没有添加Pipenv集成支持(最新进展可访问https://youtrack.jetbrains.com/issue/PY-26492查看),我们需要手动使用pipenv命令安装依赖,同时还需要为项目设置正确的Python解释器。单击菜单栏中的File→Settings打开设置,然后单击Project: helloflask-Project Interpreter选项打开项目Python解释器设置窗口,如图1-3所示。

图1-3 设置项目Python解释器

单击选择字段右侧的设置图标,然后单击“Add Local Python Interpreter”,在弹出的窗口选择Virtualenv Enviroment→Existing enviroment,在下拉框或是自定义窗口找到我们之前创建的虚拟环境中的Python解释器路径,如图1-4所示。

图1-4 选择虚拟环境中的Python解释器

使用pipenv--venv命令可以查看项目对应的虚拟环境路径。Linux或mac OS系统下的路径类似~/.local/share/virtualenvs/helloflask-k SN7ec1K/bin/python或~/.virtualenvs/helloflask-k SN7ec1K/bin/python,Windows系统的路径类似C:\Users\Administrator\.virtualenvs\helloflask-5Pa0Zf Zw\Scripts\python.exe。

正确设置以后,重新创建一个Terminal会话,你会发现命令行提示符前出现了虚拟环境名称,说明虚拟环境已经激活。以后每次打开项目,Py Charm都会自动帮你激活虚拟环境,并且把工作目录定位到项目根目录。具体行为你也可以在Settings→Tools→Terminal中设置。

附注

你可以通过Py Charm提供的入门教程(https://www.jetbrains.com/pycharm/documentation/)来了解Py Charm的更多用法。

最后,我们的Web程序需要在Web浏览器中访问,所以你还需要安装一个Web浏览器,推荐使用Firefox(https://www.mozilla.org/firefox/)或Chrome(https://www.google.com/chrome/)。现在你已经做好了一切准备,Flask之旅正式启程了!下面我们会通过一个最小的Flask程序来了解Flask的基本运作方式。

1.2 Hello, Flask!

本书的第一部分每一章对应一个示例程序,分别存储在demos目录下的不同文件夹中。本章的示例程序在helloflask/demos/hello目录下,使用下面的命令切换到该目录:

        $ cd demos/hello

在hello目录下的app.py脚本中包含一个最小的Flask程序,如代码清单1-1所示。

代码清单1-1 hello/app.py:最小的Flask程序

        from flask import Flask
        app = Flask(__name__)
        @app.route('/')
        def index():
            return '<h1>Hello Flask!</h1>'

你也许已经猜到它能做什么了,下面让我们先来一步步分解这个程序。

提示

对于简单的程序来说,程序的主模块一般命令为app.py。你也可以使用其他名称,比如hello.py,但是要避免使用flask.py,因为这和Flask本身冲突。

1.2.1 创建程序实例

我们安装Flask时,它会在Python解释器中创建一个flask包,我们可以通过flask包的构造文件导入所有开放的类和函数。我们先从flask包导入Flask类,这个类表示一个Flask程序。实例化这个类,就得到我们的程序实例app:

        from flask import Flask
        app = Flask(__name__)

传入Flask类构造方法的第一个参数是模块或包的名称,我们应该使用特殊变量__name__。Python会根据所处的模块来赋予__name__变量相应的值,对于我们的程序来说(app.py),这个值为app。除此之外,这也会帮助Flask在相应的文件夹里找到需要的资源,比如模板和静态文件。

提示

Flask类是Flask的核心类,它提供了很多与程序相关的属性和方法。在后面,我们经常会直接在程序实例app上调用这些属性和方法来实现相关功能。在第一次提及Flask类中的某个方法或属性时,我们会直接以实例方法/属性的形式写出,比如存储程序名称的属性为app.name。

1.2.2 注册路由

在一个Web应用里,客户端和服务器上的Flask程序的交互可以简单概括为以下几步:

1)用户在浏览器输入URL访问某个资源。

2)Flask接收用户请求并分析请求的URL。

3)为这个URL找到对应的处理函数。

4)执行函数并生成响应,返回给浏览器。

5)浏览器接收并解析响应,将信息显示在页面中。

在上面这些步骤中,大部分都由Flask完成,我们要做的只是建立处理请求的函数,并为其定义对应的URL规则。只需为函数附加app.route()装饰器,并传入URL规则作为参数,我们就可以让URL与函数建立关联。这个过程我们称为注册路由(route),路由负责管理URL和函数之间的映射,而这个函数则被称为视图函数(view function)。

附注

路由的含义可以从字面意义理解,作为动词时,它的含义是“按某路线发送”,即调用与请求URL对应的视图函数。

在这个程序里,app.route()装饰器把根地址/和index()函数绑定起来,当用户访问这个URL时就会触发index()函数。这个视图函数可以像其他普通函数一样执行任意操作,比如从数据库中获取信息,获取请求信息,对用户输入的数据进行计算和处理等。最后,视图函数返回的值将作为响应的主体,一般来说,响应的主体就是呈现在浏览器窗口的HTML页面。在最小程序中,视图函数index()返回一行问候:

        @app.route('/')
        def index():
            return '<h1>Hello, World!</h1>'

虽然这个程序相当简单,但它却是大部分Flask程序的基本模式。在复杂的程序中,我们会有许多个视图函数分别处理不同URL的请求,在视图函数中会完成更多的工作,并且返回包含各种链接、表单、图片的HTML文件,而不仅仅是一行字符串。返回的页面中的链接又会指向其他URL,被单击后触发对应的视图函数,获得不同的返回值,从而显示不同的页面,这就是我们浏览网页时的体验。

提示

route()装饰器的第一个参数是URL规则,用字符串表示,必须以斜杠(/)开始。这里的URL是相对URL(又称为内部URL),即不包含域名的URL。以域名www.helloflask.com为例,“/”对应的是根地址(即www.helloflask.com),如果把URL规则改为“/hello”,则实际的绝对地址(外部地址)是www.helloflask.com/hello

假如这个程序部署在域名为www.helloflask.com的服务器上,当启动服务器后,只要你在浏览器里访问www.helloflask.com,就会看到浏览器上显示一行“Hello,Flask!”问候。

附注

URL(Uniform Resource Lacator,统一资源定位符)正是我们使用浏览器访问网页时输入的网址,比如http://helloflask.com/。简单来说,URL就是指向网络中某个资源的地址。

1.为视图绑定多个URL

一个视图函数可以绑定多个URL,比如下面的代码把/hi和/hello都绑定到say_hello()函数上,这就会为say_hello视图注册两个路由,用户访问这两个URL均会触发say_hello()函数,获得相同的响应,如代码清单1-2所示。

代码清单1-2 hello/app.py:绑定多个URL到同一视图函数

        @app.route('/hi')
        @app.route('/hello')
        def say_hello():
            return '<h1>Hello, Flask!</h1>'
2.动态URL

我们不仅可以为视图函数绑定多个URL,还可以在URL规则中添加变量部分,使用“<变量名>”的形式表示。Flask处理请求时会把变量传入视图函数,所以我们可以添加参数获取这个变量值。代码清单1-3中的视图函数greet(),它的URL规则包含一个name变量。

代码清单1-3 hello/app.py:添加URL变量

        @app.route('/greet/<name>')
        def greet(name):
            return '<h1>Hello, %s!</h1>' % name

因为URL中可以包含变量,所以我们将传入app.route()的字符串称为URL规则,而不是URL。Flask会解析请求并把请求的URL与视图函数的URL规则进行匹配。比如,这个greet视图的URL规则为/greet/<name>,那么类似/greet/foo、/greet/bar的请求都会触发这个视图函数。

附注

顺便说一句,虽然示例中的URL规则和视图函数名称都包含相同的部分(greet),但这并不是必须的,你可以自由修改URL规则和视图函数名称。

这个视图返回的响应会随着请求URL中的name变量而变化。假设程序运行在http://helloflask.com上,当我们在浏览器里访问http://helloflask.com/hello/Grey时,可以看到浏览器上显示“Hello,Grey!”。

当URL规则中包含变量时,如果用户访问的URL中没有添加变量,比如/greet,那么Flask在匹配失败后会返回一个404错误响应。一个很常见的行为是在app.route()装饰器里使用defaults参数设置URL变量的默认值,这个参数接收字典作为输入,存储URL变量和默认值的映射。在下面的代码中,我们为greet视图新添加了一个app.route()装饰器,为/greet设置了默认的name值:

        @app.route('/greet', defaults={'name': 'Programmer'})
        @app.route('/greet/<name>')
        def greet(name):
            return '<h1>Hello, %s!</h1>' % name

这时如果用户访问/greet,那么变量name会使用默认值Programmer,视图函数返回<h1>Hello,Programmer! </h1>。上面的用法实际效果等同于:

        @app.route('/greet')
        @app.route('/greet/<name>')
        def greet(name='Programmer'):
            return '<h1>Hello, %s!</h1>' % name

1.3 启动开发服务器

Flask内置了一个简单的开发服务器(由依赖包Werkzeug提供),足够在开发和测试阶段使用。

注意

在生产环境需要使用性能够好的生产服务器,以提升安全和性能,具体在本书第三部分会进行介绍。

1.3.1 Run,Flask,Run!

Flask通过依赖包Click内置了一个CLI(Command Line Interface,命令行交互界面)系统。当我们安装Flask后,会自动添加一个flask命令脚本,我们可以通过flask命令执行内置命令、扩展提供的命令或是我们自己定义的命令。其中,flask run命令用来启动内置的开发服务器:

        $ flask run
          * Environment: production
            WARNING: Do not use the development server in a production environment.
            Use a production WSGI server instead.
          * Debug mode: off
              * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

注意

确保执行命令前激活了虚拟环境(pipenv shell),否则需要使用pipenv run flask run命令启动开发服务器。后面将不再提示。

你可以执行flask--help查看所有可用的命令。

提示

如果执行flask run命令后显示命令未找到提示(command not found)或其他错误,可以尝试使用python-m flask run启动服务器,其他命令亦同。

flask run命令运行的开发服务器默认会监听http://127.0.0.1:5000/地址(按Crtl+C退出),并开启多线程支持。当我们打开浏览器访问这个地址时,会看到网页上显示“Hello,World!”,如图1-5所示。

图1-5 “Hello,World!”程序主页

提示

http://127.0.0.1即localhost,是指向本地机的IP地址,一般用来测试。Flask默认使用5000端口,对于上面的地址,你也可以使用http://localhost:5000/。在本书中这两者会交替使用,除了地址不同外,两者没有实际区别,即域名和IP地址的映射关系。

提示

旧的启动开发服务器的方式是使用app.run()方法,目前已不推荐使用(deprecated)。

1.自动发现程序实例

一般来说,在执行flask run命令运行程序前,我们需要提供程序实例所在模块的位置。我们在上面可以直接运行程序,是因为Flask会自动探测程序实例,自动探测存在下面这些规则:

❑ 从当前目录寻找app.py和wsgi.py模块,并从中寻找名为app或application的程序实例。

❑ 从环境变量FLASK_APP对应的值寻找名为app或application的程序实例。

因为我们的程序主模块命名为app.py,所以flask run命令会自动在其中寻找程序实例。如果你的程序主模块是其他名称,比如hello.py,那么需要设置环境变量FLASK_APP,将包含程序实例的模块名赋值给这个变量。Linux或mac OS系统使用export命令:

        $ export FLASK_APP=hello

在Windows系统中使用set命令:

        > set FLASK_APP=hello
2.管理环境变量

Flask的自动发现程序实例机制还有第三条规则:如果安装了python-dotenv,那么在使用flask run或其他命令时会使用它自动从.flaskenv文件和.env文件中加载环境变量。

附注

当安装了python-dotenv时,Flask在加载环境变量的优先级是:手动设置的环境变量>.env中设置的环境变量>.flaskenv设置的环境变量。

除了FLASK_APP,在后面我们还会用到其他环境变量。环境变量在新创建命令行窗口或重启电脑后就清除了,每次都要重设变量有些麻烦。而且如果你同时开发多个Flask程序,这个FLASK_APP就需要在不同的值之间切换。为了避免频繁设置环境变量,我们可以使用python-dotenv管理项目的环境变量,首先使用Pipenv将它安装到虚拟环境:

        $ pipenv install python-dotenv

我们在项目根目录下分别创建两个文件:.env和.flaskenv。.flaskenv用来存储和Flask相关的公开环境变量,比如FLASK_APP;而.env用来存储包含敏感信息的环境变量,比如后面我们会用来配置Email服务器的账户名与密码。在.flaskenv或.env文件中,环境变量使用键值对的形式定义,每行一个,以#开头的为注释,如下所示:

        SOME_VAR=1
        # 这是注释
        FOO="BAR"

注意

.env包含敏感信息,除非是私有项目,否则绝对不能提交到Git仓库中。当你开发一个新项目时,记得把它的名称添加到.gitignore文件中,这会告诉Git忽略这个文件。gitignore文件是一个名为.gitignore的文本文件,它存储了项目中Git提交时的忽略文件规则清单。Python项目的.gitignore模板可以参考https://github.com/github/gitignore/blob/master/Python.gitignore。使用Py Charm编写程序时会产生一些配置文件,这些文件保存在项目根目录下的.idea目录下,关于这些文件的忽略设置可以参考https://www.gitignore.io/api/pycharm

3.使用Py Charm运行服务器

在Py Charm中,虽然我们可以使用内置的命令行窗口执行命令以启动开发服务器,但是在开发时使用Py Charm内置的运行功能更加方便。在2018.1版本后的专业版添加了Flask命令行支持,在旧版本或社区版中,如果要使用Py Charm运行程序,还需要进行一些设置。

首先,在Py Charm中,单击菜单栏中的Run→Edit Configurations打开运行配置窗口。图1-6中标出了在Py Charm中设置一个运行配置的具体步骤序号。

图1-6 运行Flask程序的配置

打开新建配置窗口后,具体的步骤如下所示:

步骤1 单击左侧的“+”符号打开下拉列表。

步骤2 新建一个Python类型的运行配置(如果你使用的是专业版,则可以直接选择Flask server),并在右侧的Name字段输入一个合适的名称,比如“Run hello”。

步骤3 勾选“Single instance only”。

步骤4 将第一项配置字段通过下列选项选为“Module Name”。

步骤5 填入模块名称flask。

步骤6 第二栏的“Parameters”填入要执行的命令run,你也可以附加其他启动选项。

步骤7 在“Working directory”字段中选择程序所在的目录作为工作目录。

提示

我们可以单击左上方的复制图标复制一份配置,然后稍加修改就可以用于其他flask命令,包括扩展提供的命令,或是我们自定义的命令。

现在单击Apply或OK保存并关闭窗口。在Py Charm右上方选择我们创建的运行配置,然后单击绿色三角形的运行按钮即可启动开发服务器。

注意

因为本章示例程序的模块名称为app.py,Flask会自动从中寻找程序实例,所以我们在Py Charm中的运行设置可以正确启动程序。如果你不打算使用python-dotenv来管理环境变量,那么需要修改Py Charm的运行配置:在Enviroment variable字段中添加环境变量FLASK_APP并设置正确的值。

1.3.2 更多的启动选项

1.使服务器外部可见

我们在上面启动的Web服务器默认是对外不可见的,可以在run命令后添加--host选项将主机地址设为0.0.0.0使其对外可见:

        $ flask run --host=0.0.0.0

这会让服务器监听所有外部请求。个人计算机(主机)一般没有公网IP(公有地址),所以你的程序只能被局域网内的其他用户通过你的个人计算机的内网IP(私有地址)访问,比如你的内网IP为192.168.191.1。当局域网内的其他用户访问http://192.168.191.1:5000时,也会看到浏览器里显示一行“Hello,Flask!”。

提示

把程序安装在拥有公网IP的服务器上,让互联网上的所有人都可以访问是我们最后要介绍的程序部署部分的内容。如果你迫切地想把你的程序分享给朋友们,可以考虑使用ngrok(https://ngrok.com/)、Localtunnel(https://localtunnel.github.io/www/)等内网穿透/端口转发工具。

2.改变默认端口

Flask提供的Web服务器默认监听5000端口,你可以在启动时传入参数来改变它:

        $ flask run --port=8000

这时服务器会监听来自8000端口的请求,程序的主页地址也相应变成了http://localhost:8000/

附注

执行flask run命令时的host和port选项也可以通过环境变量FLASK_RUN_HOST和FLASK_RUN_PORT设置。事实上,Flask内置的命令都可以使用这种模式定义默认选项值,即“FLASK_<COMMAND>_<OPTION>”,你可以使用flask--help命令查看所有可用的命令。

1.3.3 设置运行环境

开发环境(development enviroment)和生产环境(production enviroment)是我们后面会频繁接触到的概念。开发环境是指我们在本地编写和测试程序时的计算机环境,而生产环境与开发环境相对,它指的是网站部署上线供用户访问时的服务器环境。

根据运行环境的不同,Flask程序、扩展以及其他程序会改变相应的行为和设置。为了区分程序运行环境,Flask提供了一个FLASK_ENV环境变量用来设置环境,默认为production(生产)。在开发时,我们可以将其设为development(开发),这会开启所有支持开发的特性。为了方便管理,我们将把环境变量FLASK_ENV的值写入.flaskenv文件中:

        FLASK_ENV=development

现在启动程序,你会看到下面的输出提示:

        $ flask run
          * Environment: development
          * Debug mode: on
          * Debugger is active!
          * Debugger PIN: 202-005-064
          * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

在开发环境下,调试模式(Debug Mode)将被开启,这时执行flask run启动程序会自动激活Werkzeug内置的调试器(debugger)和重载器(reloader),它们会为开发带来很大的帮助。

提示

如果你想单独控制调试模式的开关,可以通过FLASK_DEBUG环境变量设置,设为1则开启,设为0则关闭,不过通常不推荐手动设置这个值。

注意

在生产环境中部署程序时,绝不能开启调试模式。尽管PIN码可以避免用户任意执行代码,提高攻击者利用调试器的难度,但并不能确保调试器完全安全,会带来巨大的安全隐患。而且攻击者可能会通过调试信息获取你的数据库结构等容易带来安全问题的信息。另一方面,调试界面显示的错误信息也会让普通用户感到困惑。

1.调试器

Werkzeug提供的调试器非常强大,当程序出错时,我们可以在网页上看到详细的错误追踪信息,这在调试错误时非常有用。运行中的调试器如图1-7所示。

图1-7 调试器界面

调试器允许你在错误页面上执行Python代码。单击错误信息右侧的命令行图标,会弹出窗口要求输入PIN码,也就是在启动服务器时命令行窗口打印出的调试器PIN码(Debugger PIN)。输入PIN码后,我们可以单击错误堆栈的某个节点右侧的命令行界面图标,这会打开一个包含代码执行上下文信息的Python Shell,我们可以利用它来进行调试。

2.重载器

当我们对代码做了修改后,期望的行为是这些改动立刻作用到程序上。重载器的作用就是监测文件变动,然后重新启动开发服务器。当我们修改了脚本内容并保存后,会在命令行看到下面的输出:

        Detected change in '/path/to/app.py', reloading
          * Restarting with stat

默认会使用Werkzeug内置的stat重载器,它的缺点是耗电较严重,而且准确性一般。为了获得更优秀的体验,我们可以安装另一个用于监测文件变动的Python库Watchdog,安装后Werkzeug会自动使用它来监测文件变动:

        $ pipenv install watchdog --dev

因为这个包只在开发时才会用到,所以我们在安装命令后添加了一个--dev选项,这用来把这个包声明为开发依赖。在Pipfile文件中,这个包会被添加到dev-packages部分。

不过,如果项目中使用了单独的CSS或Java Script文件时,那么浏览器可能会缓存这些文件,从而导致对文件做出的修改不能立刻生效。在浏览器中,我们可以按下Crtl+F5或Shift+F5执行硬重载(hard reload),即忽略缓存并重载(刷新)页面。

提示

当在一个新电脑创建运行环境时,使用pipenv install命令时需要添加额外的--dev选项才会安装dev-packages部分定义的开发依赖包。

1.4 Python Shell

本书有许多操作需要在Python Shell(即Python交互式解释器)里执行。在开发Flask程序时,我们并不会直接使用python命令启动Python Shell,而是使用flask shell命令:

        $ flask shell
        App: app [development]
        Instance: Path/to/your/helloflask/instance
        >>>

注意

和其他flask命令相同,执行这个命令前我们要确保程序实例可以被正常找到。

在本书中,如果代码片段前的提示符为三个大于号,即“>>>”,那么就表示这些代码需要在使用flask shell命令打开的Python Shell中执行。

提示

Python Shell可以执行exit()或quit()退出,在Windows系统上可以使用Crtl+Z并按Enter退出;在Linux和mac OS则可以使用Crtl+D退出。

使用flask shell命令打开的Python Shell自动包含程序上下文,并且已经导入了app实例:

        >>> app
        <Flask 'app'>
        >>> app.name
          'app'

附注

上下文(context)可以理解为环境。为了正常运行程序,一些操作相关的状态和数据需要被临时保存下来,这些状态和数据被统称为上下文。在Flask中,上下文有两种,分别为程序上下文和请求上下文,后面我们会详细了解。

1.5 Flask扩展

在本书中我们将会接触到很多Flask扩展。扩展(extension)即使用Flask提供的API接口编写的Python库,可以为Flask程序添加各种各样的功能。大部分Flask扩展用来集成其他库,作为Flask和其他库之间的薄薄一层胶水。因为Flask扩展的编写有一些约定,所以初始化的过程大致相似。大部分扩展都会提供一个扩展类,实例化这个类,并传入我们创建的程序实例app作为参数,即可完成初始化过程。通常,扩展会在传入的程序实例上注册一些处理函数,并加载一些配置。

以某扩展实现了Foo功能为例,这个扩展的名称将是Flask-Foo或Foo-Flask;程序包或模块的命名使用小写加下划线,即flask_foo(即导入时的名称);用于初始化的类一般为Foo,实例化的类实例一般使用小写,即foo。初始化这个假想中的Flask-Foo扩展的示例如下所示:

        from flask import Flask
        from flask_foo import Foo
        app = Flask(__name__)
        foo = Foo(app)

在日常开发中,大多数情况下,我们没有必要重复制造轮子,所以选用扩展可以避免让项目变得臃肿和复杂。尽管使用扩展可以简化操作,快速集成某个功能,但同时也会降低灵活性。如果过度使用扩展,在不需要的地方引入,那么相应也会导致代码不容易维护。更糟糕的是,质量差的扩展可能还会带来潜在的Bug,而不同扩展之间也可能会出现冲突。因此,在编写程序时,应该尽量从实际需求出发,只在需要的时候使用扩展,并把扩展的质量和兼容性作为考虑因素,尽量在效率和灵活性之间达到平衡。

附注

早期版本的Flask扩展使用flaskext.foo或flask.ext.something的形式导入,在实际使用中带来了许多问题,因此Flask官方推荐以flask_something形式导入扩展。在1.0版本以后的Flask中,旧的扩展导入方式已被移除。

1.6 项目配置

在很多情况下,你需要设置程序的某些行为,这时你就需要使用配置变量。在Flask中,配置变量就是一些大写形式的Python变量,你也可以称之为配置参数或配置键。使用统一的配置变量可以避免在程序中以硬编码(hard coded)的形式设置程序。

在一个项目中,你会用到许多配置:Flask提供的配置,扩展提供的配置,还有程序特定的配置。和平时使用变量不同,这些配置变量都通过Flask对象的app.config属性作为统一的接口来设置和获取,它指向的Config类实际上是字典的子类,所以你可以像操作其他字典一样操作它。

附注

Flask内置的配置可以访问Flask文档的配置章节(flask.pocoo.org/docs/latest/config/)查看,扩展提供的配置也可以在对应的文档中查看。

Flask提供了很多种方式来加载配置。比如,你可以像在字典中添加一个键值对一样来设置一个配置:

        app.config['ADMIN_NAME'] = 'Peter'

注意

配置的名称必须是全大写形式,小写的变量将不会被读取。

使用update()方法则可以一次加载多个值:

        app.config.update(
            TESTING=True,
            SECRET_KEY='_5#y F4Q8z\n\xec]/'
        )

除此之外,你还可以把配置变量存储在单独的Python脚本、JSON格式的文件或是Python类中,config对象提供了相应的方法来导入配置,具体我们会在后面了解。

和操作字典一样,读取一个配置就是从config字典里通过将配置变量的名称作为键读取对应的值:

        value = app.config['ADMIN_NAME']

提示

某些扩展需要读取配置值来完成初始化操作,比如Flask-Mail,因此我们应该尽量将加载配置的操作提前,最好在程序实例app创建后就加载配置。

1.7 URL与端点

在Web程序中,URL无处不在。如果程序中的URL都是以硬编码的方式写出,那么将会大大降低代码的易用性。比如,当你修改了某个路由的URL规则,那么程序里对应的URL都要一个一个进行修改。更好的解决办法是使用Flask提供的url_for()函数获取URL,当路由中定义的URL规则被修改时,这个函数总会返回正确的URL。

调用url_for()函数时,第一个参数为端点(endpoint)值。在Flask中,端点用来标记一个视图函数以及对应的URL规则。端点的默认值为视图函数的名称,至于为什么不直接使用视图函数名,而要引入端点这个概念,我们会在后面了解。

比如,下面的视图函数:

        @app.route('/')
        def index():
            return 'Hello Flask!'

这个路由的端点即视图函数的名称index,调用url_for('index')即可获取对应的URL,即“/”。

提示

在app.route()装饰器中使用endpoint参数可以自定义端点值,不过我们通常不需要这样做。

如果URL含有动态部分,那么我们需要在url_for()函数里传入相应的参数,以下面的视图函数为例:

        @app.route('/hello/<name>')
        def greet(name):
            return 'Hello %s!' % name

这时使用url_for('say_hello',name='Jack')得到的URL为“/hello/Jack”。

提示

我们使用url_for()函数生成的URL是相对URL(即内部URL),即URL中的path部分,比如“/hello”,不包含根URL。相对URL只能在程序内部使用。如果你想要生成供外部使用的绝对URL,可以在使用url_for()函数时,将_external参数设为True,这会生成完整的URL,比如http://helloflask.com/hello,在本地运行程序时则会获得http://localhost:5000/hello

1.8 Flask命令

除了Flask内置的flask run等命令,我们也可以自定义命令。在虚拟环境安装Flask后,包含许多内置命令的flask脚本就可以使用了。在前面我们已经接触了很多flask命令,比如运行服务器的flask run,启动shell的flask shell。

通过创建任意一个函数,并为其添加app.cli.command()装饰器,我们就可以注册一个flask命令。代码清单1-4创建了一个自定义的hello()命令函数,在函数中我们仍然只是打印一行问候。

代码清单1-4 hello/app.py:创建自定义命令

        @app.cli.command()
        def hello():
            click.echo('Hello, Human!')

函数的名称即为命令名称,这里注册的命令即hello,你可以使用flask hello命令来触发函数。作为替代,你也可以在app.cli.command()装饰器中传入参数来设置命令名称,比如app.cli.command('say-hello')会把命令名称设置为say-hello,完整的命令即flask say-hello。

借助click模块的echo()函数,我们可以在命令行界面输出字符。命令函数的文档字符串则会作为帮助信息显示(flask hello--help)。在命令行下执行flask hello命令就会触发这个hello()函数:

        $ flask hello
        Hello, Human!

在命令下执行flask--help可以查看Flask提供的命令帮助文档,我们自定义的hello命令也会出现在输出的命令列表中,如下所示:

        $ flask --help
        Usage: flask [OPTIONS] COMMAND [ARGS]...
          A general utility script for Flask applications.
          .
        Options:
          --version  Show the flask version
          --help     Show this message and exit.
        Commands:
          hello   Just say hello.  # 我们注册的自定义命令
          routes  Show the routes for the app.  # 显示所有注册的路由
          run     Runs a development server.
          shell   Runs a shell in the app context.

附注

关于自定义命令更多的设置和功能请参考Click的官方文档(http://click.pocoo.org/6/)。

1.9 模板与静态文件

一个完整的网站当然不能只返回用户一句“Hello,World!”,我们需要模板(template)和静态文件(static file)来生成更加丰富的网页。模板即包含程序页面的HTML文件,静态文件则是需要在HTML文件中加载的CSS和Java Script文件,以及图片、字体文件等资源文件。默认情况下,模板文件存放在项目根目录中的templates文件夹中,静态文件存放在static文件夹下,这两个文件夹需要和包含程序实例的模块处于同一个目录下,对应的项目结构示例如下所示:

        hello/
            - templates/
            - static/
            - app.py

在开发Flask程序时,使用CSS框架和Java Script库是很常见的需求,而且有很多扩展都提供了对CSS框架和Java Script库的集成功能。使用这些扩展时都需要加载对应的CSS和Java Script文件,通常这些扩展都会提供一些可以在HTML模板中使用的加载方法/函数,使用这些方法即可渲染出对应的link标签和script标签。这些方法一般会直接从CDN加载资源,有些提供了手动传入资源URL的功能,有些甚至提供了内置的本地资源。

我建议在开发环境下使用本地资源,这样可以提高加载速度。最好自己下载到static目录下,统一管理,出于方便的考虑也可以使用扩展内置的本地资源。在过渡到生产环境时,自己手动管理所有本地资源或自己设置CDN,避免使用扩展内置的资源。这个建议主要基于下面这些考虑因素:

❑ 鉴于国内的网络状况,扩展默认使用的国外CDN可能会无法访问,或访问过慢。

❑ 不同扩展内置的加载方法可能会加载重复的依赖资源,比如j Query。

❑ 在生产环境下,将静态文件集中在一起更方便管理。

❑ 扩展内置的资源可能会出现版本过旧的情况。

关于模板和静态文件的使用,我们将在第3章详细介绍。

附注

CDN指分布式服务器系统。服务商把你需要的资源存储在分布于不同地理位置的多个服务器,它会根据用户的地理位置来就近分配服务器提供服务(服务器越近,资源传送就越快)。使用CDN服务可以加快网页资源的加载速度,从而优化用户体验。对于开源的CSS和Java Script库,CDN提供商通常会免费提供服务。

1.10 Flask与MVC架构

你也许会困惑为什么用来处理请求并生成响应的函数被称为“视图函数(view function)”,其实这个命名并不合理。在Flask中,这个命名的约定来自Werkzeug,而Werkzeug中URL匹配的实现主要参考了Routes(一个URL匹配库),再往前追溯,Routes的实现又参考了Ruby on Rails(http://rubyonrails.org/)。在Ruby on Rails中,术语views用来表示MVC(Model-View-Controller,模型-视图-控制器)架构中的View。

MVC架构最初是用来设计桌面程序的,后来也被用于Web程序,应用了这种架构的Web框架有Django、Ruby on Rails等。在MVC架构中,程序被分为三个组件:数据处理(Model)、用户界面(View)、交互逻辑(Controller)。如果套用MVC架构的内容,那么Flask中视图函数的名称其实并不严谨,使用控制器函数(Controller Function)似乎更合适些,虽然它也附带处理用户界面。严格来说,Flask并不是MVC架构的框架,因为它没有内置数据模型支持。为了方便表述,在本书中,使用了app.route()装饰器的函数仍被称为视图函数,同时会使用“<函数名>视图”(比如index视图)的形式来代指某个视图函数。

粗略归类,如果想要使用Flask来编写一个MVC架构的程序,那么视图函数可以作为控制器(Controller),视图(View)则是我们第3章将要学习的使用Jinja2渲染的HTML模板,而模型(Model)可以使用其他库来实现,在第5章我们会介绍使用SQLAlchemy来创建数据库模型。

1.11 本章小结

本章我们学习了Flask程序的运作方式和一些基本概念,这为我们进一步学习打下了基础。下一章,我们会了解隐藏在Flask背后的重要角色——HTTP,并学习Flask是如何与之进行交互的。