Flask Web Development读书笔记(上)
meta
拥有25年开发经验的高级软件工程师,目前为广播公司开发视频软件。他常在个人博客(blog.miguelgrinberg.com)上撰写各类博文,内容主要涉及Web开发、机器人技术、摄影,偶尔也会有一些影评。
全书分成三部分:- 基础知识导论- 一个完整的例子串讲- 最后一公里的测试调优部署
preface-why Flask
在python众多的web开发框架中,有两个现象级的-Django和Flask。
两个框架风格迥异,但都带动了庞大的生态圈。
Django提供一站式解决方案,所以相应的模块也就比较多。
Flask作为一种micro framework,一开始提供一个坚实的基础,然后接入一个第三方的生态系统,这是一种不同的策略或者说境界。本书的结构是从零开始,逐渐扩展成一个成熟的项目,而不是零散的知识介绍,因此对入门应该是比较合适的。
以python相关的数据库ORM框架来说,主要有两种,一种是Django的ORM,另外一种是SqlAlchemy。就是说除了Django以外,其他各种web框架的ORM最流行的SqlAlchemy,也有很多其他的选择,比如DynamoDB和MongoDB等等。
Flask的灵活是值得称道的,从最简单的单模块开始就能够构建web应用,也可以在大型应用中使用蓝图(blueprint)。这比Django无论大小应用都先来一堆很大的框架要灵活。
from flask import Flask
app = Flask(__name__)
@app.route("/") # take note of this decorator syntax
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
而Django一开始引导一个项目的时候,文件结构如下:
hello_django
├── hello_django
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── howdy
│ ├── admin.py
│ ├── __init__.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── manage.py
Django把一个项目分成各自独立的应用,而Flask认为一个项目应该是一个包含一些视图和模型的单个应用。也可以在Flask里复制出像Django那样的项目结构,但那不是默认的。
下面是一个使用了蓝图的Flask项目文件结构
|-flasky
|-app/
|-templates/
|-static/
|-main/
|-__init__.py
|-errors.py
|-forms.py
|-views.py
|-__init__.py
|-email.py
|-models.py
|-migrations/
|-tests/
|-__init__.py
|-test*.py
|-venv/
|-requirements.txt
|-config.py
|-manage.py
templates模板
Flask默认使用Jinja2的模板,但也可以通过配置来使用其他的语言。
下面是一个for循环,{{}}是占位符标记,web后端,通过这个template文件和后端动态产生的数据data混合以后渲染出一个html文件。
{% for item in inventory %}
<div class="display-item">{{ item.render() }}</div>
{% else %}
<div class="display-warn">
<h3>No items found</h3>
<p>Try another search, maybe?</p>
</div>
{% endfor %}
模板继承
先创建一个base.html,这个base.html抽取了所有模板文件的公共部分。
然后在每个具体的html文件中引用这个base.html,语法如下:
{% extends 'base.html' %}
有点类似于面向对象里面的继承extends。
Flask扩展
0.Flask-Bootstrap:集成Twitter开发的一个开源框架Bootstrap。
一开始没有使用Flask-Bootstrp,我们之间在html代码里面引用bootstrap的css文件和js文件,base.html如下所示:
<!DOCTYPE html>
<html>
<head>
{% if title %}
<title>{{ title }} - microblog</title>
{% else %}
<title>microblog</title>
{% endif %}
<link href="../static/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="../static/css/bootstrap-responsive.min.css" rel="stylesheet">
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<script src="/static/js/moment.min.js"></script>
{% if g.locale != 'en' %}
<script src="/static/js/moment-{{ g.locale }}.min.js"></script>
{% endif %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="navbar">
<div class="navbar-inner">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="/">microblog</a>
<ul class="nav">
<li><a href="{{ url_for('index') }}">{{ _('Home') }}</a></li>
{% if g and g.user and g.user.is_authenticated() %}
<li><a href="{{ url_for('user', nickname = g.user.nickname) }}">{{ _('Your Profile') }}</a></li>
<li><a href="{{ url_for('logout') }}">{{ _('Logout') }}</a></li>
{% endif %}
</ul>
<div class="nav-collapse collapse">
{% if g and g.user and g.user.is_authenticated() %}
<form class="navbar-search pull-right" action="{{ url_for('search') }}" method="post" name="search">
{{ g.search_form.hidden_tag() }}{{ g.search_form.search(size=20,placeholder="Search",class="search-query") }}</form>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="span12">
{% block content %}{% endblock %}
</div>
</div>
</div>
<script>
function translate(sourceLang, destLang, sourceId, destId, loadingId) {
$(destId).hide();
$(loadingId).show();
$.post('/translate', {
text: $(sourceId).text(),
sourceLang: sourceLang,
destLang: destLang
}).done(function (translated) {
$(destId).text(translated['text'])
$(loadingId).hide();
$(destId).show();
}).fail(function () {
$(destId).text("{{ _('Error: Could not contact server.') }}");
$(loadingId).hide();
$(destId).show();
});
}
</script>
</body>
</html>
再看使用Flask-Bootstrap以后的base.html
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% 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" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ url_for('main.index') }}">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="{{ url_for('main.index') }}">Home</a></li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('main.user', username=current_user.username) }}">Profile</a></li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.can(Permission.MODERATE_COMMENTS) %}
<li><a href="{{ url_for('main.moderate') }}">Moderate Comments</a></li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<img src="{{ current_user.gravatar(size=18) }}">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('auth.change_password') }}">Change Password</a></li>
<li><a href="{{ url_for('auth.change_email_request') }}">Change Email</a></li>
<li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
</ul>
</li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Log In</a></li>
{% endif %}
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
1.Flask-Script:为Flask程序添加一个命令行解析器
2.Flask-Moment:本地化日期和时间
3.Flask-WTF:Web表单
4.Flask-Mail:邮件
5.Flask-SQLAlchemy:使用ORM框架SqlAlchemy
6.Flask-Login:Flask的认证扩展
• Flask-Login:管理已登录用户的用户会话。
• Werkzeug:计算密码散列值并进行核对。
• itsdangerous:生成并核对加密安全令牌。
示例14-17 app/api_1_0/posts.py:文章资源GET 请求的处理程序
@api.route('/posts/')
@auth.login_required
def get_posts():
posts = Post.query.all()
return jsonify({ 'posts': [post.to_json() for post in posts] })
@api.route('/posts/<int:id>')
@auth.login_required #route入口,要求现有登录
def get_post(id):
post = Post.query.get_or_404(id)
return jsonify(post.to_json())
7.Flask-HTTPAuth:认证用户(提供REST服务的时候需要)
程序当前的登录功能是在Flask-Login 的帮助下实现的,可以把数据存储在用户会话中。默认情况下,Flask 把会话保存在客户端cookie 中,因此服务器没有保存任何用户相关信息,都转交给客户端保存。这种实现方式看起来遵守了REST 架构的无状态要求,但在REST Web 服务中使用cookie 有点不现实,因为Web 浏览器之外的客户端(比如移动端app)很难提供对cookie 的支持。鉴于此,使用cookie 并不是一个很好的设计选择。
因为REST 架构基于HTTP 协议,所以发送密令的最佳方式是使用HTTP 认证,基本认证和摘要认证都可以。在HTTP 认证中,用户密令包含在请求的Authorization 首部中。
HTTP 认证协议很简单,可以直接实现,不过Flask-HTTPAuth 扩展提供了一个便利的包装,可以把协议的细节隐藏在修饰器之中,类似于Flask-Login 提供的login_required 装饰器。
示例14-6 app/api_1_0/authentication.py:
初始化Flask-HTTPAuth
from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(email, password):
if email == '':
g.current_user = AnonymousUser()
return True
user = User.query.filter_by(email = email).first()
if not user:
return False
g.current_user = user
return user.verify_password(password)
上面代码是基本的方法,但是在实际使用中还需要考虑更多的安全因素。
基于令牌的认证
每次请求时,客户端都要发送认证密令。为了避免总是发送敏感信息,我们可以提供一种基于令牌的认证方案。
使用基于令牌的认证方案时,客户端要先把登录密令发送给一个特殊的URL,从而生成认证令牌。一旦客户端获得令牌,就可用令牌代替登录密令认证请求。出于安全考虑,令牌有过期时间。令牌过期后,客户端必须重新发送登录密令以生成新令牌。令牌落入他人之手所带来的安全隐患受限于令牌的短暂使用期限。这是通常的模式,不是Flask框架特有的或者Flask框架某个扩展的特性,所以在这里不再细说。
示例14-18 app/api_1_0/posts.py:文章资源POST 请求的处理程序
@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE_ARTICLES)
def new_post():
post = Post.from_json(request.json)
post.author = g.current_user
db.session.add(post)
db.session.commit()
return jsonify(post.to_json()), 201, \
{'Location': url_for('api.get_post', id=post.id, _external=True)}
示例14-19 app/api_1_0/decorators.py:permission_required 修饰器
里面使用了functools的装饰器wraps,它使得wrapper看起来和wrapped一样(内置类属性'__module__', '__name__', '__doc__','__dict__')
from functools import wraps
from flask import g
from .errors import forbidden
def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not g.current_user.can(permission):
return forbidden('Insufficient permissions')
return f(*args, **kwargs)
return decorated_function
return decorator
该书中使用HTTPie测试Web服务.
想加入更多乐读创业社的活动,请访问网站→http://ledu.club
或关注微信公众号选取:
网友评论