本文为 Flask 框架学习笔记,主要参考了 The-Flask-Mega-Tutorial 和 《Flask Web开发:基于Python的Web应用开发实战》两本书,并在原项目的基础上拓展。(下文统称这两个资源为“本教程”) 不熟悉 Flask 框架请先阅读快速上手 - flask 中文文档 。
这两本书的作者是同一个人,就内容上说后者算是前者的豪华版。本教程的优点是内容全面,从入门到部署一站式服务;缺点是不够深入,且有些过时,书中举例的诸多插件均为作者为了此书而开发的,已经许久不再维护,导致很难在其示例项目上拓展。一看扉页,2015年出版,那没事了。 至于第一个网站?参见#TODO:年轻人的第一个网站
0x00 大型项目结构
在大部分面向初学者的 demo 中,应用以简单的项目结构甚至单文件表示。在大型项目中,网站的不同功能被拆分成独立的模块,以方便拓展和维护。一个更通用的 Flask 项目代码架构如下:(仅考虑业务代码)
|
|
根目录下的文件有:
app/
所有网站源代码统一归到app目录下。- 在
app/
内部,不同的功能可进一步划分成独立模块,详见[模块化应用](#0x05 模块化应用:功能解耦)一章。
- 在
main.py
: 入口脚本,通过该文件引入app中的代码并生成应用实例(命名随意).flaskenv
: flask环境变量,以配合flask命令。入口脚本被定义为FLASK_APP
,执行flask run
时将启动该脚本。config.py
: 配置脚本,整个项目的配置信息都写在Config类里。与环境变量的区别在于,因为是python脚本,功能更强大,可被任何地方的代码引用。
0x01 Hello world:模板和视图
最基本的 web 功能,无非接受请求、返回数据。其中,路由 (route) 用来区分不同的请求,模板 (templates) 用来生成不同的数据。
路由/视图
在非前后端分离的项目中,视图函数直接返回渲染好的网页,由@app.route()
修饰后,视图和路由便绑定在一起。
在mvc模型中更像controller控制器的角色,然而在flask生态中更喜欢称为视图函数。
url_for()
使用URL到视图函数的内部映射关系来生成URL,用来替换硬链接。在业务功能解耦后必须使用这种方式。- NOTE:当路由和视图函数名不一致,访问该路由可以正确响应,但是使用
url_for()
调用该视图时会报错
- NOTE:当路由和视图函数名不一致,访问该路由可以正确响应,但是使用
{% extends "base.html" %}
and{% include "_post.html" %}
使用子模板来实现网页公用的部分。如:页眉,页脚,列表项等。- 模板和 Python 代码的关系有些类似与 JSP 和 Java 代码的关系,但模板语法并不是完整的脚本语言,相较而言限制更多,安全性更好。
表单
几乎所有成功的框架都有丰富的插件生态。下面引入新功能时,大多借助插件来方便的实现。大多数Flask插件使用
flask_<name>
命名约定。
Flask-WTF插件提供了对Web表单的抽象,只需定义表单类以及设置类属性即可。
模板语法:
{{ form.<name>.label }}
渲染标签{{ form.<name>() }}
获取属性值form.hidden_tag()
模板参数生成了一个隐藏字段,其中包含一个用于保护表单免受CSRF攻击的token
将表单引入模板
|
|
flash 闪现消息
flash 通过 session 储存,用于显示只出现一次的提示消息。用法:
- 在路由中使用
flash()
,触发时消息便写入 session 中的 message 列表 - 在模板中使用
get_flashed_messages()
,从 session 中读取
0x02 数据库 ORM
很久很久以前,web网站和数据库交互还需要写很多很硬的 SQL 语句,效率低且容易出现注入漏洞(SQLi)。现代web开发都使用 ORM 框架简化数据库交互,且基本杜绝了 SQLi 漏洞。
本项目使用如下插件打通数据库:
- Flask-SQLAlchemy: Python生态最知名的ORM框架
- Flask-Migrate: 本教程作者编写的数据库迁移框架
插件首先要注册。统一流程: 初始化app实例,传入插件类作为插件实例的参数
1 2 3 4 5
# app/__init__.py app = Flask(__name__) # flask基类 app.config.from_object(Config) db = SQLAlchemy(app) migrate = Migrate(app, db)
SQLalchemy:model层
模型定义
使用类和类属性代表 table 和 colunm ,便可轻松编写数据模型。SQLalchemy 的概念抽象如下图:
Flask-SQLAlchemy 自动设置类名为小写来作为对应表的名称,也可以用__tablename__
类属性来定义。
|
|
CURD基本操作
ORM 框架通常集成了常用操作,但也支持更底层的数据库接口。
在 Springboot Jpa 中,根据方法名的拼写来写自定义查询,而在 SQLalchemy 中,提供的接口通过链式调用拼接。
在 SQLalchemy 中,基本操作大都有基于事务 (session) 的和基于查询 (query) 的两种方式。
-
查
1
session.query(User)
query方法只有构造一个查询,只有在
Query.get()
、Query.all()
、Query.one()
等结束符之后才会执行查询 -
增:
1 2
db.session.add(user) db.session.commit()
-
删
1 2 3 4
session.query(User).delete() # or session.delete(session.query(User).get(1)) session.commit()
-
改
1 2 3 4 5 6 7 8 9
query = (session .query(User) .filter_by(id=1) .update({"username": User.username + "a"}, synchronize_session=False) ) # or user = (session.query(User).get(1)) user.password = "zxcv" session.commit()
Flask-Migrate: 数据库迁移
配置数据库的初始数据框架,一般写成SQL脚本形式。 migrate 框架直接根据 model 层生成迁移脚本,可以方便的跟踪数据模型的修改和数据库的切换。(这个框架还是本教程作者自己开发的,强)
flask db子命令
flask db init
:初始化,生成migrations目录flask db migrate
:生成迁移脚本,修改model后使其生效flask db migrate -m "posts table"
flask db upgrade
:应用数据库修改(开发阶段默认使用sqlite数据库flask db downgrade
:回滚上次的迁移
0x03 开发范式:用户系统
mixin:混入,多重继承的一种形式
表单和数据库支持分别解决了前端和后端的基本需求,下面可以上线一个基本功能了,用户登录。 所需插件:Flask-Login。
UserMixin类
UserMixin类集成了login插件要求的用户模型属性,将其混入到 User 模型中,即可用@login_required
实现权限控制。
|
|
用户系统,包括登录、登出、注册几个功能。编写这些功能的步骤其实很类似:
- 设计数据库,在model.py中
- 设计表单对象,在form.py中
- 设计页面,在模板.html中
- 设计视图函数,在routes.py中
也对应了mvc框架的设计理念,比如设计表单就有些像 javaweb 中的 DAO 层。但也有区别, Flask 框架更希望业务逻辑写在数据库模型中,而视图函数尽量保持简洁,以方便单元测试。
PRG 模式
即为 Post/Redirect/Get
,其格式大概如下:
|
|
默认情况,提交 POST 请求后,如果直接刷新浏览器,会重新在 POST 一次。使用PRG模式即可解决重复提交表单的问题。
0x04 深入数据库:粉丝机制
数据库关系
要关注别人,就要让数据库记住我关注的人的名字,当然,只记住名字肯定不够,万一改名了呢。因此每个用户都需要有唯一有效的标识(其实更重要的是性能因素)。正因如此,数据库中每个表都要有一个唯一的列,称为主键(primary key)。当不同表之间存在关系,一个表要通过主键寻找其他表项,其他表的主键储存在本表中,称为外键(foreign key)。外键关联既可以表示一对一的关系,也可以一对多(1->n)。
SQLalchemy 对关系的定义如下:
- 外键:
db.ForeignKey('user.id')
- 关系:
db.relationship('Post', backref='author', lazy='dynamic')
- 参数1:所关联的表(n in 1->n),这里是模型的变量名
- 参数2:由 “n” 回调 “1” 的虚拟字段,用法:
post.author
粉丝机制
然而,粉丝机制包括关注和被关注。这是一种多对多的关系,于是需要用含有两个外键的关联表表示。又因为关注者和被关注者在一个表里(User),这种关系又称为自引用。
模型
-
关联表只有引用类型,故不需要派生模型类
1 2 3 4 5
followers = db.Table( 'followers', db.Column('follower_id', db.Integer, db.ForeignKey('user.id')), db.Column('followed_id', db.Integer, db.ForeignKey('user.id')) )
-
为User添加关系
1 2 3 4 5
followed = db.relationship('User', # 右侧实体 secondary=followers, # 指定关联表 primaryjoin=(followers.c.follower_id == id), # 指定左关系 secondaryjoin=(followers.c.followed_id == id), # 指定右关系 backref=db.backref('followers', lazy='dynamic'), lazy='dynamic') # 指定回调
复杂查询
-
查询粉丝列表
-
SQL 语句:
SELECT * FROM user, followers WHERE followers.follower_id = 3 AND followers.followed_id = user.id
-
SQLalchemy 接口:
user.followers.all()
-
实际执行的 SQL 语句:(打印 query 对象得到)
1 2
SELECT ,,, FROM user, followers WHERE followers.followed_id = ? AND followers.follower_id = user.id
-
NOTE:如果方法集成在model里,方法名不要和字段名相同,自己定义的方法会覆盖该字段。
-
-
查看已关注用户的动态
-
SQL 语句:
SELECT * FROM post JOIN followers on followers.followed_id = post.user_id where followers.follower_id = 2
-
SQLalchemy 接口:
1 2 3 4
Post.query.join( followers, (followers.c.followed_id == Post.user_id)).filter( followers.c.follower_id == self.id).order_by( Post.timestamp.desc())
-
实际执行的SQL语句:
1 2 3 4 5 6 7 8 9
SELECT ,,, FROM (SELECT ,,, FROM post JOIN followers ON followers.followed_id = post.user_id WHERE followers.follower_id = ? UNION SELECT * FROM post WHERE post.user_id = ? ) AS anon_1 ORDER BY anon_1.post_timestamp DESC
-
由于python的弱类型特征,有时候很难明白函数之间传递的是什么对象。我们从上往下梳理一遍:
- 请求到达路由函数,开始执行查询
Post.query.....
,此时只是在构造查询,并未取得数据,此时的对象类型:<class 'sqlalchemy.orm.query.Query'>
- 直到
get()
,all()
,paginate().items
结束符等出现,查询才被执行,返回数据类型实例,如User。 - 数据类实例传入模板,并由
__str__
等方法参与渲染。
0x05 网站美化
本教程提供的flask-bootstrap插件,较为简陋,且该插件年久失修,遂替换之。在此之前,先搞明白目前项目前端的架构
|
|
所有模板都有一个父模版:base.html
,其结构如下:
|
|
app_content
留空,即其余模板均在app_content
内填充。
进一步追溯bootstrap/base.html
的源码,发现其它 block 诸如navbar
也都留空或仅仅配置了 Bootstrap 的 cdn。 由此,只需将base.html
迁移即可。
在网上寻找新的UI模板,不要在中文互联网搜索,basically garbage。找到一个 Meterial 模板 还算顺眼,遂用之。
不熟悉 Bootstrap 布局的可以使用可视化工具来设计前端,如:http://www.ibootstrap.cn/
对照模板,将base.html
掏空,效果如下:
遇到的bug有:
- 下拉菜单失效:查询得知有可能是bootstrap版本冲突
- //结果并不是,只是忘记引入js文件而已,我是傻逼。
- 文件上传按钮消失:本教程中,表单渲染采用
wtf.quick_form()
,这玩意还是来自bootstrap/wtf.html
最后决定整个🐏了 Flask-Bootstrap 插件。
富文本编辑器
在《Flask Web开发:基于Python的Web应用开发实战》中提到了markdown编辑器的实现。
需要的包:
- PageDown: JS 版 Markdown 渲染器,用于客户端预览。
- Flask-PageDown: flask 集成插件。该插件需要注册
- Markdown: Python 版 Markdown 渲染器,用于服务端渲染。
- Bleach: HTML 清理器,保证安全性
为了兼顾安全和效率,做法是同时保存 markdown 源文本和 HTML 文件。步骤如下:
- 表单改为
PageDownField
- 模板引入 PageDown 宏,以实现即时预览
- 为 Post 模型增加字段,并添加 markdown 渲染方法,该方法为类方法,需要
@staticmethod
修饰 - 在模型外部监听数据库事件,仅当 markdown 文本出现变动时调用渲染方法。
- 修改模板以显示服务端返回的 html 文本
然而预览器过于简陋,也很难修改。在github仓库上发现该插件也是本教程作者写的,已经很久没有维护。顿时对本书作者有些不满。
0x06 模块化应用:功能解耦
保持 app 作为全局变量的模式,可能会给后续引入新功能和单元测试带来麻烦。 要适应大型项目需求,需要把网站功能拆分成独立的模块。
Blueprint化
要实现解耦,一种功能的相关代码可以借助Blueprint归类到一个包里。其文件结构大致如下:
|
|
创建blueprint与创建应用非常相似。
|
|
而消灭了app
,蓝图内部的引用统一变成了蓝图名。而外部诸如url_for
的参数则需要加上包名.
做前缀。
应用工厂模式
工厂函数是一个外部函数,在这个函数内部执行插件注册和配置工作,并通过他返回应用实例。
|
|
返回后,flask提供的上下文对象current_app
将指向应用实例。详见官方文档:应用上下文
多线程
current_app
是线程绑定的,若要在诸如邮件服务的位于其他线程的功能调用他,则会发现没有赋值。
需要使用current_app._get_current_object()
表达式。
Python概念辨析:包,库,插件
包 (package) 是指一种代码结构,只要有文件夹和
__init__.py
都是包。 库 (library) 和插件 (plugin) 都是从外部引入的包,区别在于,插件要集成进应用,所以需要注册等步骤;而库更独立,可以随时随地调用
0x07 开发帮手
本节讲解一些杂项。
调试
flask shell
命令:为避免每次调试都要重新import app,使用上下文调用解释器,用@app.shell_context_processor
装饰上下文函数
单元测试
unittest 库,详见下一篇。
记录日志到文件
logger 库
|
|
requirement.txt
装的库太多怎么办?只需要两条命令:
|
|
0x08 网站上线
最后简单列出几种网站部署的方法,详情参考本教程或自行搜索。
native模式
- 买主机
- 连主机:ssh
- 买域名
- 配域名
- 配环境
- 数据库
- 服务器
- 其他依赖
- 持续运维
容器化技术:docker
- 写dockerfile
- docker-compose up –build -d
云技术:PaaS
- 注册云平台账户
- 写Procfile
- git push