Flask学习日志-持续更新中

今天开始研究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 &copy; 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 SQLAlchemyimport 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 initdb migratedb 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包。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容