《Flask Web开发:基于Python的Web应用开发实战》
1 虚拟环境
- virtualenv venv
- venv\Scripts\activate.bat
- (venv) $ pip freeze >requirements.txt
- (venv) $ pip install -r requirements.txt
- pip list --outdated
- pip install --upgarade <Package1> <PackageN>
- git tag 列出所有打tag的分支
- git checkout <tag_name> 切换到tag
- git reset --hard 不保留修改
2 基本结构
- # -*- coding: utf-8 -*-
- from flask import Flask
- app = Flask(__name__)
- @app.route('/')
- def index():
- return '<h1>Hello World!</h1>'
- @app.route('/user/<name>')
- def user(name):
- return '<h1>Hello, %s!</h1>' %name
- @app.route('/user/<int:id>') # 不能有空格!
- def ...
- app.run(debug=True, port=7777)
- 公认端口(Well Known Ports):从0到1023,紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务协议。80端口实际上总是HTTP通讯。
- 注册端口(Registered Ports):从1024到49151。它们松散地绑定于一些服务。这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
- 动态和/或私有端口(Dynamic and/or Private Ports):从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。
请求-响应循环
- current_app 程序上下文 当前激活程序的程序实例
- g 程序上下文 处理请求时用作临时存储的对象。每次请求都会重设这个变量
- request 请求上下文 请求对象,封装了客户端发出的HTTP请求中的内容
- session 请求上下文 用户会话,用于存储请求之间需要“记住”的值的词典
- app.url_map
- Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>,
- <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
- <Rule '/user/<name>' (HEAD, OPTIONS, GET) -> user>])
- before_first_request • :注册一个函数,在处理第一个请求之前运行。
- before_request • :注册一个函数,在每次请求之前运行。
- after_request • :注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行。
- teardown_request • :注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行
- make_response()函数可接受1~3个参数 (html, 状态码,header)
- response = make_response('<h1>This document carries a cookie!</h1>', 200)
- response.set_cookie('answer', '42')
- return response
- 重定向的特殊响应类型,302:return redirect('http://www.example.com')
- 特殊的响应由abort函数生成,用于处理错误:abort(404)
3 模板 template
- @app.route('/user/<name>')
- def user(name):
- return render_template('user.html', name=name)
- <p>A value from a dictionary: {{ mydict['key'] }}.</p>
- <p>A value from a list: {{ mylist[3] }}.</p>
- <p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
- <p>A value from an object's method: {{ myobj.somemethod() }}.</p>
- Hello, {{ name|capitalize }
- safe 渲染值时不转义。默认情况下,出于安全考虑,Jinja2会转义所有变量
- capitalize 把值的首字母转换成大写,其他字母转换成小写
- lower 把值转换成小写形式
- upper 把值转换成大写形式
- title 把值中每个单词的首字母都转换成大写
- trim 把值的首尾空格去掉
- striptags 渲染之前把值中所有的HTML标签都删掉
- <html>
- <head>
- {%block head %}
- <title>{%block title %}{%endblock %} - My Application</title>
- {%endblock %}
- </head>
- <body>
- {%block body %}
- {%endblock %}
- </body>
- </html>
- {%extends "base.html" %}
- {%block title %}Index{%endblock %}
- {%block head %}
- {{ super() }}
- <style>
- </style>
- {%endblock %}
- {%block body %}
- <h1>Hello, World!</h1>
- {%endblock %}
Flask-Bootstrap基模板中定义的块:doc, head, title, styles, body, navbar, content, scripts
- {% block head %}
- {{super()}}
- <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
- <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
- <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css">
- <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap-theme.min.css">
- {% endblock %}
- 。。。
- {% block scripts %}
- {{super()}}
- <script src="//cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
- <script src="//cdn.bootcss.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
- {{ 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
- {%block scripts %}
- {{ super() }}
- {{ moment.include_moment() }}
- <!--使用中文,默认是英语的-->
- {{ moment.lang("zh-CN") }}
- {%endblock %}
4 Web Form表单
- 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"密码"})
很多用户都不理解浏览器发出的这个警告。 基于这个原因,最好别让 Web 程序把 POST 请求作为浏览器发送的最后一个请求。
- @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'))
Flash消息
5 数据库
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')) # 定义外键
SQLAlchemy 完整的命令、过滤器、查询执行 列表参见 SQLAlchemy 文档
- 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'
3) mysql Workbench: Database -> Reverse Engineer -> 选择 connection -> database -> 一路 Next
6 E-mail
- 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 <[email protected]>'
- 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 = '<b>HTML</b> body'
- with app.app_context():
- mail.send(msg)
- (venv) $ set MAIL_USERNAME=<your mail username>
- (venv) $ set MAIL_PASSWORD=<mail password>
7 大型程序的结构
- |-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 用于启动程序以及其他的程序任务
requirements.txt 文件,用于记录所有依赖包及其精确的版本号。以便要在另一台电脑上重新生成虚拟环境
8 用户认证
Werkzeug:计算密码散列值并进行核对。
itsdangerous:生成并核对加密安全令牌。
- <ul class="nav navbar-nav navbar-right">
- {% if current_user.is_authenticated %}
- <li><a href="{{ url_for('auth.logout') }}" title = "邮件 {{ current_user.email[:9]+'...' }}">Log Out {{ current_user.username }}</a></li>
- {% else %}
- <li><a href="{{ url_for('auth.login') }}">Log In</a></li>
- {% endif %}
- </ul>
按照第 4 章介绍的“Post/ 重定向 /Get 模式”,Login的 POST 请求最后也做了重定向,不过目标 URL 有两种可能。用户访问未授权的 URL 时会显示登录表单,Flask-Login 会把原地址保存在查询字符串的 next参数中,这个参数可从 request.args 字典中读取。如果查询字符串中没有 next 参数,则重定向到首页
- 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'))
用户注册表单 app/auth/forms.py
- >>> 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}
对蓝本来说, before_request 钩子只能应用到属于蓝本的请求上。若想在蓝本中使用针对程序全局请求的钩子,必须使用 before_app_request 修饰器
9 User Role 角色
10 User Profile 资料
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">
- <img height="24px" src="{{ url_for('static', filename='avatar/')}}{{ current_user.avatar_hash }}">
models.py:
- def gravatar(self, size=100, default='identicon', rating='g'):
- import random
- return '%.3d.jpg' % random.randint(1, XXX)
- <div class="page-header">
- <img class="img-rounded profile-thumbnail" src="{{ url_for('static', filename='avatar/')}}{{ current_user.avatar_hash }}"">
- <li class="post">
- <div class="post-thumbnail">
- <a href="{{ url_for('.user', username=post.author.username) }}">
- <img height="40px" class="img-rounded profile-thumbnail" src="{{ url_for('static', filename='avatar/')}}{{ post.author.avatar_hash }}">
- <div class="comment-thumbnail">
- <a href="{{ url_for('.user', username=comment.author.username) }}">
- <img height="40px' class="img-rounded profile-thumbnail" src="{{ url_for('static', filename='avatar/')}}{{ post.author.avatar_hash }}">
11 Blog articles 博客文章
生成虚拟信息用于测试,其中功能相对完善的是ForgeryPy
- python manage.py shell
- >>> User.generate_fake()
- >>> Post.generate_fake(200)
添加分页导航
paginate() 方法的返回值是一个 Pagination 类对象,这个类在 Flask-SQLAlchemy 中定义。这个对象包含很多属性, 用于在模板中生成分页链接
• PageDown: 使用JavaScript实现的客户端Markdown到HTML的转换程序。
• Flask-PageDown: 为Flask包装的PageDown,把PageDown集成到Flask-WTF表单中。
• Markdown: 使用Python实现的服务器端Markdown到HTML的转换程序。
• Bleach: 使用Python实现的HTML清理器。
博客文章的固定链接
博客文章编辑器
12 followers 关注者
若想显示所关注用户发布的所有文章,第一步显然先要获取这些用户,然后获取各用户的文章,再按一定顺序排列,写入单独列表。可是这种方式的伸缩性不好,随着数据库不断变大,生成这个列表的工作量也不断增长,而且分页等操作也无法高效率完成。获取博客文章的高效方式是只用一次查询。
完成这个操作的数据库操作称为联结。联结操作用到两个或更多的数据表, 在其中查找满足指定条件的记录组合, 再把记录组合插入一个临时表中,这个临时表就是联结查询的结果。
创建函数更新数据库这一技术经常用来更新已部署的程序,因为运行脚本更新比手动更新数据库更少出错。
13 User comments 评论
comments 表还和 users 表之间有一对多关系。通过这个关系可以获取用户发表的所有评论,还能间接知道用户发表了多少篇评论
14 RIA (API, REST)
资源是REST架构方式的核心概念。在REST架构中, 资源是程序中你要着重关注的事物。例如,在博客程序中,用户、博客文章和评论都是资源。
15 Testing
16 Performance 性能
17 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)
在能够托管程序之前,服务器必须完成多项管理任务。
- 安装数据库服务器,例如MySQL或Postgres。也可使用SQLite数据库,但由于其自身 •的种种限制,不建议用于生产服务器。
- 安装邮件传输代理(Mail Transport Agent,MTA),例如Sendmail,用于向用户发送邮件。
- 安装适用于生产环境的Web服务器,例如Gunicorn或uWSGI。
- 为了启用安全HTTP,购买、安装并配置SSL证书。
- (可选,但强烈推荐)安装前端反向代理服务器,例如nginx或Apache。反向代理服务器能直接服务于静态文件,而把其他请求转发给程序使用的Web服务器。Web服务器
- 监听localhost中的一个私有端口。
- 强化服务器。这一过程包含多项任务,目标在于降低服务器被攻击的可能性,例如安装防火墙以及删除不用的软件和服务等
其它:
Flask-Cache插件为你提供一组装饰器来实现多种方式的缓存
开发自定义视图装饰器来帮助我们组织自己的代码
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
作者早些时候写的网上教程:
- 全文检索:
- 国际化: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/):使用服务器端存储实现的另一种用户会话。
PEP & Import
PEP 8: 每级缩进使用4个空格。不要使用tabPEP 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()}}
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
{% endblock %}
其中 style.css 可以包含自定义的 .navbar-kevin {。。。}
配色方案 http://work.smarchal.com/twbscolor/
flask_debugtoolbar
- from flask_debugtoolbar import DebugToolbarExtension
- toolbar = DebugToolbarExtension()
- toolbar.init_app(app)
《Flask Web开发:基于Python的Web应用开发实战》
1 虚拟环境
- virtualenv venv
- venv\Scripts\activate.bat
- (venv) $ pip freeze >requirements.txt
- (venv) $ pip install -r requirements.txt
- pip list --outdated
- pip install --upgarade <Package1> <PackageN>
- git tag 列出所有打tag的分支
- git checkout <tag_name> 切换到tag
- git reset --hard 不保留修改
2 基本结构
- # -*- coding: utf-8 -*-
- from flask import Flask
- app = Flask(__name__)
- @app.route('/')
- def index():
- return '<h1>Hello World!</h1>'
- @app.route('/user/<name>')
- def user(name):
- return '<h1>Hello, %s!</h1>' %name
- @app.route('/user/<int:id>') # 不能有空格!
- def ...
- app.run(debug=True, port=7777)
- 公认端口(Well Known Ports):从0到1023,紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务协议。80端口实际上总是HTTP通讯。
- 注册端口(Registered Ports):从1024到49151。它们松散地绑定于一些服务。这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
- 动态和/或私有端口(Dynamic and/or Private Ports):从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。
请求-响应循环
- current_app 程序上下文 当前激活程序的程序实例
- g 程序上下文 处理请求时用作临时存储的对象。每次请求都会重设这个变量
- request 请求上下文 请求对象,封装了客户端发出的HTTP请求中的内容
- session 请求上下文 用户会话,用于存储请求之间需要“记住”的值的词典
- app.url_map
- Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>,
- <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
- <Rule '/user/<name>' (HEAD, OPTIONS, GET) -> user>])
- before_first_request • :注册一个函数,在处理第一个请求之前运行。
- before_request • :注册一个函数,在每次请求之前运行。
- after_request • :注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行。
- teardown_request • :注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行
- make_response()函数可接受1~3个参数 (html, 状态码,header)
- response = make_response('<h1>This document carries a cookie!</h1>', 200)
- response.set_cookie('answer', '42')
- return response
- 重定向的特殊响应类型,302:return redirect('http://www.example.com')
- 特殊的响应由abort函数生成,用于处理错误:abort(404)
3 模板 template
- @app.route('/user/<name>')
- def user(name):
- return render_template('user.html', name=name)
- <p>A value from a dictionary: {{ mydict['key'] }}.</p>
- <p>A value from a list: {{ mylist[3] }}.</p>
- <p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
- <p>A value from an object's method: {{ myobj.somemethod() }}.</p>
- Hello, {{ name|capitalize }
- safe 渲染值时不转义。默认情况下,出于安全考虑,Jinja2会转义所有变量
- capitalize 把值的首字母转换成大写,其他字母转换成小写
- lower 把值转换成小写形式
- upper 把值转换成大写形式
- title 把值中每个单词的首字母都转换成大写
- trim 把值的首尾空格去掉
- striptags 渲染之前把值中所有的HTML标签都删掉
- <html>
- <head>
- {%block head %}
- <title>{%block title %}{%endblock %} - My Application</title>
- {%endblock %}
- </head>
- <body>
- {%block body %}
- {%endblock %}
- </body>
- </html>
- {%extends "base.html" %}
- {%block title %}Index{%endblock %}
- {%block head %}
- {{ super() }}
- <style>
- </style>
- {%endblock %}
- {%block body %}
- <h1>Hello, World!</h1>
- {%endblock %}
Flask-Bootstrap基模板中定义的块:doc, head, title, styles, body, navbar, content, scripts
- {% block head %}
- {{super()}}
- <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
- <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
- <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css">
- <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap-theme.min.css">
- {% endblock %}
- 。。。
- {% block scripts %}
- {{super()}}
- <script src="//cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
- <script src="//cdn.bootcss.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
- {{ 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
- {%block scripts %}
- {{ super() }}
- {{ moment.include_moment() }}
- <!--使用中文,默认是英语的-->
- {{ moment.lang("zh-CN") }}
- {%endblock %}
4 Web Form表单
- 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"密码"})
很多用户都不理解浏览器发出的这个警告。 基于这个原因,最好别让 Web 程序把 POST 请求作为浏览器发送的最后一个请求。
- @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'))
Flash消息
5 数据库
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')) # 定义外键
SQLAlchemy 完整的命令、过滤器、查询执行 列表参见 SQLAlchemy 文档
- 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'
3) mysql Workbench: Database -> Reverse Engineer -> 选择 connection -> database -> 一路 Next
6 E-mail
- 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 <[email protected]>'
- 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 = '<b>HTML</b> body'
- with app.app_context():
- mail.send(msg)
- (venv) $ set MAIL_USERNAME=<your mail username>
- (venv) $ set MAIL_PASSWORD=<mail password>
7 大型程序的结构
- |-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 用于启动程序以及其他的程序任务
requirements.txt 文件,用于记录所有依赖包及其精确的版本号。以便要在另一台电脑上重新生成虚拟环境
8 用户认证
Werkzeug:计算密码散列值并进行核对。
itsdangerous:生成并核对加密安全令牌。
- <ul class="nav navbar-nav navbar-right">
- {% if current_user.is_authenticated %}
- <li><a href="{{ url_for('auth.logout') }}" title = "邮件 {{ current_user.email[:9]+'...' }}">Log Out {{ current_user.username }}</a></li>
- {% else %}
- <li><a href="{{ url_for('auth.login') }}">Log In</a></li>
- {% endif %}
- </ul>
按照第 4 章介绍的“Post/ 重定向 /Get 模式”,Login的 POST 请求最后也做了重定向,不过目标 URL 有两种可能。用户访问未授权的 URL 时会显示登录表单,Flask-Login 会把原地址保存在查询字符串的 next参数中,这个参数可从 request.args 字典中读取。如果查询字符串中没有 next 参数,则重定向到首页
- 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'))
用户注册表单 app/auth/forms.py
- >>> 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}
对蓝本来说, before_request 钩子只能应用到属于蓝本的请求上。若想在蓝本中使用针对程序全局请求的钩子,必须使用 before_app_request 修饰器
9 User Role 角色
10 User Profile 资料
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">
- <img height="24px" src="{{ url_for('static', filename='avatar/')}}{{ current_user.avatar_hash }}">
models.py:
- def gravatar(self, size=100, default='identicon', rating='g'):
- import random
- return '%.3d.jpg' % random.randint(1, XXX)
- <div class="page-header">
- <img class="img-rounded profile-thumbnail" src="{{ url_for('static', filename='avatar/')}}{{ current_user.avatar_hash }}"">
- <li class="post">
- <div class="post-thumbnail">
- <a href="{{ url_for('.user', username=post.author.username) }}">
- <img height="40px" class="img-rounded profile-thumbnail" src="{{ url_for('static', filename='avatar/')}}{{ post.author.avatar_hash }}">
- <div class="comment-thumbnail">
- <a href="{{ url_for('.user', username=comment.author.username) }}">
- <img height="40px' class="img-rounded profile-thumbnail" src="{{ url_for('static', filename='avatar/')}}{{ post.author.avatar_hash }}">
11 Blog articles 博客文章
生成虚拟信息用于测试,其中功能相对完善的是ForgeryPy
- python manage.py shell
- >>> User.generate_fake()
- >>> Post.generate_fake(200)
添加分页导航
paginate() 方法的返回值是一个 Pagination 类对象,这个类在 Flask-SQLAlchemy 中定义。这个对象包含很多属性, 用于在模板中生成分页链接
• PageDown: 使用JavaScript实现的客户端Markdown到HTML的转换程序。
• Flask-PageDown: 为Flask包装的PageDown,把PageDown集成到Flask-WTF表单中。
• Markdown: 使用Python实现的服务器端Markdown到HTML的转换程序。
• Bleach: 使用Python实现的HTML清理器。
博客文章的固定链接
博客文章编辑器
12 followers 关注者
若想显示所关注用户发布的所有文章,第一步显然先要获取这些用户,然后获取各用户的文章,再按一定顺序排列,写入单独列表。可是这种方式的伸缩性不好,随着数据库不断变大,生成这个列表的工作量也不断增长,而且分页等操作也无法高效率完成。获取博客文章的高效方式是只用一次查询。
完成这个操作的数据库操作称为联结。联结操作用到两个或更多的数据表, 在其中查找满足指定条件的记录组合, 再把记录组合插入一个临时表中,这个临时表就是联结查询的结果。
创建函数更新数据库这一技术经常用来更新已部署的程序,因为运行脚本更新比手动更新数据库更少出错。
13 User comments 评论
comments 表还和 users 表之间有一对多关系。通过这个关系可以获取用户发表的所有评论,还能间接知道用户发表了多少篇评论
14 RIA (API, REST)
资源是REST架构方式的核心概念。在REST架构中, 资源是程序中你要着重关注的事物。例如,在博客程序中,用户、博客文章和评论都是资源。
15 Testing
16 Performance 性能
17 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)
在能够托管程序之前,服务器必须完成多项管理任务。
- 安装数据库服务器,例如MySQL或Postgres。也可使用SQLite数据库,但由于其自身 •的种种限制,不建议用于生产服务器。
- 安装邮件传输代理(Mail Transport Agent,MTA),例如Sendmail,用于向用户发送邮件。
- 安装适用于生产环境的Web服务器,例如Gunicorn或uWSGI。
- 为了启用安全HTTP,购买、安装并配置SSL证书。
- (可选,但强烈推荐)安装前端反向代理服务器,例如nginx或Apache。反向代理服务器能直接服务于静态文件,而把其他请求转发给程序使用的Web服务器。Web服务器
- 监听localhost中的一个私有端口。
- 强化服务器。这一过程包含多项任务,目标在于降低服务器被攻击的可能性,例如安装防火墙以及删除不用的软件和服务等
其它:
Flask-Cache插件为你提供一组装饰器来实现多种方式的缓存
开发自定义视图装饰器来帮助我们组织自己的代码
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
作者早些时候写的网上教程:
- 全文检索:
- 国际化: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/):使用服务器端存储实现的另一种用户会话。
PEP & Import
PEP 8: 每级缩进使用4个空格。不要使用tabPEP 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()}}
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
{% endblock %}
其中 style.css 可以包含自定义的 .navbar-kevin {。。。}
配色方案 http://work.smarchal.com/twbscolor/
flask_debugtoolbar
- from flask_debugtoolbar import DebugToolbarExtension
- toolbar = DebugToolbarExtension()
- toolbar.init_app(app)
《Flask Web开发:基于Python的Web应用开发实战》
1 虚拟环境
- virtualenv venv
- venv\Scripts\activate.bat
- (venv) $ pip freeze >requirements.txt
- (venv) $ pip install -r requirements.txt
- pip list --outdated
- pip install --upgarade <Package1> <PackageN>
- git tag 列出所有打tag的分支
- git checkout <tag_name> 切换到tag
- git reset --hard 不保留修改
2 基本结构
- # -*- coding: utf-8 -*-
- from flask import Flask
- app = Flask(__name__)
- @app.route('/')
- def index():
- return '<h1>Hello World!</h1>'
- @app.route('/user/<name>')
- def user(name):
- return '<h1>Hello, %s!</h1>' %name
- @app.route('/user/<int:id>') # 不能有空格!
- def ...
- app.run(debug=True, port=7777)
- 公认端口(Well Known Ports):从0到1023,紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务协议。80端口实际上总是HTTP通讯。
- 注册端口(Registered Ports):从1024到49151。它们松散地绑定于一些服务。这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
- 动态和/或私有端口(Dynamic and/or Private Ports):从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。
请求-响应循环
- current_app 程序上下文 当前激活程序的程序实例
- g 程序上下文 处理请求时用作临时存储的对象。每次请求都会重设这个变量
- request 请求上下文 请求对象,封装了客户端发出的HTTP请求中的内容
- session 请求上下文 用户会话,用于存储请求之间需要“记住”的值的词典
- app.url_map
- Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>,
- <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
- <Rule '/user/<name>' (HEAD, OPTIONS, GET) -> user>])
- before_first_request • :注册一个函数,在处理第一个请求之前运行。
- before_request • :注册一个函数,在每次请求之前运行。
- after_request • :注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行。
- teardown_request • :注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行
- make_response()函数可接受1~3个参数 (html, 状态码,header)
- response = make_response('<h1>This document carries a cookie!</h1>', 200)
- response.set_cookie('answer', '42')
- return response
- 重定向的特殊响应类型,302:return redirect('http://www.example.com')
- 特殊的响应由abort函数生成,用于处理错误:abort(404)
3 模板 template
- @app.route('/user/<name>')
- def user(name):
- return render_template('user.html', name=name)
- <p>A value from a dictionary: {{ mydict['key'] }}.</p>
- <p>A value from a list: {{ mylist[3] }}.</p>
- <p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
- <p>A value from an object's method: {{ myobj.somemethod() }}.</p>
- Hello, {{ name|capitalize }
- safe 渲染值时不转义。默认情况下,出于安全考虑,Jinja2会转义所有变量
- capitalize 把值的首字母转换成大写,其他字母转换成小写
- lower 把值转换成小写形式
- upper 把值转换成大写形式
- title 把值中每个单词的首字母都转换成大写
- trim 把值的首尾空格去掉
- striptags 渲染之前把值中所有的HTML标签都删掉
- <html>
- <head>
- {%block head %}
- <title>{%block title %}{%endblock %} - My Application</title>
- {%endblock %}
- </head>
- <body>
- {%block body %}
- {%endblock %}
- </body>
- </html>
- {%extends "base.html" %}
- {%block title %}Index{%endblock %}
- {%block head %}
- {{ super() }}
- <style>
- </style>
- {%endblock %}
- {%block body %}
- <h1>Hello, World!</h1>
- {%endblock %}
Flask-Bootstrap基模板中定义的块:doc, head, title, styles, body, navbar, content, scripts
- {% block head %}
- {{super()}}
- <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
- <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
- <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css">
- <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap-theme.min.css">
- {% endblock %}
- 。。。
- {% block scripts %}
- {{super()}}
- <script src="//cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
- <script src="//cdn.bootcss.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
- {{ 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
- {%block scripts %}
- {{ super() }}
- {{ moment.include_moment() }}
- <!--使用中文,默认是英语的-->
- {{ moment.lang("zh-CN") }}
- {%endblock %}
4 Web Form表单
- 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"密码"})
很多用户都不理解浏览器发出的这个警告。 基于这个原因,最好别让 Web 程序把 POST 请求作为浏览器发送的最后一个请求。
- @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'))
Flash消息
5 数据库
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')) # 定义外键
SQLAlchemy 完整的命令、过滤器、查询执行 列表参见 SQLAlchemy 文档
- 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'
3) mysql Workbench: Database -> Reverse Engineer -> 选择 connection -> database -> 一路 Next
6 E-mail
- 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 <[email protected]>'
- 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 = '<b>HTML</b> body'
- with app.app_context():
- mail.send(msg)
- (venv) $ set MAIL_USERNAME=<your mail username>
- (venv) $ set MAIL_PASSWORD=<mail password>
7 大型程序的结构
- |-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 用于启动程序以及其他的程序任务
requirements.txt 文件,用于记录所有依赖包及其精确的版本号。以便要在另一台电脑上重新生成虚拟环境
8 用户认证
Werkzeug:计算密码散列值并进行核对。
itsdangerous:生成并核对加密安全令牌。
- <ul class="nav navbar-nav navbar-right">
- {% if current_user.is_authenticated %}
- <li><a href="{{ url_for('auth.logout') }}" title = "邮件 {{ current_user.email[:9]+'...' }}">Log Out {{ current_user.username }}</a></li>
- {% else %}
- <li><a href="{{ url_for('auth.login') }}">Log In</a></li>
- {% endif %}
- </ul>
按照第 4 章介绍的“Post/ 重定向 /Get 模式”,Login的 POST 请求最后也做了重定向,不过目标 URL 有两种可能。用户访问未授权的 URL 时会显示登录表单,Flask-Login 会把原地址保存在查询字符串的 next参数中,这个参数可从 request.args 字典中读取。如果查询字符串中没有 next 参数,则重定向到首页
- 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'))
用户注册表单 app/auth/forms.py
- >>> 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}
对蓝本来说, before_request 钩子只能应用到属于蓝本的请求上。若想在蓝本中使用针对程序全局请求的钩子,必须使用 before_app_request 修饰器
9 User Role 角色
10 User Profile 资料
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">
- <img height="24px" src="{{ url_for('static', filename='avatar/')}}{{ current_user.avatar_hash }}">
models.py:
- def gravatar(self, size=100, default='identicon', rating='g'):
- import random
- return '%.3d.jpg' % random.randint(1, XXX)
- <div class="page-header">
- <img class="img-rounded profile-thumbnail" src="{{ url_for('static', filename='avatar/')}}{{ current_user.avatar_hash }}"">
- <li class="post">
- <div class="post-thumbnail">
- <a href="{{ url_for('.user', username=post.author.username) }}">
- <img height="40px" class="img-rounded profile-thumbnail" src="{{ url_for('static', filename='avatar/')}}{{ post.author.avatar_hash }}">
- <div class="comment-thumbnail">
- <a href="{{ url_for('.user', username=comment.author.username) }}">
- <img height="40px' class="img-rounded profile-thumbnail" src="{{ url_for('static', filename='avatar/')}}{{ post.author.avatar_hash }}">
11 Blog articles 博客文章
生成虚拟信息用于测试,其中功能相对完善的是ForgeryPy
- python manage.py shell
- >>> User.generate_fake()
- >>> Post.generate_fake(200)
添加分页导航
paginate() 方法的返回值是一个 Pagination 类对象,这个类在 Flask-SQLAlchemy 中定义。这个对象包含很多属性, 用于在模板中生成分页链接
• PageDown: 使用JavaScript实现的客户端Markdown到HTML的转换程序。
• Flask-PageDown: 为Flask包装的PageDown,把PageDown集成到Flask-WTF表单中。
• Markdown: 使用Python实现的服务器端Markdown到HTML的转换程序。
• Bleach: 使用Python实现的HTML清理器。
博客文章的固定链接
博客文章编辑器
12 followers 关注者
若想显示所关注用户发布的所有文章,第一步显然先要获取这些用户,然后获取各用户的文章,再按一定顺序排列,写入单独列表。可是这种方式的伸缩性不好,随着数据库不断变大,生成这个列表的工作量也不断增长,而且分页等操作也无法高效率完成。获取博客文章的高效方式是只用一次查询。
完成这个操作的数据库操作称为联结。联结操作用到两个或更多的数据表, 在其中查找满足指定条件的记录组合, 再把记录组合插入一个临时表中,这个临时表就是联结查询的结果。
创建函数更新数据库这一技术经常用来更新已部署的程序,因为运行脚本更新比手动更新数据库更少出错。
13 User comments 评论
comments 表还和 users 表之间有一对多关系。通过这个关系可以获取用户发表的所有评论,还能间接知道用户发表了多少篇评论
14 RIA (API, REST)
资源是REST架构方式的核心概念。在REST架构中, 资源是程序中你要着重关注的事物。例如,在博客程序中,用户、博客文章和评论都是资源。
15 Testing
16 Performance 性能
17 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)
在能够托管程序之前,服务器必须完成多项管理任务。
- 安装数据库服务器,例如MySQL或Postgres。也可使用SQLite数据库,但由于其自身 •的种种限制,不建议用于生产服务器。
- 安装邮件传输代理(Mail Transport Agent,MTA),例如Sendmail,用于向用户发送邮件。
- 安装适用于生产环境的Web服务器,例如Gunicorn或uWSGI。
- 为了启用安全HTTP,购买、安装并配置SSL证书。
- (可选,但强烈推荐)安装前端反向代理服务器,例如nginx或Apache。反向代理服务器能直接服务于静态文件,而把其他请求转发给程序使用的Web服务器。Web服务器
- 监听localhost中的一个私有端口。
- 强化服务器。这一过程包含多项任务,目标在于降低服务器被攻击的可能性,例如安装防火墙以及删除不用的软件和服务等
其它:
Flask-Cache插件为你提供一组装饰器来实现多种方式的缓存
开发自定义视图装饰器来帮助我们组织自己的代码
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
作者早些时候写的网上教程:
- 全文检索:
- 国际化: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/):使用服务器端存储实现的另一种用户会话。
PEP & Import
PEP 8: 每级缩进使用4个空格。不要使用tabPEP 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()}}
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
{% endblock %}
其中 style.css 可以包含自定义的 .navbar-kevin {。。。}
配色方案 http://work.smarchal.com/twbscolor/
flask_debugtoolbar
- from flask_debugtoolbar import DebugToolbarExtension
- toolbar = DebugToolbarExtension()
- toolbar.init_app(app)