在知乎上找了个学习路线, 我会抽时间总结下面这些问题的解决方案.

定义 url route

定义路由是最基本的步骤, 通过定义路由可以让应用将指向不同url的访问映射到不同的方法中处理. 和Spring boot中的Controller组件十分相似.

使用route()装饰器

1
2
3
4
5
6
7
@app.route('/'):
def index():
return 'Index Page'

@app.route('/hello):
def hello():
return 'hello world'

简单的建立映射的方法.
可以在一个函数上附加多个URL route规则.

添加变量

使用<converter:variable_name>语法来为规则添加变量.

使用例子:

1
2
3
@app.route('/user/<int:userid>')
def show_user_profile(userid):
return f'User {userid}'

几种基本的转换器:
| 转换器 | 含义 |
| :————–: | :———————–: |
| (不显使用转换器) | 接收不含’/‘的字符串 |
| int | 接收整数 |
| float | 接收浮点数 |
| path | 和默认的类似, 但是接收’/‘ |
但是有一个问题, 就是转换器接收的变量的两端似乎必须是/.

比如说/user/<int:userid>可以匹配/user/1, /user/2等URL,

但是/user?id=<int:userid>则只能匹配user?id=<int:userid>这个URL, 也就是变量的部分不起作用了.

这个坑是否可以避免我还找到办法, 只能说目前来看这个变量不适合处理GET传来的参数.

组织 request handler 函数

写一个最简单的request handler 函数

1
2
3
@app.route('/')
def index():
return 'hello world'

从get/post请求中取出参数

默认情况下, route装饰器只会响应GET请求(和HEAD请求). 如果要处理POST, 需要给route提供methods参数:

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

那么, 如果要接收数据, 需要使用请求对象 request

1
2
3
4
5
6
7
8
9
from flask import request
@app.route('/login', methods=['GET','POST'])
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'

当form中的键值不存在时, 程序会抛出KeyError异常, 用户会看到HTTP 400 Bad Request. 所以最好捕获此异常, 或者使用request.args.get('args_name', '') 方法.

获取/修改/存储cookie,session数据

可以通过cookies属性来访问cookies.
通过相应对象的set_cookie方法来设置cookies.
例如:

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

@app.route('/')
def index():
# 读取cookies
username = request.cookies.get('username') # 这种方法不会引起KeyError异常.
@app.route('/login')
def login():
resp = make_response(render_template())
# 设置cookies
resp.set_cookie('username', 'the username')
return resp

上面的例子中演示了如何设置和读取cookies, 但是如果想使用会话, 不能直接使用cookies.

会话: session对象允许在不同请求间存储特定用户的信息. 这是在cookies的基础上实现的.
首先需要设置一个密钥.

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
from flask import Flask, session, redirect, url_for, escape, request
import os

app = Flask(__name__)

@app.route('/'):
def index():
if 'username' in session:
return f'Logged in as {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 action="" method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''
@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('index'))

app.secret_key = os.urandom(24)

修改/输出 http header 数据

在route函数中, 通常可以默认返回一个response对象. 例如 return make_response('index.html'). 这样client接收到的状态码和headers就是默认的.
可以通过return一个元组来设置. 例如

1
return make_response('style.css'), 200, {'Content-Type': 'text/css'}

这个三元组分别对应响应体, 状态码和响应头.
或者修改response对象的属性:

1
2
3
resp = make_response('style.css')
resp.headers['Content-Type'] = 'text/css'
return resp

钩子函数

钩子函数可以在handler函数执行前\后或者响应构造结束后执行. 一个典型的应用场景就是数据库的连接.

如果要在每个请求前都连接数据库, 最好使用before_request, after_requestteardown_request三个修饰器. 它们的作用是:

  • before_request: 装饰的函数会在请求前调用, 它没有参数. 但当异常抛出时, 不一定被执行.
  • after_request: 装饰的函数会在请求结束后调用, 需要传入响应.
  • teardown_request: 装饰的函数将会在响应构造后执行(无论响应构造过程中是否会抛出异常), 不允许修改请求, 返回的值会被忽略. 如果有异常抛出, 则会被作为参数传入, 否则传入None

样例:

1
2
3
4
5
6
7
@app.before_request
def before_request():
g.db = connect_db()

@app.teardown_request
def teardown_request(exception):
g.db.close()

使用模板系统

组织/访问 模板文件的目录结构

如果要使用模板, 首先要建立/templates目录, 然后向其中加入模板文件, 实例: layout.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!doctype html>
<title>Flaskr</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css')}}">
<div class=page>
<h1>Flaskr</h1>
<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>
{% for message in get_flashed_messages() %}
<div class=flash>{{ message }}</div>
{% endfor %}
</div>

有了这个
可以使用render_template方法来渲染模板, 只需要将模板名和作为关键字的参数传入模板的变量.

在模板中可以访问到 request, session, g 等对象, 以及get_flashed_messages()函数. 它可以实现消息的闪现, 在这里暂时不展开.

上述的实例模板中还有if-else-endif, for等控制语句, 都比较简单, 与 Python 的语法很接近.

配置静态文件访问

静态文件通常是css和JavaScript文件. 如果要通过Flask访问这种静态文件, 可以在模块旁边或者包中创建一个名为static的文件夹, url为/static即可访问.
如果要引用静态文件, 最好用url_for方法.
例如:

1
<script src="{{ url_for('static', filename='style.css') }}" />  # static/style.css

在模板中嵌入代码模板

有两种方式进行分别是包含include和继承block.

  • 包含

    include 比较简单, 看起来只是把一个文件的内容引用至此处. 但是把一个复杂的页面分成若干个不相干的部分, 分别写到其它文件中再一一包含至此, 可以提高代码的可读性、可维护性, 逻辑也会更清晰.

    • A.html

      1
      2
      3
      4
      <!doctype html>
      <title>Flaskr</title>
      <link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css')}}">
      {% include "B.html" %}
    • B.html

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <div class=page>
      <h1>Flaskr</h1>
      <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>
      {% for message in get_flashed_messages() %}
      <div class=flash>{{ message }}</div>
      {% endfor %}
      </div>

      此时如果通过render_template来访问 A.html , 就会发现 B.html 中的内容也被加载进来了.

  • 继承

    如果说 A 包含 B 是把 B 的代码引入A中,

    那么 A 继承 B 则更像是把 A 的代码覆盖到 B 中, 不过是根据 block 分别覆盖.

    使用的关键字是extends block

    • A.html

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      <main id="main">
      <!-- 顶部标签栏 -->
      <header class="top-header" id="header">
      <div class="flex-row clearfix">
      <a href="javascript:;" class="header-icon pull-left waves-effect waves-circle waves-light on" id="menu-toggle">
      <i class="icon icon-lg icon-navicon"></i>
      </a>
      <div class="flex-col header-title ellipsis">
      <span>{{title}}</span>
      </div>
      <a href="javascript:;" id="site_search_btn" class="header-icon pull-right waves-effect waves-circle waves-light">
      <i class="icon icon-lg icon-search"></i>
      </a>
      </div>
      </header>
      {% block main_layout %}
      主要内容显示于此, 如果你看到了这句话, 请联系站长 qq: 1348651580
      {% endblock %}
      </main>
    • B.html

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      {% extends "A.html" %}

      {% block main_layout %}
      <!-- index 页的主内容 -->
      <div class="container body-wrap">
      <!-- 文章列表 -->
      <ul class="post-list">
      <p>hello world</p>
      </ul>
      </div>
      {% endblock %}
那么此时再访问 B.html, 则会发现 B 中的 `main_layout`这个 block 替换掉了 A 中的 `main_layout`. 

虽然继承和包含使用起来很方便, 但是同时使用二者依然有的时候会有坑, 我遇到的一个就是在被 include 的文件中的 block 的可见性问题. 例如在包含的示例代码 B.html 中, 我希望将顶层的div标签的class值作为一个block, 使得后面继承的模板能让此class具有不同的值.

所以B的代码就成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class={% block class %}page{% endblock %}>
<h1>Flaskr</h1>
<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>
{% for message in get_flashed_messages() %}
<div class=flash>{{ message }}</div>
{% endfor %}
</div>

同时有一个C.html

1
2
3
{% extends "A.html" %}

{% block class %}non-page{% endblock %}

那么再访问C.html, 会发现这里没有实现对block class的继承. 我推测原因是B中的block class对于C来说是不可见的.

目前我的解决方案是, 把 block 提到 A 中.

A.html

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

或者设置局部变量应该也行吧, 还没尝试.

自定义模板函数

  • app.jinja_env中设置

    1
    app.jinja_env.globals['include_file'] = lambda filename : Markup(app.jinja_loader.get_source(app.jinja_env, filename)[0])
  • 使用app.add_app_template_global添加

    1
    routing.add_app_template_global(get_current_year, "get_current_year")

    第一个参数为函数对象, 第二个参数是模板函数的名称.

  • 在模板中定义

Flask-Bootstrap

// todo

访问数据库

重点记录关于flask_sqlalchemy的使用方法.

建立模型

示例:

  • 基本模型
    1
    2
    3
    4
    5
    class User(db.Model):
    __tablename__ = 'user'
    user_id = db.Column(db.Integer, primary_key=True)
    password = db.Column(db.String(64), nullable=False)
    name = db.Column(db.String(64), nullable=False, unique=False)
其中`__tablename__`代表表的名字, 可以省略.
  • 多对一关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class User(db.Model):
    __tablename__ = 'user'
    user_id = db.Column(db.Integer, primary_key=True)
    password = db.Column(db.String(64), nullable=False)
    name = db.Column(db.String(64), nullable=False, unique=False)

    comments = db.relationship('Comment', backref='author')

    class Comment(db.Model):
    __tablename__ = 'comment'
    comment_id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False, unique=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.user_id'))
    reply_id = db.Column(db.Integer, db.ForeignKey('comment.comment_id'), nullable=True)
    os = db.Column(db.String(32))
    browser = db.Column(db.String(32))

    replys = db.relationship('Comment', backref='reply_to')

    到这里就稍微有点复杂了. 下面分别解释下relationship, ForeignKeybackref的作用吧.

    • ForeignKey: 直译就是外键, 以上例中第13行代码为例, comment.user_id是指向user.user_id的外键. 因为每个用户(User)可以有多个评论(Comment), 但是每个评论(Comment)只能被唯一的用户(User)提出. 所以通过外键就能建立起多对一的联系.
    • relationship: 以第7行为例, 如果我们已经通过外键建立好关系后, 就可以使用relationship得到一个指向这个User所持有的Comments列表(除非指定userlist=False). 第一个参数指明了一对多关系的”多”那一侧的模型名.
    • backref: 用来定义反向关系. 还是以第7行来举例, backref='author'的含义是, 在Comment模型中插入一个名为author的属性, 它指向了多对一关系中的”一”这侧. 也就是Comment.author指向了User模型的一条数据.
  • 多对多关系

    通过建立一个辅助表, 将多对多关系拆分为两个多对一关系.

    具体做法以后更新.

掌握最基本的add

掌握最基本的delete

按字段查询

count

slice

order by

部署app

配置文件

一个简洁的方案是创建独立的 .ini 或 .py 文件

1
2
3
4
5
6
7
8
9
10
11
import sqlite3
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash

DATABASE = '/tmp/flaskr.db'
DEBUG = True
SECRET_KEY = 'development key'
USERNAME = 'admin'
PASSWORD = 'default'

app = Flask(__name__)
app.config.from_object(__name__)

from_object()会遍历给定的对象, 如果它是一个字符串, 则会导入它, 否则搜寻里面定义的全部大写的变量.

项目结构

尽管flask并不强制要求大型项目使用特定的组织方式, 应用结构的组织方式完全由开发者决定. 但是这样可能导致开发时结构混乱. 因此本人参考《Flask Web 开发——基于python的Web应用开发实战》一书所讲构建一套我自己满意的flask项目模板, 供自己日后使用, 以便在学习或者开发时能快速上手.

项目地址: https://github.com/anti-entropy123/flask-standard-template

可选的学习项目

发送email

log

图片处理