
# 模板
盡管Flask并不強迫你使用某個特定的模板語言,它還是默認你會使用Jinja。在Flask社區的大多數開發者使用Jinja,并且我建議你也跟著做。有一些插件允許你用其他模板語言進行替代(比如[Flask-Genshi](http://pythonhosted.org/Flask-Genshi/)和[Flask-Mako](http://pythonhosted.org/Flask-Mako/)),但除非你有充分理由(不懂Jinja可不是一個充分的理由!),否則請保持那個默認的選項;這樣你會避免浪費很多時間來焦頭爛額。
> **注意**
> 幾乎所有提及Jinja的資源講的都是Jinja2。Jinja1確實曾存在過,但在這里我們不會講到它。當你看到Jinja時,我們討論的是這個Jinja: http://jinja.pocoo.org/
## Jinja快速入門
**Jinja**文檔在解釋這門語言的語法和特性這方面做得很棒。在這里我不會啰嗦一遍,但還是會再一次向你強調下面一點:
{% raw %}
> Jinja有兩種定界符。`{% ... %}`和`{{ ... }}`。前者用于執行像for循環或賦值等語句,后者向模板輸出一個表達式的結果。
{% endraw %}
> **參見**: http://jinja.pocoo.org/docs/templates/#synopsis
## 怎樣組織模板
所以要將模板放進我們的應用的哪里呢?如果你是從頭開始閱讀的本文,你可能注意到了Flask在對待你如何組織項目結構的事情上十分隨意。模板也不例外。你大概也已經注意到,總會有一個放置文件的推薦位置。記住兩點。對于模板,這個最佳位置是放在包文件夾下。
```
myapp/
__init__.py
models.py
views/
templates/
static/
run.py
requirements.txt
```
讓我們打開模板文件夾看看。
```
templates/
layout.html
index.html
about.html
profile/
layout.html
index.html
photos.html
admin/
layout.html
index.html
analytics.html
```
**模板**的結構平行于對應的路由的結構。對應于路由*myapp.com/admin/analytics*的模板是*templates/admin/analytics.html*。這里也有一些額外的模板不會被直接渲染。*layout.html*文件就是用于被其他模板繼承的。
## 繼承
就像蝙蝠俠一樣,一個組織良好的模板文件夾也離不開繼承帶來的好處。**基礎模板**通常定義了一個適用于所有的*子模板*的主體結構。在我們的例子里,*layout.html*是一個基礎模板,而其他的*html*文件都是子模板。
通常,你會有一個頂級的*layout.html*定義你的應用的主體布局,外加站點的每一個節點也有自己的一個*layout.html*。如果再看一眼上面的文件夾結構,你會看到一個頂級的*myapp/templates/layout.html*,以及*myapp/templates/profile/layout.html*和*myapp/templates/admin/layout.html*。后兩個文件繼承并修改第一個文件。
{% raw %}
繼承是通過`{% extends %}`和`{% block %}`標簽實現的。在雙親模板中,你可以定義要給子模板處理的block。
{% endraw %}
_myapp/templates/layout.html_
```
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block body %}
<h1>這個標題在雙親模板中定義</h1>
{% endblock %}
</body>
</html>
```
在子模板中,你可以拓展雙親模板并定義block里面的內容。
_myapp/templates/index.html_
```
{% extends "layout.html" %}
{% block title %}Hello world!{% endblock %}
{% block body %}
{{ super() }}
<h2>這個標題在子模板中定義</h2>
{% endblock %}
```
`super()`函數讓我們在子模板里加載雙親模板中這個block的內容。
> **參見**
> 若想了解更多關于繼承的內容,請移步到Jinja模板繼承方面的文檔。
> <http://jinja.pocoo.org/docs/templates/#template-inheritance>
## 創建宏
憑借將反復出現的代碼片段抽象成**宏**,我們可以實現DRY原則(Don't Repeat Yourself)。在撰寫用于應用的導航功能的HTML時,我們可能會需要給“活躍”鏈接(比如,到當前頁面的鏈接)一個不同的類。如果沒有宏,我們將不得不使用一大堆if/else語句來從每個鏈接中過濾出“活躍”鏈接。
宏提供了模塊化模板代碼的一種方式;它們就像是函數一樣。讓我們看一下如何使用宏來標記活躍鏈接。
myapp/templates/layout.html
```
{% from "macros.html" import nav_link with context %}
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<title>我的應用</title>
{% endblock %}
</head>
<body>
<ul class="nav-list">
{{ nav_link('home', 'Home') }}
{{ nav_link('about', 'About') }}
{{ nav_link('contact', 'Get in touch') }}
</ul>
{% block body %}
{% endblock %}
</body>
</html>
```
現在我們調用了一個尚未定義的宏 - `nav_link` - 并傳遞兩個參數給它:一個目標(比如目標視圖的函數名)和我們想要展示的文本。
> **注意**
> 你可能注意到了我們在import語句中加入了**with context**。Jinja的**上下文(context)**包括了通過`render_template()`函數傳遞的參數以及在我們的Python代碼的Jinja環境上下文。這些變量能夠被用于模板的渲染。
>
{% raw %}
> 一些變量是我們顯式傳遞過去的,比如`render_template("index.html", color="red")`,但還有些變量和函數是Flask自動加入到上下文的,比如`request`,`g`和`session`。使用了`{% from ... import ... with context %}`,我們告訴Jinja讓所有的變量也在宏里可用。
{% endraw %}
> **參見**
> * 所有的全局變量都是由Flask傳遞給Jinja上下文的: http://flask.pocoo.org/docs/templating/#standard-context
> * 通過上下文處理器(context processors),我們可以增加傳遞給Jinja上下文的變量和函數: http://flask.pocoo.org/docs/templating/#context-processors
是時候定義模板中用的`nav_link`宏了。
myapp/templates/macros.html
```
{% macro nav_link(endpoint, text) %}
{% if request.endpoint.endswith(endpoint) %}
<li class="active"><a href="{{ url_for(endpoint) }}">{{text}}</a></li>
{% else %}
<li><a href="{{ url_for(endpoint) }}">{{text}}</a></li>
{% endif %}
{% endmacro %}
```
現在我們已經在*myapp/templates/macros.html*中定義了一個宏。我們所做的,就是使用Flask的`request`對象 - 默認在Jinja上下文中可用 - 來檢查當前路由是否是傳遞給`nav_link`的那個路由參數。如果是,我們就在目標鏈接指向的頁面上,于是可以標記它為活躍的。
> **注意**
> `from x import y`語句中要求x是相對于y的相對路徑。如果我們的模板位于*myapp/templates/user/blog.html*,我們需要使用`from "../macros.html" import nav_link with context`。
## 自定義過濾器
Jinja過濾器是在渲染成模板之前,作用于`{{ ... }}`中的表達式的值的函數。
```
<h2>{{ article.title|title }}</h2>
```
在這個代碼中,`title`過濾器接受`article.title`并返回一個標題格式的文本,用于輸出到模板中。它的語法,以及功能,皆一如Unix中修改程序輸出的“管道”一樣。
> **參見**
> 除了`title`,還有許許多多別的內建的過濾器。在這里可以看到完整的列表: http://jinja.pocoo.org/docs/templates/#builtin-filters
我們可以自定義用于Jinja模板的過濾器。作為例子,我們將實現一個簡單的`caps`過濾器來使字符串中所有的字母大寫。
> **注意**
> Jinja已經有一個`upper`過濾器能實現這一點,還有一個`capitalize`過濾器能大寫第一個字符并小寫剩余字符。這些過濾器還能處理Unicode轉換,不過我們的這個例子將只專注于闡述相關概念。
我們將在*myapp/util/filters.py*中定義我們的過濾器。這個`util`包可以用來放置各種雜項。
myapp/util/filters.py
```
from .. import app
@app.template_filter()
def caps(text):
"""Convert a string to all caps."""
return text.uppercase()
```
在上面的代碼中,通過`@app.template_filter()`裝飾器,我們能將某個函數注冊成Jinja過濾器。默認的過濾器名字就是函數的名字,但是通過傳遞一個參數給裝飾器,你可以改變它:
```
@app.template_filter('make_caps')
def caps(text):
"""Convert a string to all caps."""
return text.uppercase()
```
現在我們可以在模板中調用`make_caps`而不是`caps`:`{{ "hello world!"|make_caps }}`。
為了讓我們的過濾器在模板中可用,我們僅需要在頂級*\_\_init\_\_.py*中import它。
myapp/\_\_init\_\_.py
```
# 確保app已經被初始化以免導致循環import
from .util import filters
```
## 總結
* 使用Jinja作為模板語言。
{% raw %}
* Jinja有兩種定界符:`{% ... %}`和`{{ ... }}`。前者用于執行類似循環或賦值的語句,后者向模板輸出表達式求值的結果。
{% endraw %}
* 模板應該放在*myapp/templates/* - 一個在應用文件夾里面的目錄。
* 我建議*template/*文件夾的結構應該與應用URL結構一一對應。
* 你應該在*myapp/templates*以及站點的每一部分放置一個*layout.html*作為布局模板。后者是前者的拓展。
* 可以用模板語言寫類似于函數的宏。
* 可以用Python代碼寫應用在模板中的過濾器函數。