蓝图

什么是蓝图?

一个蓝图定义了视图,模板,静态文件以及可以用于应用程序的其它元素的集合。例如,让我们假设下我们有一个管理面板的蓝图。这个蓝图会定义一些包含像 /admin/login 和 /admin/dashboard 路由的视图。它也可能包含服务于这些路由的模板以及静态文件。接着我们可以使用这个蓝图添加一个管理面板到我们的应用程序中,不论我们的应用程序是什么类型的。

为什么要使用蓝图?

蓝图“杀手级”使用场景就是把我们的应用程序组织成不同的组件。对于一个类似 Twitter 的微型博客,我们可能有一个针对网站页面的蓝图,例如,index.html 和 about.html。接着我们还有另外一个带有登录面板的蓝图,在那里我们显示了所有最新的文章,然后我们还有一个用于后台管理的面板的蓝图。网站的每一个不同的区域也能够被分成不同区域的代码来实现。这能够让我们用几个小的 “apps” 构建我们的应用程序,每一个 apps 都在做一件事情。

你把它们放哪里?

使用蓝图组织我们的应用程序有很多的方式。通常情况下,我们可以考虑按功能结构和分区这两种选择(功能结构和分区这两个词语我借鉴了商业上的概念)。

功能结构

按照功能结构的话,你可以通过它们所做的事情来组织你的应用程序的结构。模板在一个文件夹中,静态文件在另一个文件夹中,视图在第三个文件夹中。

yourapp/
    __init__.py
    static/
    templates/
        home/
        control_panel/
        admin/
    views/
        __init__.py
        home.py
        control_panel.py
        admin.py
    models.py

除了 yourapp/views/init.py,在上面列表中的 yourapp/views/ 文件夹中的每一个 .py 文件都是一个蓝图。在 yourapp/init.py 中我们要导入这些蓝图并且在我们的 Flask() 对象中 注册 它们。我们会在本章的后面看看实现方式。

分区

对于分区结构了,你可以基于它们有助于应用程序的哪一部分来组织应用程序的结构。管理面板所有的模板,视图以及静态文件都在一个文件夹中,用户控制的所有的模板,视图和静态文件在另一个文件夹中。

yourapp/
    __init__.py
    admin/
        __init__.py
        views.py
        static/
        templates/
    home/
        __init__.py
        views.py
        static/
        templates/
    control_panel/
        __init__.py
        views.py
        static/
        templates/
    models.py

像上面列出的应用程序的分区结构,在 yourapp/ 中每一个文件夹都是一个单独的蓝图。所有的这些蓝图都会应用到顶层 \\init\\.py 中的 Flask() 对象中。

哪一个是最好的?

你选择的组织结构很大程度上是一种个人决定。唯一的区别是层次结构的表示方式,因此你可以自由地决策要使用的组织结构,你可以选择一个对自己有意义的。

如果你的应用程序大部分是独立的结构,仅仅共享着像模型和配置,分区结构就是合适的选择方式。一个例子就是让用户建立网站的 SaaS 应用程序。你可能就有蓝图分别针对主页,控制面板,用户网站,以及管理面板。这些组件可能有完全不同的静态文件和布局。如果考虑负责这个应用程序或者分拆/重构这个应用程序的话,分区结构会更加适用一些。

另一方面,如果你的应用程序联系地更加紧密一些的话,它可能用一个功能结构呈现更加合适。一个示例就是 Facebook。如果 Facebook 使用 Flask 的话,它可能就有静态页(例如,登录-注销页,注册,关于等等),控制面板(例如,新闻源),个人主页(/robert/about 以及 /robert/photos),设置(/settings/security 和 /settings/privacy)等等一些蓝图。这些组件共享一个通用的布局和样式,但是每一个也会有自己的布局。下面的列表中展示了一个进行大量删减版的 Facebook 的样子,如果它是使用 Flask 构建的话。

facebook/
    __init__.py
    templates/
        layout.html
        home/
            layout.html
            index.html
            about.html
            signup.html
            login.html
        dashboard/
            layout.html
            news_feed.html
            welcome.html
            find_friends.html
        profile/
            layout.html
            timeline.html
            about.html
            photos.html
            friends.html
            edit.html
        settings/
            layout.html
            privacy.html
            security.html
            general.html
    views/
        __init__.py
        home.py
        dashboard.py
        profile.py
        settings.py
    static/
        style.css
        logo.png
        models.py

在 facebook/views/ 中的蓝图仅仅是视图的集合而不是完全独立的组件。同一的静态文件将会被大多数的蓝图的视图使用。大多数模板都会扩展一个主模板。功能结构是组织这个项目的一种好的方式。

你如何使用它们?

基本用法

让我们看看 Facebook 示例中的其中一个蓝图的代码。

# facebook/views/profile.py

from flask import Blueprint, render_template

profile = Blueprint('profile', __name__)

@profile.route('/<user_url_slug\\>')
def timeline(user_url_slug):
    # Do some stuff
    return render_template('profile/timeline.html')

@profile.route('/<user_url_slug\\>/photos')
def photos(user_url_slug):
    # Do some stuff
    return render_template('profile/photos.html')

@profile.route('/<user_url_slug\\>/about')
def about(user_url_slug):
    # Do some stuff
    return render_template('profile/about.html')

要创建一个蓝图对象,我们先导入 Blueprint() 类并且用参数 name 和 import_name 初始化它。通常情况下,import_name 就是 __name__,这是一个包含当前模块名称的特殊 Python 变量。

在这个 Facebook 示例中我们使用了一个功能结构。如果我们使用分区结构的话,我们要通知 Flask 蓝图有自己的模板和静态文件夹。此块的代码大概的样子如下所示。

profile = Blueprint('profile', __name__,
                    template_folder='templates',
                    static_folder='static')

现在我们已经定义我们的蓝图。是时候在我们的 Flask 应用程序中注册它。

# facebook/__init__.py

from flask import Flask
from .views.profile import profile

app = Flask(\\_\\_name\\_\\_)
app.register_blueprint(profile)

现在定义在 facebook/views/profile.py 上的路由(例如,/<user\_url\_slug>)在应用程序上注册并且表现得像你使用 @app.route() 定义它们一样。

使用动态的 URL 前缀

继续 Facebook 例子,注意到所有的用户资料路由都是以 <user\_url\_slug> 开始并且把它的值传递给视图。我们希望用户们能够通过浏览像 https://facebo-ok.com/john.doe 类似的网址访问用户资料页。我们可以通过为所有的蓝图的路由定义一个动态的前缀来停止重复工作。

蓝图可以让我们定义动态和静态的前缀。我们可以通知 Flask 在一个蓝图中的所有的路由都是以 /profile 为前缀的(这里的 /profile 只是一个示例),这就是一个静态的前缀。至于 Facebook 示例,前缀是基于浏览的用户资料而变化。无论他们浏览哪个用户的个人资料,我们都应该在 URL 标签中显示。这就是一个动态的前缀。

我们可以选择在什么时候定义我们的前缀。我们可以在两个地方中的任意一个定义前缀:当我们实例化 Blueprint() 类或者当我们用 app.register_blueprint() 注册它的时候。

# facebook/views/profile.py

from flask import Blueprint, render_template

profile = Blueprint('profile', __name__, url_prefix='/<user_url_slug>')

# [...]
{% endhighlight %}

{% highlight python %}
# facebook/__init__.py

from flask import Flask
from .views.profile import profile

app = Flask(__name__)
app.register_blueprint(profile, url_prefix='/<user_url_slug>')

尽管没有任何技术因素限制任何一种方法,最好是在注册的时候统一定义可用的前缀。这使得以后修改或者调整更加容易和方便些。因为这个原因,我建议在注册的时候设置 url\_prefix。

我们可以在动态前缀中使用转换器,就像在 route() 调用中一样。这个也包含了我们自定义的转换器。当使用了转换器,我们可以在把前缀交给视图之前进行预处理。在这个例子中我们要基于传入到我们用户资料蓝图的 URL 中的 user\_url\_slug 来获取用户对象。这里我们需要使用 url\_value\_preprocessor() 装饰一个函数来完成这个需求。

# facebook/views/profile.py

from flask import Blueprint, render_template, g

from ..models import User

# The prefix is defined on registration in facebook/\\_\\_init\\_\\_.py.
profile = Blueprint('profile', \\_\\_name\\_\\_)

@profile.url_value_preprocessor
def get_profile_owner(endpoint, values):
    query = User.query.filter_by(url_slug=values.pop('user_url_slug'))
    g.profile_owner = query.first_or_404()

@profile.route('/')
def timeline():
    return render_template('profile/timeline.html')

@profile.route('/photos')
def photos():
    return render_template('profile/photos.html')

@profile.route('/about')
def about():
    return render_template('profile/about.html')

我们使用 g 对象来存储用户对象并且 g 可以在 Jinja2 模板中使用。这就意味着对于实现一个极其简单的系统的话,我们现在要做的就是在视图中渲染模板。

{% facebook/templates/profile/photos.html %}

{% extends "profile/layout.html" %}
{% for photo in g.profile_owner.photos.all() %}
    <img src="{{ photo.source_url }}" alt="{{ photo.alt_text }}" />
{% endfor %}

使用动态的子域(subdomain)

许多 SaaS(软件即服务)的应用程序目前提供用户一个子域,用户可以使用这个子域来访问他们的软件。例如,Harvest 是一个时间追踪管理应用程序,它允许你从 yourname.harvestapp.com 访问你的控制面板。这里我将向你展示如何使用 Flask 处理像 Harvest 一样自动生成的子域。

对于这一部分,我们将要使用允许用户创建他们自己的网站的应用程序示例。假设我们的应用程序有三个蓝图,它们分别用于用户登录的主页,用户构建他们的网站的用户管理面板以及用户的网站。由于这三部分是不相关的,我们用分区结构来组织结构。

sitemaker/
    __init__.py
    home/
        __init__.py
        views.py
        templates/
            home/
        static/
            home/
    dash/
        __init__.py
        views.py
        templates/
            dash/
        static/
            dash/
    site/
        __init__.py
        views.py
        templates/
            site/
        static/
            site/
    models.py

下面的描述展示了本应用程序中所有的蓝图。

  • URL: sitemaker.com,Route: sitemaker/home,描述: 只是一个普通的蓝图。围绕 index.html,about.html 以及 pricing.html 的视图,模板以及静态文件。
  • URL: bigdaddy.sitemaker.com,Route:sitemaker/site,描述:这个蓝图使用一个动态的子域并且包含用户网站的元素。 我们会在下面介绍一些用于实现这个蓝图的代码。
  • URL:bigdaddy.sitemaker.com/admin,Route: sitemaker/dash,描述:这个蓝图使用了一个动态的子域和一个 URL 前缀。

我们可以用定义我们 URL 前缀同样的方式来定义我们的动态子域。两个选择:在蓝图文件夹或者在顶层的 init.py 中都是可用的,但是我们坚持再一次把它定义在 sitemaker/\\init.py\\ 中。

# sitemaker/\\_\\_init\\_\\_.py

from flask import Flask
from .site import site

app = Flask(\\_\\_name\\_\\_)
app.register_blueprint(site, subdomain='<site_subdomain>')

因为我们使用了分层结构,我们会在 sitema-ker/site/\\init\\.py 中定义蓝图。

# sitemaker/site/\\_\\_init\\_\\_py

from flask import Blueprint

from ..models import Site

# Note that the capitalized Site and the lowercase site
# are two completely separate variables. Site is a model
# and site is a blueprint.

site = Blueprint('site', __name__)

@site.url_value_preprocessor
def get_site(endpoint, values):
    query = Site.query.filter_by(subdomain=values.pop('site_subdomain'))
    g.site = query.first_or_404()

# Import the views after site has been defined. The views
# module will needto import 'site' so we need to make
# sure that we import views after site has been defined.
import .views

现在我们从数据库中获取了站点信息,我们将会把用户的站点展示给正在请求他们子域的访问者。

为了让 Flask 能和子域一起工作,我们将需要指定 SERVER_NAME 配置变量。

# config.py

SERVER_NAME = 'sitemaker.com'

使用蓝图重构小的应用程序

我们将会介绍把一个应用程序重构成使用蓝图的步骤。我们选择一个很典型的 Flask 应用程序并且重构它。

config.txt
requirements.txt
run.py
U2FtIEJsYWNr/
  __init__.py
  views.py
  models.py
  templates/
  static/
tests/

views.py 文件已经增长到 10,000 行的代码!我们一直在拖延重构它的时间,但是现在是时候重构。views.py 文件包含我们网站每一部分的视图。这些部分分别是主页,用户控制面板,管理控制面板,API 和公司的博客。

步骤 1:分区或者功能?

这个应用是有完全不同的部分组成。例如,用户控制面板和公司博客之间的模板和静态文件是完全不共享的。我们将选择分区结构。

步骤 2:移动一些文件

下一步我们将继续前进,并且为我们新的应用程序创建目录树。我们可以在一个包目录里为每一个蓝图创建一个文件夹。接着我们将完整地复制 views.py,static/ 和 templates/ 到每个蓝图目录。最后,我们可以从顶层包目录中删除它们(views.py,static/ 和 templates/)。

config.txt
requirements.txt
run.py
U2FtIEJsYWNr/
  __init__.py
  home/
    views.py
    static/
    templates/
  dash/
    views.py
    static/
    templates/
  admin/
    views.py
    static/
    templates/
  api/
    views.py
    static/
    templates/
  blog/
    views.py
    static/
    templates/
  models.py
tests/

步骤 3:废话少说

现在我们可以到每一个蓝图目录中去删除那些不属于该蓝图的视图,静态文件和模板。你如何做这一步很大程度上取决你的应用程序是如何组织结构的。

最终的结果就是每一个蓝图只有一个 views.py 文件,并且 views.py 文件内的函数只适用于本蓝图。没有两个蓝图会为同一个路由定义一个视图。每一个 templates/ 目录只包含在本蓝图的视图中使用的模板。每一个 static/ 目录应该只包含有本蓝图使用的静态文件。

步骤 4:蓝图

这是我们把我们的目录转变成为蓝图的关键一步。关键就是在 init.py 文件。首先,我们看看 API 蓝图的定义。

# U2FtIEJsYWNr/api/__init__.py

from flask import Blueprint

api = Blueprint(
    'site',
    __name__,
    template_folder='templates',
    static_folder='static'
)

import .views

接下来我们在 U2FtIEJsYWNr 包顶层 init.py 文件里注册这个蓝图。

# U2FtIEJsYWNr/__init__.py

from flask import Flask
from .api import api

app = Flask(__name__)

# Puts the API blueprint on api.U2FtIEJsYWNr.com.
app.register_blueprint(api, subdomain='api')

确保路由是注册到蓝图上而不是应用程序(app)对象上。

# U2FtIEJsYWNr/views.py

from . import app

@app.route('/search', subdomain='api')
def api_search():
    pass
# U2FtIEJsYWNr/api/views.py

from . import api

@api.route('/search')
def search():
    pass

步骤 5:享受

现在我们应用程序比起它原来一个庞大的 views.py 文件已经是大大地模块化了。路由的定义十分简单,因为我们可以在每一个蓝图里面单独定义并且可以为每个蓝图像子域和 URL 前缀一样配置。

  • 一个蓝图定义了视图,模板,静态文件以及可以用于应用程序的其它元素的集合。
  • 蓝图是组织你的应用程序的一种很好的方式。
  • 在分区结构中,每一个蓝图是一个视图,模板,静态文件的集合,它们构成了应用程序的一部分。
  • 在功能结构中,每一个蓝图只是视图的集合。所有的模板放在一起,静态文件也一样。
  • 要使用一个蓝图,你首先需要定义它,接着通过调用 Flask.register\\_blueprint() 来注册它。
  • 你可以定义一个动态的 URL 前缀,它能够用于在一个蓝图里所有的路由。
  • 你也可以定义一个动态的子域,它能够用于一个蓝图里所有的路由。
  • 使用蓝图重构一个越来越大的应用程序能够用 5 个小步骤来完成。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,600评论 18 139
  • 解释1: 允许将应用组织为模块,每个模块有自洽的 MVC,开发者做些工作可以使模块间依赖尽可能少,必要时可以按 b...
    大诗兄_zl阅读 2,188评论 1 1
  • Flask蓝图提供了模块化管理程序路由的功能,使程序结构清晰、简单易懂。下面分析蓝图的使用方法 假如说我们要为某所...
    枫林夕阳阅读 27,066评论 3 13
  • 22年12月更新:个人网站关停,如果仍旧对旧教程有兴趣参考 Github 的markdown内容[https://...
    tangyefei阅读 35,160评论 22 257
  • 1、为使项目结构模块化,更加清晰,把相同模块的视图函数放在同一个蓝图中,同一个文件中 2、创建蓝图1、新建一个py...
    小船撑不起大帆阅读 522评论 0 0