
# 關于視圖和路由的進階技巧
## 視圖裝飾器
Python裝飾器讓我們可以用其他函數包裝特定函數。
當一個函數被一個裝飾器"裝飾"時,那個裝飾器會被調用,接著會做額外的工作,修改變量,調用原來的那個函數。我們可以把我們想要重用的代碼作為裝飾器來包裝一系列視圖。
裝飾器的語法看上去像這樣:
```python
@decorator_function
def decorated():
pass
```
如果你看過Flask入門指南,那么對這個語法應該不感到陌生。`@app.route`正是用于在Flask應用中給視圖函數設定路由URL的裝飾器。
讓我們看一下在你的Flask應用中用得上的一些別的裝飾器。
### 認證
Flask-Login使得用戶認證系統的實現不再困難。
除了處理用戶認證的細節之外,Flask-Login允許我們使用`@login_required`這個裝飾器來驗證用戶對某些資源的訪問權限。
下面是從一個用到Flask-Login和`@login_required`裝飾器的一個示范應用中獲取的例子:
```
from flask import render_template
from flask_login import login_required, current_user
@app.route('/')
def index():
return render_template("index.html")
@app.route('/dashboard')
@login_required
def account():
return render_template("account.html")
```
> **注意**
> `@app.route`必須是最外面的視圖裝飾器。
只有已經驗證的用戶能夠接觸到*/dashboard*路由。你可以配置Flask-Login來重定向未驗證用戶到登錄頁面,返回HTTP 401狀態碼或別的你樂意的事。
> **參見**
> 通過[官方文檔](http://flask-login.readthedocs.org/en/latest/)可以讀到更多關于Flask-Login的內容
### 緩存
意淫一下,假如你的應用突然有一天在微博/朋友圈或網上別的地方火了。
于是秒秒鐘會有成千上萬的請求涌向你的應用。你的主頁在每個請求中都要從數據庫跑上一大趟,結果海量的請求導致網站慢得像教務系統一樣。
你能做什么來加速這一過程,以免用戶以為你的應用掛掉了?
答案不止一個,不過就本章主旨而言,標準答案是實現緩存。
特別的,我們將要用到[Flask-Cache](http://pythonhosted.org/Flask-Cache/)拓展。這個拓展給我們提供一個可以用來緩存某個響應一段時間的裝飾器。
你可以將Flask-Cache配置成跟你想用的后臺緩存一起使用。一個普遍的選擇是[Redis](http://redis.io/),一個容易配置和使用的軟件。
假設Flask-Cache已經配置好了,下面是我們的被裝飾的視圖的例子:
```
from flask_cache import Cache
from flask import Flask
app = Flask()
# 通過這個方式獲取相關配置
cache = Cache(app)
@app.route('/')
@cache.cached(timeout=60)
def index():
[...] # 進行一些數據庫調用來獲取所需信息
return render_template(
'index.html',
latest_posts=latest_posts,
recent_users=recent_users,
recent_photos=recent_photos
)
```
現在這個函數將會在每60秒最多運行一次。響應的結果會被保存在緩存中,并可以讓期間的每一個請求獲取。
> **注意**
> Flask-Cache同時允許我們**記住**函數 - 或緩存通過給定的參數調用的某個函數。你甚至可以緩存過于復雜的Jinja2模板片段!
### 自定義裝飾器
在這個例子中,讓我們假設我們有一個應用,每個月要求用戶定期付費。如果一個用戶的賬戶已經過期,我們要重定向他們到賬單頁面,并告知其悲傷的現實。
myapp/util.py
```
from functools import wraps
from datetime import datetime
from flask import flash, redirect, url_for
from flask_login import current_user
def check_expired(func):
@wraps(func)
def decorated_function(*args, **kwargs):
if datetime.utcnow() > current_user.account_expires:
flash("Your account has expired. Please update your billing information.")
return redirect(url_for('account_billing'))
return func(*args, **kwargs)
return decorated_function
```
1. 當用`@check_expired`裝飾一個函數時,`check_expired()`被調用,被裝飾的函數作為一個參數被傳遞進來。
2. `@wraps`是一個裝飾器,告知Python函數`decorated_function()`包裝了視圖函數`func()`。嚴格來說這不是必須的,但是這么做會使得裝飾函數更加自然一些,更有利于文檔和調試。
3. `decorated_function`將截取原本傳遞給視圖函數`func()`的args和kwargs。在這里我們檢查用戶的賬戶是否過期。如果是,我們將閃爍一則信息,并重定向到賬單頁面。
4. 既然已經處理好自己的事情,我們把原來的參數交由視圖函數`func()`去繼續執行。
位于最頂部的裝飾器將最先運行,然后調用下一個函數:一個視圖函數或下一個裝飾器。裝飾器語法只是一個語法糖而已。
```python
# 這樣
@foo
@bar
def one():
pass
```
```python
# 等同于這樣:
def two():
pass
two = foo(bar(two))
r2 = two()
r1 == r2 # True
```
下面這個例子用到了我們自定義的裝飾器和來自Flask-Cache拓展的`@login_required`裝飾器。我們可以將多個裝飾器堆成棧來一起使用。
myapp/views.py
```
from flask import render_template
from flask_login import login_required
from . import app
from .util import check_expired
@app.route('/use_app')
@login_required
@check_expired
def use_app():
"""歡迎光臨"""
return render_template('use_app.html')
@app.route('/account/billing')
@login_required
def account_billing():
"""拿賬單來"""
# [...]
return render_template('account/billing.html')
```
當一個用戶試圖訪問*/use\_app*時,`check_expired()`將在執行視圖函數之前確保相關的賬戶資料不會泄漏。
> 參見
> 在Python文檔中可以讀到更多關于`wraps()`的內容:<http://docs.python.org/2/library/functools.html#functools.wraps>
## URL轉換器
### 內建轉換器
當你在Flask中定義一個路由時,你可以將指定的一部分轉換成Python變量并傳遞給視圖函數。
```
@app.route('/user/<username>')
def profile(username):
pass
```
在URL中作為<username>的那一部分內容將作為`username`參數傳遞給視圖函數。你也可以指定一個轉換器過濾出特定的類型。
```
@app.route('/user/id/<int:user_id>')
def profile(user_id):
pass
```
在這個代碼塊中,<http://myapp.com/user/id/tomato> 這個URL會返回一個404狀態碼 -- 此物無處覓。
這是因為URL中預期是整數的部分卻遇到了一串字符串。
我們可以有另外一個接受一個字符串的視圖函數。*/usr/id/tomato/*將調用它,而前一個函數只會被*/user/id/124*所調用。
下面是來自Flask文檔的關于默認轉換器的表格:
| 類型 | 作用 |
| -------|------------------------------- |
| string | 接受任何沒有斜杠`/`的文本(默認)|
| int | 接受整數 |
| float | 類似于`int`,但是接受的是浮點數 |
| path | 類似于`string`,但是接受斜杠`/` |
### 自定義轉換器
我們也可以按照自己的需求打造自定義的轉換器。
Reddit - 一個知名的鏈接分享網站 - 用戶在此可以創建和管理基于主題和鏈接分享的社區。
比如`/r/python`和`/r/flask`,分別由URL`reddit.com/r/python`和`reddit.com/r/flask`表示。
Reddit有一個有趣的特性是,通過在URL中用一個`+`隔開各個社區名,你可以同時看到來自多個社區的帖子。比如`reddit.com/r/python+flask`。
我們可以使用一個自定義轉換器來實現這種特性。
我們可以接受由加號隔離開來的任意數目參數,通過我們的ListConverter轉換成一個列表,并傳遞給視圖函數。
util.py
```python
from werkzeug.routing import BaseConverter
class ListConverter(BaseConverter):
def to_python(self, value):
return value.split('+')
def to_url(self, values):
return '+'.join(BaseConverter.to_url(value)
for value in values)
```
我們需要定義兩個方法:`to_python()`和`to_url()`。
一如其名,`to_python()`用于轉換路徑成一個Python對象,并傳遞給視圖函數。而`to_url()`被`url_for()`調用,來轉換參數成為符合URL的形式。
為了使用我們的ListConverter,我們首先得將它的存在告知Flask。
/myapp/\_\_init\_\_.py
```
from flask import Flask
app = Flask(__name__)
from .util import ListConverter
app.url_map.converters['list'] = ListConverter
```
> **注意**
> 假如你的util模塊有一行`from . import app`,那么有可能陷入循環import的問題。這就是為什么我等到app初始化之后才import ListConverter。
現在我們可以一如使用內建轉換器一樣使用我們的轉換器。我們在字典中指定它的鍵為"list",所以我們可以在`@app.route()`中這樣使用:
views.py
```python
from . import app
@app.route('/r/<list:subreddits>')
def subreddit_home(subreddits):
"""顯示給定subreddits里的所有帖子"""
posts = []
for subreddit in subreddits:
posts.extend(subreddit.posts)
return render_template('/r/index.html', posts=posts)
```
這應該會像Reddit的子社區系統一樣工作。這樣的方法可以用來實現你能想到的URL轉換器。
## 總結
* Custom URL converters can be a great way to implement creative features involving URL’s.
* 來自Flask-Login的`@login_required`裝飾器可以幫助你限制驗證用戶對視圖的訪問。
* Flask-Cache插件為你提供一組裝飾器來實現多種方式的緩存。
* 我們可以開發自定義視圖裝飾器來幫助我們組織自己的代碼,并堅守DRY(Don't Repeat Yourself 不重復你自己)原則。
* 自定義的URL轉換器將會讓你很嗨地玩轉URL。