《Flask Web开发实战:入门、进阶与原理解析》笔记

本文主要精炼的记录书中容易忘记的知识点,作为速查笔记使用。

第1章 初识Flask

从PyPI上下载并安装指定的包:

pip install <某个包的名称>

PyPI(Python Package Index,Python包索引)中的包名称不区分大小写。

使用国内的 PyPI 镜像源

使用 pip 安装 Python 包的速度比较慢的解决办法:使用国内的 PyPI 镜像源(mirror source)。

使用镜像源需要注意一个问题:包的版本可能不会及时更新,遇到这种情况可以通过临时换回官方源解决。

临时使用

pip install -i https://pypi.doubanio.com/simple/ flask

永久设置

如果希望永久设置镜像,需要将镜像地址写入 pip 的配置文件。

在不同的生效范围下,pip 的配置文件的文件位置也不同,以下以Windows系统为例给出的配置文件位置,其他系统见 pip 文档的配置部分

  • 用户全局(Per-user)

    %APPDATA%\pip\pip.ini
    

    兼容旧版本(legacy)的用户全局配置文件位置为:

    %HOMEPATH%\pip\pip.ini
    

    也可以通过设置环境变量PIP_CONFIG_FILE来自定义用户全局配置文件位置。

  • 虚拟环境全局(Inside a virtualenv)

    %VIRTUAL_ENV%\pip.ini
    
  • 全局配置文件
    全局配置文件由所有Python安装共享。

    • Windows XP:C:\Documents and Settings\All Users\Application Data\pip\pip.ini
    • Windows Vista 不支持全局设置
    • Windows 7 以及之后的版本:C:\ProgramData\pip\pip.ini(该文件虽然被隐藏,但是可写)

在 Windows 下,环境变量%HOMEPATH%具体值可以使用echo %HOMEPATH%命令查看。

选择好需要的配置文件生效范围后,通常你需要手动创建对应的目录和文件,然后写入下面的内容:

[global]
index-url = https://pypi.doubanio.com/simple
[install]
trusted-host = pypi.doubanio.com

其中的 https://pypi.doubanio.com/simple/ 可以修改为以下任意一条镜像,注意trusted-host也要相应更改为对应的域名。

常用的国内 PyPI 镜像列表

  • 豆瓣:https://pypi.doubanio.com/simple/
  • 网易:https://mirrors.163.com/pypi/simple/
  • 阿里云:https://mirrors.aliyun.com/pypi/simple/
  • 腾讯云:https://mirrors.cloud.tencent.com/pypi/simple
  • 清华大学:https://pypi.tuna.tsinghua.edu.cn/simple/

包管理工具

根据李辉大神的文章,本书中的包管理工具由 pipenv 更换为使用 virtualenv/venv 来管理虚拟环境,搭配 pip 来管理依赖。

包管理工具的准备

  • 使用 Python 3.3+,推荐使用标准库内置的 venv 模块替代 virtualenv,两者的使用方式基本相同,唯一不同的是创建虚拟环境的方式。

  • 如果你使用 Python 2,那就只能选择 virtualenv

    pip install virtualenv
    

创建虚拟环境

假设我们的项目名叫 snow,创建对应的文件夹然后切换到根目录:

mkdir snow
cd snow
  • 使用内置模块 venv,那么使用下面的命令创建虚拟环境

    python -m venv snow-venv
    

    其中 snow-venv 是虚拟环境的名字,也作为创建的虚拟环境文件夹名称,可以自由修改(通常会使用 venvenv 作为虚拟环境名)。

  • 如果使用 virtualenv,则使用下面的命令:

    virtualenv snow-venv
    

注意:上述命令会在当前目录创建名为 snow-venv 的虚拟环境文件夹。如果正在使用 Git,你需要把这个文件夹名称加入 .gitignore 文件以便让 Git 忽略。

激活虚拟环境

  • Windows(CMD.exe)使用下面的命令激活:

    snow-venv\scripts\activate
    
  • Linux 和 macOS(bash/zsh)使用下面的命令:

    source snow-venv/bin/activate
    

    或:

    . snow-venv/bin/activate
    

类似的,其他终端程序可以执行对应的激活脚本来激活虚拟环境。

激活虚拟环境以后,命令行提示符前会显示当前虚拟环境的名字:

(snow-venv) $

使用 deactivate 命令可以退出虚拟环境。

使用 pip 管理依赖

  • 安装依赖:

    (snow-venv) $ pip install flask
    
  • 更新依赖:

    (snow-venv) $ pip install --upgrade flask
    

    或是:

    (snow-venv) $ pip install -U flask
    
  • 卸载依赖:

(snow-venv) $ pip uninstall flask

除此之外,还有 pip show <package> 命令可以查看某个包的详细依赖信息,pip list 列出所有依赖。

下面的命令可以手动生成依赖列表:

(snow-venv) $ pip freeze > requirements.txt

如果你需要手动开发依赖和生产依赖,可以手动把开发相关的依赖放到单独的文件,比如 requirements-dev.txt

当你需要在新的机器创建程序运行环境时,(创建虚拟环境后)只需要使用下面的命令从依赖文件安装所有依赖:

(snow-venv) $ pip install -r requirements.txt

第一章切换进 helloflask 目录后,整个 1.1 小节你只需要执行下面的命令:

$ python -m venv env  # Linux、macOS 系统的 Python3 用户,使用 python3 -m venv env
$ env\Scripts\activate  # Linux、macOS 系统使用 source env/bin/activate
$ pip install -r requirements.txt  # 这个命令会安装对应项目的所有依赖

附注 :上面命令里 # 号及之后的文字是注释,不需要输入。如果你使用 Python 2,第一条命令需要改为 virtualenv env。这三行命令的作用依次为:创建虚拟环境、激活虚拟环境、从 requirements.txt 文件安装依赖列表。

第二部分每章开头的下面这两行命令:

$ pipenv install --dev
$ pipenv shell

都要替换为:

$ python -m venv env  # Linux、macOS 系统的 Python3 用户,使用 python3 -m venv env
$ env\Scripts\activate  # Linux、macOS 系统使用 source env/bin/activate
$ pip install -r requirements.txt  # 这个命令会安装对应项目的所有依赖

退出虚拟环境时,使用下面的命令:

$ deactivate

最小的Flask程序与route路由规则

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return '<h1>Hello Flask!</h1>'

传入Flask类构造方法的第一个参数是模块或包的名称,我们应该使用特殊变量__name__。这会帮助Flask在相应的文件夹里找到需要的资源,比如模板和静态文件。

Python会根据所处的模块来赋予__name__变量相应的值,对于我们的程序来说(app.py),这个值为app

存储程序名称的属性为app.name

app.route()装饰器把根地址/和index()函数绑定起来,当用户访问这个URL时就会触发index()函数。URL规则和视图函数名称没有任何必然关系。

route()装饰器的第一个参数是URL规则,用字符串表示,必须以斜杠(/)开始。这里的URL是相对URL(又称为内部URL),即不包含域名的URL。

一个视图函数可以绑定多个URL:

@app.route('/hi')
@app.route('/hello')
def say_hello():
    return '<h1>Hello, Flask!</h1>'

还可以在URL规则中添加变量部分,使用<变量名>的形式表示。Flask处理请求时会把变量传入视图函数,所以我们可以添加参数获取这个变量值。

@app.route('/greet/<name>')
def greet(name):
    return f'<h1>Hello, { name }!</h1>'

当URL规则中包含变量时,如果用户访问的URL中没有添加变量,比如/greet,那么Flask在匹配失败后会返回一个404错误响应

app.route()装饰器里使用defaults参数设置URL变量的默认值或是在函数中设置默认参数值来防止给出404相应。

@app.route('/greet', defaults={'name': 'Programmer'})
@app.route('/greet/<name>')def greet(name):
    return f'<h1>Hello, { name }!</h1>'

# 上下两种写法等价

@app.route('/greet')
@app.route('/greet/<name>')
def greet(name='Programmer'):
    return f'<h1>Hello, { name }!</h1>'

内置的开发服务器

在命令行中输入flask run命令用来启动内置的开发服务器,默认会监听http://127.0.0.1:5000/地址(按Ctrl+C退出)。

请确保执行命令前激活了虚拟环境。flask run命令是Flask依赖包Click提供的,后续还会提及如何自定义一个Flask命令。

输入flask run命令后,Flask会自动按照以下顺序规则寻找程序实例:

  • 从当前目录寻找app.pywsgi.py模块,并从中寻找名为appapplication的程序实例。

  • 从环境变量FLASK_APP对应的模块名/导入路径寻找名为appapplication的程序实例。

    在Windows中使用set命令来设置环境变量:set FLASK_APP=hello

  • 如果安装了python-dotenv,那么在使用flask run或其他命令时会使用它自动从.flaskenv文件和.env文件中加载环境变量。

    加载优先级是:手动设置的环境变量>.env中设置的环境变量>.flaskenv设置的环境变量

    安装命令:pip install python-dotenv

    • .flaskenv用来存储和Flask相关的公开环境变量,比如FLASK_APP

    • .env用来存储包含敏感信息的环境变量,比如后面我们会用来配置Email服务器的账户名与密码。

      除非是私有项目,否则绝对不能提交到Git仓库中,记得把.env添加到.gitignore文件中,这会告诉Git忽略这个文件。

    .flaskenv.env文件中,环境变量使用键值对的形式定义,每行一个,以#开头的为注释,如下所示:

    SOME_VAR=1
    #这是注释
    FOO="BAR"
    

run 命令的更多选项

run命令后添加--host选项将主机地址设为0.0.0.0,会让服务器监听所有外部请求:

flask run --host=0.0.0.0

内网穿透/端口转发工具:

Flask提供的Web服务器默认监听5000端口,你可以在启动时传入参数--port来改变为其他值(如8000):

flask run --port=8000

--host--port选项也可以通过环境变量FLASK_RUN_HOSTFLASK_RUN_PORT设置。

事实上,Flask内置的命令都可以使用这种模式定义默认选项值,即FLASK_<COMMAND>_<OPTION>,你可以使用flask --help命令查看所有可用的命令。

设置运行环境

Flask提供了一个FLASK_ENV环境变量用来设置环境,默认为production(生产)。在开发时,我们可以将其设为development(开发),这会开启所有支持开发的特性。

为了方便管理,我们将把环境变量FLASK_ENV的值写入.flaskenv文件中:FLASK_ENV=development

在开发环境下,调试模式(Debug Mode)将被开启,这时执行flask run启动程序会自动激活Werkzeug内置的调试器(debugger)和重载器(reloader),它们会为开发带来很大的帮助。

如果你想单独控制调试模式的开关,可以通过FLASK_DEBUG环境变量设置,设为1则开启,设为0则关闭,不过通常不推荐手动设置这个值

生产环境中部署程序时,绝不能开启调试模式。尽管PIN码可以避免用户任意执行代码,提高攻击者利用调试器的难度,但并不能确保调试器完全安全,会带来巨大的安全隐患。而且攻击者可能会通过调试信息获取你的数据库结构等容易带来安全问题的信息。另一方面,调试界面显示的错误信息也会让普通用户感到困惑。

默认会使用Werkzeug内置的stat重载器,它的缺点是耗电较严重,而且准确性一般。为了获得更优秀的体验,我们可以安装另一个用于监测文件变动的Python库Watchdog,安装后Werkzeug会自动使用它来监测文件变动:

pip install watchdog

如果项目中使用了单独的CSS或JavaScript文件时,那么浏览器可能会缓存这些文件,从而导致对文件做出的修改不能立刻生效。在浏览器中,我们可以按下Ctrl+F5Shift+F5执行硬重载(hard reload),即忽略缓存并重载(刷新)页面。

Flask Shell

在开发Flask程序时,我们并不会直接使用python命令启动Python Shell,而是使用flask shell命令。

和其他flask命令相同,执行这个命令前我们要确保程序实例可以被正常找到。

在Python Shell中可以执行exit()quit()退出,在Windows系统上可以使用Ctrl+Z并按Enter退出;在Linux和macOS则可以使用Ctrl+D退出。

使用flask shell命令打开的Python Shell自动包含程序上下文,并且已经导入了app实例。

项目配置

在Flask中,配置变量就是一些大写形式的Python变量,包括:

  • Flask提供的配置
  • 扩展提供的配置
  • 程序特定的配置。

和平时使用变量不同,这些配置变量都通过Flask对象的app.config属性作为统一的接口来设置和获取,它指向的Config类实际上是字典的子类,所以你可以像操作其他字典一样操作它,例如

app.config['ADMIN_NAME']='Peter'

注意:配置的名称必须是全大写形式,小写的变量将不会被读取

Flask内置的配置可以访问Flask文档的配置章节查看,扩展提供的配置也可以在对应的文档中查看。

使用update()方法则可以一次加载多个值:

app.config.update(
    TESTING=True,
    SECRET_KEY='_5#yF4Q8z\n\xec]/'
)

除此之外,你还可以把配置变量存储在单独的Python脚本、JSON格式的文件或是Python类中,config对象提供了相应的方法来导入配置,具体我们会在后面了解。

提示:某些扩展需要读取配置值来完成初始化操作,比如Flask-Mail,因此我们应该尽量将加载配置的操作提前,最好在程序实例app创建后就加载配置。

URL与端点

使用Flask提供的url_for()函数获取URL而不是硬编码的方式写出,这样能够在路由中定义的URL规则被修改时仍然使用正确的URL。

调用url_for()函数时,第一个参数为端点(endpoint)值。在Flask中,端点用来标记一个视图函数以及对应的URL规则。

端点的默认值视图函数的名称,至于为什么不直接使用视图函数名,而要引入端点这个概念,我们会在后面了解。

app.route()装饰器中使用endpoint参数可以自定义端点值,不过我们通常不需要这样做。

如果URL含有动态部分,那么我们需要在url_for()函数里传入相应的参数,以下面的视图函数为例:

@app.route('/hello/<name>')
def greet(name):
    return f'Hello { name }!'
# url_for('greet',name='Jack')得到的URL为 /hello/Jack

默认情况下,url_for()函数生成的URL是相对URL(即内部URL)。如果你想要生成供外部使用的绝对URL,可以在使用url_for()函数时,将_external参数设为True

自定义Flask命令

通过创建任意一个函数,并为其添加app.cli.command()装饰器,我们就可以注册一个flask命令,函数的名称即为命令名称,例如注册命令hello,你可以使用flask hello命令在命令行下来触发函数:

import click

@app.cli.command()
def hello():
    """该命令只是打印一行问候。"""
    click.echo('你好啊!')
$ flask hello
你好啊!

作为替代,你也可以在app.cli.command()装饰器中传入参数来设置命令名称,比如app.cli.command('say-hello')会把命令名称设置为say-hello,完整的命令即flask say-hello

借助click模块的echo()函数,我们可以在命令行界面输出字符。

命令函数的文档字符串会作为帮助信息显示(例如上述命令可以用flask hello --help来查看对应的帮助信息)。

在命令下执行flask --help可以查看Flask提供的命令以及我们自定义的命令的帮助文档:

$ flask --help
Usage: flask [OPTIONS] COMMAND [ARGS]...

  A general utility script for Flask applications.
  ...
Options:
  --version  Show the flask version
  --help     Show this message and exit.

Commands:
  hello   Just say hello.  # 我们注册的自定义命令
  routes  Show the routes for the app.  # 显示所有注册的路由
  run     Runs a development server.
  shell   Runs a shell in the app context.

关于自定义命令更多的设置和功能请参考Click的官方文档

模板与静态文件

一个完整的网站当然不能只返回用户一句“Hello,World!”,我们需要模板和静态文件来生成更加丰富的网页。

  • 模板(template)即包含程序页面的HTML文件
  • 静态文件(static file)则是需要在HTML文件中加载的CSS和JavaScript文件,以及图片、字体文件等资源文件。

默认情况下,模板文件存放在项目根目录中的templates文件夹中,静态文件存放在static文件夹下,这两个文件夹需要和包含程序实例的模块处于同一个目录下,对应的项目结构示例如下所示:

<project-name>/
    - templates/
    - static/
    - app.py

在开发Flask程序时,使用CSS框架和JavaScript库是很常见的需求,而且有很多扩展都提供了对CSS框架和JavaScript库的集成功能。

使用这些扩展时都需要加载对应的CSS和JavaScript文件,通常这些扩展都会提供一些可以在HTML模板中使用的加载方法/函数,使用这些方法即可渲染出对应的link标签和script标签。

这些方法一般会直接从CDN加载资源,有些提供了手动传入资源URL的功能,有些甚至提供了内置的本地资源。

建议在开发环境下使用本地资源,这样可以提高加载速度。最好自己下载到static目录下,统一管理,出于方便的考虑也可以使用扩展内置的本地资源。

在过渡到生产环境时,自己手动管理所有本地资源或自己设置CDN,避免使用扩展内置的资源。这个建议主要基于下面这些考虑因素:

  • 鉴于国内的网络状况,扩展默认使用的国外CDN可能会无法访问访问过慢
  • 不同扩展内置的加载方法可能会加载重复的依赖资源,比如jQuery。
  • 在生产环境下,将静态文件集中在一起更方便管理
  • 扩展内置的资源可能会出现版本过旧的情况。

第2章 Flask与HTTP

HTTP的详细定义在RFC 7231~7235中,完整的RFC列表可以在这里看到:https://tools.ietf.org/rfc/

每一个Web应用都包含这种处理模式,即“请求-响应循环(Request-Response Cycle)”:客户端发出请求,服务器端处理请求并返回响应

图2-1 请求响应循环示意图

图2-2 Flask Web程序工作流程

这部分笔记优先级不高,以后有时间整理,待办任务:

  • 2.1 请求响应循环
  • 2.2 HTTP请求
  • 2.3 HTTP响应
  • 2.4 Flask上下文
  • 2.5 HTTP进阶实践

第3章 模板

一个完整的HTML页面往往需要几十行甚至上百行代码,如果都写到视图函数里,这样的代码既不简洁也难于维护。正确的做法是把HTML代码存储在单独的文件中,以便让程序的业务逻辑和表现逻辑分离,即控制器和用户界面的分离。

当HTML代码保存到单独的文件中时,我们没法再使用字符串格式化或拼接字符串的方式来在HTML代码中插入变量,这时我们需要使用模板引擎(template engine)。借助模板引擎,我们可以在HTML文件中使用特殊的语法来标记出变量,这类包含固定内容和动态部分的可重用文件称为模板(template)。模板引擎的作用就是读取并执行模板中的特殊语法标记,并根据传入的数据将变量替换为实际值,输出最终的HTML页面,这个过程被称为渲染(rendering)。

Flask默认使用的模板引擎是 Jinja2,它是一个功能齐全的Python模板引擎,除了设置变量,还允许我们在模板中添加if判断,执行for迭代,调用函数等,以各种方式控制模板的输出。对于 Jinja2 来说,模板可以是任何格式的纯文本文件,比如HTML、XML、CSV、LaTeX等。

模板基本用法

示例:

<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
    <meta charset="utf-8">
    <title>{{ user.username }}'s Watchlist</title>
</head>
<body>
<a href="{{ url_for('index') }}">&larr; Return</a>
<h2>{{ user.username }}</h2>
{% if user.bio %}
    <i>{{ user.bio }}</i>
{% else %}
    <i>This user has not provided a bio.</i>
{% endif %}
{# Below is the movie list (this is comment) #}
<h5>{{ user.username }}'s Watchlist ({{ movies|length }}):</h5>
<ul>
    {% for movie in movies %}
    <li>{{ movie.name }} - {{ movie.year }}</li>
    {% endfor %}
</ul>
</body>
</html>

国语中文简体语言的代码为zh-cmn-Hans,推荐大家使用。

在模板中使用的&larr;是HTML实体。HTML实体除了用来转义HTML保留符号外,通常会被用来显示不容易通过键盘输入的字符。这里的&larr;会显示为左箭头←,另外,我们还经常使用&copy;来显示版权标志©,你可以访问https://dev.w3.org/html5/html-author/charref查看所有可用的HTML实体。

在模板中添加Python语句和表达式时,我们需要使用特定的定界符把它们标示出来。

  1. 语句

    比如if判断、for循环等:

    {% ... %}
    
  2. 表达式

    比如字符串、变量、函数调用等。

    {{ ... }}
    
  3. 注释

    {# ... #}
    

另外,在模板中,Jinja2支持使用.取变量的属性,比如user字典中的username键值通过.获取,即user.user-name,在效果上等同于user['username']

简单地说,我们可以在模板中使用Python语句和表达式来操作数据的输出。但需要注意的是,Jinja2并不支持所有Python语法。而且出于效率和代码组织等方面的考虑,我们应该适度使用模板,仅把和输出控制有关的逻辑操作放到模板中

Jinja2允许你在模板中使用大部分Python对象,比如字符串、列表、字典、元组、整型、浮点型、布尔值。它支持基本的运算符号(+-*/等)、比较符号(比如==!=等)、逻辑符号(andornot和括号)以及inisNone和布尔值(TrueFalse)。

Jinja2提供了多种控制结构来控制模板的输出,其中forif是最常用的两种。

在Jinja2里,语句使用{% ... %}标识,尤其需要注意的是,在语句结束的地方,我们必须添加结束标签,不能省略。

{% if user.bio %}
	<i>{{ user.bio }}</i>
{% else %}
	<i>This user has not provided a bio.</i>
{% endif %}

和在Python里一样,for语句用来迭代一个序列:

<ul>
    {% for movie in movies %}
    	<li>{{ movie.name }} - {{ movie.year }}</li>
    {% endfor %}
</ul>

在for循环内,Jinja2提供了多个特殊变量,常用的循环变量如下表所示。

变量名说明
loop.index当前迭代数(从1开始计数)
loop.index0当前迭代数(从0开始计数)
loop.revindex当前反向迭代数(最后一个元素计为1)
loop.revindex0当前反向迭代数(最后一个元素计为0)
loop.first如果是第一个元素,则为True
loop.last如果是最后一个元素,则为True
loop.previtem上一个迭代的条目
loop.nextitem下一个迭代的条目
loop.length序列包含的元素数量

在视图函数中渲染模板时,我们使用Flask提供的渲染函数render_template()

from flask import Flask, render_template

# ...

@app.route('/watchlist')
def watchlist():
    return render_template('watchlist.html', user=user, movies=movies)

render_template()函数中,我们首先传入模板的文件名作为参数。Flask会在程序根目录下的templates文件夹里寻找模板文件,所以这里传入的文件路径是相对于templates根目录的

除了模板文件路径,我们还以关键字参数的形式传入了模板中使用的变量值,以user为例:左边的user表示传入模板的变量名称,右边的user则是要传入的对象。

此外,Flask还提供了一个render_template_string()函数用来渲染模板字符串。

传入Jinja2中的变量值可以是字符串、列表和字典,也可以是函数、类和类实例,这完全取决于你在视图函数传入的值。

模板辅助工具

上下文变量

模板上下文包含了很多变量,其中包括我们调用render_template()函数时手动传入的变量以及Flask默认传入的变量。除了渲染时传入变量,你也可以在模板中定义变量,使用set标签:

{% set navigation = [('/', 'Home'), ('/about', 'About')] %}

你也可以将一部分模板数据定义为变量,使用setendset标签声明开始和结束:

{% set navigation %}
    <li><a href="/">Home</a>  
    <li><a href="/about">About</a>
{% endset %}

Flask在模板上下文中提供了一些内置变量,可以在模板中直接使用:

变量说明
config当前的配置对象
request当前的请求对象,在已激活的请求环境下可用
session当前的会话对象,在已激活的请求环境下可用
g与请求绑定的全局变量,在已激活的请求环境下可用

提示:Flask除了把gsessionconfigrequest对象注册为上下文变量,也将它们设为全局变量,因此可以全局使用。

如果多个模板都需要使用同一变量,那么比起在多个视图函数中重复传入,更好的方法是能够设置一个模板全局变量。

Flask提供了一个app.context_processor装饰器,可以用来注册模板上下文处理函数,它可以帮我们完成统一传入变量的工作。模板上下文处理函数需要返回一个包含变量键值对的字典

@app.context_processor
def inject_foo():
    foo = 'I am foo.'
    return dict(foo=foo)  # 等同于 return {'foo': foo}

当我们调用render_template()函数渲染任意一个模板时,所有使用app.context_processor装饰器注册的模板上下文处理函数(包括Flask内置的上下文处理函数)都会被执行,这些函数的返回值会被添加到模板中,因此我们可以在模板中直接使用foo变量。

除了使用app.context_processor装饰器,也可以直接将其作为方法调用,传入模板上下文处理函数:

def inject_foo():
 foo = 'I am foo.' 
 return dict(foo=foo)

app.context_processor(inject_foo)

使用lambda可以简化为:app.context_processor(lambda: dict(foo='I am foo.'))

全局对象

全局对象是指在所有的模板中都可以直接使用的对象,包括在模板中导入的模板,后面我们会详细介绍导入的概念。

Jinja2在模板中默认提供了一些全局函数,常用的三个函数如下表所示:

函数说明
range([start, ]stop[, step])和 Python中的range用法相同
lipsum(n=5, html=True, min=20, max=100)生成随机文本(lorem ipsum),可以在测试时用来填充页面,默认生成5段HTML文本,每段包含20~100个单词
dict(**items)和 Python中的dict()用法相同

除了Jinja2内置的全局函数,Flask也在模板中内置了两个全局函数:

函数说明
url_for()用于生成URL的函数,用法和在Python脚本中相同。
get_flashed_messages()用于获取flash消息的函数

与注册模板上下文处理函数类似,我们可以使用app.template_global装饰器直接将函数注册为模板全局函数:

@app.template_global()
def bar():
    return 'I am bar.'

默认使用函数的原名称传入模板,在app.template_global()装饰器中使用name参数可以指定一个自定义名称。

app.template_global()仅能用于注册全局函数,后面我们会介绍如何注册全局变量。

你可以直接使用app.add_template_global()方法注册自定义全局函数,传入函数对象和可选的自定义名称(name),比如app.add_template_global(your_global_function)

过滤器

在Jinja2中,过滤器(filter)是一些可以用来修改和过滤变量值的特殊函数。过滤器的使用方法之一是将变量和过滤器用一个竖线|(管道符号)隔开,过滤器函数的第一个参数表示被过滤的变量值(value)或字符串(s),即竖线符号左侧的值,其他的参数可以通过添加括号传入:

{{ name|title }}       {# 相当于在Python里调用 name.title() #}
{{ movies|length }}    {# 相当于在Python里调用 len(movies) #}

另一种用法是将过滤器作用于一部分模板数据,使用filter标签和endfilter标签声明开始和结束。比如,下面使用upper过滤器将一段文字转换为大写:

{% filter upper %}
	This text becomes uppercase.
{% endfilter %}

Jinja2提供了许多内置过滤器,常用的过滤器如下表所示。

过滤器说明
default(value, default_value='', boolean=False)设置默认值,默认值作为参数传入,别名为d
escape(s)转义HTML文本,别名为e
first(seq)返回序列的第一个元素
last(seq)返回序列的最后一个元素
length(object)返回变量的长度
random(seg)返回序列中的随机元素
safe(value)将变量值标记为安全,避免转义
trim(value)清除变量值前后的空格
max(value, case_sensitive=False, attribute=None)返回序列中的最大值
min(value, case_sensitive=False, attribute=None)返回序列中的最小值
unique(value, case_sensitive=False, attribute=None)返回序列中的不重复的值
striptags(value)清除变量值内的HTML标签
urlize(value, trim_url_limit=None, nofollow=False, target=None, rel=None)将URL文本转换为可单击的HTML链接
wordcount(s)计算单词数量
tojson(value, indent=None)将变量值转换为JSON格式
truncate(s, length=255,killwords=False, end='...', leeway=None)截断字符串,常用于显示文章摘要,length参数设置截断的长度,killwords参数设置是否截断单词,end参数设置结尾的符号

附注 这里只列出了一部分常用的过滤器,完整的列表请访问http://jinja.pocoo.org/docs/2.10/templates/#builtin-filters查看。

过滤器可以叠加使用,下面的示例为name变量设置默认值,并将其标题化:

<h1>Hello, {{ name|default('陌生人')|title }}!</h1>

如果内置的过滤器不能满足你的需要,还可以添加自定义过滤器。使用app.template_filter()装饰器可以注册自定义过滤器:

from flask import Markup
@app.template_filter()
def musical(s):   
    return s + Markup(' &#9835;')

和注册全局函数类似,你可以在app.template_filter()中使用name关键字设置过滤器的名称,默认会使用函数名称。

过滤器函数需要接收被处理的值作为输入,返回处理后的值。过滤器函数接收s作为被过滤的变量值,返回处理后的值。示例中创建的musical过滤器会在被过滤的变量字符后面添加一个音符(singlebar note)图标,因为音符通过HTML实体&#9835;表示,我们使用Markup类将它标记为安全字符。在使用时和其他过滤器用法相同:

{{ name|musical }}

你可以直接使用app.add_template_filter()方法注册自定义过滤器,传入函数对象和可选的自定义名称(name),比如app.add_template_filter(your_filter_function)

XSS攻击与转义

为了防范XSS攻击,根据Flask的设置,Jinja2会自动对模板中的变量进行转义,所以我们不用手动使用escape过滤器或调用escape()函数对变量进行转义。

默认的自动开启转义仅针对.html.htm.xml以及.xhtml后缀的文件。

此外,用于渲染模板字符串的render_template_string()函数也会对所有传入的字符串进行转义。

在确保变量值安全的情况下,这通常意味着你已经对用户输入的内容进行了“消毒”处理。这时如果你想避免转义,将变量作为HTML解析,可以对变量使用safe过滤器:

另一种将文本标记为安全的方法是在渲染前将变量转换为Markup对象:

from flask import Markup

@app.route('/hello')
def hello():  
    text = Markup('<h1>Hello, Flask!</h1>')   
    return render_template('index.html', text=text)

这时在模板中可以直接使用{{ text }}

绝对不要直接对用户输入的内容使用safe过滤器,否则容易被植入恶意代码,导致XSS攻击。

测试器

在Jinja2中,测试器(Test)是一些用来测试变量或表达式,返回布尔值(TrueFalse)的特殊函数。比如,number测试器用来判断一个变量或表达式是否是数字,我们使用is连接变量和测试器:

测试器说明
callable(object)判断对象是否可被调用
defined(value)判断变量是否已定义
undefined(value)判断变量是否未定义
none(value)判断变量是否为None
number(value)判断变量是否是数字
string(value)判断变量是否是字符串
sequence(value)判断变量是否是序列,比如字符串、列表、元组
iterable(value)判断变量是否可迭代
mapping(value)判断变量是否是匹配对象,比如字典
sameas(value, other)判断变量与other是否指向相同的内存地址

这里只列出了一部分常用的测试器,完整的内置测试器列表请访问http://jinja.pocoo.org/docs/2.10/tem-plates/#list-of-builtin-tests查看。

在使用测试器时,is的左侧是测试器函数的第一个参数(value),其他参数可以添加括号传入,也可以在右侧使用空格连接,以sameas为例:

{% if foo is sameas(bar) %}
...

等同于:

{% if foo is sameas bar %}
...

和过滤器类似,我们可以使用Flask提供的app.template_test()装饰器来注册一个自定义测试器。在示例程序中,我们创建了一个没有意义的baz测试器,仅用来验证被测值是否为baz

@app.template_test()
def baz(n):   
    if n == 'baz':   
        return True   
    return False

测试器的名称默认为函数名称,你可以在app.template_test()中使用name关键字指定自定义名称。测试器函数需要接收被测试的值作为输入,返回布尔值。

你可以直接使用app.add_template_test()方法注册自定义测试器,传入函数对象和可选的自定义名称(name),比如app.add_template_test(your_test_function)

模板环境对象

在Jinja2中,渲染行为由jinja2.Environment类控制,所有的配置选项、上下文变量、全局函数、过滤器和测试器都存储在Environment实例上。

当与Flask结合后,我们并不单独创建Environment对象,而是使用Flask创建的Environment对象,它存储在app.jinja_env属性上。在程序中,我们可以使用app.jinja_env更改Jinja2设置

比如,你可以自定义所有的定界符。下面使用variable_start_stringvariable_end_string分别自定义变量定界符的开始和结束符号:

app = Flask(__name__)
app.jinja_env.variable_start_string = '[['
app.jinja_env.variable_end_string = ']]'

注意:在实际开发中,如果修改Jinja2的定界符,那么需要注意与扩展提供模板的兼容问题,一般不建议修改。

模板环境中的全局函数、过滤器和测试器分别存储在Environment对象的globalsfilterstests属性中,这三个属性都是字典对象。

除了使用Flask提供的装饰器和方法注册自定义函数,我们也可以直接操作这三个字典来添加相应的函数或变量,这通过向对应的字典属性中添加一个键值对实现,要在模板里使用的变量名称作为键,对应的函数对象或变量作为值。下面是几个简单的示例。

  • 添加自定义全局对象

    app.template_global()装饰器不同,直接操作globals字典允许我们传入任意Python对象,而不仅仅是函数,类似于上下文处理函数的作用。

    下面的代码使用app.jinja_env.globals分别向模板中添加全局函数bar和全局变量foo

    def bar():
       return 'I am bar.'
    foo = 'I am foo.'
    
    app.jinja_env.globals['bar'] = bar
    app.jinja_env.globals['foo'] = foo
    
  • 添加自定义过滤器

    下面的代码使用app.jinja_env.filters向模板中添加自定义过滤器smiling

    def smiling(s): 
        return s + ' :)'
    
    app.jinja_env.filters['smiling'] = smiling
    
  • 添加自定义测试器

    下面的代码使用app.jinja_env.tests向模板中添加自定义测试器baz

    def baz(n):   
        if n == 'baz':  
            return True   
        return False
    
    app.jinja_env.tests['baz'] = baz
    

访问http://jinja.pocoo.org/docs/latest/api/#jinja2.Environment查看Enviroment类的所有属性及用法说明。

模板结构组织

局部模板

比如,多个页面中都要在页面顶部显示一个提示条,这个横幅可以定义在局部模板_ban-ner.html中。

我们使用include标签来插入一个局部模板。比如,在其他模板中,我们可以在任意位置使用下面的代码插入_banner.html的内容:

{% include'_banner.html' %}

为了和普通模板区分开,局部模板的命名通常以一个下划线开始

(macro)是Jinja2提供的一个非常有用的特性,它类似Python中的函数。在创建宏时,我们使用macroendmacro标签声明宏的开始和结束。在开始标签中定义宏的名称和接收的参数,下面是一个简单的示例:

{% macro qux(amount=1) %}   
    {% if amount == 1 %}    
        I am qux.  
    {% elif amount > 1 %}  
        We are quxs.   
    {% endif %}
{% endmacro %}

使用时,需要像从Python模块中导入函数一样使用import语句导入它,然后作为函数调用,传入必要的参数,如下所示:

{% from 'macros.html' import qux %}
...
{{ qux(amount=5) }}

另外,在使用宏时我们需要注意上下文问题。在Jinja2中,出于性能的考虑,并且为了让这一切保持显式,默认情况下包含(include)一个局部模板会传递当前上下文到局部模板中,但导入(import)却不会。具体来说,当我们使用render_template()函数渲染一个foo.html模板时,这个foo.html的模板上下文中包含下列对象:

  1. Flask使用内置的模板上下文处理函数提供的sessiongrequestconfig
  2. 扩展使用内置的模板上下文处理函数提供的变量。
  3. 自定义模板上下文处理器传入的变量。
  4. 使用render_template()函数传入的变量。
  5. Jinja2和Flask内置及自定义全局对象。
  6. Jinja2内置及自定义过滤器。
  7. Jinja2内置及自定义测试器。

使用include标签插入的局部模板(比如_banner.html)同样可以使用上述上下文中的变量和函数。而导入另一个并非被直接渲染的模板(比如macros.html)时,这个模板仅包含下列这些对象:

  1. Jinja2和Flask内置的全局函数和自定义全局函数。
  2. Jinja2内置及自定义过滤器。
  3. Jinja2内置及自定义测试器。

因此,如果我们想在导入的宏中使用前一个列表中的2、3、4项,就需要在导入时显式地使用with context声明传入当前模板的上下文:

{% from "macros.html" import foo with context %}

虽然Flask使用内置的模板上下文处理函数传入sessiongrequestconfig,但它同时也使用app.jinja_env.globals字典将这几个变量设置为全局变量,所以我们仍然可以在不显式声明传入上下文的情况下,直接在导入的宏中使用它们。

关于宏的编写,更多的细节请访问http://jinja.pocoo.org/docs/latest/templates/#macros查看。

模板继承

Jinja2的模板继承允许你定义一个基模板,把网页上的导航栏、页脚等通用内容放在基模板中,而每一个继承基模板的子模板在被渲染时都会自动包含这些部分。使用这种方式可以避免在多个模板中编写重复的代码。

编写基模板

基模板存储了程序页面的固定部分,通常被命名为base.htmllayout.html

<!DOCTYPE html>
<html>
<head>
	{% block head %}
        <meta charset="utf-8">
        <title>
            {% block title %}Template - HelloFlask{% endblock %}
        </title>
            {% block styles %}{% endblock %}
	{% endblock %}
</head>
<body>
<nav>
	<ul><li><a href="{{ url_for('index') }}">Home</a></li></ul>
</nav>
<main>
	{% block content %}{% endblock %}
</main>
<footer>
	{% block footer %}
    	...
    {% endblock %}
</footer>
{% block scripts %}{% endblock %}
</body>
</html>

当子模板继承基模板后,子模板会自动包含基模板的内容和结构。为了能够让子模板方便地覆盖或插入内容到基模板中,我们需要在基模板中定义块(block),在子模板中可以通过定义同名的块来执行继承操作

块的开始和结束分别使用blockendblock标签声明,而且块之间可以嵌套。

在这个基模板中,我们创建了六个块:head、title、styles、content、footer和scripts,分别用来划分不同的代码。其中,head块表示<head>标签的内容,title表示<title>标签的内容,content块表示页面主体内容,footer表示页脚部分,styles块和scripts块,则分别用来包含CSS文件和JavaScript文件引用链接或页内的CSS和JavaScript代码。

这里的块名称可以随意指定,而且并不是必须的。你可以按照需要设置块,如果你只需要让子模板添加主体内容,那么仅定义一个content块就足够了。

为了避免块的混乱,块的结束标签可以指明块名,同时要确保前后名称一致。比如:

{% block body %}
	...
{% endblock body %}
编写子模板
{% extends 'base.html' %}
{% from 'macros.html' import qux %}

{% block content %}{% set name='baz' %}
<h1>Template</h1>
<ul>
    <li><a href="{{ url_for('watchlist') }}">Watchlist</a></li> 
    <li>Filter: {{ foo|musical }}</li> 
    <li>Global: {{ bar() }}</li> 
    <li>Test: {% if name is baz %}I am baz.{% endif %}</li> 
    <li>Macro: {{ qux(amount=5) }}</li>
</ul>
{% endblock %}

我们使用extends标签声明扩展基模板,它告诉模板引擎当前模板派生自base.htmlextends必须是子模板的第一个标签

在子模板中,我们可以对父模板中的块执行两种操作:

  • 覆盖内容

    当在子模板里创建同名的块时,会使用子块的内容覆盖父块的内容。比如我们在子模板index.html中定义了title块,内容为Home,这会把块中的内容填充到基模板里的title块的位置,最终渲染为<title>Home</title>,content块的效果同理。

  • 追加内容

    如果想要向基模板中的块追加内容,需要使用Jinja2提供的super()函数进行声明,这会向父块添加内容。比如,下面的示例向基模板中的styles块追加了一行<style>样式定义:

    {% block styles %}
    {{ super() }}
    <style>  
        .foo {    
            color: red;  
        }
    </style>
    {% endblock %}
    

    当子模板被渲染时,它会继承基模板的所有内容,然后根据我们定义的块进行覆盖或追加操作。

模板进阶实践

这部分笔记优先级不高,以后有时间整理,待办任务:

  • 3.4.1 空白控制

    事实上,我们没有必要严格控制HTML输出,因为多余的空白并不影响浏览器的解析。

    在部署时,我们甚至可以使用工具来去除HTML响应中所有的空白、空行和换行,这样可以减小文件体积,提高数据传输速度。所以,编写模板时应以可读性为先。

加载静态文件

在Flask程序中,默认我们需要将静态文件存储在与主脚本(包含程序实例的脚本)同级目录的static文件夹中。

Flask内置了用于获取静态文件的视图函数,端点值为static,它的默认URL规则为/static/<path:filename>,URL变量filename是相对于static文件夹的文件路径。

如果你想使用其他文件夹来存储静态文件,可以在实例化Flask类时使用static_folder参数指定,静态文件的URL路径中的static也会自动跟随文件夹名称变化。在实例化Flask类时使用static_url_path参数则可以自定义静态文件的URL路径。

使用范例:

  • 添加样式表

    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename= 'styles.css' ) }}">
    
  • 添加Favicon

    <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
    
  • 使用宏加载静态资源

    {% macro static_file(type, filename_or_url, local=True) %}  
        {% if local %}     
        	{% set filename_or_url = url_for('static', filename=filename_or_url) %}  
        {% endif %}
        {% if type == 'css' %}
        	<link rel="stylesheet" href="{{ filename_or_url }}" type="text/css">
        {% elif type == 'js' %}
        	<script type="text/javascript" src="{{ filename_or_url }}"></script>
        {% elif type == 'icon' %}
        	<link rel="icon" href="{{ filename_or_url }}">
    	{% endif %}
    {% endmacro %}
    
    {# 使用方法 #}
    {{ static_file('css', 'css/bootstrap.min.css') }}
    

消息闪现

Flask提供了一个非常有用的flash()函数,它可以用来“闪现”需要显示给用户的消息,比如当用户登录成功后显示“欢迎回来!”。

在视图函数调用flash()函数,传入消息内容即可“闪现”一条消息。

from flask import Flask, render_template, flash

app = Flask(__name__)
app.secret_key = 'secret string'

@app.route('/flash')
def just_flash():    
    flash('I am flash, who is looking for me?')    
    return redirect(url_for('index'))

通过flash()函数发送的消息会存储在session对象中,所以我们需要为程序设置密钥。可以通过app.secret_key属性或配置变量SECRET_KEY设置,具体可参考2.3.4节的相关内容。

当然,它并不是我们想象的,能够立刻在用户的浏览器弹出一条消息。实际上,使用功能flash()函数发送的消息会存储在session中,我们需要将用户的页面刷新(重定向到可获取flash消息的模板),并在模板中使用全局函数get_flashed_messages()获取消息并将其显示出来。

<main>    
    {% for message in get_flashed_messages() %}        
    	<div class="alert">{{ message }}</div>    
    {% endfor %}    
    {% block content %}{% endblock %}
</main>

因为同一个页面可能包含多条要显示的消息,所以这里使用for循环迭代get_flashed_messages()返回的消息列表。

自定义错误页面

错误处理函数和视图函数很相似,返回值将会作为响应的主体,因此我们首先要创建错误页面的模板文件。

为了和普通模板区分开来,我们在模板文件夹templates里为错误页面创建了一个errors子文件夹,并在其中为最常见的404500错误创建了模板文件。例如,表示404页面的404.html模板内容如下所示。

{% extends 'base.html' %}

{% block title %}404 - Page Not Found{% endblock %}

{% block content %}
	<h1>Page Not Found</h1>
	<p>You are lost...</p>
{% endblock %}

错误处理函数需要附加app.errorhandler()装饰器,并传入错误状态码作为装饰器参数。错误处理函数本身则需要接收异常类作为参数,并在返回值中注明对应的HTTP状态码。当发生错误时,对应的错误处理函数会被调用,它的返回值会作为错误响应的主体。

from flask import Flask, render_template

# ...

@app.errorhandler(404)
def page_not_found(e):
    return render_template('errors/404.html'), 404

错误处理函数接收异常对象作为参数,内置的异常对象提供了下列常用属性,如下表所示。

属性说明
code状态码
name原因短语
description错误描述,另外使用get_description()方法还可以获取HTML格式的错误描述代码

如果你不想手动编写错误页面的内容,可以将这些信息传入错误页面模板,在模板中用它们来构建错误页面。不过需要注意的是,传入500错误处理器的是真正的异常对象,通常不会提供这几个属性,你需要手动编写这些值。

Flask通过抛出Werkzeug中定义的HTTP异常类来表示HTTP错误,错误处理函数接收的参数就是对应的异常类。

基于这个原理,你也可以使用app.errorhandler()装饰器为其他异常注册处理函数,并返回自定义响应,只需要在app.errorhandler()装饰器中传入对应的异常类即可。

比如,使用app.errorhandler(NameError)可以注册处理NameError异常的函数。

JavaScript和CSS中的Jinja2

只有使用render_template()传入的模板文件才会被渲染,如果你把Jinja2代码写在单独的JavaScript或是CSS文件中,尽管你在HTML中引入了它们,但它们包含的Jinja2代码永远也不会被执行。

对于这类情况,下面有一些Tips:

  1. 行内/嵌入式JavaScript/CSS

    如果要在JavaScript和CSS文件中使用Jinja2代码,那么就在HTML中使用<style><script>标签定义这部分CSS和JavaScript代码。

    在这部分CSS和JavaScript代码中加入Jinja2时,不用考虑编写时的语法错误,比如引号错误,因为Jinja2会在渲染后被替换掉,所以只需要确保渲染后的代码正确即可。

    不过我并不推荐使用这种方式,尤其是行内JavaScript/CSS会让维护变得困难。避免把大量JavaScript代码留在HTML中的办法就是尽量将要使用的Jinja2变量值在HTML模板中定义为JavaScript变量。

  2. 定义为JavaScript/CSS变量

    • JavaScript

      对于想要在JavaScript中获取的数据,如果是元素特定的数据,比如某个文章条目对应的id值,可以通过HTML元素的data-*属性存储。你可以自定义横线后的名称,作为元素上的自定义数据变量,如data-iddata-username等,比如:

      <span data-id="{{ user.id }}" data-username="{{ user.username }}">{{ user.username }}</span>
      

      在JavaScript中,我们可以使用DOM元素的dataset属性获取data-*属性值,比如element.dataset.username,或是使用getAttribute()方法,比如element.getAttribute('data-user-name');使用jQuery时,可以直接对jQuery对象调用data方法获取,比如$element.data('username')

      在HTML中,data-*被称为自定义数据属性(custom data attribute),我们可以用它来存储自定义的数据供JavaScript获取。在后面的其他程序中,我们也会频繁使用这种方式来传递数据。

      对于需要全局使用的数据,则可以在页面中使用嵌入式JavaScript定义变量,如果没法定义为JavaScript变量,那就考虑定义为函数,比如:

      <script type="text/javascript">
          var foo = '{{ foo_variable }}';
      </script>
      

      当你在JavaScript中插入了太多Jinja2语法时,或许这时你该考虑将程序转变为Web API,然后专心使用JavaScript来编写客户端,在本书的第二部分我们会介绍如何编写WebAPI。

    • CSS

      有些时候你会需要将Jinja2变量值传入CSS文件,比如我们希望将用户设置的主题颜色设置到对应的CSS规则中,或是需要将static目录下某个图片的URL传入CSS来设置为背景图片,除了将这部分CSS定义直接写到HTML中外,我们可以将这些值定义为CSS变量,如下所示:

      <style>
      :root {    
          --theme-color: {{ theme_color }};    
          --background-url: {{ url_for('static', filename='background.jpg') }}
      }
      </style>
      

      在CSS文件中,使用var()函数并传入变量名即可获取对应的变量值:

      #foo {    
          color: var(--theme-color);
      }
      #bar {    
          background: var(--background-url);
      }
      
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值