Flask
1.flask和python web框架介绍
python web框架介绍
本质一样,只要是web框架都会有请求对象
和响应对象
(会有路由、视图函数等东西)
django
:大而全,有很多内置和第三方的app让我们使用【主流】
flask
:小而精,没有那么多内置组件,只完成基本的web框架功能(接收请求返回视图函数执行)。只有借助第三方才能有其他丰富功能【小项目用flask】
最近兴起的异步web框架:
fastapi
:python的异步web框架,(很多公司在用) https://fastapi.tiangolo.com/zh/
sanic
:python的异步web框架,供支持异步高并发请求的web服务(3.6版本后用,但是大多都在用fastapi)
同步框架和异步框架的区别
django3.x之后就支持异步了(但是属于假的异步,并没提升多少效率)
同步框架:一个线程只处理一个请求
异步框架:一个线程可以处理多个请求
以上可以看出异步框架可以显著提高并发量
flask介绍
flask是一个基于Python开发且依赖jinja2模板和Werkzeug(基于WSGI服务的一个微型框架)
jinja2
是模板语法,与django的dtl
非常像
Werkzeug
是基于wsgi的工具包,它封装了Request、Response等(django使用的是wsgiref)
- 用wsgiref写web
from wsgiref.simple_server import make_server
# 2.wsgi规定必须传以下两个参数:http请求的东西、http响应的东西。【下面的就等同于django的框架,只不过django把environ包装成了request】
def mya(environ, start_response):
# print(environ)
start_response('200 OK', [('Content-Type', 'text/html')])
# 如果是/index则打开后读取做成response返回
if environ.get('PATH_INFO') == '/index':
with open('index.html', 'rb') as f:
data = f.read()
# 否则把Hello做成response返回
else:
data = b'<h1>Hello</h1>'
return [data] # 做成了response返回data
if __name__ == '__main__':
# 1.监听地址(不写就是127.0.0.1)、端口、可调用对象 当请求来时会执行mya(等同django中的application)加括号
myserver = make_server('', 8008, mya)
print('监听8010')
myserver.serve_forever()
- 用Werkzeug写web
需先安装flask,见flask快速使用
from werkzeug.wrappers import Request, Response
@Request.application
def hello(request):
# 2.因为里面没做路由匹配 所以访问任何地址都会返回Hello
return Response('Hello')
if __name__ == '__main__':
# 1.导入一个run_simple
from werkzeug.serving import run_simple
# 监听地址、端口、可调用对象
run_simple('localhost', 4000, hello)
总结:以前用的那些web框架其实就是别人帮我们写的一堆代码,用wsgiref
或Werkzeug
做了服务器
2.flask快速使用
安装flask
pip install flask
flask安装命令会连带安装:MarkupSafe、Werkzeug、 Jinja2、 flask
,所以如果卸载flask不要忘记卸载其他三个
flask有两个版本分别是:1.x、2.x。 两者本质上使用没区别,只是2.x源码上变更了一些东西
创建flask
其实django中帮我们写了flask的基础代码,我们可以直接使用创建
不过下面我们研究创建python文件自行写flask代码
from flask import Flask
# 需要传一个字符串,一般大家都是放双下name(反正后面也用不到)
app = Flask(__name__)
# 注册路由>>需用装饰器
@app.route('/index') # 这里可以写methods=['GET', 'POST']来表示支持GET请求和POST请求
def index():
return 'index'
@app.route('/')
def home():
return 'home'
if __name__ == '__main__':
# 以下用来区分局域网都可以访问还是只有本地才可以访问
# app.run('127.0.0.1',5000)
app.run()
'此时访问127.0.0.1:5000 访问根路径页面即可显示home,访问127.0.0.1:5000/index 即可显示index'
登录、显示用户信息案例
利用自己创建python文件写flask代码去实现登录、显示用户信息案例
步骤一:创建templates文件夹里面写login.html登录页面,用python文件写登录逻辑
【login.html】登录页面:
<body>
<form method="post">
<p>username:<input type="text" name="username"></p>
<p>password:<input type="password" name="password"></p>
<!--把要渲染的数据直接插在这里-->
<input type="submit" value="登录">{{error}}
</form>
</body>
【登录逻辑】:
注意:flask不同于django,没有request对象,但是并不代表真的没有!【只要是web框架一定有请求对象和响应对象】
flask会用一个全局的request
:from flask import Flask, request
提问:request是全局的,如果下面再写一个视图函数,那request是谁的?
回答:# 每个视图函数中的request用的是自己的request
--------------------------------------
session 同理
from flask import Flask, session
#但是如果要使用session则需要在页设置密钥
app.secret_key = 'xxxxxxxxxxx'
from flask import Flask, request, render_template, redirect, session
app = Flask(__name__)
# 配置session密钥
app.secret_key = 'xxxxxxxxxxx'
@app.route('/login', methods=['GET', 'POST'])
def index():
# 如果get请求返回login.html登录模板页面
if request.method == 'GET':
return render_template('login.html')
# post请求校验数据
else:
# 取出前端传来的用户名和密码,等同于django中的request.POST
username = request.form.get('username')
password = request.form.get('password')
if username == 'zy' and password == '123':
# 当校验通过保存登录状态到session中
session['name'] = username
# 并重定向到根路径
return redirect('/')
else:
# 校验失败则继续返回login.html登录模板页面,并提示密码错误(login.html上写了{{error}})
return render_template('login.html', error='用户名或密码错误') # 这里和django的render不同,要模板渲染的数据直接k=v
if __name__ == '__main__':
app.run()
当用户名或密码错误时则显示渲染出来的错误页面
步骤二:写登录成功后的home页面、判断是否登录成功的逻辑
【home.html】:
<body>
<h1>用户列表</h1>
<table>
{% for k,v in user_dict.items() %}
<tr>
<td>{{k}}</td>
<td>{{v.name}}</td>
<td>{{v['name']}}</td>
<td>{{v.get('name')}}</td>
<td><a href="/detail/{{k}}">查看详情</a></td>
</tr>
{% endfor %}
</table>
</body>
【校验是否登录】:
USERS = {
1: {'name': '张三', 'age': 18, 'gender': '男'},
2: {'name': '李四', 'age': 28, 'gender': '男'},
3: {'name': '王五', 'age': 18, 'gender': '女'},
}
#校验是否登录逻辑
@app.route('/')
def home():
if session.get('name'):
# 登录用户可进home页面
return render_template('home.html',user_dict=USERS)
else:
# 否则重定向到登录页面
return redirect('/login')
如果登录成功,访问首页则进入home页面,否则重定向到登录页面
步骤三:写根据id查看用户详细信息
【detail.html】
<body>
<h1>用户详情</h1>
<p>姓名:{{user.name}}</p>
<p>年龄:{{user['age']}}</p>
<p>性别:{{user.get('gender')}}</p>
</body>
【用户详细信息逻辑】
# 用户详细信息逻辑
@app.route('/detail/<int:pk>')
def detail(pk):
if session.get('name'):
# 如果登录了就可以通过点击的pk拿到该用户的详细信息
user_detail = USERS.get(pk)
return render_template('detail.html', user=user_detail)
else:
return redirect('/login')
登录后即可查看任意用户详细信息
步骤四:在页面上显示json格式
from flask import Flask, jsonify
@app.route('/test')
def test:
return jsonify({'name':'zy', 'age':18})
发给前端json格式数据
涉及新知识总结
1.注册路由:
app.route(路径, methods=[请求方式, 请求方式])
2.新手四件套
-render_template 渲染模板
-redirect 重定向
-return 返回字符传
-jsonify 返回json格式字符串 # 和django一样必须是字典或列表
3.请求的reuqest对象是全局的,直接导入使用即可#(每个视图函数中的request用的是自己的request)
-request.method 请求方式
-request.form 获取前端POST请求传来的数据 等同于request.POST 详细点就是:post请求的body体中的内容转成了字典可以.get()取值
4.session 同样也是全局的,且互不干扰。# 但是必须要指定密钥 app.secret_key = '随便写密钥'
-放值:session['xxx'] = 'yyy'
-取值:session.get('xxx')
5.模板的渲染
-兼容django的dtl
-更强大 且 可以加括号 字典可以采用.get .values() .item()....
-{% for %}也一样
6.转换器
@app.route('/detail/<int:pk>')
3.配置文件使用方式
django的配置文件叫settings
flask的配置文件有多种使用方式:
flask的所有配置都放在app中了,直接使用app对象获取配置信息即可
【方式一】:测试用
只能放debug和secret_key
app = Flask(__name__)
app.debug = True # 调试模式,错误提示信息更详细,修改完代码会自动重启
app.secret_key = 'xxxxxx' # 密钥
【方式二】:用app.config
设置
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'xxxxxxx'
打印app.config
可以看到里面的配置有哪些
【方式三】:新建一个'settings.py'里面写配置 并导入(不常用)
# 【settings.py】
DEBUG = True
SECRET_KEY = 'xxxxxx'
-------------------------------------------
# 导入配置文件使用
app.config.from_pyfile('settings.py')
【方式四】:配置文件中用类的方式区分不同配置(常用)
同样要新建一个'settings.py',不过里面写上类分别是【测试的】、【上线的】等其他
区别于django,django做不到所以才会建两个配置文件(dev、prod)
#【settings.py】 写一个父类 让两个子类分别继承 做不同修改来区分测试和上线的配置
class BASE(object):
DEBUG = False
class DevelopmentConfig(BASE):
pass
class ProductionConfig(BASE):
pass
------------------------------------------
# 直接使用不同的类来区分要用的配置
# app.config.from_object('settings.DevelopmentConfig')
app.config.from_object('settings.ProductionConfig')
【方式五】:通过环境变量配置
app.config.from_envvar('环境改变量的名字')
【方式六】:json
app.config.from_json('json文件名称')
# JSON文件名称必须是json格式,因为内部会执行json.loads
【方式七】:字典格式
app.config.from_mapping({'DEBUG':True})
额外补充:配置中心
主要在微服务中会使用:一个项目拆成N个服务,每个服务都有自己的配置文件,当把服务开在10台机器上,就需要一台一台去改配置。 用了配置中心统一去改
内置的配置字段
其他可以写自己的,如:redis的连接地址、mysql的连接地址
DEBUG # 是否开启Debug模式
SECRET_KEY # 密钥
SESSION_COOKIE_NAME # cookie过期时间
PERMANENT_SESSION_LIFETIME # session的名字
4.路由系统
1)路由的本质
django中配置路由 是在urls.py中写path,写在一个列表中
flask大多是基于装饰器
来做,很少会有抽取到urls.py里
(1)路由装饰器源码
装饰器其实就是闭包函数
装饰器的作用:不改变程序源代码和调用方式的基础上,为程序增加新功能
装饰器的本质:被装饰后再执行被装饰的函数,其实执行的不是之前的函数了,而是装饰器返回的函数。
装饰器语法糖:python的特殊语法,把被装饰的函数当作参数传给装饰器,并把装饰器的执行结果赋值给被装饰的函数
@app.route('/login')
def index():
pass
---------点进route------------------------------
def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
# rule是路径
# 其他参数都给了options
def decorator(f: T_route) -> T_route:
endpoint = options.pop("endpoint", None)
self.add_url_rule(rule, endpoint, f, **options)
return f
return decorator
---------发现route中返回了decorator---------
所以会把index当作参数传到 def decorator()中,也就是:
def decorator(index): # f就是index
endpoint = options.pop("endpoint", None) # 此时endpoint = None
# 核心:self是实例化flask得到的app对象
# app对象中有一个方法:add_url_rule,这是【在添加路由】
self.add_url_rule(rule, endpoint, f, **options)
return f
(2)flask路由的本质
就是app对象的add_url_rule
来完成路由注册
(3)自己注册路由
现在我们明白route里最重要的是在添加路由,那么我们也可以自己去写注册路由不用装饰器的方式:
def home():
return '这是自己写的路由'
# 自己写的注册路由
app.add_url_rule('/', endpoint=None, view_func=home, methods=['GET'])
当访问根路径时即可发现成功了
仔细看app.add_url_rule('/', endpoint=None, view_func=home, methods=['GET'])
会发现它很像django中的path
2)路由的参数add_url_rule
-rule # URL规则
-view_func # 视图函数名称
-defaults = None # 默认值,当URL中无参数,函数需要参数时,使用defaults = {'k':'v'}为函数提供参数
-endpoint = None # 路径的别名,用于反向解析URL,即url_for('名称')
-methods = None # 允许的请求方式,如:['GET', 'POST']
-----------以上必须记住---------------------
-strict_slashes = None # 对URL最后的/符号是否严格要求
-redirect_to = None # 重定向到指定地址
3)转换器
※ 'default': UnicodeConverter, # 不写默认就是default<pk>
'string': UnicodeConverter,
'any': AnyConverter,
※ 'path': PathConverter,
※ 'int': IntegerConverter, # <int:pk>
'float': FloatConverter,
'uuid': UUIDConverter,
4)cbv写法
基于类的视图
from flask import Flask, request
from flask.views import MethodView
app = Flask(__name__)
app.debug = True
# 写视图类继承MethodView,类中写和请求方式同名的方法
class IndexView(MethodView):
def get(self):
print(request.method)
return '这是get请求'
def post(self):
print(request.method)
return '这是post请求'
# 自己注册路由
app.add_url_rule('/index', endpoint='index', view_func=IndexView.as_view('index'))
if __name__ == '__main__':
app.run()
5)cbv源码分析
------入口还是as_view------------------------------
IndexView.as_view('index')
---1.点击as_view分析源码------------------------------
def as_view(cls, name, *class_args, **class_kwargs)
def view(**kwargs):
# 下面是加了异步,本质是在执行self.dispatch_request
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
return view
--------发现IndexView.as_view('index')执行完的结果是函数view的内存地址----------
所以当请求来了会先执行view()>>>也就是执行:self.dispatch_request
---2.点击dispatch_request分析源码---------------------------
# 注意:我们继承的是MethodView,不是View 不要找错了
def dispatch_request(self, **kwargs):
# 反射,self:视图类的对象
meth = getattr(self, request.method.lower(), None)
# 下面是加了异步,本质是在执行meth()
return current_app.ensure_sync(meth)(**kwargs)
以上源码读完发现和django没什么区别,大致一样
(1)as_view()必传参
上面我们发现as_view()中必须传一个参数name
,这个参数就是别名,即便我们有endpoint
别名,这个name也必须传
在as_view中往下看有一个view.__name__ = name
这是在修改函数的名字变成我们传进去的
# 扩展:
'路径中如果不传别名,就会用函数名作为别名'
分析源码可得:
@app.route('/index') # 如果没有传endpoint的值那就是None >>调用app.add_url_rule传入None
if endpoint is None:
#_endpoint_from_view_func 就是返回函数的名字
endpoint = _endpoint_from_view_func(view_func)
view是as_view内的内层函数:闭包函数
如果不传参数,所有人的别名(endpoint)都是内层函数view,反向解析时会报错
(2)登录认证装饰器和路由装饰器
flask的路由注册会用到装饰器,这个时候我们又写了一个登录认证的装饰器,那谁在上谁在下?
# 登录认证装饰器放在路由装饰器下面,如果放在路由装饰器上面那路由都没匹配成功你还认证个毛啊
但是有个问题:当被登录认证装饰后,视图函数就变成了装饰器的内层函数,如果在多个视图函数上写该装饰器,那所有的__name__都一样了,所以:【#加装饰器,那路由必须指定传endpoint,如果不传会报错】
(3)视图类继承谁
视图类必须继承MethodView
,如果继承View,它的dispatch_request
没有具体实现(在抛异常)还需重写,为了不那么麻烦所以我们选择继承MethodView
#【View中在抛异常 没有具体实现功能 还需要重写】
def dispatch_request(self) -> ft.ResponseReturnValue:
raise NotImplementedError()
(4)视图类属性加装饰器
在视图类属性上decorators
即可
class IndexView(MethodView):
decorators = [auth] # 装饰器
def get(self):
print(request.method)
return 'get请求'
查看decorators源码:
# cls是视图类,里面有decorators
if cls.decorators:
for decorator in cls.decorators:
view = decorator(view) # view = auth(view)
6)cbv源码总结
①as_view执行流程和django中的一样
②如果路径不传别名,那别名就是函数名
③多个视图函数加装饰器,注意和路由的上下顺序且必须传endpoint
④视图类必须继承MethodView,否则就要重写dispatch_request
⑤视图类加装饰器:写类属性decorators = [auth]
5.模板语法
以下仅作了解
模板的路径必须是【templates】
,且要和app【同级】
Flask源码中:
def __init__(
self,
....
static_folder: t.Optional[t.Union[str, os.PathLike]] = "static", # 静态文件资源
template_folder: t.Optional[t.Union[str, os.PathLike]] = "templates", # 因为这里定义了要是templates,如果我们传参数把它改为abc,那么我们模板的目录名就可以叫abc。一般不做修改 顶多改路径时会用到
)
【index.html】
<body>
<hr>
<h1>模板语法:static</h1>
<img src="/static/default.png" alt="">
<hr>
<h1>模板语法:if</h1>
{% if name %}
<h2>Hi{{name}}</h2>
{% else %}
<h2>没名字</h2>
{% endif %}
<hr>
<h1>模板语法:标签渲染</h1>
{{a|safe}} <!--渲染了标签-->
{{a}} <!--出来的是字符串-->
{{b}} <!--渲染了标签-->
<hr>
<h1>模板语法:执行函数</h1>
{{add(1,2)}} <!--django不支持该写法-->
</body>
【py】
from flask import Flask, render_template, Markup
app = Flask(__name__)
app.debug = True
def add(a, b):
return a + b
@app.route('/')
def index():
# 标签渲染,方式一:在页面上用a|safe
a = '<a href="http://www.baidu.com">点击进入百度</a>' # 不存在xss攻击,处理了
# 标签渲染,方式二:Markup包起来
b = Markup(a)
return render_template('index.html', name='zy', a=a, b=b, add=add)
if __name__ == '__main__':
app.run()
6.请求和响应
请求:全局的request对象,在不同视图函数中大胆用,不会错乱
响应:四件套(响应对象需要用make_response包一下四件套)
from flask import Flask, request, make_response, render_template
app = Flask(__name__)
app.debug = True
@app.route('/', methods=['GET', 'POST'])
def index():
# 请求
print(request.method) # 提交的方法
print(request.args) # get请求提及的数据
print(request.form) # post请求提交的数据
print(request.values) # post和get提交的数据总和
print(request.cookies) # 客户端所带的cookie
print(request.headers) # 请求头
print(request.path) # 不带域名,请求路径
print(request.full_path) # 不带域名,带参数的请求路径
print(request.url) # 带域名带参数的请求路径
print(request.base_url) # 带域名请求路径
print(request.host_url) # 域名
print(request.host) # 127.0.0.1:500
# 获取前端传来的文件并保存下来
obj = request.files['文件名']
obj.save(obj.filename)
# 响应(前面的四件套)(新手四件套都用make_response包起来)
# 1.响应头中写上cookie
# response = 'hello'
# res = make_response(response)
# # print(type(res)) # 发现res变成了响应对象
# res.set_cookie('xx','xx') # 往cookie中写值
# return res
# 2.响应头中写数据
response = render_template('index.html')
res = make_response(response)
print(type(res))
res.headers['yy'] = 'yy'
return res
if __name__ == '__main__':
app.run()
7.session及源码分析
1)session的使用
【login.html】
<body>
<form method="post">
<p>账号:<input type="text" name="name"></p>
<p>密码:<input type="password" name="pwd"></p>
<p><input type="submit" value="提交"></p>
</form>
</body>
【py】
from flask import Flask, request, session, render_template, redirect
app = Flask(__name__)
app.debug = True
app.secret_key = 'sdfafdsdf'
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
else:
name = request.form.get('name')
pwd = request.form.get('pwd')
# 只要登录成功即保存session
session['name'] = name
return redirect('/index')
@app.route('/index', methods=['GET', 'POST'])
def index():
# 如果有则拿名字,没有则拿匿名用户
return 'hello %s' % session.get('name', '匿名用户')
if __name__ == '__main__':
app.run()
此时如果在不同浏览器登录成功进入index页面 分别显示的是不同浏览器登录的用户名。证明session也一样自己是自己的不会乱
2)session源码分析
cookie:存在于客户端浏览器上的键值对
session:存在于服务端上的键值对
django:把session存放在django_session表中
flask:把它加密后放在cookie中
,如果session发生变化那cookie也会跟着变
【flask中session源码】:
app.session_interface
------1.点进session_interface发现里面配置了一个类的对象,这就是session的执行流程----------
session_interface: SessionInterface = SecureCookieSessionInterface()
------2.继续点进SecureCookieSessionInterface中查看-----------------------------------------
# 发现里面有两个重要的方法:请求来执行open_session,请求走执行save_session
# 请求来执行的方法
def open_session(self, app, request) :
#1 取出前端传入cookie的value值
val = request.cookies.get(self.get_cookie_name(app))
#2 如果没有,则构造一个空session对象
if not val:
return self.session_class()
# 取出过期时间
max_age = int(app.permanent_session_lifetime.total_seconds())
try:
# 如果没有过期,解码做成session对象,后续直接用session即可
data = s.loads(val, max_age=max_age)
return self.session_class(data)
except BadSignature:
# 如果过期了,也是空session
return self.session_class()
# 请求走执行的方法
def save_session(self, app, session, response) :
name = self.get_cookie_name(app)
# 取出过期时间,和把session加密转成字符串,放到cookie中
expires = self.get_expiration_time(app, session)
# 把session转成字符串
val = self.get_signing_serializer(app).dumps(dict(session))
response.set_cookie(
name,
val,
expires=expires,
)
假如想把session放到redis、mysql中怎么做?
只需要写个类,重写open_session,save_session自己写
其实已经有人第三方写好了,后面会讲
3)session执行原理
8.闪现
前后端混合时用的多,分离几乎不用
闪现flash:当次请求先把一些数据放在某个位置,下一次请求再把这些数据取出来,取完就没有了
闪现作用:①可以跨请求保存数据
②当次请求访问出错就重定向到其他地址,到了该地址后可以拿到当时的错误(拿了一次后里面就没有了
)
django中也有类似的东西:message框架
用法
from flask import Flask, flash, get_flashed_messages
#【设置 闪现】
-flash(f'{name}写进的闪现') # 可以设置多次,放到列表中
-flash('超时错误',category="debug") # 分类存(可设置一个标签)
#【获取 闪现】
-get_flashed_messages() # 取完就删除
-get_flashed_messages(category_filter=['debug']) # 分类取(只取该标签的)
本质就是放到session中,如果清空session再获取会发现变成空了。
from flask import Flask, request, flash, get_flashed_messages
app = Flask(__name__)
app.secret_key = 'xadafs'
# 设置闪现
@app.route('/set_flash')
def set_flash():
name = request.args.get('name')
flash(f'{name}写进的闪现')
return '设置闪现'
# 获取闪现
@app.route('/get_flash')
def get_flash():
res = get_flashed_messages()
print(res)
return '获取闪现'
if __name__ == '__main__':
app.run()
当访问http://127.0.0.1:5000/set_flash?name=zs时,再访问http://127.0.0.1:5000/get_flash就会打印出['zs写进的闪现'],当执行多次set_flash时就会存入多次。取的话是一次全取出。打开两个浏览器写入获取的不通用。
9.请求扩展
请求来、请求走中可以绑定一些函数,到这里时就会执行该函数
,类似于django的中间件。flask用请求扩展来代替django中间件
# 请求扩展都有哪些:
-before_request: # 请求来了会走,如果返回了四件套就结束了
-after_request : # 请求走了会走,一定要返回response对象
-before_first_request: # 第一次来会走
-teardown_request: # 无论是否出异常,都会走
-errorhandler: # 监听状态码,404 500
-------' 以下忽略即可'----------
-template_global: # 标签
-template_filter: # 过滤器
1)before_request请求来
2)after_request请求走
# 请求来执行一个函数(来是从上往下执行)
@app.before_request
def before():
print('我来了')
# 请求走执行一个函数(走是从下往上执行)
@app.after_request
def after(response): # 必须返回response
print('我走了')
return response
当有多个时 请求1来了>>请求2来了>>请求2走了>>请求1走了
当请求来时返回的不再是None,说明被拦截了就不让走了 请求1来了>>请求2走了>>请求1走了
3)before_first_request第一次请求来
因为没人用,已经被弃用了
# 项目启动后的第一次请求来才执行
@app.before_first_request
def first():
print('第一次请求才执行')
4)teardown_request无论是否出错都走
常用在记录错误日志
@app.teardown_request
def teardown(e):
print(e)
print('不管有没有错我都执行')
5)errorhandler监听响应状态码
监听响应状态码,只有符合监听的状态码才会走
@app.errorhandler(404)
def error_404(arg):
return '监听到404我才走'
6)template_global标签
@app.template_global()
def add(a1, a2):
return a1 + a2
7)template_filter过滤器
@app.template_filter()
def db(a1, a2, a3):
return a1 + a2 + a3
10.蓝图
蓝图(blueprint):对程序进行目录结构划分
1)不用蓝图划分目录
├── no_blueprint_flask # 项目名
├── src # 核心代码位置
├── __init__.py # 包:里面实例化得到app对象
├── models.py # 表模型
└── views.py # 视图函数
├── static # 静态文件资源
├── template # 模板
└── manage.py # 启动文件
蓝图的使用步骤
# 第一步:导入蓝图类
from flask import Blueprint
# 第二步:实例化得到蓝图对象,可以指定static和templates
user_bp=Blueprint('user',__name__)
# 第三步:在不同的views.py中使用蓝图注册路由
@us.route('/user')
def userinfo():
pass
-----------------------------------------------------
# 第四步:app注册蓝图,可以指定前缀
app.register_blueprint(us)
①蓝图可以有自己的静态文件和模板
②注册蓝图时可以使用前缀
,必须以/
开头 url_prefix='/前缀'
2)用蓝图划分中小型项目
├── little_blueprint_flask # 项目名
├── src # 核心代码
├── static # 静态文件
└── 1.png # 图片
├── templates # 模板文件
└── user.html # 模板
├── views # 视图函数
└── user.py # 用户视图
├── __init__.py # 包:里面实例化得到app对象
└── models.py # 表模型
└── manage.py # 启动文件
views>>user.py
# 1.导入蓝图类
from flask import Blueprint, render_template
# 2.实例化得到蓝图类对象
user_bp = Blueprint('user', __name__)
# 3.使用蓝图注册路由
@user_bp.route('/user')
def userinfo():
return render_template('user.html', name='zy')
src>>__init__.py
from flask import Flask
app = Flask(__name__)
app.debug = True
app.secret_key = 'sdfaf'
# 4.注册蓝图
from .views.user import user_bp
app.register_blueprint(user_bp)
manage.py启动
from src import app
if __name__ == '__main__':
app.run()
3)用蓝图划分大型项目
有多个app
├── big_blueprint_flask # 项目名
└── src # 核心文件
├── admin # admin的app
├── static # 静态文件
├── templates # 模板文件目录
└── admin.html # 模板文件
├── __init__.py # 包
├── models.py # 表模型
└── views.py # 视图函数
├── home # home的app
├── __init__.py # 包
└── settings.py # 配置文件
└── manage.py # 启动文件
src>>admin>>__init__.py
# 1.导入蓝图类
from flask import Blueprint
# 2.实例化得到蓝图对象,并用自己的模板和静态资源
admin_bp = Blueprint('admin', __name__, template_folder='templates', static_folder='static')
from . import views
src>>admin>>views.py
from flask import render_template
from . import admin_bp
# 3.使用蓝图类注册路由
@admin_bp.route('/admin')
def admin():
return render_template('admin.html')
src>>__init__.py
from flask import Flask
app = Flask(__name__)
app.config.from_pyfile('settings.py')
# 注册蓝图
from .admin import admin_bp
from .home import home_bp
# 加上url的前缀
app.register_blueprint(admin_bp, url_prefix='/admin')
app.register_blueprint(home_bp, url_prefix='/home')
'加了前缀后访问127.0.0.1:5000/前缀/路由 即可访问'
11.g对象
1)什么是g对象
from flask import g
g对象就是global
的缩写,因为它在python中是个关键字,不能以关键字做变量名
,所以用了g
g对象就是专门用来存储用户信息
的。
g对象:当此请求的全局对象
,可以在请求中放值
和取值
。
它是全局
的,在任意位置
都可以导入使用
这个在其他框架中叫context上下文
,django用request
2)为何不用request做上下文
# 为什么不学django用request作为上下文?
因为使用request可能会造成request'数据污染',容易不小心改了request的属性而我们察觉不到
所以建议用g,它是空的。放入后在当次请求中全局优先
今后想在当次请求中放一些数据后面再使用,就可以使用g对象
3)g对象和session的区别
g对象只针对'当次请求',这次请求放就有,下次请求不放就没有
session针对'多次请求',这次请求放就有,下次请求不放还有
session对象是可以跨request的,只要session还未失效,不同的request的请求会获取到同一个session
但是g对象不是,g对象不需要管过期时间,请求一次就g对象就改变了一次,或者重新赋值了一次
4)代码操作
from flask import Flask, g, request
app = Flask(__name__)
app.debug = True
def add(a, b):
print(g.name)
return a + b
@app.route('/')
def index():
# 从url?后取出name
name = request.args.get('name')
g.name = name
res = add(1, 2)
print(res)
return 'index'
if __name__ == '__main__':
app.run()
12.数据库连接池
django中没有数据库连接池,可以使用第三方
1)pymysql连接数据库
pymysql,在一个视图函数中创建一个连接对象操作,操作完就关闭连接。
连接对象不要用单例,可能会出现数据错乱问题
(1)不用数据库连接池
推导一:在视图函数中创建pymysql的连接操作数据,操作完关闭连接返回给前端
from flask import Flask,jsonify
import pymysql
app = Flask(__name__)
app.debug = True
@app.route('/article')
def article():
# 从mysql的cnblogs库中的article表中取出数据
# 连接mysql服务端
conn = pymysql.connect(
user='root',
password="",
host='127.0.0.1',
database='cnblogs',
port=3306,
)
# 产生游标对象(数据组成列表套字典格式,不写就是列表套元组)
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 发给服务端sql语句:拿两条数据
cursor.execute('select id,title from article limit 2')
# 获取sql语句执行结果
res = cursor.fetchall()
# print(res)
return jsonify(res)
if __name__ == '__main__':
app.run()
当访问http://127.0.0.1:5000/article 即可在前端显示拿到的数据
这样做有问题:来一个请求创建一个连接,请求结束连接关闭(django就是这样做的)
推导二:把连接对象做成全局,在视图函数中使用全局的连接查询、返回给前端
from flask import Flask, jsonify
import pymysql
app = Flask(__name__)
app.debug = True
# 连接mysql服务端
conn = pymysql.connect(
user='root',
password="",
host='127.0.0.1',
database='cnblogs',
port=3306,
)
@app.route('/article')
def article():
# 从mysql的cnblogs库中的article表中取出数据
# 产生游标对象(数据组成列表套字典格式,不写就是列表套元组)
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 发给服务端sql语句:拿两条数据
cursor.execute('select id,title from article limit 2')
# 获取sql语句执行结果
res = cursor.fetchall()
# print(res)
return jsonify(res)
if __name__ == '__main__':
app.run()
当访问http://127.0.0.1:5000/article 也可在前端显示拿到的数据
这样做有问题:会出现数据错乱,如果同时来两个请求分别查两个表,当取的时候请求1可能取得是请求2的数据,请求2拿的时候发现是空的
(2)用数据库连接池
为了解决上面两种遇到的问题,采用数据库连接池
第一步:# 创建一个全局的池
第二步:# 每次进入视图函数,从池中取一个连接使用。使用完就放回到池中,只要控制池的大小就能控制mysql的连接数
这里我们采用第三方数据库连接池操作:dbutils
# 安装
pip install dbutils
# 步骤
1.导入,实例化得到一个pool对象
2.pool做成单例以模块导入
3.从pool池中拿到一个连接 'pool.connection()'
4.使用连接获得游标:'cursor',使用游标操作数据库
新建POOL.py
from dbutils.pooled_db import PooledDB
import pymysql
pool = PooledDB(
creator=pymysql, # 使用链接数据库的模块
maxconnections=6, # 连接池允许的最大连接数,0和None表示不限制连接数
mincached=2, # 初始化时,链接池中至少创建的空闲的链接,0表示不创建
maxcached=5, # 链接池中最多闲置的链接,0和None不限制
maxshared=3,
# 链接池中最多共享的链接数量,0和None表示全部共享。PS: 无用,因为pymysql和MySQLdb等模块的 threadsafety都为1,所有值无论设置为多少,_maxcached永远为0,所以永远是所有链接都共享。
blocking=True, # 连接池中如果没有可用连接后,是否阻塞等待。True,等待;False,不等待然后报错
maxusage=None, # 一个链接最多被重复使用的次数,None表示无限制
setsession=[], # 开始会话前执行的命令列表。如:["set datestyle to ...", "set time zone ..."]
ping=0,
# ping MySQL服务端,检查是否服务可用。# 如:0 = None = never, 1 = default = whenever it is requested, 2 = when a cursor is created, 4 = when a query is executed, 7 = always
host='127.0.0.1',
port=3306,
user='root',
password='123456',
database='s8day127db',
charset='utf8'
)
from flask import Flask, jsonify
import pymysql
app = Flask(__name__)
app.debug = True
# 导入池
from .POOL import pool
# 使用池
@app.route('/article_pool')
def article_pool():
# 从池中拿一个链接
conn = pool.connection()
# 产生游标对象(数据组成列表套字典格式,不写就是列表套元组)
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 发给服务端sql语句:拿两条数据
cursor.execute('select id,title from article limit 2')
# 获取sql语句执行结果
res = cursor.fetchall()
# print(res)
return jsonify(res)
if __name__ == '__main__':
app.run()
2)对两种压力测试
用连接池和不用连接池
from flask import Flask, jsonify
import pymysql
app = Flask(__name__)
app.debug = True
from .POOL import pool
import time
import random
# 使用池
@app.route('/article_pool')
def article_pool():
# 从池中拿一个链接
conn = pool.connection()
# 产生游标对象(数据组成列表套字典格式,不写就是列表套元组)
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 发给服务端sql语句:拿两条数据
cursor.execute('select id,title from article limit 2')
# 获取sql语句执行结果
res = cursor.fetchall()
# print(res)
return jsonify(res)
# 不使用池
@app.route('/article')
def article():
# 连接mysql服务端
conn = pymysql.connect(
user='root',
password="",
host='127.0.0.1',
database='bbs',
port=3306,
)
# 产生游标对象(数据组成列表套字典格式,不写就是列表套元组)
cursor = conn.cursor(pymysql.cursors.DictCursor)
time.sleep(random.randint(1, 3))
# 发给服务端sql语句:拿两条数据
cursor.execute('select id,title from article limit 2')
# 获取sql语句执行结果
res = cursor.fetchall()
# 关闭
cursor.close()
conn.close()
# print(res)
return jsonify(res)
if __name__ == '__main__':
app.run()
新建s1.py开启线程测试
from threading import Thread
import requests
def task():
res = requests.get('http://127.0.0.1:5000/article_pool')
print(len(res.text))
if __name__ == '__main__':
# 启动线程
for i in range(500):
t = Thread(target=task)
t.start()
# 查看数据库连接数
show status like 'Threads%'
结果
# 使用池的连接数明显【小】
# 不使用池连接数明显很【大】
13.请求上下文分析(源码:request原理)
1)pipreqs导出项目依赖
之前的用法:
pip freeze >requirements.txt
# 它会把当前解释器环境下所有的第三方依赖导出来,不好用!
现在:用第三方模块精确导出该项目的依赖pipreqs
# 安装
pip install pipreqs
使用pipreqs:
1.使用命令导出依赖:pipreqs ./
# win由于编码问题可能会出错。建议:pipreqs ./--encoding=utf8
# mac、linux不会出错
2.此时就会在项目根路径下生成:requirements.txt 依赖文件
2)函数和方法
只要会自动传值
那就是方法
【函数】:就是普通的函数,有几个参数就要传几个参数
【方法】:绑定给对象的方法、绑定给类的方法,绑定给谁就由谁来调用,会自动把自身当作参数传入
动静态方法:
【绑定给对象的方法】:(类中直接写函数默认是绑定给对象的方法
)类也可以调用,但是需要手动把对象当作第一个参数传入
【绑定给类的方法】:(被@classmethod修饰的函数默认是绑定给类的方法
)对象也可以调用,会自动把类传入
被@staticmethod修饰的函数
会变成普通的函数,无论对象调用还是类调用,有多少参数就传多少参数
MethodType # 检查一个对象是不是【方法】
FunctionType # 检查一个对象是不是【函数】
isinstance(obj , cls) # 判断obj对象,是不是cls类的对象
issubclass(cls1,cls) # 判断一个cls1,是不是cls的子类
from types import MethodType, FunctionType
class Foo(object):
def objTask(self):
pass
@classmethod
def clsTask(cls):
pass
@staticmethod
def task1():
pass
"""
对象调用绑定给对象的方法(是方法)
"""
obj = Foo()
print(isinstance(obj.objTask, MethodType)) # true
"""
类调用绑定给对象的方法(会变成普通函数)
"""
print(isinstance(Foo.objTask, MethodType)) # false
"""
类调用绑定给类的方法(是方法)
"""
print(isinstance(Foo.clsTask, MethodType)) # true
"""
对象调用绑定给类的方法(是方法)
"""
print(isinstance(obj.clsTask, MethodType)) # true
"""
对象和类调用被静态方法修饰的方法(是函数)
"""
print(isinstance(obj.task1, FunctionType)) # true
print(isinstance(Foo.task1, FunctionType)) # true
3)threading.local对象
local对象
在并发编程时,多个线程操作同一个变量会出现并发安全问题
(你在改我也在改),所以需要加锁
。
使用local对象多线程并发时,则不需要加锁,也不会出现数据错乱,这是threading模块给我们提供的local
对象
local对象本质原理
多个线程修改同一数据,会复制多份变量给每个线程用并为每个线程开一块空间
进行数据存储
,每个线程操作自己那部分数据
多线程遇到io操作数据错乱问题
from threading import Thread, get_ident
import time
num = -1
def task(arg):
global num
num = arg
time.sleep(2) # 如果让睡2秒,等cpu再调度该线程执行时会发现num都变成了9,造成了数据错乱
print('线程id号:%s' % get_ident(), num)
# 开启10个线程
for i in range(10):
t = Thread(target=task, args=(i,))
t.start()
(1)互斥锁解决并发问题
from threading import Thread, get_ident
from threading import Lock
import time
num = -1
l = Lock() # 生成锁
def task(arg):
l.acquire() # 获得锁
global num
num = arg
time.sleep(2)
print('线程id号:%s' % get_ident(), num)
l.release() # 释放锁
# 开启10个线程
for i in range(10):
t = Thread(target=task, args=(i,))
t.start()
这样虽然数据不会错乱 但是加锁的地方
变成了串行 本来2秒结束的变成了20秒
(2)local解决并发问题
local不会出现并发数据错乱问题
from threading import Thread, get_ident
from threading import local
import time
num = local()
def task(arg):
num.arg = arg
time.sleep(2)
print('线程id号:%s' % get_ident(), num.arg)
for i in range(10):
t = Thread(target=task, args=(i,))
t.start()
虽然打印结果窜行了,但是每个线程用的都是自己的数据,没有造成数据错乱全是9的情况
4)偏函数
偏函数的作用就是可以提前传值
,比如我函数需要有三个参数,但是我只知道一个另外两个需要一会才能知道,这样就可以用偏函数提前把第一个参数传进去,后面知道了另外两个参数然后再传它们两个
from functools import partial
def add(a, b, c):
return a + b + c
# print(add(1, 2, 3)) # 如果少传会报错
# 利用偏函数先传一个值进去
add = partial(add, 1)
# 这里可以去执行其他代码 让程序先不报错
# 然后再传另外两个值
print(add(2, 3))
5)flask生命周期流程(源码分析)
# 请求来了---》app()----->Flask.__call__--->self.wsgi_app(environ, start_response)
def wsgi_app(self, environ, start_response):
# environ:http请求拆成了字典
# ctx对象:RequestContext类的对象,对象里有:当次的requets对象,app对象,session对象
ctx = self.request_context(environ)
error = None
try:
try:
#ctx RequestContext类 push方法
ctx.push()
# 匹配成路由后,执行视图函数
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
# RequestContext :ctx.push
def push(self):
# _request_ctx_stack = LocalStack() ---》push(ctx对象)--》ctx:request,session,app
_request_ctx_stack.push(self)
#session相关的
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(self.app, self.request)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
# 路由匹配相关的
if self.url_adapter is not None:
self.match_request()
# LocalStack() push --->obj 是ctx对象
def push(self, obj):
#self._local _local 就是咱们刚刚自己写的Local的对象---》LocalStack的init初始化的_local---》self._local = Local()---》Local对象可以根据线程协程区分数据
rv = getattr(self._local, "stack", None)
# 一开始没有值
if rv is None:
rv = []
self._local.stack = rv # self._local.stack 根据不同线程用的是自己的数据
rv.append(obj) # self._local.stack.append(obj)
# {'线程id号':{stack:[ctx]},'线程id号2':{stack:[ctx]}}
return rv
# 再往后执行,就会进入到路由匹配,执行视图函数
# request = LocalProxy(partial(_lookup_req_object, "request"))
# LocalProxy 代理类---》method---》代理类去当前线程的stack取出ctx,取出当时放进去的request
视图函数中:print(request.method)
# print(request) 执行LocalProxy类的__str__方法
# request.method 执行LocalProxy类的__getattr__
def __getattr__(self, name): #name 是method
# self._get_current_object() 就是当次请求的request
return getattr(self._get_current_object(), name)
# LocalProxy类的方法_get_current_object
def _get_current_object(self):
if not hasattr(self.__local, "__release_local__"):
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError("no object bound to %s" % self.__name__)
# self.__local 是在 LocalProxy 类实例化的时候传入的local
# 在这里实例化的:request = LocalProxy(partial(_lookup_req_object, "request"))
# local 是 partial(_lookup_req_object, "request")
#_lookup_req_object ,name=request
def _lookup_req_object(name):
top = _request_ctx_stack.top # 取出了ctx,是当前线程的ctx
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name) #从ctx中反射出request,当次请求的request
请求上下文执行流程(ctx):
-0 flask项目一启动,有6个全局变量
-_request_ctx_stack:LocalStack对象
-_app_ctx_stack :LocalStack对象
-request : LocalProxy对象
-session : LocalProxy对象
-1 请求来了 app.__call__()---->内部执行:self.wsgi_app(environ, start_response)
-2 wsgi_app()
-2.1 执行:ctx = self.request_context(environ):返回一个RequestContext对象,并且封装了request(当次请求的request对象),session,flash,当前app对象
-2.2 执行: ctx.push():RequestContext对象的push方法
-2.2.1 push方法中中间位置有:_request_ctx_stack.push(self),self是ctx对象
-2.2.2 去_request_ctx_stack对象的类中找push方法(LocalStack中找push方法)
-2.2.3 push方法源码:
def push(self, obj):
#通过反射找self._local,在init实例化的时候生成的:self._local = Local()
#Local(),flask封装的支持线程和协程的local对象
# 一开始取不到stack,返回None
rv = getattr(self._local, "stack", None)
if rv is None:
#走到这,self._local.stack=[],rv=self._local.stack
self._local.stack = rv = []
# 把ctx放到了列表中
#self._local={'线程id1':{'stack':[ctx,]},'线程id2':{'stack':[ctx,]},'线程id3':{'stack':[ctx,]}}
rv.append(obj)
return rv
-3 如果在视图函数中使用request对象,比如:print(request)
-3.1 会调用request对象的__str__方法,request类是:LocalProxy
-3.2 LocalProxy中的__str__方法:lambda x: str(x._get_current_object())
-3.2.1 内部执行self._get_current_object()
-3.2.2 _get_current_object()方法的源码如下:
def _get_current_object(self):
if not hasattr(self.__local, "__release_local__"):
#self.__local() 在init的时候,实例化的,在init中:object.__setattr__(self, "_LocalProxy__local", local)
# 用了隐藏属性
#self.__local 实例化该类的时候传入的local(偏函数的内存地址:partial(_lookup_req_object, "request"))
#加括号返回,就会执行偏函数,也就是执行_lookup_req_object,不需要传参数了
#这个地方的返回值就是request对象(当此请求的request,没有乱)
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError("no object bound to %s" % self.__name__)
-3.2.3 _lookup_req_object函数源码如下:
def _lookup_req_object(name):
#name是'request'字符串
#top方法是把第二步中放入的ctx取出来,因为都在一个线程内,当前取到的就是当次请求的ctx对象
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
#通过反射,去ctx中把request对象返回
return getattr(top, name)
-3.2.4 所以:print(request) 实质上是在打印当此请求的request对象的__str__
-4 如果在视图函数中使用request对象,比如:print(request.method):实质上是取到当次请求的reuquest对象的method属性
-5 最终,请求结束执行: ctx.auto_pop(error),把ctx移除掉
其他的东西:
-session:
-请求来了opensession
-ctx.push()---->也就是RequestContext类的push方法的最后的地方:
if self.session is None:
#self是ctx,ctx中有个app就是flask对象, self.app.session_interface也就是它:SecureCookieSessionInterface()
session_interface = self.app.session_interface
self.session = session_interface.open_session(self.app, self.request)
if self.session is None:
#经过上面还是None的话,生成了个空session
self.session = session_interface.make_null_session(self.app)
-请求走了savesession
-response = self.full_dispatch_request() 方法内部:执行了before_first_request,before_request,视图函数,after_request,savesession
-self.full_dispatch_request()---->执行:self.finalize_request(rv)-----》self.process_response(response)----》最后:self.session_interface.save_session(self, ctx.session, response)
-请求扩展相关
before_first_request,before_request,after_request依次执行
-flask有一个请求上下文,一个应用上下文
-ctx:
-是:RequestContext对象:封装了request和session
-调用了:_request_ctx_stack.push(self)就是把:ctx放到了那个位置
-app_ctx:
-是:AppContext(self) 对象:封装了当前的app和g
-调用 _app_ctx_stack.push(self) 就是把:app_ctx放到了那个位置
-g是个什么鬼?
专门用来存储用户信息的g对象,g的全称的为global
g对象在一次请求中的所有的代码的地方,都是可以使用的
-代理模式
-request和session就是代理对象,用的就是代理模式
14.wtforms(了解)
django中有forms组件:生成前端模板、校验数据、渲染错误信息
flask中用第三方wtforms
可实现和django的forms一样的功能
由于以后都是前后端分离,所以这里仅作了解
# 安装
pip install wtforms
1.导入,定义一个类继承forms
2.模板中, for循环生成模板
3.视图函数中,使用form校验数据
py
from flask import Flask, render_template, request, redirect
from wtforms import Form
from wtforms.fields import simple
from wtforms import validators
from wtforms import widgets
app = Flask(__name__, template_folder='templates')
app.debug = True
class LoginForm(Form):
# 字段(内部包含正则表达式)
name = simple.StringField(
label='用户名',
validators=[
validators.DataRequired(message='用户名不能为空.'),
validators.Length(min=6, max=18, message='用户名长度必须大于%(min)d且小于%(max)d')
],
widget=widgets.TextInput(), # 页面上显示的插件
render_kw={'class': 'form-control'}
)
# 字段(内部包含正则表达式)
pwd = simple.PasswordField(
label='密码',
validators=[
validators.DataRequired(message='密码不能为空.'),
validators.Length(min=8, message='用户名长度必须大于%(min)d'),
validators.Regexp(regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}",
message='密码至少8个字符,至少1个大写字母,1个小写字母,1个数字和1个特殊字符')
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
form = LoginForm()
return render_template('login.html', form=form)
else:
form = LoginForm(formdata=request.form)
if form.validate():
print('用户提交数据通过格式验证,提交的值为:', form.data)
else:
print(form.errors)
return render_template('login.html', form=form)
if __name__ == '__main__':
app.run()
templates>>login.html
<body>
<h1>登录</h1>
<form method="post" novalidate>
<p>{{form.name.label}}: {{form.name}} {{form.name.errors[0] }}</p>
<p>{{form.pwd.label}} {{form.pwd}} {{form.pwd.errors[0] }}</p>
<input type="submit" value="提交">
</form>
</body>
标签:__,name,框架,Flask,app,request,session,self
From: https://www.cnblogs.com/mmxka/p/17298780.html