第六章 实例项目的描述

尽管在单一脚本中编写小型WEB程序很方便,但是这种方法并不能广泛使用。程序变复杂之后,使用单个大型源码文件会导致后期的维护及扩展困难。但是不同于其它的WEB框架,Flask并不强制要求大型项目使用特定的组织方式,程序结构的组织方式完全由开发者决定。

项目的结构

ousimd/

|-- flask/

    |-- <python虚拟环境>

|-- app/ <项目的模块名称>

    |-- static/ <静态文件夹>

    |-- templates/ <HTML模板文件夹>

        |-- main/ <前端蓝图>

            |-- __init__.py

            |-- views.py <路由和视图函数文件>

            |-- forms.py <表单类文件, wtforms插件必须项>
        |-- admin/ <后台管理蓝图>

            |-- __init__.py

            |-- views.py <路由和视图函数文件>

            |-- forms.py <表单类文件, wtforms插件必须项>

    |-- __init__.py

    |-- another.py <可选项,根据需要增加>

    |-- models.py <数据库模型文件>

|-- migrations/ <数据库表关系文件夹,Flask-Migrate迁移数据库时使用>

|-- tests/

    |-- __init__.py

    |-- test*.py <测试文件>

|-- requeirements.txt <插件依赖包>

|-- config.py <项目的配置文件>

|-- manage.py <用于启动程序以及其它程序任务>

配置选项

在项目中配置config.py

# -*- coding:utf-8 -*-
__author__ = u'东方鹗'

import os
import hashlib

basedir = os.path.abspath(os.path.dirname(__file__))


class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or hashlib.new(name='md5', string='ousikeji@#').hexdigest()
    SQLALCHEMY_COMMIT_ON_TEARDOWN = True    

    @staticmethod
    def init_app(app):
        pass


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


class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
                            'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')


class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
                            'sqlite:///' + os.path.join(basedir, 'data.sqlite')


config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

基类Config中包含通用配置,子类分别定义专用的配置。如果需要,你还可以添加其它配置类。

为了让配置方式更加灵活且安全,某些配置可以从环境变量中导入。例如,SECRET_KEY的值,这是个敏感信息,可以在环境中设定,但系统也提供了一个默认值,以防环境中没有定义。
在3个子类中,SQLALCHEMY_DATABASE_URI变量都被指定了不同的值,这样程序就可以在不同的配置环境中运行,每个环境都使用不同的数据库。其中SQLALCHEMY_DATABASE_URI变量对应的数据库引擎在 ** 《第七章 使用 Flask 扩展管理数据库》 ** 有详细介绍。
配置类可以定义init_app()类方法,其参数是程序实例。在这个方法中,可以执行对当前环境的配置初始化。现在,基类Config中的init_app()方法为空。

程序包

程序包是用来保存程序的所有代码、模板和静态文件。我们可以把这个包直接成为app(应用),如果有需求,也可使用一个程序专用名字。templates和static文件夹是程序包的一部分,因此这两个文件夹被移到了app中。数据库模型app/models.py和其他专用支持函数也被移到这个包中。

使用工厂函数

构造文件导入了大多数正在使用的Flask扩展。由于尚未初始化所需的程序实例,所以没有初始化扩展,创建扩展类时没有向构造函数传入参数。create_app()函数就是程序的工厂函数,接受一个参数,是程序使用的配置名。配置类在config.py文件中定义,其中保存的配置可以使用Flask的app.config配置对象提供的from_object()方法直接导入程序。至于配置对象,测可以通过名字从config字典中选择。程序创建并配置好后,接能初始化扩展了。在之前创建的扩展对象上调用init_app()可以完成初始化过程。

程序包的构造文件app/__init__.py

# -*- coding:utf-8 -*-
__author__ = '东方鹗'

from flask import Flask
from config import config

# db = SQLAlchemy()

def create_app(config_name):
    """ 使用工厂函数初始化程序实例"""
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app=app)

    # db.init_app(app=app)

    return app

工厂函数返回创建的程序示例,不过要注意,现在工厂函数创建的程序还不完成,在今后会陆续添加和完善。

使用蓝图

为了获得最大的灵活性,程序包中创建一个子包,用于保存蓝图。

创建 蓝图main和蓝图admin

app/main/__init__.py

# -*- coding:utf-8 -*-
__author__ = '东方鹗'


from flask import Blueprint

main = Blueprint('main', __name__)

# 在末尾导入相关模块,是为了避免循环导入依赖,因为在下面的模块中还要导入蓝本main
from . import views, errors

app/admin/__init__.py

# -*- coding:utf-8 -*-
__author__ = '东方鹗'


from flask import Blueprint

admin = Blueprint('admin', __name__)

# 在末尾导入相关模块,是为了避免循环导入依赖,因为在下面的模块中还要导入蓝本main
from . import views, errors

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

蓝图在工厂函数create_app()中注册

app/__init__.py中注册蓝图

def create_app(config_name):
    # ...

    # 注册蓝本 main
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint, url_prefix='/main')

    # 注册蓝本 admin
    from .admin import admin as admin_blueprint
    app.register_blueprint(admin_blueprint, url_prefix='/admin')

    return app

蓝图的错误处理(以蓝图main为例)

app/main/errors.py


# -*- coding:utf-8 -*-
__author__ = '东方鹗'

from flask import render_template, request, jsonify
from . import main


@main.app_errorhandler(403)
def forbidden(e):
    if request.accept_mimetypes.accept_json and \
            not request.accept_mimetypes.accept_html:
        response = jsonify({'error': 'forbidden'})
        response.status_code = 403
        return response
    return render_template('main/errors/403.html'), 403


@main.app_errorhandler(404)
def page_not_found(e):
    if request.accept_mimetypes.accept_json and \
            not request.accept_mimetypes.accept_html:
        response = jsonify({'error': 'not found'})
        response.status_code = 404
        return response
    return render_template('main/errors/404.html'), 404


@main.app_errorhandler(500)
def internal_server_error(e):
    if request.accept_mimetypes.accept_json and \
            not request.accept_mimetypes.accept_html:
        response = jsonify({'error': 'internal server error'})
        response.status_code = 500
        return response
    return render_template('main/errors/500.html'), 500

在蓝图中编写错误处理程序少有不同,如果使用errorhandler修时期,那么只有蓝图中的错误才能触发处理程序。要想注册程序全局的错误处理程序,必须使用app_errorhandler。当然,示例还提供基于json的错误提示编写方法。

蓝图中定义程序路由(以蓝图main为例)

app/main/views.py


# -*- coding:utf-8 -*-
__author__ = '东方鹗'

from flask import render_template
from . import main


@main.route('/', methods=['GET', 'POST'])
def index():

    return render_template('main/index.html')

templates/main/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <h1>蓝图main欢迎您!</h1>
</body>
</html>

在蓝图中编写视图函数主要有两点不同:第一,和前面的错误处理程序一样,路由修饰器由蓝图提供;第二,url_for()函数的用法不同。在蓝图中,Flask会为蓝图中的全部端点加上一个 ** 命名空间 ** ,这样就可以在不同的蓝图中使用相同的端点名定义视图函数,而不会产生冲突。命名空间就是蓝图的名字(Blueprint构造函数的第一个参数),所以视图函数index()注册的端点名是main.index,其URL使用url_for('main.index')获取。

启动脚本

manage.py

# -*- coding:utf-8 -*-
__author__ = '东方鹗'

import os
from app import create_app, db
from flask_script import Manager, Shell
# from flask_migrate import Migrate, MigrateCommand

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


def make_shell_context():
    return dict(app=app, db=db)

manager.add_command("shell", Shell(make_context=make_shell_context))
# manager.add_command('db', MigrateCommand)


@manager.command
def test():
    """ 单元测试 """
    import unittest
    tests = unittest.TestLoader().discover('tests')
    unittest.TextTestRunner(verbosity=2).run(test=tests)

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

需求文件

程序中必须包含一个requirements.txt文件,用于记录所有依赖包及其精确的版本号。如果要在另一台电脑上重新生成虚拟环境,这个文件的重要性就体现出来了,例如部署程序时使用的电脑。pip可以使用如下命令自动生成这个文件:

(flask)$ pip freeze > requirements.txt

安装或升级包后,最好更新这个文件。

如果你要创建这个虚拟环境的完全副本,你可以创建一个心的虚拟环境,并在其上运行以下命令:

(flask)$ pip install -r requirements.txt

如果在今后由于升级程序包遇到问题,你可以随时换回这个需求文件中的版本,因为这些版本和程序兼容。

结果展示

启动程序

(flask)$ python manage.py runserver --host 0.0.0.0

在浏览器中输入http://localhost:5000/main,显示如下内容。
[图片上传失败...(image-8ff1f3-1509964243171)]

OK,到此一个按照大型项目结构组织的项目已经完成了基础的构造。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容