Python框架Flask基础学习
2018-08-09 15:25:33

原文地址:http://www.bjhee.com/flask-1.html [toc]

简介

Flask是由python实现的一个web微框架,让我们可以使用Python语言快速实现一个网站或Web服务。而且有对应的python3及python2版本。我这里用的是python3 安装Flask

1
pip install flask

目录结构

通过别人的目录大致了解一下flask框架的目录结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
flask-demo/
run.py # 应用启动程序
├ config.py # 环境配置
├ requirements.txt # 列出应用程序依赖的所有Python包
├ tests/ # 测试代码包
│ ├ __init__.py
│ └ test_*.py # 测试用例
└ myapp/
├ admin/ # 蓝图目录
static/
│ ├ css/ # css文件目录
│ ├ img/ # 图片文件目录
│ └ js/ # js文件目录
├ templates/ # 模板文件目录
├ __init__.py
├ forms.py # 存放所有表单,如果多,将其变为一个包
├ models.py # 存放所有数据模型,如果多,将其变为一个包
└ views.py # 存放所有视图函数,如果多,将其变为一个包

开始 Hello world

1
2
3
4
5
6
7
8
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World'
if __name__ == '__main__':
app.debug = True # 设置调试模式,生产模式的时候要关掉debug
app.run()

这是flask框架制作的一个最小的应用。使用python运行后访问localhost:5000就能看到网页显示Hello world。 这里首先引入了Flask类,然后给这个类创建了一个实例,__name__代表这个模块的名字。因为这个模块是直接被运行的所以此时__name__的值是__main__。然后用route()这个修饰器定义了一个路由,告诉flask如何访问该函数。最后用run()函数使这个应用在服务器上运行起来。

路由

flask通过route()装饰器将一个函数绑定到对应的URL上了。 例如:

1
2
3
4
5
6
@app.route('/')
def index():
return 'Hello World!'
@app.route('/home')
def index():
return 'This is home page!'

我访问localhost:5000/home,就能在页面看到This is home page!。 flask支持在路由上制定参数及参数类型,通过<variable_name>可以标记变量,这个部分将会作为命名参数传递到你的函数,也可以通过<converter:variable_name>指定一个可选的装饰器:

1
2
3
4
5
6
7
@app.route('/user/<username>')
def show_user_profile(username):
return 'User %s' % username

@app.route('/post/<int:post_id>')
def show_post(post_id):
return 'Post %d' % post_id

这里访问localhost:5000/user/test,会看到User test。 访问localhost:5000/post/1,会看到Post 1,且必须为int型,因为这里限制了参数类型。

类型转换器

作用

缺省

字符型,但不能有斜杠

int:

整型

float:

浮点型

path:

字符型,可有斜杠

HTTP方法

如果需要处理具体的HTTP方法,在Flask中也很容易,使用route装饰器的methods参数设置即可。

1
2
3
4
5
6
7
8
from flask import request

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
do_the_login()
else:
show_the_login_form()

当你请求地址http://localhost:5000/login,”GET”和”POST”请求会返回不同的内容,其他请求方法则会返回405错误。

唯一 URL / 重定向行为

Flask的URL规则是基于Werkzeug的路由模块。模块背后的思想是基于 Apache 以及更早的 HTTP 服务器主张的先例,保证优雅且唯一的 URL。

1
2
3
4
5
6
7
@app.route('/projects/')
def projects():
return 'The project page'

@app.route('/about')
def about():
return 'The about page'

访问第一个路由不带/时,Flask会自动重定向到正确地址。 访问第二个路由时末尾带上/后Flask会直接报404 NOT FOUND错误。

构造URL

Flask提供了url_for()方法来快速获取及构建URL,方法的第一个参数指向函数名(加过@app.route注解的函数),后续的参数对应于要构建的URL变量。

1
2
3
4
url_for('login')    # 返回/login
url_for('login', id='1') # 将id作为URL参数,返回/login?id=1
url_for('hello', name='man') # 适配hello函数的name参数,返回/hello/man
url_for('static', filename='style.css') # 静态文件地址,返回/static/style.css

静态文件

Web程序中常常需要处理静态文件,在Flask中需要使用url_for函数并指定static端点名和文件名。在下面的例子中,实际的文件应放在static/文件夹下。

1
url_for('static', filename='style.css')

Request 对象

1
2
3
4
5
6
7
8
9
10
11
from flask import request

@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
if request.form['user'] == 'admin':
return 'Admin login successfully!'
else:
return 'No such user!'
title = request.args.get('title', 'Default')
return render_template('login.html', title=title)

request.args.get()方法则可以获取Get请求URL中的参数,该函数的第二个参数是默认值,当URL参数不存在时,则返回默认值 templates目录下,添加layout.html文件

1
2
3
4
5
6
7
<!doctype html>
<title>Hello Sample</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
<div class="page">
{% block body %}
{% endblock %}
</div>

login.html

1
2
3
4
5
6
7
{% extends "layout.html" %}
{% block body %}
<form name="login" action="/login" method="post">
Hello {{ title }}, please login by:
<input type="text" name="user" />
</form>
{% endblock %}

图片.png 图片.png

全局对象g

flask.g是Flask一个全局对象,g的作用范围,就在一个请求(也就是一个线程)里,它不能在多个请求间共享。你可以在g对象里保存任何你想保存的内容。

构建响应

我们可以先构建响应对象,设置一些参数(比如响应头)后,再将其返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import request, session, make_response

@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
...
if 'user' in session:
...
else:
title = request.args.get('title', 'Default')
response = make_response(render_template('login.html', title=title), 200)
response.headers['key'] = 'value'
return response

make_response方法就是用来构建response对象的,第二个参数代表响应状态码,缺省就是200。

会话对象session

会话可以用来保存当前请求的一些状态,以便于在请求之前共享信息。

登入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask,request,render_template,session

app=Flask(__name__)

@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
if request.form['user'] == 'admin':
session['user']=request.form['user']
return 'Admin login successfully!'
else:
return 'No such user!'
if 'user' in session:
return 'Hello %s!'%session['user']
else:
title = request.args.get('title', 'Default')
return render_template('login.html', title=title)
app.secret_key='123456'
if __name__=='__main__':
app.run()

使用session时一定要设置一个密钥app.secret_key,密钥要尽量复杂,最好使用一个随机数。

登出

1
2
3
4
5
6
from flask import request, session, redirect, url_for

@app.route('/logout')
def logout():
session.pop('user', None)
return redirect(url_for('login'))

模板简介

模板生成

Flask默认使用Jinja2作为模板,默认情况下,模板文件需要放在templates文件夹下。 使用 Jinja 模板,只需要使用render_template函数并传入模板文件名和参数名即可。

1
2
3
4
5
6
from flask import render_template

@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('hello.html', name=name)

hello()函数并不是直接返回字符串,而是调用了render_template()方法来渲染模板。方法的第一个参数hello.html指向你想渲染的模板名称,第二个参数name是你要传到模板去的变量,变量可以传多个。 相应的模板文件hello.html如下。

1
2
3
4
5
6
7
8
9
<!Doctype html>
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}

变量或表达式由{{ }}修饰,而控制语句由{% %}修饰,其他的代码,就是我们常见的HTML。

模板标签

1
2
3
4
5
6
7
8
9
10
11
代码块需要包含在{% %}块中

{% extends 'layout.html' %}
{% block title %}主页{% endblock %}
{% block body %}

<div class="jumbotron">
<h1>主页</h1>
</div>

{% endblock %}

双大括号中的内容不会被转义,所有内容都会原样输出,它常常和其他辅助函数一起使用。

1
<a class="navbar-brand" href={{ url_for('index') }}>Flask小例子</a>

继承

模板可以继承其他模板,我们可以将布局设置为父模板,让其他模板继承,这样可以非常方便的控制整个程序的外观。 例如这里有一个layout.html模板,它是整个程序的布局文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static',filename='css/bootstrap.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static',filename='css/bootstrap-theme.css') }}"/>

</head>
<body>

<div class="container body-content">
{% block body %}{% endblock %}
</div>

<div class="container footer">
<hr>
<p>这是页脚</p>
</div>

<script src="{{ url_for('static',filename='js/jquery.js') }}"></script>
<script src="{{ url_for('static',filename='js/bootstrap.js') }}"></script>

</body>
</html>

其他模板可以这么写。对比一下面向对象编程的继承概念:

1
2
3
4
5
6
7
8
9
10
{% extends 'layout.html' %}
{% block title %}主页{% endblock %}
{% block body %}

<div class="jumbotron">
<h1>主页</h1>
<p>本项目演示了Flask的简单使用方法,点击导航栏上的菜单条查看具体功能。</p>
</div>

{% endblock %}

控制流

条件判断可以这么写,类似于JSP标签中的Java 代码。

1
2
3
4
5
6
7
8
9
{%_%}中也可以写Python代码

<div class=metanav>
{% if not session.logged_in %}
<a href="{{ url_for('login') }}">log in</a>
{% else %}
<a href="{{ url_for('logout') }}">log out</a>
{% endif %}
</div>

循环,和在Python中遍历差不多。

1
2
3
4
5
6
7
8
9
10
11
12
<tbody>
{% for key,value in data.items() %}
<tr>
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
{% endfor %}
<tr>
<td>文件</td>
<td></td>
</tr>
</tbody>

不是所有的Python代码都可以写在模板里,如果希望从模板中引用其他文件的函数,需要显式将函数注册到模板中。

错误处理

使用abort()函数可以直接退出请求,返回错误代码:

1
2
3
4
5
from flask import abort

@app.route('/error')
def error():
abort(404)

上例会显示浏览器的404错误页面。我们可以在遇到特定错误代码时做些事情,或者重写错误页面

1
2
3
@app.errorhandler(404)
def page_not_found(error):
return render_template('404.html'), 404

此时,当再次遇到404错误时,即会调用page_not_found()函数,其返回”404.html”的模板页。第二个参数代表错误代码。 在实际开发过程中,并不会经常使用abort()来退出,常用的错误处理方法一般都是异常的抛出或捕获。装饰器@app.errorhandler()除了可以注册错误代码外,还可以注册指定的异常类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
class InvalidUsage(Exception):
status_code=400

def __init__ (self,message,status_code=400):
Exception.__init__(self)
self.message=message
self.status_code=status_code

@app.errorhandler(InvalidUsage)
def invalid_usage(error):
response=make_response(error.message)
response.status_code=error.status_code
return response

这里定义了一个异常InvalidUsage,同时我们通过装饰器@app.errorhandler()修饰了函数invalid_usage(),装饰器中注册了我们刚定义的异常类。一但遇到InvalidUsage异常被抛出,这个invalid_usage()函数就会被调用

1
2
3
@app.route('/exception')
def exception():
raise InvalidUsage('No privilege to access the resource', status_code=403)

日志

Flask提供logger对象,其是一个标准的Python Logger类。 修改上例中的exception()函数:

1
2
3
4
5
@app.route('/exception')
def exception():
app.logger.debug('Enter exception method')
app.logger.error('403 error happened')
raise InvalidUsage('No privilege to access the resource', status_code=403)

执行后,会在控制台看到日志信息。在debug模式下,日志会默认输出到标准错误stderr中。 可以添加FileHandler来使其输出到日志文件中去,也可以修改日志的记录格式。 简单的日志配置代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server_log = TimedRotatingFileHandler('server.log','D')
server_log.setLevel(logging.DEBUG)
server_log.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s'
))

error_log = TimedRotatingFileHandler('error.log', 'D')
error_log.setLevel(logging.ERROR)
error_log.setFormatter(logging.Formatter(
'%(asctime)s: %(message)s [in %(pathname)s:%(lineno)d]'
))

app.logger.addHandler(server_log)
app.logger.addHandler(error_log)

在本地目录下创建了两个日志文件,分别是server.log记录所有级别日志;error.log只记录错误日志。 分别给两个文件不同的内容格式。 使用TimedRotatingFileHandler并给了参数D,这样日志每天会创建一个新的文件,并将旧文件加日期后缀来归档。

消息闪现

Flask Message一个操作完成后,在页面上闪出一个消息,告诉用户操作的结果。用户看完后,这个消息就不复存在了。 app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from flask import render_template, request, session, url_for, redirect, flash,Flask
app = Flask(__name__)
@app.route('/')
def index():
if 'user' in session:
return render_template('hello.html', name=session['user'])
else:
return redirect(url_for('login'), 302)

@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
session['user'] = request.form['user']
flash('Login successfully!!!!')
return redirect(url_for('index'))
else:
return '''
<form name="login" action="/login" method="post">
Username: <input type="text" name="user" />
</form>
'''
app.secret_key='123456'
if __name__=='__main__':
app.run()

app.secret_key不赋值会报错。 hello.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!doctype html>
<title>Hello Sample</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class="flash">
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div class="page">
{% block body %}
{% endblock %}
</div>

get_flashed_messages()函数会获取我们在login()中通过flash()闪出的消息。 图片.png 再刷新文字就会消失。 flash()方法的第二个参数是消息类型,可选择的有message, info, warning, error可以在获取消息时,同时获取消息类型,还可以过滤特定的消息类型。

请求上下文环境

请求上下文的生命周期

上下文装饰器@app.before_request@app.teardown_request@app.after_request,用其修饰的函数也可以称为上下文Hook函数。 被before_request修饰的函数会在请求处理之前被调用,after_requestteardown_request会在请求处理完成后被调用。 区别是after_request只会在请求正常退出时才会被调用,它必须传入一个参数来接受响应对象,并返回一个响应对象,一般用来统一修改响应的内容。而teardown_request在任何情况下都会被调用,它必须传入一个参数来接受异常对象,一般用来统一释放请求所占有的资源。同一种类型的Hook函数可以存在多个,程序会按代码中的顺序执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from flask import Flask, g, request

app = Flask(__name__)

@app.before_request
def before_request():
print 'before request started'
print request.url

@app.before_request
def before_request2():
print 'before request started 2'
print request.url
g.name="SampleApp"

@app.after_request
def after_request(response):
print 'after request finished'
print request.url
response.headers['key'] = 'value'
return response

@app.teardown_request
def teardown_request(exception):
print 'teardown request'
print request.url

@app.route('/')
def index():
return 'Hello, %s!' % g.name

if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)

访问http://localhost:5000/后,会在控制台输出: 图片.png 如果一个before_request函数中有返回response,则后面的before_request以及该请求的处理函数将不再被执行。直接进入after_request

1
2
3
4
5
6
7
@app.before_request
def before_request():
print('before request started')
print(request.url)
return 'Hello'
......

图片.png request对象只有在请求上下文的生命周期内才可以访问

构建请求上下文环境

一个请求一般是由客户端发起的,Flask提供了在没有客户端的情况下实现自动测试,可通过test_request_context()来模拟客户端请求。

1
2
3
4
5
6
7
8
from werkzeug.test import EnvironBuilder

ctx = app.request_context(EnvironBuilder('/','http://localhost/').get_environ())
ctx.push()
try:
print request.url
finally:
ctx.pop()

request_context()会创建一个请求上下文RequestContext类型的对象,其需接收werkzeug中的environ对象为参数。werkzeug是Flask所依赖的WSGI函数库。 上面的代码可以用with语句来简化:

1
2
3
4
from werkzeug.test import EnvironBuilder

with app.request_context(EnvironBuilder('/','http://localhost/').get_environ()):
print request.url

请求上下文的实现方式

对于Flask Web应用来说,每个请求就是一个独立的线程。请求之间的信息要完全隔离,避免冲突,这就需要使用本地线程环境(ThreadLocal)。ctx.push()方法,会将当前请求上下文,压入flask._request_ctx_stack的栈中,这个_request_ctx_stack是内部对象,我们在应用开发时最好不要使用它,一般在Flask扩展开发中才会使用。同时这个_request_ctx_stack栈是个ThreadLocal对象。也就是flask._request_ctx_stack看似全局对象,其实每个线程的都不一样。请求上下文压入栈后,再次访问其都会从这个栈的顶端通过_request_ctx_stack.top来获取,所以取到的永远是只属于本线程中的对象,这样不同请求之间的上下文就做到了完全隔离。请求结束后,线程退出,ThreadLocal线程本地变量也随即销毁,ctx.pop()用来将请求上下文从栈里弹出,避免内存无法回收。

应用上下文环境

current_app代理

1
2
3
4
5
6
7
from flask import Flask, current_app

app = Flask('SampleApp')

@app.route('/')
def index():
return 'Hello, %s!' % current_app.name

我们可以通过current_app.name来获取当前应用的名称,也就是SampleAppcurrent_app是一个本地代理,它的类型是werkzeug.local. LocalProxy,它所代理的即是我们的app对象,也就是说current_app == LocalProxy(app)使用current_app是因为它也是一个ThreadLocal变量,对它的改动不会影响到其他线程。 既然是ThreadLocal对象,那它就只在请求线程内存在,它的生命周期就是在应用上下文里。离开了应用上下文,current_app一样无法使用。

1
2
3
4
app = Flask('SampleApp')
print(current_app.name)

RuntimeError: working outside of application context

构建应用上下文环境

1
2
with app.app_context():
print(current_app.name)

app_context()方法会创建一个AppContext类型对象,即应用上下文对象,此后我们就可以在应用上下文中访问current_app对象了。

应用上下文的实现方式

在请求线程创建时,Flask会创建应用上下文对象,并将其压入flask._app_ctx_stack的栈中,然后在线程退出前将其从栈里弹出。这个_app_ctx_stack同请求上下文中的_request_ctx_stack一样,都是ThreadLocal变量。也就是说应用上下文的生命周期,也只在一个请求线程内,我们无法通过应用上下文在请求之间传递信息。

应用上下文Hook函数

应用上下文也提供了装饰器来修饰Hook函数,不过只有一个@app.teardown_appcontext。它会在应用上下文生命周期结束前,也就是从_app_ctx_stack出栈时被调用。

1
2
3
@app.teardown_appcontext
def teardown_db(exception):
print(teardown application)

信号

信号(Signal)就是两个独立的模块用来传递消息的方式,它有一个消息的发送者Sender,还有一个消息的订阅者Subscriber。信号的存在使得模块之间可以摆脱互相调用的模式,也就是解耦合。发送者无需知道谁会接收消息,接收者也可自由选择订阅何种消息。

订阅一个信号

1
2
3
4
5
6
7
from flask import template_rendered, request

def print_template_rendered(sender, template, context, **extra):
print 'Using template: %s with context: %s' % (template.name, context)
print request.url

template_rendered.connect(print_template_rendered, app)

访问http://localhost:5000/hello会在控制台看到

1
2
Using template: hello.html with context: {'session': , 'request': , 'name': None, 'g': }
http://localhost:5000/hello

而访问http://localhost:5000/时,却没有这些信息 flask.template_rendered是Flask的核心信号。当任意一个模板被渲染成功后,这个信号就会被发出。 信号的connect()方法用来连接订阅者,它的第一个参数就是订阅者回调函数,当信号发出时,这个回调函数就会被调用;第二个参数是指定消息的发送者,也就是指明只有app作为发送者发出的template_rendered消息才会被此订阅者接收。 如果不指定发送者,任何发送者发出的”template_rendered”都会被接收。 connect()方法可以多次调用,来连接多个订阅者。 这个回调函数,它有四个参数,前三个参数是必须的。 * sender: 获取消息的发送者 * template: 被渲染的模板对象 * context: 当前请求的上下文环境 * **extra: 匹配任意额外的参数。如果上面三个存在,这个参数不加也没问题。但是如果你的参数少于三个,就一定要有这个参数。一般习惯上加上此参数。 我们在回调函数中,可以获取请求上下文,也就是它在一个请求的生命周期和线程内。所以,我们可以在函数中访问request对象。 Flask同时提供了信号装饰器来简化代码,上面的信号订阅也可以写成:

1
2
3
4
5
6
from flask import template_rendered, request

@template_rendered.connect_via(app)
def with_template_rendered(sender, template, context, **extra):
print 'Using template: %s with context: %s' % (template.name, context)
print request.url

Flask核心信号

信号 作用 回调函数参数 —— —— —– request_started 请求开始时发送 1、sender: 消息的发送者 request_finished 请求结束后发送 1、sender: 消息的发送者 2、response: 待返回的响应对象 got_request_exception请求发生异常时发送1、sender: 消息的发送者 2、exception: 被抛出的异常对象 request_tearing_down 请求被销毁时发送,不管有无异常都会被发送1、sender: 消息的发送者 2、exc: 有异常时,抛出的异常对象 appcontext_tearing_down应用上下文被销毁时发送1、sender: 消息的发送者 appcontext_pushed 应用上下文被压入”_app_ctx_stack”栈后发送1、sender: 消息的发送者 appcontext_popped应用上下文从”_app_ctx_stack”栈中弹出后发送1、sender: 消息的发送者 message_flashed消息闪现时发送1、sender: 消息的发送者 2、message: 被闪现的消息内容 3、category: 被闪现的消息类别 注,所有回调函数都建议加上**extra作为最后的参数

自定义信号

1
2
3
4
from blinker import Namespace

signals = Namespace()
index_called = signals.signal('index-called')

我们在全局定义了一个index_called信号对象,表示根路径被访问了。然后我们在根路径的请求处理中发出这个信号:

1
2
3
4
@app.route('/')
def index():
index_called.send(current_app._get_current_object(), method=request.method)
return 'Hello Flask!'

发送信号消息的方法是send(),它必须包含一个参数指向信号的发送者。current_app._get_current_object()用来获取应用上下文中的app应用对象。这样每次客户端访问根路径时,都会发送index_called信号。

视图装饰器

当用户访问admin页面时,必须先登录。可以在admin()方法里判断会话session。这样的确可以达成目的。不过当我们有n多页面都要进行用户验证的话,判断用户登录的代码就会到处都是。即便我们封装在一个函数里,至少调此函数的代码也会重复出现。Flask没有提供特别的功能来实现这个,因为Python本身有,那就是装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
from functools import wraps
from flask import session, abort

def login_required(func):
@wraps(func)
def decorated_function(*args, **kwargs):
if not 'user' in session:
abort(401)
return func(*args, **kwargs)
return decorated_function

app.secret_key = '12345678'

在调用函数前,先检查session里有没有用户。此后,我们只需将此装饰器加在每个需要验证登录的请求方法上即可:

1
2
3
4
@app.route('/admin')
@login_required
def admin():
return '<h1>Admin Dashboard</h1>'

这个装饰器就被称为视图装饰器(View Decorator)。 我们也可以把页面的路径作为键,页面内容作为值,放在缓存里。每次进入请求函数前,先判断缓存里有没有该页面,有就直接将缓存里的值返回,没有则执行请求函数,将结果存在缓存后再返回。

URL集中映射

Flask也支持像Django一样,把URL路由规则统一管理,而不是写在视图函数上。 views.py

1
2
def foo():
return '<h1>Hello Foo!</h1>'

然后在Flask主程序上调用app.add_url_rule方法:

1
app.add_url_rule('/foo', view_func=views.foo)

这样,路由/foo就绑定在views.foo()函数上了,效果等同于在views.foo()函数上加上@app.route(‘/foo’)装饰器。通过app.add_url_rule方法,我们就可以将路由同视图分开,将路由统一管理,实现完全的MVC。 定义的装饰器本质上是一个闭包函数,所以我们当然可以把它当函数使用:

1
app.add_url_rule('/foo', view_func=login_required(views.foo))

可插拔视图Pluggable View

视图类

URL集中映射,就是视图可插拔的基础,因为它可以支持在程序中动态的绑定路由和视图。Flask提供了视图类,使其可以变得更灵活

1
2
3
4
5
6
7
8
9
from flask.views import View

class HelloView(View):
def dispatch_request(self, name=None):
return render_template('hello-view.html', name=name)

view = HelloView.as_view('helloview')
app.add_url_rule('/helloview', view_func=view)
app.add_url_rule('/helloview/<name>', view_func=view)

创建了一个flask.views.View的子类,并覆盖了其dispatch_request()数,渲染视图的主要代码必须写在这个函数里。然后我们通过as_view()方法把类转换为实际的视图函数,as_view()必须传入一个唯一的视图名。此后,这个视图就可以由app.add_url_rule方法绑定到路由上了。 例如

1
2
3
4
5
6
7
8
9
class RenderTemplateView(View):
def __init__(self, template):
self.template = template

def dispatch_request(self):
return render_template(self.template)

app.add_url_rule('/hello', view_func=RenderTemplateView.as_view('hello', template='hello-view.html'))
app.add_url_rule('/login', view_func=RenderTemplateView.as_view('login', template='login-view.html'))

视图装饰器支持

在使用视图类的情况下,使用视图装饰器

1
2
3
4
5
class HelloView(View):
decorators = [login_required]

def dispatch_request(self, name=None):
return render_template('hello-view.html', name=name)

只需将装饰器函数加入到视图类变量decorators中即可,它是一个列表,所以能够支持多个装饰器,并按列表中的顺序执行。

请求方法的支持

1
2
3
4
5
6
7
8
9
10
class MyMethodView(View):
methods = ['GET', 'POST']

def dispatch_request(self):
if request.method == 'GET':
return '<h1>Hello World!</h1>This is GET method.'
elif request.method == 'POST':
return '<h1>Hello World!</h1>This is POST method.'

app.add_url_rule('/mmview', view_func=MyMethodView.as_view('mmview'))

只需将需要支持的HTTP请求方法加入到视图类变量methods中即可。没加的话,默认只支持GET请求。

基于方法的视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask.views import MethodView

class UserAPI(MethodView):
def get(self, user_id):
if user_id is None:
return 'Get User called, return all users'
else:
return 'Get User called with id %s' % user_id

def post(self):
return 'Post User called'

def put(self, user_id):
return 'Put User called with id %s' % user_id

def delete(self, user_id):
return 'Delete User called with id %s' % user_id

分别定义get, post, put, delete方法来对应四种类型的HTTP请求,注意函数名必须这么写。 绑定到路由

1
2
3
4
5
6
7
8
9
10
11
12
user_view = UserAPI.as_view('users')
# 将GET /users/请求绑定到UserAPI.get()方法上,并将get()方法参数user_id默认为None
app.add_url_rule('/users/', view_func=user_view,
defaults={'user_id': None},
methods=['GET',])
# 将POST /users/请求绑定到UserAPI.post()方法上
app.add_url_rule('/users/', view_func=user_view,
methods=['POST',])
# 将/users/<user_id>URL路径的GET,PUT,DELETE请求,
# 绑定到UserAPI的get(), put(), delete()方法上,并将参数user_id传入。
app.add_url_rule('/users/<user_id>', view_func=user_view,
methods=['GET', 'PUT', 'DELETE'])

app.add_url_rule()可以传入参数default,来设置默认值;参数methods,来指定支持的请求方法。 如果API多,可以将其封装成函数:

1
2
3
4
5
6
7
8
9
10
11
12
def register_api(view, endpoint, url, primary_id='id', id_type='int'):
view_func = view.as_view(endpoint)
app.add_url_rule(url, view_func=view_func,
defaults={primary_id: None},
methods=['GET',])
app.add_url_rule(url, view_func=view_func,
methods=['POST',])
app.add_url_rule('%s<%s:%s>' % (url, id_type, primary_id),
view_func=view_func,
methods=['GET', 'PUT', 'DELETE'])

register_api(UserAPI, 'users', '/users/', primary_id='user_id')

一个register_api()就可以绑定一个API了

文件和流

当我们要往客户端发送大量的数据,比如一个大文件时,将它保存在内存中再一次性发到客户端开销很大。比较好的方式是使用流。

响应流的生成

Flask响应流的实现原理就是通过Python的生成器,也就是yield的表达式,将yield的内容直接发送到客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask, Response

app = Flask(__name__)

@app.route('/large.csv')
def generate_large_csv():
def generate():
for row in range(50000):
line = []
for col in range(500):
line.append(str(col))

if row % 1000 == 0:
print 'row: %d' % row
yield ','.join(line) + '\n'

return Response(generate(), mimetype='text/csv')

这段代码会生成一个5万行100M的csv文件,每一行会通过yield表达式分别发送给客户端。运行时你会发现文件行的生成与浏览器文件的下载是同时进行的,而不是文件全部生成完毕后再开始下载。 这里我们用到了响应类flask.Response,它是werkzeug.wrappers.Response类的一个包装,它的初始化方法第一个参数就是我们定义的生成器函数,第二个参数指定了响应类型。 我们将上述方法应用到模板中,如果模板的内容很大,我们要自己写个流式渲染模板的方法。`

1
2
3
4
5
6
7
8
9
10
11
12
13
# 流式渲染模板
def stream_template(template_name, **context):
# 将app中的请求上下文内容更新至传入的上下文对象context,
# 这样确保请求上下文会传入即将被渲染的模板中
app.update_template_context(context)
# 获取Jinja2的模板对象
template = app.jinja_env.get_template(template_name)
# 获取流式渲染模板的生成器
generator = template.stream(context)
# 启用缓存,这样不会每一条都发送,而是缓存满了再发送
generator.enable_buffering(5)

return generator

现在我们就可以在视图方法中,采用stream_template(),而不是以前介绍的render_template()来渲染模板了:

1
2
3
4
5
@app.route('/stream.html')
def render_large_template():
file = open('server.log')
return Response(stream_template('stream-view.html',
logs=file.readlines()))

上例的代码会将本地的server.log日志文件内容传入模板,并以流的方式渲染在页面上。 另外注意,在生成器中是无法访问请求上下文的。不过Flask从版本0.9开始提供了”stream_with_context()”方法,它允许生成器在运行期间获取请求上下文:

1
2
3
4
5
6
7
8
9
from flask import request, stream_with_context

@app.route('/method')
def streamed_response():
def generate():
yield 'Request method is: '
yield request.method
yield '.'
return Response(stream_with_context(generate()))

因为我们初始化Response对象时调用了stream_with_context()方法,所以才能在yield表达式中访问request对象。

文件上传

首先建立一个让用户上传文件的页面,我们将其放在模板upload.html

1
2
3
4
5
6
7
<!DOCTYPE html>
<title>Upload File</title>
<h1>Upload new File</h1>
<form action="" method="post" enctype="multipart/form-data">
<p><input type="file" name="file">
<input type="submit" value="Upload">
</form>

定义一个文件合法性检查函数

1
2
3
4
5
6
7
8
# 设置允许上传的文件类型
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg'])

# 检查文件类型是否合法
def allowed_file(filename):
# 判断文件的扩展名是否在配置项ALLOWED_EXTENSIONS中
return '.' in filename and \
filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

这样避免用户上传脚本文件如js或php文件,来干坏事! 文件提交后,在POST请求的视图函数中,通过request.files获取文件对象 这个request.files是一个字典,字典的键值就是之前模板中文件选择框的”name”属性的值,上例中是”file”;键值所对应的内容就是上传过来的文件对象。 检查文件对象的合法性后,通过文件对象的save()方法将文件保存在本地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import os
from flask import flask, render_template
from werkzeug import secure_filename

app = Flask(__name__)
# 设置请求内容的大小限制,即限制了上传文件的大小
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024

# 设置上传文件存放的目录
UPLOAD_FOLDER = './uploads'

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
# 获取上传过来的文件对象
file = request.files['file']
# 检查文件对象是否存在,且文件名合法
if file and allowed_file(file.filename):
# 去除文件名中不合法的内容
filename = secure_filename(file.filename)
# 将文件保存在本地UPLOAD_FOLDER目录下
file.save(os.path.join(UPLOAD_FOLDER, filename))
return 'Upload Successfully'
else: # 文件不合法
return 'Upload Failed'
else: # GET方法
return render_template('upload.html')

Flask的MAX_CONTENT_LENGTH配置项可以限制请求内容的大小默认是没有限制,上例中我们设为5M。 必须调用werkzeug.secure_filename()来使文件名安全比如用户上传的文件名为../../../../home/username/.bashrcsecure_filename()方法可以将其转为home_username_.bashrc。还是用来避免用户干坏事! Flask处理文件上传的方式如果上传的文件很小,那么会把它直接存在内存中。否则就会把它保存到一个临时目录下,通过tempfile.gettempdir()方法可以获取这个临时目录的位置。 另外Flask从0.5版本开始提供了一个简便的方法来让用户获取已上传的文件:

1
2
3
4
5
from flask import send_from_directory

@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(UPLOAD_FOLDER, filename)

这个帮助方法send_from_directory()可以安全地将文件发送给客户端,它还可以接受一个参数mimetype来指定文件类型,和参数as_attachment=True来添加响应头Content-Disposition: attachment

Flask Blueprint 蓝图

我们的应用经常会区分用户站点和管理员后台,两者虽然都在同一个应用中,但是风格迥异。把它们分成两个应用的话,总有些代码我们想重用;放在一起,耦合度太高,代码不便于管理。所以Flask提供了蓝图(Blueprint)功能。蓝图使用起来就像应用当中的子应用一样,可以有自己的模板,静态目录,有自己的视图函数和URL规则,蓝图之间互相不影响。但是它们又属于应用中,可以共享应用的配置。对于大型应用来说,我们可以通过添加蓝图来扩展应用功能,而不至于影响原来的程序。不过有一点要注意,目前Flask蓝图的注册是静态的,不支持可插拔。

创建一个蓝图

比较好的习惯是将蓝图放在一个单独的包里,所以让我们先创建一个”admin”子目录,并创建一个空的__init__.py表示它是一个Python的包。现在我们来编写蓝图,将其存在”admin/admin_module.py”文件里:

1
2
3
4
5
6
7
from flask import Blueprint

admin_bp = Blueprint('admin', __name__)

@admin_bp.route('/')
def index(name):
return '<h1>Hello, this is admin blueprint</h1>'

初始化Blueprint对象的第一个参数’admin’指定了这个蓝图的名称,第二个参数指定了该蓝图所在的模块名。 接下来,我们在应用中注册该蓝图。在Flask应用主程序中,使用app.register_blueprint()方法即可:

1
2
3
4
5
6
7
8
from flask import Flask
from admin.admin_module import admin_bp

app = Flask(__name__)
app.register_blueprint(admin_bp, url_prefix='/admin')

if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)

app.register_blueprint()方法的”url_prefix”指定了这个蓝图的URL前缀。现在,访问http://localhost:5000/admin/就可以加载蓝图的index视图了。 也可以在创建蓝图对象时指定其URL前缀:

1
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')

这样注册时就无需指定:

1
app.register_blueprint(admin_bp)

蓝图资源

蓝图有自己的目录,它的所有资源都在其目录下。蓝图的资源目录是由创建Blueprint对象时传入的模块名__name__所在的位置决定的。 同时,我们可以指定蓝图自己的模板目录和静态目录。比如我们创建蓝图时传入

1
2
3
admin_bp = Blueprint('admin', __name__,
template_folder='templates',
static_folder='static')

这样,该蓝图的模板目录就在admin/templates下,而静态目录就在admin/static下。默认值就是这两个位置,不指定也没关系。

构建URL

1
2
3
4
from flask import url_for

url_for('admin.index') # return /admin/
url_for('admin.static', filename='style.css') # return /admin/static/style.css

如果,url_for()函数的调用就在本蓝图下,蓝图名可以省略,但必须留下.表示当前蓝图:

1
2
url_for('.index')
url_for('.static', filename='style.css')

部署和分发

到目前为止,我们启动Flask应用都是通过”app.run()”方法,在开发环境中,这样固然可行,不过到了生产环境上,势必需要采用一个健壮的,功能强大的Web应用服务器来处理各种复杂情形。同时,由于开发过程中,应用变化频繁,手动将每次改动部署到生产环境上很是繁琐,最好有一个自动化的工具来简化持续集成的工作。接下来讲如何将Flask的应用程序自动打包,分发,并部署到像Apache, Nginx等服务器中去。

使用setuptools打包Flask应用

作为Python标准的打包及分发工具,setuptools可以说相当地简单易用。它会随着Python一起安装在你的机器上。你只需写一个简短的setup.py安装文件,就可以将你的Python应用打包。

第一个安装文件

假设我们的项目名为setup-demo,包名为myapp,目录结构如下:

1
2
3
4
5
setup-demo/
├ setup.py # 安装文件
└ myapp/ # 源代码
├ __init__.py
...

一个最基本的setup.py文件如下:

1
2
3
4
5
6
7
8
#coding:utf8
from setuptools import setup

setup(
name='MyApp', # 应用名
version='1.0', # 版本号
packages=['myapp'] # 包括在安装包内的Python包
)
执行安装文件

有了上面的setup.py文件,我们就可以打各种包,也可以将应用安装在本地Python环境中。 创建egg包

1
$ python setup.py bdist_egg

该命令会在当前目录下的”dist”目录内创建一个egg文件,名为”MyApp-1.0-py2.7.egg”。文件名格式就是”应用名-版本号-Python版本.egg”,我本地Python版本是2.7。同时你会注意到,当前目录多了”build”和”MyApp.egg-info”子目录来存放打包的中间结果。 创建tar.gz包

1
$ python setup.py sdist --formats=gztar

同上例类似,只不过创建的文件类型是tar.gz,文件名为”MyApp-1.0.tar.gz”。 安装应用

1
$ python setup.py install

该命令会将当前的Python应用安装到当前Python环境的”site-packages”目录下,这样其他程序就可以像导入标准库一样导入该应用的代码了。 开发方式安装

1
$ python setup.py develop

如果应用在开发过程中会频繁变更,每次安装还需要先将原来的版本卸掉,很麻烦。使用”develop”开发方式安装的话,应用代码不会真的被拷贝到本地Python环境的”site-packages”目录下,而是在”site-packages”目录里创建一个指向当前应用位置的链接。这样如果当前位置的源码被改动,就会马上反映到”site-packages”里。

引入非Python文件

上例中,我们只会将”myapp”包下的源码打包,如果我们还想将其他非Python文件也打包,比如静态文件(JS,CSS,图片)。 这时我们要在项目目录下添加一个”MANIFEST.in”文件夹。假设我们把所有静态文件都放在”static”子目录下,现在的项目结构如下:

1
2
3
4
5
6
7
setup-demo/
├ setup.py # 安装文件
├ MANIFEST.in # 清单文件
└ myapp/ # 源代码
static/ # 静态文件目录
├ __init__.py
...

我们在清单文件”MANIFEST.in”中,列出想要在包内引入的目录路径:

1
2
recursive-include myapp/static *
recursive-include myapp/xxx *

“recursive-include”表明包含子目录。还要在”setup.py”中将” include_package_data”参数设为True:

1
2
3
4
5
6
7
8
9
#coding:utf8
from setuptools import setup

setup(
name='MyApp', # 应用名
version='1.0', # 版本号
packages=['myapp'], # 包括在安装包内的Python包
include_package_data=True # 启用清单文件MANIFEST.in
)

之后再次打包或者安装,”myapp/static”目录下的所有文件都会被包含在内。如果你想排除一部分文件,可以在setup.py中使用”exclude_package_date”参数,比如:

1
2
3
4
5
setup(
...
include_package_data=True, # 启用清单文件MANIFEST.in
exclude_package_date={'':['.gitignore']}
)

上面的代码会将所有”.gitignore”文件排除在包外。如果上述”exclude_package_date”对象属性不为空,比如”{‘myapp’:[‘.gitignore’]}”,就表明只排除”myapp”包下的所有”.gitignore”文件。

自动安装依赖

我们的应用会依赖于第三方的Python包,虽然可以在说明文件中要求用户提前安装依赖包,但毕竟很麻烦,用户还有可能装错版本。其实我们可以在setup.py文件中指定依赖包,然后在使用setuptools安装应用时,依赖包的相应版本就会被自动安装。让我们来修改上例中的setup.py文件,加入”install_requires”参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#coding:utf8
from setuptools import setup

setup(
name='MyApp', # 应用名
version='1.0', # 版本号
packages=['myapp'], # 包括在安装包内的Python包
include_package_data=True, # 启用清单文件MANIFEST.in
exclude_package_date={'':['.gitignore']},
install_requires=[ # 依赖列表
'Flask>=0.10',
'Flask-SQLAlchemy>=1.5,<=2.1'
]
)

setuptools会先检查本地有没有符合要求的依赖包,如果没有的话,就会从PyPI中获得一个符合条件的最新的包安装到本地。 如果应用依赖的包无法从PyPI中获取,我们需要指定其下载路径:

1
2
3
4
5
6
7
8
9
10
setup(
...
install_requires=[ # 依赖列表
'Flask>=0.10',
'Flask-SQLAlchemy>=1.5,<=2.1'
],
dependency_links=[ # 依赖包下载路径
'http://example.com/dependency.tar.gz'
]
)

路径应指向一个egg包或tar.gz包,也可以是个包含下载地址(一个egg包或tar.gz包)的页面。

自动搜索Python包

之前我们在setup.py中指定了”packages=[‘myapp’]”,说明将Python包”myapp”下的源码打包。如果我们的应用很大,Python包很多。setuptools提供了”find_packages()”方法来自动搜索可以引入的Python包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#coding:utf8
from setuptools import setup, find_packages

setup(
name='MyApp', # 应用名
version='1.0', # 版本号
packages=find_packages(), # 包括在安装包内的Python包
include_package_data=True, # 启用清单文件MANIFEST.in
exclude_package_date={'':['.gitignore']},
install_requires=[ # 依赖列表
'Flask>=0.10',
'Flask-SQLAlchemy>=1.5,<=2.1'
]
)

这样当前项目内所有的Python包都会自动被搜索到并引入到打好的包内。”find_packages()”方法可以限定你要搜索的路径,比如使用”find_packages(‘src’)”就表明只在”src”子目录下搜索所有的Python包。

使用setuptools打包Flask应用

setup.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from setuptools import setup

setup(
name='MyApp',
version='1.0',
long_description=__doc__,
packages=['myapp','myapp.main','myapp.admin'],
include_package_data=True,
zip_safe=False,
install_requires=[
'Flask>=0.10',
'Flask-Mail>=0.9',
'Flask-SQLAlchemy>=2.1'
]
)

把文件放在项目的根目录下,还要写一个MANIFEST.in文件:

1
2
recursive-include myapp/templates *
recursive-include myapp/static *

编写完毕后,你可以创建一个干净的虚拟环境,然后运行安装命令

1
$ python setup.py install

使用Fabric远程部署Flask应用

Fabric是一个Python的库,它提供了丰富的同SSH交互的接口,可以用来在本地或远程机器上自动化、流水化地执行Shell命令。因此它非常适合用来做应用的远程部署及系统维护。

安装Fabric

首先Python的版本必须是2.7以上,可以通过下面的命令查看当前Python的版本:

1
$ python -V

Fabric的官网是www.fabfile.org,源码托管在Github上。你可以clone源码到本地,然后通过下面的命令来安装。

1
$ python setup.py develop

在执行源码安装前,你必须先将Fabric的依赖包Paramiko装上。推荐使用pip安装,只需一条命令即可:

1
$ pip install fabric
第一个例子

我们创建一个”fabfile.py”文件,然后写个hello函数:

1
2
def hello():
print "Hello Fabric!"

现在,让我们在”fabfile.py”的目录下执行命令:

1
$ fab hello

你可以在终端看到”Hello Fabric!”字样。 fabfile.py文件中每个函数就是一个任务,任务名即函数名,上例中是hellofab命令就是用来执行fabfile.py中定义的任务,它必须显式地指定任务名。你可以使用参数-l来列出当前fabfile.py文件中定义了哪些任务:

1
$ fab -l

任务可以带参数,比如我们将hello函数改为:

1
2
def hello(name, value):
print "Hello Fabric! %s=%s" % (name,value)

此时执行hello任务时,就要传入参数值:

1
$ fab hello:name=Year,value=2016

Fabric的脚本建议写在fabfile.py文件中,如果你想换文件名,那就要在”fab”命令中用”-f”指定。比如我们将脚本放在script.py中,就要执行:

1
$ fab -f script.py hello
执行本地命令

fabric.api包里的local()方法可以用来执行本地Shell命令,比如让我们列出本地/home/bjhee目录下的所有文件及目录:

1
2
3
4
from fabric.api import local

def hello():
local('ls -l /home/bjhee/')

local()方法有一个capture参数用来捕获标准输出,比如:

1
2
def hello():
output = local('echo Hello', capture=True)

这样,Hello字样不会输出到屏幕上,而是保存在变量output里。capture参数的默认值是False。

执行远程命令

Fabric真正强大之处不是在执行本地命令,而是可以方便的执行远程机器上的Shell命令。它通过SSH实现,你需要的是在脚本中配置远程机器地址及登录信息:

1
2
3
4
5
6
7
8
from fabric.api import run, env

env.hosts = ['example1.com', 'example2.com']
env.user = 'bjhee'
env.password = '111111'

def hello():
run('ls -l /home/bjhee/')

fabric.api包里的run()方法可以用来执行远程Shell命令。上面的任务会分别到两台服务器example1.comexample2.com上执行ls -l /home/bjhee/命令。这里假设两台服务器的用户名都是bjhee,密码都是6个1。你也可以把用户直接写在hosts里,比如:

1
env.hosts = ['bjhee@example1.com', 'bjhee@example2.com']

如果你的env.hosts里没有配置某个服务器,但是你又想在这个服务器上执行任务,你可以在命令行中通过-H指定远程服务器地址,多个服务器地址用逗号分隔:

1
$ fab -H bjhee@example3.com,bjhee@example4.com hello

另外,多台机器的任务是串行执行的。 如果对于不同的服务器,我们想执行不同的任务,上面的方法似乎做不到,4我们要对服务器定义角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from fabric.api import env, roles, run, execute, cd

env.roledefs = {
'staging': ['bjhee@example1.com','bjhee@example2.com'],
'build': ['build@example3.com']
}

env.passwords = {
'staging': '11111',
'build': '123456'
}

@roles('build')
def build():
with cd('/home/build/myapp/'):
run('git pull')
run('python setup.py')

@roles('staging')
def deploy():
run('tar xfz /tmp/myapp.tar.gz')
run('cp /tmp/myapp /home/bjhee/www/')

def task():
execute(build)
execute(deploy)

执行

1
$ fab task

这时Fabric会先在一台build服务器上执行build任务,然后在两台staging服务器上分别执行deploy任务。@roles装饰器指定了它所装饰的任务会被哪个角色的服务器执行。 如果某一任务上没有指定某个角色,但是你又想让这个角色的服务器也能运行该任务,你可以通过-R来指定角色名,多个角色用逗号分隔:

1
$ fab -R build deploy

这样buildstaging角色的服务器都会运行deploy任务了。注:staging是装饰器默认的,因此不用通过-R指定。 此外,上面的例子中,服务器的登录密码都是明文写在脚本里的。这样做不安全,推荐的方式是设置SSH自动登录。 其余操作可以去http://www.bjhee.com/fabric.html查看

使用Fabric远程部署Flask应用

编写fabfile.py文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from fabric.api import *

env.hosts = ['example1.com', 'example2.com']
env.user = 'bjhee'

def package():
local('python setup.py sdist --formats=gztar', capture=False)

def deploy():
dist = local('python setup.py --fullname', capture=True).strip()
put('dist/%s.tar.gz' % dist, '/tmp/myapp.tar.gz')
run('mkdir /tmp/myapp')
with cd('/tmp/myapp'):
run('tar xzf /tmp/myapp.tar.gz')
run('/home/bjhee/virtualenv/bin/python setup.py install')
run('rm -rf /tmp/myapp /tmp/myapp.tar.gz')
run('touch /var/www/myapp.wsgi')

上例中,package任务是用来将应用程序打包,而deploy”任务是用来将Python包安装到远程服务器的虚拟环境中,这里假设虚拟环境在/home/bjhee/virtualenv下。安装完后,我们将/var/www/myapp.wsgi文件的修改时间更新,以通知WSGI服务器(如Apache)重新加载它。对于非WSGI服务器,比如uWSGI,这条语句可以省去。 编写完后,运行部署脚本测试下:

1
$ fab package deploy

使用Apache+mod_wsgi运行Flask应用

Flask应用是基于WSGI规范的,所以它可以运行在任何一个支持WSGI协议的Web应用服务器中,最常用的就是Apache+mod_wsgi的方式。上面的Fabric脚本已经完成了将Flask应用部署到远程服务器上,接下来要做的就是编写WSGI的入口文件”myapp.wsgi”,我们假设将其放在Apache的文档根目录在”/var/www”下。

1
2
3
4
5
6
7
8
9
10
11
12
activate_this = '/home/bjhee/virtualenv/bin/activate_this.py'
execfile(activate_this, dict(__file__=activate_this))

import os
os.environ['PYTHON_EGG_CACHE'] = '/home/bjhee/.python-eggs'

import sys;
sys.path.append("/var/www")

from myapp import create_app
import config
application = create_app('config')

你需要预先创建配置文件”config.py”,并将其放在远程服务器的Python模块导入路径中。上例中,我们将”/var/www”加入到了Python的模块导入路径,因此可以将”config.py”放在其中。另外,记得用setuptools打包时不能包括”config.py”,以免在部署过程中将开发环境中的配置覆盖了生产环境。 在Apache的”httpd.conf”中加上脚本更新自动重载和URL路径映射:

1
2
WSGIScriptReloading On
WSGIScriptAlias /myapp /var/www/myapp.wsgi

重启Apache服务器后,就可以通过”http://example1.com/myapp”来访问应用了。

使用Nginx+uWSGI运行Flask应用

需要先准备好Nginx+uWSGI的环境 uWSGI是一个Web应用服务器,它具有应用服务器,代理,进程管理及应用监控等功能。它支持WSGI协议,同时它也支持自有的uWSGI协议,该协议据说性能非常高,而且内存占用率低,为mod_wsgi的一半左右,我没有实测过。它还支持多应用的管理及应用的性能监控。虽然uWSGI本身就可以直接用来当Web服务器,但一般建议将其作为应用服务器配合Nginx一起使用,这样可以更好的发挥Nginx在Web端的强大功能。

安装uWSGI
1
$ pip install uwsgi

查看当前的uwsgi的版本:

1
$ uwsgi --version

让我们来写个Hello World的WSGI应用,并保存在”server.py”文件中:

1
2
3
4
5
6
7
8
9
def application(environ, start_response):
status = '200 OK'
output = 'Hello World!'

response_headers = [('Content-type', 'text/plain'),
('Content-Length', str(len(output)))]
start_response(status, response_headers)

return [output]

在uWSGI中运行它,执行命令:

1
$ uwsgi --http :9090 --wsgi-file server.py

然后打开浏览器,访问”http://localhost:9090″,你就可以看到”Hello World!”字样了。 上面的命令中”- -http”参数指定了HTTP监听地址和端口,”- -wsgi-file”参数指定了WSGI应用程序入口,uWSGI会自动搜寻名为”application”的应用对象并调用它。 更进一步,uWSGI可以支持多进程和多线程的方式启动应用,也可以监控应用的运行状态。我们将启动的命令改为:

1
$ uwsgi --http :9090 --wsgi-file server.py --master --processes 4 --threads 2 --stats 127.0.0.1:9191

执行它后,uWSGI将启动4个应用进程,每个进程有2个线程,和一个master主进程(监控其他进程状态,如果有进程死了,则重启)。同时,你可以访问”127.0.0.1:9191″来获取JSON格式的应用运行信息,uWSGI还提供了工具命令”uwsgitop”来像top一样监控应用运行状态,你可以用pip来安装它。 我们可以将参数写在配置文件里,启动uWSGI时指定配置文件即可。配置文件可以是键值对的格式,也可以是XML,YAML格式,这里我们使用键值对的格式。让我们创建一个配置文件”myapp.ini”:

1
2
3
4
5
6
7
[uwsgi]
http=:9090
wsgi-file=server.py
master=true
processes=4
threads=2
stats=127.0.0.1:9191

然后就可以将启动命令简化为:

1
$ uwsgi myapp.ini

配置Nginx

首先,我们将uWSGI的HTTP端口监听改为socket端口监听,即将配置文件中的”http”项去掉,改为”socket”项:

1
2
3
4
5
6
7
[uwsgi]
socket=127.0.0.1:3031
wsgi-file=server.py
master=true
processes=4
threads=2
stats=127.0.0.1:9191

然后,打开Nginx的配置文件,Ubuntu上默认是”/etc/nginx/sites-enabled/default”文件,将其中的根路径部分配置为:

1
2
3
4
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:3031;
}

这段配置表明Nginx会将收到的所有请求都转发到”127.0.0.1:3031″端口上,即uWSGI服务器上。现在让我们重启Nginx,并启动uWSGI服务器:

1
2
$ sudo service nginx restart
$ uwsgi myapp.ini

访问”http://localhost”,我们会再次看到”Hello World!”。

部署多个应用

一个Nginx中,可以同时运行多个应用,不一定是Python的应用。我们期望通过不同的路径来路由不同的应用,因此就不能像上例那样直接修改根目录的配置。假设我们希望通过”http://localhost/myapp”来访问我们的应用,首先要在Nginx的配置文件中,加入下面的内容:

1
2
3
4
5
location /myapp {
include uwsgi_params;
uwsgi_param SCRIPT_NAME /myapp;
uwsgi_pass 127.0.0.1:3031;
}

这里我们定义了一个uWSGI参数”SCRIPT_NAME”,值为应用的路径”/myapp”。接下来,在uWSGI的启动配置中,去掉”wsgi-file”项,并加上:

1
2
3
4
[uwsgi]
...
mount=/myapp=server.py
manage-script-name=true

“mount”参数表示将”/myapp”地址路由到”server.py”中,”manage-script-name”参数表示启用之前在Nginx里配置的”SCRIPT_NAME”参数。再次重启Nginx和uWSGI,你就可以通过”http://localhost/myapp”来访问应用了。

使用Nginx+uWSGI运行Flask应用

编写uWSGI的启动文件”myapp.ini”:

1
2
3
4
5
6
7
8
9
10
[uwsgi]
socket=127.0.0.1:3031
callable=app
mount=/myapp=run.py
manage-script-name=true
master=true
processes=4
threads=2
stats=127.0.0.1:9191
virtualenv=/home/bjhee/virtualenv

再修改Nginx的配置文件,Linux上默认是”/etc/nginx/sites-enabled/default”,加上目录配置:

1
2
3
4
5
location /myapp {
include uwsgi_params;
uwsgi_param SCRIPT_NAME /myapp;
uwsgi_pass 127.0.0.1:3031;
}

重启Nginx和uWSGI后,就可以通过”http://example1.com/myapp”来访问应用了。

使用Tornado运行Flask应用

Tornado的强大之处在于它是非阻塞式异步IO及Epoll模型,采用Tornado的可以支持数以万计的并发连接,对于高并发的应用有着很好的性能。本文不会展开Tornado的介绍,感兴趣的朋友们可以参阅其官方文档。使用Tornado来运行Flask应用很简单,只要编写下面的运行程序,并执行它即可:

1
2
3
4
5
6
7
8
9
10
11
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from myapp import create_app
import config

app = create_app('config')

http_server = HTTPServer(WSGIContainer(app))
http_server.listen(5000)
IOLoop.instance().start()

之后你就可以通过”http://example1.com:5000″来访问应用了。

使用Gunicorn运行Flask应用

Gunicorn是一个Python的WSGI Web应用服务器,是从Ruby的Unicorn移植过来的。它基于”pre-fork worker”模型,即预先开启大量的进程,等待并处理收到的请求,每个单独的进程可以同时处理各自的请求,又避免进程启动及销毁的开销。不过Gunicorn是基于阻塞式IO,并发性能无法同Tornado比。更多内容可以参阅其官方网站。另外,Gunicorn同uWSGI一样,一般都是配合着Nginx等Web服务器一同使用。 让我们先将应用安装到远程服务器上,然后采用Gunicorn启动应用,使用下面的命令即可:

1
$ gunicorn run:app

解释下,因为我们的应用使用了工厂方法,所以只在run.py文件中创建了应用对象app,gunicorn命令的参数必须是应用对象,所以这里是”run:app”。现在你就可以通过”http://example1.com:8000″来访问应用了。默认监听端口是8000。 假设我们想预先开启4个工作进程,并监听本地的5000端口,我们可以将启动命令改为:

1
$ gunicorn -w 4 -b 127.0.0.1:5000 run:app

来源

http://www.bjhee.com/flask-1.html https://blog.csdn.net/u011054333/article/details/70151857/ http://www.bjhee.com/flask-ad6.html

Prev
2018-08-09 15:25:33
Next