Flask整理

个人总结的博客

flask学习笔记1

flask学习笔记2

flask学习笔记3-CBV实现登陆

flask代码——装饰器与CBV

flask蓝图的一个实例

SQLAlchemy使用汇总

flask操作session--登陆认证与注销的例子

Flask给视图增加多个装饰器的问题及解决方案

flask项目中使用Flask-SQLAlchemy以及利用脚本方式启动flask项目的方法

第一部分 基础篇

配套代码在github上:helloflask

搭建开发环境

书中介绍使用pipenv,笔者pipenv与virtualenv都使用过,具体使用可以参考笔者的博客园文章:
pipenv:python虚拟环境与包管理工具介绍
linux下虚拟环境模块virtualenv及管理工具virtualenvwrapper的使用
windows与mac下virtualenv与Pycharm的结合使用

一个简单的Flask程序及说明

# -*- coding:utf-8 -*-
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "<h1 style='color:red'>Hello Flask!</h1>"

### 注意原书中没有下面这些 是我自己加的
if __name__ == '__main__':
    debug = True # 生产环境需要把debug设置为False
    # 启动
    app.run(host="127.0.0.1",port=9200,debug=debug)

创建程序实例

我们安装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。

注册路由

在一个Web应用里,客户端和服务器上的Flask程序的交互可以简单概括为以下几步:
1)用户在浏览器输入URL访问某个资源。
2)Flask接收用户请求并分析请求的URL。
3)为这个URL找到对应的处理函数。
4)执行函数并生成响应,返回给浏览器。
5)浏览器接收并解析响应,将信息显示在页面中。

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

提示

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()函数,获得相同的响应,

@app.route("/hi")
@app.route("/hello")
def say_hello():
    return "HELLO!"
2. 动态URL

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

@app.route("/greet/<name>")
def greet(name):
    return "hello {}".format(name)

因为URL中可以包含变量,所以我们将传入app.route()的字符串称为URL规则,而不是URL。Flask会解析请求并把请求的URL与视图函数的URL规则进行匹配。比如,这个greet视图的URL规则为/greet/,那么类似/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":"whw"})
@app.route("/greet/<name>")
def greet(name):
    return "hello {}".format(name)

这时如果用户访问/greet,那么变量name会使用默认值whw,视图函数返回

Hello,whw!

。上面的用法实际效果等同于:

上面的用法实际效果等同于:

@app.route("/greet")
@app.route("/greet/<name>")
def greet(name=“whw”):
    return "hello {}".format(name)
url_for反向解析URL

end_point

endpoint是为flask路由作别名用的。

STUDENT_DICT = {}

@app.route('/index',endpoint='index')
def index():
    return render_template('index.html',stu_dic=STUDENT_DICT)

url_for的使用:包含带参数与不带参数

参考我的博客:Flask中的路由

在app.route()装饰器中使用endpoint参数可以自定义端点值,不过我们通常不需要这样做。如果URL含有动态部分,那么我们需要在url_for()函数里传入相应的参数,以下面的视图函数为例:

@app.route("/hello/<name>",endpoint="greet")
def greet(name):
    return "hello {}".format(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。

启动开发服务器

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

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

方法1 app.run()方法

直接运行上面的程序(有 if _name_ == "_main_")

但是书中说这种方法已经不推荐了:旧的启动开发服务器的方式是使用app.run()方法,目前已不推荐使用(deprecated)。

方法2 命令行的方式

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

 # 不指定host与port的话默认是127.0.0.1:5000
 flask run --host=127.0.0.1 --port=9000  

默认情况下:这个命令必须在我们之前创建的虚拟环境里执行,并且必须在我们python脚本所在的目录下执行,并且脚本的名字必须是:app.py

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,那么在使用flaskrun或其他命令时会使用它自动从.flaskenv文件和.env文件中加载环境变量。

附注

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

$ pip3 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。

设置运行环境

开发环境(development enviroment)和生产环境(production enviroment)是我们后面会频繁接触到的概念。开发环境是指我们在本地编写和测试程序时的计算机环境,而生产环境与开发环境相对,它指的是网站部署上线供用户访问时的服务器环境。根据运行环境的不同,Flask程序、扩展以及其他程序会改变相应的行为和设置。

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

FLASK_ENV=development

启动程序时会提示环境(Environment)为development

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

Python Shell VS Flask Shell

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

$ flask shell
Python 3.6.8rc1 (v3.6.8rc1:cc3e73212a, Dec 11 2018, 17:37:34) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] on darwin
App: app [production]
Instance: /Users/wanghongwei/PycharmProjects/hello_flask/chapter1/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.name
'app'

附注

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

Flask自定义的命令

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

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

import click
from flask import Flask

app = Flask(__name__)

@app.cli.command()
def hi():
    click.echo("Hi!!!")

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

>>> flask hi
"Hi!!!"

flask help查看可以使用的命令

>>> flask --help

Usage: flask [OPTIONS] COMMAND [ARGS]...

  A general utility script for Flask applications.

  Provides commands from Flask, extensions, and the application. Loads the
  application defined in the FLASK_APP environment variable, or from a
  wsgi.py file. Setting the FLASK_ENV environment variable to 'development'
  will enable debug mode.

    $ export FLASK_APP=hello.py
    $ export FLASK_ENV=development
    $ flask run

Options:
  --version  Show the flask version
  --help     Show this message and exit.

# 可以使用的命令
Commands:
  hi
  routes  Show the routes for the app.
  run     Runs a development server.
  shell   Runs a shell in the app context.

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中,旧的扩展导入方式已被移除。

项目配置

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

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

附注

Flask内置的配置可以访问Flask文档的配置章节(flask.pocoo.org/docs/latest/config/)查看,扩展提供的配置也可以在对应的文档中查看。Flask提供了很多种方式来加载配置。比如,你可以像在字典中添加一个键值对一样来设置一个配置:

app.config["ADMIN_NAME"] = "whw"

配置的名称必须是全大写形式,小写的变量将不会被读取。使用update()方法则可以一次加载多个值:

app.config.update(
    TESTING=True,
    ADMIN_NAME="whw"
)

Flask与HTTP

如果想要进一步开发更复杂的Flask应用,我们就得了解Flask与HTTP协议的交互方式。HTTP(Hypertext Transfer Protocol,超文本传输协议)定义了服务器和客户端之间信息交流的格式和传递方式,它是万维网(World Wide Web)中数据交换的基础。在这一章,我们会了解Flask处理请求和响应的各种方式,并对HTTP协议以及其他非常规HTTP请求进行简单的介绍。

附注

HTTP的详细定义在RFC 7231~7235中可以看到。RFC(Request For Comment,请求评议)是一系列关于互联网标准和信息的文件,可以将其理解为互联网(Internet)的设计文档。完整的RFC列表可以在这里看到:https://tools.ietf.org/rfc/。本章的示例程序在helloflask/demos/http目录下,确保当前工作目录在helloflask/demos/http下并激活了虚拟环境!

经典问题:

当我们在浏览器中的地址栏中输入这个URL,然后按下Enter时,稍等片刻,浏览器会显示一个问候页面。这背后到底发生了什么?

这背后也有一个类似我们第1章编写的程序运行着。它负责接收用户的请求,并把对应的内容返回给客户端,显示在用户的浏览器上。事实上,每一个Web应用都包含这种处理模式,即“请求-响应循环(Request-ResponseCycle)”:客户端发出请求,服务器端处理请求并返回响应!

请求响应循环示意图

a79f4deea39e9974645a6e5430aa0a4e.png

Flask Web工作流程

31cb6b524195ee43fd19f1430537df83.png

HTTP请求

请求报文示意图

2664e436140e910b30c10f3858f0b6b2.png

常见HTTP请求方法

3992f4ecec7b553b1f3fe52064255b38.png

Request对象

现在该让Flask的请求对象request出场了,这个请求对象封装了从客户端发来的请求报文,我们能从它获取请求报文中的所有数据。

注意

请求解析和响应封装实际上大部分是由Werkzeug完成的,Flask子类化Werkzeug的请求(Request)和响应(Response)对象并添加了和程序相关的特定功能。

request对象常用的属性和方法

8ceb85491a14408a633438821c850e48.png

获取请求URL

43433d7d2583dda82c7466efd8f6f183.png

提示

Werkzeug的Mutli Dict类是字典的子类,它主要实现了同一个键对应多个值的情况。比如一个文件上传字段可能会接收多个文件。这时就可以通过getlist()方法来获取文件对象列表。而Immutable Multi Dict类继承了Mutli Dict类,但其值不可更改。

flask routes命令查看项目所有路由

 $ flask routes
 
 Endpoint                         Methods  Rule
-------------------------------  -------  --------------------------------
fb_adset.create_adset            POST     /fb_adset/create_adset
fb_adset.del_adset               POST     /fb_adset/del_adset
fb_adset.get_account_adsets      GET      /fb_adset/get_account_adsets
fb_adset.get_adset               GET      /fb_adset/get_adset
fb_adset.get_campaign_adsets     GET      /fb_adset/get_campaign_adsets
fb_adset.update_adset            POST     /fb_adset/update_adset
index                            GET      /
operate.batch_get_campaign_data  GET      /operate/batch_get_campaign_data
operate.get_campaign_data        GET      /operate/get_campaign_data
operate.update_campaign_data     POST     /operate/update_campaign_data
static                           GET      /static/<path:filename>

在输出的文本中,我们可以看到每个路由对应的端点(Endpoint)、HTTP方法(Methods)和URL规则(Rule),其中static端点是Flask添加的特殊路由,用来访问静态文件。

设置监听HTTP方法

GET是最常用的HTTP方法,所以视图函数默认监听的方法类型就是GET,HEAD、OPTIONS方法的请求由Flask处理,而像DELETE、PUT等方法一般不会在程序中实现,在后面我们构建Web API时才会用到这些方法。

我们可以在app.route()装饰器中使用methods参数传入一个包含监听的HTTP方法的可迭代对象。比如,下面的视图函数同时监听GET请求和POST请求:

@app.route("/hello",methods=["GET","POST"])
def hello():
    return "HELLO"

当某个请求的方法不符合要求时,请求将无法被正常处理。比如,在提交表单时通常使用POST方法,而如果提交的目标URL对应的视图函数只允许GET方法,这时Flask会自动返回一个405错误响应(Method Not Allowed,表示请求方法不允许)

请求钩子

有时我们需要对请求进行预处理(preprocessing)和后处理(postprocessing),这时可以使用Flask提供的一些请求钩子(Hook),它们可以用来注册在请求处理的不同阶段执行的处理函数(或称为回调函数,即Callback)。这些请求钩子使用装饰器实现,通过程序实例app调用,用法很简单:以before_request钩子(请求之前)为例,当你对一个函数附加了app.before_request装饰器后,就会将这个函数注册为before_request处理函数,每次执行请求前都会触发所有before_request处理函数。Flask默认实现的五种请求钩子:

9b789431e5ba0a2d2510271118f61403.png

这些钩子使用起来和app.route()装饰器基本相同,每个钩子可以注册任意多个处理函数,函数名并不是必须和钩子名称相同,下面是一个基本示例:

@app.before_request
def do_sth():
    pass # 在请求前执行

图示如下:

1fc2d7b43fe9e82d2e1fc0518ee2e704.png

下面是请求钩子的一些常见使用场景

  • before_first_request:在玩具程序中,运行程序前我们需要进行一些程序的初始化操作,比如创建数据库表,添加管理员用户。这些工作可以放到使用before_first_request装饰器注册的函数中。

  • before_request:比如网站上要记录用户最后在线的时间,可以通过用户最后发送的请求时间来实现。为了避免在每个视图函数都添加更新在线时间的代码,我们可以仅在使用before_request钩子注册的函数中调用这段代码。

  • after_request:我们经常在视图函数中进行数据库操作,比如更新、插入等,之后需要将更改提交到数据库中。提交更改的代码就可以放到after_request钩子注册的函数中。

  • 另一种常见的应用是建立数据库连接,通常会有多个视图函数需要建立和关闭数据库连接,这些操作基本相同。一个理想的解决方法是在请求之前(before_request)建立连接,在请求之后(teardown_request)关闭连接。通过在使用相应的请求钩子注册的函数中添加代码就可以实现。这很像单元测试中的set Up()方法和tear Down()方法。

注意

after_request钩子和after_this_request钩子必须接收一个响应类对象作为参数,并且返回同一个或更新后的响应对象。

HTTP响应

响应报文

响应报文主要由协议版本、状态码(status code)、原因短语(reason phrase)、响应首部和响应主体组成。以发向localhost:5000/hello的请求为例,服务器生成的响应报文示意如表2-8所示。

0886795de5fd7397f3d617ff057bd14f.png

响应报文的首部包含一些关于响应和服务器的信息,这些内容由Flask生成,而我们在视图函数中返回的内容即为响应报文中的主体内容。浏览器接收到响应后,会把返回的响应主体解析并显示在浏览器窗口上。

常见HTTP响应状态码

93f8f4e8ec256fdda76fa796851b56b2.png

提示

当关闭调试模式时,即FLASK_ENV使用默认值production,如果程序出错,Flask会自动返回500错误响应;而调试模式下则会显示调试信息和错误堆栈。

在Flask中生成响应

Response类常用的属性和方法

de434dba1ed6e8ca8808255093d0dd55.png

可以使用make_response生成响应信息

from flask import make_response

@app.route("/foo")
def foo():
    rep = make_response("hello flask")
    rep.mimetype = "text/plain"
    return rep

MIME类型(又称为media type或content type)是一种用来标识文件类型的机制,它与文件扩展名相对应,可以让客户端区分不同的内容类型,并执行不同的操作。一般的格式为“类型名/子类型名”,其中的子类型名一般为文件扩展名。比如,HTML的MIME类型为“text/html”,png图片的MIME类型为“image/png”。完整的标准MIME类型列表可以在这里看到:https://www.iana.org/assignments/media-types/media-types.xhtml。

其他

现在绝大多数都是前后端分离的程序,因此响应基本都会返回json格式的数据了。

我们可以在after_request装饰的方法中设置响应头:

@after_request
def af_req(response):
    response.headers["date"] = "xxx"
    return response
cookie相关操作

set_cookie()方法

3bf0e0fcdfcf3c40d5abde2ad6954bae.png

from flask import Flask,make_response
...
@app.route("/set/<name>")
def set_cookies(name):
    rep = make_response(redirect(url_for("hello")))
    rep.set_cookie("name":name)
    return rep

在这个make_response()函数中,我们传入的是使用redirect()函数生成的重定向响应。set_cookie视图会在生成的响应报文首部中创建一个Set-Cookie字段,即“Set-Cookie:name=Grey;Path=/”。

session相关操作

在编程中,session指用户会话(user session),又称为对话(dialogue),即服务器和客户端/浏览器之间或桌面程序和用户之间建立的交互活动。在Flask中,session对象用来加密Cookie。默认情况下,它会把数据存储在浏览器上一个名为session的cookie里。

1.设置程序密钥

session通过密钥对数据进行签名以加密数据,因此,我们得先设置一个密钥。这里的密钥就是一个具有一定复杂度和随机性的字符串,比如“Drmhze6EPcv0f N_81Bj-n A”。

程序的密钥可以通过Flask.secret_key属性或配置变量SECRET_KEY设置,比如:

app.secret_key = "xxxaaahhh"

更安全的做法是把密钥写进系统环境变量(在命令行中使用export或set命令),或是保存在.env文件中:

SECRET_KEY = xxx

然后在程序脚本中使用os模块提供的getenv()方法获取:

import os 
...
app.secret_key = os.getenv("SECRET_KEY","xxx")

这里的密钥只是示例。在生产环境中,为了安全考虑,你必须使用随机生成的密钥

2.模拟用户认证

登陆设置session

from flask import Flask,redirect,session,url_for,request

app = Flask(__name__)
app.secret_key = "666"

# 登陆设置session
@app.route("/login")
def login():
    session["login_in"] = True # 写入session
    return redirect(url_for("hello"))

# 获取session
@app.route("/",endpoint="hello")
@app.route("/hello",endpoint="hello")
def hello():
    if "login_in" in session:
        rep = "[Authenticated]"
    else:
        rep = "[Not Authenticated]"
    return rep

个人的例子
flask操作session--登陆认证与注销的例子

Flask上下文简述

我们可以把编程中的上下文理解为当前环境(environment)的快照(snapshot)。如果把一个Flask程序比作一条可怜的生活在鱼缸里的鱼的话,那么它当然离不开身边的环境。

提示

这里的上下文和阅读文章时的上下文基本相同。如果在某篇文章里单独抽出一句话来看,我们可能会觉得摸不着头脑,只有联系上下文后我们才能正确理解文章。

Flask中有两种上下文,程序上下文(application context)和请求上下文(requestcontext)。

如果鱼想要存活,水是必不可少的元素。对于Flask程序来说,程序上下文就是我们的水。水里包含了各种浮游生物以及微生物,正如程序上下文中存储了程序运行所必须的信息;要想健康地活下去,鱼还离不开阳光。

射进鱼缸的阳光就像是我们的程序接收的请求。当客户端发来请求时,请求上下文就登场了。请求上下文里包含了请求的各种信息,比如请求的URL,请求的HTTP方法等。

上下文全局变量

每一个视图函数都需要上下文信息,在前面我们学习过Flask将请求报文封装在request对象中。按照一般的思路,如果我们要在视图函数中使用它,就得把它作为参数传入视图函数,就像我们接收URL变量一样。但是这样一来就会导致大量的重复,而且增加了视图函数的复杂度。

在前面的示例中,我们并没有传递这个参数,而是直接从Flask导入一个全局的request对象,然后在视图函数里直接调用request的属性获取数据。你一定好奇,我们在全局导入时request只是一个普通的Python对象,为什么在处理请求时,视图函数里的request就会自动包含对应请求的数据?这是因为Flask会在每个请求产生后自动激活当前请求的上下文,激活请求上下文后,request被临时设为全局可访问。而当每个请求结束后,Flask就销毁对应的请求上下文。

我们在前面说request是全局对象,但这里的“全局”并不是实际意义上的全局。我们可以把这些变量理解为动态的全局变量。

在多线程服务器中,在同一时间可能会有多个请求在处理。假设有三个客户端同时向服务器发送请求,这时每个请求都有各自不同的请求报文,所以请求对象也必然是不同的。因此,请求对象只在各自的线程内是全局的。Flask通过本地线程(thread local)技术将请求对象在特定的线程和请求中全局可访问。具体内容和应用我们会在后面进行详细介绍。

Flask中的上下文变量

ded574bd09758caad6ed5a4520af3b1a.png

提示

这四个变量都是代理对象(proxy),即指向真实对象的代理。一般情况下,我们不需要太关注其中的区别。在某些特定的情况下,如果你需要获取原始对象,可以对代理对象调用_get_current_object()方法获取被代理的真实对象。我们在前面对session和request都了解得差不多了,这里简单介绍一下current_app和g。

current_app与g

你在这里也许会疑惑,既然有了程序实例app对象,为什么还需要current_app变量。在不同的视图函数中,request对象都表示和视图函数对应的请求,也就是当前请求(currentrequest)。而程序也会有多个程序实例的情况,为了能获取对应的程序实例,而不是固定的某一个程序实例,我们就需要使用current_app变量,后面会详细介绍。

因为g存储在程序上下文中,而程序上下文会随着每一个请求的进入而激活,随着每一个请求的处理完毕而销毁,所以每次请求都会重设这个值。我们通常会使用它结合请求钩子来保存每个请求处理前所需要的全局变量,比如当前登入的用户对象,数据库连接等。在前面的示例中,我们在hello视图中从查询字符串获取name的值,如果每一个视图都需要这个值,那么就要在每个视图重复这行代码。借助g我们可以将这个操作移动到before_request处理函数中执行,然后保存到g的任意属性上:

from flask import g

@app.before_request
def get_name():
    g.name = request.args.get("name","")

设置这个函数后,在其他视图中可以直接使用g.name获取对应的值。另外,g也支持使用类似字典的get()、pop()以及setdefault()方法进行操作。

激活上下文

阳光柔和,鱼儿在水里欢快地游动,这一切都是上下文存在后的美好景象。如果没有上下文,我们的程序只能直挺挺地躺在鱼缸里。在下面这些情况下,Flask会自动帮我们激活程序上下文:

  • 当我们使用flask run命令启动程序时。
  • 使用旧的app.run()方法启动程序时。
  • 执行使用@app.cli.command()装饰器注册的flask命令时。
  • 使用flask shell命令启动Python Shell时。

当请求进入时,Flask会自动激活请求上下文,这时我们可以使用request和session变量。另外,当请求上下文被激活时,程序上下文也被自动激活。当请求处理完毕后,请求上下文和程序上下文也会自动销毁。也就是说,在请求处理时这两者拥有相同的生命周期。

结合Python的代码执行机制理解,这也就意味着,我们可以在视图函数中或在视图函数内调用的函数/方法中使用所有上下文全局变量。在使用flask shell命令打开的Python Shell中,或是自定义的flask命令函数中,我们可以使用current_app和g变量,也可以手动激活请求上下文来使用request和session。

如果我们在没有激活相关上下文时使用这些变量,Flask就会抛出Runtime Error异常:“Runtime Error: Working outside of application context.”或是“Runtime Error: Workingoutside of request context.”。

提示

同样依赖于上下文的还有url_for()、jsonify()等函数,所以你也只能在视图函数中使用它们。其中jsonify()函数内部调用中使用了current_app变量,而url_for()则需要依赖请求上下文才可以正常运行。

如果你需要在没有激活上下文的情况下使用这些变量,可以手动激活上下文。比如,下面是一个普通的Python shell,通过python命令打开。程序上下文对象使用app.app_context()获取,我们可以使用with语句执行上下文操作:

>>> from app import app
>>> from flask import current_app
>>> with app.app_context():
   ... current_app.name
"app"

或是显式地使用push()方法推送(激活)上下文,在执行完相关操作时使用pop()方法销毁上下文:

>>> from app import app
>>> from flask import current_app
>>> app_ctx = app.app_context()
>>> app_ctx.push()
>>> current_app.name
"app"
>>> app_ctx.pop()

而请求上下文可以通过test_request_context()方法临时创建:

>>> from app import app
>>> from flask import request
>>> with app.test_request_context("/hello"):
...    request.method
"GET"

同样的,这里也可以使用push()和pop()方法显式地推送和销毁请求上下文。

上下文钩子

在前面我们学习了请求生命周期中可以使用的几种钩子,Flask也为上下文提供了一个teardown_appcontext钩子,使用它注册的回调函数会在程序上下文被销毁时调用,而且通常也会在请求上下文被销毁时调用。比如,你需要在每个请求处理结束后销毁数据库连接:

@app.teardown_appcontext
def teardown_db(exception):
    ...
    db.close()

使用app.teardown_appcontext装饰器注册的回调函数需要接收异常对象作为参数,当请求被正常处理时这个参数值将是None,这个函数的返回值将被忽略。上下文是Flask的重要话题,在这里我们也只是简单了解一下,在本书的第三部分,我们会详细了解上下文的实现原理。

HTTP进阶实践

这部分几乎都是重点,在书中详细阅读。

包括的主要内容有:

重定向回上一个页面

1.获取上一个页面的URL
2.对URL进行安全验证

虽然我们已经实现了重定向回上一个页面的功能,但安全问题不容小觑,鉴于referer和next容易被篡改的特性,如果我们不对这些值进行验证,则会形成开放重定向(Open Redirect)漏洞。假设我们的应用是一个银行业务系统(下面简称网站A),某个攻击者模仿我们的网站外观做了一个几乎一模一样的网站(下面简称网站B)。接着,攻击者伪造了一封电子邮件,告诉用户网站A账户信息需要更新,然后向用户提供一个指向网站A登录页面的链接,但链接中包含一个重定向到网站B的next变量,比如:http://example A.com/login?next=http://maliciousB.com。当用户在A网站登录后,如果A网站重定向到next对应的URL,那么就会导致重定向到攻击者编写的B网站。因为B网站完全模仿A网站的外观,攻击者就可以在重定向后的B网站诱导用户输入敏感信息,比如银行卡号及密码。

主要代码如下
如果你使用Python3,那么这里需要从urllib.parse模块导入urlparse和urljoin函数。示例程序仓库中实际的代码做了兼容性处理。这个函数接收目标URL作为参数,并通过request.host_url获取程序内的主机URL,然后使用urljoin()函数将目标URL转换为绝对URL。接着,分别使用urlparse模块提供的urlparse()函数解析两个URL,最后对目标URL的URL模式和主机地址进行验证,确保只有属于程序内部的URL才会被返回。在执行重定向回上一个页面的redirect_back()函数中,我们使用is_safe_url()验证next和referer的值:

# redirect to last page
@app.route('/foo')
def foo():
    return '<h1>Foo page</h1><a href="%s">Do something and redirect</a>'% url_for('do_something', next=request.full_path)

@app.route('/bar')
def bar():
    return '<h1>Bar page</h1><a href="%s">Do something and redirect</a>'% url_for('do_something', next=request.full_path)

@app.route('/do-something')
def do_something():
    # do something here
    return redirect_back()

def is_safe_url(target):
    ref_url = urlparse(request.host_url)
    test_url = urlparse(urljoin(request.host_url, target))
    return test_url.scheme in ('http', 'https') and \
           ref_url.netloc == test_url.netloc

def redirect_back(default='hello', **kwargs):
    for target in request.args.get('next'), request.referrer:
        if not target:
            continue
        if is_safe_url(target):
            return redirect(target)
    return redirect(url_for(default, **kwargs))
使用AJAX技术发送异步请求
HTTP服务器端推送
Web安全防范

1.注入攻击
2.XSS攻击
3.CSRF攻击

Flask中的模板与表单

由于现在的项目基本都是前后端分离了,因此这2部分的内容略过。

这里记录一下比较重要的包的链接:

WTForms(2.2)

主页:https://github.com/wtforms/wtforms
文档:https://wtforms.readthedocs.io/en/latest/

Flask-WTF(0.14.2)

主页:https://github.com/lepture/flask-wtf
文档:https://flask-wtf.readthedocs.io/en/latest/

Flask-CKEditor(0.4.0)

主页:https://github.com/greyli/flask-ckeditor
文档:https://flask-ckeditor.readthedocs.io/

与数据库的交互

xxx

电子邮件

xxx

第二部分 实战篇

个人博客程序:Flask蓝图与工厂函数创建实例

clone项目地址:

git clone https://github.com/greyli/bluelog.git 

使用蓝图模块化程序

bluelog
├── __init__.py
├── blueprints
│   ├── __init__.py
│   ├── admin.py
│   ├── auth.py
│   └── blog.py
└── extensions.py
└── settings.py

实例化Flask提供的Blueprint类就创建一个蓝本实例。像程序实例一样,我们可以为蓝本实例注册路由、错误处理函数、上下文处理函数,请求处理函数,甚至是单独的静态文件文件夹和模板文件夹。在使用上,它和程序实例也很相似。比如,蓝本实例同样拥有一个route()装饰器,可以用来注册路由,但实际上蓝本对象和程序对象却有很大的不同。

在实例化Blueprint类时,除了传入构造函数的第一个参数是蓝本名称之外,创建蓝本实例和使用Flask对象创建程序实例的代码基本相同。例如,下面的代码创建了一个blog蓝本:

from flask import Blueprint

blog = Blueprint("blog",__name__)

使用蓝本不仅仅是对视图函数分类,而是将程序某一部分的所有操作组织在一起。这个蓝本实例以及一系列注册在蓝本实例上的操作的集合被称为一个蓝本。你可以把蓝本想象成模子,它描述了程序某一部分的细节,定义了相应的路由错误处理器上下文处理器请求处理器等一系列操作。它本身却不能发挥作用,因为它只是一个模子。只有当你把它注册到程序上时,它才会把物体相应的部分印刻出来——把蓝本中的操作附加到程序上。

使用蓝本可以将程序模块化(modular)。一个程序可以注册多个蓝本,我们可以把程序按照功能分离成不同的组件,然后使用蓝本来组织这些组件。蓝本不仅仅是在代码层面上的组织程序,还可以在程序层面上定义属性,具体的形式即为蓝本下的所有路由设置不同的URL前缀或子域名。

举一个常见的例子,为了让移动设备拥有更好的体验,我们为移动设备创建了单独的视图函数,这部分视图函数可以使用单独的mobile蓝本注册,然后为这个蓝本设置子域名m。用户访问m.example.com的请求会自动被该蓝本的视图函数处理。

创建蓝图

蓝本一般在子包中创建,比如创建一个blog子包,然后在构造文件中创建蓝本实例,使用包管理蓝本允许你设置蓝本独有的静态文件和模板,并在蓝本内对各类函数分模块存储,在简单的程序中,我们也可以直接在模块中创建蓝本实例。

根据程序的功能,我们分别创建了三个脚本:auth.py(用户认证)、blog.py(博客前台)、admin.py(博客后台),分别存储各自蓝本的代码。以auth.py为例,蓝本实例auth_bp在auth.py脚本顶端创建:

form flask import Bluepring

auth_bp = Blueprint("auth",__name__)

在上面的代码中,我们从Flask导入Blueprint类,实例化这个类就获得了我们的蓝本对象。构造方法中的第一个参数是蓝本的名称;第二个参数是包或模块的名称,我们可以使用__name__变量。Blueprint类的构造函数还接收其他参数来定义蓝本,我们会在后面进行介绍。

装配蓝图

蓝本实例是一个用于注册路由等操作的临时对象。这一节我们会了解在蓝本上可以注册哪些操作,以及其中的一些细节。

提示

我们在下面介绍的方法和属性都是在表示蓝本的Blueprint类中定义的,因此可以通过我们的蓝本实例调用,在提及这些方法和属性时,我们会省略掉类名称,比如Blueprint. route()会写为route()。

(1) 视图函数

蓝本中的视图函数通过蓝本实例提供的route()装饰器注册,即auth_bp.route()。我们把和认证相关的视图函数移动到这个模块,然后注册到auth蓝本上,如下所示:

form flask import Bluepring

auth_bp = Blueprint("auth",__name__)

@auth_bp.route("/login")
def login():
    pass

(2) 错误处理函数

使用蓝本实例的errorhandler()装饰器可以把错误处理器注册到蓝本上,这些错误处理器只会捕捉访问该蓝本中的路由发生的错误;使用蓝本实例的app_errorhandler()装饰器则可以注册一个全局的错误处理器。

404和405错误仅会被全局的错误处理函数捕捉,如果你想区分蓝本URL下的404和405错误,可以在全局定义的404错误处理函数中使用request.path.startswith('<蓝本的URL前缀>')来判断请求的URL是否属于某个蓝本。下面我们会介绍如何为蓝本设置URL前缀。

(3) 请求处理函数

在蓝本中,使用before_request、after_request、teardown_request等装饰器注册的请求处理函数是蓝本独有的,也就是说,只有该蓝本中的视图函数对应的请求才会触发相应的请求处理函数。另外,在蓝本中也可以使用before_app_request、after_app_request、teardown_app_request、before_app_first_request方法,这些方法注册的请求处理函数是全局的。

(4) 模板的上下文处理函数

和请求钩子类似,蓝本实例可以使用context_processor装饰器注册蓝本特有的模板上下文处理器;使用app_context_processor装饰器则会注册程序全局的模板上下文处理器。另外,蓝本对象也可以使用app_template_global()、app_template_filter()和app_template_test()装饰器,分别用来注册全局的模板全局函数、模板过滤器和模板测试器。

并不是所有程序实例提供的方法和属性都可以在蓝本对象上调用,蓝本对象只提供了少量用于注册处理函数的方法,大部分的属性和方法我们仍然需要通过程序实例获取,比如表示配置的config属性,或是注册自定义命令的cli.command()装饰器。

注册蓝图

我们在本章开始曾把蓝本比喻成模子,为了让这些模子发挥作用,我们需要把蓝本注册到程序实例上:

from blueblog.blueprints.auth import auth_bp

...
app.register_blueprint(auth_bp)

蓝本使用Flask.register_blueprint()方法注册,必须传入的参数是我们在上面创建的蓝本对象。其他的参数可以用来控制蓝本的行为。比如,我们使用url_prefix参数为auth蓝本下的所有视图URL附加一个URL前缀:

app.register_blueprint(auth_bp,url_prefix="/auth")

这时,auth蓝本下的视图的URL前都会添加一个/auth前缀,比如login视图的URL规则会变为/auth/login。使用subdomain参数可以为蓝本下的路由设置子域名。比如,下面蓝本中的所有视图会匹配来自auth子域的请求:

app.register_blueprint(auth_bp,subdomin="auth")

蓝图的路由端点

端点作为URL规则和视图函数的中间媒介,是我们第1章介绍url_for()函数时提及的概念。下面先来深入了解一下端点,我们使用app.route()装饰器将视图函数注册为路由:

@app.route("/hello")
def hello():
    return "Hello!"

如果你没有接触过装饰器,可能会感到很神秘,其实它只是一层包装而已。如果不用app.route()装饰器,使用app.add_url_rule()方法同样也可以注册路由:

def hello():
    return "Hello!"
    
app.add_url_rule("/hello","say_hello",hello)

add_url_rule(rule, endpoint, view_func)的第二个参数即指定的端点(endpoint),第三个参数是视图函数对象。在路由里,URL规则和视图函数并不是直接映射的,而是通过端点作为中间媒介。:

/hello(URL规则) -> "say_hello"(端点) -> hello(视图函数)

默认情况下,端点是视图函数的名称,在这里即"hello"。我们也可以显式地使用endpoint参数改变它:

@app.route("/hello",endpoint="say_hello")
def hello():
    return "Hello!"

现在使用flask routes命令查看当前程序注册的所有路由:

$ flask routes

Endpoint          Methods    Rule
----------------  ---------  ---------------------------------
auth.login        GET, POST  /auth/login
auth.logout       GET        /auth/logout
blog.about        GET        /about
blog.category     GET        /category/<int:category_id>
bootstrap.static  GET        /static/bootstrap/<path:filename>
ckeditor.static   GET        /ckeditor/static/<path:filename>
static            GET        /static/<path:filename>

从上面的输出可以看到,每个路由的URL规则(Rule)对应的端点(Endpoint)值不再仅仅是视图函数名,而是“蓝本名.视图函数名”的形式(这里的蓝本名即我们实例化Blueprint类时传入的第一个参数)。前面我们留下了一个疑问:为什么不直接映射URL规则到视图函数呢?现在是揭晓答案的时候了。答案就是——使用端点可以实现蓝本的视图函数命名空间。

当使用蓝本时,你可能会在不同的蓝本中创建同名的视图函数。比如,在两个蓝本中都有一个index视图,这时在模板中使用url_for()获取URL时,因为填入的端点参数值是视图函数的名称,就会产生冲突。Flask在端点前添加蓝本的名称,扩展了端点的命名空间,解决了视图函数重名的问题。正因为这样,一旦使用蓝本,我们就要对程序中所有url_for()函数中的端点值进行修改,添加蓝本名来明确端点的归属。比如,在生成auth蓝本下的login视图的URL时,需要使用下面的端点:

url_for("auth.login")

端点也有一种简写的方式,在蓝本内部可以使用“.视图函数名”的形式来省略蓝本名称,比如“auth.login”可以简写为“.login”。但是在全局环境中,比如在多个蓝本中都要使用的基模板,或是在A蓝本中的脚本或渲染的模板中需要生成B蓝本的URL,这时的端点值则必须使用完整的名称。

使用蓝本可以避免端点值的重复冲突,但是路由的URL规则还是会产生重复。比如,两个蓝本中的主页视图的URL规则都是‘/home’,当在浏览器中访问这个地址时,请求只会分配到第一个被注册的蓝本中的主页视图。为了避免这种情况,可以在注册蓝本时使用关键字参数url_prefix在蓝本的URL规则前添加一个URL前缀来解决。

提示

一个蓝本可以注册多次。有时你需要让程序在不同的URL规则下都可以访问,这时就可以为同一个蓝本注册多次,每次设置对应的URL前缀或子域名。

蓝图资源

有时,你引入蓝本的唯一目的就是用来提供资源文件。

这里省略,因为前后端分离项目基本用不到这些。

使用类组织配置

在实际需求中,我们往往需要不同的配置组合。例如,开发用的配置,测试用的配置,生产环境用的配置。为了能方便地在这些配置中切换,你可以像本章开始介绍的那样把配置文件升级为包,然后为这些使用场景分别创建不同的配置文件,但是最方便的做法是在单个配置文件中使用Python类来组织多个不同类别的配置。

代码清单是Bluelog的配置文件,现在它包含一个基本配置类(Base Config),还有其他特定的配置类,即测试配置类(Testing Config)、开发配置类(Development Config)和生产配置类(Production Config),这些特定配置类都继承自基本配置类。

import os
import sys

basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))

# SQLite URI compatible
WIN = sys.platform.startswith('win')
if WIN:
    prefix = 'sqlite:///'
else:
    prefix = 'sqlite:////'


class BaseConfig(object):
    SECRET_KEY = os.getenv('SECRET_KEY', 'dev key')

    SQLALCHEMY_TRACK_MODIFICATIONS = False
    MAIL_SERVER = os.getenv('MAIL_SERVER')
    MAIL_PORT = 465
    MAIL_USE_SSL = True
    MAIL_USERNAME = os.getenv('MAIL_USERNAME')
    MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
    MAIL_DEFAULT_SENDER = ('Bluelog Admin', MAIL_USERNAME)

    BLUELOG_EMAIL = os.getenv('BLUELOG_EMAIL')
    BLUELOG_POST_PER_PAGE = 10
    BLUELOG_MANAGE_POST_PER_PAGE = 15
    BLUELOG_COMMENT_PER_PAGE = 15


class DevelopmentConfig(BaseConfig):
    SQLALCHEMY_DATABASE_URI = prefix + os.path.join(basedir, 'data-dev.db')


class TestingConfig(BaseConfig):
    TESTING = True
    WTF_CSRF_ENABLED = False
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'  # in-memory database


class ProductionConfig(BaseConfig):
    SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', prefix + os.path.join(basedir, 'data.db'))

config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig
}

在新版本的配置中,我们为不同的使用场景设置了不同的数据库URL,避免互相干扰。生产环境下优先从环境变量DATABASE_URL读取,如果没有获取到则使用SQLite,文件名为data.db(在实际生产中我们通常会使用更健壮的DBMS,这里只是示例),在开发时用的数据库文件名为data-dev.db,而测试时的配置则使用SQLite内存型数据库。为了获取数据库文件的路径,我们使用os模块的方法创建了一个定位到项目根目录的basedir变量,最终的绝对路径通过os.path模块的方法基于当前脚本的特殊变量__file__获取。

在配置文件的底部,我们创建了一个存储配置名称和对应配置类的字典,用于在创建程序实例时通过配置名称来获取对应的配置类。现在我们在创建程序实例后使用app.config.from_object()方法加载配置,传入配置类:

import os
from flask import Flask
from bluelog.settings import config

app = Flask(__name__)
comfig_name = os.getenv("FLASK_CONFIG","development")
# app.config.from_object
app.config.from_object(config[config_name])

我们首先从配置文件中导入匹配配置名到配置类的config字典。为了方便修改配置类型,配置名称会先从环境变量FLASK_CONFIG中导入,从环境变量加载配置可以方便地在不改动代码的情况下切换配置。这个值可以在.flaskenv文件中设置,如果没有获取到,则使用默认值development,对应的配置类即Development Config。

提示

Flask并不限制你存储和加载配置的方式,可以使用JSON文件存储配置,然后使用app.config.from_json()方法导入;也可以使用INI风格的配置文件,然后自己手动导入。

使用工厂函数创建程序实例

使用蓝本还有一个重要的好处,那就是允许使用工厂函数来创建程序实例。在OOP (Object-Oriented Programming,面向对象编程)中,工厂(factory)是指创建其他对象的对象,通常是一个返回其他类的对象的函数或方法。

在Bluelog程序的新版本中,程序实例在工厂函数中创建,这个函数返回程序实例app。按照惯例,这个函数被命名为create_app()或make_app()。我们把这个工厂函数称为程序工厂(Application Factory)——即“生产程序的工厂”,使用它可以在任何地方创建程序实例。

工厂函数使得测试和部署更加方便。我们不必将加载的配置写死在某处,而是直接在不同的地方按照需要的配置创建程序实例。通过支持创建多个程序实例,工厂函数提供了很大的灵活性。另外,借助工厂函数,我们还可以分离扩展的初始化操作。创建扩展对象的操作可以分离到单独的模块,这样可以有效减少循环依赖的发生。Bluelog程序的工厂函数如代码清单所示(blueblog/__init__.py)

import os

import click
from flask import Flask, render_template

from bluelog.blueprints.admin import admin_bp
from bluelog.blueprints.auth import auth_bp
from bluelog.blueprints.blog import blog_bp
from bluelog.extensions import bootstrap, db, ckeditor, mail, moment
from bluelog.settings import config

basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))


def create_app(config_name=None):
    if config_name is None:
        config_name = os.getenv('FLASK_CONFIG', 'development')
    # 注意这里得指定名称
    app = Flask('bluelog')
    app.config.from_object(config[config_name])
    # 注册扩展
    register_logging(app)
    register_extensions(app)
    # 注册蓝图
    register_blueprints(app)
    # 注册其他
    register_commands(app)
    register_errors(app)
    register_shell_context(app)
    register_template_context(app)
    return app


def register_logging(app):
    pass

# 注册扩展
def register_extensions(app):
    bootstrap.init_app(app)
    db.init_app(app)
    ckeditor.init_app(app)
    mail.init_app(app)
    moment.init_app(app)


def register_blueprints(app):
    app.register_blueprint(blog_bp)
    app.register_blueprint(admin_bp, url_prefix='/admin')
    app.register_blueprint(auth_bp, url_prefix='/auth')


def register_shell_context(app):
    @app.shell_context_processor
    def make_shell_context():
        return dict(db=db)


def register_template_context(app):
    pass


def register_errors(app):
    @app.errorhandler(400)
    def bad_request(e):
        return render_template('errors/400.html'), 400

    @app.errorhandler(404)
    def page_not_found(e):
        return render_template('errors/404.html'), 404

    @app.errorhandler(500)
    def internal_server_error(e):
        return render_template('errors/500.html'), 500


def register_commands(app):
    @app.cli.command()
    @click.option('--drop', is_flag=True, help='Create after drop.')
    def initdb(drop):
        """Initialize the database."""
        if drop:
            click.confirm('This operation will delete the database, do you want to continue?', abort=True)
            db.drop_all()
            click.echo('Drop tables.')
        db.create_all()
        click.echo('Initialized database.')

工厂函数接收配置名作为参数,返回创建好的程序实例。如果没有传入配置名,我们会从FLASK_CONFIG环境变量获取,如果没有获取到则使用默认值development。在这个工厂函数中,我们会创建程序实例,然后为其加载配置,注册我们在前面创建的三个蓝本,最后返回程序实例。

1. 加载配置

工厂函数接收配置名称作为参数,这允许我们在程序的不同位置传入不同的配置来创建程序实例。比如,使用工厂函数后,我们可以在测试脚本中使用测试配置来调用工厂函数,创建一个单独用于测试的程序实例,而不用从某个模块导入程序实例。

2. 初始化扩展

为了完成扩展的初始化操作,我们需要在实例化扩展类时传入程序实例。但使用工厂函数时,并没有一个创建好的程序实例可以导入。如果我们把实例化操作放到工厂函数中,那么我们就没有一个全局的扩展对象可以使用,比如表示数据库的db对象。

为了解决这个问题,大部分扩展都提供了一个init_app()方法来支持分离扩展的实例化和初始化操作。现在我们仍然像往常一样初始化扩展类,但是并不传入程序实例。这时扩展类实例化的工作可以集中放到bluelog/extensions.py脚本中:

from flask_bootstrap import Bootstrap
from flask_ckeditor import CKEditor
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy

bootstrap = Bootstrap()
db = SQLAlchemy()
ckeditor = CKEditor()
mail = Mail()
moment = Moment()

现在,当我们需要在程序中使用扩展对象时,直接从这个extensions模块导入即可。在工厂函数中,我们导入所有扩展对象,并对其调用init_app()方法,传入程序实例完成初始化操作!详细代码见create_app那里。

3. 组织工厂函数

除了扩展初始化操作,还有很多处理函数要注册到程序上,比如错误处理函数、上下文处理函数等。虽然蓝本也可以注册全局的处理函数,但是为了便于管理,除了蓝本特定的处理函数,这些处理函数一般都放到工厂函数中注册。

为了避免把工厂函数弄得太长太复杂,我们可以根据类别把这些代码分离成多个函数,这些函数接收程序实例app作为参数,分别用来为程序实例初始化扩展、注册蓝本、注册错误处理函数、注册上下文处理函数等一系列操作 ——就像create_app展示的那样。

现在,当工厂函数被调用后。首先创建一个特定配置类的程序实例,然后执行一系列注册函数为程序实例注册扩展、蓝本、错误处理器、上下文处理器、请求处理器……在这个程序工厂的加工流水线的尽头,我们可以得到一个包含所有基本组件的可以直接运行的程序实例。

在使用工厂函数时,因为扩展初始化操作分离,db.create_all()将依赖于程序上下文才能正常执行。执行flask shell命令启动的Python Shell会自动激活程序上下文,Flask命令也会默认在程序上下文环境下执行,所以目前程序中的db.create_all()方法可以被正确执行。当在其他脚本中直接调用db.create_all(),或是在普通的Python Shell中调用时,则需要手动激活程序上下文,具体可参考第2章内容,我们会在第12章详细介绍。

4. 启动程序

当使用flask run命令启动程序时,Flask的自动发现程序实例机制还包含另一种行为:Flask会自动从环境变量FLASK_APP的值定义的模块中寻找名为create_app()或make_app()的工厂函数,自动调用工厂函数创建程序实例并运行。因为我们已经在.flaskenv文件中将FLASK_APP设为bluelog,所以不需要更改任何设置,继续使用flask run命令即可运行程序:

flask run

如果你想设置特定的配置名称,最简单的方式是通过环境变量FLASK_CONFIG设置。另外,你也可以使用FLASK_APP显式地指定工厂函数并传入参数:

FLASK_APP="bluelog:create_app('development')"

为了支持Flask自动从FLASK_APP环境变量对应值指向的模块或包中发现工厂函数,工厂函数中接收的参数必须是默认参数,即设置了默认值的参数,比如“config_name=None”。

5. current_app

使用工厂函数后,我们会遇到一个问题:对于蓝本实例没有提供,程序实例独有的属性和方法应该如何调用呢(比如获取配置的app.config属性)?考虑下面的因素:

  • 使用工厂函数创建程序实例后,在其他模块中并没有一个创建好的程序实例可以让我们导入使用。
  • 使用工厂函数后,程序实例可以在任何地方被创建。你不能固定导入某一个程序实例,因为不同程序实例可能加载不同的配置变量。

解决方法是使用current_app对象,它是一个表示当前程序实例的代理对象。当某个程序实例被创建并运行时,它会自动指向当前运行的程序实例,并把所有操作都转发到当前的程序实例。比如,当我们需要获取配置值时,会使用current_app.config,其他方法和属性亦同。

todoism程序:Flask API

本文也介绍了Flask的国际化与本地化,平时用的较少,有兴趣的话可以看看本章的那部分

关于Web API与web程序

clone项目地址:

git clone https://github.com/greyli/todoism.git

使用jinja2作为前端模板的前后端不分离的程序都是web程序。Web程序提供了完整的交互流程,访问某个URL,服务器返回指定的资源(以HTML的格式),浏览器接收响应并显示设计好的HTML页面,页面上的按钮和链接又指向其他资源。而另外一种形式是,当我们访问某个资源,服务器返回的不是HTML,而是使用特定格式表示的纯数据。没有按钮,没有表单,只有数据。与Web程序相对,这种形式被称为Web API或是Web服务。与Web程序不同,Web API提供的资源主要用于机器处理,所以一般使用JSON、XML等格式以提高重用性。这类API也因此被称为JSONover HTTP或XML over HTTP。

附注

在Web中,资源(Resource)就是URL指向的目标,可以在Web中定位的对象,比如一个文件、一张图片等。在Web API语境中,我们用它来表示可以通过URL获取的数据信息。

Web API的现状

随着Web API的发展,Web世界也变得更加丰富和繁荣。借助Web API,不同的程序可以通过其他在线服务提供的Web API来集成功能。比如在阅读和资讯程序中集成第三方分享,使用社交网站的Web API来集成第三方登录功能,使用Pay Pal、支付宝、Stripe等服务的Web API提供支付功能。

为什么编写Web API

对于我们的程序来说,为什么要提供Web API呢?假设我们做了一个优秀的Web程序,用户疯狂增长,编写Android和i OS客户端的计划很快就要被排上日程了。那么,我们如何让这些客户端都能和数据库进行数据交换操作呢?这时我们需要有一个中间人专门处理数据的传递工作,这个中间人就是Web API。

同时,随着各种优秀Java Script框架的流行,比如Angular、React、Ember、Backbone、Vue.js等,借助这些框架,我们可以直接在客户端实现路由处理(routing)、模板渲染(templating)、表单验证等功能,从而编写出交互性良好的现代Web应用,这时服务器仅需要提供数据操作功能。如果你想使用这些框架编写程序客户端,那么我们就要先编写Web API。

现在,几乎所有成功的在线服务和网站,都将自己的服务以Web API的形式开放出来。开放WebAPI可以带来潜在的价值和影响力。其他用户使用你的Web API开发的其他应用,也会间接为你的产品做广告。在这一节,我们将学习使用Flask为Todoism编写Web API。这样,我们就可以轻松地为其编写桌面应用或移动应用。

在现在的公司中,开发大型程序往往由两个团队负责,分别为前端和后端。这时后端开发者负责开发程序基础功能并以Web API的形式开放这些功能;前端开发者(广义的前端也包括Android、iOS等客户端)负责编写页面逻辑,处理用户交互(HTML/CSS/Java Script)。如果后端能提供Web API,那么前后端就可以完全做到并行开发,后端不用考虑页面交互,而前端可以通过Mock测试来(使用虚拟数据)模拟后端。这样可以在一定程度上提高开发效率。

REST与Web API

使用URL定义资源

资源是Web API的核心,这里共有两种资源:单个资源,比如一篇文章,一条评论;集合资源,比如某用户的所有文章,或是某篇文章下的所有评论。每一个资源都使用一个独一无二的URL表示,URL的设计应该遵循下列要求。

  • 尽量保持简短易懂
  • 避免暴露服务器端架构
  • 使用类似文件系统的层级结构

在Web API的语境中,表示资源的URL也被称为端点或API端点。假设我们在api. example.com上为一个博客程序编写了Web API,那么博客中的各类资源与其端点将会是这样:

  • api.example.com/users:所有用户。
  • api.example.com/users/123/:id为123的用户。
  • api.example.com/users/123/posts:id为33的用户的所有文章。
  • api.example.com/posts:所有文章。
  • api.example.com/posts/23:id为23的文章。
  • api.example.com/posts/23/comments:id为23的文章的所有评论。
使用HTTP方法描述操作

既然有资源,我们就需要对资源进行常见的操作,比如创建、读取、更新、删除(CRUD)。对同一个资源的不同操作可以使用不同的HTTP方法来表示。比如,向api.example.com/posts/23发送GET请求就代表要获取这篇文章的数据,而向这个URL发送DELETE请求则表示要删除这个资源。
API中常用的HTTP方法与对应URL的关系如表:
00a2917a2c20862b8d571ad229e836a6.png

HTTP方法的响应内容:
ef1e57b45e63a3a3fdbee8bfcd450417.png

使用JSON交换数据

JSON已经取代XML成为了API的标准数据格式。大多数在线服务都使用JSON作为数据格式。

设置API版本

Web API和程序一样,都需要在完成后进行维护和更新。当程序的Web版本需要更新时,因为客户端是浏览器,每次请求都会重载页面,所以更新一般都可以立即生效。

而如果是其他安装在用户设备上的专用客户端,比如桌面软件或是移动软件,更新就不会那么简单了。虽然你可以通过添加没有取消按钮的弹窗来强迫用户更新,但这并不是个友好的做法。当打算对API进行更新时,我们就不得不考虑还有大量的用户使用的客户端依赖于旧版本的API。如果我们贸然更新,那么这些用户的客户端很可能会无法正常工作。为了解决这个问题,我们需要保留旧版本的API,创建一个新版本。

为了同时提供多个版本的API,较为常见的做法是在API的URL中指定版本:

这在Flask中很容易实现。借助Flask的蓝本特性,我们可以为不同的API版本设置蓝本,并添加URL前缀。

还有一个更简洁的方法,就是直接在子域中指定:

后面有介绍在Flask中如何设置子域名,但是这种方式实际上并不常用!有用到的话再来这里学一下!

提示
除了这两种方式,还有一种在报文首部里设置版本信息的方式,不过并不常用。

使用Flask编写Web API

创建API蓝图

为了同时支持多个版本,我们在程序包中添加一个apis子包,用来存储API相关的脚本。我们再在apis包中创建子包来表示API的某个版本(v1表示version1.0,即初始版本),每个版本使用独立的蓝本表示。当需要创建新版本时,只需要新建一个子包及蓝本即可。目录结构如下:

todoism/apis
├── __init__.py
└── v1
    ├── __init__.py # v1包的初始化文件
    ├── auth.py
    ├── errors.py
    ├── resources.py
    └── schemas.py

因为我们的程序比较简单,所以所有表示资源的视图都存放在resources模块中。对于大型程序来说,我们可以把resources模块转换为包,然后将程序的资源视图按照类别分成多个模块,比如users.py、items.py等。

初始版本的API蓝本在v1子包的构造文件(__init__.py)中创建,如下所示:

from flask import Bluepring

api_v1 = Blueprint("api_v1",__name__)

from todoism.apis.v1 import resources

为了避免多个API版本的蓝本名称发生冲突,我们将蓝本名称以及Blueprint实例命名为api_v1。

为了避免产生导入循环依赖,我们在脚本末尾导入resources模块,以便让蓝本和对应的视图关联起来。

另外,我们还要在程序包(todoism/_init_.py)的构造文件中将这个蓝本注册到程序实例上:

from todoism.apis.v1 import api_v1

def create_app():
    ...
    register_blueprints(app)
    
def regidter_blueprint(app):
    ...
    app.register_blueprint(api_v1,url_prefix="/api/v1")

在resgister_blueprint()函数中,我们使用url_prefix参数为蓝本设置URL前缀。你也可以为API蓝本设置子域,下面有介绍(但实际中用的少,就不做笔记了)

取消CSRF保护

Web API中的视图并不需要使用CSRF防护,因为Web API并不使用cookie认证用户。我们可以使用csrf.exempt()方法来取消对API蓝本的CSRF保护,它接收蓝本对象作为参数,在todoism/_init_.py中的注册扩展的函数中加上相关代码:

from todoism.apis.v1 import api_v1

...
def register_extensions(app):
    db.init_app(app)
    # login_manager扩展
    login_manager.init_app(app)
    # csrf扩展
    csrf.init_app(app)
    csrf.exempt(api_v1)
    # babel扩展
    babel.init_app(app)

在这个程序中,我们把API的代码作为一个蓝本集成到程序中。作为替代,你也可以只创建API,这样就不用再考虑Flask-WTF的CSRF保护问题。在这种情况下,后端(back-end)和前端(front-end)可以分为两个独立的程序,两者借助HTTP通过API进行数据交换。

设置子域

实际中很少用,略。

添加CORS支持

在介绍CORS(Cross Origin Resource Sharing,跨域资源共享)之前,我们需要先了解一下同源策略(Same origin policy)。出于安全考虑,浏览器会限制从脚本内发起的跨域请求。这里的跨域包括不同域名、不同端口、不同HTTP模式(HTTP、HTTPS等)。比如,从exampleA.com向example B.com发起的请求就属于跨域请求。

当API蓝本设置了子域后,假设我们的Web API部署在api.example.com中,而程序部署在www.example.com中,这时从www.example.com向API发起的AJAX请求就会因为同源策略而失败。对于向第三方大范围公开的API,更要考虑支持CORS。

在CORS流行之前,大多数API都通过支持JSONP(JSON with Padding)来支持跨域请求。和JSONP相比,CORS更加方便灵活,支持更多的跨域请求方法,并且在2014年成为W3C的推荐标准,逐渐开始替代JSONP。

首先需要安装flask-cors:

pip3 install flask-cors -i https://pypi.douban.com/simple

因为我们只需要对API蓝本中的路由添加跨域请求支持,所以Flask-CORS扩展只在蓝本中初始化,传入蓝本对象作为参数:

from flask import Blueprint
from flask_cors import CORS

api_v1 = Blueprint('api_v1', __name__)

CORS(api_v1)

from todoism.apis.v1 import resources

默认情况下,Flask-CORS会为蓝本下的所有路由添加跨域请求支持,并且允许来自任意源的跨域请求。

设计资源端点

b3b60edd17acf8e027d99f657848bfcd.png

提示
因为Todoism属于私人在线应用,所有资源都只有当前用户可以获取,除了用于创建用户的users端点,其他URL都从表示当前用户的user开头。

创建资源类

在Flask中,资源端点可以使用普通的视图函数来表示,通过为同一个URL定义不同的方法实现,比如:

# 在resources.py中配置
@app_v1.route("/items/<int:id>",methods=["GET"])
def get_post(id):
    pass
    
@app_v1.route("/items/<int:id>",methods=["DELETE"])
def del_post(id):
    pass

对于简单的程序,使用这种方式就足够了。不过,Flask提供了使用Python类来组织视图函数的支持,其中的方法视图(MethodView类)可以让Web API的编写更加方便,并且让资源的表示更加直观。借助方法视图,我们可以定义一个继承自MethodView的资源类,整个类表示一个资源端点。我们使用资源端点支持的HTTP方法作为类方法名,它会处理对应类型的请求。比如,当客户端向/items/int:id发起一个GET请求时,资源类中的get()方法将会被调用:

# 在resources.py中配置
from flask.views import MethodView

class Item(MethodView):
    def get(self,item_id):
        pass
        
    def delete(self,item_id):
        pass

在使用方法视图时,除了定义资源类,我们还需要使用add_url_rule()方法来注册路由:

app.add_url_rule("/item/<int:item_id>",view_func=Item.as_view("item_api"),methods=["GET","DELETE"])

因为整个资源类表示实现多个处理方法的视图,我们需要对资源类调用as_view()方法把其转换为视图函数,传入自定义的端点值(用来生成URL),最后将它赋给view_func参数。另外,在methods参数的列表中,我们需要写出所有在资源类中使用的方法。

提示

除了手动使用MethodView实现资源类外,你也可以考虑使用扩展,比如:
Flask-RESTful(https://github.com/flask-restful/flask-restful)
Flask-apispec(https://github.com/jmcarp/flask-apispec)
Flask-Classful(https://github.com/teracyhq/flask-classful)
Flask-RestPlus(https://github.com/noirbizarre/flask-restplus)
flask-Restless(https://github.com/jfinkels/flask-restless)

使用OAuth认证

OAuth认证会在第11章的聊天室程序中讲解。

使用密码认证方式 —— 待续

使用token令牌的方式。

整个流程如下:

67c3a9a2f306dcb5c4974b311b54481e.png

在图10-6中,Resource Server(资源服务器)是提供API资源的服务器,AuthorizationServer(授权服务器)是用于管理授权的服务器;Resource Owner(资源拥有者)即用户,而Client(客户端)指第三方程序。因为我们的程序很简单,所以API服务和授权操作使用同一个服务器提供。

需要自己用代码实现一下

资源的序列化 —— 待续

我们调用Flask提供的jsonify()方法将模式函数返回的字典对象转换为标准的JSON数据,它会为响应报文设置正确的Content-Type字段(即“application/json”)。

资源的反序列化 —— 待续

Web API的测试与发布 —— 待续

第三部分 进阶篇

Flask工作原理与机制解析

本章内容:Flask的上下文是如何实现的?Werkzeug和Flask是什么关系?蓝图到底是什么?

阅读Flask源码

一般来说,阅读源码通常会出于下面的目的:

  • 了解某个功能的具体实现。
  • 学习Flask的设计模式和代码组织方式

通过阅读源码,我们可以在日常开发中更加得心应手,而且在出现错误时可以更好地理解和解决问题。另外,Flask的代码非常Pythonic,而且有丰富的文档字符串,学习和阅读优美的代码也会有助于我们自己编写出优美的代码,而且探索本身也是一种乐趣。

posted on 2020-07-16 23:23  江湖乄夜雨  阅读(557)  评论(0编辑  收藏  举报