《Flask Web开发:基于Python的Web应用开发实战》笔记(原创)

内容提要

在学习“狗书”《Flask Web开发:基于Python的Web应用开发实战》的过程中,一直遇到各种各样的坑。该书的第一部分是“Flask简介”,主要介绍的一些基础知识。第二部分是“实例:社交博客程序”,讲的是如何搭建一个社交博客的框架。目前我学到第二部分,回过头来看,觉得按照程序运行的实际逻辑来说明程序的原理,并在这个过程中分析遇到的各种“坑”,也更能让人理解(至少让我自己理解)。

正文

  1. 程序包结构

flask文件夹结构
《Flask Web开发:基于Python的Web应用开发实战》笔记(原创)_第1张图片
其中:
app为程序包,Flask程序保存在这个包中
migrations文件夹包含数据库迁移脚本
tests包保存单元测试
requirements文件夹中记录程序的依赖
config.py是程序的配置文件
manage.py是程序的运行文件,用于启动程序即程序的其他任务


app包结构
《Flask Web开发:基于Python的Web应用开发实战》笔记(原创)_第2张图片
其中:
auth为保存专门用于认证的auth蓝本
main为保存main蓝本的包
static文件夹用于保存静态文件,例如HTML代码中引用的图片、 JavaScript 源码文件和 CSS
templates用于保存网页的模板


蓝本文件夹结构(以auth文件夹为例,main蓝本等不再赘述)
《Flask Web开发:基于Python的Web应用开发实战》笔记(原创)_第3张图片
蓝本的细节就不在此赘述,在flask框架中用到蓝本,可以对不同的程序功能使用不同的蓝本,这是保证程序整齐有序的办法。(想想把所有功能都写在一起会多么混乱)。这里说明一下到蓝本的程序运行原理:app/auth/views.py 模块引入蓝本,然后使用蓝本的 route 修饰器定义与认证相关的路由,然后再渲染views中设定的网页模板。看起来是不是如果程序能运行到蓝本这一步,我们就可以对网页进行操作了。
2. 运行说明
在运行程序的时候,我们在虚拟环境下,通过如下命令来完成。由此可见,程序的运行是由manage.py来开始的。

(venv) $ python manage.py runserver

那么,我们来看看这个manage.py吧,期待不期待?兴奋不兴奋?

#!/usr/bin/env python
import os
COV = None
if os.environ.get('FLASK_COVERAGE'):
    import coverage
    COV = coverage.coverage(branch=True, include='app/*')
    COV.start()

if os.path.exists('.env'):
    print('Importing environment from .env...')
    for line in open('.env'):
        var = line.strip().split('=')
        if len(var) == 2:
            os.environ[var[0]] = var[1]

from app import create_app, db
from app.models import User, Follow, Role, Permission, Post, Comment
from flask_script import Manager, Shell
from flask_migrate import Migrate, MigrateCommand

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
migrate = Migrate(app, db)


def make_shell_context():
    return dict(app=app, db=db, User=User, Follow=Follow, Role=Role,
                Permission=Permission, Post=Post, Comment=Comment)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)


@manager.command
def test(coverage=False):
    """Run the unit tests."""
    if coverage and not os.environ.get('FLASK_COVERAGE'):
        import sys
        os.environ['FLASK_COVERAGE'] = '1'
        os.execvp(sys.executable, [sys.executable] + sys.argv)
    import unittest
    tests = unittest.TestLoader().discover('tests')
    unittest.TextTestRunner(verbosity=2).run(tests)
    if COV:
        COV.stop()
        COV.save()
        print('Coverage Summary:')
        COV.report()
        basedir = os.path.abspath(os.path.dirname(__file__))
        covdir = os.path.join(basedir, 'tmp/coverage')
        COV.html_report(directory=covdir)
        print('HTML version: file://%s/index.html' % covdir)
        COV.erase()


@manager.command
def profile(length=25, profile_dir=None):
    """Start the application under the code profiler."""
    from werkzeug.contrib.profiler import ProfilerMiddleware
    app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length],
                                      profile_dir=profile_dir)
    app.run()


@manager.command
def deploy():
    """Run deployment tasks."""
    from flask_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()


if __name__ == '__main__':
    manager.run()

下面我们按照顺序去理一下,看看manage.py到底是如何运行的。

import os
COV = None
if os.environ.get('FLASK_COVERAGE'):
    import coverage
    COV = coverage.coverage(branch=True, include='app/*')
    COV.start()

?这段代码是关于程序测试覆盖度方面的,我们可以先行略去,测试本身并不影响程序的运行。

if os.path.exists('.env'):
    print('Importing environment from .env...')
    for line in open('.env'):
        var = line.strip().split('=')
        if len(var) == 2:
            os.environ[var[0]] = var[1]

?这段代码的作用是从.env文件中导入环境变量。具体的大家可以去搜索一下这个文件,在程序配置中,有些信息是在环境变量中设置的。但是此处我们也略过细节。

from app import create_app, db
from app.models import User, Follow, Role, Permission, Post, Comment
from flask_script import Manager, Shell
from flask_migrate import Migrate, MigrateCommand

?各种导入,从app包中导入create_app工厂函数和数据库db。从app.models模块中导入User, Follow, Role, Permission, Post, Comment等类。在flask_script扩展中导入Manager, Shell, 从flask_migrate扩展中导入Migrate, MigrateCommand。什么意思呢?自己去搜索一下。
此处有一个坑:在原文中flask的扩展采用from flask.ext.script import Manager, Shell的导入方式,但是实际用的时候却会报错,从报错的信息中可以知道flask.ext.script已经弃用了,改为flask_script即可。其他的扩展也是一样。

>>> from flask.ext.script import Manager
__main__:1: ExtDeprecationWarning: Importing flask.ext.script is deprecated, use flask_script instead.

让我们继续吧!

app = create_app(os.getenv ('FLASK_CONFIG') or 'default')

这一句是创建工厂函数的实例,create_app是由from app import create_app, db导入的。
这里有一个坑需要注意一下,from app import create_app, db中的app是我们创建的一个包,其放置于顶层文件夹中,其中包括一个__init__.py文件。而语句app = create_app(os.getenv (‘FLASK_CONFIG’) or ‘default’)的app则是给create_app函数创建的实例的一个命名,这两个的含义是不一样的。对于我这种小白,刚刚开始也是迷惑了一阵子。
现在我们来看一下具体实现的方法。
os.getenv(‘FLASK_CONFIG’) or ‘default’),这个’FLASK_CONFIG’我们是我们设置的环境变量,通过os.getenv我们可以获得这个环境参数。因为这是对工厂函数create_app进行实例化,因此我们得到的环境参数就作为create_app的参数,如果没有设置’FLASK_CONFIG’这个环境变量,那么就将’default’这个默认值赋给工厂函数。那么这个设置的’FLASK_CONFIG’环境变量或这个’default’到底是什么呢?这需要到工厂函数中看一下。
既然create_app是从app包中导入的,那么让我们一起来看看这个包里面都有什么吧!打开app下面的__init__.py文件,看到没有create_app藏在这里呢。

此处插播工厂函数广告,为不影响文章的总体逻辑,特做分割


app / __ init __.py

from flask import Flask
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_pagedown import PageDown
from config import config
#依旧是各种导入,这里应该不难理解,在工厂函数中导入了flask及其扩展,也通过from config import config导入了程序配置文件config.py中的config属性

bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()
pagedown = PageDown()

login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
#这里是创建导入的Flask扩展的实例。


def create_app(config_name): 
    app = Flask(__name__) #创建Flask的实例
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)

    bootstrap.init_app(app)
    mail.init_app(app)
    moment.init_app(app)
    db.init_app(app)
    login_manager.init_app(app)
    pagedown.init_app(app)
    #创建flask扩展的实例

    if not app.debug and not app.testing and not app.config['SSL_DISABLE']:
        from flask_sslify import SSLify
        sslify = SSLify(app)

    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/auth')

    from .api_1_0 import api as api_1_0_blueprint
    app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')
    #注册蓝图到工厂函数中

    return app

广告结束,继续manage.py


我们来分析

app = create_app(os.getenv ('FLASK_CONFIG') or 'default')

在create_app中有如下的语句:

def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])

这里的os.getenv (‘FLASK_CONFIG’) or 'default’作为参数被传递给app.config.from_object(config[config_name]),其中的app是Flask的一个实例。
又一个坑:app.config中的config是Flask类的一个属性,与我们的配置文件config.py不是同一个东西,也不是config.py中的config属性。而config[config_name]得config是配置文件config.py中导入的,我们在config.py文件中看一下:

config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'heroku': HerokuConfig,
    'unix': UnixConfig,

    'default': DevelopmentConfig
}

可以发现,FLASK_CONFIG应该被设置成字典的键,根据你需要运行的模式选取’development’, ‘testing’, ‘production’, ‘heroku’, ‘unix’, ‘default’中的一个。当然,如果不在环境变量中设置的话,就默认选取的’default’。那么from_object返回的是该健对应值。例如设置’default’返回的就是’DevelopmentConfig’。
到了这里我们知道app.config.from_object(config[config_name])实际上就是app.config.DevelopmentConfig,这个config.DevelopmentConfig就是config.py中的DevelopmentConfig类。那么看看这个DevelopmentConfig:

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')

这个DevelopmentConfig的父类是Config类,继承Config中的所有属性和方法。这样app.config.DevelopmentConfig就将DevelopmentConfig设定的配置绑定到了app——即Flask的实例上去。
当然,我们这里是以DevelopmentConfig为例,其他的配置是一样的原理。
接着执行create_app中的语句是:

config[config_name].init_app(app)

这里的init_app是在DevelopmentConfig类或其他运行环境配置类中的一个初始化方法,但值得注意的是,DevelopmentConfig类中并没有直接写入这个方法,而是继承Config类。
在config类中有一个静态方法,不执行任何操作。

@staticmethod
    def init_app(app):
        pass

因此在DevelopmentConfig中,这个初始化实际上并没有进行,直接pass。这是因为这里根本不需要初始化,之前执行的配置就完全够了。
但是值得注意的是,在ProductionConfig中是有重写这个方法,因此会根据重写的方法对app进行初始化配置。

bootstrap.init_app(app)
    mail.init_app(app)
    moment.init_app(app)
    db.init_app(app)
    login_manager.init_app(app)
    pagedown.init_app(app)

?这时我们已经完成了实例化Flask,并对实例app进行了初始化配置。对于我们用到的flask扩展也进行同样的初始化配置。
坑:此处的init_app方法是各扩展中自带的方法,并不是刚才我们用到的在config.py中自己写的init_app方法,不过功能差不太多。但有时候让人挺迷惑的。

if not app.debug and not app.testing and not app.config['SSL_DISABLE']:
        from flask_sslify import SSLify
        sslify = SSLify(app)

?这里是启用安全HTTP的flask扩展flask_sslify,先略去。

from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/auth')

    from .api_1_0 import api as api_1_0_blueprint
    app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')

?刚才说了那么多,无非是完成了实例化和配置初始化,但是我们的对网页处理的视图函数以及网页本身的内容在哪里处理呢?
视图函数我们是交给蓝本来处理的,此处是将蓝本注册到工厂函数中去。
到此有没有发现,在manage.py中大部分事情都让工厂函数干去了,首先是创建实例,然后初始化配置,最后把工作交给蓝本去干。
根据实现的功能不同分成不同的蓝本。比如在这个程序中,认证就放在auth这个蓝本中。蓝本去分配其中的路由。通过蓝本中的视图文件去操作form和template,对数据库的操作也是在视图文件中实现。原理就是这个原理,细节就不多说了。
接下来的代码:

manager = Manager(app)
migrate = Migrate(app, db)

这是方便程序在脚本下操作的flask_script中的Manage类的实例和数据库迁移的实例。
可能大家还有一个疑问:数据库是什么时候创建的,这其实在对工厂函数初始化配置的时候,通过调用config.py中的配置已经完成了。

总结

通过运行manage.py的过程分析,发现总体框架特点如下:
manage.py——公司的总经理
工厂函数——项目经理
config.py——公司财务总监
蓝本——各技术部门经理
视图文件——广大苦逼工程师
template/form——生产工具(枪/炮)

总经理:干
项目经理:好, 财务总监,这粮和钱…, 部门经理们,我给你们说个事呗
财务总监:有的,你有我有全都有。都在这里,别客气
技术部门经理:好的。那啥,小明,小红,抄家伙
工程师:…(端着枪扛着炮就冲出去了)

你可能感兴趣的:(《Flask Web开发:基于Python的Web应用开发实战》笔记(原创))