Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

14 Jun 2020

Flask文档阅读笔记(一)

Flask 文档1.1.x 走读笔记。

Foreword

Flask是micro框架,micro不意味着功能确实,而是意味着将核心功能保持在小范围,通过扩展的方式提供更多功能。比如,Flask默认不带数据库抽象层。

Flask有许多配置选项,但都提供了有意义的默认配置。比如模板和静态文件放在源代码树的template和static目录中。

Foreword for Experienced Programmers

Flask使用Thread Locals来存储请求和会话。

Flask会尽量保证网站的安全性。比如,HTML会被默认为不安全的,可能引起XSS。但是开发者本身也需要对网站安全性提供警觉。

Installation

Flask支持Python3.5,Python 2.7以及PyPy。并默认依赖于Werkzeug, Jinja, MarkupSafe(来自Jinja,对HTML进行转义), ItsDangerous(保护cookie)以及Click(用于方便书写命令行)。

以下依赖不是默认的,但是Flask会自动检测它们是否存在:

  • Blinker提供Signals的支持
  • SimpleJSON,Python的json模块的替代品
  • python-dotenv,提供虚拟环境的支持
  • Watchdog,快速重新加载服务端

章节的余下部分介绍了如果使用venv创建虚拟环境(对于python2的话,需要使用virtualenv)。

Quickstart

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'
  • Flask需要通过__name__来决定从哪里获取模板以及静态文件
  • Flask通过app.route来修饰函数,以便将函数关联到route

Flask可以从环境变量FLASK_APP中查找app所在文件:

$ export FLASK_APP=hello.py
$ flask run
 * Running on http://127.0.0.1:5000/

如果要接受本地以外的连接,则要:

flask run --host=0.0.0.0

开启Debug Mode,这样Flask可以在有代码改动的时候重新加载自己:

$ export FLASK_ENV=development
$ flask run

上面的操作还会开启Debug功能,如果只是想开启Debug功能而不需要自动加载,那么可以使用FLASK_DEBUG=1。

Werkzeug documentation 说明如何使用自带的Debugger。如果想使用其他的Debugger,参考 Working with Debuggers.

Routing

在绑定route的时候,可以用 <converter:variable_name>的方式动态匹配URL,例子:

@app.route('/path/<path:subpath>')
def show_subpath(subpath):
    # show the subpath after /path/
    return 'Subpath %s' % escape(subpath)

Converter可以是:

  • string,默认的,接受任何文本,但是不支持slash,也就是/
  • int,接受正整型
  • flaot,接受正浮点型
  • path,类似string,但是接受slash
  • uuid,接受UUID字符串

对于URL后面需不需要带slash这个问题,Flask是这么处理的:

  • 如果route里面声明的是/projects/,那么访问/projects的时候就会被重定向到/projects/
  • 如果route里面声明的是/projects,那么访问/projects/的时候就会出现404错误。

Flask提供一个叫做url_for()的函数来生成相对于根目录的url,例子:

from flask import Flask, url_for
from markupsafe import escape

app = Flask(__name__)

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

@app.route('/login')
def login():
    return 'login'

@app.route('/user/<username>')
def profile(username):
    return '{}\'s profile'.format(escape(username))

with app.test_request_context():
    print(url_for('index'))
    print(url_for('login'))
    print(url_for('login', next='/'))
    print(url_for('profile', username='John Doe'))

输出:

/
/login
/login?next=/
/user/John%20Doe

在route上可以指定HTTP方法,比如:

from flask import request

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

如果不指定methods,那么默认就只有GET(以及附带的HEAD和OPTIONS)。

对于静态文件,最好是通过HTTP server来提供服务。如果非得经过Flask,可以在源代码树中创建一个static目录,然后代码中通过下面的方法来获取:

url_for('static', filename='style.css')  # 获取 static/style.css

Rendering Templates¶

render_template()可以用来渲染Jinja模板:

from flask import render_template

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

Flask会在templates目录下查找hello.html。

对于模块的话:

/application.py
/templates
    /hello.html

如果打包的话:

/application
    /__init__.py
    /templates
        /hello.html

参考Jinja2 Template Documentation 获得更多Jinja相关的文档信息。

在模板代码里面,可以访问request,session以及g对象,还有get_flashed_messages()方法。

模板中的HTML会被自动转义,如果需要保持原样,可以使用|safe过滤器。

Accessing Request Data¶

导入request:

from flask import request

Flask中的request和context包含的是具体请求相关的数据。reqeust的使用示例:

@app.route('/login', methods=['POST', 'GET'])
def login():
    error = None
    if request.method == 'POST':
        if valid_login(request.form['username'],
                       request.form['password']):
            return log_the_user_in(request.form['username'])
        else:
            error = 'Invalid username/password'
    # the code below is executed if the request method
    # was GET or the credentials were invalid
    return render_template('login.html', error=error)

上述代码中,如果request.form访问出错,会抛出KeyError错误,造成一个400 Bad Request页面。

如果要访问URL中的参数,可以使用:

searchword = request.args.get('key', '')

别忘了将表单的enctype="multipart/form-data"属性设置好

Flask支持处理文件上传。上传的文件可以保存在内存或者文件系统中。可以通过request.files来访问文件。request.files是一个字典,有着文件名到文件对象的映射。文件对象和Python的文件对象类似,不过具有一个save()方法,可以将文件保存到文件系统:

from flask import request

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        f = request.files['the_file']
        f.save('/var/www/uploads/uploaded_file.txt')
    ...

文件对象的filename 属性保存着文件在客户端上传前的命名。但是客户端可以伪造这个名字,所以不一定可信。建议通过Werkzeug提供的secure_filename()来对filename进行处理。

        f.save('/var/www/uploads/' + secure_filename(f.filename))

设置cookie的话可以在response上调用set_cookie()方法:

from flask import make_response

@app.route('/')
def index():
    resp = make_response(render_template(...))
    resp.set_cookie('username', 'the username')
    return resp

如果要设置cookie,但是response对象还不存在,那么可以参考Deferred Request Callbacks

Redirects and Errors¶

使用redirect()可以将用户重新定向到一个新的url。如果要中止处理,那么可以使用abort():

from flask import abort, redirect, url_for

@app.route('/')
def index():
    return redirect(url_for('login'))

@app.route('/login')
def login():
    abort(401)
    this_is_never_executed()

上面的abort(401)会产生一个401错误,如果要自定义这个错误页面,需要使用:

from flask import render_template

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

About Responses¶

route处理函数返回的对象会被自动转化为response对象:

  1. 如果返回的就是response,那么直接会被当作response使用
  2. 如果是字符串,那么会创建一个response,并且把返回的字符串当作内容
  3. 如果是字典,那么会创建一个response,并把返回的字典转为json
  4. 如果是元组,那么必须是这些格式:(response, status),(reponse, headers),(response, status, headers).
  5. 如果上面的都行不通,那么Flask会假设返回的是一个WSGI应用,并调用相应的方法将其转化为response

下面是返回response的一个例子:

@app.errorhandler(404)
def not_found(error):
    resp = make_response(render_template('error.html'), 404)
    resp.headers['X-Something'] = 'A value'
    return resp

返回字典的例子:

@app.route("/me")
def me_api():
    user = get_current_user()
    return {
        "username": user.username,
        "theme": user.theme,
        "image": url_for("user_image", filename=user.image),
    }

或者可以调用jsonify显示将字典转化为json

@app.route("/users")
def users_api():
    users = get_all_users()
    return jsonify([user.to_json() for user in users])

Sessions

session可以帮助在同一个用户不同request之间的共享数据。实现上,session是基于加密的cookie的:

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

app = Flask(__name__)

# Set the secret key to some random bytes. Keep this really secret!
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'

@app.route('/')
def index():
    if 'username' in session:
        return 'Logged in as %s' % escape(session['username'])
    return 'You are not logged in'

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        session['username'] = request.form['username']
        return redirect(url_for('index'))
    return '''
        <form method="post">
            <p><input type=text name=username>
            <p><input type=submit value=Login>
        </form>
    '''

@app.route('/logout')
def logout():
    # remove the username from the session if it's there
    session.pop('username', None)
    return redirect(url_for('index'))

因为没有使用模板,所以上面的显示使用escape来转义。

快速生成密钥:

$ python -c 'import os; print(os.urandom(16))'
b'_5#y2L"F4Q8z\n\xec]/'

注意,cookie的大小可能会受到浏览器的限制。Flask默认提供的是客户端的session,如果需要服务端的session,需要使用扩展来实现。

Message Flashing¶

Flask在用户反馈上,提供了简单的支持。通过flash()方法,Flask可以给用户展现一条消息。flash()应该是在reqeust后面附加了一条消息,然后紧邻的request中可以通过get_flashed_messages()获取。

更多参考Message Flashing

Logging

使用app.logger可以记录debug信息:

app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')

更多查看Application Errors

Hooking in WSGI Middleware¶

如果要跟WSGI中间件挂钩,那么可以使用app示例的wsgi_app属性:

from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)

ProxyFix是Werkzeug提供的中间件,可以让Flask运行在Nginx之上。使用app.wsgi_app而不是直接app,意味着你依然可以使用正常的方式来配置app。

Additional Notes > Design Decisions in Flask

The Explicit Application Object

一个Flask对象是一个支持wsgi应用规范的实例,需要显示创建,比如:

from flask import Flask
app = Flask(__name__)

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

文档列出了几个理由说明为什么不支持隐性实例化。

The Routing System

Flask所基于的Werkzeug允许以任意的顺序来定义routes。

Werkzeug还有努力保证URLs具有唯一性,如果URLs具有歧义的话,会自动跳转到一个规范过的URL。

One Template Engine

Flask默认绑定的是Jinja,并且依赖与很多Jinja的特性。你或许可以使用其他引擎,比如Genshi、Marko,但是无法把Jinja从Flask中剥离。

Micro with Dependencies

为啥作为Micro框架的Flask还基于Werkzeug和Jinja?这里举的例子是关于Ruby,Ruby有一个和WSGI类似的规范,叫做Rack,并且提供了一个实现了Rack规范的库,这个库本身也叫做Rack。基本上所有Ruby的Web框架都是基于Rack库的。对于Python来说,WSGI相关的库有WebOb(以前叫做Paste)和Werkzeug。

Thread Locals

Flask使用Thread Locals来保存请求和会话。在大型程序里面这个选择可能是一个有争议的选项,但是作为Micro框架,则是一个合理的选择。

What Flask is, What Flask is Not

Flask挺高兴自己是这个样子的。

其他

Tutorial 举了一个很好的例子,讲解了如何以flask构建一个blog。

(本篇完)

Categories

Tags