一、Flask框架
1、Flask介绍
1、Flask相当于一个内核,只提供Werkzeug(路由模块)和Jinja2引擎这两个核心,除此之外的几乎所有的功能都需要用第三方扩展来实现。
2、框架轻重
重量级的框架:为方便业务程序的开发,提供了丰富的工具、组件,如Django。
轻量级的框架:只提供Web框架的核心功能,自由、灵活、高度定制,如Flask、Tornado。
3、实现helloworld:
# 导入Flask类
from flask import Flask
#Flask类接收一个参数__name__
app = Flask(__name__)
# 添加参数后
# app = Flask(__name__, static_url_path='/url_path_param', static_folder='folder_param')
# 装饰器的作用是将路由映射到视图函数index
@app.route('/')
def index():
return 'Hello World'
# Flask应用程序实例的run方法启动WEB服务器
if __name__ == '__main__':
app.run()
注意:__name__表示当前文件(模块)的名字,用于指明项目目录即当前文件(模块)所在的目录,flask会默认在该目录下的static包中查找静态文件,在templates包中查找模板文件,不需要额外指明。
4、flask中的三大参数:Flask对象初始化参数、应用程序配置参数、app.run 参数。
应用程序配置参数:
设置参数的方式 | 优点 | 缺点 |
| 可以继承复用 | 敏感数据暴露 |
| 独立文件,可保护敏感数据 | 不能继承,文件路径固定不灵活 |
| 保护敏感数据,文件路径固定灵活 | 不方便,记得设置环境变量 |
项目中的实战应用:在配置对象中设置所有的参数,在环境变量设置真实的敏感的变量信息进行同名覆盖,即利用环境变量设置配置文件在服务器中的绝对路径,再加载配置文件中的参数。
5、设计模式之工厂模式:将初始化实例对象的代码封装到一个函数当中,该函数即工厂
def create_flask_app(config):
"""
创建Flask应用
:param config: 配置对象
:return: Flask应用
"""
app = Flask(__name__)
app.config.from_object(config)
# 从环境变量指向的配置文件中读取的配置信息会覆盖掉从配置对象中加载的同名参数
app.config.from_envvar("PROJECT_SETTING", silent=True)
return app
class DefaultConfig(object):
"""默认配置"""
SECRET_KEY = 'itcast1'
app = create_flask_app(DefaultConfig)
注意:config中默认DEBUG为False,开发时要先设为True。
6、命令行启动flask方式
win命令行中:先将app.run()注释掉,在终端执行flask run -h ip -p port 即可;
linux中:
$ export FLASK_APP=helloworld # helloworld是文件名
$ export FLASK_ENV=production 运行在生产模式,未指明则默认为此方式
$ export FLASK_ENV=development # 运行在开发模式
$ flask run [-h 0.0.0.0 -p 8000]
pycharm中(重点使用):
注意:flask run 等价于 python -m flask run。
2、路由和蓝图
1、请求方式之OPTIONS:简化版的get请求,用于询问服务器接口信息。
在解决cors跨域请求中的应用:比如要在www.meiduo.site中访问api.meiduo.site/user/1,首先浏览器发送OPTIONS请求给api.meiduo.site/user/1,allow-origin包含了白名单的中允许的域名,如果在白名单中则返回response允许访问的域名,如果不在则返回报错信息。所以跨域请求会发两次,即先发一次OPTIONS请求,django-cors-header将对该处理该请求的视图封装在中间件中。
注意:支持options请求不等价于实现了cors跨域解决方案。
2、请求方式之HEAD:也是简化版的GET请求,只返回GET请求处理时的响应头,不返回响应体。
注意:如果浏览器和视图路由的请求方式不一致的话会报错405。
3、flask中的app和django中的startproject(项目)对等,flask中的蓝图和django中的startapp(子应用)对等 。
4、__init__.py 控制着包的导入行为。假如 __init__.py 为空,那么仅仅导入包是什么都做不了的。当你将一个包作为模块导入的时候,实际上导入了它的 __init__.py 文件。__init__.py 中还有一个重要的变量,叫做 __all__。我们有时会使出一招“全部导入”,也就是这样:
from PackageName import *
__all__ = ["Module1", "Module2", "subPackage1", "subPackage2"]
即,只有存在于init中的数据才能被别的地方导入并且在import导包时会执行init.py文件。
init导包在蓝图中的应用:
# 新建一个goods包
# 在包中的init.py文件中
from flask import Blueprint
goods_bp = Blueprint('goods', __name__) # __name__指定static和template目录在当前py文件所在目录在查找
from . import views
# 在该包下新建views.py文件
from . import goods_bp
@goods_bp.route('/goods')
def get_goods():
return 'get_goods'
# 在主程序app.py中注册
from goods import goods_bp
app.register_blueprint(goods_bp)
注意:在init.py文件最后加from . import views是为了让主程序在执行init.py文件时能导入views.py文件,从而遍历到views.py中的函数。
5、注意:和应用对象不同,蓝图对象创建时不会默认注册静态目录的路由。
6、rule.endpoint表示视图函数,rule.rule表示路由路径。
for rule in app.url_map.iter_rules():
print('name={} path={}'.format(rule.endpoint, rule.rule))
7、Django Debug:① 支持静态文件 ② 后端出现错误会直接返回完整错误给前端 ③ 修改代码后自动重启服务器;Flask Debug中没有第一条。
3、请求和响应
1、处理请求
请求报文格式:
GET /user/1?id=1 HTTP/1.1
Content-Type:applications/json
...
body --> file form json xml
提取路由参数:采用转换器语法<变量名>,和django path中的相同。
注意:Flask中的return后写响应的body(如data),Flask框架自动执行make_response(data),且data只能是str类型的数据。
Flask中默认的转换器:string(传入的数据就是str类型,默认不作转换)、any、path、int、float、uuid。
自定义转换器:
from werkzeug.routing import BaseConverter
class MobileConverter(BaseConverter):
"""
手机号格式
"""
regex = r'1[3-9]\d{9}' # regex指明正则匹配的规则,该变量名不能改
# 注册
app.url_map.converters['mobile'] = MobileConverter
获取其他参数:request对象在Flask中是作为全局变量存在,需要在from flask import request,request对象包括以下属性
属性 | 说明 | 类型 |
---|---|---|
data | 记录请求的数据,并转换为字符串 | * |
form | 记录请求中的表单数据 | MultiDict |
args | 记录请求中的查询参数,即get传参 | MultiDict |
cookies | 记录请求中的cookie信息 | Dict |
headers | 记录请求中的报文头 | EnvironHeaders |
method | 记录请求使用的HTTP方法 | GET/POST |
url | 记录请求的URL地址 | string |
files | 记录请求上传的文件 | * |
from flask import request
@app.route('/articles')
def get_articles():
channel_id = request.args.get('channel_id')
return 'you wanna get articles of channel {}'.format(channel_id)
获取文件数据并保存:文件操作原生方法是read和write,flask框架封装了保存方法是save。
from flask import request
@app.route('/upload', methods=['POST'])
def upload_file():
f = request.files['pic']
# with open('./demo.png', 'wb') as new_file:
# new_file.write(f.read())
f.save('./demo.png')
return 'ok'
2、处理响应
响应报文格式:
HTTP/1.1 200 OK
Content-Type:application/json
...
body
Flask渲染模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
我的模板html内容
<br/>{{ my_str }}
<br/>{{ my_int }}
</body>
</html>
from flask import render_template
@app.route('/')
def index():
mstr = 'Hello 黑马程序员'
mint = 10
return render_template('index.html', my_str=mstr, my_int=mint) # 关键字传参
# 或者
data = {
'my_str':'hello',
'my_int':11
}
return render_template('index.html', **data) # 拆包传参,但字典关键字的名字要和模板变量名一致
3、重定向
from flask import redirect
@app.route('/demo2')
def demo2():
return redirect('http://www.itheima.com')
4、返回json数据
from flask import jsonify
@app.route('/demo3')
def demo3():
json_dict = {
"user_id": 10,
"user_name": "laowang"
}
return jsonify(json_dict)
# return json.dumps(json_dict)
比较return jsonify(json_dict)和json.dumps(json_dict):①相同:都将字典转化为json数据 ②区别:jsonify设置响应头Content-Type:application/json。
5、自定义状态码和响应头
1) 元祖方式
可以返回一个元组,这样的元组必须是 (response, status, headers) 的形式,且至少包含一个元素。 status 值会覆盖状态代码, headers 可以是一个列表或字典,作为额外的消息标头值。
@app.route('/demo4')
def demo4():
# return '状态码为 666', 666
# return '状态码为 666', 666, [('Itcast', 'Python')]
return '状态码为 666', 666, {'Itcast': 'Python'}
2) make_response方式
@app.route('/demo5')
def demo5():
resp = make_response('make response测试')
resp.headers[“Itcast”] = “Python”
resp.status = “404 not found”
return resp
6、设置cookie和session:
设置Cookie:
from flask import Flask, make_response
app = Flask(__name__)
@app.route('/cookie')
def set_cookie():
resp = make_response('set cookie ok')
resp.set_cookie('username', 'itcast')
return resp
设置Cookie有效期:如果不设置有效即表示临时cookie
@app.route('/cookie')
def set_cookie():
response = make_response('hello world')
response.set_cookie('username', 'itheima', max_age=3600)
return response
读取Cookie:
from flask import request
@app.route('/get_cookie')
def get_cookie():
resp = request.cookies.get('username')
return resp
删除Cookie:实质为设置有效期为过期
from flask import request
@app.route('/delete_cookie')
def delete_cookie():
response = make_response('hello world')
response.delete_cookie('username')
return response
2、Session,同request作为全局变量
需要先设置SECRET_KEY:
class DefaultConfig(object):
SECRET_KEY = 'fih9fh9eh9gh2'
app.config.from_object(DefaultConfig)
或者直接设置
app.secret_key='xihwidfw9efw'
设置Session:
from flask import session
@app.route('/set_session')
def set_session():
session['username'] = 'itcast'
return 'set session ok'
读取Session:
@app.route('/get_session')
def get_session():
username = session.get('username')
return 'get session username {}'.format(username)
思考1:Flask将session数据保存在哪?答:和django保存session机制不同,Flask将session同cookie一样都放在浏览器中,为了保密并签名加密。
思考2:为什么Flask在设置session之前要先在config添加SECRET_KEY?答:用于签名加密。
6、Flask框架额外
1、异常处理:abort(http错误状态码),快速抛出异常。
2、多处异常集中统一处理:errorhandler 装饰器,注册一个错误处理程序,当程序抛出指定错误状态码的时候,就会调用该装饰器所装饰的方法。
3、请求钩子:即中间件/层(即装饰器),不区分视图即对所有视图都进行拦截处理。
- before_first_request
- 在处理第一个请求前执行,开张第一单,不需要接收参数
- before_request
- 在每次请求前执行,不需要接收参数
- after_request
- 如果视图没有抛出错误,在每次请求后执行
- 接受一个参数:视图函数作出的响应response
- 在此函数中可以对响应值在返回之前做最后一步修改处理
- 需要将参数中的响应response在此函数中进行返回
- teardown_request:
- 在每次请求后执行,无论视图是否抛出错误
- 接受一个参数:错误信息,如果有相关错误抛出
中间件调用流程:middleware1.pre_process()-->middleware2.pre_process()-->view()-->middleware1.after_process()-->middleware2.after_process()
4、Flask中的两类上下文:请求上下文(request、session)和应用上下文(current_app,g)
思考:request作为全局变量如何区分不同线程的请求?
答:Flask中的request和session叫做请求上下文对象,保存了当前本次请求的相关数据,在当前线程取出来的数据只和当前线程有关。
5、上下文案例
需求:
- 构建认证机制
- 对于特定视图可以提供强制要求用户登录的限制
- 对于所有视图,无论是否强制要求用户登录,都可以在视图中尝试获取用户认证后的身份信息
分析:
-
特定强制需求 -> 装饰器
-
所有视图的需求 -> 请求钩子
流程:请求--> 请求钩子(尝试判断用户的身份,对于未登录用户不做处理 放行)用g对象保存用户身份信息,登录g.user=123,未登录g.user=None
-->如果是普通视图-->正常处理,g.user_id是否为None都行
-->如果是强制登录视图-->装饰器处理,g.user_id为None则强制登录
实现:
from flask import Flask, abort, g
app = Flask(__name__)
@app.before_request
def authentication():
"""
利用before_request请求钩子,在进入所有视图前先尝试判断用户身份
"""
# TODO 此处利用鉴权机制(如cookie、session、jwt等)鉴别用户身份信息
# if 已登录用户,用户有身份信息
g.user_id = 123
# else 未登录用户,用户无身份信息
# g.user_id = None
def login_required(func):
"""
强制登录装饰器
"""
def wrapper(*args, **kwargs):
if g.user_id is not None: # 判断用户是否登录(判断用户身份)
return func(*args, **kwargs)
else:
abort(401)
return wrapper
@app.route('/')
def index():
return 'home page user_id={}'.format(g.user_id)
@app.route('/profile')
@login_required
def get_user_profile():
return 'user profile page user_id={}'.format(g.user_id)
6、思考:在Flask程序未运行的情况下,调试代码时在终端需要使用current_app、g、request这些对象,会不会有问题?该如何使用?
答:app_context为我们提供了应用上下文环境,允许我们在外部使用应用上下文current_app、g。
实现:可以通过with语句进行使用app_context
>>> from flask import Flask
>>> app = Flask('')
>>> app.redis_cli = 'redis client'
>>>
>>> from flask import current_app
>>> current_app.redis_cli # 错误,没有上下文环境
报错
>>> with app.app_context(): # 借助with语句使用app_context创建应用上下文
... print(current_app.redis_cli)
...
redis client
答:request_context为我们提供了请求上下文环境,允许我们在外部使用请求上下文request、session。
实现:可以通过with语句进行使用request_context
>>> from flask import Flask
>>> app = Flask('')
>>> request.args # 错误,没有上下文环境
报错
>>> environ = {'wsgi.version':(1,0), 'wsgi.input': '', 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/', 'SERVER_NAME': 'itcast server', 'wsgi.url_scheme': 'http', 'SERVER_PORT': '80'} # 模拟解析客户端请求之后的wsgi字典数据
>>> with app.request_context(environ): # 借助with语句使用request_context创建请求上下文
... print(request.path)
...
/
7、上下文实现的原理:通过Threadlocal线程局部变量,虽然request是全局变量,但是在request中存储数据的时候都和线程挂钩。
举例说明:
/articles?channel_id=123 -> request.args.get('channel_id') -> 123 Thread id A
/articles?channel_id=124 -> request.args.get('channel_id') -> 124 Thread id B
request.args = {
'thread_a_id': 123,
'thread_b_id': 124
}