Flask框架
Flask框架
Flask诞生于2010年,是Armin ronacher(人名)用 Python 语言基于 Werkzeug 工具箱编写的轻量级Web开发框架。
Flask 本身相当于一个内核,其他几乎所有的功能都要用到扩展(邮件扩展Flask-Mail,用户认证Flask-Login,数据库Flask-SQLAlchemy),都需要用第三方的扩展来实现。比如可以用 Flask 扩展加入ORM、窗体验证工具,文件上传、身份验证等。Flask 没有默认使用的数据库,你可以选择 MySQL,也可以用 NoSQL。
Flask是一个基于Python开发并且依赖jinja2模板和Werkzeug WSGI服务的一个微型框架,对于Werkzeug本质是Socket服务端,其用于接收http请求并对请求进行预处理,然后触发Flask框架,开发人员基于Flask框架提供的功能对请求进行相应的处理,并返回给用户,如果要返回给用户复杂的内容时,需要借助jinja2模板来实现对模板的处理,即:将模板和数据进行渲染,将渲染后的字符串返回给用户浏览器。
“微”(micro) 并不表示你需要把整个 Web 应用塞进单个 Python 文件(虽然确实可以 ),也不意味着 Flask 在功能上有所欠缺。微框架中的“微”意味着 Flask 旨在保持核心简单而易于扩展。Flask 不会替你做出太多决策——比如使用何种数据库。而那些 Flask 所选择的——比如使用何种模板引擎——则很容易替换。除此之外的一切都由可由你掌握。如此,Flask 可以与您珠联璧合。
默认情况下,Flask 不包含数据库抽象层、表单验证,或是其它任何已有多种库可以胜任的功能。然而,Flask 支持用扩展来给应用添加这些功能,如同是 Flask 本身实现的一样。众多的扩展提供了数据库集成、表单验证、上传处理、各种各样的开放认证技术等功能。Flask 也许是“微小”的,但它已准备好在需求繁杂的生产环境中投入使用。
注:其 WSGI 工具箱采用 Werkzeug(路由模块),模板引擎则使用 Jinja2。这两个也是 Flask 框架的核心。
官网
官方文档
Falsk常用第三方扩展包
- Flask-SQLalchemy:操作数据库,ORM;
- Flask-script:终端脚本工具,脚手架;
- Flask-migrate:管理迁移数据;
- Flask-Session:Session存储方式指定;
- Flask-WTF:表单;
- Flask-Mail:邮件;
- Flask-Bable:提供国际化和本地支持,翻译;
- Flask-Login:认证用户状态;
- Falsk:OpenID:认证,OAuth;
- Falsk JSON-RPC:开发rpc远程服务[过程]调用
- Flask-Bootstrap:集成前端Twitter Bootstrap框架
- Flask-Moment:本地化日期和时间
- Flask-Admin:简单而可扩展的管理接口的框架
可以通过 https://pypi.org/search/?c=Framework+%3A%3A+Flask 查看更多flask官方推荐的扩展
一、路由
1、路由的使用
路由和视图的名称必须全局统一,不能出现重复,否则报错
2、路由映射的方式
-
任意路由参数
-
@app.route(“/user/<username>”)
# 路由传递参数[没有限定类型] @app.route("/user/<username>") def user_info(username): return "hello %s" % username
-
-
限定路由参数
-
限定路由参数的类型,flask系统自带转换器编写在werkzeug.routing.py文件中。底部可以看到以下字典:
DEFAULT_CONVERTERS = { "default": UnicodeConverter, "string": UnicodeConverter, "any": AnyConverter, "path": PathConverter, "int": IntegerConverter, "float": FloatConverter, "uuid": UUIDConverter, }
系统自带的转换器具体使用方式在每种转换器的注释代码中有写,请留意每种转换器初始化的参数。
转换器名称 描述 string 默认类型,接受不带斜杠的任何文本 int 接受正整数 float 接受正浮点值 path 接收 string
但也接受斜线uuid 接受UUID(通用唯一识别码)字符串 xxxx-xxxx-xxxxx-xxxxx -
@app.route(“/post/<int:post_id>”)
# flask提供了路由转换器可以让我们对路由参数进行限定 # 路由传递参数[限定数据类型] @app.route("/user/<int:user_id>") def user_info(user_id): return "hello %d" % user_id
-
@app.route(“/post/<float:post_id>”)
# flask提供了路由转换器可以让我们对路由参数进行限定 # 路由传递参数[限定数据类型] @app.route("/user/<float:user_id>") def user_info(user_id): return "hello %d" % user_id
-
@app.route(“/post/<path:path>”)
# flask提供了路由转换器可以让我们对路由参数进行限定 # 路由传递参数[限定数据类型] @app.route("/user/<path:user_id>") def user_info(user_id): return "hello %d" % user_id
-
@app.route(“/login”, methods=[“GET”, “POST”])
# flask提供了路由转换器可以让我们对路由参数进行限定 # 路由传递参数[限定数据类型] @app.route("/login", methods=["GET", "POST"]) def login(): return "hello World!!!"
-
3、自定义路由参数转换器
正则匹配路由参数
在web开发中,可能会出现限制用户访问规则的场景,这种情况就需要使用正则匹配,根据自己的规则去限定请求参数在进行访问
实现步骤:
-
导入转换器基类:在Flask中,所有的路由的匹配规则都是使用转换器对象进行记录
from werkzeug.routing import BaseConverter
-
自定义转换器:自定义类继承于转换器基类
# 自定义正则转换器 from werkzeug.routing import BaseConverter class RegexConverter(BaseConverter): def __init__(self,map,*args): super().__init__(map) # 正则参数 self.regex = args[0]
-
添加转换器到默认的转换器字典中
# 将自定义转换器添加到转换器字典中,并指定转换器使用时名字为: re app.url_map.converters["re"] = RegexConverter
-
使用自定义转换器实现自定义匹配规则
# 正则匹配路由(手机号正则) @app.route("/login/<re("1d{10}"):mobile>") def login(mobile): return mobile
-
测试代码:
from flask import Flask,request # 初始化 app = Flask(import_name=__name__) # 编写路由视图 @app.route(rule="/") def index(): return "<h1>hello world!</h1>" # 关于路由参数的限制,flask内置的类型不够具体,在开发中,我们经常接受参数,需要更加精确的限制 # 这时候,可以使用正则匹配路由参数 # 正则匹配路由参数,其实就是扩展flask内置的路由限定类型,需要完成4个步骤 # 1. 引入flask的路由转换器 from werkzeug.routing import BaseConverter # 2. 创建自定义路由转换器 class MobileConverter(BaseConverter): """手机号码类型限制""" def __init__(self,map,*args): super().__init__(map) self.regex = "1[3-9]d{9}" # 3. 把自定义转换器添加到flask默认的转换器字典中,也就是和原来的int,float等放在一块 app.url_map.converters["mob"] = MobileConverter # 4. 类似原来的路由参数限制一样,调用自定义转换器名称即可 @app.route(rule="/user/<mob:mobile>") def user(mobile): return mobile # 1. 引入flask的路由转换器 from werkzeug.routing import BaseConverter # 2. 创建自定义路由转换器 class RegexConverter(BaseConverter): """根据正则进行参数限制""" def __init__(self,map,*args): super().__init__(map) self.regex = args[0] # 3. 把自定义转换器添加到flask默认的转换器字典中,也就是和原来的int,float等放在一块 app.url_map.converters["re"] = RegexConverter # 4. 类似原来的路由参数限制一样,调用自定义转换器名称即可 @app.route(rule="/user/<re("w+@w+.w+"):email>") def user2(email): print(app.url_map) # 获取所有的路由列表 return email # 声明和加载配置 class Config(): DEBUG = True app.config.from_object(Config) if __name__ == "__main__": # 运行flask app.run(host="0.0.0.0")
二、HTTP请求与响应
1、请求
官方文档:
-
request:flask中代表当前请求的
request 对象
-
作用:在视图函数中取出本次请求数据
-
导入:
from flask import request
-
代码位置:
from flask.app import Request
-
常用属性
属性 说明 类型 data 记录请求体的数据,并转换为字符串
只要是通过其他属性无法识别转换的请求体数据
最终都是保留到data属性中bytes类型 form 记录请求中的html表单数据 MultiDict args 记录请求中的查询字符串,也可以是query_string MultiDict cookies 记录请求中的cookie信息 Dict headers 记录请求中的请求头 EnvironHeaders method 记录请求使用的HTTP方法 GET/POST url 记录请求的URL地址 string files 记录请求上传的文件列表 * json 记录ajax请求的json数据 json
2、获取用户请求相关的信息
- request.method
- request.args
- request.form
- request.values
- request.files
- request.cookies
- request.headers
- request.path
- request.full_path
- request.script_root
- request.url
- request.base_url
- request.url_root
- request.host_url
- request.host
获取请求中各项数据
from flask import Flask,request
# 初始化
app = Flask(import_name=__name__)
# 编写路由视图
@app.route(rule="/")
def index():
return "<h1>hello world!</h1>"
"""== 获取查询字符串 =="""
@app.route(rule="/args",methods=["post","get"])
def args():
print(request.args) # 获取查询字符串
"""
请求地址:
http://127.0.0.1:5000/args?name=xiaoming&password=123&lve=swimming&lve=shopping
打印效果:
ImmutableMultiDict([("name", "xiaoming"), ("password", "123")])
ImmutableMultiDict是一个由flask封装的字典类,在字典的基础上,提供了一些其他的方法而已。
格式:
ImmutableMultiDict([("键", "值"), ("键", "值")])
字典本身不支持同名键的,ImmutableMultiDict类解决了键同名问题
操作ImmutableMultiDict,完全可以操作字典操作,同时还提供了get,getlist方法,获取指定键的1个值或多个值
"""
print(request.args["name"]) # xiaoming
print(request.args.get("name")) # xiaoming
print(request.args.getlist("lve")) # ["swimming", "shopping"]
# 把ImmutableMultiDict转换成普通字典
print(request.args.to_dict(flat=False)) # {"name": ["xiaoming"], "password": ["123"], "lve": ["swimming", "shopping"]}
print(request.args.to_dict(flat=True)) # {"name": "xiaoming", "password": "123", "lve": "swimming"}
return "ok"
"""== 获取请求体数据 =="""
@app.route(rule="/data",methods=["post","put","patch"])
def data():
"""接受客户端发送过来的请求体数据,是request.json,request.form,request.files等无法接受的数据,全部会保留到这里"""
print(request.data) #
# 接受表单提交的数据
print(request.form) # ImmutableMultiDict([("username", "root"), ("password", "123456")])
# 接受ajax或其他客户端提交过来的json数据
print(request.json) # {"username": "root", "password": "123456"}
# 接受上传文件
avatar = request.files["avatar"] # ImmutableMultiDict([("avatar", <FileStorage: "123.jpg" ("image/jpeg")>)])
print(avatar) # <FileStorage: "123.jpg" ("image/jpeg")>
# 获取请求头信息
print( request.headers ) # 获取全部的而请求头信息
print( request.headers.get("Host") )
# 获取自定义请求头
print( request.headers.get("company") ) # oldboy
print( request.headers["company"] ) # oldboy
# 本次请求的url地址
print( request.url) # http://127.0.0.1:5000/data
print( request.path ) # /data
return "ok"
# 声明和加载配置
class Config():
DEBUG = True
app.config.from_object(Config)
if __name__ == "__main__":
# 运行flask
app.run(host="0.0.0.0")
3、响应
flask默认支持两种相应方式:
数据响应:默认响应html文本,也可以返回JSON格式,或者其他格式
# 响应html文本
from flask import make_response
@app.route("/")
def index():
# [默认支持]响应html文本
return "<img src="http://flask.pocoo.org/static/logo.png">"
return make_response("<h1>hello user</h1>") # 等同于上面的一段
# 返回JSON数据:在Flsak中可以直接使用jsonify生成一个JSON的响应
from flask import Flask, request, jsonify
# jsonify 就是json里面的jsonify
@app.route("/")
def index():
# 也可以响应json格式代码
data = [
{"id":1,"username":"liulaoshi","age":18},
{"id":2,"username":"liulaoshi","age":17},
{"id":3,"username":"liulaoshi","age":16},
{"id":4,"username":"liulaoshi","age":15},
]
return jsonify(data)
# flask中返回json 数据,都是flask的jsonify方法返回就可以了.
页面响应:
-
重定向
-
url_for 视图之间的跳转
# 重定向到百度页面 from flask import redirect # 页面跳转响应 @app.route("/user") def user(): # 页面跳转 redirect函数就是response对象的页面跳转的封装 # Location: http://www.baidu.com return redirect("http://www.baidu.com") # 重定向到自己写的视图函数:可以直接填些自己的URL路径,也可以使用url_for生成指定试图函数所对应的URL----from flask import url_for # 内容响应 @app.route("/") def index(): # [默认支持]响应html文本 # return "<img src="http://flask.pocoo.org/static/logo.png">" # 也可以响应json格式代码 data = [ {"id":1,"username":"liulaoshi","age":18}, {"id":2,"username":"liulaoshi","age":17}, {"id":3,"username":"liulaoshi","age":16}, {"id":4,"username":"liulaoshi","age":15}, ] return jsonify(data) #使用url_for可以实现视图方法之间的内部跳转 # url_for("视图方法名") @app.route("/login") def login(): return redirect( url_for("index") ) # 重定向到带有参数的视图函数:在url_for函数中传入参数 # 路由传递参数 @app.route("/user/<user_id>") def user_info(user_id): return "hello %d" % user_id # 重定向 @app.route("/demo4") def demo4(): # 使用 url_for 生成指定视图函数所对应的 url return redirect( url_for(endpoint="user",user_id=100) )
自定义http响应状态码:在Flask中,可以很方便的返回自定义状态码,可以实现不符合http协议的状态码
@app.route("/demo4")
def demo4():
return "状态码为 666", 400
"""还可以使用make_response创建Response对象,然后通过response对象返回数据"""
from flask import make_response
@app.route("/rep")
def index7():
response = make_response("ok")
print(response)
response.headers["Company"] = "oldboy" # 自定义响应头
response.status_code = 201 # 自定义响应状态码
return response
4、http的会话机制
所谓的会话,就是客户端浏览器和服务端网站之间一次完整的交互过程.
会话的开始是在用户通过浏览器第一次访问服务端网站开始.
会话的结束时在用户通过关闭浏览器以后,与服务端断开.
所谓的会话控制,就是在客户端浏览器和服务端网站之间,进行多次http请求响应之间,记录、跟踪和识别用户的信息而已。
因为 http 是一种无状态协议,浏览器请求服务器是无状态的。
无状态:指一次用户请求时,浏览器、服务器无法知道之前这个用户做过什么,每次请求都是一次新的请求。
无状态原因:浏览器与服务器是使用 socket 套接字进行通信的,服务器将请求结果返回给浏览器之后,会关闭当前的 socket 连接,而且服务器也会在处理页面完毕之后销毁页面对象。
有时需要保持下来用户浏览的状态,比如用户是否登录过,浏览过哪些商品等
实现状态保持主要有两种方式:
- 在客户端存储信息使用
Cookie,token[jwt,oauth]
- 在服务器端存储信息使用
Session
1)Cookie
Cookie是由服务器端生成,发送给客户端浏览器,浏览器会将Cookie的key/value保存,下次请求同一网站时就发送该Cookie给服务器(前提是浏览器设置为启用cookie)。Cookie的key/value可以由服务器端自己定义。
使用场景: 登录状态, 浏览历史, 网站足迹,购物车 [不登录也可以使用购物车]
Cookie是存储在浏览器中的一段纯文本信息,建议不要存储敏感信息如密码,因为电脑上的浏览器可能被其它人使用
Cookie基于域名安全,不同域名的Cookie是不能互相访问的
如访问luffy.com时向浏览器中写了Cookie信息,使用同一浏览器访问baidu.com时,无法访问到luffy.com写的Cookie信息
浏览器的同源策略针对cookie也有限制作用.
当浏览器请求某网站时,会将本网站下所有Cookie信息提交给服务器,所以在request中可以读取Cookie信息
设置cookie
设置cookie需要通过flask的Response响应对象来进行设置,由响应对象会提供了方法set_cookie给我们可以快速设置cookie信息。
from flask imoprt Flask,make_response
@app.route("/set_cookie")
def set_cookie():
resp = make_response("this is to set cookie")
resp.set_cookie("username", "xiaoming", max_age=3600)
return resp
获取cookie
from flask import Flask,request
@app.route("/get_cookie")
def resp_cookie():
resp = request.cookies.get("username")
return resp
2)Session
对于敏感、重要的信息,建议要存储在服务器端,不能存储在浏览器中,如用户名、余额、等级、验证码等信息
在服务器端进行状态保持的方案就是Session
Session依赖于Cookie,session的ID一般默认通过cookie来保存到客户端。
flask中的session需要加密,所以使用session之前必须配置SECRET_KEY选项,否则报错.
session的有效期默认是会话期,会话结束了,session就废弃了。
如果将来希望session的生命周期延长,可以通过修改cookie中的sessionID来完成配置。
注:session也可以通过一些方式做到不依赖cookie;
设置session
from flask import session
@app.route("/set_session")
def set_session():
session["username"] = "xiaoming"
return "ok!"
获取session
from flask import session
@app.route("/get_session")
def get_session():
return session.get("username")
三、请求钩子函数
在客户端和服务器交互的过程中,有些准备工作需要处理,比如:
- 在请求开始时,建立数据库连接;
- 在请求开始时,根据需求进行权限校验;
- 在请求结束时,指定数据的交互式;
为了让每个视图函数避免编写重复功能的代码,Flask提供了通用设置的功能,即请求钩子。
请求钩子是通过装饰器的形式实现,Flask支持如下四种请求钩子:
- before_first_request
- 在处理第一个请求前执行[项目初始化时的钩子]
- before_request
- 在每次请求前执行
- 如果在某修饰的函数中返回了一个响应,视图函数将不再被调用
- after_reauest
- 如果没有抛出错误,在每次请求后执行
- 接收一个参数:视图函数做出的响应
- 在此函数中可以对响应值在返回之前做最后一步修改的处理
- 需要将参数中的相应在此参数中进行返回
- teardown_request:
- 在每次请求后执行
- 接收一个参数:错误信息,如果有相关错误抛出
- 需要设置flask的配置DEBUG=False,teardown_request才会接收到异常对象
代码示例
from flask import Flask,request
# 初始化
app = Flask(import_name=__name__)
# 声明和加载配置
class Config():
DEBUG = True
app.config.from_object(Config)
@app.before_first_request
def before_first_request():
"""
这个钩子会在项目启动后第一次被用户访问时执行
可以编写一些初始化项目的代码,例如,数据库初始化,加载一些可以延后引入的全局配置
"""
print("----before_first_request----")
print("系统初始化的时候,执行这个钩子方法")
print("会在接收到第一个客户端请求时,执行这里的代码")
@app.before_request
def before_request():
"""
这个钩子会在每次客户端访问视图的时候执行
# 可以在请求之前进行用户的身份识别,以及对于本次访问的用户权限等进行判断。..
"""
print("----before_request----")
print("每一次接收到客户端请求时,执行这个钩子方法")
print("一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据")
@app.after_request
def after_request(response):
print("----after_request----")
print("在处理请求以后,执行这个钩子方法")
print("一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作")
response.headers["Content-Type"] = "application/json"
response.headers["Company"] = "python oldboy..."
# 必须返回response参数
return response
@app.teardown_request
def teardown_request(exc):
print("----teardown_request----")
print("在每一次请求以后,执行这个钩子方法")
print("如果有异常错误,则会传递错误异常对象到当前方法的参数中")
# 在项目关闭了DEBUG模式以后,则异常信息就会被传递到exc中,我们可以记录异常信息到日志文件中
print(exc)
# 编写路由视图
@app.route(rule="/")
def index():
print("-----------视图函数执行了---------------")
return "<h1>hello world!</h1>"
if __name__ == "__main__":
# 运行flask
app.run(host="0.0.0.0")
- 在第1次请求时的打印:
----before_first_request----
系统初始化的时候,执行这个钩子方法
会在接收到第一个客户端请求时,执行这里的代码
----before_request----
127.0.0.1 - - [04/Aug/2020 14:40:22] "GET / HTTP/1.1" 200 -
每一次接收到客户端请求时,执行这个钩子方法
一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据
-----------视图函数执行了---------------
----after_request----
在处理请求以后,执行这个钩子方法
一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作
----teardown_request----
在每一次请求以后,执行这个钩子方法
如果有异常错误,则会传递错误异常对象到当前方法的参数中
None
- 在第2次请求时的打印:
----before_request----
每一次接收到客户端请求时,执行这个钩子方法
一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据
127.0.0.1 - - [04/Aug/2020 14:40:49] "GET / HTTP/1.1" 200 -
-----------视图函数执行了---------------
----after_request----
在处理请求以后,执行这个钩子方法
一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作
----teardown_request----
在每一次请求以后,执行这个钩子方法
如果有异常错误,则会传递错误异常对象到当前方法的参数中
None
四、异常捕获
1、主动抛出异常
-
abort方法
- 抛出一个给定的状态码的HTTPException或者指定响应,例如想要一个页面未找到异常来终止请求,可以调用啊abort(404)
-
参数
- code – HTTP的错误状态码
# abort(404)
abort(500)
抛出状态码的话,只能抛出 HTTP 协议的错误状态码
abort在工作中基本不会被使用,工作中的异常抛出往往在业务错误的时候使用raise进行抛出错误类型,而不是抛出http异常。
abort一般用于权限等页面上错误的展示提示.
2、捕获错误
- errorhandler 装饰器
- 注册一个错误处理程序,当程序抛出指定错误状态码的时候,就会调用该装饰器所装饰的方法
- 参数:
- code_or_exception – HTTP的错误状态码或指定异常
- 例如统一处理状态码为500的错误给用户友好的提示:
@app.errorhandler(500)
def internal_server_error(e):
return "服务器搬家了"
- 捕获指定异常类型
@app.errorhandler(ZeroDivisionError)
def zero_division_error(e):
return "除数不能为0"
代码:
from flask import Flask
# 创建flask应用
app = Flask(__name__)
"""加载配置"""
class Config():
DEBUG = True
app.config.from_object(Config)
"""
flask中内置了app.errorhander提供给我们捕获异常,实现一些在业务发生错误时的自定义处理。
1. 通过http状态码捕获异常信息
2. 通过异常类进行异常捕获
"""
"""1. 捕获http异常[在装饰器中写上要捕获的异常状态码也可以是异常类]"""
@app.errorhandler(404)
def error_404(e):
return "<h1>您访问的页面失联了!</h1>"
# return redirect("/")
"""2. 捕获系统异常或者自定义异常"""
class APIError(Exception):
pass
@app.route("/")
def index():
raise APIError("api接口调用参数有误!")
return "个人中心,视图执行了!!"
@app.errorhandler(APIError)
def error_apierror(e):
return "错误: %s" % e
if __name__ == "__main__":
app.run(host="localhost",port=8080)
五、context
执行上下文:即语境,语意,在程序中可以理解为在代码执行到某一行时,根据之前代码所做的操作以及下文即将要执行的逻辑,可以决定在当前时刻下可以使用到的变量,或者可以完成的事情。
Flask中上下文对象:相当于一个容器,保存了 Flask 程序运行过程中的一些信息[变量、函数、类与对象等信息]。
Flask中有两种上下文,请求上下文(request context)和应用上下文(application context)。
-
application 指的就是当你调用
app = Flask(__name__)
创建的这个对象app
; -
request 指的是每次
http
请求发生时,WSGI server
(比如gunicorn)调用Flask.__call__()
之后,在Flask
对象内部创建的Request
对象; - application 表示用于响应WSGI请求的应用本身,request 表示每次http请求;
- application的生命周期大于request,一个application存活期间,可能发生多次http请求,所以,也就会有多个request
请求上下文(request context)
思考:在视图函数中,如何取到当前请求的相关数据?比如:请求地址,请求方式,cookie等等
在 flask 中,可以直接在视图函数中使用 request 这个对象进行获取相关数据,而 request 就是请求上下文的对象,保存了当前本次请求的相关数据,请求上下文对象有:request、session
- request
- 封装了HTTP请求的内容,针对的是http请求。举例:user = request.args.get(“user”),获取的是get请求的参数。
- session
- 用来记录请求会话中的信息,针对的是用户信息。举例:session[“name”] = user.id,可以记录用户信息。还可以通过session.get(“name”)获取用户信息。
请求上下文提供的变量/属性/方法/函数/类与对象,只能在视图中或者被视图调用的地方使用
应用上下文(application context)
它的字面意思是 应用上下文,但它不是一直存在的,它只是request context 中操作当前falsk应用对象 app 的代理(人),所谓local proxy。它的作用主要是帮助 request 获取当前的flask应用相关的信息,它是伴 request 而生,随 request 而灭的。
应用上下文对象有:current_app,g
1、current_app
应用程序上下文,用于存储应用程序中的变量,可以通过current_app.name打印当前app的名称,也可以在current_app中存储一些变量,例如:
- 应用的启动脚本是哪个文件,启动时指定了哪些参数
- 加载了哪些配置文件,导入了哪些配置
- 连接了哪个数据库
- 有哪些可以调用的工具类、常量
- 当前flask应用在哪个机器上,哪个IP上运行,内存多大
from flask import Flask,request,session,current_app,g
# 初始化
app = Flask(import_name=__name__)
# 声明和加载配置
class Config():
DEBUG = True
app.config.from_object(Config)
# 编写路由视图
@app.route(rule="/")
def index():
# 应用上下文提供给我们使用的变量,也是只能在视图或者被视图调用的地方进行使用,
# 但是应用上下文的所有数据来源于于app,每个视图中的应用上下文基本一样
print(current_app.config) # 获取当前项目的所有配置信息
print(current_app.url_map) # 获取当前项目的所有路由信息
return "<h1>hello world!</h1>"
if __name__ == "__main__":
# 运行flask
app.run(host="0.0.0.0")
2、g变量
g 作为 flask 程序全局的一个临时变量,充当者中间媒介的作用,我们可以通过它传递一些数据,g 保存的是当前请求的全局变量,不同的请求会有不同的全局变量,通过不同的thread id区别
g.name="abc" # name是举例,实际要保存什么数据到g变量中,可以根据业务而定,你可以任意的数据进去
注意:不同的请求,会有不同的全局变量g
from flask import Flask,request,session,current_app,g
# 初始化
app = Flask(import_name=__name__)
# 声明和加载配置
class Config():
DEBUG = True
app.config.from_object(Config)
@app.before_request
def before_request():
g.name = "root"
def get_two_func():
name = g.name
print("g.name=%s" % name)
def get_one_func():
get_two_func()
# 编写路由视图
@app.route(rule="/")
def index():
# 请求上下文提供的变量/属性/方法/函数/类与对象,只能在视图中或者被视图调用的地方使用
# 请求上下文里面信息来源于每次客户端的请求,所以每个视图中请求上下文的信息都不一样
# print(session)
# 应用上下文提供给我们使用的变量,也是只能在视图或者被视图调用的地方进行使用,
# 但是应用上下文的所有数据来源于于app,每个视图中的应用上下文基本一样
print(current_app.config) # 获取当前项目的所有配置信息
print(current_app.url_map) # 获取当前项目的所有路由信息
get_one_func()
return "<h1>hello world!</h1>"
if __name__ == "__main__":
# 运行flask
app.run(host="0.0.0.0")
两者区别:
- 请求上下文:保存了客户端和服务器交互的数据,一般来自于客户端。
- 应用上下文:flask 应用程序运行过程中,保存的一些配置信息,比如路由列表,程序名、数据库连接、应用信息等
六、Flask-Script扩展
这个模块的作用可以让我们通过终端来控制flask项目的运行,类似于django的manage.py
安装命令:
pip install flask-script
集成 Flask-Script到flask应用中,创建一个主应用程序,一般我们叫manage.py/run.py/main.py
都行。
from flask import Flas
app = Flask(__name__)
"""使用flask_script启动项目"""
from flask_script import Manager
manage = Manager(app)
@app.route("/")
def index():
return "hello world"
if __name__ == "__main__":
manager.run()
启动终端脚本的命令:
# 端口和域名不写,默认为127.0.0.1:5000
python run.py runserver
# 通过-h设置启动域名,-p设置启动端口
python run.py runserver -h127.0.0.1 -p8888
Flask-Script 还可以为当前应用程序添加脚本命令
1. 引入Command命令基类
2. 创建命令类必须直接或间接继承Command,并在内部实现run方法,同时如果有自定义的其他参数,则必须实现__init__
3. 使用flask_script应用对象manage.add_command对命令类进行注册,并设置调用终端别名。
"""自定义flask_script终端命令"""
from flask_script import Command
class HelloCommand(Command):
"""命令的相关描述"""
def run(self):
with open("text.txt","w") as f:
f.write("hello
hello")
pass
print("这是执行了hello命令")
manage.add_command("hello", HelloCommand() )
七、Jinja2模板引擎
Falsk内置的模板语言,它的设计思想来源于Django的模板引擎,并扩展了其语法和一系列强大的功能。
渲染模版函数
- Flask提供的 render_template 函数封装了该模板引擎
- render_template 函数的第一个参数是模板的文件名,后面的参数都是键值对,表示模板中变量对应的真实值。
1、模板基本使用
-
在flask应用对象创建的时候,设置或者保留template_folder参数,创建模板目录
默认的情况创建templates文件夹下flask可以自动找到,否则需要使用下面的方式加载
app = Flask(__name__,template_folder="templates")
-
在项目下创建
templates
文件夹,用于存放所有的模板文件,并在目录下创建一个模板html文件index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>{{title}}</h1> </body> </html>
-
在视图函数设置渲染模板并设置模板数据
from flask import Flask, render_template # 初始化 app = Flask(import_name=__name__,template_folder="templates") # 配置终端脚本运行项目 from flask_script import Manager manager = Manager(app) # 声明和加载配置 class Config(): DEBUG = True app.config.from_object(Config) # 编写路由视图 @app.route(rule="/") def index(): data={} data["title"] = "我的flask项目" return render_template("index.html",**data) if __name__ == "__main__": # 运行flask manager.run()
2、输出变量
{{}} 来表示变量名,这种 {{}} 语法叫做 变量代码块
视图代码:
@app.route("/")
def index():
data={}
data["title"] = "我的flask项目"
return render_template("index.html",**data)
模板代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
</head>
<body>
<h1>{{title}}</h1>
</body>
</html>
Jinja2 模版中的变量代码块可以是任意 Python 类型或者对象,只要它能够被 Python 的 __str__
方法或者str()转换为一个字符串就可以,比如,可以通过下面的方式显示一个字典或者列表中的某个元素:
视图代码:
from flask import Flask,render_template
from settings.dev import Config
from flask_script import Manager
"""创建flask应用"""
app = Flask(__name__,template_folder="templates")
"""使用脚手架[终端脚本]启动项目"""
manage = Manager(app)
"""加载配置"""
app.config.from_object(Config)
@app.route("/")
def index():
data = {}
data["title"] = "我的项目"
data["data_list"] = ["a","b","c"]
data["data_dict"] = {
"name":"xiaoming",
"id":100,
}
# return render_template("index.html",
# title="我的flask项目",
# data_list=data_list,
# data_dict=data_dict
# )
return render_template("index.html",**data)
if __name__ == "__main__":
manage.run()
模板代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
</head>
<body>
<div>{{title}}</div>
<div>{{list}}</div>
<div>{{list[0]}}</div>
<div>{{list.0}}</div>
<div>{{list[-1]}}</div>
<div>{{dict}}</div>
<div>{{dict["name"]}}</div>
<div>{{dict.name}}</div>
</body>
</html>
使用 {# #} 进行注释,注释的内容不会在html中被渲染出来
{# {{ name }} #}
3、模板中特有的变量和函数
你可以在自己的模板中访问一些 Flask 默认内置的函数和对象
config
你可以从模板中直接访问Flask当前的config对象:
{{config.SQLALCHEMY_DATABASE_URI}}
sqlite:///database.db
request
就是flask中代表当前请求的request对象:
{{request.url}}
http://127.0.0.1
session
为Flask的session对象,显示session数据
{{session.new}}
True
g变量
在视图函数中设置g变量的 name 属性的值,然后在模板中直接可以取出
{{ g.name }}
url_for()
url_for会根据传入的路由器函数名,返回该路由对应的URL,在模板中始终使用url_for()就可以安全的修改路由绑定的URL,则不必担心模板中渲染出错的链接:
{{url_for("home")}}
如果我们定义的路由URL是带有参数的,则可以把它们作为关键字参数传入url_for(),Flask会把他们填充进最终生成的URL中:
{{ url_for("index", post_id=1)}}
/1
代码示例:
主程序 run.py:
from flask import Flask, render_template
# 初始化
app = Flask(import_name=__name__,template_folder="templates")
# 配置终端脚本运行项目
from flask_script import Manager
manager = Manager(app)
# 声明和加载配置
class Config():
DEBUG = True
SECRET_KEY = "abc"
app.config.from_object(Config)
# 编写路由视图
@app.route(rule="/")
def index():
data={}
data["title"] = "我的项目"
data["list"] = ["a","b","c"]
data["dict"] = {
"name":"xiaoming",
"id":100,
}
return render_template("index.html",**data)
from flask import session
@app.route("/session/set")
def set_session():
session["name"] = "root"
return "ok"
if __name__ == "__main__":
# 运行flask
manager.run()
模板 templates/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
</head>
<body>
<div>{{title}}</div>
<div>{{list}}</div>
<div>{{list[0]}}</div>
<div>{{list.0}}</div>
<div>{{list[-1]}}</div>
<div>{{dict}}</div>
<div>{{dict["name"]}}</div>
{# flask模板引擎的注释 #}
{# <div>{{dict.name}}</div> #}
<div>{{config}}</div>
<div>{{config.DEBUG}}</div>
<div>name={{request.args.name}}</div>
<div>session.name={{session.name}}</div>
<div>{{config.PREFERRED_URL_SCHEME}}://{{request.headers.Host}}{{url_for("set_session")}}</div>
</body>
</html>
pycharm中设置当前项目的模板语言:
files/settings/languages & frameworks/python template languages。
4、流程控制
主要包含两个:
- if/elif /else / endif
- for / endfor
1)if语句
Jinja2 语法中的if语句跟 Python 中的 if 语句相似,后面的布尔值或返回布尔值的表达式将决定代码中的哪个流程会被执行.
用 {%%} 定义的控制代码块,可以实现一些语言层次的功能,比如循环或者if语句
视图代码:
from flask import Flask,render_template,request
from settings.dev import Config
from flask_script import Manager
"""创建flask应用"""
app = Flask(__name__,template_folder="templates")
"""使用脚手架[终端脚本]启动项目"""
manage = Manager(app)
"""加载配置"""
app.config.from_object(Config)
@app.route("/list")
def list_page():
data = {}
data["book_list"] = [
{"id":1,"price":78.50,"title":"javascript入门"},
{"id":2,"price":78.50,"title":"python入门"},
{"id":3,"price":78.50,"title":"django项目实战"}
]
data["name"] = int( request.args.get("name") )
return render_template("list.html",**data)
if __name__ == "__main__":
manage.run()
list.html,模板代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table border="1" align="center" width="680">
<tr>
<th>id</th>
<th>标题</th>
<th>价格</th>
</tr>
{# for循环 #}
{% for book in book_list %}
<tr>
<td>{{ book.id }}</td>
<td>{{ book.title }}</td>
<td>{{ book.price }}</td>
</tr>
{% endfor %}
</table>
{# 判断一个参数是否是奇数 #}
{% if name % 2 == 0 %}
偶数<br>
{% else %}
奇数<br>
{% endif %}
</body>
</html>
flask中也有过滤器,并且也可以被用在 if 语句或者for语句中:
视图代码:
from flask import Flask,render_template,request
from settings.dev import Config
from flask_script import Manager
"""创建flask应用"""
app = Flask(__name__,template_folder="templates")
"""使用脚手架[终端脚本]启动项目"""
manage = Manager(app)
"""加载配置"""
app.config.from_object(Config)
@app.route("/")
def index():
data = {}
data["title"] = "我的项目"
data["data_list"] = ["a","b","c"]
data["data_dict"] = {
"name":"xiaoming",
"id":100,
}
# return render_template("index.html",
# title="我的flask项目",
# data_list=data_list,
# data_dict=data_dict
# )
return render_template("index.html",**data)
@app.route("/list")
def list_page():
data = {}
data["book_list"] = [
{"id":1,"price":78.50,"title":"javascript入门"},
{"id":2,"price":78.50,"title":"python入门"},
{"id":3,"price":78.50,"title":"django项目实战"}
]
data["name"] = int( request.args.get("name") )
return render_template("list.html",**data)
@app.route("/filter")
def filter():
data = {}
data["text"] = "hello flask"
data["img_url"] = "<img width="300px" src="https://github.githubassets.com/images/modules/site/heroes/octocat-paper.svg">"
return render_template("fitler.html",**data)
if __name__ == "__main__":
manage.run()
filter.html,模板代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>{{ text }}</p>
<p>{{ text|upper }}</p>
<p>{{ text|length }}</p>
<p>{{ img_url }}</p>
<p>{{ img_url|safe }}</p>
{% if request.args.get("name")| int % 2 == 0 %}
<p>偶数</p>
{% else %}
<p>奇数</p>
{% endif %}
</body>
</html>
2)循环语句
- 我们可以在 Jinja2 中使用循环来迭代任何列表或者生成器函数
{% for post in posts %}
<div>
<h1>{{ post.title }}</h1>
<p>{{ post.text | safe }}</p>
</div>
{% endfor %}
- 循环和if语句可以组合使用,以模拟 Python 循环中的 continue 功能,下面这个循环将只会渲染post.text不为None的那些post:
{% for post in posts if post.text %}
<div>
<h1>{{ post.title }}</h1>
<p>{{ post.text | safe }}</p>
</div>
{% endfor %}
- 在一个 for 循环块中你可以访问这些特殊的变量:
变量 | 描述 |
---|---|
loop.index | 当前循环迭代的次数(从 1 开始) |
loop.index0 | 当前循环迭代的次数(从 0 开始) |
loop.revindex | 到循环结束需要迭代的次数(从 1 开始) |
loop.revindex0 | 到循环结束需要迭代的次数(从 0 开始) |
loop.first | 如果是第一次迭代,为 True 。 |
loop.last | 如果是最后一次迭代,为 True 。 |
loop.length | 序列中的项目数。 |
loop.cycle | 在一串序列间期取值的辅助函数。见下面示例程序。 |
- 在循环内部,你可以使用一个叫做loop的特殊变量来获得关于for循环的一些信息
- 比如:要是我们想知道当前被迭代的元素序号,并模拟Python中的enumerate函数做的事情,则可以使用loop变量的index属性,例如:
{% for post in posts%}
{{loop.index}}, {{post.title}}
{% endfor %}
- 会输出这样的结果
1, Post title
2, Second Post
- cycle函数会在每次循环的时候,返回其参数中的下一个元素,可以拿上面的例子来说明:
{% for post in posts%}
{{loop.cycle("odd","even")}} {{post.title}}
{% endfor %}
- 会输出这样的结果:
odd Post Title
even Second Post
代码示例,视图代码:
from flask import Flask, render_template
# 初始化
app = Flask(import_name=__name__,template_folder="templates")
# 配置终端脚本运行项目
from flask_script import Manager
manager = Manager(app)
# 声明和加载配置
class Config():
DEBUG = True
SECRET_KEY = "abc"
app.config.from_object(Config)
# 编写路由视图
@app.route(rule="/")
def index():
data={}
data["title"] = "我的项目"
data["book_list"] = [
{"id":10,"price":78.50,"title":"javascript入门"},
{"id":21,"price":78.50,"title":"python入门"},
{"id":33,"price":78.50,"title":"django项目实战"},
{"id":34,"price":78.50,"title":"django项目实战"},
{"id":33,"price":78.50,"title":"django项目实战"},
]
return render_template("index.html",**data)
if __name__ == "__main__":
# 运行flask
manager.run()
continue.html,模板代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
</head>
<body>
<p>判断</p>
<!--{% if request.args.name %}-->
<!-- <p>欢迎回来,{{request.args.name}}</p>-->
<!--{% endif %}-->
<!--{% if request.args.name %}-->
<!-- <p>欢迎回来,{{request.args.name}}</p>-->
<!--{% else %}-->
<!-- <p>对不起,您尚未登录</p>-->
<!--{% endif %}-->
<!--{% if request.args.name=="root" %}-->
<!-- <p>欢迎回来,您是当前网站的超级管理员~</p>-->
<!--{% elif request.args.name %}-->
<!-- <p>尊敬的用户{{request.args.name}},欢迎回来</p>-->
<!--{% else %}-->
<!-- <p>对不起,您尚未登录</p>-->
<!--{% endif %}-->
<p>循环</p>
<table border="1" align="center" width="600">
<tr>
<th>序号</th>
<th>ID</th>
<th>price</th>
<th>title</th>
</tr>
{% for book in book_list %}
{% if loop.index %2 == 0 %}
<tr bgcolor="#add8e6">
{% else %}
<tr>
{% endif %}
<td>{{ loop.index }}</td>
<td>{{ book.id }}</td>
<td>{{ book.price }}</td>
<td>{{ book.title }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>
5、过滤器
1)常见的内建滤器
-
字符串操作
-
safe:禁用转义
<p>{{ "<em>hello</em>" | safe }}</p>
-
capitalize:把变量值的首字母转成是大写,其语字母转校小写
<p>{{ "hello" | capitalize }}</p>
-
lower:把值转成小写
<p>{{ "HELLO" | lower }}</p>
-
upper:把值转成小写
<p>{{ "HELLO" | lower }}</p>
-
title:把之中的每个单词的首字母都转成大写
<p>{{ "hello" | title }}</p>
-
reverse:字符串反转
<p>{{ "olleh" | reverse }}</p>
-
format:格式化输出
<p>{{ "%s is %d" | format("name",17) }}</p>
-
striptags:渲染之前把值中所有的HTML标签都删掉
如果内容中,存在大小于号的情况,则不要使用这个过滤器,容易误删内容。
<p>{{ "<em>hello</em>" | striptags }}</p> <p>{{ "如果x<y,z>x,那么x和z之间是否相等?" | striptags }}</p>
-
truncate:字符串截断
<p>{{ "hello every one" | truncate(9)}}</p>
-
-
列表操作
-
first:取第一个元素
<p>{{ [1,2,3,4,5,6] | first }}</p>
-
last:取最后一个元素
<p>{{ [1,2,3,4,5,6] | last }}</p>
-
length:获取列表长度
<p>{{ [1,2,3,4,5,6] | length }}</p>
-
sum:列表求和
<p>{{ [1,2,3,4,5,6] | sum }}</p>
-
sort:列表排序
<p>{{ [6,2,3,1,5,4] | sort }}</p>
-
-
语句块过滤
{% filter upper %} #一大堆文字# {% endfilter %}
2)自定义过滤器
过滤器的本质是函数。当模板内置的过滤器不能满足需求,可以自定义过滤器。自定义过滤器有两种实现方式:
- 一种是通过Flask应用对象的 add_template_filter 方法
- 通过装饰器来实现自定义过滤器
重要:自定义的过滤器名称如果和内置的过滤器重名,会覆盖内置的过滤器。
需求:添加列表的过滤器
方式一:
通过调用应用程序实例的add_template_filter方法实现系定义的过滤器。该方法第一个参数是函数名,第二个参数是自定义的过滤器名称:
# 自定义过滤器
# 自定义过滤器
def do_list_reverse(old_list):
# 因为字典/列表是属于复合类型的数据,所以改动数据的结构,也会应该能影响到原来的变量
# 通过list新建一个列表进行操作,就不会影响到原来的数据
new_list = list(old_list)
new_list.reverse()
return new_list
# 注册过滤器
app.add_template_filter(do_list_reverse, "lrev")
方式二:
用装饰器来实现自定义过滤器。装饰器传入的参数是自定义的过滤器名称。
-
主程序中创建和注册过滤器
@app.template_filter("lrev") def do_list_reverse(old_list): # 因为字典/列表是属于复合类型的数据,所以改动数据的结构,也会应该能影响到原来的变量 # 通过list新建一个列表进行操作,就不会影响到原来的数据 new_list = list(old_list) new_list.reverse() return new_list @app.route(rule="/") def index(): data={} data["user_list"] = ["xiaoming","小黑白","小红"] return render_template("index.html",**data)
-
html调用过滤器
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>title</title> </head> <body> <p>{{ user_list }}</p> <p>{{ user_list | lrev }}</p> <p>{{ user_list }}</p> </body> </html>
手机进行部分屏蔽示例:
# 后端代码
from flask import Flask,render_template,request
from settings.dev import Config
from flask_script import Manager
"""创建flask应用"""
app = Flask(__name__,template_folder="templates")
"""使用脚手架[终端脚本]启动项目"""
manage = Manager(app)
"""加载配置"""
app.config.from_object(Config)
@app.template_filter("mobile")
def do_mobile(data,string):
return data[:3]+string+data[7:]
@app.route("/")
def index():
data = {}
data["user_list"] = [
{"id":1,"name":"张三","mobile":"13112345678"},
{"id":2,"name":"张三","mobile":"13112345678"},
{"id":3,"name":"张三","mobile":"13112345678"},
{"id":4,"name":"张三","mobile":"13112345678"},
]
return render_template("index2.html",**data)
if __name__ == "__main__":
manage.run()
<!-- 前端模板代码 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table border="1" align="center" width="680">
<tr>
<th>ID</th>
<th>姓名</th>
<th>手机</th>
</tr>
{% for user in user_list %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.mobile | mobile(string="****") }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>
6、模板继承
在模板中,可能会遇到以下情况:
- 多个模板具有完全相同的顶部和底部内容
- 多个模板中具有相同的模板代码内容,但是内容中部分值不一样
- 多个模板中具有相同的Html代码块内容
遇到这种情况,可以使用jinjia2模板中的继承来实现
模板继承是为了重用模板中的工共内容。一般Web开法中,继承主要使用在网站的顶部菜单、底部。这些内容可以定义在父模板中,子模版直接继承,而不需要重复书写
-
标签定义的内容
{% block top %} {% endblock %}
-
相当于在父模板中挖个坑,当子模版继承父模板时,可以进行填充
-
子模版使用extends指令声明这个模板继承自那个模板
-
父膜板中定义的块在子模版中被重新定义,在子模版中调用父模板的内容时可以使用super()
父模板代码:
base.html
{% block top %}
顶部菜单
{% endblock top %}
{% block content %}
{% endblock content %}
{% block bottom %}
底部
{% endblock bottom %}
子模板代码:
- extends指令声明这个模板继承自哪
{% extends "base.html" %}
{% block content %}
需要填充的内容
{% endblock content %}
模板继承使用时需要注意:
- 不支持多继承,支持多级继承
- 为了便于阅读,在子模版中使用extends时,尽量写在模板的第一行。
- 不能在一个模版文件中定义多个相同名字的block标签。
- 当在页面中使用多个block标签时,建议给结束标签起个名字,当多个block嵌套时,阅读性更好。
7、CSRF跨站脚本攻击
pip install flask_wtf
在Flask中,Flask-wtf扩展有一套完善的csrf防护体系,对于我们开发者来说,使用起来非常简单
-
设置应用程序的secret_key,用于加密生成的csrf_token的值
# 1. session加密的时候已经配置过了.如果没有在配置项中设置,则如下: app.secret_key = "#此处可以写随机字符串#" # 2. 也可以写在配置类中。 class Config(object): DEBUG = True SECRET_KEY = "dsad32DASSLD*13%^32" """加载配置""" app.config.from_object(Config)
-
导入flask_wtf.csrf中的CSRFProtect类,进行初始化,并在初始化的时候关联app
from flask.ext.wtf import CSRFProtect CSRFProtect(app)
-
在表单中使用CSRF令牌
<code class="lang