美文网首页乐读创业社57周
Flask Web Development读书笔记(上)

Flask Web Development读书笔记(上)

作者: cajan2 | 来源:发表于2016-06-05 10:39 被阅读183次

    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
    或关注微信公众号选取:

    相关文章

      网友评论

      本文标题:Flask Web Development读书笔记(上)

      本文链接:https://www.haomeiwen.com/subject/nkbkdttx.html