大型程序的结构(三)【程序包】重点!!!

本文详细介绍了Flask应用程序的结构,特别是程序包的组织方式,包括将templates和static文件夹移至app包下,以及数据库模型和邮件支持函数的放置。重点讨论了程序工厂函数的使用,解释了`init_app`方法在Flask扩展中的作用,以及如何在蓝本中实现程序功能。同时,提到了静态文件和模板文件的路径调整,以及蓝本的命名空间和端点名的规则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

程序包用来保存程序的所有代码,模板和静态文件。
这个包直接称为app(应用),也可使用一个程序专用的名字。
templates和static文件夹是程序包的一部分,因此两个文件夹被移到app文件夹中。
数据库模型和电子邮件支持函数也被移到了这个包中,分别保存为app/models.py和app/email.py。

使用程序工厂函数

课外资料

Flask 扩展开发
自定义的flask扩展flask_sqlite3
扩展的代码flask_sqlite3.py如下:
下面是用来复制/粘贴的 flask_sqlite3.py 的内容:

import sqlite3
from flask import current_app

# Find the stack on which we want to store the database connection.
# Starting with Flask 0.9, the _app_ctx_stack is the correct one,
# before that we need to use the _request_ctx_stack.
try:
    from flask import _app_ctx_stack as stack
except ImportError:
    from flask import _request_ctx_stack as stack


class SQLite3(object):

    def __init__(self, app=None):
        self.app = app
        if app is not None:
            self.init_app(app)

    def init_app(self, app):
        app.config.setdefault('SQLITE3_DATABASE', ':memory:')
        # Use the newstyle teardown_appcontext if it's available,
        # otherwise fall back to the request context
        if hasattr(app, 'teardown_appcontext'):
            app.teardown_appcontext(self.teardown)
        else:
            app.teardown_request(self.teardown)

    def connect(self):
        return sqlite3.connect(current_app.config['SQLITE3_DATABASE'])

    def teardown(self, exception):
        ctx = stack.top
        if hasattr(ctx, 'sqlite3_db'):
            ctx.sqlite3_db.close()

    @property
    def connection(self):
        ctx = stack.top
        if ctx is not None:
            if not hasattr(ctx, 'sqlite3_db'):
                ctx.sqlite3_db = self.connect()
            return ctx.sqlite3_db

仔细看其中一段代码:

def __init__(self, app=None):
        self.app = app
        if app is not None:
            self.init_app(app)

__init__方法接受一个可选的应用对象,并且如果提供,会调用 init_app 。
init_app 方法使得 SQLite3 对象不需要应用对象就可以实例化。这个方法支持工厂模式来创建应用。 init_app 会为数据库设定配置,如果不提供配置,默认是一个内存中的数据库。此外, init_app 方法附加了 teardown 处理器。 它会试图使用新样式的应用上下文处理器,并且如果它不存在,退回到请求上下文处理器。

此外, init_app 方法用于支持创建应用的工厂模式:

db = Sqlite3()
# Then later on.
app = create_app('the-config.cfg')
db.init_app(app)


正文

flasky/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 config import config
###构造文件导入了大多数正在使用的Flask扩展。

bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()
###由于尚未初始化所需的程序实例app,所以没有初始化扩展,创建扩展类是没有向构造函数传入参数。

def create_app(config_name):
###create_app()函数就是程序的工厂函数,接受一个参数,是程序使用的配置名
    app = Flask(__name__)
    ###初始化Flask的程序实例app
    app.config.from_object(config[config_name])
    ###配置类在config.py文件中定义,其中保存的配置可以使用Flask app.config配置对象提供的from_object()方法直接导入程序。
    config[config_name].init_app(app)

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

    from .main import main as main_blueprint 
    ###从本级目录下的main文件夹中引入main作为main_blueprint蓝本
    app.register_blueprint(main_blueprint)
    ###main_blueprint蓝本注册到程序app上。
    ###在蓝本中定义的路由处于休眠状态,直到蓝本注册程序上后,路由才真正成为程序的一部分。
    return app

config[config_name]调用init_app的方法是在config类中定义的,flask的扩展也能调用init_app方法是因为在flask的扩展Bootstrap,Mail,Moment,SQLAlchemy等等也定义了init_app方法,这两个调用的不是同一个。
flask实例化成app之后,通过app.config.from_object(config[config_name])关联的相关配置,而flask的扩展Bootstrap,Mail,Moment,SQLAlchemy等等只要关联上app这个实例就可以直接访问配置。flask扩展在关联app一般会看到两种方式,一种直接app实例化,像这样Bootstrap(app);一种是先实例化,再init_app(app) 。
这里写图片描述
init_app函数的讲解
1. 把 db 和 app 联系起来,这是 Flask 的插件机制,这样可以将你的设置同 sqlalchemy 关联起来,这个关联就是用的init_app
2. init_app 函数的作用就是把你在 Flask 中的 config 和 sqlalchemy关联,这个可以查看源代码:
flask-sqlalchemy/flask_sqlalchemy/init.py
,而且可以在每次请求结束后自动session.remove()
3. 这个插件同时提供了很多有用的功能,比如分页等,以及一些数据库方面的设置
4. 把 sqlalchemy 的查询等操作方式改为了类似 Django 的方式,这点我比较喜欢
5. create_all()时记得执行这个函数的源文件导入了你创建的 Model
6. 这个插件只是对 sqlalchemy 的封装,只是让其在 Flask 中更加易用,在 June 的 Tornado中也使用了这个插件的一点功能。如果你觉得它的方式让自己觉得不爽,可以直接使用 sqlalchemy。
7. 在 Flask 中的插件貌似都要跟 app 关联。
部分源代码:

def init_app(self, app):
        """This callback can be used to initialize an application for the
        use with this database setup.  Never use a database in the context
        of an application not initialized that way or connections will
        leak.
        """
        app.config.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite://')
        app.config.setdefault('SQLALCHEMY_BINDS', None)
        app.config.setdefault('SQLALCHEMY_NATIVE_UNICODE', None)
        app.config.setdefault('SQLALCHEMY_ECHO', False)
        app.config.setdefault('SQLALCHEMY_RECORD_QUERIES', None)
        app.config.setdefault('SQLALCHEMY_POOL_SIZE', None)
        app.config.setdefault('SQLALCHEMY_POOL_TIMEOUT', None)
        app.config.setdefault('SQLALCHEMY_POOL_RECYCLE', None)
        app.config.setdefault('SQLALCHEMY_MAX_OVERFLOW', None)
        app.config.setdefault('SQLALCHEMY_COMMIT_ON_TEARDOWN', False)
        track_modifications = app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', None)

        if track_modifications is None:
            warnings.warn(FSADeprecationWarning('SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.'))

        if not hasattr(app, 'extensions'):
            app.extensions = {}
        app.extensions['sqlalchemy'] = _SQLAlchemyState(self, app)

        # 0.9 and later
        if hasattr(app, 'teardown_appcontext'):
            teardown = app.teardown_appcontext
        # 0.7 to 0.8
        elif hasattr(app, 'teardown_request'):
            teardown = app.teardown_request
        # Older Flask versions
        else:
            if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
                raise RuntimeError("Commit on teardown requires Flask >= 0.7")
            teardown = app.after_request

        @teardown
        def shutdown_session(response_or_exc):
            if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
                if response_or_exc is None:
                    self.session.commit()
            self.session.remove()
            return response_or_exc

在蓝本中实现程序功能

flasky/app/main/_init_.py
在单脚本程序中,程序实例存在于全局作用域中,路由可以直接使用app.route修饰器定义。但现在程序在运行时创建,只有调用create_app()之后才能使用app.route修饰器,这时定义路由就太晚了。
Flask提供的蓝本和程序类似,也可以定义路由。
不同的是在蓝本中定义的路由处于休眠状态,直到蓝本注册程序上后,路由才真正成为程序的一部分。
使用位于全局作用域中的蓝本时,定义路由的方法几乎和单脚本程序一样。

from flask import Blueprint
###从flask导入BluePrint模块

main = Blueprint('main', __name__)
###通过实例化一个Blueprint类对象可以创建蓝本。这个构造函数有两个必须指定的参数:蓝本的名字和蓝本所在的包或者模块。
###???和程序一样。大多数情况下第二个参数使用Python的__name__变量即可。

from . import views, errors
###导入这两个模块就能把路由和错误处理程序与蓝本关联起来。
###???在最后一行导入,在其他模块导入main时就不导入最后一句了,避免了循环导入依赖。

flasky/app/main/views.py

from flask import render_template, session, redirect, url_for, current_app
from .. import db
###从上级目录下的__init__.py导入db对象【SQLAlchemy类实例,表示程序正在使用的数据库】
from ..models import User
###从上级目录下的models.py导入User模型
from ..email import send_email
###从上级目录下的email.py导入send_email()函数
from . import main
###从本目录__init__.py导入main
###【.后面不写明哪个模块的话默认导入本目录下的 __init__.py】
from .forms import NameForm
###从本目录下的forms.py导入NameForm函数

@main.route('/', methods=['GET', 'POST'])
###路由修饰器有蓝本提供
def index():
    form = NameForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.name.data).first()
        if user is None:
            user = User(username=form.name.data)
            db.session.add(user)
            session['known'] = False
            if current_app.config['FLASKY_ADMIN']:
                send_email(current_app.config['FLASKY_ADMIN'], 'New User',
                           'mail/new_user', user=user)
        else:
            session['known'] = True
        session['name'] = form.name.data
        return redirect(url_for('.index'))
    return render_template('index.html',
                           form=form, name=session.get('name'),
                           known=session.get('known', False))

url_for()函数的第一个参数是路由的端点名,程序路由中默认为视图函数的名字。
在单脚本程序中,index()视图函数的URL可使用url_for("index")获取。
命名空间就是蓝本的名字(Blueprint构造函数的第一个参数),所以视图函数index()注册的端点名是main.index,其URL使用url_for('main.index')获取,如果命名空间是当前请求所在的蓝本,可以使用省略蓝本名,例如url_for('.index')
flasky/app/main/errors.py

from flask import render_template
from . import main


@main.app_errorhandler(404)
###如果使用errorhandler修饰器,那么只有蓝本中的错误才能触发处理程序。
###要想注册处理全局的错误处理程序,必须使用app_errorhandler。
def page_not_found(e):
    return render_template('404.html'), 404


@main.app_errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

flasky/app/main/forms.py
表单对象也要移到蓝本中,保存于app/main/forms.py模块

from flask_wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required


class NameForm(Form):
    name = StringField('What is your name?', validators=[Required()])
    submit = SubmitField('Submit')



app程序包下的所有蓝本共同使用的模块

模型函数【定义Role和User模型】

flasky/app/models.py

from . import db


class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    users = db.relationship('User', backref='role', lazy='dynamic')

    def __repr__(self):
        return '<Role %r>' % self.name


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

    def __repr__(self):
        return '<User %r>' % self.username

发送邮件的模块

flasky/app/email.py

from threading import Thread
from flask import current_app, render_template
from flask_mail import Message
from . import mail


def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)


def send_email(to, subject, template, **kwargs):
    app = current_app._get_current_object()
    ###!!!!!!!!!!!!
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject,
                  sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    thr = Thread(target=send_async_email, args=[app, msg])
    thr.start()
    return thr

课外知识:

链接传递代理作为发送端
current_app._get_current_object()
发送信号
如果你想要发出信号,调用 send() 方法可以做到。 它接受发送端作为第一个参数,和一些推送到信号订阅者的可选关键字参数:

class Model(object):
    ...

    def save(self):
        model_saved.send(self)

永远尝试选择一个合适的发送端。如果你有一个发出信号的类,把 self 作为发送端。如果你从一个随机的函数发出信号,把 current_app._get_current_object() 作为发送端。

传递代理作为发送端
永远不要向信号传递 current_app 作为发送端,使用 current_app._get_current_object() 作为替代。这样的原因是, current_app 是一个代理,而不是真正的应用对象。

静态文件夹放在app程序包下面

static/favicon.ico → app/static/favicon.ico
File renamed without changes.(没变)

模板文件夹放在app程序包下面

templates/404.html → app/templates/404.html
File renamed without changes.(没变)
templates/500.html → app/templates/500.html
File renamed without changes.(没变)
templates/base.html → app/templates/base.html
href=”/”更改成href=”{{ url_for(‘main.index’) }}
这个的具体到哪个命名空间下的端点名
【蓝本文件夹的名字.这个文件夹下面templates里视图函数的名字】
命名空间就是蓝本的名字(Blueprint构造函数的第一个参数),所以视图函数index()注册的端点名是main.index,其URL使用url_for(‘main.index’)获取。

 {% 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">
 {% 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="/">Flasky</a>
 #####################减少
 +            <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="/">Home</a></li>
 #####################减少
 +                <li><a href="{{ url_for('main.index') }}">Home</a></li>
 ####################增加
              </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">&times;</button>
         {{ message }}
     </div>
     {% endfor %}

     {% block page_content %}{% endblock %}
 </div>
 {% endblock %}

 {% block scripts %}
 {{ super() }}
 {{ moment.include_moment() }}
 {% endblock %}

templates/index.html → app/templates/index.html
File renamed without changes.(没变)
templates/mail/new_user.html → app/templates/mail/new_user.html
File renamed without changes.(没变)
templates/mail/new_user.txt → app/templates/mail/new_user.txt
File renamed without changes.(没变)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值