《Flask Web开发:基于Python的Web应用开发实战》
virtualenv venv
venv\Scripts\activate.bat
(venv) $ pip freeze >requirements.txt
(venv) $ pip install -r requirements.txt
pip list --outdated
pip install --upgarade
git tag 列出所有打tag的分支
git checkout 切换到tag
git reset --hard 不保留修改
.gitignore:指定哪些文件或目录不作同步,比如 ./venv/,*.pyc,数据库文件.sqlite3, .mysql
# -*- coding: utf-8 -*-
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World!
'
修饰器是Python语言的标准特性,可以使用不同的方式修改函数的行为。惯常用法是使用修饰器把函数注册为事件的处理程序。
@app.route('/user/')
def user(name):
return 'Hello, %s!
' %name
@app.route('/user/') # 不能有空格!
def ...
app.run(debug=True, port=7777)
app.url_map
Map([ index>,
' (HEAD, OPTIONS, GET) -> static>,
' (HEAD, OPTIONS, GET) -> user>])
HEAD、Options、GET是请求方法,由路由进行处理。Flask为每个路由都指定了请求方法,这样不同的请求方法发送到相同的URL上时,会使用不同的视图函数进行处理。HEAD和OPTIONS方法由Flask自动处理
response = make_response('This document carries a cookie!
', 200)
response.set_cookie('answer', '42')
return response
@app.route('/user/')
def user(name):
return render_template('user.html', name=name)
模板变量
A value from a dictionary: {{ mydict['key'] }}.
A value from a list: {{ mylist[3] }}.
A value from a list, with a variable index: {{ mylist[myintvar] }}.
A value from an object's method: {{ myobj.somemethod() }}.
Hello, {{ name|capitalize }
{%block head %}
{%block title %}{%endblock %} - My Application
{%endblock %}
{%block body %}
{%endblock %}
extends指令声明这个模板衍生自base.html。在
extends指令之后,基模板中的3个块被重新定义,模板引擎会将其插入适当的位置。注意新定义的head块,在基模板中其内容不是空的,所以使用
super()获取原来的内容(向已经有内容的块中添加新内容)。
{%extends "base.html" %}
{%block title %}Index{%endblock %}
{%block head %}
{{ super() }}
{%endblock %}
{%block body %}
Hello, World!
{%endblock %}
使用Flask-Bootstrap
{% block head %}
{{super()}}
{% endblock %}
。。。
{% block scripts %}
{{super()}}
{{ moment.include_moment() }}
{{ moment.lang("zh-CN") }}
{% endblock %}
2016-10-20: 以上本地加速证明是有误的!会重复下载本地和官方Bootstrap 的 css/js
解决:
/app/__init__.py
from flask_bootstrap import Bootstrap, WebCDN, ConditionalCDN, BOOTSTRAP_VERSION, JQUERY_VERSION, HTML5SHIV_VERSION, RESPONDJS_VERSION
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
bootstrap.init_app(app)
def change_cdn_domestic(tar_app):
static = tar_app.extensions['bootstrap']['cdns']['static']
local = tar_app.extensions['bootstrap']['cdns']['local']
def change_one(tar_lib, tar_ver, fallback):
tar_js = ConditionalCDN('BOOTSTRAP_SERVE_LOCAL', fallback,
WebCDN('//cdn.bootcss.com/' + tar_lib + '/' + tar_ver + '/'))
tar_app.extensions['bootstrap']['cdns'][tar_lib] = tar_js
libs = {'jquery': {'ver': JQUERY_VERSION, 'fallback': local},
'bootstrap': {'ver': BOOTSTRAP_VERSION, 'fallback': local},
'html5shiv': {'ver': HTML5SHIV_VERSION, 'fallback': static},
'respond.js': {'ver': RESPONDJS_VERSION, 'fallback': static}}
for lib, par in libs.items():
change_one(lib, par['ver'], par['fallback'])
change_cdn_domestic(app)
# 。。。
return app
另外:本地加速 moment.js
这个文件也在国外服务器,访问很慢,而且 size=160KB
/app/templates/base.html
{% block scripts %} {{ super() }} {{ moment.include_moment(local_js="/static/moment-with-locales.min.js") }} {{ moment.lang('zh-CN') }} {% endblock %}
需要单独下载 https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.3/moment-with-locales.min.js 到本地目录 /app/static/
自定义错误页面
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
url_for() 链接辅助函数
{%block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{{ moment.lang("zh-CN") }}
{%endblock %}
from flask_wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NameForm(Form):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
password = PasswordField('Password', validators=[DataRequired()], render_kw={"placeholder": u"密码"})
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'))
MySQLdb 中文乱码的处理:
conn = MySQLdb.connect(host='localhost', user='root', passwd='XXX', db='app_englishgo', charset = 'utf8')
显示:title.encode('gbk')
接收输入:unicode(request.form['title'])
Relationship 关系型数据库
class Role(db.Model):
# ...
users = db.relationship('User', backref='role') # 面向对象视角
class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) # 定义外键
engine = create_engine('sqlite:///C:\\Temp\\testsqlalchemy.db', encoding='utf8', convert_unicode=True, echo=True)
# echo:显示出内部过程及SQL语句。debug或学习时打开
# encoding:防止乱码
'mysql://uid:pwd@localhost/mydb?charset=utf8'
集成Python shell
每次启动 shell 会话都要导入数据库实例和模型。为避免重复导入,我们可以做些配置,让 Flask-Script 的 shell 命令自动导入特定的对象。为 shell 命令注册一个 make_context 回调函数
新数据库迁移 flask-migrate
由于模型中经常会新加一行或几行column (比如用来保存账户的确认状态),此时要修改 models.py,并执行一次新数据库迁移
MySQL Workbench 自动产生EER,可以清楚地看到各个表格之间关系:一对多 Foreign_Key、Index等
1) config.py:
class DevelopmentConfig(Config):
DEBUG = True
# SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
SQLALCHEMY_DATABASE_URI = 'mysql://USER:PASSWORD@localhost:3306/flaskr'
2) python manage.py deploy
3) MySQL Workbench: Database -> Reverse Engineer -> 选择 connection -> database -> 一路 Next
app.config['MAIL_SERVER'] = 'smtp.163.com'
app.config['MAIL_PORT'] = 25
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin '
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
mail = Mail(app)
def sendmail(mail):
msg = Message('test subject', sender='[email protected]', recipients = ['[email protected]'])
msg.body = 'text body'
msg.html = 'HTML body'
with app.app_context():
mail.send(msg)
千万不要把账户密令直接写入脚本,特别是当你计划开源自己的作品时。让脚本
从本机环境中导入敏感信息
(venv) $ set MAIL_USERNAME=
(venv) $ set MAIL_PASSWORD=
|-flasky
|-app/ Flask 程序一般都保存在名为 app 的程序包中
|-templates/ templates 和 static 文件夹是程序包的一部分
|-static/
|-main/ 程序包中创建了一个子包,用于保存蓝本。/main/__init__.py脚本的末尾导入views.py & errors.py,避免循环导入依赖
|-__init__.py 程序工厂函数 create_app(),注册蓝本
|-errors.py 错误处理路由. 注册程序全局的错误处理程序,必须使用 app_errorhandler
|-forms.py 表单对象
|-views.py 路由。蓝本中的全部端点会加上一个命名空间,如 url_for('main.index')
|-__init__.py
|-email.py 电子邮件支持函数
|-models.py 数据库模型
|-migrations/ 数据库迁移脚本
|-tests/ 单元测试
|-__init__.py 文件可以为空,因为 unittest 包会扫描所有模块并查找测试
|-test*.py
|-venv/ 虚拟环境
|-requirements.txt 列出了所有依赖包,便于在其他电脑中重新生成相同的虚拟环境
|-config.py 存储配置。开发、测试和生产环境要使用不同的数据库
|-manage.py 用于启动程序以及其他的程序任务
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
return redirect(request.args.get('next') or url_for('main.index'))
>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
>>> s = Serializer(app.config['SECRET_KEY'], expires_in = 3600)
>>> token = s.dumps({ 'confirm': 23 })
>>> token
'eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4MTcxODU1OCwiaWF0IjoxMzgxNzE0OTU4fQ.ey ...'
>>> data = s.loads(token)
>>> data
{u'confirm': 23}
def gravatar(self, size=100, default='identicon', rating='g'):
import random
return '%.3d.jpg' % random.randint(1, XXX)
_posts.html:
_commments.html:
效果:
扩展TODO:加入性别、用户自选头像。。。
11 Blog articles 博客文章
2016-6-12
实现
功能,即允许用户阅读、撰写博客文章。本章新技术:重用模板、分页显示长列表以及处理富文本。
生成虚拟信息用于测试,其中功能相对完善的是ForgeryPy
python manage.py shell
>>> User.generate_fake()
>>> Post.generate_fake(200)
添加分页导航
paginate() 方法的返回值是一个 Pagination 类对象,这个类在 Flask-SQLAlchemy 中定义。这个对象包含很多属性, 用于在模板中生成分页链接
使用Markdown和Flask-PageDown支持富文本文章
• PageDown: 使用JavaScript实现的客户端Markdown到HTML的转换程序。
• Flask-PageDown: 为Flask包装的PageDown,把PageDown集成到Flask-WTF表单中。
• Markdown: 使用Python实现的服务器端Markdown到HTML的转换程序。
• Bleach: 使用Python实现的HTML清理器。
博客文章的固定链接
博客文章编辑器
12 followers 关注者
2016-6-12
再论数据库关系
一对多关系:是最常用的关系类型,它把一个记录和一组相关的记录联系在一起。实现这种关系时,要在“多”这一侧加入一个外键,指向“一”这一侧联接的记录。
多对多关系: 这种问题的解决方法是添加第三张表, 这个表称为关联表。多对多关系可以分解成原表和关联表之间的两个一对多关系
若想显示所关注用户发布的所有文章,第一步显然先要获取这些用户,然后获取各用户的文章,再按一定顺序排列,写入单独列表。可是这种方式的伸缩性不好,随着数据库不断变大,生成这个列表的工作量也不断增长,而且分页等操作也无法高效率完成。获取博客文章的高效方式是只用一次查询。
完成这个操作的数据库操作称为联结。联结操作用到两个或更多的数据表, 在其中查找满足指定条件的记录组合, 再把记录组合插入一个临时表中,这个临时表就是联结查询的结果。
创建函数更新数据库这一技术经常用来更新已部署的程序,因为运行脚本更新比手动更新数据库更少出错。
13 User comments 评论
评论属于某篇博客文章,因此定义了一个从posts表到comments表的一对多关系。使用这个关系可以获取某篇特定博客文章的评论列表。
comments 表还和 users 表之间有一对多关系。通过这个关系可以获取用户发表的所有评论,还能间接知道用户发表了多少篇评论
管理评论,我们要在导航条中添加一个链接,具有权限的用户才能看到。
14 RIA (API, REST)
2016-6-12
资源就是一切
资源是REST架构方式的核心概念。在REST架构中, 资源是程序中你要着重关注的事物。例如,在博客程序中,用户、博客文章和评论都是资源。
15 Testing
16 Performance 性能
17 Deploy 部署
创建新的Shell命令 deploy:
@manager.command
def deploy():
"""Run deployment tasks."""
from flask.ext.migrate import upgrade
from app.models import Role, User
# migrate database to latest revision
upgrade()
# create user roles
Role.insert_roles()
# create self-follows for all users
User.add_self_follows()
# 测试:User.generate_fake(100)
# Post.generate_fake(500)
Flask自带的开发Web服务器表现很差,因为它不是为生产环境设计的服务器。有两个可以在生产环境中使用、性能良好且支持Flask程序的服务器,分别是 Gunicorn 和 uWSGI。
manage:app参数冒号左边的部分表示定义程序的包或者模块,冒号右边的部分表示包中程序实例的名字。注意,Gunicor默认使用端口8000,而Flask默认使用5000。
添加ProxyFix等WSGI中间件的方法是包装WSGI程序。收到请求时,中间件有机会审查环境,在处理请求之前做些修改。不仅Heroku需要使用ProxyFix中间件,任何使用反向代理的部署环境都需要。
架设服务器
在能够托管程序之前,服务器必须完成多项管理任务。
- 安装数据库服务器,例如MySQL或Postgres。也可使用SQLite数据库,但由于其自身 •的种种限制,不建议用于生产服务器。
- 安装邮件传输代理(Mail Transport Agent,MTA),例如Sendmail,用于向用户发送邮件。
- 安装适用于生产环境的Web服务器,例如Gunicorn或uWSGI。
- 为了启用安全HTTP,购买、安装并配置SSL证书。
- (可选,但强烈推荐)安装前端反向代理服务器,例如nginx或Apache。反向代理服务器能直接服务于静态文件,而把其他请求转发给程序使用的Web服务器。Web服务器
- 监听localhost中的一个私有端口。
- 强化服务器。这一过程包含多项任务,目标在于降低服务器被攻击的可能性,例如安装防火墙以及删除不用的软件和服务等
其它:
Flask-Cache插件为你提供一组装饰器来实现多种方式的缓存
开发自定义视图装饰器来帮助我们组织自己的代码
自定义的URL转换器将会让你很嗨地玩转URL:https://spacewander.github.io/explore-flask-zh/6-advanced_patterns_for_views_and_routing.html
Write a Tumblelog Application with Flask and MongoEngine
这是MongoDB官方文档中的一个教程,也是学习Flask开发的一个很好案例,尤其适合Flask+MongoDB开发的应用场景
The Hitchhiker’s Guide to Python!
这个资料虽然不直接与Flask有关,但对初学者,绝对有学习的价值
GitHub - humiaozuzu/awesome-flask: A curated list of awesome Flask resources and plugins
作者早些时候写的网上教程:
The Flask Mega-Tutorial, Part I: Hello, World! - miguelgrinberg.com
大多数内容是本书上写的更加 advanced,但以下话题只在“Mega Tutorial”里提到:
- 全文检索:
- 国际化:I18n and L10n
- Ajax
- Debug:pdb
查找Flask扩展
一些值得研究的包。
- Flask-Babel(https://pythonhosted.org/Flask-Babel/):提供国际化和本地化支持。
- FLask-RESTful(http://flask-restful.readthedocs.org/en/latest/):开发REST API的工具。
- Celery(http://docs.celeryproject.org/en/latest/):处理后台作业的任务队列。 •
- Frozen-Flask(https://pythonhosted.org/Frozen-Flask/):把Flask程序转换成静态网站。
- Flask-DebugToolbar:在浏览器中使用的调试工具。
- Flask-Assets(https://github.com/miracle2k/flask-assets):用于合并、压缩、编译CSS和JavaScript静态资源文件。
- Flask-OAuth(http://pythonhosted.org/Flask-OAuth/):使用OAuth服务进行认证。
- Flask-OpenID(http://pythonhosted.org/Flask-OpenID/):使用OpenID服务进行认证。
- Flask-WhooshAlchemy: 使 用Whoosh实现Flask-SQLAlchemy模型的全文搜索。
- Flask-KVsession(http://flask-kvsession.readthedocs.org/en/latest/):使用服务器端存储实现的另一种用户会话。
Flask官方扩展网站
PEP & Import
PEP 8: 每级缩进使用4个空格。不要使用tab
PEP 257: docstrings""" docstrings """
用了相对形式的import 点标记法:第一个.来表示当前目录,之后的每一个.表示下一个父目录。
from .models
import User
什么时候会用到蓝图?
https://spacewander.github.io/explore-flask-zh/7-blueprints.html
什么是蓝图?
一个蓝图定义了可用于单个应用的视图,模板,静态文件等等的集合。举个例子,想象一下我们有一个用于管理面板的蓝图。这个蓝图将定义像/admin/login和/admin/dashboard这样的路由的视图。它可能还包括所需的模板和静态文件。你可以把这个蓝图当做你的应用的管理面板,管它是宇航员的交友网站,还是火箭推销员的CRM系统。
蓝图的杀手锏是将你的应用组织成不同的组件。假如我们有一个微博客,我们可能需要有一个蓝图用于网站页面,比如index.html和about.html。然后我们还需要一个用于在登录面板中展示最新消息的蓝图,以及另外一个用于管理员面板的蓝图。站点中每一个独立的区域也可以在代码上隔绝开来。最终你将能够把你的应用依据许多能完成单一任务的小应用组织起来。
- 一个蓝图包括了可以作为独立应用的视图,模板,静态文件和其他插件。
- 蓝图是组织你的应用的好办法。
- 在分区式架构下,每个蓝图对应你的应用的一个部分。
- 在功能式架构下,每个蓝图就只是视图的集合。所有的模板和静态文件都放在一块。
- 要使用蓝图,你需要定义它,并在应用中用
Flask.register_blueprint()
注册它。
- 你可以给一个蓝图中的所有路由定义一个动态URL前缀。
- 你也可以给蓝图中的所有路由定义一个动态子域名。
- 仅需五步走,你可以用蓝图重构一个应用。
flask-bootstrap 前端插件
pip install flask-sqlalchemy
pip install flask-login
...
from flask.ext.bootstrap import Bootstrap
bootstrap = Bootstrap(app)
然后 /template/目录下创建自己的 html,{% extends "bootstrap/base.html" %}
模板:\Lib\site-packages\flask_bootstrap\templates\bootstrap\
Bootstrap 导航栏 自定义配色:
{% block styles %}
{{super()}}
{% endblock %}
其中 style.css 可以包含自定义的 .navbar-kevin {。。。}
配色方案 http://work.smarchal.com/twbscolor/
flask_debugtoolbar
修改文件:/app/__init__.py
from flask_debugtoolbar import DebugToolbarExtension
toolbar = DebugToolbarExtension()
toolbar.init_app(app)