
# 藍圖
## 什么是藍圖?
一個藍圖定義了可用于單個應用的視圖,模板,靜態文件等等的集合。舉個例子,想象一下我們有一個用于管理面板的藍圖。這個藍圖將定義像*/admin/login*和*/admin/dashboard*這樣的路由的視圖。它可能還包括所需的模板和靜態文件。你可以把這個藍圖當做你的應用的管理面板,管它是宇航員的交友網站,還是火箭推銷員的CRM系統。
## 我什么時候會用到藍圖?
藍圖的殺手锏是將你的應用組織成不同的組件。假如我們有一個微博客,我們可能需要有一個藍圖用于網站頁面,比如*index.html*和*about.html*。然后我們還需要一個用于在登錄面板中展示最新消息的藍圖,以及另外一個用于管理員面板的藍圖。站點中每一個獨立的區域也可以在代碼上隔絕開來。最終你將能夠把你的應用依據許多能完成單一任務的小應用組織起來。
> **參見**
> 從Flask文檔中讀到更多使用藍圖的理由
> [Why Blueprints](http://flask.pocoo.org/docs/blueprints/#why-blueprints)
## 我要把它們放哪里?
就像Flask里的每一件事情一樣,你可以使用多種方式組織應用中的藍圖。對我而言,我喜歡按照功能(functional)而非分區(divisional)來組織。(這些術語是我從商業世界借來的)
### 功能式架構
在功能式架構中,按照每部分代碼的功能來組織你的應用。所有模板放到同一個文件夾中,靜態文件放在另一個文件夾中,而視圖放在第三個文件夾中。
```
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()`對象中**注冊**它們。等會我們將在本章了解到這是怎么實現的。
> **參見**
> 當我下筆之時, flask.pocoo.org(Flask官網)就是使用這樣的結構的。 https://github.com/mitsuhiko/flask/tree/website/flask_website
### 分區式架構
在分區式架構中,按照每一部分所屬的藍圖來組織你的應用。管理面板的所有的模板,視圖和靜態文件放在一個文件夾中,用戶控制面板的則放在另一個文件夾中。
```
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()`中。
### 哪種更勝一籌?
選擇使用哪種架構實際上是一個個人問題。兩者間的唯一區別是表達層次性的方式不同 -- 你可以使用任意一種方式架構Flask應用 -- 所以你所需的就是選擇貼近你的需求的那個。
如果你的應用是由獨立的,僅僅共享模型和配置的各組件組成,分區式將是個好選擇。一個例子是允許用戶建立網站的SaaS應用。你將會有獨立的藍圖用于主頁,控制面板,用戶網站,和高亮面板。這些組件有著完全不同的靜態文件和布局。如果你想要將你的藍圖提取成插件,或用之于別的項目,一個分區式架構將是正確的選擇。
另一方面,如果你的應用的組件之間的聯系較為緊密,使用功能式架構會更好。如果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/view/*下的藍圖更多的是視圖的集合而非獨立的組件。同樣的靜態文件將被大多數藍圖重用。大多數模板都拓展自一個主模板。一個功能式的架構是組織這個項目的好的方式。
## 我該怎么使用它們?
### 基本用法
讓我們看看來自Facebook例子的一個藍圖的代碼:
facebook/views/profile.py
```python
from flask import Blueprint, render_template
profile = Blueprint('profile', __name__)
@profile.route('/<user_url_slug>')
def timeline(user_url_slug):
# 做些處理
return render_template('profile/timeline.html')
@profile.route('/<user_url_slug>/photos')
def photos(user_url_slug):
# 做些處理
return render_template('profile/photos.html')
@profile.route('/<user_url_slug>/about')
def about(user_url_slug):
# 做些處理
return render_template('profile/about.html')
```
要想創建一個藍圖對象,你需要import`Blueprint()`類并用參數`name`和`import_name`初始化。通常用`__name__`,一個表示當前模塊的特殊的Python變量,作為`import_name`的取值。
假如使用分區式架構,你得告訴Flask某個藍圖是有著自己的模板和靜態文件夾的。下面是這種情況下我們的定義大概的樣子:
```
profile = Blueprint('profile', __name__,
template_folder='templates',
static_folder='static')
```
現在我們已經定義好了藍圖。是時候向Flask app注冊它了。
facebook/\_\_init\_\_.py
```python
from flask import Flask
from .views.profile import profile
app = Flask(__name__)
app.register_blueprint(profile)
```
現在在*fackbook/views/profile.py*中定義的路徑(比如`/<user_url_slug>`)會被注冊到應用中,就像是被通過`@app.route()`定義的。
### 使用一個動態的URL前綴
繼續看Facebook的例子,注意到所有的個人信息路由都以`<user_url_slug>`開頭并把它傳遞給視圖函數。我們想要用戶通過類似*http://facebook.com/john.doe*的URL訪問個人信息。通過給所有的藍圖的路由定義一個動態前綴,我們可以結束這種單調的重復。
藍圖允許我們定義靜態的或動態的前綴。舉個例子,我們可以告訴Flask藍圖中所有的路由應該以*/profile*作為前綴;這樣是一個靜態前綴。在Fackbook這個例子中,前綴取決于用戶瀏覽的是誰的個人信息。他們在URL對應片段中輸入的文本將決定我們輸出的視圖;這樣是一個動態前綴。
我們可以選擇何時定義我們的前綴。我們可以在下列兩個時機中選擇一個定義前綴:當我們實例化`Blueprint()`類的時候,或當我們在`app.register_blueprint()`中注冊的時候。
下面我們在實例化的時候設置URL前綴:
facebook/views/profile.py
```python
from flask import Blueprint, render_template
profile = Blueprint('profile', __name__, url_prefix='/<user_url_slug>')
# [...]
```
下面我們在注冊的時候設置URL前綴:
facebook/\_\_init\_\_.py
```python
from flask import Flask
from .views.profile import profile
app = Flask(__name__)
app.register_blueprint(profile, url_prefix='/<user_url_slug>')
```
盡管這兩種方式在技術上沒有區別,最好還是在注冊的同時定義前綴。這使得前綴的定義可以集中到頂級目錄中。因此,我推薦使用`url_prefix`。
我們可以在前綴中使用轉換器(converters),就像調用route()一樣。同樣也可以使用我們定義過的任意自定義轉換器。通過這樣做,我們可以自動處理在藍圖前綴中傳遞過來的值。在這個例子中,我們將根據URL片段獲取用戶類并傳遞到我們的profile藍圖中。我們將通過一個名為`url_value_preprocessor()`裝飾器來做到這一點。
facebook/views/profile.py
```python
from flask import Blueprint, render_template, g
from ..models import User
# The prefix is defined 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 %}
```
> **參見**
> Flask文檔中有一個關于如何將你的URL國際化的好教程: http://flask.pocoo.org/docs/patterns/urlprocessors/#internationalized-blueprint-urls }
### 使用一個動態子域名
今天,許多SaaS應用提供用戶一個子域名來訪問他們的軟件。舉個例子,Harvest,是一個針對顧問的日程管理軟件,它在yourname.harvestapp.com給你提供了一個控制面板。下面我將展示在Flask中如何像這樣自動生成一個子域名。
在這一節,我將使用一個允許用戶創建自己的網站的應用作為例子。假設我們的應用有三個藍圖分別針對以下的部分:用戶注冊的主頁面,可用于建立自己的網站的用戶管理面板,用戶的網站。考慮到這三個部分相對獨立,我們將用分區式結構組織起來。
```
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/ | sitemaker/home | 一個普通的藍圖。包括用于*index.html*,*about.html*和*pricing.html*的視圖,模板和靜態文件。
| bigdaddy.sitemaker.com | sitemaker/site | 這個藍圖使用了動態子域名,并包括了用戶網站的一些元素。等下我們來看看用于實現這個藍圖的一些代碼。
| bigdaddy.sitemaker.com/admin | sitemaker/dash | 這個藍圖將使用一個動態子域名和一個URL前綴,把這一節的技術和上一節的結合起來。
定義動態子域名的方式和定義URL前綴一樣。同樣的,我們可以選擇在藍圖文件夾中,或在頂級目錄的\_\_init\_\_.py中定義它。這一次,我們還是在*sitemaker/\_\_init\_\_.py*中放置所有的定義。
sitemaker/\_\_init\_\_.py
```python
from flask import Flask
from .site import site
app = Flask(__name__)
app.register_blueprint(site, subdomain='<site_subdomain>')
```
既然我們用的是分區式架構,藍圖將在*sitemaker/site/\_\_init\_\_.py*定義。
sitemaker/site/\_\_init\_\_py
```python
from flask import Blueprint
from ..models import Site
# 注意首字母大寫的Site和全小寫的site是兩個完全不同的變量。
# Site是一個模塊,而site是一個藍圖。
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()
# 在定義site后才import views。視圖模塊需要import 'site',所以我們需要確保在import views之前定義site。
from . import views
```
現在我們已經從數據庫中獲取可以向請求子域名的用戶展示的站點信息了。
為了使Flask能夠支持子域名,你需要修改配置變量`SERVER_NAME`。
*config.py*
```python
SERVER_NAME = 'sitemaker.com'
```
> **注意**
> 幾分鐘之前,當我正在打這一章的草稿時,聊天室中某人求助稱他們的子域名能夠在開發環境下正常工作,但在生產環境下就會失敗。我問他們是否配置了`SERVER_NAME`,結果發現他們只在開發環境中配置了這個變量。在生產環境中設置這個變量解決了他們的問題。從這里可以看到我(imrobert)和aplavin之間的對話: http://dev.pocoo.org/irclogs/%23pocoo.2013-07-30.log
> **注意**
> 你可以同時設置一個子域名和URL前綴。想一下使用上面的表格的URL結構,我們要怎樣來配置*sitemaker/dash*。
## 使用藍圖重構小型應用
我打算通過一個簡單的例子來展示用藍圖重寫一個應用的幾個步驟。我們將從一個典型的Flask應用起步,然后重構它。
```
config.txt
requirements.txt
run.py
gnizama/
__init__.py
views.py
models.py
templates/
static/
tests/
```
*views.py*文件已經膨脹到10,000行代碼了。重構的工作被一推再推,到現在已經無路可退。這個文件包括了我們的網站的所有的視圖,比如主頁,用戶面板,管理員面板,API和公司博客。
### Step 1:分區式還是功能式?
這個應用由關聯較小的各部分構成。模板和靜態文件不太可能在藍圖間共享,所以我們將使用分區式結構。
### Step 2:分而治之
> **注意**
> 在你對你的應用大刀闊斧之前,把一切提交到版本控制。你不會接受對任何有用的東西的意外刪除。
接下來我們將繼續前進,為我們的新應用創建目錄樹。從為每一個藍圖創建一個目錄開始吧。然后整體復制*views.py*,*static/*和*templates/*到每一個藍圖文件夾。接著你可以從頂級目錄刪除掉它們了。
```
config.txt
requirements.txt
run.py
gnizama/
__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/
```
### Step 3:大掃除
現在我們可以到每一個藍圖中,移除無關的視圖,靜態文件和模板。你在這一階段的處境很大程度上取決于一開始你是怎么組織你的應用的。
最終結果應該是:每個藍圖有一個`views.py`包括了藍圖里的所有視圖,沒有兩個藍圖對同一個路由定義了視圖;每一個*templates/*文件夾應該只包括該藍圖所需的模板;每一個*static/*文件夾應該只包括該藍圖所需的靜態文件。
> **注意**
> 趁此機會消除所有不必要的import。很容易忽略掉他們的存在,但他們會擁塞你的代碼,甚至拖慢你的應用。
### Step 4:藍圖
在這一部分我們把文件夾轉換成藍圖。關鍵在于*\_\_init\_\_.py*文件。作為開始,讓我們看一下API藍圖的定義。
<em>gnizama/api/\_\_init\_\_.py</em>
```python
from flask import Blueprint
api = Blueprint(
'site',
__name__,
template_folder='templates',
static_folder='static'
)
from . import views
```
接著我們可以在gnizama的頂級目錄下的*\_\_init\_\_.py*中注冊這個藍圖。
<em>gnizama/\_\_init\_\_.py</em>
```python
from flask import Flask
from .api import api
app = Flask(__name__)
# 在api.gnizama.com中添加API藍圖
app.register_blueprint(api, subdomain='api')
```
確保路由現在是在藍圖中注冊的,而不是在app對象。下面是在我們重構應用之前,一個在*gnizama/views.py*的API路由可能的樣子。
_gnizama/views.py_
```python
from . import app
@app.route('/search', subdomain='api')
def api_search():
pass
```
在藍圖中它看上去像這樣:
_gnizama/api/views.py_
```python
from . import api
@api.route('/search')
def search():
pass
```
### Step 5:大功告成
現在我們的應用已經比只有單個臃腫的*views.py*的時候更加模塊化了。
## 總結
* 一個藍圖包括了可以作為獨立應用的視圖,模板,靜態文件和其他插件。
* 藍圖是組織你的應用的好辦法。
* 在分區式架構下,每個藍圖對應你的應用的一個部分。
* 在功能式架構下,每個藍圖就只是視圖的集合。所有的模板和靜態文件都放在一塊。
* 要使用藍圖,你需要定義它,并在應用中用`Flask.register_blueprint()`注冊它。
* 你可以給一個藍圖中的所有路由定義一個動態URL前綴。
* 你也可以給藍圖中的所有路由定義一個動態子域名。
* 僅需五步走,你可以用藍圖重構一個應用。