Flask入门系列(一)–Hello Worldjavascript
项目开发中,常常要写一些小系统来辅助,好比监控系统,配置系统等等。用传统的Java写,太笨重了,连PHP都嫌麻烦。一直在寻找一个轻量级的后台框架,学习成本低,维护简单。发现Flask后,我立马被它的轻巧所吸引,它充分发挥了Python语言的优雅和轻便,连Django这样强大的框架在它面前都以为繁琐。能够说简单就是美。这里咱们不讨论到底哪一个框架语言更好,只是从简单这个角度出发,Flask绝对是佼佼者。这一系列文章就会给你们展现Flask的轻巧之美。css
系列文章html
Hello World前端
程序员的经典学习方法,从Hello World开始。不要忘了,先安装python, pip,而后运行”pip install Flask”,环境就装好了。固然本人仍是强烈建议使用virtualenv来安装环境。细节就很少说了,让咱们写个Hello World吧:java
1node 2python 3mysql 4git 5程序员 6 7 8 9 |
from flask import Flask app = Flask(__name__)
@app.route('/') def index(): return '<h1>Hello World</h1>'
if __name__ == '__main__': app.run() |
一个Web应用的代码就写完了,对,就是这么简单!保存为”hello.py”,打开控制台,到该文件目录下,运行
$ python hello.py
看到”* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)”字样后,就说明服务器启动完成。打开你的浏览器,访问”http://127.0.0.1:5000/”,一个硕大的”Hello World”映入眼帘:)。
简单解释下这段代码
1 2 |
from flask import Flask app = Flask(__name__) |
这里给的实例名称就是这个python模块名。
1 |
@app.route('/') |
这个函数级别的注解指明了当地址是根路径时,就调用下面的函数。能够定义多个路由规则,会在下篇文章里详细介绍。说的高大上些,这里就是MVC中的Contoller。
1 2 |
def index(): return '<h1>Hello World</h1>' |
当请求的地址符合路由规则时,就会进入该函数。能够说,这里是MVC的Model层。你能够在里面获取请求的request对象,返回的内容就是response。本例中的response就是大标题”Hello World”。
1 2 |
if __name__ == '__main__': app.run() |
当本文件为程序入口(也就是用python命令直接执行本文件)时,就会经过”app.run()”启动Web服务器。若是不是程序入口,那么该文件就是一个模块。Web服务器会默认监听本地的5000端口,但不支持远程访问。若是你想支持远程,须要在”run()”方法传入”host=0.0.0.0″,想改变监听端口的话,传入”port=端口号”,你还能够设置调试模式。具体例子以下:
1 2 |
if __name__ == '__main__': app.run(host='0.0.0.0', port=8888, debug=True) |
注意,Flask自带的Web服务器主要仍是给开发人员调试用的,在生产环境中,你最好是经过WSGI将Flask工程部署到相似Apache或Nginx的服务器上。
本例中的代码能够在这里下载。
Flask入门系列(二)–路由
上一篇中,咱们用Flask写了一个Hello World程序,让你们领略到了Flask的简洁轻便。从这篇开始咱们将对Flask框架的各功能做更详细的介绍,咱们首先从路由(Route)开始。
系列文章
路由
从Hello World中,咱们了解到URL的路由能够直接写在其要执行的函数上。有人会质疑,这样不是把Model和Controller绑在一块儿了吗?的确,若是你想灵活的配置Model和Controller,这样是不方便,可是对于轻量级系统来讲,灵活配置意义不大,反而写在一块更利于维护。Flask路由规则都是基于Werkzeug的路由模块的,它还提供了不少强大的功能。
带参数的路由
让咱们在上一篇Hello World的基础上,加上下面的函数。并运行程序。
1 2 3 |
@app.route('/hello/<name>') def hello(name): return 'Hello %s' % name |
当你在浏览器的地址栏中输入”http://localhost:5000/hello/man”,你将在页面上看到”Hello man”的字样。URL路径中”/hello/”后面的参数被做为”hello()”函数的”name”参数传了进来。
你还能够在URL参数前添加转换器来转换参数类型,咱们再来加个函数:
1 2 3 |
@app.route('/user/<int:user_id>') def get_user(user_id): return 'User ID: %d' % user_id |
试下访问”http://localhost:5000/user/man”,你会看到404错误。可是试下”http://localhost:5000/user/123″,页面上就会有”User ID: 123″显示出来。参数类型转换器”int:”帮你控制好了传入参数的类型只能是整形。目前支持的参数类型转换器有:
类型转换器 |
做用 |
缺省 |
字符型,但不能有斜杠 |
int: |
整型 |
float: |
浮点型 |
path: |
字符型,可有斜杠 |
另外,你们有没有注意到,Flask自带的Web服务器支持热部署。当你修改好文件并保存后,Web服务器自动部署完毕,你无需从新运行程序。
多URL的路由
一个函数上能够设施多个URL路由规则
1 2 3 4 5 6 7 |
@app.route('/') @app.route('/hello') @app.route('/hello/<name>') def hello(name=None): if name is None: name = 'World' return 'Hello %s' % name |
这个例子接受三种URL规则,”/”和”/hello”都不带参数,函数参数”name”值将为空,页面显示”Hello World”;”/hello/“带参数,页面会显示参数”name”的值,效果与上面第一个例子相同。
HTTP请求方法设置
HTTP请求方法经常使用的有Get, Post, Put, Delete。不熟悉的朋友们能够去度娘查下。Flask路由规则也能够设置请求方法。
1 2 3 4 5 6 7 8 |
from flask import request
@app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': return 'This is a POST request' else: return 'This is a GET request' |
当你请求地址”http://localhost:5000/login”,”GET”和”POST”请求会返回不一样的内容,其余请求方法则会返回405错误。有没有以为用Flask来实现Restful风格很方便啊?
URL构建方法
Flask提供了”url_for()”方法来快速获取及构建URL,方法的第一个参数指向函数名(加过”@app.route”注解的函数),后续的参数对应于要构建的URL变量。下面是几个例子:
1 2 3 4 |
url_for('login') # 返回/login url_for('login', id='1') # 将id做为URL参数,返回/login?id=1 url_for('hello', name='man') # 适配hello函数的name参数,返回/hello/man url_for('static', filename='style.css') # 静态文件地址,返回/static/style.css |
静态文件位置
一个Web应用的静态文件包括了JS, CSS, 图片等,Flask的风格是将全部静态文件放在”static”子目录下。而且在代码或模板(下篇会介绍)中,使用”url_for(‘static’)”来获取静态文件目录。上小节中第四个的例子就是经过”url_for()”函数获取”static”目录下的指定文件。若是你想改变这个静态目录的位置,你能够在建立应用时,指定”static_folder”参数。
1 |
app = Flask(__name__, static_folder='files') |
本例中的代码能够在这里下载。
Flask入门系列(三)–模板
在第一篇中,咱们讲到了Flask中的Controller和Model,可是一个完整的MVC,没有View怎么行?前端代码若是都靠后台拼接而成,就太麻烦了。本篇,咱们就介绍下Flask中的View,即模板。
系列文章
模板
Flask的模板功能是基于Jinja2模板引擎实现的。让咱们来实现一个例子吧。建立一个新的Flask运行文件(你应该不会忘了怎么写吧),代码以下:
1 2 3 4 5 6 7 8 9 10 11 12 |
from flask import Flask from flask import render_template
app = Flask(__name__)
@app.route('/hello') @app.route('/hello/<name>') def hello(name=None): return render_template('hello.html', name=name)
if __name__ == '__main__': app.run(host='0.0.0.0', debug=True) |
这段代码同上一篇的多URL路由的例子很是类似,区别就是”hello()”函数并非直接返回字符串,而是调用了”render_template()”方法来渲染模板。方法的第一个参数”hello.html”指向你想渲染的模板名称,第二个参数”name”是你要传到模板去的变量,变量能够传多个。
那么这个模板”hello.html”在哪儿呢,变量参数又该怎么用呢?别急,接下来咱们建立模板文件。在当前目录下,建立一个子目录”templates”(注意,必定要使用这个名字)。而后在”templates”目录下建立文件”hello.html”,内容以下:
1 2 3 4 5 6 7 |
<!doctype html> <title>Hello Sample</title> {% if name %} <h1>Hello {{ name }}!</h1> {% else %} <h1>Hello World!</h1> {% endif %} |
这段代码是否是很像HTML?接触过其余模板引擎的朋友们确定立马秒懂了这段代码。它就是一个HTML模板,根据”name”变量的值,显示不一样的内容。变量或表达式由”{{ }}”修饰,而控制语句由”{% %}”修饰,其余的代码,就是咱们常见的HTML。
让咱们打开浏览器,输入”http://localhost:5000/hello/man”,页面上即显示大标题”Hello man!”。咱们再看下页面源代码
1 2 3 4 |
<!doctype html> <title>Hello from Flask</title>
<h1>Hello man!</h1> |
果真,模板代码进入了”Hello {{ name }}!”分支,并且变量”{{ name }}”被替换为了”man”。Jinja2的模板引擎还有更多强大的功能,包括for循环,过滤器等。模板里也能够直接访问内置对象如request, session等。对于Jinja2的细节,感兴趣的朋友们能够本身去查查。
模板继承
通常咱们的网站虽然页面多,可是不少部分是重用的,好比页首,页脚,导航栏之类的。对于每一个页面,都要写这些代码,很麻烦。Flask的Jinja2模板支持模板继承功能,省去了这些重复代码。让咱们基于上面的例子,在”templates”目录下,建立一个名为”layout.html”的模板:
1 2 3 4 5 6 7 |
<!doctype html> <title>Hello Sample</title> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> <div class="page"> {% block body %} {% endblock %} </div> |
再修改以前的”hello.html”,把原来的代码定义在”block body”中,并在代码一开始”继承”上面的”layout.html”:
1 2 3 4 5 6 7 8 |
{% extends "layout.html" %} {% block body %} {% if name %} <h1>Hello {{ name }}!</h1> {% else %} <h1>Hello World!</h1> {% endif %} {% endblock %} |
打开浏览器,再看下”http://localhost:5000/hello/man”页面的源码。
1 2 3 4 5 6 7 8 |
<!doctype html> <title>Hello Sample</title> <link rel="stylesheet" type="text/css" href="/static/style.css"> <div class="page">
<h1>Hello man!</h1>
</div> |
你会发现,虽然”render_template()”加载了”hello.html”模板,可是”layout.html”的内容也一块儿被加载了。并且”hello.html”中的内容被放置在”layout.html”中”{% block body %}”的位置上。形象的说,就是”hello.html”继承了”layout.html”。
HTML自动转义
咱们看下下面的代码:
1 2 3 |
@app.route('/') def index(): return '<div>Hello %s</div>' % '<em>Flask</em>' |
打开页面,你会看到”Hello Flask”字样,并且”Flask”是斜体的,由于咱们加了”em”标签。但有时咱们并不想让这些HTML标签自动转义,特别是传递表单参数时,很容易致使HTML注入的漏洞。咱们把上面的代码改下,引入”Markup”类:
1 2 3 4 5 6 7 |
from flask import Flask, Markup
app = Flask(__name__)
@app.route('/') def index(): return Markup('<div>Hello %s</div>') % '<em>Flask</em>' |
再次打开页面,”em”标签显示在页面上了。Markup还有不少方法,好比”escape()”呈现HTML标签, “striptags()”去除HTML标签。这里就不一一列举了。
咱们会在Flask进阶系列里对模板功能做更详细的介绍。
本文中的示例代码能够在这里下载。
Flask入门系列(四)–请求,响应及会话
一个完整的HTTP请求,包括了客户端的请求Request,服务器端的响应Response,会话Session等。一个基本的Web框架必定会提供内建的对象来访问这些信息,Flask固然也不例外。咱们来看看在Flask中该怎么使用这些内建对象。
系列文章
Flask内建对象
Flask提供的内建对象经常使用的有request, session, g,经过request,你还能够获取cookie对象。这些对象不但能够在请求函数中使用,在模板中也可使用。
请求对象request
引入flask包中的request对象,就能够直接在请求函数中直接使用该对象了。让咱们改进下第二篇中的login方法:
1 2 3 4 5 6 7 8 9 10 11 |
from flask import request
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': if request.form['user'] == 'admin': return 'Admin login successfully!' else: return 'No such user!' title = request.args.get('title', 'Default') return render_template('login.html', title=title) |
在第三篇的templates目录下,添加”login.html”文件
1 2 3 4 5 6 7 |
{% extends "layout.html" %} {% block body %} <form name="login" action="/login" method="post"> Hello {{ title }}, please login by: <input type="text" name="user" /> </form> {% endblock %} |
执行上面的例子,结果我就很少描述了。简单解释下,request中”method”变量能够获取当前请求的方法,即”GET”, “POST”, “DELETE”, “PUT”等;”form”变量是一个字典,能够获取Post请求表单中的内容,在上例中,若是提交的表单中不存在”user”项,则会返回一个”KeyError”,你能够不捕获,页面会返回400错误(想避免抛出这”KeyError”,你能够用request.form.get(“user”)来替代)。而”request.args.get()”方法则能够获取Get请求URL中的参数,该函数的第二个参数是默认值,当URL参数不存在时,则返回默认值。request的详细使用可参阅Flask的官方API文档。
会话对象session
会话能够用来保存当前请求的一些状态,以便于在请求以前共享信息。咱们将上面的python代码改动下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from flask import request, session
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': if request.form['user'] == 'admin': session['user'] = request.form['user'] return 'Admin login successfully!' else: return 'No such user!' if 'user' in session: return 'Hello %s!' % session['user'] else: title = request.args.get('title', 'Default') return render_template('login.html', title=title)
app.secret_key = '123456' |
你能够看到,”admin”登录成功后,再打开”login”页面就不会出现表单了。session对象的操做就跟一个字典同样。特别提醒,使用session时必定要设置一个密钥”app.secret_key”,如上例。否则你会获得一个运行时错误,内容大体是”RuntimeError: the session is unavailable because no secret key was set”。密钥要尽可能复杂,最好使用一个随机数,这样不会有重复,上面的例子不是一个好密钥。
咱们顺便写个登出的方法,估计我不放例子,你们也都猜到怎么写,就是清除字典里的键值:
1 2 3 4 5 6 |
from flask import request, session, redirect, url_for
@app.route('/logout') def logout(): session.pop('user', None) return redirect(url_for('login')) |
关于”redirect”方法,咱们会在下一篇介绍。
构建响应
在以前的例子中,请求的响应咱们都是直接返回字符串内容,或者经过模板来构建响应内容而后返回。其实咱们也能够先构建响应对象,设置一些参数(好比响应头)后,再将其返回。修改下上例中的Get请求部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from flask import request, session, make_response
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': ... if 'user' in session: ... else: title = request.args.get('title', 'Default') response = make_response(render_template('login.html', title=title), 200) response.headers['key'] = 'value' return response |
打开浏览器调试,在Get请求用户未登陆状态下,你会看到响应头中有一个”key”项。”make_response”方法就是用来构建response对象的,第二个参数表明响应状态码,缺省就是200。response对象的详细使用可参阅Flask的官方API文档。
Cookie的使用
提到了Session,固然也要介绍Cookie喽,毕竟没有Cookie,Session就根本无法用(不知道为何?查查去)。Flask中使用Cookie也很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from flask import request, session, make_response import time
@app.route('/login', methods=['POST', 'GET']) def login(): response = None if request.method == 'POST': if request.form['user'] == 'admin': session['user'] = request.form['user'] response = make_response('Admin login successfully!') response.set_cookie('login_time', time.strftime('%Y-%m-%d %H:%M:%S')) ... else: if 'user' in session: login_time = request.cookies.get('login_time') response = make_response('Hello %s, you logged in on %s' % (session['user'], login_time)) ...
return response |
例子愈来愈长了,此次咱们引入了”time”模块来获取当前系统时间。咱们在返回响应时,经过”response.set_cookie()”函数,来设置Cookie项,以后这个项值会被保存在浏览器中。这个函数的第三个参数(max_age)能够设置该Cookie项的有效期,单位是秒,不设的话,在浏览器关闭后,该Cookie项即失效。
在请求中,”request.cookies”对象就是一个保存了浏览器Cookie的字典,使用其”get()”函数就能够获取相应的键值。
全局对象g
“flask.g”是Flask一个全局对象,这里有点容易让人误解,其实”g”的做用范围,就在一个请求(也就是一个线程)里,它不能在多个请求间共享。你能够在”g”对象里保存任何你想保存的内容。一个最经常使用的例子,就是在进入请求前,保存数据库链接。这个咱们会在介绍数据库集成时讲到。
本例中的代码能够在这里下载。
Flask入门系列(五)–错误处理及消息闪现
本篇将补充一些Flask的基本功能,包括错误处理,URL重定向,日志功能,还有一个颇有趣的消息闪现功能。
系列文章
错误处理
使用”abort()”函数能够直接退出请求,返回错误代码:
1 2 3 4 5 |
from flask import abort
@app.route('/error') def error(): abort(404) |
上例会显示浏览器的404错误页面。有时候,咱们想要在遇到特定错误代码时作些事情,或者重写错误页面,能够用下面的方法:
1 2 3 |
@app.errorhandler(404) def page_not_found(error): return render_template('404.html'), 404 |
此时,当再次遇到404错误时,即会调用”page_not_found()”函数,其返回”404.html”的模板页。第二个参数表明错误代码。
不过,在实际开发过程当中,咱们并不会常用”abort()”来退出,经常使用的错误处理方法通常都是异常的抛出或捕获。装饰器”@app.errorhandler()”除了能够注册错误代码外,还能够注册指定的异常类型。让咱们来自定义一个异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class InvalidUsage(Exception): status_code = 400
def __init__(self, message, status_code=400): Exception.__init__(self) self.message = message self.status_code = status_code
@app.errorhandler(InvalidUsage) def invalid_usage(error): response = make_response(error.message) response.status_code = error.status_code return response |
咱们在上面的代码中定义了一个异常”InvalidUsage”,同时咱们经过装饰器”@app.errorhandler()”修饰了函数”invalid_usage()”,装饰器中注册了咱们刚定义的异常类。这也就意味着,一但遇到”InvalidUsage”异常被抛出,这个”invalid_usage()”函数就会被调用。写个路由试一试吧。
1 2 3 |
@app.route('/exception') def exception(): raise InvalidUsage('No privilege to access the resource', status_code=403) |
URL重定向
重定向”redirect()”函数的使用在上一篇logout的例子中已有出现。做用就是当客户端浏览某个网址时,将其导向到另外一个网址。常见的例子,好比用户在未登陆时浏览某个需受权的页面,咱们将其重定向到登陆页要求其登陆先。
1 2 3 4 5 6 7 8 |
from flask import session, redirect
@app.route('/') def index(): if 'user' in session: return 'Hello %s!' % session['user'] else: return redirect(url_for('login'), 302) |
“redirect()”的第二个参数时HTTP状态码,可取的值有301, 302, 303, 305和307,默认即302(为何没有304?留给你们去思考)。
日志
提到错误处理,那必定要说到日志。Flask提供logger对象,其是一个标准的Python Logger类。修改上例中的”exception()”函数:
1 2 3 4 5 |
@app.route('/exception') def exception(): app.logger.debug('Enter exception method') app.logger.error('403 error happened') raise InvalidUsage('No privilege to access the resource', status_code=403) |
执行后,你会在控制台看到日志信息。在debug模式下,日志会默认输出到标准错误stderr中。你能够添加FileHandler来使其输出到日志文件中去,也能够修改日志的记录格式,下面演示一个简单的日志配置代码:
import logging
from logging.handlers import TimedRotatingFileHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
server_log = TimedRotatingFileHandler('server.log','D') server_log.setLevel(logging.DEBUG) server_log.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s: %(message)s' ))
error_log = TimedRotatingFileHandler('error.log', 'D') error_log.setLevel(logging.ERROR) error_log.setFormatter(logging.Formatter( '%(asctime)s: %(message)s [in %(pathname)s:%(lineno)d]' ))
app.logger.addHandler(server_log) app.logger.addHandler(error_log) |
上例中,咱们在本地目录下建立了两个日志文件,分别是”server.log”记录全部级别日志;”error.log”只记录错误日志。咱们分别给两个文件不一样的内容格式。另外,咱们使用了”TimedRotatingFileHandler”并给了参数”D”,这样日志天天会建立一个新的文件,并将旧文件加日期后缀来归档。
注:执行后会生成server.log和error.log俩文件,访问正常或错误页面这俩均无内容,不知道是自己相关代码有问题,仍是访问方式有问题
你还能够将错误信息发送邮件。更详细的日志使用可参阅Python logging官方文档。
消息闪现
“Flask Message”是一个颇有意思的功能,通常一个操做完成后,咱们都但愿在页面上闪出一个消息,告诉用户操做的结果。用户看完后,这个消息就不复存在了。Flask提供的”flash”功能就是为了这个。咱们仍是拿用户登陆来举例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
from flask import render_template, request, session, url_for, redirect, flash
@app.route('/') def index(): if 'user' in session: return render_template('hello.html', name=session['user']) else: return redirect(url_for('login'), 302)
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': session['user'] = request.form['user'] flash('Login successfully!') return redirect(url_for('index')) else: return ''' <form name="login" action="/login" method="post"> Username: <input type="text" name="user" /> </form> ''' |
上例中,当用户登陆成功后,就用”flash()”函数闪出一个消息。让咱们找回第三篇中的模板代码,在”layout.html”加上消息显示的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!doctype html> <title>Hello Sample</title> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> {% with messages = get_flashed_messages() %} {% if messages %} <ul class="flash"> {% for message in messages %} <li>{{ message }}</li> {% endfor %} </ul> {% endif %} {% endwith %} <div class="page"> {% block body %} {% endblock %} </div> |
上例中”get_flashed_messages()”函数就会获取咱们在”login()”中经过”flash()”闪出的消息。从代码中咱们能够看出,闪出的消息能够有多个。模板”hello.html”不用改。运行下试试。登陆成功后,是否是出现了一条”Login successfully”文字?再刷新下页面,你会发现文字消失了。你能够经过CSS来控制这个消息的显示方式。
“flash()”方法的第二个参数是消息类型,可选择的有”message”, “info”, “warning”, “error”。你能够在获取消息时,同时获取消息类型,还能够过滤特定的消息类型。只需设置”get_flashed_messages()”方法的”with_categories”和”category_filter”参数便可。好比,Python部分可改成:
1 2 3 4 5 6 7 8 |
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': session['user'] = request.form['user'] flash('Login successfully!', 'message') flash('Login as user: %s.' % request.form['user'], 'info') return redirect(url_for('index')) ... |
layout模板部分可改成:
1 2 3 4 5 6 7 8 9 10 11 |
... {% with messages = get_flashed_messages(with_categories=true, category_filter=["message","error"]) %} {% if messages %} <ul class="flash"> {% for category, message in messages %} <li class="{{ category }}">{{ category }}: {{ message }}</li> {% endfor %} </ul> {% endif %} {% endwith %} ... |
运行结果你们就本身试试吧。
本例中的代码能够在这里下载。
转眼,咱们要进入本系列的最后一篇了。一个基本的Web应用功能其实已经讲完了,如今就让咱们引入数据库。简单起见,咱们就使用SQLite3做为例子。
既然前几篇都用用户登陆做为例子,咱们这篇就继续讲登陆,只是登陆的信息会由数据库来验证。让咱们先准备SQLite环境吧。
怎么安装SQLite这里就不说了。咱们先写个数据库表的初始化SQL,保存在”init.sql”文件中:
1 2 3 4 5 6 7 8 9 |
drop table if exists users; create table users ( id integer primary key autoincrement, name text not null, password text not null );
insert into users (name, password) values ('visit', '111'); insert into users (name, password) values ('admin', '123'); |
运行sqlite3命令,初始化数据库。咱们的数据库文件就放在”db”子目录下的”user.db”文件中。
$ sqlite3 db/user.db < init.sql
建立配置文件"config.py",保存配置信息:
1 2 3 4 |
#coding:utf8 DATABASE = 'db/user.db' # 数据库文件位置 DEBUG = True # 调试模式 SECRET_KEY = 'secret_key_1' # 会话密钥 |
在建立Flask应用时,导入配置信息:
1 2 3 4 5 |
from flask import Flask import config
app = Flask(__name__) app.config.from_object('config') |
这里也能够用"app.config.from_envvar('FLASK_SETTINGS', silent=True)"方法来导入配置信息,此时程序会读取系统环境变量中"FLASK_SETTINGS"的值,来获取配置文件路径,并加载此文件。若是文件不存在,该语句返回False。参数"silent=True"表示忽略错误。
这里要用到请求的上下文装饰器,咱们会在进阶系列的第一篇里详细介绍上下文。
1 2 3 4 5 6 7 8 9 |
@app.before_request def before_request(): g.db = sqlite3.connect(app.config['DATABASE'])
@app.teardown_request def teardown_request(exception): db = getattr(g, 'db', None) if db is not None: db.close() |
咱们在"before_request()"里创建数据库链接,它会在每次请求开始时被调用;并在"teardown_request()"关闭它,它会在每次请求关闭前被调用。
让咱们取回上一篇登陆部分的代码,"index()"和"logout()"请求不用修改,在"login()"请求中,咱们会查询数据库,验证客户端输入的用户名和密码是否存在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': name = request.form['user'] passwd = request.form['passwd'] cursor = g.db.execute('select * from users where name=? and password=?', [name, passwd]) if cursor.fetchone() is not None: session['user'] = name flash('Login successfully!') return redirect(url_for('index')) else: flash('No such user!', 'error') return redirect(url_for('login')) else: return render_template('login.html') |
模板中加上"login.html"文件
1 2 3 4 5 6 7 8 |
{% extends "layout.html" %} {% block body %} <form name="login" action="/login" method="post"> Username: <input type="text" name="user" /><br> Password: <input type="password" name="passwd" /><br> <input type="submit" value="Submit" /> </form> {% endblock %} |
终于一个真正的登陆验证写完了(前几篇都是假的),打开浏览器登陆下吧。由于比较懒,就不写CSS美化了,受不了这粗糙界面的朋友们就本身调吧。
到目前为止,Flask的基础功能已经介绍完了,是否很想动手写个应用啦?其实Flask还有更强大的高级功能,以后会在进阶系列里介绍。
本例中的代码能够在这里下载。
让咱们开启Jinja2模板引擎之旅,虽然说标题是Flask中的Jinja2,其实介绍的主要是Jinja2自己,Flask是用来作例子的。若是对Flask不熟悉的朋友们建议将本博客的入门系列先看下。怎么,不知道什么是模板引擎?你能够将模板比做MVC模式中的View视图层,而模板引擎就是用来将模板同业务代码分离,并解析模板语言的程序。你能够耐心地看下本系列文章,就能体会到什么是模板引擎了。
咱们在Flask入门系列第三篇中已经介绍了Jinja2模板的基本使用方式,让咱们先回顾下,把其中的代码拿过来。
Flask Python代码:
1 2 3 4 5 6 7 8 9 10 11 |
from flask import Flask,render_template
app = Flask(__name__)
@app.route('/hello') @app.route('/hello/<name>') def hello(name=None): return render_template('hello.html', name=name)
if __name__ == '__main__': app.run(host='0.0.0.0', debug=True) |
模板代码:
1 2 3 4 5 6 7 |
<!doctype html> <title>Hello Sample</title> {% if name %} <h1>Hello {{ name }}!</h1> {% else %} <h1>Hello World!</h1> {% endif %} |
咱们了解到,模板的表达式都是包含在分隔符”{{ }}”内的;控制语句都是包含在分隔符”{% %}”内的;另外,模板也支持注释,都是包含在分隔符”{# #}”内,支持块注释。
表达式通常有这么几种:
有没有以为,这里的表达式很像Python的语法呀?
Jinja2的控制语句主要就是条件控制语句if,和循环控制语句for,语法相似于Python。咱们能够改动下上节的模板代码:
1 2 3 4 5 6 7 |
{% if name and name == 'admin' %} <h1>This is admin console</h1> {% elif name %} <h1>Welcome {{ name }}!</h1> {% else %} <h1>Please login</h1> {% endif %} |
上面是一个条件控制语句的例子,注意if控制语句要用”{% endif %}”来结束。模板中没法像代码中同样靠缩进来判断代码块的结束。再来看个循环的例子,咱们先改下Python代码中的”hello”函数,让其传两个列表进模板。
1 2 3 4 5 6 |
def hello(name=None): return render_template('hello-1.html', name=name, digits=[1,2,3,4,5], users=[{'name':'John'}, {'name':'Tom', 'hidden':True}, {'name':'Lisa'} {'name':'Bob'}]) |
而后在模板中加上:
1 2 3 |
{% for digit in digits %} {{ digit }} {% endfor %} |
是否是列表被显示出来了?同if语句同样,for控制语句要用”{% endfor %}”来结束。页面上,每一个元素之间会有空格,若是你不但愿有空格,就要在”for”语句的最后,和”endfor”语句的最前面各加上一个”-“号。如:
1 2 3 |
{% for digit in digits -%} {{ digit }} {%- endfor %} |
如今,你能够看到数字”12345″被一块儿显示出来了。咱们再来看个复杂的循环例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<dl> {% for user in users if not user.hidden %} {% if loop.first %} <div>User List:</div> {% endif %} <div class="{{ loop.cycle('odd', 'even') }}"> <dt>User No {{ loop.index }}:</dt> <dd>{{ user.name }}</dd> </div> {% if loop.last %} <div>Total Users: {{ loop.length }}</div> {% endif %} {% else %} <li>No users found</li> {% endfor %} </dl> |
这里有三个知识点。首先for循环支持else语句,当待遍历的列表”users”为空或者为None时,就进入else语句。
其次,在for语句后使用if关键字,能够对循环中的项做过滤。本例中,全部hidden属性为True的user都会被过滤掉。
另外,for循环中能够访问Jinja2的循环内置变量。本例中,咱们会在第一项前显示标题,最后一项后显示总数,每一项显示序号。另外,奇偶项的HTML div元素会有不一样的class。若是咱们加入下面的CSS style,就能看到斑马线。
直接在html页面中写入下列内容便可。
不过也可单独写入一个css文件路,不过须要引用才能生效
1 2 3 4 5 |
<style type="text/css"> .odd { background-color: #BDF; } </style> |
Jinja2的循环内置变量主要有如下几个:
变量 |
内容 |
loop.index |
循环迭代计数(从1开始) |
loop.index0 |
循环迭代计数(从0开始) |
loop.revindex |
循环迭代倒序计数(从len开始,到1结束) |
loop.revindex0 |
循环迭代倒序计数(从len-1开始,到0结束) |
loop.first |
是否为循环的第一个元素 |
loop.last |
是否为循环的最后一个元素 |
loop.length |
循环序列中元素的个数 |
loop.cycle |
在给定的序列中轮循,如上例在”odd”和”even”两个值间轮循 |
loop.depth |
当前循环在递归中的层级(从1开始) |
loop.depth0 |
当前循环在递归中的层级(从0开始) |
关于递归循环,你们看看下面的例子,我就很少介绍了:
1 2 3 4 5 6 7 8 |
{% for item in [[1,2],[3,4,5]] recursive %} Depth: {{ loop.depth }} {% if item[0] %} {{ loop(item) }} {% else %} Number: {{ item }} ; {% endif %} {% endfor %} |
另外,若是你启用了”jinja2.ext.loopcontrols”扩展的话,你还能够在循环中使用”{% break %}”和”{% continue %}”来控制循环执行。关于Jinja2的扩展,咱们会在本系列的第七篇和第八篇中介绍。
有时候,咱们在页面上就是要显示”{{ }}”这样的符号怎么办?Jinja2提供了”raw”语句来忽略全部模板语法。
1 2 3 4 5 6 7 |
{% raw %} <ul> {% for item in items %} <li>{{ item }}</li> {% endfor %} </ul> {% endraw %} |
咱们将本文一开始的Flask代码”hello()”方法改动下:
1 2 3 4 5 6 |
@app.route('/hello') @app.route('/hello/<name>') def hello(name=None): if name is None: name = '<em>World</em>' return render_template('hello.html', name=name) |
此时,访问”http://localhost:5000/hello”,页面上会显示”Welcome <em>World</em>!”,也就是这个HTML标签”<em>”被自动转义了。正如咱们曾经提到过的,Flask会对”.html”, “.htm”, “.xml”, “.xhtml”这四种类型的模板文件开启HTML格式自动转义。这样也能够防止HTML语法注入。若是咱们不想被转义怎么办?
1 2 3 |
{% autoescape false %} <h1>Hello {{ name }}!</h1> {% endautoescape %} |
将”autoescape”开关设为”false”便可,反之,设为”true”即开启自动转义。使用”autoescape”开关前要启用”jinja2.ext.autoescape”扩展,在Flask框架中,这个扩展默认已启用。
使用”set”关键字给变量赋值:
1 |
{% set items = [[1,2],[3,4,5]] %} |
用法能够参考下面的with语句
相似于Python中的”with”关键字,它能够限制with语句块内对象的做用域:
1 2 3 4 5 |
{% with foo = 1 %} {% set bar = 2 %} {{ foo + bar }} {% endwith %} {# foo and bar are not visible here #} |
使用”with”关键字前要启用”jinja2.ext.with_”扩展,在Flask框架中,这个扩展默认已启用。
1 2 3 4 |
{% with arr = ['Sunny'] %} {{ arr.append('Rainy') }} {{ arr }} {% endwith %} |
看上面这段代码,咱们想执行列表的”append”操做,这时使用”{{ arr.append(‘Rainy’) }}”页面会输出”None”,换成”{% %}”来执行,程序会报错,由于这是个表达式,不是语句。那怎么办?咱们能够启用”jinja2.ext.do”扩展。而后在模板中执行”do”语句便可:
1 2 3 4 |
{% with arr = ['Sunny'] %} {% do arr.append('Rainy') %} {{ arr }} {% endwith %} 默认jinja2没开启这个,须要启用 在py文件中添加这个:app.jinja_env.add_extension("jinja2.ext.do"),表示启用jinja2的do扩展,而后就能在html文件中使用上述语句了 |
本篇中的示例代码能够在这里下载。
Flask每一个请求都有生命周期,在生命周期内请求有其上下文环境Request Context。咱们在Flask进阶系列第一篇中有详细介绍。做为在请求中渲染的模板,天然也在请求的生命周期内,因此Flask应用中的模板可使用到请求上下文中的环境变量,及一些辅助函数。本文就会介绍下这些变量和函数。
request对象能够用来获取请求的方法”request.method”,表单”request.form”,请求的参数”request.args”,请求地址”request.url”等。它自己是一个字典。在模板中,你同样能够获取这些内容,只要用表达式符号”{{ }}”括起来便可。
1 |
<p>{{ request.url }}</p> |
在没有请求上下文的环境中,这个对象不可用。
session对象能够用来获取当前会话中保存的状态,它自己是一个字典。在模板中,你能够用表达式符号”{{ }}”来获取这个对象。
Flask代码以下,别忘了设置会话密钥哦:
注:须要导入from flask import Flask,render_template,session
1 2 3 4 5 6 |
@app.route('/') def index(): session['user'] = 'guest' return render_template('hello.html')
app.secret_key = '123456' |
模板代码:
1 |
<p>User: {{ session.user }}</p> |
在没有请求上下文的环境中,这个对象不可用。
全局变量g,用来保存请求中会用到全局内容,好比数据库链接。模板中也能够访问。
Flask代码:
注:须要导入from flask import Flask,render_template,g
1 2 3 4 |
@app.route('/') def index(): g.db = 'mysql' return render_template('hello.html') |
模板代码:
1 |
<p>DB: {{ g.db }}</p> |
g对象是保存在应用上下文环境中的,也只在一个请求生命周期内有效。在没有应用上下文的环境中,这个对象不可用。
在Flask入门系列第六篇中,咱们曾介绍过如何将配置信息导入Flask应用中。导入的配置信息,就保存在”app.config”对象中。这个配置对象在模板中也能够访问。
1 |
<p>Host: {{ config.DEBUG }}</p> |
结果返回:Host:True,表示的是开启了调试模式
“config”是全局对象,离开了请求生命周期也能够访问。
url_for()函数能够用来快速获取及构建URL,Flask也将此函数引入到了模板中,好比下面的代码,就能够获取静态目录下的”style.css”文件。
1 |
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> |
该函数是全局的,离开了请求生命周期也能够调用。
get_flashed_messages()函数是用来获取消息闪现的。具体的示例咱们在入门系列第五篇中已经讲过,这里就再也不赘述了。这也是一个全局可以使用的函数。
除了Flask提供的标准上下文变量和函数,咱们还能够本身定义。下面咱们就来先定义一个上下文变量,在Flask应用代码中,加入下面的函数:
1 2 3 4 5 |
from flask import current_app
@app.context_processor def appinfo(): return dict(appname=current_app.name) |
函数返回的是一个字典,里面有一个属性”appname”,值为当前应用的名称。咱们曾经介绍过,这里的”current_app”对象是一个定义在应用上下文中的代理。函数用”@app.context_processor”装饰器修饰,它是一个上下文处理器,它的做用是在模板被渲染前运行其所修饰的函数,并将函数返回的字典导入到模板上下文环境中,与模板上下文合并。而后,在模板中”appname”就如同上节介绍的”request”, “session”同样,成为了可访问的上下文对象。咱们能够在模板中将其输出:
1 |
<p>Current App is: {{ appname }}</p> |
同理咱们能够自定义上下文函数,只需将上例中返回字典的属性指向一个函数便可,下面咱们就来定义一个上下文函数来获取系统当前时间:
1 2 3 4 5 6 7 |
import time
@app.context_processor def get_current_time(): def get_time(timeFormat="%b %d, %Y - %H:%M:%S"): return time.strftime(timeFormat) return dict(current_time=get_time) |
咱们能够试下在模板中将其输出:
1 2 |
<p>Current Time is: {{ current_time() }}</p> <p>Current Day is: {{ current_time("%Y-%m-%d") }}</p> |
上下文处理器能够修饰多个函数,也就是咱们能够定义多个上下文环境变量和函数。
本篇中的示例代码能够在这里下载。
我所了解的模板引擎大部分都会提供相似Jinja2过滤器的功能,只不过叫法不一样罢了。好比PHP Smarty中的Modifiers(变量调节器或修饰器),FreeMarker中的Build-ins(内建函数),连AngularJS这样的前端框架也提供了Filter过滤器。它们都是用来在变量被显示或使用前,对其做转换处理的。能够把它认为是一种转换函数,输入的参数就是其所修饰的变量,返回的就是变量转换后的值。
回到咱们第一篇开篇的例子,咱们在模板中对变量name做以下处理:
1 |
<h1>Hello {{ name | upper }}!</h1> |
你会看到name的输出都变成大写了。这就是过滤器,只需在待过滤的变量后面加上”|”符号,再加上过滤器名称,就能够对该变量做过滤转换。上面例子就是转换成全大写字母。过滤器能够连续使用:
1 |
<h1>Hello {{ name | upper | truncate(3, True) }}!</h1> |
如今name变量不但被转换为大写,并且当它的长度大于3后,只显示前3个字符,后面默认用”…”显示。过滤器”truncate”有3个参数,第一个是字符截取长度;第二个决定是否保留截取后的子串,默认是False,也就是当字符大于3后,只显示”…”,截取部分也不出现;第三个是省略符号,默认是”…”。
其实从例子中咱们能够猜到,过滤器本质上就是一个转换函数,它的第一个参数就是待过滤的变量,在模板中使用时能够省略去。若是它有第二个参数,模板中就必须传进去。
Jinja2模板引擎提供了丰富的内置过滤器。这里介绍几个经常使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
{# 当变量未定义时,显示默认字符串,能够缩写为d #} <p>{{ name | default('No name', true) }}</p>
{# 单词首字母大写 #} <p>{{ 'hello' | capitalize }}</p>
{# 单词全小写 #} <p>{{ 'XML' | lower }}</p>
{# 去除字符串先后的空白字符 #} <p>{{ ' hello ' | trim }}</p>
{# 字符串反转,返回"olleh" #} <p>{{ 'hello' | reverse }}</p>
{# 格式化输出,返回"Number is 2" #} <p>{{ '%s is %d' | format("Number", 2) }}</p>
{# 关闭HTML自动转义 #} <p>{{ '<em>name</em>' | safe }}</p>
{% autoescape false %} {# HTML转义,即便autoescape关了也转义,能够缩写为e #} <p>{{ '<em>name</em>' | escape }}</p> {% endautoescape %} |
1 2 3 4 5 6 7 8 |
{# 四舍五入取整,返回13.0 #} <p>{{ 12.8888 | round }}</p>
{# 四舍五入向下截取到小数点后2位,返回12.89 #} <p>{{ 12.8888 | round(2) }}</p>
{# 向下截取到小数点后2位,返回12.88 #} <p>{{ 12.8888 | round(2, 'floor') }}</p>
{# 绝对值,返回12 #} <p>{{ -12 | abs }}</p> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{# 取第一个元素 #} <p>{{ [1,2,3,4,5] | first }}</p>
{# 取最后一个元素 #} <p>{{ [1,2,3,4,5] | last }}</p>
{# 返回列表长度,能够写为count #} <p>{{ [1,2,3,4,5] | length }}</p>
{# 列表求和 #} <p>{{ [1,2,3,4,5] | sum }}</p>
{# 列表排序,默认为升序 #} <p>{{ [3,2,1,5,4] | sort }}</p>
{# 合并为字符串,返回"1 | 2 | 3 | 4 | 5" #} <p>{{ [1,2,3,4,5] | join(' | ') }}</p>
{# 列表中全部元素都全大写。这里能够用upper,lower,但capitalize无效 #} <p>{{ ['tom','bob','ada'] | upper }}</p> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
{% set users=[{'name':'Tom','gender':'M','age':20}, {'name':'John','gender':'M','age':18}, {'name':'Mary','gender':'F','age':24}, {'name':'Bob','gender':'M','age':31}, {'name':'Lisa','gender':'F','age':19}] %}
{# 按指定字段排序,这里设reverse为true使其按降序排 #} <ul> {% for user in users | sort(attribute='age', reverse=true) %} <li>{{ user.name }}, {{ user.age }}</li> {% endfor %} </ul>
{# 列表分组,每组是一个子列表,组名就是分组项的值 #} <ul> {% for group in users|groupby('gender') %} <li>{{ group.grouper }}<ul> {% for user in group.list %} <li>{{ user.name }}</li> {% endfor %}</ul></li> {% endfor %} </ul>
{# 取字典中的某一项组成列表,再将其链接起来 #} <p>{{ users | map(attribute='name') | join(', ') }}</p> |
更全的内置过滤器介绍能够从Jinja2的官方文档中找到。
Flask提供了一个内置过滤器”tojson”,它的做用是将变量输出为JSON字符串。这个在配合Javascript使用时很是有用。咱们延用上节字典列表操做中定义的”users”变量
1 2 3 4 |
<script type="text/javascript"> var users = {{ users | tojson | safe }}; console.log(users[0].name); </script> |
注意,这里要避免HTML自动转义,因此加上safe过滤器。
注:暂不知道具体用法
Jinja2还能够对整块的语句使用过滤器。
1 2 3 |
{% filter upper %} This is a Flask Jinja2 introduction. {% endfilter %} |
不过上述这种场景不常常用到。
内置的过滤器不知足需求怎么办?本身写呗。过滤器说白了就是一个函数嘛,咱们立刻就来写一个。回到Flask应用代码中:
注:这个颇有用
1 2 |
def double_step_filter(l): return l[::2] |
咱们定义了一个”double_step_filter”函数,返回输入列表的偶数位元素(第0位,第2位,..)。怎么把它加到模板中当过滤器用呢?Flask应用对象提供了”add_template_filter”方法来帮咱们实现。咱们加入下面的代码:
1 |
app.add_template_filter(double_step_filter, 'double_step') |
函数的第一个参数是过滤器函数,第二个参数是过滤器名称。而后,咱们就能够愉快地在模板中使用这个叫”double_step”的过滤器了:
1 2 |
{# 返回[1,3,5] #} <p>{{ [1,2,3,4,5] | double_step }}</p> |
Flask还提供了添加过滤器的装饰器”template_filter”,使用起来更简单。下面的代码就添加了一个取子列表的过滤器。装饰器的参数定义了该过滤器的名称”sub”。
1 2 3 |
@app.template_filter('sub') def sub(l, start, end): return l[start:end] |
咱们在模板中能够这样使用它:
1 2 |
{# 返回[2,3,4] #} <p>{{ [1,2,3,4,5] | sub(1,4) }}</p> |
Flask添加过滤器的方法其实是封装了对Jinja2环境变量的操做。上述添加”sub”过滤器的方法,等同于下面的代码。
1 |
app.jinja_env.filters['sub'] = sub |
咱们在Flask应用中,不建议直接访问Jinja2的环境变量。若是离开Flask环境直接使用Jinja2的话,就能够经过”jinja2.Environment”来获取环境变量,并添加过滤器。
本篇中的示例代码能够在这里下载。
Jinja2中的测试器Test和过滤器很是类似,区别是测试器老是返回一个布尔值,它能够用来测试一个变量或者表达式,你须要使用”is”关键字来进行测试。测试器通常都是跟着if控制语句一块儿使用的。下面咱们就来深刻了解下这个测试器。
再次取回第一篇开篇的例子,咱们在模板中对变量name做以下判断:
1 2 3 |
{% if name is lower %} <h2>"{{ name }}" are all lower case.</h2> {% endif %} |
当name变量中的字母都是小写时,这段文字就会显示。这就是测试器,在if语句中,变量或表达式的后面加上is关键字,再加上测试器名称,就能够对该变量或表达式做测试,并根据其测试结果的真或假,来决定是否进入if语句块。测试器也能够有参数,用括号括起。当其只有一个参数时,能够省去括号。
1 2 3 |
{% if 6 is divisibleby 3 %} <h2>"divisibleby" test pass</h2> {% endif %} |
上例中,测试器”divisibleby”能够判断其所接收的变量是否能够被其参数整除。由于它只有一个参数,咱们就能够用空格来分隔测试器和其参数。上面的调用同”divisibleby(3)”效果一致。测试器也能够配合not关键字一块儿使用:
1 2 3 |
{% if 6 is not divisibleby(4) %} <h2>"not divisibleby" test pass</h2> {% endif %} |
显然测试器本质上也是一个函数,它的第一个参数就是待测试的变量,在模板中使用时能够省略去。若是它有第二个参数,模板中就必须传进去。测试器函数返回的必须是一个布尔值,这样才能够用来给if语句做判断。
同过滤器同样,Jinja2模板引擎提供了丰富的内置测试器。这里介绍几个经常使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
{# 检查变量是否被定义,也能够用undefined检查是否未被定义 #} {% if name is defined %} <p>Name is: {{ name }}</p> {% endif %}
{# 检查是否全部字符都是大写 #} {% if name is upper %} <h2>"{{ name }}" are all upper case.</h2> {% endif %}
{# 检查变量是否为空 #} {% if name is none %} <h2>Variable is none.</h2> {% endif %}
{# 检查变量是否为字符串,也能够用number检查是否为数值 #} {% if name is string %} <h2>{{ name }} is a string.</h2> {% endif %}
{# 检查数值是不是偶数,也能够用odd检查是否为奇数 #} {% if 2 is even %} <h2>Variable is an even number.</h2> {% endif %}
{# 检查变量是否可被迭代循环,也能够用sequence检查是不是序列 #} {% if [1,2,3] is iterable %} <h2>Variable is iterable.</h2> {% endif %}
{# 检查变量是不是字典 #} {% if {'name':'test'} is mapping %} <h2>Variable is dict.</h2> {% endif %} |
更全的内置测试器介绍能够从Jinja2的官方文档中找到。
若是内置测试器不知足需求,咱们就来本身写一个。写法很相似于过滤器,先在Flask应用代码中定义测试器函数,而后经过”add_template_test”将其添加为模板测试器:
1 2 3 4 |
import re def has_number(str): return re.match(r'.*\d+', str) app.add_template_test(has_number,'contain_number') |
咱们定义了一个”has_number”函数,用正则来判断输入参数是否包含数字。而后调用”app.add_template_test”方法,第一个参数是测试器函数,第二个是测试器名称。以后,咱们就能够在模板中使用”contain_number”测试器了:
1 2 3 |
{% if name is contain_number %} <h2>"{{ name }}" contains number.</h2> {% endif %} |
同过滤器同样,Flask提供了添加测试器的装饰器”template_test”。下面的代码就添加了一个判断字符串是否以某一子串结尾的测试器。装饰器的参数定义了该测试器的名称”end_with”:
1 2 3 |
@app.template_test('end_with') def end_with(str, suffix): return str.lower().endswith(suffix.lower()) |
咱们在模板中能够这样使用它:
1 2 3 |
{% if name is end_with "me" %} <h2>"{{ name }}" ends with "me".</h2> {% endif %} |
Flask添加测试器的方法是封装了对Jinja2环境变量的操做。上述添加”end_with”测试器的方法,等同于下面的代码。
1 |
app.jinja_env.tests['end_with'] = " end_with " |
咱们在Flask应用中,不建议直接访问Jinja2的环境变量。若是离开Flask环境直接使用Jinja2的话,就能够经过”jinja2.Environment”来获取环境变量,并添加测试器。
本文中的示例代码能够在这里下载。
介绍完了过滤器和测试器,接下来要讲的是Jinja2模板引擎的另外一个辅助函数功能,即全局函数Global Functions。若是说过滤器是一个变量转换函数,测试器是一个返回布尔值的函数,那全局函数就能够是任意函数。能够在任一场景使用,没有输入和输出值的限制。本篇咱们就来阐述下这个全局函数。
仍是取出第一篇开篇的代码,咱们在模板中加入下面的代码:
1 2 3 4 5 |
<ul> {% for num in range(10, 20, 2) %} <li>Number is "{{ num }}"</li> {% endfor %} </ul> |
页面上会显示”10,12,14,16,18″5个列表项。全局函数”range()”的做用同Python里的同样,返回指定范围内的数值序列。三个参数分别是开始值,结束值(不包含),间隔。若是只传两个参数,那间隔默认为1;若是只传1个参数,那开始值默认为0。
因而可知,全局函数如同其名字同样,就是全局范围内能够被使用的函数。其同第二篇介绍的上下文环境中定义的函数不一样,没有请求生命周期的限制。
演示几个经常使用的内置全局函数。
1 2 3 4 |
{% set user = dict(name='Mike',age=15) %} <p>{{ user | tojson | safe }}</p>
{# 显示 '{"age": 15, "name": "Mike"}' #} |
1 2 3 4 5 6 |
{% set sep = joiner("|") %} {% for val in range(5) %} {{ sep() }} <span>{{ val }}</span> {% endfor %}
{# 显示 "0 | 1 | 2 | 3 | 4" #} |
1 2 3 4 5 6 7 |
{% set cycle = cycler('odd', 'even') %} <ul> {% for num in range(10, 20, 2) %} <li class="{{ cycle.next() }}">Number is "{{ num }}", next line is "{{ cycle.current }}" line.</li> {% endfor %} </ul> |
基于上一节的例子,加上”cycler()”函数的使用,你会发现列表项<li>的”class”在”odd”和”even”两个值间轮循。加入第一篇中的CSS style,就能够看到斑马线了。
“cycler()”函数返回的对象能够作以下操做
更全的内置全局函数介绍能够从Jinja2的官方文档中找到。
咱们固然也能够写本身的全局函数,方法同以前介绍的过滤器啦,测试器啦都很相似。就是将Flask应用代码中定义的函数,经过”add_template_global”将其传入模板便可:
1 2 3 4 5 6 7 8 9 |
import re def accept_pattern(pattern_str): pattern = re.compile(pattern_str, re.S) def search(content): return pattern.findall(content)
return dict(search=search, current_pattern=pattern_str)
app.add_template_global(accept_pattern, 'accept_pattern') |
上例中的accept_pattern函数会先预编译一个正则,而后返回的字典中包含一个查询函数”search”,以后调用”search”函数就能够用编译好的正则来搜索内容了。”app.add_template_global”方法的第一个参数是自定义的全局函数,第二个是全局函数名称。如今,让咱们在模板中使用”accept_pattern”全局函数:
1 2 3 4 5 6 7 8 9 |
{% with pattern = accept_pattern("<li>(.*?)</li>") %} {% set founds = pattern.search("<li>Tom</li><li>Bob</li>") %} <ul> {% for item in founds %} <li>Found: {{ item }}</li> {% endfor %} </ul> <p>Current Pattern: {{ pattern.current_pattern }}</p> {% endwith %} |
“Tom”和”Bob”被抽取出来了,很牛掰的样子。你还能够根据须要在”accept_pattern”的返回字典里定义更多的方法。
Flask一样提供了添加全局函数的装饰器”template_global”,以方便全局函数的添加。咱们来用它将第二篇中取系统当前时间的函数”current_time”定义为全局函数。
1 2 3 4 |
import time @app.template_global('end_with') def current_time(timeFormat="%b %d, %Y - %H:%M:%S"): return time.strftime(timeFormat) |
同第二篇中的同样,咱们在模板中能够这样使用它:
1 2 |
<p>Current Time is: {{ current_time() }}</p> <p>Current Day is: {{ current_time("%Y-%m-%d") }}</p> |
Flask添加全局函数的方法是封装了对Jinja2环境变量的操做。上述添加”current_time”全局函数的方法,等同于下面的代码。
1 |
app.jinja_env.globals['current_time'] = current_time |
咱们在Flask应用中,不建议直接访问Jinja2的环境变量。若是离开Flask环境直接使用Jinja2的话,就能够经过”jinja2.Environment”来获取环境变量,并添加全局函数。
本文中的示例代码能够在这里下载。
考虑到模板代码的重用,Jinja2提供了块 (Block)和宏 (Macro)的功能。块功能有些相似于C语言中的宏,原理就是代码替换;而宏的功能有些相似于函数,能够传入参数。本篇咱们就来介绍下块和宏的用法。
在Flask入门系列第三篇介绍模板时,咱们提到了模板的继承。咱们在子模板的开头定义了”{% extend ‘parent.html’ %}”语句来声明继承,此后在子模板中由”{% block block_name %}”和”{% endblock %}”所包括的语句块,将会替换父模板中一样由”{% block block_name %}”和”{% endblock %}”所包括的部分。
这就是块的功能,模板语句的替换。这里要注意几个点:
另外,咱们建议在”endblock”关键字后也加上块名,好比”{% endblock block_name %}”。虽然对程序没什么做用,可是当有多个块嵌套时,可读性好不少。
若是父模板中的块里有内容不想被子模板替换怎么办?咱们可使用”super( )”方法。基于Flask入门系列第三篇的例子,咱们将父模板”layout.html”改成:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!doctype html> <head> {% block head %} <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <title>{% block title %}{% endblock %}</title> {% endblock %} </head> <body> <div class="page"> {% block body %} {% endblock %} </div> </body> |
并在子模板里,加上”head”块和”title”块:
1 2 3 4 5 6 7 |
{% block title %}Block Sample{% endblock %} {% block head %} {{ super() }} <style type="text/css"> h1 { color: #336699; } </style> {% endblock %} |
父模板同子模板的”head”块中都有内容。运行后,你能够看到,父模板中的”head”块语句先被加载,然后是子模板中的”head”块语句。这就得益于咱们在子模板的”head”块中加上了表达式”{{ super( ) }}”。效果有点像Java中的”super( )”吧。
默认状况下,块内语句是没法访问块外做用域中的变量。好比咱们在”layout.html”加上一个循环:
1 2 3 |
{% for item in range(5) %} <li>{% block list %}{% endblock %}</li> {% endfor %} |
而后在子模板中定义”list”块并访问循环中的”item”变量:
1 2 3 |
{% block list %} <em>{{ item }}</em> {% endblock %} |
你会发现页面上什么数字也没显示。若是你想在块内访问这个块外的变量,你就须要在块声明时添加”scoped”关键字。好比咱们在”layout.html”中这样声明”list”块便可:
1 2 3 |
{% for item in range(5) %} <li>{% block list scoped %}{% endblock %}</li> {% endfor %} |
文章的开头咱们就讲过,Jinja2的宏功能有些相似于传统程序语言中的函数,既然是函数就有其声明和调用两个部分。那就让咱们先声明一个宏:
1 2 3 |
{% macro input(name, type='text', value='') -%} <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}"> {%- endmacro %} |
代码中,宏的名称就是”input”,它有三个参数分别是”name”, “type”和”value”,后两个参数有默认值。如今,让咱们使用表达式来调用这个宏:
1 2 3 |
<p>{{ input('username', value='user') }}</p> <p>{{ input('password', 'password') }}</p> <p>{{ input('submit', 'submit', 'Submit') }}</p> |
你们能够在页面上看到一个文本输入框,一个密码输入框及一个提交按钮。是否是同函数同样啊?其实它还有比函数更丰富的功能,以后咱们来介绍。
咱们先来建立个宏”list_users”:
1 2 3 4 5 6 7 8 |
{% macro list_users(users) -%} <table> <tr><th>Name</th><th>Action</th></tr> {%- for user in users %} <tr><td>{{ user.name |e }}</td>{{ caller() }}</tr> {%- endfor %} </table> {%- endmacro %} |
宏的做用就是将用户列表显示在表格里,表格每一行用户名称后面调用了”{{ caller( ) }}”方法,这个有什么用呢?先别急,咱们来写调用者的代码:
1 2 3 4 5 6 7 8 |
{% set users=[{'name':'Tom','gender':'M','age':20}, {'name':'John','gender':'M','age':18}, {'name':'Mary','gender':'F','age':24}] %}
{% call list_users(users) %} <td><input name="delete" type="button" value="Delete"></td> {% endcall %} |
与上例不一样,这里咱们使用了”{% call %}”语句块来调用宏,语句块中包括了一段生成”Delete”按钮的代码。运行下试试,你会发现每一个用户名后面都出现了”Delete”按钮,也就是”{{ caller( ) }}”部分被调用者”{% call %}”语句块内部的内容替代了。不明觉厉吧!其实吧,这个跟函数传个参数进去没啥大区别,我的以为,主要是有些时候HTML语句太复杂(如上例),不方便写在调用参数上,因此就写在”{% call %}”语句块里了。
Jinja2的宏不但能访问调用者语句块的内容,还能给调用者传递参数。嚯,这又是个什么鬼?咱们来扩展下上面的例子。首先,咱们将表格增长一列性别,并在宏里调用”caller()”方法时,传入一个变量”user.gender”:
1 2 3 4 5 6 7 8 |
{% macro list_users(users) -%} <table> <tr><th>Name</th><th>Gender</th><th>Action</th></tr> {%- for user in users %} <tr><td>{{ user.name |e }}</td>{{ caller(user.gender) }}</tr> {%- endfor %} </table> {%- endmacro %} |
而后,咱们修改下调用者语句块:
1 2 3 4 5 6 7 8 9 10 |
{% call(gender) list_users(users) %} <td> {% if gender == 'M' %} <img src="{{ url_for('static', filename='img/male.png') }}" width="20px"> {% else %} <img src="{{ url_for('static', filename='img/female.png') }}" width="20px"> {% endif %} </td> <td><input name="delete" type="button" value="Delete"></td> {% endcall %} |
你们注意到,咱们在使用”{% call %}”语句时,将其改成了”{% call(gender) … %}”,这个括号中的”gender”就是用来接受宏里传来的”user.gender”变量。所以咱们就能够在”{% call %}”语句中使用这个”gender”变量来判断用户性别。这样宏就成功地向调用者传递了参数。
上例中,咱们看到宏的内部可使用”caller( )”方法获取调用者的内容。此外宏还提供了两个内部变量:
这是一个列表。若是调用宏时传入的参数多于宏声明时的参数,多出来的没指定参数名的参数就会保存在这个列表中。
这是一个字典。若是调用宏时传入的参数多于宏声明时的参数,多出来的指定了参数名的参数就会保存在这个字典中。
让咱们回到第一个例子input宏,在调用时增长其传入的参数,并在宏内将上述两个变量打印出来:
1 2 3 4 5 6 |
{% macro input(name, type='text', value='') -%} <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}"> <br /> {{ varargs }} <br /> {{ kwargs }} {%- endmacro %} <p>{{ input('submit', 'submit', 'Submit', 'more arg1', 'more arg2', ext='more arg3') }}</p> |
能够看到,varargs变量存了参数列表”[‘more arg1’, ‘more arg2’]”,而kwargs字典存了参数”{‘ext’:’more arg3′}”。
一个宏能够被不一样的模板使用,因此咱们建议将其声明在一个单独的模板文件中。须要使用时导入进来便可,而导入的方法也很是相似于Python中的”import”。让咱们将第一个例子中”input”宏的声明放到一个”form.html”模板文件中,而后将调用的代码改成:
1 2 3 4 |
{% import 'form.html' as form %} <p>{{ form.input('username', value='user') }}</p> <p>{{ form.input('password', 'password') }}</p> <p>{{ form.input('submit', 'submit', 'Submit') }}</p> |
运行下,效果是否是同以前的同样?你也能够采用下面的方式导入:
1 2 3 4 |
{% from 'form.html' import input %} <p>{{ input('username', value='user') }}</p> <p>{{ input('password', 'password') }}</p> <p>{{ input('submit', 'submit', 'Submit') }}</p> |
这里咱们再介绍一个Jinja2模板中代码重用的功能,就是包含 (Include),使用的方法就是”{% include %}”语句。其功能就是将另外一个模板加载到当前模板中,并直接渲染在当前位置上。它同导入”import”不同,”import”以后你还须要调用宏来渲染你的内容,”include”是直接将目标模板渲染出来。它同block块继承也不同,它一次渲染整个模板文件内容,不分块。
咱们能够建立一个”footer.html”模板,并在”layout.html”中包含这个模板:
1 2 3 4 |
<body> ... {% include 'footer.html' %} </body> |
当”include”的模板文件不存在时,程序会抛出异常。你能够加上”ignore missing”关键字,这样若是模板不存在,就会忽略这段”{% include %}”语句。
1 |
{% include 'footer.html' ignore missing %} |
“{% include %}”语句还能够跟一个模板列表:
1 |
{% include ['footer.html','bottom.html','end.html'] ignore missing %} |
上例中,程序会按顺序寻找模板文件,第一个被找到的模板即被加载,而其后的模板都会被忽略。若是都没找到,那整个语句都会被忽略。
本篇中的示例代码能够在这里下载。
一个强大的工具通常都支持扩展或插件的开发功能,来容许第三方经过开发新扩展或插件,扩充工具自己功能,并能够贡献给社区。Jinja2也不例外,Jinja2自己提供了一部分扩展,你能够在程序中启用。同时,你还能够建立本身的扩展,来扩充模板引擎功能。本篇会先介绍Jinja2自带的扩展”jinja2.ext.i18n”的使用,自定义扩展的开发会放在下一篇阐述。
任什么时候候使用Jinja2时,都须要先建立Jinja2环境,因此启用扩展的方法就是在建立环境时指定:
1 2 |
from jinja2 import Environment jinja_env = Environment(extensions=['jinja2.ext.i18n','jinja2.ext.do']) |
可是你在使用Flask时,其已经有了一个Jinja2环境,你不能再建立一个,因此你须要想办法添加扩展。Flask对于扩展不像过滤器或测试器那样封装了添加方法和装饰器,这样你就只能直接访问Flask中的Jinja2环境变量来添加。
1 2 3 4 |
from flask import Flask app = Flask(__name__) app.jinja_env.add_extension('jinja2.ext.i18n') app.jinja_env.add_extension('jinja2.ext.do') |
注:Flask默认已加载了”jinja2.ext.autoescape”和”jinja2.ext.with_”扩展。
在本系列第一篇中,咱们已经介绍了四个Jinja2内置扩展的使用:”jinja2.ext.autoescape”, “jinja2.ext.with_”, “jinja2.ext.do”和”jinja2.ext.loopcontrols”。除了这几个之外,Jinja2还有一个很是重要的扩展,就是提供本地化功能的”jinja2.ext.i18n”。它能够与”gettext”或”babel”联合使用,接下来咱们采用”gettext”来介绍怎么使用这个本地化扩展。
注:本地化这个其实就是翻译页面语言,这个详看Flask-Babel
建议你们先去了解下Python gettext相关知识,篇幅关系本文就不许备细讲。这里咱们使用Python源代码(记住不是安装包)中”Tools/i18n”目录下的工具来建立翻译文件。
$ python pygettext.py
上述命令会在当前目录下生成一个名为”message.pot”的翻译文件模板,内容以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2016-02-22 21:45+CST\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: ENCODING\n" "Generated-By: pygettext.py 1.5\n" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Jinja2 i18n Extention Sample # Copyright (C) 2016 bjhee.com # Billy J. Hee <billy@bjhee.com>, 2016. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2016-02-22 21:45+CST\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: UTF-8\n" "Generated-By: pygettext.py 1.5\n" |
修改完后,将其另存为翻译文件”lang.po”。
1 2 |
msgid "Hello World!" msgstr "世界,你好!" |
将其加在文件末尾。这里”msgid”指定了待翻译的文字,而”msgstr”就是翻译后的文字。
咱们依然使用”Tools/i18n”目录提供的工具,”msgfmt.py”:
$ python msgfmt.py lang.po
执行完后,当前目录生成了”lang.mo”文件。注意,只有这个”*.mo”文件才能被应用程序识别。另外,推荐一个工具Poedit,很强的图形化po编辑工具,也能够用来生成mo文件,很是好用,Mac和Windows下都能用。
咱们在当前Flask工程下建立子目录”locale/zh_CN/LC_MESSAGES/”,并将刚才生成的”lang.po”和”lang.mo”文件放到这个目录下。这里”locale”子目录的名字能够更改,其余的我的建议不要改。
让咱们在Flask应用代码中启用”jinja2.ext.i18n”,并加载刚建立的翻译文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#coding:utf8 import gettext from flask import Flask,render_template app = Flask(__name__) # 加载扩展 app.jinja_env.add_extension('jinja2.ext.i18n')
# 'lang'表示翻译文件名为"lang.mo",'locale'表示全部翻译文件都在"locale"子目录下, # 'zh_CN'表示二级子目录,含义上讲就是加载中文翻译。因此下面的代码会加载文件: # "locale/zh_CN/LC_MESSAGES/lang.mo" gettext.install('lang', 'locale', unicode=True) translations = gettext.translation('lang', 'locale', languages=['zh_CN']) translations.install(True) app.jinja_env.install_gettext_translations(translations) |
这个”install_gettext_translations()”方法就是Jinja2提供来加载”gettext”翻译文件对象的。加载完后,你就能够在模板中使用本地化功能了。方法有两种:”{% trans %}”语句或”gettext()”方法。咱们先来试下”{% trans %}”语句,在模板文件中,咱们加上:
1 |
<h1>{% trans %}Hello World!{% endtrans %}</h1> |
运行下,有没有看到页面上打印了”世界,你好!”,恭喜你,成功了!使用”gettext()”方法以下,效果同”{% trans %}”语句同样。
1 |
<h1>{{ gettext('Hello World!') }}</h1> |
Jinja2还提供了”_( )”方法来替代”gettext( )”,代码看起来很简洁,我的推荐使用这个方法。
1 |
<h1>{{ _('Hello World!') }}</h1> |
上面的例子是在程序中指定本地化语言,你也能够在请求上下文中判断请求头”Accept-Language”的内容,来动态的设置本地化语言。
有时候,待翻译的文字内有一个变量必须在运行时才能肯定,怎么办?我能够在翻译文字上加参数。首先,你要在po文件中定义带参数的翻译文字,并生成mo文件:
1 2 |
msgid "Hello %(user)s!" msgstr "%(user)s,你好!" |
而后,你就能够在模板中,使用”{% trans %}”语句或”gettext()”方法来显示它:
1 2 |
<h1>{% trans user=name %}Hello {{ user }}!{% endtrans %}</h1> <h1>{{ _('Hello %(user)s!')|format(user=name) }}</h1> |
上例中,咱们把模板中的变量”name”赋给了翻译文字中的变量”user”。翻译文字上能够有多个变量。
Jinja2从2.5版本开始,支持新的gettext样式,使得带参数的本地化更简洁,上面的例子在新样式中能够写成:
1 |
<h1>{{ _('Hello %(user)s!', user=name) }}</h1> |
不过使用新样式前,你必须先启用它。还记得咱们介绍过Jinja2加载翻译文件的方法吗?对,就是”install_gettext_translations()”。调用它时,加上”newstyle=True”参数便可。
1 |
app.jinja_env.install_gettext_translations(translations, newstyle=True) |
英文有个特色就是名词有单/复数形式,通常复数都是单数后面加s,而中文就不区分了,哎,老外就是麻烦。所谓外国人创造的Python gettext,天然也对单/复数提供了特殊的支持。让咱们如今po文件中,加上下面的内容,并生成mo文件:
1 2 3 4 |
msgid "%(num)d item" msgid_plural "%(num)d items" msgstr[0] "%(num)d个物品" msgstr[1] "%(num)d个物品集" |
什么意思呢,这个”msgid_plural”就是指定了它上面”msgid”文字的复数形式。而”msgstr”的[0], [1]分别对应了单/复数形式翻译后的内容。为何这么写?你别管了,照着写就是了。
在模板中,咱们加上下面的代码:
1 2 3 |
{% set items = [1,2,3,4,5] %} {{ ngettext('%(num)d item', '%(num)d items', items|count) }}<br /> {{ ngettext('%(num)d item', '%(num)d items', items|first) }} |
你会很惊奇的发现,当”num”变量为5时,页面显示”5个物品集”;而当”num”变量为1时,页面显示”1个物品”。也就是程序自动匹配单/复数。很神奇吧!
原本准备在本篇把扩展都介绍完的,发现单写个”i18n”后篇幅就很长了,只好把自定义扩展部分另起一篇。
本篇中的示例代码能够在这里下载。
说实话,关于自定义扩展的开发,Jinja2的官方文档写得真心的简单。到目前为止网上可参考的资料也很是少,你必须得好好读下源码,还好依然有乐于奉献的大牛们分享了些文章来帮助我理解怎么开发扩展。本文我就彻底借鉴网上前人的例子,来给你们演示一个Jinja2的自定义扩展的开发方法。
Pygments是Python提供语法高亮的工具,官网是pygments.org。咱们在介绍Jinja2的自定义扩展时为何要介绍Pygments呢?由于Jinja2的功能已经很强了,我一时半会想不出该开发哪一个有用的扩展,写个没意义的扩展嘛,又怕误导了读者。恰巧网上找到了一位叫Larry的外国友人开发了一个基于Pygments的代码语法高亮扩展,感受很是实用。他的代码使用了MIT License,那就我放心拿过来用了,不过仍是要注明下这位Larry才是原创。
你须要先执行”pip install pygments”命令安装Pygments包。代码中用到Pygments的部分很是简单,主要就是调用”pygments.highlight( )”方法来生成HTML文档。Pygments强的地方是它不把样式写在HTML当中,这样就给了咱们很大的灵活性。开始写扩展前,让咱们预先经过代码
1 2 |
from pygments.formatters import HtmlFormatter HtmlFormatter(style='vim').get_style_defs('.highlight') |
生成样式内容并将其保存在”static/css/style.css”文件中。这个css文件就是用来高亮语法的。
想深刻了解Pygments的朋友们,能够先把官方文档看一下。
咱们在Flask应用目录下,建立一个”pygments_ext.py”文件,内容以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
#coding:utf8 from jinja2 import nodes from jinja2.ext import Extension
from pygments import highlight from pygments.formatters import HtmlFormatter from pygments.lexers import guess_lexer, get_lexer_by_name
# 建立一个自定义扩展类,继承jinja2.ext.Extension class PygmentsExtension(Extension): # 定义该扩展的语句关键字,这里表示模板中的{% code %}语句会该扩展处理 tags = set(['code'])
def __init__(self, environment): # 初始化父类,必须这样写 super(PygmentsExtension, self).__init__(environment)
# 在Jinja2的环境变量中添加属性, # 这样在Flask中,就能够用app.jinja_env.pygments来访问 environment.extend( pygments=self, pygments_support=True )
# 重写jinja2.ext.Extension类的parse函数 # 这是处理模板中{% code %}语句的主程序 def parse(self, parser): # 进入此函数时,即表示{% code %}标签被找到了 # 下面的代码会获取当前{% code %}语句在模板文件中的行号 lineno = next(parser.stream).lineno
# 获取{% code %}语句中的参数,好比咱们调用{% code 'python' %}, # 这里就会返回一个jinja2.nodes.Const类型的对象,值为'python' lang_type = parser.parse_expression()
# 将参数封装为列表 args = [] if lang_type is not None: args.append(lang_type)
# 下面的代码能够支持两个参数,参数之间用逗号分隔,不过本例中用不到 # 这里先检查当前处理流的位置是否是个逗号,是的话就再获取一个参数 # 不是的话,就在参数列表最后加个空值对象 # if parser.stream.skip_if('comma'): # args.append(parser.parse_expression()) # else: # args.append(nodes.Const(None))
# 解析从{% code %}标志开始,到{% endcode %}为止中间的全部语句 # 将解析完后的内容存在body里,并将当前流位置移到{% endcode %}以后 body = parser.parse_statements(['name:endcode'],drop_needle=True)
# 返回一个CallBlock类型的节点,并将其以前取得的行号设置在该节点中 # 初始化CallBlock节点时,传入咱们自定义的"_pygmentize"方法的调用, # 两个空列表,还有刚才解析后的语句内容body return nodes.CallBlock(self.call_method('_pygmentize', args), [], [], body).set_lineno(lineno)
# 这个自定义的内部函数,包含了本扩展的主要逻辑。 # 其实上面parse()函数内容,大部分扩展均可以重用 def _pygmentize(self, lang_type, caller): # 初始化HTML格式器 formatter = HtmlFormatter(linenos='table')
# 获取{% code %}语句中的内容 # 这里caller()对应了上面调用CallBlock()时传入的body content = caller()
# 将模板语句中解析到了lang_type设置为咱们要高亮的语言类型 # 若是这个变量不存在,则让Pygmentize猜想可能的语言类型 lexer = None if lang_type is None: lexer = guess_lexer(content) else: lexer = get_lexer_by_name(lang_type)
# 将{% code %}语句中的内容高亮,即添加各类<span>, class等标签属性 return highlight(content, lexer, formatter) |
这段程序解释起来太麻烦,我就把注释都写在代码里了。总的来讲,扩展中核心部分就在”parse()”函数里,而最关键的就是这个”parser”对象,它是一个”jinja2.parser.Parser”的对象。建议你们能够参考下它的源码。咱们使用的主要方法有:
在”parse()”函数最后,咱们建立了一个”nodes.CallBlock”的块节点对象,并将其返回。初始化时,咱们先传入了”_pygmentize()”方法的调用;而后两个空列表分别对应了字段和属性,本例中用不到,因此设空;再传入解析后的语句块”body”。”CallBlock”节点初始化完后,还要记得将当前行号设置进去。接下来,咱们对于语句块的全部操做,均可以写在”_pygmentize()”方法里了。
“_pygmentize()”里的内容我就很少介绍了,只须要记得声明这个方法时,最后必定要接收一个参数caller,它是个回调函数,能够获取以前建立”CallBlock”节点时传入的语句块内容。
扩展写完了,其实也没几行代码,就是注释多了点。如今咱们在Flask应用代码中将其启用:
1 2 3 4 5 |
from flask import Flask,render_template from pygments_ext import PygmentsExtension
app = Flask(__name__) app.jinja_env.add_extension(PygmentsExtension) |
而后让咱们在模板中试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<head> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <p>A sample of JS code</p> {% autoescape false %} {% code 'javascript' %} var name = 'World'; function foo() { console.log('Hello ' + name); } {% endcode %} {% endautoescape %} </body> |
运行下,页面上这段代码是否是有VIM的效果呀?这里咱们引入了刚才建立在”static/css”目录下”style.css”样式文件,另外千万别忘了要将自动转义关掉,否则你会看到一堆的HTML标签。
另外提醒下你们,网上有文章说,对于单条语句,也就是不须要结束标志的语句,”parse()”函数里无需调用”nodes.CallBlock”,只需返回”return self.call_method(xxx)”便可。别相信他,看看源码就知道,这个方法返回的是一个”nodes.Expr”表达式对象,而”parse()”必须返回一个”nodes.Stmt”语句对象。
本篇中的示例代码能够在这里下载。