今天开始研究Flask,感觉比Django更容易明白自己的代码在干什么,而不是在别人装修好的房子里摆家俱。可能要学习的内容包括(简书的Markdown不支持目录[TOC]???,也不支持页内链接自己手动做目录都不可能???明白为什么叫“简”书了):
一、环境与安装
基于Docker开发环境配置,能跑起来的第一个应用Hello World!
二、MVT
基于Model,View和Template的应用
1. 模板
2. View与路由
3. ORM与数据库
三、登录与认证
处理登录与认证
1. 表单与flask-wtf
2. 会话
四、RESTful API
将前后端分离所需要的工作
五、部署
wsgi的几种部署(Gunicorn 和 Tornado,Nginx)
——这里有一个参考网址准备研究,不过只有基于模板的开发:http://www.pythondoc.com/flask-mega-tutorial/index.html
Let's GO
一、环境与安装
网上的教程一般从虚拟环境开始,跟之前的django开发环境一样,这里还是直接搭建docker环境用于开发,保证开发到部署的一致性。PyCharm的好处之一就是其支持配置Python解释器到docker-compose.yml文件设置的远程容器中的python。
- 建立一个目录放项目
在里面写好Dockerfile和requirements.txt - Dockerfile:
FROM python:3.7
ADD requirements.txt /tmp/requirements.txt
RUN pip install -r /tmp/requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
EXPOSE 5000
WORKDIR /myapp
- requirements.txt,参照网址教程中所需要的包,flask核心很小,其他功能是通过松散的包实现所的,所以比django要装的多。
flask==1.1.1
flask-login==0.5.0
flask-openid==1.2.5
flask-mail==0.9.1
flask-whooshalchemy
flask-wtf==0.14.3
flask-babel==1.0.0
flask-script==2.0.6
flask-migrate==2.5.3
flask_mysql==1.5.1
flask_sqlalchemy==2.4.1
pymysql==0.9.3
mysqlclient==1.4.6
sqlalchemy-migrate==0.13.0
gunicorn==19.9.0
guess_language==0.2
flipflop==1.0
coverage==5.0.4
pytz==2018.9
urllib3==1.25.3
transitions==0.7.1
Markdown==3.0.1
# Developer Tools
mkdocs==1.0.4
# Static and Media Storage
boto3==1.9.93
oss2==2.9.1
- docker-compose.yml:
还是准备用MySQL数据库,所以docker-compose.yml仍使用三个容器,其中一个mkdocs服务,一个MySQL服务,一个作为开发项目。MySQL容器配置的用户名密码等放在.envs/mysql.env文件中。开发容器映射文件夹/myapp到当前项目目录。
version: '3'
services:
mysql_server:
image: mysql:5.7.26
container_name: flask_mysql_container
restart: always
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
env_file:
- .envs/mysql.env
ports:
- "3302:3306"
volumes:
- ./mysql_data:/var/lib/mysqldocker
flask_server:
build: .
container_name: flask_container
env_file:
- .envs/flask.env
- .envs/mysql.env
depends_on:
- mysql_server
- doc_server
volumes:
- .:/myapp
ports:
- 8080:5000
command: >
bash -c "python wait_db_ready.py
flask run -h 0.0.0.0"
doc_server:
restart: always
build: .
container_name: flask_docs
command: "mkdocs serve"
volumes:
- .:/myapp
ports:
- "8081:80"
- 容器所需要的环境变量放在.envs目录下:
mysql.env,弱密码,仅用于试验
# root password
MYSQL_ROOT_PASSWORD=root
# user
MYSQL_USER=myapp
# user password
MYSQL_PASSWORD=myapp
# db_name
MYSQL_DATABASE=myapp
flask.env
# For wait_db_ready.py
MYSQL_CHECK_TIMEOUT=30
MYSQL_CHECK_INTERVAL=1
# Tell 'flask run' where app is
FLASK_APP=apps.site
# Url for SQLALCHEMY
SQLALCHEMY_DATABASE_URI=mysql://myapp:myapp@flask_mysql_container/myapp?charset=utf8mb4
- 先写一个最简单的Helloworld
当前目录下建立一个apps包用来管理应用,增加site.py源码:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World!'
if __name__ == '__main__':
app.run(host='0.0.0.0')
- 初始的目录结构
$ tree -a
.
├── apps #建立一个包管理应用
│ ├── static #静态文件目录
│ ├── templates #模板目录
│ ├── __init__.py
│ └── site.py #第一个应用
├── docker-compose.yml #docker-compose.yml
├── Dockerfile #dockerfile
├── docs #mkdocs容器的映射数据目录
│ ├── index.md #mkdocs文档首页,内容空
│ └── api
│ └── helloworld.md #mkdocs测试文档,内容空
├── .envs
│ ├── flask.env #环境变量
│ └── mysql.env #mysql环境变量设置
├── mysql_data #映射的mysql容器数据目录,容器启动后自动生成(docker组权限)
├── requirements.txt #docker的包需求
├── mkdocs.yml #mkdoc服务配置
└── wait_db_ready.py #防止mysql服务还没启动完成app容器先启动了
- 给项目mkdocs容器一个初始配置,用来提供以后的文档
site_name: myappdocs
site_description: myapp mkdocs site
site_dir: site
copyright: Copyright © 2019, <a>myapp site</a>.
dev_addr: 0.0.0.0:80
nav:
- Home: 'index.md'
- API:
- helloworld: 'api/helloworld.md'
- 试验docker容器是否正常:
$ docker-compose up
正常的话浏览器访问http://localhost:8080应该能看到Hello World!了,MySQL容器起来后mysql_data会生成数据库的文件及myapp数据库。http://localhost:8081是mkdocs服务首页。
- 继续配置PyCharm
打开当前项目文件夹,File->Settings中当前项目的Project Interpreter增加一个Docker Compose项,设置好Docker以及当前目录的docker-compose.yml,选择Service为flask_server,调试器Edit Configurations新增一个Flask Server模板,Additional options:填写-h 0.0.0.0
以让外部访问。在app.py中下断点,调试看是否能断下,以后就可以直接在PyCharm调试启动容器了。
应用的启动入口是docker-compose.yml中flask_server指定的
command: > bash -c "python wait_db_ready.py flask run -h 0.0.0.0"
,wait_db_ready.py是实现了一个死循环直到数据库容器能连接,再启动flask应用。flask run
从环境变量中获得FLASK_APP
变量的值并加载(这里是apps.site,放在了apps包里),Flask官方不建议用python app.py
方式启动,可能是要强调跟部署的一致性。 - 如果启动版本控制,在项目目录运行:
$ git init
之后可以在PyCharm进行操作了。在码云上建了一个仓库,随学习更新。
https://gitee.com/njbinbin/flask_demo.git
二、MVT
基于Model,View和Template的应用。
首先对项目进行一些改造
- static目录下存放静态文件,如site.css全局css,先做一个简单的备用:
body {
background: #f0f0f0;
}
- templates目录下存放模板
1. 模板
模板默认在项目templates目录下,先建立base.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>{{Title}}</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='site.css') }}">
</head>
<body>
{% block contents %}
{% endblock %}
</body>
</html>
再继承一个首页模板index.html:
{% extends 'base.html' %}
{% block contents %}
{{ Welcome }}
{% endblock %}
现在使用模板实现Hello World,修改site.py:
from flask import Flask, render_template
app = Flask(__name__, static_folder='static', static_url_path='/static', template_folder='templates')
@app.route('/')
def hello_world():
# return 'Hello World!'
return render_template('index.html', Title='My flask site', Welcome='Hello World!')
if __name__ == '__main__':
app.run(host='0.0.0.0')
2. View与路由
flask的路由用装饰器实现,就像在前面site.py中实现的,在函数前面修饰后即将url与处理函数绑定:
# 这里将根url绑定到hello_world函数上
@app.route('/')
def hello_world():
return 'Hello World!'
@app.route('/api/v1/<user>/<action>')
这样带参数也是可以的,绑定的函数对应接收两个参数:def func(user, action)
。与django不同,flask的路由没有像urls.py这样统一的配置入口,如果直接用装饰器与函数在一起生成则可能随模块分散在各处管理起来不太方便。大型项目建议使用Blueprint来管理。项目下新建一个test_route包,其中加一个views.py:
from flask import Blueprint, render_template
test = Blueprint('test', __name__)
@test.route('/hello')
def test_hello():
return render_template('index.html', Title="My flask site - test route", Welcome="Hello again!")
site.py现在用来集中管理各模块的路由:
from flask import Flask, render_template
#引入test_route包views模块中的test对象
from apps.test_route.views import test
app = Flask(__name__, static_folder='static', static_url_path='/static', template_folder='templates')
#将引入的test_route.views/test其注册到蓝图中统一管理,前缀test
app.register_blueprint(test, url_prefix='/test')
@app.route('/')
def hello_world():
# return 'Hello World!'
return render_template('index.html', Title='My flask site', Welcome='Hello World!')
if __name__ == '__main__':
app.run(host='0.0.0.0')
实际的路由为前缀+模块用装饰器注册的路由,这里apps.test_route.views/test_hello函数对应的URL是/test/hello/
3. ORM与数据库
- 配置config.py,更新flask.env和site.py
将相关的参数单独放到一个config.py中,与site.py相同目录:
import os
# 静态文件配置
STATIC_DIR = os.getenv('STATIC_DIR', 'static')
STATIC_URL = os.getenv('STATIC_URL', '/static')
TEMPLATE_DIR = os.getenv('TEMPLATE_DIR', 'templates')
# 读取static目录下的css文件存入列表,在base.html中演示使用模板循环
STYLE_SHEETS = []
for f in os.listdir(os.path.join(os.path.dirname(__file__), STATIC_DIR)):
if f.endswith('.css'):
STYLE_SHEETS.append(f)
# 数据庫配置
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI', '')
SQLALCHEMY_TRACK_MODIFICATIONS = os.getenv('SQLALCHEMY_TRACK_MODIFICATIONS', '').upper() == 'TRUE'
参数来自环境变量,更新flask.env
# For wait_db_ready.py
MYSQL_CHECK_TIMEOUT=30
MYSQL_CHECK_INTERVAL=1
# Tell flask run where apps in
FLASK_APP=apps.site
# For SQLALCHEMY
SQLALCHEMY_DATABASE_URI=mysql+pymysql://myapp:myapp@flask_mysql_container/myapp?charset=utf8mb4
SQLALCHEMY_MIGRATE_REPO=./migrations
SQLALCHEMY_TRACK_MODIFICATIONS=True
# For static files and templates
STATIC_DIR=static
STATIC_URL=/static
TEMPLATES_DIR=templates
site.py增加from flask_sqlalchemy import SQLAlchemy
和import apps.config
两个引入
修改app对象的建立代码:
app = Flask(__name__, static_folder=apps.config.STATIC_DIR, static_url_path=apps.config.STATIC_URL, template_folder=apps.config.TEMPLATE_DIR)
app.config.from_object(apps.config)
- 建立数据库对象
site.py中app对象建立后,增加一行代码建立一个数据库对象与app绑定:db = SQLAlchemy(app)
- Models与ORM
建立user包和post包,用来存放用户及贴子的模型与视图,先建立model
user和post包中增加models.py
user/models.py:
from apps.site import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return '<User {}>'.format(self.username)
post/models.py:
from datetime import datetime
from apps.site import db
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return '<Post {}>'.format(self.body)
- 表间关系的模型化
需要注意的一点是,在处理模型关系时(DBMS的核心),主外键关系可以通过ORM来实现操作,即将关联表的关联对象或关联对象列表作为本模型的一个属性,这个属性在关系两边的模型中都可以声明,在参数中使用backref做一个反向参照即可在模型两边各生成一对属性,还可以通过lazy参数用于指定一对多或多对多关系中多方的数据是即时生成(获得对象列表),还是延迟查询(一个查询对象,其数据再通过对象的first(),filter()等方法查询),默认是“select”即时生成,一般大型应用我们用"dynamic"提高性能,因为它涉及的是一个多数据的处理方式,所以这一端一定是“多”,因此只能定义在一个方向上,如在另一边定义,那就得在backref参数上使用db.backref()方法,把lazy='dynamic'用在这个方法的参数里:
# 假设主表是TypeDef类别表,从表是Product产品表
# 在主表这边定义
class User(db.Model):
# ...
posts = db.relationship('Post', backref='user', lazy='dynamic')
#定义一个延迟加载的贴子列表查询对象在主表User模型中,同时在从表Post模型中生成一个user属性指向所属用户
# 也可以这样在另一边定义
class Post(db.Model):
# ...
user = db.relationship('User', backref=db.backref('posts', lazy='dynamic'))
#将用户user属性定义在从表Post模型中,同时在主表User模型中生成一个延迟加载的posts列表查询对象
另外,back_populates是广泛使用的backref的升级版,backref仍然可以使用,并且将一直可以使用。back_populates只是更加冗长、更容易操作,relatioinship和back_populates参数只有放在TODO:里慢慢研究了。
- 数据迁移与管理,写一个manage.py(现在flask直接支持flask db init/migrate/upgradet管理数据迁移了,在
db = SQLAlchemy(app)
后面加一句migrate = Migrate(app, db)
,再import需要迁移的models即可):
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from apps.site import app
from apps.site import db
#要迁移维护的模型,引入即可
import apps.user.models
import apps.post.models
manager = Manager(app)
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
为三条数据库命令建立对应的PyCharm运行调试参数配置并分析运行(Run/Debug configurations)
Script path为manage.py
Parameters分别为db init
、db migrate
和db upgrade
Working directory是项目所在主机目录的绝对路径,我的是/home/zzz/Projects/myFlask
分别运行后数据库应该迁移完成,可以尝试在python环境操作数据库并查询,看是否正常工作:
进入PyCharm的Python Console标签页,将激活容器中的Python环境:
/usr/bin/docker-compose -f /home/zzz/Projects/myFlask/docker-compose.yml -f /home/zzz/.PyCharm2019.3/system/tmp/docker-compose.override.1323.yml run --rm -p 0.0.0.0:35361:35361 flask_server
Starting flask_docs ...
Starting flask_mysql_container ...
import sys; print('Python %s on %s' % (sys.version, sys.platform))
sys.path.extend(['/myapp', '/myapp'])
PyDev console: starting.
Python 3.7.7 (default, Mar 11 2020, 00:27:03)
[GCC 8.3.0] on linux
>>> from apps.site import db
>>> from apps.post.models import Post
>>> from apps.user.models import User
>>> u = User(username='john', email='john@email.com')
>>> db.session.add(u)
>>> db.session.commit()
>>> users = User.query.all()
>>> for u in users:
... print(u.id, u.username)
...
1 john
>>> u = User.query.get(1)
>>> u
<User 'john'>
>>> import datetime
>>> p = Post(body='my first post!', timestamp=datetime.datetime.utcnow(), author=u)
>>> db.session.add(p)
>>> db.session.commit()
休息一下,现在的项目结构如下(不含.git,mysql_data等文件):
$ tree
.
├── apps
│ ├── __init__.py
│ ├── config.py
│ ├── site.py
│ ├── static
│ │ └── site.css
│ ├── templates
│ │ ├── base.html
│ │ └── index.html
│ ├── test_route
│ │ ├── __init__.py
│ │ └── views.py
│ ├── post
│ │ ├── __init__.py
│ │ └── models.py
│ └── user
│ ├── __init__.py
│ └── models.py
├── docs
│ ├── api
│ │ └── helloworld.md
│ └── index.md
├── migrations # 自动生成的数据迁移目录
├── manage.py
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── mkdocs.yml
├── README.md
├── LICENSE
└── wait_db_ready.py
- 为user和post编写views、模板及路由,在views用ORM存取数据并渲染到模板中即可呈现数据库中的信息,如果一个纯展示的网站这就够了,但对于一个用户系统,在没有登录和认证机制之前,这些还没有意义,所以继续实现相关的登录功能并完善相关的MTV。
三、登录与认证
处理登录与认证,使用flask-login包。