上一篇简单介绍了flask,现在接着学习模板引擎和表单处理。
一、Jinja2 模板引擎
Jinja2 是 Flask 的默认模板引擎,允许开发者将动态数据嵌入到 HTML 中,生成富有交互性的页面。它支持变量、控制结构(如 for 循环和 if 判断)以及宏(类似于函数的代码片段,可以复用)。
1. 导入与使用
Flask通过render_template来实现模板的渲染,要使用这个方法,我们需要导入from flask import rander_template
,模板中注释需放在{# #}
中
在模板文件 templates/index2.html
中:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>你好</title>
</head>
<body>
<h1>Hello, {{ name }}!</h1>
</body>
</html>
在 Flask 应用中使用这个模板:
from flask import render_template
from gevent.local import local
from flask_base import FlaskBase
class Test1(FlaskBase):
@staticmethod
@FlaskBase.app.route("/", methods=['GET'])
def index():
name = '张三'
# 以键值对的形式传参给模板index2.html
return render_template('index2.html', name=name)
# 如果有多个变量需要传递,直接使用**locals()替代我们在当前视图函数中定义的所有变量
# return render_template('index2.html', **locals())
if __name__ == '__main__':
test1 = Test1()
test1.run()
2. 控制语句
jinja2模板引擎中也可使用if和for控制语句,但是语句需要放置在{% %}
中;
在模板文件 templates/index3.html
中:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>你好</title>
</head>
<body>
{% if data.name=='张三' %}
<h1>你是张三!</h1>
{% if data.name=='李四' %}
<h1>你是李四!</h1>
{% else %}
<h1>你是谁!</h1>
{% endif %}
{% endif %}
{% for item in data.list %}
<p>目标:{{item}}</p>
{% endfor %}
</body>
</html>
在 Flask 应用中使用这个模板:
from flask import render_template
from flask_base import FlaskBase
class Test1(FlaskBase):
@staticmethod
@FlaskBase.app.route("/", methods=['GET'])
def index():
data = {
'name': '张三',
'list': [1, 2, 3]
}
return render_template('index3.html', data=data)
if __name__ == '__main__':
test1 = Test1()
test1.run()
3. 过滤器
可以使用内置过滤器前端模板内{{ 内容 | 过滤器 }}
的" | "后使用
也可以使用add_template_filter(函数方法名,'过滤器名')
来自定义过滤器;
在模板文件 templates/index4.html
中:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>你好</title>
</head>
<body>
<!-- 过滤器的使用 -->
<!-- 全大写 -->
{{ 'hello world' | upper }}
<br>
<!-- 单词首字母大写 -->
{{ 'hello world' | title }}
<br>
<!-- 替换左边的内容为右边的内容 -->
{{ 'hello world' | replace('hello','hi') }}
<br>
<!-- 调用自定义的过滤器 -->
list列表:{{ data.list | li2 }}
</body>
</html>
在 Flask 应用中使用这个模板:
from flask import render_template
from flask_base import FlaskBase
class Test1(FlaskBase):
@staticmethod
@FlaskBase.app.route("/", methods=['GET'])
def index():
data = {
'name': '张三',
'list': [1, 2, 3]
}
return render_template('index4.html', data=data)
# 自定义过滤器
def list_step(li):
# 返回列表,步长为2
return li[::2]
# 注册模板过滤器(filter)
# 参数1为该过滤器调用的函数,参数2为在前端中调用该过滤器使用的名称
FlaskBase.app.add_template_filter(list_step, 'li2')
if __name__ == '__main__':
test1 = Test1()
test1.run()
4. 宏
- 宏的定义是为了将前端模板中需要反复创建的模块变成一个方便调用的“函数”,可以传参,但不能有返回值
- 宏的定义以macro标志开始,以endmacro结束,同样需要在
{% %}
中进行
定义宏:macro.html
<!-- 不带参数的宏定义, 像定义一个函数一样 -->
{% macro input1() %}
<!-- 宏内执行的操作,生成一个input表单项 -->
<label>表单项1:
<input type="text" name='username' value=''>
<br>
</label>
{% endmacro %}
<!-- 带参数的宏定义,在括号添加参数和默认值 -->
{% macro input2(name, value='', type='text', size=30) %}
<!-- 同样是宏内执行的操作,生成一个input表单项 -->
<!-- 此处双括号内的参数,指向我们在定义时设定的参数,调用时没有传值就使用设定的默认值 -->
<label>表单项2:
<input type="{{ type }}" name="{{ name }}" value="{{ value }}" size="{{ size }}">
<br>
</label>
{% endmacro %}
在模板中使用上述定义在外部文件中的宏
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>你好</title>
</head>
<body>
{% import 'macro.html' as func %}
<div>
<!-- 调用导入的宏模板文件中的宏,实现登录页面构建 -->
<p>用户名:{{func.input2('username')}}</p>
<p>密码:{{func.input2('password',type='password')}}</p>
<p>登录:{{func.input2('submit',type='submit',value='登录')}}</p>
</div>
<!-- 另一种导入方式 -->
{% from 'macro.html' import input2 %}
<div>
<!-- 此时直接调用input2即可 -->
<p>用户名:{{input2('username')}}</p>
<p>密码:{{input2('password',type='password')}}</p>
<p>登录:{{input2('submit',type='submit',value='登录')}}</p>
</div>
</body>
</html>
5. include
- include用于在一个模板的指定位置导入另一个模板的内容,区别于宏的调用,include更像从另一个模板“复制+粘贴”;
- include同样在
{% %}
中使用,采用语句{% include 模块名 %}
,需要注意两点:
- include是直接将目标模板中的所有内容直接“copy”在当前位置,所以被导入的模板如果有head和body部分也将被导入过来;
- include和import都是在templates这个目录下搜索的,所以使用路径时不需要添加相对路径:上级目录 “ …/ ” 和当前目录 “ ./ ” ;
头部模版:
<nav>
<div class="top">
这是顶部
</div>
</nav>
底部模版:
<footer>
<div class="bottom">
这是底部
<!-- 说明:子模板中可以直接使用父模板的变量,不需要其他操作
因为这一代码是被复制到父模板中去运行的 -->
作者:{{ data.name }}
</div>
</footer>
使用:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
{% include "common/header.html" %}
<div class="content">
中间的
</div>
{% include "common/footer.html" %}
</body>
</html>
6. 加载静态文件
在static文件目录下定义名为css、image和js的文件夹分别存储这些静态文件。通常配合url_for函数使用(需要在双括号内调用),将模板标签的src、herf属性通过url_for(静态文件名称)
设置为反转url要比使用相对路径更好。
<head>
<!-- 导入js文件 -->
<script type="text/javascript" src="{{url_for('static',filename='js/jquery-3.5.1/jquery-3.5.1.js')}}"></script>
<!-- 导入css文件 -->
<link rel="stylesheet" href="{{url_for('static',filename='css/test.css')}}">
</head>
<body>
<!-- 导入图片 -->
<img alt="" src="{{ url_for('static',filename='image/test.jpg') }}"/>
</body>
7. extends
有别于include中我们在后端导入的父模板,将子模板插入到父模板中需要的地方。extend中我们在后端导入子模板,将父模板的框架(页面布局)拿来显示子模板
父模板:
<!DOCTYPE html>
<html lang="en">
<head>
<!--除了装载部分,其他部分子模板一律安照当前父模板的定义显示-->
<meta charset="UTF-8">
<title>
<!--标题中子模板内容的装载位置-->
{% block title %}
{% endblock %}
-我的网站
</title>
</head>
<body>
<!--主体中子模板内容的装载位置-->
{% block body %}
这是基类中的内容
{% endblock %}
</body>
</html>
子模板1:
<!--继承的父类模板文件名称-->
{% extends "father.html" %}
<!--插入到父类代码的title区块的内容-->
{% block title %}
网站首页
{% endblock %}
<!--插入到父类代码的body区块的内容-->
{% block body %}
<!--保留父模板该block中原本的内容-->
{{ super() }}
<h4>这是网站首页的内容!</h4>
{% endblock %}
子模板2:
<!--继承的父类模板文件名称-->
{% extends "father.html" %}
<!--插入到父类代码的title区块的内容-->
{% block title %}
产品列表页
{% endblock %}
<!--插入到父类代码的body区块的内容-->
{% block body %}
<h4>这是产品列表页的内容!</h4>
取得网页标题的内容:
<!--调用当前模板中其他block的内容 -->
<h4>{{ self.title() }}</h4>
{% endblock %}
二、表单处理
设计一个用户登录功能
1. 基础表单处理
表单数据接收及业务处理
from flask import render_template, request, redirect, url_for, session
from flask_base import FlaskBase
class Test1(FlaskBase):
FlaskBase.app.secret_key = 'your_secret_key_here'
@staticmethod
@FlaskBase.app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
# 验证用户名和密码
if username == 'admin' and password == 'secret':
session['username'] = username # 将用户名存入 session
return redirect(url_for('dashboard'))
else:
error = '无效的用户名或密码'
return render_template('login.html', error=error)
# GET请求显示表单
return render_template('login.html')
@staticmethod
@FlaskBase.app.route('/dashboard')
def dashboard():
# 检查用户是否已登录(通过 session)
if 'username' not in session:
return redirect(url_for('login')) # 未登录则重定向到登录页
return render_template('common/dashboard.html', username=session['username'])
@staticmethod
@FlaskBase.app.route('/logout')
def logout():
session.pop('username', None) # 清除 session
return redirect(url_for('login'))
if __name__ == '__main__':
test1 = Test1()
test1.run()
login.html
<!DOCTYPE html>
<html>
<head>
<title>登录</title>
</head>
<body>
<h1>登录</h1>
{% if error %}
<p style="color: red;">{{ error }}</p>
{% endif %}
<form method="post">
<div>
<label>用户名:</label>
<input type="text" name="username" required>
</div>
<div>
<label>密码:</label>
<input type="password" name="password" required>
</div>
<button type="submit">登录</button>
</form>
</body>
</html>
dashboard.html
<!DOCTYPE html>
<html>
<head>
<title>登录成功页面</title>
</head>
<body>
<h1>欢迎, {{ username }}!</h1>
<p>这是您登录后的主界面。</p>
<a href="{{ url_for('logout') }}">退出登录</a>
</body>
</html>
运行结果:
2. Flask-WTF扩展
对于复杂表单,推荐使用Flask-WTF扩展,它结合了WTForms库,提供了表单验证、CSRF保护等功能。
安装Flask-WTF
pip install flask-wtf
# 2.3.0以后的WTForms已经不支持email验证器,所以需要单独安装
pip show email_validator
WTForms提供了丰富的验证器:
- DataRequired:字段不能为空
- Email:必须是有效的电子邮件地址
- Length:字符串长度限制
- NumberRange:数值范围限制
- EqualTo:字段必须与另一个字段值相等(如密码确认)
- URL:必须是有效的URL
- Regexp:必须匹配正则表达式
表单数据接收及处理
from flask import render_template, redirect,request, url_for, session
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length
from flask_base import FlaskBase
class LoginForm(FlaskForm):
email = StringField('邮箱', validators=[DataRequired(), Email(), Length(1, 256)])
password = PasswordField('密码', validators=[DataRequired(), Length(min=6)])
submit = SubmitField('登录')
class Test1(FlaskBase):
FlaskBase.app.secret_key = 'your_secret_key_here'
@staticmethod
@FlaskBase.app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if request.method == 'POST' and form.validate(): # 移除可能冲突的验证方式
# 表单验证通过
email = form.email.data
session['username'] = email # 将用户名存入 session
# 处理登录逻辑
return redirect(url_for('dashboard'))
return render_template('login_wtf.html', form=form)
@staticmethod
@FlaskBase.app.route('/dashboard')
def dashboard():
# 检查用户是否已登录(通过 session)
if 'username' not in session:
return redirect(url_for('login')) # 未登录则重定向到登录页
return render_template('common/dashboard.html', username=session['username'])
@staticmethod
@FlaskBase.app.route('/logout')
def logout():
session.pop('username', None) # 清除 session
return redirect(url_for('login'))
if __name__ == '__main__':
test1 = Test1()
test1.run()
login_wtf.html
<!DOCTYPE html>
<html>
<head>
<title>登录</title>
</head>
<body>
<h1>登录</h1>
<form method="post">
{{ form.hidden_tag() }}
<div>
{{ form.email.label }}
{{ form.email }}
{% if form.email.errors %}
<ul>
{% for error in form.email.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div>
{{ form.password.label }}
{{ form.password }}
{% if form.password.errors %}
<ul>
{% for error in form.password.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{{ form.submit }}
</form>
</body>
</html>
运行结果
3. 文件上传
实现上传
from flask import render_template, request
from werkzeug.utils import secure_filename
import os
from flask_base import FlaskBase
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
FlaskBase.app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
class Test1(FlaskBase):
def __init__(self):
super().__init__()
# 确保上传目录存在
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
@staticmethod
@FlaskBase.app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
# 检查是否有文件部分
if 'file' not in request.files:
return '没有文件部分'
file = request.files['file']
# 如果用户未选择文件,浏览器会提交一个没有文件名的空部分
if file.filename == '':
return '未选择文件'
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(FlaskBase.app.config['UPLOAD_FOLDER'], filename))
return f'文件 {filename} 上传成功'
return render_template('upload_file.html')
if __name__ == '__main__':
test1 = Test1()
test1.run()
upload_file.html
<title>上传文件</title>
<h1>上传文件</h1>
<form method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="上传">
</form>