python全栈-flask
入门
万能开发,可以开发网址、公众号、App、小程序
Flask 是一款发布于2010年非常流行的 Python Web 框架
- 特点
1 微框架、简洁,给开发者提供了很大的扩展性。
2 Flask和相应的插件写得很好,用起来很爽。
3开发效率非常高,比如使用 SQLAlchemy 的 ORM 操作数据库可以节省开发者大量书写 sql 的时间。
Flask 的灵活度非常之高,他不会帮你做太多的决策,很多都可以按照自己的意愿进行更改。
比如:使用 Flask 开发数据库的时候,具体是使用 SQLAlchemy 还是MongoEngine,选择权完全掌握在你自己的手中。
Flask 本身相当于一个内核,其他几乎所有的功能都要用到扩展(邮件扩展 Flask-Mail ,用户认证 Flask-Login ,数据库 Flask-SQLAlchemy ),都需要用第三方的扩展来实现。
Flask 没有默认使用的数据库,你可以选择 MySQL ,也可以用NoSQL 。
其 WSGI 工具箱采用 Werkzeug (路由模块),模板引擎则使用Jinja2 。这两个也是 Flask 框架的核心。
Werkzeug (路由模块):处理网络请求和请求逻辑的模块
- 常用插件
- Flask-SQLalchemy:操作数据库;
Flask-script:插入脚本;
Flask-migrate:管理迁移数据库;
Flask-Session:Session存储方式指定;
Flask-WTF:表单;
Flask-Mail:邮件;
Flask-Bable:提供国际化和本地化支持,翻译;Flask-Login:认证用户状态;
Flask-OpenID:认证;
Flask-RESTful:开发REST API的工具;
Flask-Bootstrap:集成前端Twitter Bootstrap框架;
Flask-Moment:本地化日期和时间;
Flask-Admin:简单而可扩展的管理接口的框架
- Flask-SQLalchemy:操作数据库;
文档地址
中文文档(http://docs.jinkan.org/docs/flask/)
英文文档(http://flask.pocoo.org/docs/1.0/)
上手hello world
终于是又回到了py文件,太舒服了
from flask import Flask # 引入flask模块
# 实例化flask对象
app = Flask(__name__)
# 服务器路由地址
@app.route('/')
def index():
return 'Hello World!'
if __name__ == '__main__':
# 启动实例化的flask对象
app.run()
- Serving Flask app ‘hello’
- Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.- Running on http://127.0.0.1:5000
一般会显示一个本机端口,打开就是服务器的根目录了
- 修改服务器端口 app.run(port=8888)
flask运行方式
通过对象运行
运行程序时,可以指定运行的主机IP地址,端口 app.run(host=“0.0.0.0”, port=5000) #127.0.0.1
-
参数解释
- host 主机IP地址,可以不传,默认localhost,127.0.0.1
- port 端口号,可以不传,默认5000
-
如果想在同一个局域网下的其他电脑访问自己电脑上的Flask网站,需要设置 host=‘0.0.0.0’ 才能访问得到
flask除了在pycharm里面直接运行,还可以在终端里面运行,比如linux里面的命令行环境。命令行环境不能给项目带有中文路径。
测试路由with app.test_request_context():
with app.test_request_context():
print(url_for('denglu'))
把这个写在服务器里面,可以在pycharm直接看到返回值或者其他打印信息。就不用不断打开网页测试了。
和debug模式配合,就能测试服务器了。
debug模式
if __name__ == '__main__':
app.run(debug=True)
- 好处
- 自动重启服务器
- 报错信息直接输出到前端
在app.run里面,可以写一个参数debug=true。意思就是开启debug模式
在返回的信息里面可以看到debug mode:on
开启了这个debug模式,我们在修改代码的时候,服务器会自带更新给前端的数据。就不用一遍遍重启服务器了。
类似编写编运行的效果。
会自动重启服务器。
我们在前端只需要刷新页面就可以了。
还可以把服务器的报错信息直接输出到前端。就不用去服务器查看了。
- 由于我的pycharm里面的虚拟环境老是出错,需要使用os强制设置虚拟环境变量
import os
from flask import Flask, request
# 在代码中强制设置环境变量
os.environ['FLASK_ENV'] = 'development'
os.environ['FLASK_DEBUG'] = '1'
配置flask参数
除了直接写,还有7种方式配置参数:
- 除了直接在run方法里面写参数,还有别的方式
if __name__ == '__main__':
app.config['DEBUG'] = True
app.run()
因为是一个键值对的方式对app的参数进行修改
- 还可以使用字典更新的方式修改参数
if __name__ == '__main__':
app.config.update({'DEBUG': True})
app.run()
- 还有一种方法
if __name__ == '__main__':
app.config.from_mapping({'DEBUG': True})
app.run()
- 还可以使用对象的方式配置参数
class config:
DEBUG = True
if __name__ == '__main__':
app.config.from_object(config)
app.run()
- 还可以从json文件里面配置参数
首先配置一个json文件,内容是{“DEBUG”: “True”}
然后导入一个json库
使用from_file方法,第一个参数是json文件的路径,第二个参数是json.load不能变
import json
if __name__ == '__main__':
app.config.from_file('config.json',json.load)
app.run()
- 还可以从py文件里面加载配置
py文件写参数DEBUG = True
if __name__ == '__main__':
app.config.from_pyfile('config.py')
app.run()
- 还有一个读取系统环境变量的方法from_envvar
因为要配置系统的环境变量,太麻烦了。懒
动态路由
URL与函数的映射(动态路由)
我们知道使用@app.route(‘路由’),可以匹配一个服务器的路由地址
但是对于一个网站来说,不一定是每个路由都要存在的意义。
@app.route('/page/1')
def page_1():
return 'Page 1'
@app.route('/page/2')
def page_2():
return 'Page 2'
比如,网站有很多用户,每个用户都要很多的帖子。这些帖子的页面布局是一样的。我们没有必要给每一个帖子都在服务器配置一个路由地址。而是动态获取用户的参数,然后去数据库加载相应的内容。
像上面这样,为每一个帖子都配置路由,太浪费服务器资源了。
所以要动态路由
@app.route('/page/<num>')
def page_1(num):
# 服务器去数据库查询文字
return f'服务器查询到的{num}文章'
我们把具体的数字部分用尖括号和变量的方式代替,这样用户在访问的时候,服务器就可以获取用户的信息了。
如果用户访问的是/page/12345,服务器就可以获取num是12345.然后传到参数num里面,然后进行服务器查询,最后返回出去。
这种方式叫路径传参,或者url地址传参
数据类型
这里的数据类型,指的是url地址传参的参数数据类型
默认是string字符串,还有int,float,path,uuid,any
@app.route('/page/<int:num>')
def page_1(num):
# 服务器去数据库查询文字
print(type(num))
return f'服务器查询到的{num}文章'
在尖括号里面的变量前面,加上数据类型。因为这里我们想要设计的就是类似帖子id的变量,也就是int类型。
在控制台也可以看到num的类型是int了。
path类型指的是路由里面的/。因为字符串string无法识别含有/的信息。或者说,只能识别两个/之间的信息,一旦有多个/,就无法访问
由于文章也可能有分类,不止是只有后面的id会变,前面的路由也会变。
比如,/vip/1 和 /page/2 .
我们把文章分为两种类型,一种是vip文章,一种是普通文章page。
可以使用any把这两种类型的路由合并。
@app.route('/<any(page,vip):tem>/<int:num>')
def page_1(tem,num):
# 服务器
return f'服务器查询到的{num}文章,类型{tem}'
不难发现,我们可以在服务器获取路由的类型,以及跟在url后面的id。
拿到这两个参数,我们可以进行逻辑判断,等服务器操作。
自定义转换器
首先,去from werkzeug.routing import BaseConverter里面的BaseConverter模块,可以查看所有的转换器数据类型
我这边没有办法直接打开BaseConverter文件,只能从项目里面搜索。
位于venv>lib>werkzeug>routing>converters.py
所有的转换器都继承BaseConverter类
在文件的最下面还有一个转换器注册操作
#: the default converter mapping for the map.
DEFAULT_CONVERTERS: t.Mapping[str, type[BaseConverter]] = {
"default": UnicodeConverter,
"string": UnicodeConverter,
"any": AnyConverter,
"path": PathConverter,
"int": IntegerConverter,
"float": FloatConverter,
"uuid": UUIDConverter,
}
这是一个对象,每一个转换器的键,都对应一个子类。
class PathConverter(BaseConverter):
regex = "[^/].*?"
每一个转换器都有一个匹配规则,也就是正则表达式regex。
只要url路径上的内容符合正则表达式,就是验证通过。
依照这个原理,我们可以自定义转换器。比如手机号转换器phone类型。正则表达式是:^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$
不会写正则表达式的去网上搜,正则表达式在线测试 | 菜鸟工具
from flask import Flask
from werkzeug.routing import BaseConverter
app = Flask(__name__)
class PhoneConverter(BaseConverter):
regex = r'^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$'
app.url_map.converters['phone'] = PhoneConverter
@app.route('/')
def hello_world():
return 'Hello World!'
@app.route('/<phone:number>')
def phone(number):
return f'欢迎您,{number}'
if __name__ == '__main__':
app.run()
大概有三步:
- 构造phone转换器类
- 使用app.url_map.converters注册转换器,类型定义为phone
- 调用
@app.route('/<phone:number>')
使用了这个转换器,不符合要求的乱码,数字会被拦截。
to_python
解决url含多个参数的问题
比如,/user/zhangsan+18+n
我们定义的规则是/user/姓名+年龄+性别
这是一个字符串类型。在服务器我们可以使用split(‘+’) 对字符串进行分割。
如果有很多个需要字符串分割的路由匹配,我们就要使用自定义的转换器了
- 转换器的原理:regex匹配成功之后,把字符串直接交给类方法to_python,由to_python加工数据,然后返回。
- to_python的value就是接收的参数
class NumberConverter(BaseConverter):
def to_python(self, value: str) -> t.Any:
if self.fixed_digits and len(value) != self.fixed_digits:
raise ValidationError()
value_num = self.num_convert(value)
if (self.min is not None and value_num < self.min) or (
self.max is not None and value_num > self.max
):
raise ValidationError()
return value_num
- 构造自定义转换器的类方法
class LiConverter(BaseConverter):
def to_python(self, value):
return value.split('+') # 对参数处理
app.url_map.converters['li']=LiConverter
@app.route('/<li:info>')
def phone(info):
return f'拿到了{info}'
获取的是字符串分割后的参数列表。
就是配置url的字符串是li类型,然后这个字符串会被li类型转换器的topython切割。自动切割。
PostMan(API测试
Postman一款非常流行的API调试工具。其实,开发人员用的更多。
因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。
不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。
Postman: The World’s Leading API Platform | Sign Up for Free
API就是服务器的路由,浏览器访问服务器的路由就可以获取数据。所以叫API接口
Postman提供了独立的安装包Download Postman | Get Started for Free
- 参数介绍
- params就是url里面的键值对
- headers请求头
- body 请求体,主要是post请求
就是一个有GUI界面的爬虫
查询参数的获取
就是url参数的获取
比如http://127.0.0.1:5000/?pwd=python&name=zhangsan,获取里面的两个键值对
需要引入request库,然后在路由里面使用request.args.get(‘wd’)
- request.args.get(‘wd’)
这个操作的本质是把url的参数部分识别成字典,存放在request.args里面。我们获取的时候使用get方法。也可以用中括号[‘wd’]。但是get方法允许得到一个空值。不会报错。
@app.route('/')
def hello_world():
uname = request.args.get('name')
pwd = request.args.get('pwd')
return f'Hello {uname},{pwd}'
url需要在路由的后面跟一个?问号。然后,后面的就是url的参数了
- request.values.get(‘wd’)
和request.args.get(‘wd’)是一样的
如果你的这个页面的想要做 SEO 优化,就是被搜索引擎搜索到,那么推荐使用第一种形式(path的形式)。
如果不在乎搜索引擎优化,那么就可以使用第二种(查询字符串的形式)。
请求体参数
当请求的数据很长,或是需要隐藏传递的时候,就要用到请求体了
比如登录的时候,密码需要隐藏,不要暴露在网址里面
url路径里面的参数和请求体里面的参数不一样,可以同时存在。
因为我们没有前端框架的搭建,无法在浏览器直接发送post请求,只能用postman了。
在postman里面选择请求方式,,,啊,postman就是一个有着GUI界面的爬虫代码
python实战案例----使用 PyQt5 构建简单的 HTTP 接口测试工具_python 提供htttp 接口-CSDN博客
只是可惜,当初我学GUI界面的时候,确实写过类似的软件。但是我获取的主要是网络状态码,和网址返回的请求头信息。
因为在调试爬虫的时候,获取整个网页是没有意义的。最关键的是能不能正常访问网址,有没有被反爬。
在服务器的路由里面,使用methods指定请求方式,这是一个列表,可以同时写多个请求方式
@app.route('/',methods=['post'])
def hello_world():
return f'Hello'
在postman里面选择body,再选择form-data表单数据,就可以传参了
因为是从前端传递的post里面的表单数据。所以服务器也要form接收。
@app.route('/',methods=['post'])
def hello_world():
name = request.form.get('name')
pwd = request.form.get('pwd')
return f'Hello {name},{pwd}'
- request.values.get(‘pwd’) 也可以获取数据
上传文件
客户端上传图片到服务器,并保存到服务器中
图片,视频,在服务器都是二进制流。把二进制流保存下来就可以了
一般使用的是post方式上传
- 前端
在postman里面选择post请求body选项,form数据。在key的输入框的最右边有一个箭头,这个箭头可以选择文本或者文件。
我们选择文件的时候,在value里面就可以上传本地文件了。在key里面还可以定义这个键名,
在headers选项里面,有隐藏请求头键值对。其中content-type键对应的值,有一个multipart/form-data值,只有content-type含有multipart/form-data属性的时候,服务器才允许图片上传。
postman已经自动补充了headers
- 服务器
在路由里面,使用request.files就可以接收多个文件了。
@app.route('/upload', methods=['POST'])
def upload_file():
pic = request.files.get('pic')
with open(f'imgs/{pic.filename}', 'wb') as f:
f.write(pic.read())
print(pic.filename)
return '上传成功'
首先是使用request.files.get获取图片二进制流。
然后是使用withopen保存图片。使用pic.filename可以获取图片的名称。可以根据图片的名称来给图片命名保存在imgs目录下面
其它参数
如果想要获取其他地方传递的参数,可以通过Flask提供的request对象来读取。
不同位置的参数都存放在request的不同属性中
values 记录请求的数据,并转换为字符串 *
form 记录请求中的表单数据 MultiDict
args 记录请求中的查询参数 MultiDict
cookies 记录请求中的cookie信息 Dict
headers 记录请求中的报文头 EnvironHeaders
method 记录请求使用的HTTP方法 GET/POST
url 记录请求的URL地址 string
files 记录请求上传的文件 *
在服务器里面同一个路由,比如登录路由,如果是get请求就返回登录界面,如果是post请求,就验证身份
一个路由地址处理两个事务
from flask import Flask, request
app = Flask(__name__)
@app.route('/',methods=['GET','POST'])
def hello_world():
url = request.url
method = request.method # 获取请求方式
ua = request.headers.get('User-Agent')
uid = request.cookies.get('uid')
return f'url={url}\nmethod={method} \nuseragent={ua}\nuid={uid}'
if __name__ == '__main__':
app.run(port=8080)
cookie的结构是{cookie:{uid:1001}}
- method处理不同事务
- headers处理反爬
- cookies获取用户信息
url_for 函数
之前都是一个路由绑定一个函数,现在想要一个函数去绑定路由
@app.route('/post/list/<page>/')
def my_list(page):
return 'my list'
@app.route('/')
def hello_world():
return url_for('my_list',page=2,num=8)
# return "/post/list/2?num=8"
假如我们访问的是根路由,现在进入了helloworld函数。然后进入return语句,执行url_for函数。这个函数会去调用mylist函数,并且还能携带参数给mylist函数。mulist函数只接受一个参数page,所以,num无法正常传递。num会以请求体参数的形式附的路由上。
url_for是生成一个路由的。根据第一个参数,去生成路由
- 使用url_for函数原因
如果很多页面都绑定了一个路由,当修改这个路由的时候,所有页面的api都要重新绑定
将来如果修改了 URL ,但没有修改该 URL 对应的函数名,就不用到处去替换URL 了
url_for() 函数会转义一些特殊字符和 unicode 字符串,这些事情 url_for 会自动的帮我们
比如下面的mulist函数,他上边的路由有两个,一个是新的,一个是旧的。我们在helloworld函数里面总是能得到mulist最新的路由地址。
@app.route('/post/<page>/')
def my_list(page):
return 'my list'
@app.route('/post/list/<page>/')
def my_list(page):
return 'my list'
@app.route('/')
def hello_world():
return url_for('my_list', page=2)
在定义url的时候,一定要记得在最后加一个斜杠。
- 如果不加斜杠,那么在浏览器中访问这个url的时候,如果最后加了斜杠,那么就访问不到。这样用户体验不太好。
- 搜索引擎会将不加斜杠的和加斜杠的视为两个不同的url。而其实加和不加斜杠的都是同一个url,那么就会给搜索引擎造成一个误解。加了斜杠,就不会出现没有斜杠的情况。
就是有斜杠结尾的时候,用户在访问时,可以有最后的斜杠也可以没有。但是我们在设置路由的时候,没有斜杠结尾,用户只有不加斜杠才能访问,浏览器默认添加斜杠,不太友好
能加最后的斜杠就加上
重定向
永久性重定向:
http 的状态码是 301,多用于旧网址被废弃了要转到一个新的网址确保用户的访问。
比如:你输入 www.jingdong.com 的时候,会被重定向到 www.jd.com ,因为 jingdong.com 这个网址已经被废弃了,被改成 jd.com,所以这种情况下应该用永久重定向
暂时性重定向:
http 的状态码是 302,表示页面的暂时性跳转。
比如:访问一个需要权限的网址,如果当前用户没有登录,应该重定向到登录页面,这种情况下,应该用暂时性重定向。
- flask中重定向
重定向是通过 redirect(location,code=302) 这个函数来实现的,
location表示需要重定向到的 URL, 应该配合之前讲的 url_for() 函数来使用,
code 表示采用哪个重定向,默认是 302 暂时性重定向, 可以修改成 301 实现永久性重定向
from flask import Flask, request,url_for,redirect
app = Flask(__name__)
@app.route('/post/')
def post():
return '来到post'
@app.route('/')
def info():
return redirect(url_for(post),code=301)
if __name__ == '__main__':
app.run(port=5555, debug=True)
这里访问根路由,重定向到post路由
响应内容
就是路由给浏览器返回的内容
- 默认返回字符串return “你好,少年”
- 返回JSON
import json
@app.route('/json/')
def r_json():
json1 = {
"id":"101",
"title":"这是一个文本内容"
}
return json1
浏览器接收json数据默认进行转码处理,就是"title": "\u8fd9\u662f\u4e00\u4e2a\u6587\u672c\u5185\u5bb9"
我们可以加一个参数,阻止浏览器转码:app.config['JSON_AS_ASCII'] = False
在早期flask版本不支持json数据,需要导入jsonify模块,对json数据包裹,再传出去
- 返回元组tuple
可以返回一个元组,元组中必须至少包含一个项目,且项目应当由(response, status) 、 (response, headers) 或者 (response, status, headers) 组成。
status的值会重载状态代码, headers 可以是一个列表或字典,作为额外的消息标头值。
@app.route('/tuple/')
def r_tuple():
return 'is tuple',201
# return 'is tuple', {"id": "11"}
# return 'is tuple', 201, {"id": "11"}
# return 'is tuple', 201, [('id','11')]
浏览器得到了一个字符串和一个201状态码
这里面的json数据在浏览器作为响应头参数。
自定义响应
导入Response模块,一定是大写的R开头
@app.route('/')
def info():
return Response('你好',status=404)
# return Response('你好',status=404,headers={'uid':'166'})
我们可以自定义状态码,甚至是404
还可以携带响应请求头,注意是键值对的格式
还可以什么都不写,前端还是可以得到200的状态码。Response(‘’)
还可以只返回状态码,比如404提醒用户没有成功加载
- 把所有的状态提前定义到对象里面,make_response方式
@app.route('/demo2')
def demo2():
resp = make_response('make response测试')
resp.headers['itbaizhan'] = 'Python'
resp.status = '404 not found'
return resp
模板
- 思考 : 网站如何向客户端返回一个漂亮的页面呢?
漂亮的页面需要 html 、 css 、 js . 可以把这一堆字段串全都写到视图中, 作为 HttpResponse() 的参数,响应给客户端
@app.route('/')
def info():
return '<h2>你好</h2>'
可以这样美化页面,但是太麻烦了
- 问题
视图部分代码臃肿, 耦合度高,这样定义的字符串是不会出任何语法效果和错误的,效果无法及时查看.有错也不容易及时发现
- 解决问题MVT
模板 Template
MVT 设计模式
M全拼为Model,与MVC中的M功能相同,负责和数据库交互,进行数据处理。
V全拼为View,与MVC中的C功能相同,接收请求,进行业务处理,返回应答。
T全拼为Template,与MVC中的V功能相同,负责封装构造要返回的html。
整个网站的流程:
- 客户端发送请求
- router根据url找函数去
- 进入view层面
- view分发事务给model去数据库找数据,同时分发事务给template去融合html和css,js代码
- 最后view汇总数据,返还给客户端
使用
在 Flask中,配套的模板是 Jinja2,Jinja2的作者也是Flask的作者。这个模板非常的强大,并且执行效率高。
- 使用步骤
- 创建模板
- 在 应用 同级目录下创建模板文件夹 templates . 文件夹名称固定写法.
- 在 templates 文件夹下, 创建 应用 同名文件夹. 例, Book
- 在 应用 同名文件夹下创建 网页模板 文件. 例 : index.html
- 设置模板查找路径
- 模板处理数据
- 创建模板
需要引入render_template模块,
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def info():
return render_template('test.html')
if __name__ == '__main__':
app.run(port=8888, debug=True)
这个html需要在templates目录下面才可以,html文件像正常的网页一样去创建。
这个目录是默认的,既然说是默认了,就可以修改。
app = Flask(__name__,template_folder='temp')
需要在实例化服务器的时候,使用template_folder重新设置目录
传参
因为是模板,我们需要传参给模板,生成指定的内容,也就真正的网页
<div class="box">
<div class="box1">登录成功</div>
<div class="box2">欢迎您,{{ user }}</div>
</div>
@app.route('/')
def info():
return render_template('test.html',user='zzzz')
只需要在模板中用花括号包裹变量,然后在调用模板的时候传参
如果参数很多,还可以用字典来传参。随便定义一个字典,然后把网页的参数都写进去。最后通过**的方式写在网页文件后面
@app.route('/')
def info():
canshu={
'user':'zhangsan',
'weather':'晴',
'wendu':'20'
}
return render_template('test.html',**canshu) # **是解包的意思,原理还是把参数写在括号里面
参数可以嵌套字典
<p>今天心情如何:{{ feel.good }}{{ feel['good'] }}{{ feel.get('good') }}</p>
有三种方式获取字典内容。也可以把字典直接写在括号里面,页面上展示的时候就是一个字典格式的内容
@app.route('/')
def info():
canshu={
'user':'zhangsan',
'weather':'晴',
'wendu':'20',
'feel':{
'good':'巴适',
'bad':'不行'
}
}
return render_template('test.html',**canshu)
然后在传这个字典参数的时候,使用两个星号,将字典打散成关键字参数(也叫命名参数)
模板使用url_for函数
主要应用在模板里面的a标签的href属性上面。也可以写在其他标签里面,作为文本显示。
<a href="{{ url_for('home') }}">跳转到home视图</a>
<a href="/home/">跳转到home视图</a>
在模板里面这两个a标签都可以跳转到home页面。
使用了url_for函数,在后期修改路由的时候,不会影响home函数。
就不用频繁修改模板里面的路由了
<a href="{{ url_for('home',id=10,arg=101) }}">跳转到home视图</a>
也可以给页面传参
@app.route('/home/<int:id>')
def home(id):
return f'温度页面{id}'
这里获取了id作为url路由参数,arg作为路径参数。127.0.0.1:8888/home/10?arg=101
过滤器
就是获取到客户端的数据之后,把一些数据进行格式化,比如数字格式化为整型数字。
有时候我们想要在模版中对一些变量进行处理,那么就必须需要类似于Python中的函数一样,可以将这个值传到函数中,然后做一些操作。
在模版中,过滤器相当于是一个函数,把当前的变量传入到过滤器中,然后过滤器根据自己的功能,再返回相应的值,之后再将结果渲染到页面中
- {{ num | int }} 其中num是我们传给前端的数据,然后是一个管道符,后面是对num的格式化处理int。
Jinja模板自带过滤器
官方提供的:Template Designer Documentation — Jinja Documentation (3.1.x)
- default 设置参数默认值
<div class="box2">欢迎您,{{ user | default('用户001') }}</div>
当前端没有给这个参数的时候默认显示一个内容,不会是空,以免影响客户体验
-
default(‘默认值’,boolean=True) 还可以有第二个参数,利用布尔值,过滤空内容,比如None,{},‘’
-
or 也可以设置默认值 {{ user or ‘用户2’ }} 当user没有接收到参数的时候,使用后面的默认值
-
safe 取消转义
我们有一个向前端传参的需求,并且传递的是html标签,还希望他显示在页面上
我们在服务器给前端传参的时候,前端会对参数进行转义操作。比如尖括号,会转义成url字符<
。虽然我们可以在前端看到我们传过去的html标签,但源代码里面是转移过的内容,只是前端显示在浏览器的时候,又进行了反转义。
我们需要在前端的源代码里面取消转义,然后在浏览器显示的时候就直接使用了这个标签。
{{ info | safe }} info是服务器传给的标签内容
-
escape 和safe是反过来的。默认就使用了这个过滤器。没用。
-
{% autoescape true/false %} {{ info }} {% endautoescape %}
- {% autoescape true/false %} 后面的布尔值为真的时候,就是escape效果,布尔值为假就是safe。这个大括号会对里面的参数统一进行字符转义或者取消转义。就不用给每一个变量都添加过滤器了
- 这里是允许大量标签内容
- {{ info }}
- {{ arg }}
- {{ arg | escape }} 允许内部的变量使用自己的过滤器。
- {{ arg }}
- {% endautoescape %}
-
{{ “%s”-“%s”|format(‘Hello?’,“Foo!”) }} 输出 Hello?-Fool!
-
replace(value,old,new) 替换将old替换为new的字符串
-
还有很多过滤器,去文档中查看吧。什么取第一个字符,取最后一个字符等等。
自定义过滤器
只有当系统提供的过滤器不符合需求后,才须自定义过滤器
过滤器本质上就是一个函数。
如果在模版中调用这个过滤器,那么就会将这个变量的值作为第一个参数传给过滤器这个函数,然后函数的返回值会作为这个过滤器的返回值。需要使用到一个装饰器: @app.template_filter(‘过滤器名称’)
就是在服务器里面定义一个过滤器,这里定义了一个cut过滤器,名字随便取,然后写在变量后面
<p>{{ text | cut }}</p>
需要自定义过滤器
- 自定义数据替换过滤器
@app.template_filter('cut')
def cut(s):
s = s.replace('老太太','阿婆')
return s
这里是使用了python自带的replace方法,把老太太替换成阿婆。在前端显示的时候,只会显示阿婆了。
- 自定义时间过滤器
例如:操作发布新闻 与现在的时间间隔
time距离现在的时间间隔
- 如果时间间隔小于1分钟以内,那么就显示“刚刚”
- 如果是大于1分钟小于1小时,那么就显示“xx分钟前”
- 如果是大于1小时小于24小时,那么就显示“xx小时前”
- 如果是大于24小时小于30天以内,那么就显示“xx天前”
- 否则就是显示具体的时间 2030/10/20 16:15
就是一个python函数,给它个日期,它自己判断状态
@app.template_filter('handle_time')
def handle_time(time):
if isinstance(time, datetime):
now = datetime.now()
timestamp = (now - time).total_seconds()
if timestamp < 60:
return "刚刚"
elif timestamp >= 60 and timestamp < 60 * 60:
minutes = timestamp / 60
return "%s分钟前" % int(minutes)
elif timestamp >= 60 * 60 and timestamp < 60 * 60 * 24:
hours = timestamp / (60 * 60)
return '%s小时前' % int(hours)
elif timestamp >= 60 * 60 * 24 and timestamp < 60 * 60 * 24 * 30:
days = timestamp / (60 * 60 * 24)
return "%s天前" % int(days)
else:
return time.strftime('%Y/%m/%d%H:%M')
else:
return time
@app.route('/wendu/') # 调用路由
def wendu():
time1 = datetime(2025,4,14,19,22,20)
return render_template('test.html',time1=time1)
流程控制
和python基本相同,只是学一下专门的语法
所有的控制语句都是放在 {% … %} 中,并且有一个语句 {% endxxx %} 来进行结束!
if:if语句和python中的类似,可以使用 >,<,<=,>=,==,!= 来进行判断,也可以通过 and,or,not,() 来进行逻辑合并操作
{% if uname == 'sxt' %}
<p>尚学堂</p>
{% elif uname == 'baizhan'%}
<p>百战程序员</p>
{% else %}
<p>学Python 找百战程序员!!</p>
{% endif %}
没有elif和else就是单分支,没有elif或者else就是双分支。无限添加elif,更多分支
选择
场景:用户登录之前,在登录位置提示用户登录,用户登录之后显示用户昵称
就是判断用户user这个参数是不是空的,然后使用上面的控制语句就ok了
循环
for…in… for循环可以遍历任何一个序列包括列表、字典、元组。并且可以进行反向遍历
- 列表
<ul>
{% for user in users%}
<li>{{ user}}</li>
{% endfor %}
</ul>
- 遍历字典
<tr>
{% for key in person.keys() %}
<td>{{ key}}</td>
{% endfor %}
</tr>
<tr>
{% for val in person.values() %}
<td>{{ val }}</td>
{% endfor %}
</tr>
<tr>
{% for item in person.items() %}
<td>{{ item }}</td>
{% endfor %}
</tr>
<tr>
{% for key,value in person.items() %}
<td>{{ value }}</td>
{% endfor %}
</tr>
- 如果序列中没有值的时候,进入else,反向遍历用过滤器 reverse:
<ul>
{% for user in users|reverse %}
<li>{{ user }}</li>
{% else %}
<li>没有任何用户</li>
{% endfor %}
</ul>
并且Jinja中的for循环还包含以下变量,可以用来获取当前的遍历状态:
loop.index 当前迭代的索引(从1开始)
loop.index0 当前迭代的索引(从0开始)
loop.first 是否是第一次迭代,返回True或False
loop.last 是否是最后一次迭代,返回True或False
loop.length 序列的长度
在 jinja2 中的 for 循环,跟 python 中的 for 循环基本上是一模一样的也是 for…in… 的形式。并且也可以遍历所有的序列以及迭代器唯一不同的是, jinja2 中的 for 循环没有 break 和 continue 语句
循环练习 99乘法表
{% for x in range(1,10) %}
<tr>
{% for y in range(1,x + 1) %}
<td>{{y}}*{{x}}={{ x*y }}</td>
{% endfor %}
</tr>
{% endfor %}
宏macro
macro就是宏关键字,类似函数关键字def
模板中的宏跟python中的函数一样,可以传递参数,但是不能有返回值
可以将一些经常用到的代码片段放到宏中,然后6把一些不固定的值抽取出来当成一个变量
- 定义宏
{% macro inp(name,value,type) %}
<input type="{{type}}" name="{{name}}" value="{{value}}">
{% endmacro %}
- 优化宏定义
{% macro inp(name,value='',type='text') %} 这里是给宏的参数设置默认值,没有参数的时候使用默认值
<input type="{{type}}" name="{{name}}" value="{{value}}">
{% endmacro %}
- 使用上面的宏
<table>
<tr>
<td>用户名:</td>
<td>{{ inp('text','username','')}}</td>
</tr>
<tr>
<td>密码:</td>
<td>
{{ inp('password','pwd','')}}
</td>
</tr>
<tr>
<td>密码:</td>
<td>
{{ inp(type='password',name='pwd')}} 优化宏定义之后,可以指定参数传参的位置
</td>
</tr>
<tr>
<td></td>
<td>{{ inp('submit','','提交') }}</td>
</tr>
</table>
实际开发中,不会把宏在一个页面内定义 并直接使用
一般把宏定义放到一个专门的文件夹中,方便进行统一管理
之后,哪一个页面需要使用某个宏,需要导入宏才能使用
- 导入宏方式
-
from ‘宏文件的路径’ import 宏的名字 [as 起别名]
{% from "users/users.html" import input as inp %}
-
import “宏文件的路径” as 别名 [with context]
{% import "users/users.html" as usr with context %}
宏文件路径,不要以相对路径去寻找,都要以 templates 作为绝对路径去找
如果想要在导入宏的时候,就把当前模版的一些参数传给宏所在的模版,那么就应该在导入的时候使用 with context
{% import "users/users.html" as usr with context %}
导入模板include
- 这个标签相当于是直接将指定的模版中的代码复制粘贴到当前位置。
- include 标签,如果想要使用父模版中的变量,直接用就可以了,不需要使用 with context 。
- include 的路径,也是跟 import 一样,直接从 templates 根目录下去找,不要以相对路径去找。
<!--通过include 引入头部log信息-->
{% include "common/head.html" %}
就是很多页面有相同的结构,如果使用vue框架,就是把这些结构变成组件,再导入各个网页。
在模板里面,通过include把这写固定的结构,添加到页面。比如网页的头部,大部分是一样的。那么就可以把头部提取出来。其他页面导入。
set与with标签
- 使用 set 语句来定义变量
{% set uname='sxt'%}
<p>用户名:{{ uname }}</p>
一旦定义了这个变量,那么在后面的代码中,都可以使用这个变量,就类似于Python的变量定义是一样的
- with 语句定义的变量,只能在 with 语句块中使用,超过了这个代码块,就不能再使用了
{% with classroom='python202'%}
<p>班级:{{ classroom }}</p>
{% endwith %}
关于定义的变量, with 语句也不一定要跟一个变量,可以定义一个空的 with 语句,需要在指定的区域才能使用的情况,可以set与with组合使用。
静态文件
静态文件:css文件 js文件 图片文件等文件
加载静态文件使用的是 url_for 函数。然后第一个参数需要为 static ,第二个参数需要为一个关键字参数 filename=‘路径’ 。
前提是把这些静态文件放入static目录下
{{ url_for("static",filename='xxx') }}
路径查找,要以当前项目的 static 目录作为根目录
图片也可以使用下面的方式引入模板
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<SCRIPT src="{{ url_for('static', filename='js/test.js') }}"></SCRIPT>
这些静态文件的根目录static也可以修改
app = Flask(__name__,static_folder='static1',template_folder='templates1')
模板继承
- 为什么需要模版继承
模版继承可以把一些公用的代码单独抽取出来放到一个父模板中,以后子模板直接继承就可以使用了。这样可以重复的利用代码,并且以后修改起来也比较方便
- include导入与继承的区别
导入是使用子模板面,导入头部和脚部。条件不同时,只能使用不同的子模版分别导入父模板。
继承是使用父模板,但是在父模板里面添加子模版内容。可以添加多个子内容。根据不同的条件,父模板添加不同的子模块。
- 模版继承语法
使用 extends 语句,来指明继承的父模板。父模板的路径,默认也是相对于 templates 文件夹下的绝对路径
{% extends "base.html" %}
- block语法
一般在父模版中,定义一些公共的代码。子模板可能要根据具体的需求实现不同的代码
这时候父模版就应该有能力提供一个接口,让子模板来实现。从而实现具体业务需求的功能。
父模版
{% block block的名字 %} {% endblock %}
子模板
{% block block的名字 %} 子模板中的代码 {% endblock %}
- 调用父模版代码block中的代码
默认情况下,子模板如果实现了父模版定义的block。那么子模板block中的代码就会覆盖掉父模板中的代码。如果想要在子模板中仍然保持父模板中的代码,那么可以使用 {{ super() }} 来实现
{% block block_body%}
{{ super() }}
<p style="background-color: green">我是子模版block_body处的内容</p>
{% endblock %}
实操
- 原始父模板
<body>
<div>
<h1>头部内容开始</h1>
<h1>头部内容结束</h1>
<h1>主体内容开始</h1>
<h1>主体内容结束</h1>
<h1>尾部内容开始</h1>
<h1>尾部内容结束</h1>
</div>
</body>
- 父模板挖空
<body>
<div>
<h1>头部内容开始</h1>
{% block head %}
<p>头部</p>
{% endblock %}
<h1>头部内容结束</h1>
<h1>主体内容开始</h1>
{% block cont %}
<p>主体</p>
{% endblock %}
<h1>主体内容结束</h1>
<h1>尾部内容开始</h1>
{% block foot %}
<p>尾部</p>
{% endblock %}
<h1>尾部内容结束</h1>
</div>
</body>
直接使用父模板的时候,会忽视block语句,把block里面的内容当成正常html结构渲染。
- 子模板调用父模板
子模板内容:
{% extends 'fath.html' %}
服务器调用:
@app.route('/')
def index():
return render_template('son.html')
- 子模块替换父模块的block内容
{% extends 'fath.html' %}
{% block head %}
<p>子模版的头部内容</p>
{% endblock %}
- 子模版继承父模板的同时,保留父模板内容,使用super,保留父模块内容。super在哪里,父模块内容就在哪里
{% extends 'fath.html' %}
{% block head %}
{{ super() }}
<p>子模版的头部内容</p>
{% endblock %}
- 子模块的block调用其他block内容,这里是子模块的foot内容是head的内容。就是block的head内容,既在head显示,又在foot显示,也可以super继承,和编写自己的内容。
{% extends 'fath.html' %}
{% block head %}
{{ super() }}
<p>子模版的头部内容</p>
{% endblock %}
{% block foot %}
{{ self.head() }}
{% endblock %}
- 子模版编写的block模块,在父模板里面没有找到,就不显示。
继承模板实战
需求,使用模板继承,引入静态资源。
场景:一个登录界面,一个注册界面。
登录界面是一个用户名,一个密码和一个登录按钮,以及忘记密码超链接。
注册界面比登录界面,多一个确认密码,和注册按钮,还有一个去登录界面的超链接。
其中父模板作为网址入口,然后是跳转登录的超链接,其中超链接都是urlfor匹配路由和文件。
- 父模块
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}父模板{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/css1.css') }}">
</head>
<body>
<div class="box">
{% block cont %}
<p>主体</p>
<a href="{{ url_for('login') }}"> 去登录 </a>
{% endblock %}
</div>
</body>
</html>
- 子模版登录
{% extends "fath.html" %}
{% block title %}登录{% endblock %}
{% block cont %}
<div class="login-box">
<p style="height: 40px; line-height: 40px; font-size: 24px">登录</p>
<hr>
<div><p>用户名</p><input type="text" alt=""></div>
<div><p>密码</p><input type="text" alt=""></div>
<br>
<input type="submit" value="登录">
</div>
<a href="{{ url_for('res') }}">去注册</a>
{% endblock %}
- 子模版注册
{% extends "fath.html" %}
{% block title %}注册{% endblock %}
{% block cont %}
<div class="login-box">
<p style="height: 40px; line-height: 40px; font-size: 24px">注册</p>
<hr>
<div><p>用户名</p><input type="text" alt=""></div>
<div><p>密码</p><input type="text" alt=""></div>
<div><p>确认密码</p><input type="text" alt=""></div>
<br>
<input type="submit" value="注册">
</div>
<a href="{{ url_for('login') }}">去登录</a>
{% endblock %}
- 服务器
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('fath.html')
@app.route('/login/')
def login():
return render_template('login.html')
@app.route('/res/')
def res():
return render_template('res.html')
if __name__ == '__main__':
app.run(port=8888, debug=True)
flask高级路由/视图
add_url_rule与app.route
- add_url_rule
add_url_rule(rule,endpoint=None,view_func=None)这个方法用来添加url与视图函数的映射。
如果没有填写 endpoint ,那么默认会使用 view_func 的名字作为 endpoint 。以后在使用 url_for 的时候,就要看在映射的时候有没有传递 endpoint 参数,如果传递了,那么就应该使用 endpoint 指定的字符串,如果没有传递,那么就应该使用 view_func 的名字
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('fath.html')
def login():
return render_template('login.html')
def res():
return render_template('res.html')
app.add_url_rule('/login/', view_func=login ,endpoint='denglu')
app.add_url_rule('/res/', view_func=res ,endpoint='zhuce')
if __name__ == '__main__':
app.run(port=8888, debug=True)
就是不用@装饰器了。使用的是app里面的add_url_rule方法,第一个参数是路由,view_func的参数是函数名,endpoint可有可无,endpoint是作用就是给路由绑定的函数起别名,比如login变成了denglu,如果没有这个参数endpoint默认就是函数名了,和view_func一样。
endpoint相当于强制给函数起别名,在模板里面使用urlfor的时候,只能绑定endpoint后面的名字。不能绑定login了,只能绑定denglu。
装饰器@app.route的原理就是调用了这个函数add_url_rule.
类视图
在flask里面,函数叫视图,把函数放在类里面,这个类就是类视图。
之前我们接触的视图都是函数,所以一般简称函数视图。其实视图也可以基于类来实现,类视图的好处是支持继承
但是类视图不能跟函数视图一样,写完类视图还需要通过app.add_url_rule(url_rule,view_func) 来进行注册
因为类可以继承,所以建议使用类视图
- 标准类视图使用步骤
- 标准类视图,必须继承自 flask.views.View
- 必须实现 dispatch_request 方法,以后请求过来后,都会执行这个方法。
这个方法的返回值就相当于是之前的视图函数一样。也必须返回Response 或者子类的对象,或者是字符串,或者是元组。- 必须通过 app.add_url_rule(rule,endpoint,view_func) 来做url与视图的映射。
view_func 这个参数,需要使用类视图下的 as_view 类方法类转换:ListView.as_view(‘list’) 。- 如果指定了 endpoint ,那么在使用 url_for 反转的时候就必须使用endpoint 指定的那个值。如果没有指定 endpoint ,那么就可以使用as_view(视图名字) 中指定的视图名字来作为反转。
from flask.views import View
class ListView(View):
def dispatch_request(self):
return '返回了一个List的内容!!'
app.add_url_rule('/list',endpoint='my',view_func=ListView.as_view('mylist'))
当路由list被触发的时候,会调用类里面的dispatch_request方法,就像init一样,必须有,还是构造的时候就使用了。这里是返回了一个字符串。
view_func=ListView.as_view(‘mylist’) 是给这个类视图起名字。endpoint是起别名。
- 类视图的优点
1.可以继承,把一些共性的东西抽取出来放到父视图中,子视图直接拿来用就可以了。
2.但是也不是说所有的视图都要使用类视图,这个要根据情况而定。视图函数用得最多。
from flask import Flask,jsonify
from flask.views import View
app = Flask(__name__)
# 需求:返回的结果都必须是json数据
class BaseView(View):
def get_data(self):
raise NotImplementedError
def dispatch_request(self):
return jsonify(self.get_data())
class JsonView(BaseView):
def get_data(self):
return {'uname':'吕布','age':20}
class Json2View(BaseView):
def get_data(self):
return [
{'name':'尚学堂','lua':'Python'},
{'name':'百战程序员','lua':'Python'},
]
app.add_url_rule('/base',view_func=BaseView.as_view('base'))
app.add_url_rule('/json',view_func=JsonView.as_view('json'))
app.add_url_rule('/json2',view_func=Json2View.as_view('json2'))
if __name__ =='__main__':
app.run(debug=True)
这里面定义了一个父类视图baseview。并且自执行里面的dispatch_request方法,调用了get_data方法。
在由于父类里面定义了dispatch_request方法,子类视图只需要重新定义get_data方法。并且父类视图还对子类视图的返回值进行json处理。
在父类的get_data里面定义了一个raise,手动抛出异常,就是不允许直接使用父模板的get方法。
类视图优点
如果有一些文本内容可能在后期使用的时候,频繁更改,我们就不能在模板里面把这个东西写成静态的。
我们可以通过render_template调用html文件的时候,传参数给模板。但是当我们的页面很多的时候,就必须每个类视图都更改变量,太麻烦了,所以我们可以构造一个父类视图的init里面的字典专门存放各种频繁更改的变量。然后让哪些类视图继承父类视图,通过self使用父类变量。
当然子类视图里面在使用父类变量的时候,也可以使用自己的变量
也可以使用字典传参,使用字典传参的时候,模板可以直接使用字典里面的键值对,使用键就能获得值
lass BaseView(View):
def __init__(self):
self.msg = {
'main':'百战课程又更新了!!123'
}
class LoginView(BaseView):
def dispatch_request(self):
my_msg = '神奇的登录功能'
self.msg['my_msg'] = '神奇的登录功能' # 继承父类的变量,还修改变量
# return render_template('login.html',msg=self.msg.get('main'),my_msg = my_msg) 多参数传参
return render_template('login.html',**self.msg) # 字典传参
基于调度方法的类视图
需要继承MethodView类
通过不同的网络请求方式处理不同的事务
默认get请求只获取数据,post请求是添加数据,delete是删除数据,put修改数据
基于方法的类视图,是根据请求的 method 来执行不同的方法的。
如果用户是发送的 get 请求,那么将会执行这个类的 get 方法。
如果用户发送的是 post 请求,那么将会执行这个类的 post 方法。其他的method类似,比如 delete 、 put这种方式,可以让代码更加简洁。
所有和 get 请求相关的代码都放在 get 方法中,所有和 post 请求相关的代码都放在 post 方法中。就不需要跟之前的函数一样,通过 request.method == ‘GET’
直接使用get和post作为类视图,不同请求方式执行不同类视图
class LoginView(views.MethodView):
def get(self,error=None):
# 数据库查询,封装数据
return render_template('login.html',error=error)
def post(self):
uname = request.form['uname']
pwd = request.form['pwd']
if uname and pwd :
return render_template('index.html')
else:
# return render_template('login.html',error="用户名或者密码错误")
return self.get(error="用户名或者密码错误")
app.add_url_rule('/login/',view_func=LoginView.as_view('my_login')) 注册路由
- 优化
基于调度方法的类视图, 通常get()方法处理get请求,post()方法处理post请求,
为了便于管理,不推荐post方法和get方法互相调用
class LoginView(views.MethodView):
def __jump(self,error=None):
return render_template('login.html',error=error)
def get(self, error=None):
return self.__jump()
def post(self):
# 模拟实现 拿到前端页面传过来的 账号 和密码 去数据库做查询操作
# 查询到 (跳转主页面) ,反之跳转到login.html页面并给出错误提示信息
uname = request.form['uname']
pwd = request.form['pwd']
if uname == "sxt" and pwd == "123":
return render_template('index.html') # 成功登录
else:
return self.__jump(error="用户名或者密码错误")
# 注册类视图
app.add_url_rule('/login/',view_func=LoginView.as_view('my_login'))
装饰器
简言之,python装饰器就是用于拓展原来函数功能的一种函数,这个函数的特殊之处在于它的返回值也是一个函数
使用python装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。
案例1
需求:查看设置个人信息时,只有检测到用户已经登录了才能查看,若没有登录,则无法查看并给出提示信息
# 引入装饰器和日志
from functools import wraps
import logging
logging.basicConfig(level=logging.INFO) # 初始化日志
def login_required(func):
@wraps(func)
def wrapper(*arg,**kwargs):
# 下面是自定义的
uname = request.args.get('uname')
pwd = request.args.get('pwd')
if uname == 'zs' and pwd == '123':
logging.info(f'{uname}:登录成功')
return func(*arg,**kwargs)
else:
logging.info(f'{uname}:尝试登录,但没成功')
return '请先登录'
# 上面是自定义的
return wrapper
这个装饰器的结构是固定的。
- 使用装饰器
在普通视图使用时,需要在路由和函数之间
@app.route('/settings/')
@login_requierd
def settings():
return '这是设置界面'
在类视图里面使用装饰器,只需要使用decorators = [login_requierd] ,因为这是数组,允许多个装饰器同时使用
class ProfileView(View):
decorators = [login_requierd]
def dispatch_request(self):
return '这是个人中心界面'
app.add_url_rule('/profile/',view_func=ProfileView.as_view('profile'))
蓝图Blueprint(bp)
引入import Blueprint模块
降低功能耦合度
-
针对的问题
就是正常编辑网站页面,至少有几个模块,比如用户,产品等等。用户里面还有注册,登录,注销等等。一个模块几个功能,随之而来的就是大量的路由地址。后期修改的时候,需要一个个去调整。如果我们编写代码不规范,一个模块的功能不是都放在一起,那还要从整个项目文件里去找。
在Flask中,使用蓝图Blueprint来分模块组织管理。
- 蓝图实际可以理解为是存储一组视图方法的容器对象,其具有如下特点:
- 一个应用可以具有多个Blueprint
- 可以将一个Blueprint注册到任何一个未使用的URL下比如 “/user” 、 “/goods”
- Blueprint可以单独具有自己的模板、目录、静态文件或者其它的通用操作方法,它并不是必须要实现应用的视图和函数的
- 在一个应用初始化时,就应该要注册需要使用的Blueprint
Blueprint并不是一个完整的应用,它不能独立于应用运行,而必须要注册到某一个应用中
使用方式:使用蓝图可以分为三个步骤
1 创建一个蓝图对象
user_bp=Blueprint('user',__name__)
第一个参数是蓝图的名称,第二个参数固定写死
2 这个蓝图管理的子功能
@user_bp.route('/login/')
def login():
return '用户模块的登录功能'
3 在应用对象上注册这个蓝图对象
app.register_blueprint(user_bp)
只要没有第三步的注册,这个蓝图就不会生效。意味着,后续不使用这个模块的时候,把注册取消就完了
-
效果:蓝图批量管理路由地址
-
问题:目前从路由上看不出功能所属的模块
- 解决:在路由前面加上模块的前缀:
app.register_blueprint(user_bp,url_prefix='/user')
- 对于用户模块下的所有功能,在访问的时候都会加上user前缀,从’/login/’ 变成’/user/login/’
- 这个是自动加的,只有注册的地方发生改变,其他的地方不用变
- 解决:在路由前面加上模块的前缀:
蓝图的目录结构
为了让项目代码更加清晰,可以通过将代码分在不同的文件里进行管理
有两种模块分类方式:根据功能模块,根据技术模块
根据功能模块:比如用户模块单独一个文件夹,产品一个文件夹
根据技术模块:比如前端视图模块:处理呈现给用户的前端页面路由。
- 引入位于项目里面其他文件夹的蓝图
from user.view import user_bp
其中user是文件夹,view是蓝图文件,user_bp是蓝图的名称
蓝图文件也需要引入Blueprint库。
创建使用蓝图
条件:每一个模块比如用户模块,单独文件夹。文件夹里面要有view.py文件,作为视图文件,就是专门盛放路由的程序。还有一个init.py文件,把蓝图和视图文件关联。
- init部分 固定步骤,不要调整顺序。顶多发生替换放在其他文件
from flask import Blueprint
user_bp = Blueprint('user', __name__) # 只需要调整蓝图的名称user
from user import view # 从user目录导入view文件
- view文件
from flask import render_template
from user import user_bp
@user_bp.route('/')
def index():
return render_template('index.html')
- 主文件 这里就是建议连着写在主程序里面。方便后期注销
from user import user_bp
app.register_blueprint(user_bp, url_prefix='/user/')
一定要有url_prefix参数,避免路由之间的冲突。
蓝图管理模板文件
- 在蓝图里面使用render_template方法,也是从整个项目找templates目录,再找html文件
- 或者去user目录下的init部分,给初始化蓝图的时候,加参数,指定template_folder参数为templates目录。也不一定非要是templates目录,随意更改。
如果没有2里面的参数。就只有第一种情况。有了参数,就会使用user目录下的templates文件了。
反正没有第二步的操作,无论在项目的哪里调用render_template方法,都是在项目目录下的templates目录找html文件。
如果项目和蓝图模块都有templates目录,优先从项目找。然后才会到蓝图模块。找不到就报错
寻找规则
如果项目中的templates文件夹中有相应的模版文件,就直接使用了。
如果项目中的templates文件夹中没有相应的模版文件,那么就到在定义蓝图的时候指定的路径中寻找。并且蓝图中指定的路径可以为相对路径,相对的是当前这个蓝图文件所在的目录因为这个蓝图文件是在user/view.py,那么就会到blueprints这个文件夹下的user_page文件夹中寻找模版文件。
小总结:
常规:蓝图文件在查找模版文件时,会以templates为根目录进行查找1 个性化coder喜欢在【创建蓝图对象的时候】 指定 模版文件的查找路径,如下news_bp=Blueprint(‘news’,name,url_prefix=‘/news’,template_folder=‘news_page’)
2 只有确定templates目录下没有对应的 html文件名的时候,才会去蓝图文件指定的目录下查找,指定才会生效
3 若templates目录下,有一个与蓝图文件指定的目录下同名的一个 html文件时,优先走templates目录下的东西
蓝图中的静态文件
蓝图内部静态文件
蓝图对象创建时不会默认注册静态目录的路由。需要我们在创建时指定 static_folder 参数。
下面的示例将蓝图所在目录下的 static_admin 目录设置为静态目录user=Blueprint("user",__name__,static_folder='user_static') app.register_blueprint(admin,url_prefix='/user')
也可通过 static_url_path 改变访问路径
user =Blueprint(‘user’,name,template_folder=‘user_page’,static_folder=‘user_static’,static_url_path=‘/static’)
app.register_blueprint(user,url_prefix=‘/user’)
蓝图url_for函数
如果使用蓝图,那么以后想要反转蓝图中的视图函数为url,就应该在使用url_for的时候指定这个蓝图名字。
app类中、模版中、同一个蓝图类中都是如此。否则就找不到这个endpoint
<a href="{{ url_for('user.user_list')}}">新闻列表 OK写法</a>
就是使用“ 蓝图名称.资源目录 ”
子域名实现
蓝图实现子域名:
使用蓝图技术。
在创建蓝图对象的时候,需要传递一个 subdomain 参数,来指定这个子域名的前缀。
cms_bp=Blueprint('cms',__name__,subdomain='cms')
- 需要在主app文件中,需要配置app.config的SERVER_NAME参数。例如:
app.config['SERVER_NAME']='sxt.com:5000'
- 在windows: C:\Windows\System32\drivers\etc 下,找到hosts文件,然后添加域名与本机的映射。Linux: /etc/hosts
域名和子域名都需要做映射127.0.0.1 sxt.com
127.0.0.1 python.sxt.comip地址不能有子域名
localhost也不能有子域名
flask高级知识
Cookie
主要功能,帮助服务器识别用户信息。
场景:
例如,用户A在超市购买的任何商品都应该放在A的购物车内,不论是用户A什么时间购买的,这都是属于同一个会话的,不能放入用户B或用户C的购物车内,这不属于同一个会话。
而Web应用程序是使用HTTP协议传输数据的。HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭。再次交换数据需要建立新的连接,这就意味着服务器无法从连接上跟踪会话。即用户A购买了一件商品放入购物车内,当再次购买商品时服务器已经无法判断该购买行为是属于用户A的会话还是用户B的会话了。因此,必须引入一种机制,让服务器记住用户。
Cookie就是这样的一种机制。它可以弥补HTTP协议无状态的不足。在Session出现之前,基本上所有的网站都采用Cookie来跟踪会话。
由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。
Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。
当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。
浏览器对cookie数量和大小有限制的!如果超过了这个限制,你的信息将丢失。不同的浏览器存储的Cookie的数量不同!
尽量保证cookie的数量以及相应的大小。cookie个数最好 <20~30个;cookie大小最好 < 4K
设置Cookie
设置cookie是在Response的对象上设置。
flask.Response 对象有一个 set_cookie 方法,可以通过这个方法来设置 cookie信息。key,value形式设置信息from flask import Flask, make_response app = Flask(__name__) @app.route('/cookie') def set_cookie(): resp = make_response('set cookie ok') resp.set_cookie('uname', 'itbaizhan') return resp
查看Cookie
在Chrome浏览器中查看cookie的方式:
方式1:借助于 开发调式工具进行查看
方式2:在Chrome的设置界面->高级设置->内容设置->所有cookie->找到当前域名下的cookie。from flask import request @app.route('/get_cookie') def get_cookie(): resp = request.cookies.get('uname') return resp
删除cookie
方式1:通过 Response对象.delete_cookie ,指定cookie的key,就可以删除cookie了from flask import request @app.route('/delete_cookie') def delete_cookie(): response = make_response('helloworld') response.delete_cookie('uname') return response
方式2:在客户端浏览器人为的删除(清除浏览器浏览历史记录后,很多网站之前免密登录的都不好使了)
Cookie的有效期
默认的过期时间:如果没有显示的指定过期时间,那么这个cookie将会在浏览器关闭后过期。
max_age:以秒为单位,距离现在多少秒后cookie会过期。
expires:为datetime类型。这个时间需要设置为格林尼治时间,相对北京时间来说 会自动+8小时如果max_age和expires都设置了,那么这时候以max_age为标准。
max_age在IE8以下的浏览器是不支持的。
expires虽然在新版的HTTP协议中是被废弃了,但是到目前为止,所有的浏览器都还是能够支持,所以如果想要兼容IE8以下的浏览器,那么应该使用expires,否则可以使用max_age。from flask import Flask,Response app = Flask(__name__) @app.route('/') def index(): return 'Hello!!' @app.route('/create_cookie/defualt/') def create_cookie1(): resp = Response('通过默认值,设置cookie有效期') # 如果没有设置有效期,默认会在浏览器关闭的时候,让cookie过期 resp.set_cookie('uname','sxt') return resp @app.route('/create_cookie/max_age/') def create_cookie2(): resp = Response('通过max_age,设置cookie有效期') # max_age以秒为单位设置cookie的有效期 age = 60*60*2 resp.set_cookie('uname','itbaizhan',max_age=age) return resp from datetime import datetime @app.route('/create_cookie/expires/') def create_cookie3(): resp = Response('通过expires,设置cookie有效期') # expires 以指定时间为cookie的有效期 # 16+8 == 24 tmp_time = datetime(2021, 11,11,hour=18,minute=0,second=0) resp.set_cookie('uname','python',expires=tmp_time) return resp from datetime import timedelta @app.route('/create_cookie/expires2/') def create_cookie4(): resp = Response('通过expires,设置cookie有效期') # expires 以指定时间为cookie的有效期 tmp_time = datetime.now() +timedelta(days=2) resp.set_cookie('uname','python_sql',expires=tmp_time) return resp @app.route('/create_cookie/exp_max/') def create_cookie5(): resp = Response('通过expires与max_age,设置cookie有效期') # expires 与max_age同时设置了,会以max_age为准 tmp_time = datetime.now() +timedelta(days=2) resp.set_cookie('uname','python_sql',expires=tmp_time,max_age = 60*60*2) return resp if __name__ == '__main__': app.run(debug=True)
Session
Session和Cookie的作用有点类似,都是为了存储用户相关的信息,都是为了解决http协议无状态的这个特点。
不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。
客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。
客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。不同的语言,不同的框架,有不同的实现。
虽然底层的实现不完全一样,但目的都是让服务器端能方便的存储数据而产生的。Session的出现,是为了解决cookie存储数据不安全的问题的。
如果说Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话
那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。
Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。
Session的跟踪机制
Flask框架中,session的跟踪机制跟Cookie有关,这也就意味着脱离了Cookie,session就不好使了。
因为session跟踪机制跟cookie有关,所以,要分服务器端和客户端分别起到什么功能来理解。
存储在服务器的数据会更加的安全,不容易被窃取。
但存储在服务器也有一定的弊端,就是会占用服务器的资源,但现在服务器已经发展至今,一些session信息还是绰绰有余的。
问题
(面试题)若 客户端禁用了浏览器的Cookie功能,session功能想
继续保留,该咋整?给出你的实现思路(能代码实现最好)解决方案
URL地址携带SessionID
使用Session
需要先设置SECRET_KEY (混淆加密算法,密钥)
可以使用from os import urandom
里面的urandom(length) 生成指定长度的随机字符串
class DefaultConfig(object):
SECRET_KEY = 'fih9fh9eh9gh2' # urandom(18)
app.config.from_object(DefaultConfig)
# 或者直接设置
app.secret_key='xihwidfw9efw'
设置、修改
from flask import session
@app.route('/set_session/')
def set_session():
session['username'] = 'itbaizhan'
return 'set session ok'
读取
@app.route('/get_session/')
def get_session():
username = session.get('username')
# 去数据库对比信息
return 'get session username{}'.format(username)
删除
@app.route('/del_session/')
def delete_session():
#删除指定的key的session
# session.pop('uname')
#删除session中的所有的key 【删除所有】
session.clear()
return '删除成功'
设置Session的有效期
如果没有设置session的有效期。那么默认就是浏览器关闭后过期。
如果设置session.permanent=True,那么就会默认在31天后过期。
如果不想在31天后过期,按如下步骤操作
1 session.permanent=True
2 可以设置 app.config[‘PERMANENT_SESSION_LIFETIME’] = timedelta(hour=2) 在两个小时后过期。
timedelta 可以修改permanent的过期时间,默认是31天,使用了这个库就可以修改了
app.config[‘PERMANENT_SESSION_LIFETIME’] =timedelta(days=2) # 过期时间是两天
from flask import Flask,session from datetime import timedelta # 这个库可以修改session的过期时间 app = Flask(__name__) app.secret_key = 'sdfdfdsfsss' app.config['PERMANENT_SESSION_LIFETIME'] =timedelta(days=2) # 过期时间是两天 @app.route('/') def index(): return 'Hello!!' @app.route('/set_session/') def set_session(): # 设置session的持久化,默认是增加了31天 session.permanent = True session['uname'] = '10001' return '设置一个Session的信息' @app.route('/get_session/') def get_session(): return session.get('uname') if __name__ == '__main__': app.run(debug=True)
如果服务器关闭掉了,session的有效期,依然是之前系统保存日期
app.secret_key = 'sdfdfdsfsss'
如果secret_key设置是一个固定的值,那么服务器重启不会影响session的有效器
如果secret_key设置不是一个固定的值,那么服务器之前设置的session将全部过期
Session实战–免登录效果
- 引入的库,os设置debug模式,flask服务器,render_template引入页面,request获取url参数,session设置/获取session值,redirect重定向,url_for获取动态路由,methodview类视图,timedelta设置session过期时间
import os
from flask import Flask, render_template, request, session, redirect, url_for
from flask.views import MethodView
from datetime import timedelta
# 在代码中强制设置环境变量
os.environ['FLASK_DEBUG'] = '1'
app = Flask(__name__)
app.secret_key='zhao' # 密钥,这个一改,所有session失效。
app.config['PERMANENT_SESSION_LIFETIME'] =timedelta(days=2) # session过期时间是两天
@app.route('/')
def hello_world():
uname = session.get('uname') # 获取session值
pwd = session.get('pwd')
if uname: # 如果有值
return render_template('index.html',uname=uname,pwd=pwd)
return redirect(url_for('login',msg='未登录')) # 重定向登录界面,并告知用户未登录
class loginview(MethodView):
def __jump(self,msg=None): # 专门跳转登录界面的
return render_template('login.html',msg=msg)
def get(self):
msg = request.args.get('msg') # 处理的是首页跳转给的地址参数
return self.__jump(msg=msg)
def post(self):
uname = request.form['uname']
pwd = request.form['pwd']
session.permanent = True
session['uname'] = uname
session['pwd'] = pwd
return render_template('index.html',uname=uname,pwd=pwd) # 按理来说,有个用户信息验证,没有该用户就调用jump方法
app.add_url_rule('/login/', view_func=loginview.as_view('login')) # 注册路由
if __name__ == '__main__':
app.run(debug=True)
- 登录界面
<form id="loginForm" method="POST" action="/login">
<label for="username">用户名:</label>
<input type="text" id="username" name="uname" required/>
<br>
<label for="password">密码:</label>
<input type="password" id="password" name="pwd" required/>
<br>
<button type="submit">登录</button>
<br>
{% if msg %} <!-- 如果msg是空,就不显示 -->
{{ msg }}
{% endif %}
</form>
- 个人主页
<p>个人主页!!</p>
<h2>欢迎您:{{ uname }}</h2>
<br>
<span>密码:{{ pwd }}</span>
Local对象
作用:保证各个请求之间的通信互不干扰
需求
要实现并发效果, 每一个请求进来的时候我们都开启一个进程, 这显然是不合理的, 于是就可以使用线程
那么线程中数据互相不隔离,存在修改数据的时候数据不安全的问题
Local对象
在Flask中,类似于 request 对象,其实是绑定到了一个 werkzeug.local.Local对象上。
这样,即使是同一个对象,那么在多个线程中都是隔离的。类似的对象还有 session 对象。
from werkzeug.local import Local #flask=werkzeug + sqlalchemy + jinja2
werkzeug是数据隔离库。thread库里面的local也有一个类似的效果。
ThreadLocal变量
Python提供了ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。
总结
只要满足绑定到"local"或"Local"对象上的属性,在每个线程中都是隔离的,那么他就叫做 ThreadLocal 对象,也叫’ThreadLocal’变量。
无论是thread里面的local还是werkzeug里面的local效果是一样的。但是多线程还是要靠thread开启。
app上下文
App上下文,也叫应用上下文
上下文(感性的理解)
每一段程序都有很多外部变量,只有像add这种简单的函数才是没有外部变量的。 一旦一段程序有了外部变量,这段程序就不完整,不能独立运行。为了能让这段程序可以运行,就要给所有的外部变量一个一个设置一些值。就些值所在的集合就是叫上下文。
并且上下文这一概念在中断任务的场景下具有重大意义,其中任务在被中断后,处理器保存上下文并提供中断处理,因些在这之后,任务可以在同一个地方继续执行。(上下文越小,延迟越小)举例
运行的Flask项目,每一个路由映射的内容片段,都不可以单独拿出来使用。
当获取到了APP_Context以后,就可以直接通过程序映射的地址访问逻辑,并且可以重复使用。
上下文的一个典型应用场景就是用来缓存一些我们需要在发生请求之前或者要使用的资源。举个例子,比如数据库连接。当我们在应用上下文中来存储东西的时候你得选择一个唯一的名字,这是因为应用上下文为 Flask 应用和扩展所共享。
应用上下文:
应用上下文是存放到一个 LocalStack 的栈中。和应用app相关的操作就必须要用到应用上下文
首先要引入一个库,current_app
通过current_app.name
获取服务器运行的文件名。
在视图函数中,不用担心应用上下文的问题。因为视图函数要执行,那么肯定是通过访问url的方式执行的,那么这种情况下,Flask底层就已经自动的帮我们把应用上下文都推入到了相应的栈中。
注意
如果想要在视图函数外面执行相关的操作,比如: 获取当前的app名称,那么就必须要手动推入应用上下文
- 第一种方式:便于理解的写法
from flask import Flask,current_app app = Flask(__name__) #app上下文 app_context = app.app_context() app_context.push() print(current_app.name) @app.route('/') def hello_world(): print(current_app.name) #获取应用的名称 return 'Hello World!' if __name__ == '__main__': app.run(debug=True)
- 第二种方式:用with语句
from flask import Flask,current_app app = Flask(__name__) #app上下文 #换一种写法 with app.app_context(): print(current_app.name) # print(current_app.name) 只能在with的作用范围内使用 @app.route('/') def hello_world(): print(current_app.name) #获取应用的名称 return 'Hello World!' if __name__ == '__main__': app.run(debug=True)
request上下文详解
请求上下文
请求上下文:请求上下文也是存放到一个 LocalStack 的栈中。和请求相关的操作就必须用到请求上下文,比如使用 url_for 反转视图函数。
注意
在视图函数中,不用担心请求上下文的问题。因为视图函数要执行,那么肯定是通过访问url的方式执行的,那么这种情况下,Flask底层就已经自动的帮我们把应用上下文和请求上下文都推入到了相应的栈中。
如果想要在视图函数外面执行相关的操作,比如反转url,那么就必须要手动推入请求上下文
底层代码执行说明:
- 推入请求上下文到栈中,会首先判断有没有应用上下文
- 如果没有那么就会先推入应用上下文到栈中
- 然后再推入请求上下文到栈中
总结
为什么上下文需要放在栈中?
- 应用上下文:Flask底层是基于werkzeug,werkzeug是可以包含多个app的,所以这时候用一个栈来保存。
如果你在使用app1,那么app1应该是要在栈的顶部,如果用完了app1,那么app1应该从栈中删除。方便其他代码使用下面的app。- 如果在写测试代码,或者离线脚本的时候,我们有时候可能需要创建多个请求上下文,这时候就需要存放到一个栈中了。
使用哪个请求上下文的时候,就把对应的请求上下文放到栈的顶部,用完了就要把这个请求上下文从栈中移除掉。
线程隔离的g对象
保存为全局对象g对象的好处:
g对象是在整个Flask应用运行期间都是可以使用的。并且也跟request一样,是线程隔离的。
这个对象是专门用来存储开发者自己定义的一些数据,方便在整个Flask程序中都可以使用。
一般使用就是,将一些经常会用到的数据绑定到上面,以后就直接从g上面取就可以了,而不需要通过传参的形式,这样更加方便。
g对象使用场景:有一个工具类utils.py 和 用户办理业务:
def funa(uname): print(f'funa {uname}') def funb(uname): print(f'funb {uname}') def func(uname): print(f'func {uname}')
用户办理业务
from flask import Flask,request from utils import funa,funb,func app = Flask(__name__) #Flask_线程隔离的g对象使用详解 @app.route("/profile/") def my_profile(): #从url中取参 uname = request.args.get('uname') #调用功能函数办理业务 funa(uname) funb(uname) func(uname) #每次都得传参 麻烦,引入g对象进行优化 return "办理业务成功" if __name__ == '__main__': app.run(debug=True)
优化工具类utils.py
from flask import g def funa(): print(f'funa {g.uname}') def funb(): print(f'funb {g.uname}') def func(): print(f'func {g.uname}')
优化用户办理业务
from flask import Flask,request,g from utils import funa,funb,func app = Flask(__name__) #Flask_线程隔离的g对象使用详解 @app.route("/profile/") def my_profile(): #从url中取参 uname = request.args.get('uname') #调用功能函数办理业务 # funa(uname) # funb(uname) # func(uname) #每次都得传参 麻烦,引入g对象进行优化 g.uname = uname funa() funb() func() return "办理业务成功" if __name__ == '__main__': app.run(debug=True)
就是设置了一共全局对象g,这个g可以储存很多变量,就是一个键值对对象,所以的文件都可以引用。我们在flask里面给对象赋值。在别的文件里面使用值。
钩子函数
钩子函数概念
在Flask中钩子函数是使用特定的装饰器装饰的函数。
为什么叫做钩子函数呢,是因为钩子函数可以在正常执行的代码中,插入一段自己想要执行的代码。那么这种函数就叫做钩子函数。
常见的钩子函数
before_first_request:处理项目的第一次请求之前执行。
@app.before_first_request def first_request(): print('first time request')
before_request:在每次请求之前执行。通常可以用这个装饰器来给视图函数增加一些变量。请求已经到达了Flask,但是还没有进入到具体的视图函数之前调用。一般这个就是在视图函数之前,我们可以把一些后面需要用到的数据先处理好,方便视图函数使用。
teardown_appcontext:不管是否有异常,注册的函数都会在每次请求之后执行。(提交事务)
template_filter:在使用Jinja2模板的时候自定义过滤器。
context_processor:上下文处理器。使用这个钩子函数,必须返回一个字典。这个字典中的值在所有模版中都可以使用。这个钩子函数的函数是,如果一些在很多模版中都要用到的变量,那么就可以使用这个钩子函数来返回,而不用在每个视图函数中的 render_template 中去写,这样可以让代码更加简洁和好维护。
errorhandler:errorhandler接收状态码,可以自定义返回这种状态码的响应的处理方法。在发生一些异常的时候,比如404错误,比如500错误,那么如果想要优雅的处理这些错误,就可以使用 errorhandler 来出来。
before_first_request
处理项目的第一次请求之前执行。
应用场景:初始化项目数据
from flask import Flask,request
app = Flask(__name__)
@app.route('/')
def hello_world():
print("hi")
return "hello world "
@app.before_first_request
def first_request():
print('hello world')
if __name__ == '__main__':
app.run(debug=True)
就是我们运行项目之后,第一次打开网站,这个@app.before_first_request才会运行,而且是在打开网站之前调用。
也就是说我们第一次打开网站之后,先调用@app.before_first_request,再调用@app.route(‘/’)。
仅限第一次打开网站的时候执行。
before_request
每次请求都会先执行这个函数
在每次请求之前执行。通常可以用这个装饰器来给视图函数增加一些变量。
请求已经到达了Flask,但是还没有进入到具体的视图函数之前调用。
一般这个就是在视图函数之前,我们可以把一些后面需要用到的数据先处理好,方便视图函数使用。
from flask import Flask,g,session app = Flask(__name__) app.config['SECRET_KEY'] = 'skidhfikshighsd' @app.route('/login/') def login(): print('运行代码 222222222') print('Hello!!!!') session['uname'] = '吕布' return f'Hello' @app.route('/home/') def home(): print('运行代码 3333333333') if hasattr(g, 'uname'): return f'用户已登录!用户名是:{g.uname}' return '用户没有登录!' @app.before_request def before_request(): print('运行代码 111111111111') uname = session.get('uname') if uname: g.uname = uname print('这个是每次请求时,需要执行的逻辑!!!')# 需求: 判断用户是否登录,如果登录了,就返回用户的信息,如果没有登录就返回None if __name__ == '__main__': app.run(debug = True)
context_processor
上下文处理器。使用这个钩子函数,必须返回一个字典。这个字典中的值在所有模版中都可以使用。
使用场景
这个钩子函数的功能是,如果一些在很多模版中都要用到的变量,那么就可以使用这个钩子函数来返回,而不用在每个视图函数中的render_template 中去写,这样可以让代码更加简洁和好维护。
@app.context_processor def context_processor(): if hasattr(g,'user'): return {"current_user":g.user} else: return {}
类似之前写的用户登录,和个人主页。我们从用户登录开始获取用户名和密码,然后又在打开个人主页的时候再次填写这些信息。如果使用了这个钩子函数,就可以统一给所有的视图传递参数了。
这个钩子函数会在其他视图执行之后,和网页渲染之前执行。反正能把数据传递给模板。
因为是给所有视图匹配,视图需要使用这个参数,就正好。视图不用也不影响
errorhandler
在发生一些异常的时候,比如404错误,比如500错误, 那么如果想要优雅的处理这些错误,就可以使用 errorhandler 来出来。
需要注意几点:
在errorhandler装饰的钩子函数下,记得要返回相应的状态码。
在errorhandler装饰的钩子函数中,必须要写一个参数,来接收错误的信息,如果没有参数,就会直接报错。
使用 flask.abort 可以手动的抛出相应的错误,比如开发者在发现参数不正确的时候可以自己手动的抛出一个400错误。
@app.errorhandler(500) def server_error(error): # server_error() takes 0 positional arguments but 1 was given return render_template('500.html'),500 #状态码虽然可以不写,但是推荐写上,这样可以告诉服务器是哪个错误 # return render_template('500.html') 没有后面的状态码,浏览器默认是200.浏览器无法判断是报错页面
主要给开发人员使用,为什么出错,哪里出错
就是发生错误的时候,可以自定义错误页面
Flask中的abort函数可以手动的抛出相应的错误
@app.errorhandler(404) def server_error(error): return render_template('404.html'),404 @app.route('/home/') def home(): abort(404)
前提是先定义一个errorhandler错误网页。abort不能单独使用,因为前端没有可以显示的页面
信号机制blinker
大白话来说,类似于两方属于敌对关系时,某人在敌对方阵营进行交谈,一旦遇到特殊情况,某人便会发送信号,他的同伙接收(监听)到他发的信号后,同伙便会做出一系列的应对策略(进攻|撤退)。
flask中的信号使用的是一个第三方插件,叫做blinker。通过pip list看一下,如果没有安装,通过以下命令即可安装blinker
- 自定义信号步骤
自定义信号可分为3步来完成。
第一是创建一个信号,第二是监听一个信号,第三是发送一个信号。
以下将对这三步进行讲解:
创建信号:
定义信号需要使用到blinker这个包的Namespace类来创建一个命名空间。比如定义一个在访问了某个视图函数的时候的信号。示例代码如下:
# Namespace的作用:为了防止多人开发的时候,信号名字冲突的问题 from blinker import Namespace mysignal = Namespace() signal1 = mysignal.signal('信号名称')
监听信号:
监听信号使用signal1对象的connect方法,在这个方法中需要传递一个函数,用来监听到这个信号后做该做的事情。示例代码如下:
def func1(sender,uname): print(sender) print(uname) signal1.connect(func1)
发送信号:
发送信号使用signal1对象的send方法,这个方法可以传递一些其他参数过去。示例代码如下:
signal1.send(uname='momo')
信号使用场景_存储用户登录日志
信号使用场景
定义一个登录的信号,以后用户登录进来以后
就发送一个登录信号,然后能够监听这个信号
在监听到这个信号以后,就记录当前这个用户登录的信息
用信号的方式,记录用户的登录信息即登录日志编写一个signals.py文件创建登录信号
from blinker import Namespace from datetime import datetime from flask import request,g namespace = Namespace() #创建登录信号 login_signal = namespace.signal('login') def login_log(sender): # 用户名 登录时间 ip地址 now = datetime.now() ip = request.remote_addr log_data = "{uname}*{now}*{ip}".format(uname=g.uname, now=now, ip=ip) with open('login_log.txt','a') as f: f.write(log_data + "\n") f.close() #监听信号 login_signal.connect(login_log)
使用信号存储用户登录日志
from flask import Flask,request,g from signals import login_signal app = Flask(__name__) @app.route('/login/') def login(): # 通过查询字符串的形式来传递uname这个参数 uname = request.args.get('uname') if uname: g.uname = uname # 发送信号 login_signal.send() return '登录成功!' else: return '请输入用户名!' if __name__ == '__main__': app.run(debug=True)
内置信号
Flask内置了10个常用的信号
1 template_rendered:模版渲染完成后的信号。
2 before_render_template:模版渲染之前的信号。
3 request_started:请求开始之前,在到达视图函数之前发送信号。进入路由之后,函数之前
4 request_finished:请求结束时,在响应发送给客户端之前发送信号。
5 request_tearing_down:请求对象被销毁时发送的信号,即使在请求过程中发生异常也会发送信号。
6 got_request_exception:在请求过程中抛出异常时发送信号,异常本身会通过exception传递到订阅(监听)的函数中。一般可以监听这个信号,来记录网站异常信息。
7 appcontext_tearing_down:应用上下文被销毁时发送的信号。
8 appcontext_pushed:应用上下文被推入到栈上时发送的信号。
9 appcontext_popped:应用上下文被推出栈时发送的信号。
10 message_flashed:调用了Flask的 flash 方法时发送的信号。
有了这些信号,就可以直接使用。监听并使用
- template_rendered的使用
from flask importFlask,render_template,template_rendered
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello!!'
@app.route('/home/')
def home():
return render_template('home.html')
def render_function(sender,template,context):
print(sender) # 哪个文件发送的
print(template) # 哪个页面发送的
print(context) # 上下文:url+session+flask.g
template_rendered.connect(render_function)
if __name__ =='__main__':
app.run(debug=True)
- got_request_exception的使用
from flask import Flask,request,got_request_exception,render_template
app = Flask(__name__)
#内置信号
#got_request_exception:在请求过程中抛出异常时发送信号,异常本身会通过exception传递到订阅(监听)的函数中。
# 一般可以监听这个信号,来记录网站异常信息。
# def request_exception_log(sender,*args,**kwargs): #掌握写参数技巧
# print(sender)
# print(args)
# print(kwargs)
def request_exception_log(sender,exception): # 第二个参数是异常信息
print(sender)
print(exception)
got_request_exception.connect(request_exception_log)
@app.route('/')
def hello_world():
#制造bug
a = 1/0
return render_template("index.html",data="momo")
if __name__ == '__main__':
app.run(debug=True)
WTForms
应用场景:表单验证,模板渲染
前端有js验证,后端就是wtf验证
这个插件库主要有两个作用。
第一个是做表单验证,将用户提交上来的数据进行验证是否符合系统要求。
第二个是做模版渲染。 (了解即可)Flask-WTF是简化了WTForms操作的一个第三方库。WTForms表单的两个主要功能是验证用户提交数据的合法性以及渲染模板。而Flask-WTF还包括一些其他的功能:CSRF保护,文件上传等。
安装Flask-WTF默认也会安装WTForms
WTForms表单验证的基本使用
1 自定义一个表单类,继承自wtforms.Form类。
2 定义好需要验证的字段,字段的名字必须和模版中那些需要验证的input标签的name属性值保持一致。正则表达式
3 在需要验证的字段上,需要指定好具体的数据类型。
4 在相关的字段上,指定验证器。
5 以后在视图函数中,只需要使用这个表单类的对象,并且把需要验证的数据,也就是request.form传给这个表单类,再调用表单对象.validate()方法进行,如果返回True,那么代表用户输入的数据都是符合格式要求的,Flase则代表用户输入的数据是有问题的。如果验证失败了,那么可以通过表单类对象.errors来获取具体的错误信息。from flask import Flask,render_template,request from wtforms import Form,StringField # 引入form是进行表单验证,使用StringField是字符串类型 from wtforms.validators import Length,EqualTo # wtforms.validators是验证规则 Length是长度验证,EqualTo是看跟谁相等(第一个参数是变量,第二个参数是报错message app = Flask(__name__) @app.route('/') def index(): return 'Hello! ' class RegisterForm(Form): uname = StringField(validators=[Length(min=2,max=10,message='用户名长度2-10之间')]) # uname的验证类型是字符串类型,长度在2到10字符,message是出错的时候给form.errors的 pwd = StringField(validators=[Length(min=2,max=10)]) pwd2 = StringField(validators=[Length(min=2,max=10),EqualTo('pwd',message='2次密码不一致')]) @app.route('/register/', methods=['GET','POST']) def register(): if request.method == 'GET': return render_template('register.html') else: form = RegisterForm(request.form) if form.validate(): # 验证成功:True,失败:False return '验证成功!' else: return f'验证失败!{form.errors}' if __name__ == '__main__': app.run(debug=True)
常用验证器
长度,必选项,email,url,值范围
Length:字符串长度限制,有min和max两个值进行限制。
validators=[Length(min=3,max=10,message=“用户名长度必须在3到10位之间”)]
EqualTo:验证数据是否和另外一个字段相等,常用的就是密码和确认密码两个字段是否相等。
validators=[Length(min=6,max=10),EqualTo(“password”)]
Email:验证上传的数据是否为邮箱数据格式 如:223333@qq.com。
validators=[Email()]
InputRequired:验证该项数据为必填项,即要求该项非空。
validators=[input_required()]
NumberRange:数值的区间,有min和max两个值限制,如果处在这两个数字之间则满足。
validators=[NumberRange(12,18)]
Regexp:定义正则表达式进行验证,如验证手机号码。
validators=[Regexp(r’1[34578]\d{9}')]
URL:必须是URL的形式 如http://www.baidu.com。
validators=[URL()]
UUID:验证数据是UUID类型。
validators=[UUID()]
from wtforms import Form,StringField,IntegerField
from wtforms.validators import Length,EqualTo,Email,InputRequired,NumberRange,Regexp,URL,UUID
class RegisterForm(Form):
uname =StringField(validators=[Length(min=2,max=15,message='用户名长度必须在2-15之间')])
pwd = StringField(validators=[Length(min=6,max=12)])
pwd2 = StringField(validators=[Length(min=6,max=12),EqualTo("pwd")])
email = StringField(validators=[Email()])
uname = StringField(validators=[InputRequired()])
age = IntegerField(validators=[NumberRange(18,40)])
phone = StringField(validators=[Regexp(r'1[34578]\d{9}')])
phomepage = StringField(validators=[URL()])
uuid = StringField(validators=[UUID()])
自定义验证器
只有当WTForms内置的验证器不够使的时候,才需要使用自定义验证器。
如果想要对表单中的某个字段进行更细化的验证,那么可以针对这个字段进行单独的验证。
自定义验证器步骤如下:
1 定义一个方法,方法的名字规则是: validate_字段名(self,field) 。
2 在方法中,使用 field.data 可以获取到这个字段的具体的值。
3 验证时,如果数据满足条件,那么可以什么都不做。如果验证失败那么应该抛出一个 wtforms.validators.ValidationError 的异常,并且把验证失败的信息传到这个异常类中。
场景:验证码实现
class RegisterForm2(Form):
code = StringField(validators=[Length(4,4)]) #限定长度
#取到的值 和服务器上 session上存储的值对比
def validate_code(self,field): # 自定义验证器
print(field.data,session.get('code')) # 打印前端的值和服务器的值
if field.data !=session.get('code'): # 数据满足条件,那么可以什么都不做
raise ValidationError('验证码不一致!')
渲染模版
生成html代码,但是不好用,用到的概率很小
渲染模版是WTForms的第二个作用,不过,我们只需要了解即可,不需要花太多精力和时间去研究它。
- 定义一个wtforms
class CreateForm(Form):
uname = StringField("用户名:",validators= [InputRequired()])
age = IntegerField("年龄:",validators =[NumberRange(18,40)])
remember = BooleanField("记住我:")
addr = SelectField('地址:',choices=[('bj',"北京"),('sj','上海'),('tj','天津')])
- 服务器调用
#WTForms渲染模版 了解
@app.route('/createform/')
def createform():
form = CreateForm()
return render_template('create_form.html',form=form)
- html文件
{{form.uname,label}} 接收的是wtform定义的uname里面的第一个参数:“用户名:”
{{form.uname()}} 这里是validators= [InputRequired()],就是一个输入框
{{form.age,label}} "年龄:"
{{form.age()}} NumberRange(18,40) 是一个有上下箭头的数值输入框
{{form.remember,label}} "记住我:"
{{form.remember()}} 复选框
{{form.addr,label}} '地址:'
{{form.addr()}} 下拉式列表:分别是北京,上海,天津
这里类似{{msg}} 只是接收参数而已,没有格式,一大堆连在一起。
- 前端生成的格式 (其他的类似,只写了uname)
<label for='uname'>Uname</label>
<input id='uname' maxlength='10' minlength='1' name='uname' type='text' value=''>
安全上传文件/访问文件
上传文件步骤:
- 在模版html中,表单需要指定 enctype=‘multipart/form-data’ 才能上传文件。
- 在后台如果想要获取上传的文件,那么应该使用 request.files.get(‘文件名’) 来获取。
- 保存文件之前,先要使用 werkzeug.utils.secure_filename 来对上传上来的文件名进行一个过滤。能保证不会有安全问题。
- 获取到上传上来的文件后,使用 文件对象.save(路径) 方法来保存文件。路径=完整路径=路径名+文件名
<form action="" method="post" enctype="multipart/form-data">
头像:<input type="file" name="pichead"><br> 前端会显示选择文件,如果选择了,就会显示文件名
<input type="submit" value="提交">
</form>
pichead = request.files.get("pichead") # 获取用户提交文件
filename = secure_filename(pichead.filename) # 获取文件名,secure_filename可以处理文件名里面的非法字符
剩下的就是通过os库保存文件了
访问文件
从服务器上读取文件,应该定义一个url与视图函数,来获取指定的文件。
在这个视图函数中,使用 send_from_directory(文件的目录,文件名) 来获取。from flask import Flask import os from flask import send_from_directory app = Flask(__name__) UPLOAD_PATH = os.path.join(os.path.dirname(__file__),'images') # 图片目录 @app.route('/images/<filename>/') def get_image(filename): return send_from_directory(UPLOAD_PATH,filename) # 图片目录+图片名=图片 if __name__ == '__main__': app.run(debug=True)
wtf验证上传文件
关键点:
1 定义验证表单类的时候,对文件类型的字段,需要采用 FileField 这个类型,即wtforms.FileField
2 验证器需要从 flask_wtf.file 中导入。 flask_wtf.file.FileRequired 和 flask_wtf.file.FileAllowed
3 flask_wtf.file.FileRequired 是用来验证文件上传不能为空。
4 flask_wtf.file.FileAllowed 用来验证上传的文件的后缀名, 如常见图片后缀 .jpg 和.png以及.gif等。
5 在视图函数中,需要使用 from werkzeug.datastructures import CombinedMultiDict 来把request.form 与 request.files 来进行合并。
6 最后使用 表单验证对象.validate()进行验证。
from wtforms import Form,FileField,StringField
from wtforms.validators import InputRequired # flask_wtf
from flask_wtf.file import FileRequired,FileAllowed
class UploadForm(Form):
pichead = FileField(validators=[FileRequired(),FileAllowed(['jpg','png','gif'])]) # 允许上传,数据类型
desc = StringField(validators=[InputRequired()])
FileRequired 是否允许上传
FileAllowed 上传的类型
from flask import Flask,request,render_template
import os
from werkzeug.utils import secure_filename
from formscheck import UploadForm
from werkzeug.datastructures import CombinedMultiDict
app = Flask(__name__)
UPLOAD_PATH =os.path.join(os.path.dirname(__file__),'images')
#利用flask-wtf验证上传的文件
@app.route('/upload/',methods=['GET','POST'])
def upload():
if request.method == 'GET':
return render_template('upload.html')
else:
form = UploadForm(CombinedMultiDict([request.form,request.files])) # wtf验证
if form.validate(): # 验证结果
# desc = request.form.get("desc")
# pichead =request.files.get("pichead")
desc = form.desc.data
pichead = form.pichead.data # 上传的文件
filename = secure_filename(pichead.filename) # 文件名
pichead.save(os.path.join(UPLOAD_PATH,filename)) # 保存文件
print(desc)
return '文件上传成功'
else:
print(form.errors)
return "文件上传失败"
if __name__ == '__main__':
app.run(debug=True)
Restful/api
Restful接口规范
REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。
RESTful是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。
它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次。
RESTful接口规范是用于在前端与后台进行通信的一套规范。使用这个规范可以让前后端开发变得更加轻松。适用场景:一个系统的数据库数据,展现的平台有PC端、移动端、app端、ios端。
前端工程师:都遵循RESTful编程规范
后端工程师:都遵循RESTful编程规范
最终结果:开发效率高,便于管理协议:用http或者https协议。
数据传输格式:数据传输的格式应该都用json格式。
url链接规则:
url链接中,不能有动词,只能有名词。
并且对于一些名词,如果出现复数,那么应该在后面加s。比如:获取新闻列表,应该使用 /news/ ,而不应该使用/get_news/HTTP请求方式:
GET:从服务器上获取资源。
POST:在服务器上新增或者修改一个资源。
PUT:在服务器上更新资源。(客户端提供所有改变后的数据)
PATCH:在服务器上更新资源。(客户端只提供需要改变的属性)
DELETE:从服务器上删除资源。常用状态码:
200 服务器成功响应客户端的请求。
400 用户发出的请求有错误,服务器没有进行新建或修改数据的操作
401 用户没有权限访问这个请求
403 因为某些原因禁止访问这个请求
404 用户请求的url不存在
406 用户请求不被服务器接收(比如服务器期望客户端发送某个字段,但是没有发送)。
500 服务器内部错误,比如遇到bug
基本使用
就是设计api的。通过api路由返回json数据。
优势:
Flask-Restful是一个专门用来写restful api的一个插件。
使用它可以快速的集成restful api接口功能。
在系统的纯api的后台中,这个插件可以帮助我们节省很多时间。缺点:
如果在普通的网站中,这个插件就没有优势了,因为在普通的网站开发中,是需要去渲染HTML代码的,而Flask-Restful在每个请求中都是返回json格式的数据。
使用方法:
定义Restful的类视图:
- 从 flask_restful 中导入 Api ,来创建一个 api 对象。
- 写一个类视图,让他继承自 Resource 类,然后在这个里面,使用你想要的请求方式来定义相应的方法,比如你想要将这个类视图只能采用 post 请求,那么就定义一个 post 方法。
- 使用 api.add_resource 来添加类视图与 url 。
from flask import Flask,url_for
from flask_restful import Resource,Api
app = Flask(__name__)
# 建立Api对象,并绑定应用APP
api = Api(app)
class LoginView(Resource): # 继承Resource
def get(self):
return {"flag":True}
def post(self):
return {"flag":False}
# 建立路由映射
api.add_resource(LoginView,'/login/','/login2/',endpoint='login') # 第一个参数指定类视图,后面是多个路由
with app.test_request_context():
# 默认没有写endpoint反向url_for函数通过小写函数名loginView
# 如果有多个url,会返回第1个URL,print(url_for('loginview'))
print(url_for('login'))
if __name__ == '__main__':
app.run(debug=True)
使用127.0.0.1/login/访问,在postman测试的时候,选get请求就是true,post就是false。
我们可以在api路由映射写多个路由,都可以拿到一样的内容
1 如果你想返回json数据,那么就使用flask_restful,如果你是想渲染模版,那么还是采用之前的方式,就是 app.route 的方式。
2 url还是跟之前的一样,可以传递参数。也跟之前的不一样,可以指定多个url。
3 endpoint是用来给url_for反转url的时候指定的。如果不写endpoint,那么将会使用视图的名字的小写来作为endpoint。
4 add_resource的第二个参数是访问这个视图函数的url,这个url可以跟之前的route一样,可以传递参数,并且还有一点不同的是,这个方法可以传递多个url来指定这个视图函数
参数验证
参数验证也叫参数解析
Flask-Restful插件提供了类似WTForms来验证提交的数据是否合法的包,叫做reqparse。
基本用法
1 通过 flask_restful.reqparse 中 RequestParser 建立解析器
2 通过 RequestParser 中的 add_argument 方法定义字段与解析规则
3 通过 RequestParser 中的 parse_args 来解析参数
1 解析正确,返回正确参数
2 解析错误,返回错误信息给前端
parser.add_argument(‘uname’,type=str,required=True,help=‘用户名验证错误’,trim=True)
uname是前端变量,type是变量类型,help是返回错误提示,trim是去除空格
参数详解
1 default:默认值,如果这个参数没有值,那么将使用这个参数指定的默认值。
2 required:是否必须填写。默认为False,如果设置为True,那么这个参数就必须提交上来。
3 type:这个参数的数据类型,如果指定,那么将使用指定的数据类型来强制转换提交上来的值。可以使用python自带的一些数据类型(如str或者int),也可以使用flask_restful.inputs下的一些特定的数据类型来强制转换。
url:会判断这个参数的值是否是一个url,如果不是,那么就会抛出异常。
regex:正则表达式。
date:将这个字符串转换为datetime.date数据类型。如果转换不成功,则会抛出一个异常.4 choices:固定选项。提交上来的值只有满足这个选项中的值才符合验证通过,否则验证不通过。
5 help:错误信息。如果验证失败后,将会使用这个参数指定的值作为错误信息。
6 trim:是否要去掉前后的空格。
from flask import Flask from flask_restful import Api,Resource,inputs from flask_restful.reqparse import RequestParser app = Flask(__name__) api = Api(app) class RegisterView(Resource): def post(self): # 建立解析器 parser = RequestParser() # 定义解析规则 parser.add_argument('uname',type=str,required=True,trim=True,help='用户名不符合规范') parser.add_argument('pwd',type=str,help='密码错误',default='123456') parser.add_argument('age',type=int,help='年龄验证错误!') parser.add_argument('gender',type=str,choices=['男', '女','保密'],help='性别验证错误') parser.add_argument('birthday',type=inputs.date,help='生日验证错误') parser.add_argument('phone',type=inputs.regex('^1[356789]\d{9}$'),help='电话验证错误') parser.add_argument('homepage',type=inputs.url,help='个人主页验证错误') # 解析数据 args = parser.parse_args() print(args) return {'msg':'注册成功!'} api.add_resource(RegisterView,'/register/') if __name__ == '__main__': app.run(debug=True)
这种方法就不用我们一个个去手动判断了
规范返回值
对于一个类视图,可以指定好一些字段做标准化用于返回。以后使用ORM模型或者自定义模型的时候,他会自动的获取模型中的相应的字段,生成json格式数据,然后再返回给客户端。
- 使用方法
导入 flask_restful.marshal_with 装饰器
定义一个字典变量来指定需要返回的标准化字段,以及该字段的数据类型在请求方法中,返回自定义对象的时候,flask_restful会自动的读取对象模型上的所有属性。组装成一个符合标准化参数的json格式字符串返回给客户端
哪个请求方法需要规范化,就在哪个方法上加装饰器
- fields是官方提供的规范
规范的种类有
["String", "FormattedString", "Url", "DateTime", "Float","Integer", "Arbitrary", "Nested", "List", "Raw", "Boolean","Fixed", "Price"]
marshal_with是装饰器
自动把数据变成json数据
from flask import Flask
from flask_restful import Api, Resource, fields, marshal_with
app = Flask(__name__)
api = Api(app)
class News:
def __init__(self, code, msg, state, content):
self.code = code
self.msg = msg
self.state1 = state
self.content = content
class NewsView(Resource):
resouce_fields = {
'code': fields.Integer, # 整数
'msg': fields.String, # 字符串
'state': fields.String
}
@marshal_with(resouce_fields)
def get(self):
return {'code': 200, 'msg': '访问成功!', 'state': '移动'}
@marshal_with(resouce_fields)
def post(self):
return {'msg': '注册成功!'} # 装饰器补全其余数据
@marshal_with(resouce_fields)
def put(self):
# 在返回对象时,会自动在对象中获取与约定好的字段,并获取封装成json。
news = News(404, 'OK', '电脑端', '尚学堂')
return news
api.add_resource(NewsView, '/news/')
if __name__ == '__main__':
app.run(debug=True)
规范返回值-参数设置
设置默认值,或者别名
问题
规范给出的属性名和模型内部的属性名不相同解决方案
使用 attribute 配置这种映射,比如: fields.String(attribute=‘username’)
就是一个变量,在不同地方有不同的名字。比如用户名,在设定规范的时候,是uname,但是传给前端的是name。在其他地方可能又是username。
由于变量的变量名不同,导致不通用。要么全部重构,统一变量名,前后端全部统一。要么就无法使用。所以使用attribute ,让不同的变量名可以操作一个变量
uname:fields.String(attribute='username')
uname指向username
问题
某些字段,没有值,但想给一个值做为默认值解决方案
使用 default 指定默认值,比如: fields.String(default=‘sxt’)
就是重新设计默认值。某一个变量不给赋值的时候,显示的那个默认值
默认值的优先级最低。只要有其他情况可以给变量赋值,或者前端传参。默认值就不会生效
规范返回值-类型设置
大型的互联网项目中,返回的数据格式,有时是比较复杂的结构。
如:豆瓣电影
https://movie.douban.com/j/chart/top_list?type=24&interval_id=100%3A90&action=&start=20&limit=20
返回的值里有json或者列表数据,这时可以通过以字段来实现
fields.List 放置一个列表
fields.Nested放置一个字典
class NewsView(Resource):
resouce_fields ={
'code':fields.Integer,
'msg':fields.String,
'user':fields.Nested({
'uname':fields.String
}),
'_type':fields.List(fields.Nested({
'_type':fields.String
}))
}
RESTful结合蓝图
Flask_RESTful结合蓝图使用
在蓝图中,如果使用Flask_RESTful,创建Api对象的时候,传入蓝图对象即可,不再是传入 app 对象
# user/views.py
from user import user_bp
from flask_restful import Api,Resource
api = Api(user_bp)
class LoginView(Resource):
def get(self):
return {'msg':'注册成功!!'}
# 建立映射关系
api.add_resource(LoginView,'/login/')
渲染模版(不推荐)
这个章节就是api。api就是传json数据的。传模板用route。别乱用就完了
渲染模版就是在Flask_RESTful的类视图中要返回html片段代码,或者是整个html文件代码。如何需要浏览器渲染模板内容应该使用 api.representation 这个装饰器来定义一个函数,在这个函数中,应该对 html 代码进行一个封装,再返回。
注意
api.representation装饰器修饰的函数必须返回一个Response对象
from flask import Flask,render_template,Response
from flask_restful import Api,Resource
import json
app = Flask(__name__)
# 如果想要前端的中文不再是\u这样的编码,可以加如下参数配置
app.config['RESTFUL_JSON'] = dict(ensure_ascii=False) # 让浏览器反编译url码
api = Api(app)
class HomeView(Resource):
def get(self):
return {"msg":"这个是个人主页"} # json数据
class IndexView(Resource):
def get(self):
return render_template('index.html') # html模板
api.add_resource(IndexView,'/index/')
api.add_resource(HomeView,'/home/')
@api.representation('text/html')
def out_html(data,code,headers):
# 必须返回一个response对象
if isinstance(data, str): # 判断是不是html数据
resp = Response(data)
return resp
else:
return Response(json.dumps(data,ensure_ascii=False).encode('gbk'))
if __name__ == '__main__':
app.run(debug=True)
SQLAlchemy
操作数据库的
数据库是一个网站的基础。
比如MySQL、MongoDB、SQLite、PostgreSQL等,这里我们以MySQL为例进行讲解。SQLAlchemy是一个ORM框架
对象关系映射(英语:Object Relational Mapping,简称ORM,或O/RM,或O/R mapping),是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。从效果上说,它其实是创建了一个可在编程语言里使用的“虚拟对象数据库”。
大白话
对象模型与数据库表的映射
- 为什么要有SQLAlchemy?
随着项目的越来越大,采用写原生SQL的方式在代码中会出现大量重复的SQL语句,那么,问题就出现了:
1 SQL语句重复利用率不高,越复杂的SQL语句条件越多,代码越长,会出现很多相近的SQL语句
2 很多SQL语句 是在业务逻辑中拼接出来的,如果数据库需要更改,就要去修改这些逻辑,这会容易漏掉对某些SQL语句的修改
3 写SQL时容易忽略web安全问题,造成隐患
- 而ORM可以通过类的方式去操作数据库而不用再写原生的SQL语句,通过把表映射成类,把行作为实例(一条数据),把字段作为属性,ORM在执行对象操作的时候最终还是会把对象的操作转换为数据库的原生语句,但使用ORM有许多优点:
1 易用性:使用ORM做数据库开发可以有效减少重复SQL语句的概率,写出来的模型也更加直观、清晰
2 性能损耗小:ORM转换成底层数据库操作指令确实会有一些开销。但是从实际情况来看,这种性能损耗很少(不足5%),只要不是针对性能有严苛的要求,综合考虑开发效率、代码阅读性,带来的好处远大于性能损耗,而且项目越大作用越明显。
3 设计灵活:可以轻松的写出复杂的查询。
4 可移植性:SQLAlchemy封装了底层的数据库实现,支持多个关系数据库引擎,包括流行的Mysql、PostgreSQL和SQLite,可以非常轻松的切换数据库。
- 使用ORM操作数据库将变得非常简单
class Person:
name = 'xx'
age = 18
country ='xx'
# Person类 -> 数据库中的一张表
# Person类中的属性 -> 数据库中一张表字段
# Person类的一个对象 -> 数据库中表的一条数据
# p = Person('xx',xx)
# p.save() 提交一条数据 # insert into table values ('xx',xx) 他自动生成提交数据库操作
我们会以 MySQL+ SQLAlchemy 组合进行讲解。在操作数据库操作之前,先确保你已经安装了以下软件:
mysql
如果是在windows上,到官网下载pymysql:pymysql是用Python来操作mysql的包
SQLAlchemy:SQLAlchemy是一个数据库的ORM框架,我们在后面会用到
链接数据库
在pycharm里面,连接好数据库之后,右键数据库,点击新建,查询控制台
from sqlalchemy import create_engine, text
def conn_db1():
# 数据库的变量
HOST = '127.0.0.1' # 127.0.0.1/localhost
PORT = 3306
DATA_BASE = 'flask_db'
USER = 'root'
PWD = 'root'
# DB_URI = f'数据库的名+驱动名://{USER}:{PWD}@{HOST}:{PORT}/{DATA_BASE}'
DB_URI = f'mysql+pymysql://{USER}:{PWD}@{HOST}:{PORT}/{DATA_BASE}'
engine = create_engine(DB_URI) # 连接数据库的引擎
# 执行一个SQL
sql = 'select 1;'
conn = engine.connect()
rs = conn.execute(text(sql))
print(rs.fetchone())
if __name__ == '__main__':
conn_db1()
- with自动断开数据库
# 创建一个引擎,专门链接数据库用的
engine = create_engine(DB_URI)
sql = 'create table t_user1(id int primary key auto_increment, name varchar(32));'
# 链接数据库
with engine.connect() as conn: # 自动断开数据库
# 执行SQL即可
conn.execute(sql)
ORM模型映射到数据库中
ORM模型------------就是python操作的数据库对象的类
映射到数据库中-----------------就是根据类去数据库里面创建表
from sqlalchemy.ext.declarative import declarative_base # 基类,负责数据库对象连接
engine = create_engine(DB_URI) # 连接数据库对象
Base = declarative_base() # 基类,类对象必须继承基类,才能映射
class Person(Base):
__tablename__ ='t_person' # 表的名字
# 在这个ORM模型中创建一些属性,来跟表中的字段进行一一映射。
# 这些属性必须是sqlalchemy给我们提供好的数据类型
id = Column(Integer,primary_key=True,autoincrement=True) # 整型,主键,自增长
name = Column(String(50)) # 字符串长度
age = Column(Integer)
country = Column(String(50))
Base.metadata.create_all() # 提交表结构
你给的代码整体上没什么大问题,但有两点需要注意,可能会导致你之前遇到的兼容性或警告错误:
1.
declarative_base
的导入路径你当前是:
from sqlalchemy.ext.declarative import declarative_base
而 SQLAlchemy 2.0 里推荐改成:
from sqlalchemy.orm import declarative_base
sqlalchemy.ext.declarative
中的declarative_base
仍然存在,但会产生 MovedIn20Warning,建议改用新的导入路径。
2. 你没有传入
engine
给declarative_base()
,这是对的
declarative_base()
不再接受参数,符合 2.0 的用法。
3. 你的代码示例调整建议
把导入改成如下即可,其他部分不变:
from sqlalchemy import create_engine, Column, Integer, String from sqlalchemy.orm import declarative_base # 改这里 # 数据库连接参数 HOST = '127.0.0.1' PORT = 3306 DATA_BASE = 'flask_db' USER = 'root' PWD = 'root' DB_URI = f'mysql+pymysql://{USER}:{PWD}@{HOST}:{PORT}/{DATA_BASE}' print(DB_URI) engine = create_engine(DB_URI) Base = declarative_base() # 不传参数 class Person(Base): # 表对象类 __tablename__ = 't_person' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(50)) age = Column(Integer) country = Column(String(50)) Base.metadata.create_all(engine) # engine写在这里
4. 额外建议
- 确保你安装的
pymysql
驱动版本和 SQLAlchemy 版本兼容。- 运行时,若还提示警告或错误,请完整贴日志,我可以帮你更具体定位。
小结
- 改用
from sqlalchemy.orm import declarative_base
declarative_base()
不接受参数- 你的模型定义和
create_all()
调用没问题这样你的代码就符合 SQLAlchemy 2.0 的规范,避免警告和错误。
如果你需要,我可以帮你写一个完整可运行的示范代码。
增删改查
构建session对象
所有和数据库的ORM操作都必须通过一个叫做 session 的会话对象来实现,通过以下代码来获取会话对象
from sqlalchemy.orm import sessionmaker
engine = create_engine(DB_URI)
Base = declarative_base(engine)
session = sessionmaker(engine)() # 操作的数据库对象
- 增
def create_data_one():
with Session() as session:
# 编辑添加的数据
p1 = Person(name = '百战',age = 6 ,country='北京')
# 把编辑好的数据给session对象
session.add(p1)
session.commit() # 提交session对象
session创建之后还要执行一下。所以直接给session()起别名了。
- 增加多个数据add_all
def create_data_one():
with Session() as session:
# 编辑添加的数据
p1 = Person(name='百战', age=6, country='北京')
p2 = Person(name='张三', age=8, country='上海')
p3 = Person(name='李四', age=9, country='天津')
# 使用session.add_all()添加多个实例
session.add_all([p1, p2, p3])
session.commit() # 提交session对象
- 问题:我的数据库表,字符集出错
需要在sql控制台,输入下面的内容:
USE flask_db;
ALTER TABLE t_person CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
指定数据库,然后修改表的字符集
- 查
- 获取第一个数据
def query_data_one():
with Session() as session:
p1 = session.query(Person).first()
print(p1.name)
- 获取多个数据
def query_data_all():
with Session() as session:
all_person = session.query(Person).all() # 获取全部数据
for p in all_person:
print(p.name) # 打印name的值
- 有条件过滤的查询
def query_data_by_params():
with Session() as session:
# p1 = session.query(Person).filter_by(name='张三').first()
p1 = session.query(Person).filter(Person.name == '张三').first()
print(p1.age)
里面的两种方式都可以获取特定的对象
为了解决报错,使用try,except结构
def query_data_by_params(): with Session() as session: try: # p1 = session.query(Person).filter_by(name='张三').first() p1 = session.query(Person).filter(Person.name == '张三').first() print(p1.age) except : print('无了')
- 改
def update_data():
with Session() as session:
p1 = session.query(Person).filter(Person.name == '张三').first()
p1.age = 20
# 提交事务
session.commit()
- 删
def delete_data():
with Session() as session:
p1 = session.query(Person).filter(Person.name == '张三').first()
session.delete(p1)
session.commit()
常用数据类型
Integer:整形,映射到数据库中是int类型。
Float:浮点类型,映射到数据库中是float类型。他占据的32位。
Double:双精度浮点类型,映射到数据库中是double类型,占据64位 (SQLALCHEMY中没有)。
String:可变字符类型,映射到数据库中是varchar类型.
Boolean:布尔类型,映射到数据库中的是tinyint类型。一个字节0/1
DECIMAL:定点类型。是专门为了解决浮点类型精度丢失的问题的。在存储钱相关的字段的时候建议大家都使用这个数据类型。
- 这个类型使用的时候需要传递两个参数,第一个参数是用来标记这个字段总能能存储多少个数字,第二个参数表示小数点后有多少位。
Enum:枚举类型。指定某个字段只能是枚举中指定的几个值,不能为其他值。在ORM模型中,使用Enum来作为枚举,示例代码如下:
class News(Base): __tablename__ = 't_news' tag =Column(Enum("python",'flask','django'))
在Python3中,已经内置了enum这个枚举的模块,我们也可以使用这个模块去定义相关的字段。示例代码如下:
class TagEnum(enum.Enum): python = "python" flask = "flask" django = "django" class News(Base): __tablename__ = 't_news' id = Column(Integer,primary_key=True,autoincrement=True) tag = Column(Enum(TagEnum)) news = News(tag=TagEnum.flask)
Date:存储时间,只能存储年月日。映射到数据库中是date类型。在Python代码中,可以使用 datetime.date 来指定。
DateTime:存储时间,可以存储年月日时分秒毫秒等。映射到数据库中也是datetime类型。在Python代码中,可以使用datetime.datetime 来指定。
Time:存储时间,可以存储时分秒。映射到数据库中也是time类型。在Python代码中,可以使用 datetime.time 来至此那个。示例代码如下:
class News(Base): __tablename__ = 't_news' create_time = Column(Time) news =News(create_time=time(hour=11,minute=11,second=11))
- Text:存储长字符串。一般可以存储6W多个字符。如果超出了这个范围,可以使用LONGTEXT类型。映射到数据库中就是text类型。
from sqlalchemy import create_engine, Column, Integer, String, Float, Enum, Boolean, DECIMAL, Text, Date, DateTime, Time
from sqlalchemy.orm import declarative_base, sessionmaker
import enum
from datetime import date, datetime, time
# 准备数据库连接信息
HOSTNAME = '127.0.0.1'
PORT = '3306'
DATABASE = 'flask_db'
USERNAME = 'root'
PASSWORD = 'root'
# 根据格式组织数据库信息
DB_URI = f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8"
# 创建数据库引擎
engine = create_engine(DB_URI, echo=True) # echo=True 将输出所有执行的 SQL 语句
# 创建会话对象
Session = sessionmaker(bind=engine)
session = Session() # 在 SQLAlchemy 2.0 中,确保会话是用 bind 参数绑定到引擎
# 定义一个枚举类
class TagEnum(enum.Enum):
python = "PYTHON"
flask = "FLASK"
django = "DJANGO"
# 创建一个ORM模型
Base = declarative_base()
class News(Base):
__tablename__ = 'news'
id = Column(Integer, primary_key=True, autoincrement=True)
price1 = Column(Float) # 注意 Float 可能会有精度问题
price2 = Column(DECIMAL(10, 4)) # 精度和范围配置
title = Column(String(50))
is_delete = Column(Boolean)
tag1 = Column(Enum('PYTHON', 'FLASK', 'DJANGO'))
tag2 = Column(Enum(TagEnum)) # 使用枚举类
create_time1 = Column(Date)
create_time2 = Column(DateTime)
create_time3 = Column(Time)
content1 = Column(Text)
# 创建数据库表
Base.metadata.create_all(engine)
# 新增数据到表 news 中
a1 = News(price1=1000.0078,
price2=1000.0078,
title='测试数据',
is_delete=False,
tag1="PYTHON",
tag2=TagEnum.python,
create_time1=date(2018, 12, 12),
create_time2=datetime(2019, 2, 20, 12, 12, 30),
create_time3=time(hour=11, minute=12, second=13),
content1="hello")
session.add(a1)
session.commit()
# 关闭会话
session.close()
Column常用参数
primary_key:True设置某个字段为主键。
autoincrement:True设置这个字段为自动增长的。
default:设置某个字段的默认值。在发表时间这些字段上面经常用。
nullable:指定某个字段是否为空。默认值是True,就是可以为空。
unique:指定某个字段的值是否唯一。默认是False。
onupdate:在数据更新的时候会调用这个参数指定的值或者函数。在第一次插入这条数据的时候,不会用onupdate的值,只会使用default的值。常用于是 update_time 字段(每次更新数据的时候都要更新该字段值)。
name:指定ORM模型中某个属性映射到表中的字段名。如果不指定,那么会使用这个属性的名字来作为字段名。如果指定了,就会使用指定的这个值作为表字段名。这个参数也可以当作位置参数,在第1个参数来指定。
class News(Base): __tablename__ = 't_news2' id = Column(Integer,primary_key = True,autoincrement = True) phone = Column(String(11),unique = True) title = Column(String(32),nullable =False) # 不能为空 read_count = Column(Integer,default=1) create_time = Column(DateTime,default =datetime.now) update_time = Column(DateTime,default =datetime.now, onupdate =datetime.now ) # 当数据更新后,参数的内容才会更改
onupdate一直记录最新时间,default只在创建的时候记录,unique与表里的其他数据不能重复
query函数
-
直接传类的名字,就是全表查询
-
指定字段,就是表的哪一列
-
聚合函数:对里面的数据进行统计
- func.count:统计行的数量。
- func.avg:求平均值。
- func.max:求最大值。
- func.min:求最小值。
- func.sum:求和。
-
生成数据
from random import randint
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker
# 数据库连接参数
HOST = '127.0.0.1'
PORT = 3306
DATA_BASE = 'flask_db'
USER = 'root'
PWD = 'root'
DB_URI = f'mysql+pymysql://{USER}:{PWD}@{HOST}:{PORT}/{DATA_BASE}'
engine = create_engine(DB_URI)
Base = declarative_base()
Session = sessionmaker(engine)
class Person(Base):
__tablename__ = 't_person1'
id = Column(Integer, primary_key=True, autoincrement=True)
age = Column(Integer)
def create_data_one():
with Session() as session:
for i in range(10):
p = Person(age=randint(0,100))
session.add(p)
session.commit() # 提交session对象
if __name__ == '__main__':
Base.metadata.create_all(engine)
create_data_one()
- 获取随机数
def query_data_all():
with Session() as session:
all_person = session.query(Person).all()
for p in all_person:
print(p.age)
if __name__ == '__main__':
Base.metadata.create_all(engine)
query_data_all()
- 统计数据
p1 = session.query(func.count(Person.age)).first()
- 获取最大值
p1 = session.query(func.max(Person.age)).first()
- 其他
def query_func():
with Session() as session:
# p1 = session.query(func.count(Person.age)).first()
# p1 = session.query(func.max(Person.age)).first()
# p1 = session.query(func.avg(Person.age)).first()
p1 = session.query(func.sum(Person.age)).first()
print(p1)
Person.age就是指定的列
filter过滤数据
和query一样,作用都是过滤数据。但是filter更专业
- equals 精准匹配
- query(News).filter(News.title ==“title1”).first()
- not equals
- query(User).filter(User.name != ‘ed’)
- like & ilike [不区分大小写] 模糊匹配
- query(User).filter(User.name.like(‘%ed%’))
- in 在不在某个范围内
- query(User).filter(User.name.in_([‘ed’,‘wendy’,‘jack’]))
- not in
- query(User).filter(~User.name.in_([‘ed’,‘wendy’,‘jack’]))
- is null 是否为空
- query(User).filter(User.name==None)
- query(User).filter(User.name.is_(None))
- is not null
- query(User).filter(User.name != None)
- query(User).filter(User.name.isnot(None))
- and 多个条件拼接的,在filter里面也可以使用逗号代替and
- or 满足一个条件即可
表关系ForeignKey
表之间的关系存在三种:一对一、一对多、多对多。
而SQLAlchemy中的ORM也可以模拟这三种关系。因为一对一其实在SQLAlchemy中底层是通过一对多的方式模拟的,所以先来看下一对多的关系:外键:使用SQLAlchemy创建外键非常简单。在从表中增加一个字段,指定这个字段外键的是哪个表的哪个字段就可以了。从表中外键的字段,必须和主表的主键字段类型保持一致。
Column(Integer,ForeignKey('t_user.id',)
- 外键约束有以下几项:
RESTRICT:若子表中有父表对应的关联数据,删除父表对应数据,会阻止删除。 (父表的数据如果被子表使用,就不能删除)
NO ACTION:在MySQL中,同RESTRICT。
CASCADE:级联删除。 (删除父表数据,子表关联的数据也删除)级联删除是 删除一整条记录,而不是一列数据。
SET NULL:父表对应数据被删除,子表对应数据项会设置为NULL。 这个是删除一列。
一对多relationship
mysql级别的外键,还不够爽,必须拿到一个表的外键,然后通过这个外键再去另外一张表中查找,这样太麻烦了。
SQLAlchemy提供了一个 relationship ,这个类可以定义属性,以后在访问相关联的表的时候就直接可以通过属性访问的方式就可访问得到了。
另外,可以通过 backref 来指定反向访问的属性名称。newss是指有多篇新闻。他们之间的关系是一个“一对多”的关系
relationship让父表与子表互相查询
一对多,要让多的那边的表去绑定一ForeignKey。
想要让子表通过外键去主表拿数据,需要使用relationship。在子表里面user = relationship(‘user’) 子表就能拿主表数据了,子表就可以直接使用点的方式获取值了。p1.user 获取主表数据。使用baekref=‘news’还可以把子表数据通过外键给主表。
一对一
场景:用户的完整信息=用户登录信息+用户基本信息。
在登录验证的时候只需要用户登录信息表。在查看用户信息的时候,再二表合一组成完整的表。
在一个表里面创建外键,两个表都要使用relationship互相绑定。外键是建立表关系。绑定只是能互相查询。
from sqlalchemy import Column,Integer,String,Text,ForeignKey from sqlalchemy.orm import relationship,backref from db_util import Base,Session class LoginUser(Base): __tablename__ = 't_user_login' id =Column(Integer,primary_key=True,autoincrement=True) uname =Column(String(32),nullable=False) passwd =Column(String(32),nullable=False) # user =relationship('User',uselist=False) # 不友好,总有警告 def __repr__(self): return f'<User: id={self.id} uname={self.uname} passwd={self.passwd}>' class User(Base): __tablename__ = 't_user' id =Column(Integer,primary_key=True,autoincrement=True) name =Column(String(32),nullable=False,name='name') gender = Column(String(1)) address = Column(String(64)) # 创建1对1的关系, 创建一个字段来做别一个表的标识(外键) login_id =Column(Integer,ForeignKey('t_user_login.id')) login_user =relationship('LoginUser',backref=backref('user',uselist=False)) def __repr__(self): return f'<User: id={self.id} name={self.name} gender={self.gender} address={self.address}>' def create_data(): login = LoginUser(uname = 'baizhan',passwd = '123') user = User(name='百战',gender ='女',address ='北京') # login.user = user # 建立关联关系 user.login_user = login with Session() as ses: ses.add(user) ses.commit() def query_data(): # with Session() as ses: # login =ses.query(LoginUser).first() # print(login.user) with Session() as ses: user = ses.query(User).first() print(user.login_user) if __name__ == '__main__': # Base.metadata.create_all() # create_data() query_data()
改写repr方法,就可以在控制台打印我们想要的信息了。
查询的时候,因为两个表都互相绑定了,所以任意一个表都可以拿来访问所以信息。因为是一对一,就是一个人的信息。
- db_util文件
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base,sessionmaker
# 数据库连接参数
HOST = '127.0.0.1'
PORT = 3306
DATA_BASE = 'flask_db'
USER = 'root'
PWD = 'root'
DB_URI = f'mysql+pymysql://{USER}:{PWD}@{HOST}:{PORT}/{DATA_BASE}'
engine = create_engine(DB_URI)
Base = declarative_base()
Session = sessionmaker(engine)
多对多
第三个表专门处理两个表之间的关系。一个新闻多个标签,一个标签多个新闻
多对多的关系需要通过一张中间表来绑定他们之间的关系。
先把两个需要做多对多的模型定义出来
使用Table定义一个中间表,中间表一般就是包含两个模型的外键字段就可以了,并且让他们两个来作为一个“复合主键”
在两个需要做多对多的模型中随便选择一个模型,定义一个relationship属性,来绑定三者之间的关系,在使用relationship的时候,需要传入一个secondary=中间表对象名from sqlalchemy import Column,Integer,String,ForeignKey from sqlalchemy import Table from sqlalchemy.orm import relationship,backref from db_util import Base,Session # 创建第3张表,来建立多对多关系 # 放到2个模型之上 news_tag = Table( 't_news_tag', # 表的名字 Base.metadata, # 固定格式 Column('news_id',Integer,ForeignKey('t_news.id'),primary_key = True), # 关联new的id,并且设有主键(防止重复 Column('tag_id',Integer,ForeignKey('t_tag.id'),primary_key = True), ) class News(Base): __tablename__ = 't_news' id =Column(Integer,primary_key=True,autoincrement=True) title =Column(String(32),nullable=False) tags =relationship('Tag',backref='newss',secondary= news_tag) # 第一个参数是连接的第二个表,第二个参数是给第二个表返回第一个表的信息,第三个参数是关联的第三个表 def __repr__(self): return f'<News: id={self.id} title={self.title}>' class Tag(Base): __tablename__ = 't_tag' id =Column(Integer,primary_key=True,autoincrement=True) name = Column(String(32),nullable=False) # news =relationship('News',backref='tags',secondary= news_tag) def __repr__(self): return f'<Tag: id={self.id} name={self.name}>' def create_data(): news1 = News(title = 'Python更新了!') news2 = News(title = 'SQLAlchemy功能又强大了!') tag1 = Tag(name = 'IT新闻') tag2 = Tag(name = '科学技术') news1.tags.append(tag1) news1.tags.append(tag2) news2.tags.append(tag1) news2.tags.append(tag2) with Session() as ses: ses.add(news1) ses.add(news2) ses.commit() def query_data(): with Session() as ses: news = ses.query(News).first() print(news.tags) if __name__ == '__main__': # Base.metadata.create_all() create_data() query_data()
删除数据注意事项
ORM层面删除数据,会无视mysql级别的外键约束。
直接会将对应的数据删除,然后将从表中的那个外键设置为NULL,也就是数据库的 SET NULL 。
如果想要避免这种行为,应该将从表中的外键的 nullable=False 。
默认:删除主表的时候,会把子表关联主表外键的列,置null
在子表里面的外键那一列,设置nullable=False,主表就删不了了
relationship的cascade
就是级联操作
cascade属性值为:
save-update:默认选项。在添加一条数据的时候,会把其他和他相关联的数据都添加到数据库中。
delete:表示当删除某一个模型中的数据的时候,是否也删掉使用relationship和他关联的数据。
delete-orphan:表示当对一个ORM对象解除了父表中的关联对象的时候,自己便会被删除掉。当然如果父表中的数据被删除,自己也会被删除。这个选项只能用在一对多上,并且还需要在子模型中的relationship中,增加一个single_parent=True的参数。
merge:默认选项。当在使用session.merge,合并一个对象的时候,会将使用了relationship相关联的对象也进行merge操作。
expunge:移除操作的时候,会将相关联的对象也进行移除。这个操作只是从session中移除,并不会真正的从数据库中删除。
all:是对save-update, merge, refresh-expire, expunge, delete几种的缩写。
cascade为空的时候,就不会关联了。
默认是save-update,级联添加和删除。
delete是关联的数据的那一列置空
delete-orphan和single_parent=True,需要同时使用。删除级联数据的时候,两个表都清空
排序
order_by方法排序:可以指定根据模型中某个属性进行排序,"模型名.属性名.desc()"代表的是降序排序。
relationship的方法中order_by属性:在指定relationship方法的时候,添加order_by属性来指定排序的字段。
# 升序 users = ses.query(User).order_by(User.age).all() # 降序 users = ses.query(User).order_by(User.age.desc()).all()
_mapper_args_ 参数的1.1版本已被抛弃
分页
limit:可以限制查询的时候只查询前几条数据。 属top-N查询
offset:可以限制查找数据的时候过滤掉前面多少条。可指定开始查询时的偏移量。
切片:可以对Query对象使用切片操作,来获取想要的数据。
可以使用 slice(start,stop) 方法来做切片操作。 这个是python自带的
也可以使用 [start:stop] 的方式来进行切片操作。
一般在实际开发中,中括号的形式是用得比较多的。def query_by_limit(): with Session() as ses: newss = ses.query(News).limit(3).all() # 只获取前3条 for n in newss: print(n) def query_by_offset(): with Session() as ses: newss = ses.query(News).offset(3).all() # 跳过前3条,再获取全部 for n in newss: print(n) def query_by_slice(): with Session() as ses: # 从哪个索引开始,到哪个索引结束 newss = ses.query(News).slice(3,6).all() for n in newss: print(n) def query_by_page(): # limit 获取N个数据 # offset 跳过n数据 # 分页效果 1-3 4-6 7-9 # 3 0 1 (pagenum-1)*pagesize # 3 3 2 (2-1)*3 = 3 # 3 6 3 (3-1)*3 = 6 # 3 9 4 (4-1)*3 = 6 with Session() as ses: # (pagenum-1)*pagesize newss = ses.query(News).limit(3).offset(3).all() # offset跳过前3个,limit获取三个 for n in newss: print(n)
懒加载
在一对多,或者多对多关系的时候,如果想要获取多的一方这一部分的数据的时候,往往能通过一个属性就可以全部获取了。
如有一个作者,想要这个作者的所有文章,通过user.articles就可以获取所有的
但有时候我们不想获取所有的数据,如只想获取这个作者今天发表的文章,那么这时候我们可以给relationship方法添加属性lazy=‘dynamic’ ,以后通过 user.articles 获取到的就不是一个列表,而是一个AppenderQuery对象了。这样就可以对这个对象再进行一层过滤和排序等操作通过 lazy=‘dynamic’ ,获取出来的多的那一部分的数据,就是一个AppenderQuery 对象了。这种对象既可以添加新数据,也可以跟 Query 一样,可以再进行一层过滤
lazy可用的选项:
1select : (默认) 后台会用select语句一次性加载所有数据,即访问到属性的时候,就会全部加载该属性的数据
2joined - 数据会被JOIN语句加载,即对关联的两个表进行join操作,从而获取到所有相关的对象
3subquery - 数据被用subquery子查询SQL语句加载4 dynamic :这个也是懒加载。在访问属性的时候,并不在内存中加载数据,而是返回一个 AppenderQuery 对象, 需要执行相应方法才可以获取对象。适用于数据量大的时候
注意
lazy=“dynamic” 只可以用在一对多和多对对关系中,不可以用在一对一和多对一中。这样也合理:如果返回结果很少的话,就没必要延迟加载数据了。
分组
group_by
根据某个字段进行分组。如想要根据年龄进行分组,来统计每个分组分别有多少人
r =session.query(User.age,func.count(User.id)).group_by(User.age).all()
having
having是对分组查找结果作进一步过滤。如只想要看未成年人的人数,那么可以首先对年龄进行分组统计人数,然后再对分组进行having过滤。
r =session.query(User.age,func.count(User.id)).group_by(User.age).having(User.age < 18).all()
join多表查询
mysql中的外连接、内连接
两个表同时查询,以哪个表为主表,哪个表的数据就全。另一个表没有就显示空
left join是以A表的记录为基础的,A可以看成左表,B可以看成右表,leftjoin是以左表为准的.
换句话说,左表(A)的记录将会全部表示出来,而右表(B)只会显示符合搜索条件的记录(例子中为: A.aID = B.bID).
B表记录不足的地方均为NULL
- join的使用_高级查询
1 join分为left join(左外连接)和right join(右外连接)以及内连接(等值连接)。
2 在sqlalchemy中,使用join来完成内连接。在写join的时候,如果不写join的条件,那么默认将使用外键来作为条件连接。
3 查询出来的字段,跟join后面的东西无关,而是取决于query方法中传了什么参数。(模型名=全表;模型名.属性=表名.字段)。
4 在sqlalchemy中,使用outerjoin来完成外连接(默认是左外连接)。
subquery子查询
subquery方法
子查询即select语句中还有select。
那么在sqlalchemy中,要实现一个子查询,需以下几个步骤:
将子查询按照传统的方式写好查询代码,然后在 query 对象后面执行 subquery 方法,将这个查询变成一个子查询。
在子查询中,将以后需要用到的字段通过 label 方法,取个别名。
在父查询中,如果想要使用子查询的字段,那么可以通过子查询的返回值上的 c 属性拿到(c=Column)。
aliased别名
就是领导查询
工号1的上司是工号2,工号2的上司是工号3.工号3没有上司。这些人都在人名单里面
当多表关联查询的时候,有时候同一个表要用到多次,这时候用别名就可以方便的解决命名冲突的问题了
Flask-SQLAlchemy
Flask-SQLAlchemy是一个插件, Flask-SQLAlchemy是对SQLAlchemy进行了一个简单的封装的一个插件,使得我们在flask中使用sqlalchemy更加的简单。
Flask-SQLAlchemy的使用要点
- 数据库连接
数据库初始化不再是通过create_engine。- 1 跟sqlalchemy一样,定义好数据库连接字符串DB_URI。
- 2 将这个定义好的数据库连接字符串DB_URI,通过SQLALCHEMY_DATABASE_URI 这个key名配置到 app.config 中。
app.config["SQLALCHEMY_DATABASE_URI"] = DB_URI
- 3 使用 flask_sqlalchemy.SQLAlchemy 这个类定义一个对象,并将 app 传入进去。
db = SQLAlchemy(app)
- 创建ORM模型类
之前都是通过Base = declarative_base()来初始化一个基类,然后再继承,在Flask-SQLAlchemy中更加简单了- 1 还是跟使用sqlalchemy一样,定义模型。现在不再是需要使用 delarative_base 来创建一个基类。而是使用 db.Model 来作为基类
- 2 在模型类中, Column 、 String 、 Integer 以及 relationship 等,都不需要导入了,直接使用 db下面相应的属性名就可以了
- 3 在定义模型的时候,可以不写 tablename ,那么 flask_sqlalchemy 会默认使用当前的模型的名字转换成小写来作为表的名字
并且如果这个模型的名字用到了多个单词并且使用了驼峰命名法,那么会在多个单词之间使用下划线来进行连接,
虽然flask_sqlalchemy给我们提供了这个特性,但是不推荐使用。(增强代码可读性,提高团队合作效率)
将ORM模型映射到数据库表
写完模型类后,要将模型映射到数据库的表中,使用以下代码即可删除数据库表: db.drop_all()
创建数据库表: db.create_all()
session的使用
以后session也不需要使用 sessionmaker 来创建了,直接使用 db.session 就可以了,操作这个session的时候就跟之前的 sqlalchemy 的 session 是一样一样的。添加数据
这时候就可以在数据库中看到已经生成了对应表了
添加数据和之前的没有区别,只是session成为了一个db的属性查询数据
单表查询
查询数据不再是之前的session.query方法了,而是将query属性放在了db.Model上,所以查询就是通过“模型名.query”的方式进行查询了, query 就跟之前的sqlalchemy中的query方法是一样用的。多表查询
如果查找数据涉及多个模型,只能使用db.session.query(模型名).all() 这种方式修改数据
修改数据和之前的没有区别,只是session成为了一个db的属性删除数据
删除数据跟添加数据和修改数据类似,只不过session是db的一个属性而已
数据库迁移工具alembic
alembic介绍
alembic是sqlalchemy的作者开发的,用来做ORM模型与数据库的迁移与映射,
alembic使用方式跟git有点了类似,
alembic的所有命令都是以alembic开头,
alembic的迁移文件也是通过版本进行控制的,
- 使用alembic创建一个仓库(初始化仓库)
1 打开dos系统界面
2 cd到当前项目目录中,注意:如果想要使用alembic,则需要先进入到安装了alembic的虚拟环境中,不然就找不到这个命令。
3 然后执行命令 “alembic init [仓库的名字,推荐使用alembic]”
- 修改配置文件
在 alembic.ini 中,给 sqlalchemy.url 项设置数据库的连接方式。方式跟sqlalchemy的方式是一样的。
sqlalchemy.url = driver://user:pass@localhost/dbname
给 sqlalchemy.url 项设置数据库的连接操作为:
sqlalchemy.url = mysql+pymysql://root:root@localhost/alembic_demo?charset=utf8
为了使用模型类更新数据库,需要在 alembic/env.py 文件中设置target_metadata项,默认为target_metadata=None。需要将 target_metadata 的值设置为模型 Base.metadata ,但是要导入 models使用sys模块和os模块把当前项目的路径导入到path中:
import sys,os sys.path.append(os.path.dirname(os.path.dirname(__file__))) import models
设置target_metadata项操作为
target_metadata = models.Base.metadata
- 自动生成迁移文件
使用alembic revision --autogenerate -m "提示信息"将当前模型中的状态生成迁移文件。- 将生成的迁移文件映射到数据库中
使用alembic upgrade head将刚刚生成的迁移文件,真正映射到数据库中。同理,如果要降级,那么使用alembic downgrade head。以后如果修改了模型,重复4、5步骤
Flask-Migrate
封装了alembic。再次升级,简化操作。
项目结构重构
一个良好的项目结构努力可以清晰的看出来各个模块的作用,方便扩展,易于修改
虽然Flask并没有强制要求开发者项目的目录层次结构应该是怎么样的,但是如果我们以包和模块的形式组织项目的话,后期的开发会非常的有条理
|project_name |--templates # 模板文件 |--static # 静态资源文件 |----js # JS脚本 |----css # 样式表 |----img # 图片 |--app.py # 项目启动控制文件 |--migrations # 数据库迁移目录
项目结构
基本结构如下:可根据实际需求做微小调整。|project_name |--pro_name # 整个程序的包目录 |----__init__.py # 项目包文件 |----templates # 模板文件 |------common # 通用模板 |------errors # 错误页面 |------user # 用户模板 |------email # 邮件模板 |----static # 静态资源文件 |------js # JS脚本 |------css # 样式表 |------img # 图片 |------favicon.ico # 网站图表 |----user # 用户模块 |------__init__.py # 用户模块-包文件 |------views.py # 用户模块-视图文件 |----item # 产品模块 |------__init__.py # 产品模块-包文件 |------views.py # 产品模块-视图文件 |----models.py # 数据模型 |--app.py # 项目启动控制文件 |--config.py # 配置文件 |--requirements.txt # 依赖包列表 |--migrations # 数据库迁移目录
AJAX
- AJAX概述
AJAX 即“Asynchronous Javascript And XML”(异步 JavaScript 和XML),是指一种创建交互式、快速动态网页应用的网页开发技术,无需重新加载整个网页的情况下,能够更新部分网页的技术。- 传统网站中存在的问题
网速慢的情况下,页面加载时间长,用户只能等待
表单提交后,如果一项内容不合格,需要重新填写所有表单内容
页面跳转,重新加载页面,造成资源浪费,增加用户等待时间- 解决方案
AJAX它是浏览器提供的一套方法,可以实现页面无刷新更新数据,提高用户浏览网站应用的体验- AJAX的应用场景
页面上拉加载更多数据
列表数据无刷新分页
表单页离开焦点数据验证
搜索框提示文字下拉列表
级联显示- AJAX的运行环境
AJAX技术需要运行在网站环境中才能生效- AJAX 运行原理
AJAX相当于浏览器发送请求与接收响应的代理人,以实现在不影响浏览页面的情况下,局部更新页面数据,从而提高用户体验
使用
就是一个js文件,但是可以异步加载到页面上,并且不刷新页面。就像瀑布流一样
实现通过AJAX发送请求获取数据,需要如下4步:
1 创建AJAX XMLHttpRequest 对象
let xhr = new XMLHttpRequest()
2 设置AJAX请求地址以及请求方式
通过 XMLHttpRequest.open() 方法用于指定 HTTP 请求的参数,或者说初始化 XMLHttpRequest 实例对象。它一共可以接受五个参数。
void open( string method, string url, optional boolean async, optional string user, optional string password );
- method :表示 HTTP 动词方法,比如 GET 、 POST 、 PUT 、 DELETE 、 HEAD等。
url : 表示请求发送目标 URL。
async : 布尔值,表示请求是否为异步,默认为 true 。如果设为 false ,则 send() 方法只有等到收到服务器返回了结果,才会进行下一步操作。该参数可选。由于同步AJAX 请求会造成浏览器失去响应,许多浏览器已经禁止在主线程使用,只允许 Worker里面使用。所以,这个参数轻易不应该设为 false 。
user :表示用于认证的用户名,默认为空字符串。该参数可选。
password :表示用于认证的密码,默认为空字符串。该参数可选。xhr.open('GET','http://www.example.com')
3 发送请求
XMLHttpRequest.send() 方法用于实际发出 HTTP 请求。它的参数是可选的,如果不带参数,就表示 HTTP 请求只包含头信息,也就是只有一个 URL,典型例子就是 GET 请求;如果带有参数,就表示除了头信息,还带有包含具体数据的信息体,典型例子就是POST 请求。
xhr.send()
4 获取服务器端给客户端的响应数据
XMLHttpRequest 对象可以对以下事件指定监听函数
- XMLHttpRequest.onloadstart:loadstart 事件(HTTP 请求发出)的监听函数
- XMLHttpRequest.onprogress:progress事件(正在发送和加载数据)的监听函数
- XMLHttpRequest.onabort:abort 事件(请求中止,比如用户调用了 abort() 方法)的监听函数
- XMLHttpRequest.onerror:error 事件(请求失败)的监听函数
- XMLHttpRequest.onload:load 事件(请求成功完成)的监听函数
- XMLHttpRequest.ontimeout:timeout 事件(用户指定的时限超过了,请求还未完成)的监听函数
- XMLHttpRequest.onloadend:loadend 事件(请求完成,不管成功或失败)的监听函数
- XMLHttpRequest.onreadystatechange: readystatechange事件(当 readyState 属性变化)的监听函数
get请求参数
在get请求中,参数是拼接在url中的,所以此时可以获取到参数,拼接到url即可
function submitForm(){ # 获取用户输入的参数 let name =document.getElementById('name').value let pwd =document.getElementById('pwd').value url = 'http://httpbin.org/get' url = url +"?name="+name+"&pwd="+pwd # 拼接url地址 let xhr = new XMLHttpRequest() xhr.open('get',url) # 发送数据 xhr.send() # 获取响应的数据 xhr.onload = function(){ console.log(xhr.responseText) } }
post
AJAX使用post的请求基本一样。但在传参时,有些不同,参数应该放在body中,并通过XMLHttpRequest.setRequestHeader() 设置请求信息的格式
XMLHttpRequest.setRequestHeader() 方法用于设置浏览器发送的 HTTP 请求的头信息。该方法接受两个参数。第一个参数是字符串,表示头信息的字段名,第二个参数是字段值
XMLHttpRequest.setRequestHeader() 该方法必须在 open() 之后、 send() 之前调用。如果该方法多次调用,设定同一个字段,则每一次调用的值会被合并成一个单一的值发送。
- 字符串传递
function submitForm1(){ // 获取input里面的数据 uname = document.getElementById('uname').value pwd = document.getElementById('pwd').value // 拼接参数 args = 'uname='+uname+'&pwd='+pwd // 创建xhr对象 let xhr = new XMLHttpRequest() // 设置请求的方式与请求地址 xhr.open('POST','http://httpbin.org/post') // 设置请求内容的类型(推荐) xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded') // form表单提交 // 发送请求 xhr.send(args) // 获取服务器响应 xhr.onload = () => { console.log(xhr.responseText) } }
- json传递
function submitForm2(){ // 获取input里面的数据 uname = document.getElementById('uname').value pwd = document.getElementById('pwd').value // 拼接参数 args = {'uname':uname,'pwd':pwd} args = JSON.stringify(args) // json格式转换 // 创建xhr对象 let xhr = new XMLHttpRequest() // 设置请求的方式与请求地址 xhr.open('POST','http://httpbin.org/post') // 设置请求内容的类型(推荐) xhr.setRequestHeader('Content-Type','application/json') // json表单提交 // 发送请求 xhr.send(args) // 获取服务器响应 xhr.onload = () => { console.log(xhr.responseText) } }
get 请求是不能提交 json 对象数据格式的,传统网站的表单提交也是不支持 json 对象数据格式的
处理响应结果
在创建AJAX对象,配置AJAX对象,发送请求,以及接收完服务器端响应数据,这个过程中的每一个步骤都会对应一个数值,这个数据就是AJAX状态码
XMLHttpRequest.readyState 返回一个整数,表示实例对象的当前状态。该属性只读。它可能返回以下值。
0:表示 XMLHttpRequest 实例已经生成,但是实例的 open() 方法还没有被调用。
1:表示 open() 方法已经调用,但是实例的 send() 方法还没有调用,仍然可以使用实例的setRequestHeader() 方法,设定 HTTP 请求的头信息。
2:表示实例的 send() 方法已经调用,并且服务器返回的头信息和状态码已经收到。
3:表示正在接收服务器传来的数据体(body 部分)。这时,如果实例的 responseType 属性等于 text 或者空字符串, responseText 属性就会包含已经收到的部分信息。
4:表示服务器返回的数据已经完全接收,或者本次接收已经失败。HTTP状态码
XMLHttpRequest.status 属性返回一个整数,表示服务器回应的 HTTP 状态码。一般来说,如果通信成功的话,这个状态码是200;如果服务器没有返回状态码,那么这个属性默认是200。请求发出之前,该属性为 0 。该属性只读
在 XMLHttpRequest.onreadystatechange 事件中,我们规定当服务器响应已做好被处理的准备时,再执行任务
function get_data(){ // 创建xhr对象 let xhr = new XMLHttpRequest() // 设置请求的方式与请求地址 xhr.open('get','http://httpbin.org/get') // 发送请求 xhr.send() // 获取服务器响应 xhr.onreadystatechange = () => { // 判断ajax状态码是否为4 if (xhr.readyState == 4){ // 判断Http状态码是否为200 if (xhr.status == 200){ // console.log(xhr.responseText) // 将数据转换成json类型对象 data = JSON.parse(xhr.responseText) console.log(data) }else{ // 如果没有正常响应做出处理 console.log(xhr.status) } }else{ console.log(xhr.readyState) } } }
服务器端响应的数据格式
在真实的项目中,服务器端大多数情况下会以JSON对象作为响应数据的格式。
在http请求与响应的过程中,无论是请求参数还是响应内容,如果是对象类型,一般都会被转换为对象字符串进行传输
JSON.parse() //将json字符串转换为json对象
错误处理
网络畅通,服务器端能接收到请求,服务器端返回的结果不是预期的结果。可以判断服务器端返回的状态码,分别进行处理。
xhr.status获取http状态码
网络畅通,服务器端没有接收到请求,返回404状态。检查请求地址是否错误
网络畅通,服务器端能接收到请求,服务器端返回500状态码
网络中断,请求无法发送到服务器端在AJAX中设置一个专门处理请求失败的监听函数 XMLHttpRequest.onerror :error 事件(请求失败)的监听函数
封装
问题
发送一次请求代码过多,发送多次就会请求代码冗余重复解决方案
将请求代码封装到函数中,发请求时调用函数即可封装后的ajax使用
AJAX({ type:'get', url:'http://www.example.com', success:function(data){ console.log(data) } })
- ajax原型
function AJAX(options){ // options 是一个json数据 ,包含type\url // 创建一个XHR对象 let xhr = new XMLHttpRequest() // 配置对象 // xhr.open('get','http://httpbin.org/get') xhr.open(options.type,options.url) // 发送请求 xhr.send() // 获取响应 xhr.onreadystatechange = ()=>{ // 判断是否完成请求 if (xhr.readyState == 4 && xhr.status == 200){ options.success(xhr.responseText) } } }
请求参数
function AJAX(options){
/**
* options:
* type:请求的方式
* url:请求的地址
* data:请求的参数,类型是JSON
*/
let xhr = new XMLHttpRequest()
// 处理请求的参数
let params = ''
for(let key in options.data){
value = options.data[key]
// name=sxt&
params = params + key+'='+value+'&'
}
// 去掉最后一个&
params = params.substr(0,params.length-1)
// get请求拼接参数(传递参数)
if(options.type == 'get'){
options.url = options.url+'?'+params
}
xhr.open(options.type,options.url)
// post传递参数
if(options.type =='post'){
xhr.setRequestHeader('ContentType','application/x-www-form-urlencoded')
xhr.send(params)
}else{
xhr.send()
}
xhr.onreadystatechange = ()=>{
if (xhr.readyState == 4 && xhr.status ==200){
console.log(xhr.responseText)
}
}
}
function get_data_get(){
AJAX({
type:"get",
url:"http://httpbin.org/get",
data:{"name":"sxt","pwd":123}
})
}
function get_data_post(){
AJAX({
type:"post",
url:"http://httpbin.org/post",
data:{"name":"sxt","pwd":123}
})
}
处理响应
当AJAX发送了请求后,AJAX都应该回馈一个内容。这时就会分如下2个情况:
失败处理
可以通过onerror监听函数处理错误情况,代码如下function AJAX (options) { var xhr = new XMLHttpRequest() var parmas = '' for(var atr in options.data){ params += atr + '=' + options.data['atr'] + '&' } params = params.substr(0,params.length-1) // 去掉最后一个& if (options.type == 'get'){ options.url = options.url + '?' + params } xhr.open(options.type,options.url) if (options.type == 'post'){ xhr.setRequestHeader('ContentType','application/x-www-form-urlencoded') xhr.send(params) }else{ xhr.send() } xhr.onreadystatechange = function(){ if (xhr.readyState==4 && xhr.status==200){ options.success(xhr.responseText,xhr) }else{ options.error(xhr.responseText,xhr) } } }
处理返回数据
请求成功能可以,通过onreadystatechange监听函数处理。以下是可以使用的属性XMLHttpRequest.response
- XMLHttpRequest.response 属性表示服务器返回的数据体(即 HTTP 回应的 body 部分)。它可能是任何数据类型,比如字符串、对象、二进制对象等等,具体的类型由XMLHttpRequest.responseType 属性决定。该属性只读
- 如果本次请求没有成功或者数据不完整,该属性等于 null 。但是,如果 responseType 属性等于 text 或空字符串,在请求没有结束之前( readyState 等于3的阶段), response 属性包含服务器已经返回的部分数据。
XMLHttpRequest.responseType
- XMLHttpRequest.responseType 属性是一个字符串,表示服务器返回数据的类型。这个属性是可写的,可以在调用 open() 方法之后、调用 send() 方法之前,设置这个属性的值,告诉服务器返回指定类型的数据。如果 responseType 设为空字符串,就等同于默认值 text 。
- XMLHttpRequest.responseType 属性可以等于以下值。
- ”“(空字符串):等同于 text ,表示服务器返回文本数据。
- “arraybuffer”:ArrayBuffer 对象,表示服务器返回二进制数组。
“blob”:Blob 对象,表示服务器返回二进制对象。
“document”:Document 对象,表示服务器返回一个文档对象。
“json”:JSON 对象。
“text”:字符串。上面几种类型之中, text 类型适合大多数情况,而且直接处理文本也比较方便。 document 类型适合返回 HTML / XML 文档的情况,这意味着,对于那些打开 CORS 的网站,可以直接用AJAX 抓取网页,然后不用解析 HTML 字符串,直接对抓取回来的数据进行 DOM 操作。 blob 类型适合读取二进制数据,比如图片文件
XMLHttpRequest.responseText
XMLHttpRequest.responseText 属性返回从服务器接收到的字符串,该属性为只读。只有HTTP 请求完成接收以后,该属性才会包含完整的数据XMLHttpRequest.responseXML
XMLHttpRequest.responseXML 属性返回从服务器接收到的 HTML 或 XML 文档对象,该属性为只读。如果本次请求没有成功,或者收到的数据不能被解析为 XML 或 HTML,该属性等于null 。
该属性生效的前提是 HTTP 回应的 Content-Type 头信息等于 text/xml 或 application/xml 。这要求在发送请求前, XMLHttpRequest.responseType 属性要设为 document 。如果 HTTP回应的 Content-Type 头信息不等于 text/xml 和 application/xml ,但是想从 responseXML 拿到数据(即把数据按照 DOM 格式解析),那么需要手动调用XMLHttpRequest.overrideMimeType() 方法,强制进行 XML 解析。
该属性得到的数据,是直接解析后的文档 DOM 树。XMLHttpRequest.responseURL
XMLHttpRequest.responseURL 属性是字符串,表示发送数据的服务器的网址这个属性的值与 open() 方法指定的请求网址不一定相同。如果服务器端发生跳转,这个属性返回最后实际返回数据的网址。另外,如果原始 URL 包括锚点(fragment),该属性会把锚点剥离。
function AJAX (options) { var xhr = new XMLHttpRequest() var parmas = '' for(var atr in options.data){ params += atr + '=' + options.data['atr'] + '&' } params = params.substr(0,params.length-1) // 去掉最后一个& if (options.type == 'get'){ options.url = options.url + '?' + params } xhr.open(options.type,options.url) if (options.type == 'post'){ xhr.setRequestHeader('ContentType','application/x-www-form-urlencoded') xhr.send(params) }else{ xhr.send() } xhr.onreadystatechange = function(){ if (xhr.readyState==4 && xhr.status==200){ var contentType = xhr.getResponseHeader('Content-Type') // 下面是两种响应结果 if (contentType.includes('application/json')){ respText = JSON.parse(xhr.responseText) options.success(respText) }else{ options.success(xhr.responseText) } }else{ options.error(xhr.responseText,xhr) } } }
默认值
function AJAX (options) {
var defaults ={
type:'get',
url:'',
async:true,
data:{}
success:function(data){
},
error:function(data,xhr){
}
}
Object.assign(defaults,options)
}
就是在之前的基础上,再加上一个defaults对象。还有Object.assign方法。这个方法可以把我们后面调用ajax的对象进行替换。就是有冲突的变量进行替换。比如default里面的type是get。但是我们再调用的时候是post,它就会覆盖type属性。
JQuery&AJAX
JQuery的线上地址:
<script src="https://code.jquery.com/jquery-3.6.0.js"></script>
jQuery.AJAX([settings])
– type 请求方式
– url 请求地址
– data 参数
– contentType 请求类型
– beforSend 发送请求前可修改 XMLHttpRequest对象的函数,如添加自定义 HTTP 头
– success 请求成功的处理
– error
在js里面使用
$.ajax({
type:'post',
url:'http://httpbin.org/post',
success: function(resp){
console.log(resp)
}
})
- data的传递方式:
- 传递str数据
$.AJAX({
type:'post',
url:'http://httpbin.org/get',
data : 'name=zs&age=18'
})
- json的形式传递
$.AJAX({
type:'post',
url:'http://httpbin.org/post',
data:{
name:'zs',
age:18
}
})
- beforeSend的使用
主要应用于发送请求前的处理:
• 获取前 将div设置面加载…
• 提交表单前 验证数据
如果在函数中返回ture继续执行发送请求,如果返回flase取消发送
$.AJAX({
type:'post',
url:'http://httpbin.org/get',
data:{
name:'zs',
age:18
}
success: function(resp){
console.log(resp)
}
beforeSend:function(){
alert("请求不会被发送")
return false
}
})
就是在请求之前执行的函数beforeSend。可以进行判断,或者处理内容。 也可以使用if判断,结果不满意就return false。如果return false,就不会发送请求了。
jquery给get和post封装了方法
GET请求
$.get(url,data,function(resp))
里面的函数专门处理请求成功的操作
post也一样。
就是可以省略type类型。
传参的时候要注意参数位置
Graphql
官网:https://graphql.cn/
GraphQL 是Facebook于 2012 年在内部开发的数据查询语言,在 2015 年开源
是一种网站架构,一种前后端通信规范
革命性的API工具
不像传统的RESTful那样只能呆板地在服务端进行预定义
-
GraphQL VS RESTful
-
相同点
都有资源这个概念,而且都能通过ID去获取资源
都可以通过HTTP GET方式来获取资源
都可以使用JSON作为响应格式
-
差异点
在RESTful中,你所访问的路径就是该资源的唯一标识(ID);在GraphQL中,该标识与访问方式并不相关
在RESTful中,资源的返回结构与返回数量是由服务端决定;在GraphQL,服务端只负责定义哪些资源是可用的,由客户端自己决定需要得到什么资源
使用
这个是python里面使用的方法 GraphQL Code Libraries, Tools and Services
from graphene import ObjectType, String, Schema class Query(ObjectType): """定义一个字符串属性域hello""" hello = String() # 等号左边是方法名,等号右边是方法的类型 def resolve_hello(root,info): # 这是调用方法执行的函数 return f'Hello World!' schema = Schema(query=Query) # 注册类 if __name__ == '__main__': query_string = ''' { hello # 去调用hello方法 } ''' result = schema.execute(query_string) # 获取返回值 print(result.data) # 打印返回值的信息
这里有两部分,一个是基于graphene的class类,一个是调用graphene的方式
首先class继承graphene里面的ObjectType,然后是hello方法。后面的hello的返回类型string。
然后是hello绑定的函数resolve_hello。里面至少有两个参数,第一个root是self,第二个参数info是调用的解释器类型
schema = Schema(query=Query)是类似注册的意思
后面就是调用这个方法了。使用query_string加一个字符串,hello是我们要调用的graphene方法。
属性
name 起别名,在前端只能使用别名访问
required 必填项
default_value 默认值
class Query(ObjectType):
"""定义一个字符串属性域hello"""
hello = String(name = 'text',sex=String(default_value="man"),desc = String(required = True))
def resolve_hello(root,resolve,sex,desc = 'python'):
return f'Hello World! sex:{sex} desc: {desc}'
我们在hello里面定义了sex属性,那么在resolve_hello必须有参数去接收。必须是同名变量。
而且resolve_hello的自定义参数,只能在info后面,如果自定义的参数在info前面,那么前面的自定义参数无效
self和info之间参数都会被忽略。info是一个占位符,info可以随便起名info222等等
数据类型
-
graphene.String
-
graphene.Int
-
graphene.Float
-
graphene.Boolean
-
graphene.ID
-
graphene.Date
-
graphene.DateTime
-
graphene.Time
-
graphene.Decimal
-
graphene.JSONString
-
数据返回不能为空
user_name = graphene.String(required = True)
客户端不能用空值请求score_list2 = graphene.NonNull(graphene.List(graphene.Int))
客户端不能用空值请求
接口
需要继承graphene.Interface类
class Animal(graphene.Interface):
id = graphene.ID(required=True)
name = graphene.String(required=True)
累了,不想听了,搞项目去。记录一下140