- Django框架开发
- Django简介
- Django简单实例
- 静态文件配置
- 路由控制器
- 视图层(视图函数)
- 模板语法———>变量,标签
- 模型层ORM
- Ajax请求
- 分页器
- forms组件
- HTTP协议无状态保存
- 用户认证组件
- 中间件
- ImageField 和 FileField
- Django3的ASGI
- 关于ASGI
Django框架开发
web应用程序框架
web应用程序本质:
- web server gateway interface
- wsgi只是一个协议规定了两个规范确定客户端请求如何到达服务应用的规范以及服务应用如何把处理的结果返回
解析请求数据,封装响应数据
原理:
1. 浏览器到WSGI Server:浏览器发送的请求会先到WSGI Server。
2. environ:WSGI Server会将HTTP请求中的参数等信息封装到environ(一个字典)中。
3. WSGI Server到WSGI App:App就是我们自己编写的后台程序,每个URL会映射到对应的入口处理函数(或其他可调用对象),WSGI Server调用后台App时,会将environ和WSGI Server中自己的一个start_response函数注入到后台App中。
4. 逻辑处理:后台函数(或其他可调用对象)需要接收environ和start_response,进行逻辑处理后返回一个可迭代对象,可迭代对象中的元素为HTTP正文。
5. WSGI App到WSGI Server:后台函数处理完后,会先调用start_response函数将HTTP状态码、报文头等信息(响应头)返回给WSGI Server,然后再将函数的返回值作为HTTP正文(响应body)返回给WSGI Server。
6. WSGI Server到浏览器:WSGI Server将从App中得到的所有信息封装为一个response返回给浏览器。
from wsgiref.simple_server import make_server
def application(environ,start_response):
# 按照http协议解析数据:environ 解析的数据是字典类型
# 按照http封装响应数据:start_response
path = environ.get("PATH_INFO")#当前请求路径
start_response("200 OK",[])
if path == "/login":
with open("login.html","r") as f:
data = f.read()
elif path == "/index":
with open("index.html","r") as f:
data = f.read()
return[data.encode("utf8")]
httpd = make_server("127.0.0.1","8080",application) # 服务端地址 端口 回调函数 封装socket
httpd.serve_forever() # 等待连接,有连接的时候执行application函数
from wsgiref.simple_server import make_server
def application(environ,start_response):
path = environ.get("PATH_INFO")
start_response("200 OK",[('content-type','text/html')])
if path == "/login":
with open("login.html","r") as f:
data = f.read()
elif path == "/index":
with open("index.html","r") as f:
data = f.read()
return[data.encode("utf8")]
httpd = make_server("127.0.0.1","8080",application)
httpd.serve_forever()
Django简介
MVC与MTV
web服务器开发领域里著名的mvc模式,就是把web应用分为模型(M),控制器(C),和视图(V)三层,他们之间是以一种插件方式的,松耦合的方式连接在一起的模型负责业务对象与数据库的映射(ORM),视图负责与用户的交互页面,控制器接收用户的输入调用模型和视图完成用户的请求
Django的MTV模式本质上和mvc是一样的,也是为了各组件间保持松耦合关系,只是定义上有些许不同,
Django的MTV分别是指:
- M:模型model :用于和数据库进行交互,负责业务对象和数据库的关系映射(ORM) object relation mapping 对象关系映射
- T:模板template: view将数据传递给模板层进行渲染,负责如何把页面展示给用户(HTML)
- V:视图view: 负责处理业务逻辑和用户的请求已经响应,并在适当的时候调用Model和Template
除了上面的3层外,还需要一个URL分发器,作用是将一个URL的页面请求分发给不同的VIEW处理,view再调用响应的model和template
一般是用户通过游览器像我们的服务器发起一个请求,这个请求会去访问视图函数,(如如果不涉及到数据调用,那么这个时候视图函数返回一个模板也就是一个页面给用户,)视图函数调用模型去数据库查找数据,然后逐级返回,视图函数把返回的数据填充到模板中空格中,最后返回给网页用户
Django的下载与基本命令
pip3 install django
创建一个django项目
django-admin.py startproject 项目名
manage.py
- Django项目里面的工具,通过他可以调用django shell 和数据库等,
- manage.py就是django-admin区别是django-admin会被加入到环境变量可以在任何目录下使用,manage.py只能在该项目下使用
mysite文件夹: 这个django项目的全局配置文件,主应用开发目录,保存了项目中的所有开发人员编写的代码, 目录是生成项目时指定的
settings.py
- 包含了项目的默认配置包括数据库信息,调试标志以及一些其他的变量,项目的配置文件
urls.py
- 负责把url模式映射到应用程序,绑定url和视图的映射关系
wsgi.py
- 封装socket模块,就是项目在运行wsgi服务器时的入口文件
__init__.py
让python将当前目录识别为一个包
asgi.py django3.0以后新增的,用于让django运行在异步编程模式的一个web应用对象
应用文件夹下: models:该应用的模型类模块 views: 该应用的视图模块 tests: 该应用的单元测试模块 apps: 该应用的一些配置,自动生成 admin.py 该应用的后台管理系统配置
在mysite目录下创建应用
python manage.py startapp 应用名
启动项目
python manage.py runserver ip 端口号
不指定端口默认为8000
Django简单实例
1.创建项目
给前端页面返回用户登录
2.在url控制器里面添加路径信息以及视图函数
3.在视图里面创建相应的函数来处理逻辑关系
4.在templates里面创建相应的html文件
静态文件配置
1.项目目录下新建:static文件夹,用于存放静态文件如css和js文件
2.settings.py文件下增加路径:STATICFILES_DIRS=[os.path.join(BASE_DIR,”static”)]
3.用户访问static路径的时候就会自动访问static文件夹,也就是拼接的路径
4.在做其他配置的时候涉及到static文件夹都要使用别名也就是static
为什么要配置静态文件
1.给用户返回网页,用户在点击的时候可以改变颜色案例
1.在返回的模板文件引入js,实现点击功能
2.用户访问该网站
3.解决办法:
项目目录下新建static文件夹存放js文件并且settings.py文件下增加配置STATICFILES_DIRS=[os.path.join(BASE_DIR,”static”)]
路由控制器
Route路由是一种映射关系,路由是客户端请求的url路径和用户请求的应用程序,(也就是django里面的视图进行绑定映射的一种关系)
路由配置就是根据收到的url路由配置视图函数 在django中所有的路由最终都被保存到 urlpatterns变量中,urlpatterns必须声明在主应用下的urls.py总路由中,这是由配置文件setting设置的
在django运行中,当客户端发的送了一个http请求到服务端,服务端的web服务器会从http协议中提取url地址,从程序内部中找到项目中添加到urlpatterns里面的所有路由信息的url进行遍历匹配,如果相等或者匹配成功,则调用当前url对象的视图方法
注意点:
如果从URL中捕获一个值,只需要在他周围放置一对圆括号()
不需要添加一个前导的反斜杠,因为每个url都有,例,应该是artic而不是/artic
每个正则表达式前面的r是可选的,是用于告诉python这个字符串是原始的字符串中的任何字符都不应该转义
路由控制——-> 有名分组
给分组起个名字
加括号的作用是执行函数的时候会将正则匹配到的值自动传参给函数,如果不加的话就不会传参但是还是根据整个匹配
路由控制———>路由分发
需要引入 include函数
如果一个项目存在多个应用的情况下,不应该将所有的url都写在全局的urls的配置文件中,而是在全局urls中配置路由分发,具体url应该是放在该应用的文件夹下创建urls.py文件,在该文件配置该应用的url
1.在全局urls下配置路由分发
2.在应用下的urls进行具体的路由配置
3.用户访问
路由控制之登录验证实例
1.在全局urls下配置路由分发
2.在应用下的urls中配置具体的路由信息
3.编写登录验证页面
4.编写视图函数来处理登录验证请求
路由控制———>反向解析(模板中配置的)
当urls文件中的路由更改名称后,模板文件里的路径也需要更改比如(form表单的提交地址),否则数据提交不上来,且手动修改路径不好维护,可以通过模板的反向解析来实现
实现
1.在urls路由中给url起个别名
2.在模板文件中将需要写路径地址的地方用{% url ‘别名’ %} 来替代
{% url ‘别名’ %} 语法 是视图函数里的render在渲染的时候执行的,意思是去urls里面找name = 别名的然后找到的路径来替换这个占位,然后将页面返回给客户端
3.客户端访问
路由控制———>反向解析(视图函数配置的)
可以在视图函数中反向解析到urls中的路径信息
1.在urls中为路由起个名字
2.在函数视图中配置
路由控制———>名称空间
如果存在多个app,在做路由分发的时候起的别名重复 了,那么在做反向解析的时候解析就会出错都解析为一个,因为name别名是没有作用域的,django在做解析的时候会在全局顺序搜索,当查到第一个name’指定的url时候会立即返回如:
1.创建两个app,在全局urls进行路由分发
2.在各个应用里面进行详细的urls路由分发
3.在视图函数里面创建视图函数并返回解析的路径
4.用户访问
5.解决办法,使用名称空间来解决
路由控制——->path方法
- 在url控制中如果路径有分组则会向相应的函数视图传参,传参默认是str类型的,如果涉及到类型转换会出现TypeError或者ValueError 如何解决?
- 路由控制中要是多个路径中的匹配规则是一样的,需要重复写多次,如果修改的话也需要修改多次。如何解决?
- 用path解决
使用规则:
使用<>从url中捕获值
捕获值中可以包含一个转换器类型,比如用 < int:name >捕获一个整数变量如果没有转换器,将匹配任何字符串包括/字符
无需添加前导斜杠
Django默认支持5个转换器
str,匹配除了路径分隔符/之外的非空字符串这是默认的
int,匹配正整数包含0
slug,匹配字母数字以及横杠下划线组成的字符串
uuid,匹配格式化的uuid如0751443d-123-123e-1234-6c931e272f00
path,匹配任何非空字符串包含了路径分隔符
自定义转换器
- 新建py文件存放定义的转换器
- 在urls中调用
路由控制———>路由转发器
有时候内置的url转换器并不能满足我们的需求,一次django给我们提供了一个接口可以让我们自己定义自己的url转换器,自己定义路由的匹配规则
from django.urls import register_converter
from django.shortcuts import HttpResponse
#自定义路由转换器
class Mobile(object):
regex = "1[3-9]\d{9}" #必须叫regex,代表规则
#下面的方法名都是固定的
def to_python(self,value):
print(type(value))
#value接收的是根据regex规则匹配成功的值
#将匹配结果传到视图内部使用
#返回什么类型的数据,看需求
return value
def to_url(self,value):
#将匹配结果用于反向解析传值时使用
return value
#register_converter(定义的路由转换器类名,调用时的别名)
register_converter(Mobile,'moile')
在urls中使用自定义的路由转发器
#当路径是index\开头,且后面根的值满足自定义的转换规则的时候,执行视图函数
path('index\<mobile:moile>',视图函数名)
#在视图函数中定义函数的时候要加形参,因为会给函数传参,传参就是to_python方法中return的值如:
def index(request,mobile):
print(mobile)
视图层(视图函数)
一个视图函数(FBV),简称视图,就是一个python函数,接收web请求并返回web响应,响应可以是一张网页的html内容,一个重定向,或者错误代码等等,无论视图本身包含什么逻辑都要返回响应, 还有类视图(CBV)
视图请求对象常用属性:
response 和 request 都是由wsgi进行封装的
def index(request):
# 获取请求方式 GET POST
met = request.method
# 获取所有的get请求的数据字典形式,获取数据首先发送的数据要包含数据,对于get是在地址栏中输入用?分隔
get_data = request.GET
# 获取所有的post请求的数据字典形式,只会把url-encoded格式的数据解析到post中
post_data = request.POST
#获取请求体中的数据
data = request.body
# 获取路径信息,url组成是 协议://ip:端口/路径/?数据,只写协议ip端口匹配到的是跟路径/,要保证路由控制中可以匹配到/
path_data = request.path
#获取请求路径包含了请求参数
data = request.get_full_path()
# 一个字符串,表示提交数据的编码方式如果是None表示用的DEFAULE_CHARSET也就是utf8
encoding_data = request.encoding
# 包含所有的HTTP头部信息是一个字典形式
'''
CONTENT_LENGTH —— 请求的正文的长度(是一个字符串)。
CONTENT_TYPE —— 请求的正文的MIME 类型。
HTTP_ACCEPT —— 响应可接收的Content-Type。
HTTP_ACCEPT_ENCODING —— 响应可接收的编码。
HTTP_ACCEPT_LANGUAGE —— 响应可接收的语言。
HTTP_HOST —— 客服端发送的HTTP Host 头部。
HTTP_REFERER —— Referring 页面。
HTTP_USER_AGENT —— 客户端的user-agent 字符串。
QUERY_STRING —— 单个字符串形式的查询字符串(未解析过的形式)。
REMOTE_ADDR —— 客户端的IP 地址。
REMOTE_HOST —— 客户端的主机名。
REMOTE_USER —— 服务器认证后的用户。
REQUEST_METHOD —— 一个字符串,例如"GET" 或"POST"。
SERVER_NAME —— 服务器的主机名。
SERVER_PORT —— 服务器的端口(是一个字符串)。
从上面可以看到,除 CONTENT_LENGTH 和 CONTENT_TYPE 之外,请求中的任何 HTTP 首部转换为 META
的键时,都会将所有字母大写并将连接符替换为下划线最后加上 HTTP_ 前缀。 所以,一个叫做 X-Bender
的头部将转换成 META 中的 HTTP_X_BENDER 键。
'''
meta_data = request.META
# 类似与字典的对象包含所有的上传文件信息
'''
FILES 中的每个键为<input type="file" name="" /> 中的name,值则为对应的数据。
注意,FILES 只有在请求的方法为POST 且提交的<form> 带有enctype="multipart/form-data" 的情况
下才会包含数据。否则,FILES 将为一个空的类似于字典的对象。
'''
files_data = request.FILES
# 字典形式包含所有的cookie信息,键值都是字符串
cook_data = request.COOKIES
# 类似与字典的对象可以读写,表示当前的会话只有Gjango启用会话支持时候才可以用
session_data = request.session
# 一个AUTH_USER_MODEL 类型的对象表示当前登录的用户,没有用户登录user将设置为
django.contrib.auth.models.AnonymousUser 的一个实例。你可以通过is_authenticated() 区分它们
'''
例如:
if request.user.is_authenticated():
# Do something for logged-in users.
else:
# Do something for anonymous users.
user 只有当Django 启用 AuthenticationMiddleware 中间件时才可用。
------------------------------------------------------------------------
匿名用户
class models.AnonymousUser
django.contrib.auth.models.AnonymousUser 类实现了django.contrib.auth.models.User 接口,
但具有下面几个不同点:
id 永远为None。
username 永远为空字符串。
get_username() 永远返回空字符串。
is_staff 和 is_superuser 永远为False。
is_active 永远为 False。
groups 和 user_permissions 永远为空。
is_anonymous() 返回True 而不是False。
is_authenticated() 返回False 而不是True。
set_password()、check_password()、save() 和delete() 引发 NotImplementedError。
New in Django 1.8:
新增 AnonymousUser.get_username() 以更好地模拟 django.contrib.auth.models.User。
'''
user_data = request.user
return HttpResponse(request.GET)
视图请求对象常用方法:
1.HttpRequest.get_full_path()
返回 path,包含路径信息和数据信息
2.HttpRequest.is_ajax()
如果请求是通过XMLHttpRequest 发起的,则返回True,方法是检查 HTTP_X_REQUESTED_WITH 相应的
首部是否是字符串'XMLHttpRequest'。
大部分现代的 JavaScript 库都会发送这个头部。如果你编写自己的 XMLHttpRequest 调用(在浏览器端),你必须手工设置这个值来让 is_ajax() 可以工作。
如果一个响应需要根据请求是否是通过AJAX 发起的,并且你正在使用某种形式的缓存例如Django 的 cachemiddleware,你应该使用 vary_on_headers('HTTP_X_REQUESTED_WITH') 装饰你的视图以让响应能够正确地缓存。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
{#默认不写是使用当前页面的url,如果只写路径,默认使用当前url的ip和端口#}
<form action="index/" method="post">
年龄: <input type="text" name="age">
姓名:<input type="text" name="name">
<input type="submit">
</form>
</body>
</html>
视图函数响应对象
响应对象主要有四种形式:
- HttpResponse()
HttpResponse()括号内直接跟一个具体的字符串作为响应体,比较直接很简单,所以这里主要介绍后面两种形式。 django服务器接收客户端发送过来的请求后,会将提交上来的数据封装成http request对象传给视图函数,那么视图函数在处理完相关的逻辑后,需要返回一个响应给游览器,这个响应,必须用 httpresponseBase 或 其他的子类的对象,而http request base 和 response base 是用的最多的
常用的属性有:
content :返回的内容
status : 返回的http响应状态码
content-type: 返回的数据的mime类型
设置响应头: response[‘xxx’] =’xxx’
- jsonResponse()
jsonresponse只能解析字典数据,如果是其他数据要加上safe=false,jsonresponse(dict,safe=false)
- render ```python render(request, ‘template_name.html’,context) 结合一个给定的模板和一个给定的上下文字典,并返回一个渲染后的 HttpResponse 对象。
参数: request: 用于生成响应的请求对象。 template_name:要使用的模板的完整名称,可选的参数 context:添加到模板上下文的一个字典。默认是一个空字典。如果字典中的某个值是可调用的,视图将在渲染模板之前调用它。
- redirect()
```python
redirect传递要重定向的一个硬编码的URL
def my_view(request):
...
return redirect('/some/url/')
也可以是一个完整的URL:
def my_view(request):
...
return redirect('http://example.com/')
key:两次请求
1)301和302的区别。
301和302状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取
(用户看到的效果就是他输入的地址A瞬间变成了另一个地址B)——这是它们的共同点。
他们的不同在于。301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;
302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎
会抓取新的内容而保存旧的网址。 SEO302好于301
2)重定向原因:
(1)网站调整(如改变网页目录结构);
(2)网页被移到一个新地址;
(3)网页扩展名改变(如应用需要把.php改成.Html或.shtml)。
这种情况下,如果不做重定向,则用户收藏夹或搜索引擎数据库中旧地址只能让访问客户得到一个404
页面错误信息,访问流量白白丧失;再者某些注册了多个域名的网站,也需要通过重定向让访问这些域名的用户自动跳转到主站点等。
django append——slash:如果我们在地址栏输入url时候如果最后一个/没有输入,就发起访问,django会发起一个重定向给我们补上最后一个/,前提是访问的url路径能匹配上只是缺少/的情况
用redirect可以解释APPEND_SLASH的用法!
模板语法———>变量,标签
模板就是templates文件夹下的html文件,setting中自动配置了temtemplates的路径是在项目的根路径下。django查找模板文件夹的顺序是先去项目目录下查找如果没有就根据app的注册顺序去app目录下查找。
模板引擎是一种可以让开发者把服务端数据填充到html网页中完成渲染效果的技术,他实现了把前端代码和服务端代码分离的作用,让项目中的业务逻辑代码和数据表现代码分离。
django框架中内置了web开发领域非常出名的一个django template模板引擎 DTL.
要在django框架中使用模板引擎把视图函数中的数据更好的展现给客户端,需要完成3个步骤
- 在项目文件中指定保存模板文件的模板目录,一般目录是设置在项目根目录或主应用目录下的
- 在视图中基于django提供的渲染函数绑定模板文件和需要展示的数据变量
- 在模板目录下创建对应的模板文件,并根据模板引擎内置的模板语法,填写输出视图传递过来的数据。
DTL模板文件与普通html文件的区别?
DTL模板文件是一种带有特殊语法的HTML文件,这个HTML文件可以被Django编译,可以传递参数进去,实现数据动态变化,再编译完成后,生成一个普通的html,然后返回给客户端。
html文件:
<h3> hi {{name}} <h3>
views文件:
from django.template.loader import get_template
from django.shortcuts import render
def index(request):
#获取模板文件
template = get_template('index.html')
#获取数据
context = {"name":"si"}
#渲染,找到模板文件中为name的占位符 替换为context字典中 name键对应的值
html = template.render(context,request)
#快捷步骤
return render(request,'index.html',{"name":"si"})
1.渲染变量:{{ }}
深度查询 句点符 用点来完成.
2.过滤器
将视图中render传过来的变量加以修饰
{{obj | filter_name:param}}
{{变量名|过滤器名称:过滤器参数}}
- 自带的过滤器
- default
如果变量是false或者是空,则使用给定的默认值否则使用其他变量
{{value|default:"数据是空"}}
变量value如果是空或者false就使用数据是空
- length
{{value | length}}
返回值的长度。它对字符串和列表都起作用
- data
value=datetime.datetime.now()
{{value | data:"Y-m-d"}}
将value的显示格式修改为2001-12-13的形式
- slice
value="hello world"
{{ value|slice:"2:-1" }} 切片
- truncatechars
如果字符串字符多于指定的字符数量,那么会被截断。截断的字符串将以可翻译成省略号序列(“...”)结尾。
参数:要截断的字符数
{{ value|truncatechars:9 }}
- safe
Django的模板中会对HTML标签和JS等语法标签进行自动转义,原因显而易见,这样是为了安全。但是有的时候我们可能不希望这些HTML元素被转义,比如我们做一个内容管理系统,后台添加的文章中是经过修饰的,这些修饰可能是通过一个类似于FCKeditor编辑加注了HTML修饰符的文本,如果自动转义的话显示的就是保护HTML标签的源文件。为了在Django中关闭HTML的自动转义有两种方式,如果是一个单独的变量我们可以通过过滤器“|safe”的方式告诉Django这段代码是安全的不必转义。比如:
value="<a href="">点击</a>"
{{ value|safe}}
- last
{books|last}
#取books中的最后一个值
过滤器 | 用法 | 代码 |
---|---|---|
last | 获取列表/元组的最后一个成员 | {{liast | last}} |
first | 获取列表/元组的第一个成员 | {{list|first}} |
length | 获取数据的长度 | {{list | length}} |
defualt | 当变量没有值的情况下, 系统输出默认值, | {{str|default=”默认值”}} |
safe | 让系统不要对内容中的html代码进行实体转义 | {{htmlcontent| safe}} |
upper | 字母转换成大写 | {{str | upper}} |
lower | 字母转换成小写 | {{str | lower}} |
title | 每个单词首字母转换成大写 | {{str | title}} |
date | 日期时间格式转换 | {{ value| date:”D d M Y” }} |
cut | 从内容中截取掉同样字符的内容 | {{content | cut:”hello”}} |
list | 把内容转换成列表格式 | {{content | list}} |
add | 加法 | {{num| add}} |
filesizeformat | 把文件大小的数值转换成单位表示 | {{filesize | filesizeformat}} |
join | 按指定字符拼接内容 | {{list| join(“-“)}} |
random | 随机提取某个成员 | {list | random}} |
slice | 按切片提取成员 | {{list | slice:”:-2”}} |
truncatechars | 按字符长度截取内容 | {{content | truncatechars:30}} |
truncatewords | 按单词长度截取内容 | 同上 |
过滤器举例:
视图代码 home.views.py;
def index(request):
"""过滤器 filters"""
content = "<a href='http://baidu.com'>baidu</a>"
# content1 = '<script>alert(1);</script>'
from datetime import datetime
now = datetime.now()
content2= "hello wrold!"
return render(request,"index.html",locals())
# 模板代码,templates/index.html:
{{ content | safe }}
{{ content1 | safe }}
{# 过滤器本质就是函数,但是模板语法不支持小括号调用,所以需要使用:号分割参数 #}
<p>{{ now | date:"Y-m-d H:i:s" }}</p>
<p>{{ conten1 | default:"默认值" }}</p>
{# 一个数据可以连续调用多个过滤器 #}
<p>{{ content2 | truncatechars:6 | upper }}</p>
3.渲染标签{% %}
标签是这样的{%tag%},标签比变更更加复杂,一些在输出中创建文本,一些通过循环或者逻辑来控制流程,一些加载其后的变量将使用的额外信息加载到模板中,一些标签需要开始和结束标签(例如{%tag%}….标签内容…..{%endtag%})
- for 标签
```
{% for i in L%} # 循环函数视图传过来的L变量,每循环一次显示其中的一个元素
{{i}}
{%endfor%}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ {% for i in L%}
{{forloop.counter}} {{i}}
#forloop.counter序号只在循环内有效 {%endfor%}forloop是内置的
{{forloop.first}} # 判断是否是第一次循环,返回值是True或False
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ {% for i in L%}
{{forloop.counter}} {{i}}
{%empty%} #如果L为空话则无法输出信息,使用empty可以实现列L为空的时候想输出的信息列表为空
{%endfor%}
循环中, 模板引擎提供的forloop对象,用于给开发者获取循环次数或者判断循环过程的.
| **属性** | **描述** |
| --- | --- |
| forloop.counter | 显示循环的次数,从1开始 |
| forloop.counter0 | 显示循环的次数,从0开始 |
| forloop.revcounter0 | 倒数显示循环的次数,从0开始 |
| forloop.revcounter | 倒数显示循环的次数,从1开始 |
| forloop.first | 判断如果本次是循环的第一次,则结果为True |
| forloop.last | 判断如果本次是循环的最后一次,则结果为True |
| forloop.parentloop | 在嵌套循环中,指向当前循环的上级循环 |
- if 标签
{% if %}会对一个变量求值,如果它的值是“True”(存在、不为空、且不是boolean类型的false值),对应的内容块会输出。
{% if num > 100 or num < 0 %}
无效
{% elif num > 80 and num < 100 %}优秀
{% else %}凑活吧
{% endif %}
- **with 标签**
使用一个简单地名字缓存一个复杂的变量,当你需要使用一个“昂贵的”方法(比如访问数据库)很多次的时候是非常有用的,也可以用于从对象中取值并且赋值别名 例如: {% with total=business.employees.count %} {{ total }} employee{{ total|pluralize }} {% endwith %}
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ {%with 对象.属性 as 别名%}
别名就等于从对象中取道的值
{% endwith %}
- c**srf_token** 标签用于跨站请求伪造保护
def file_put(request): if request.method==”POST”: print(request.POST) print(request.FILES) file_obj = request.FILES.get(‘tx’) #获取上传的文件对象 with open(file_obj.name,’wb’) as f: for line in file_obj: f.write(line)
上传成功的数据放在request.Files里面不是在request.POST里 file_obj.name是上传文件的文件名
<a name="6110ff0c"></a>
### 请求头之contentType
```python
contentType 决定了传输的数据也就是请求体以什么格式进行编码
multipart/form-data 格式:可以包含文件传输
application/x-www-form-urlencoded 格式:默认的,数据以键值对的形式传输 user=123&pwd=456
#request.POST只有contentType为application/x-www-form-urlencoded的时候才可以取到数据
#contentType为json格式的时候 数据在 request.body里面
#里面就是请求体的原数据
# 所有的原本的请求体数据都在request.body,只有当contentType为application/x-www-form-urlencoded
的时候django会把传输过来的数据给解析成queryDict格式方便取数据
模型层ORM
Django中内嵌了ORM框架,不需要直接编写SQL语句进行数据库操作,而是通过定义模型类,操作模型类来完成对数据库中表的增删改查和创建等操作。
MVC框架中的一个重要部分ORM,实现了数据模型与数据库的解耦,就是数据模型的涉及不需要依赖与特定的数据库,通过简单的配置就可以轻松更换数据库,减轻了开发人员的工作量,不需要面对因数据库变更而导致的无效劳动
ORM就是 对象>>>>关系>>>>>映射
用python写数据表的描述信息,orm可以将python写的代码翻译成sql语句,对表操作无法对库操作
定义的模型类 相当于数据库中的表
类成员变量,相当于数据库表中的字段,变量值相当于字段的类型以及约束
类实例化的对象,相当于 sql表中的表记录
orm优点
- 数据模型类都在一个地方定义,更容易更新和维护,也利于重用代码。
- ORM 有现成的工具,很多功能都可以自动完成,比如数据消除、预处理、事务等等。
- 它迫使你使用 MVC 架构,ORM 就是天然的 Model,最终使代码更清晰。
- 基于 ORM 的业务代码比较简单,代码量少,语义性好,容易理解。
- 新手对于复杂业务容易写出性能不佳的 SQL,有了ORM不必编写复杂的SQL语句, 只需要通过操作模型对象即可同步修改数据表中的数据.
- 开发中应用ORM将来如果要切换数据库.只需要切换ORM底层对接数据库的驱动【修改配置文件的连接地址即可】
orm缺点
- ORM 库不是轻量级工具,需要花很多精力学习和设置,甚至不同的框架,会存在不同操作的ORM。
- 对于复杂的业务查询,ORM表达起来比原生的SQL要更加困难和复杂。
- ORM操作数据库的性能要比使用原生的SQL差。
- ORM 抽象掉了数据库层,开发者无法了解底层的数据库操作,也无法定制一些特殊的 SQL。【自己使用pymysql另外操作即可,用了ORM并不表示当前项目不能使用别的数据库操作工具了。】
可以通过以下步骤来使用django的数据库操作
- 配置数据库连接信息
- 在models.py中定义模型类
- 生成数据库迁移文件并执行迁文件[注意:数据迁移是一个独立的功能,这个功能在其他web框架未必和ORM一块的]
- 通过模型类对象提供的方法或属性完成数据表的增删改查操作
数据类型:
类型 | 说明 |
---|---|
AutoField | 自动增长的IntegerField,通常不用指定,不指定时Django会自动创建属性名为id的自动增长属性 |
BooleanField | 布尔字段,值为True或False |
NullBooleanField | 支持Null、True、False三种值 |
CharField | 字符串,参数max_length表示最大字符个数,对应mysql中的varchar |
TextField | 大文本字段,一般大段文本(超过4000个字符)才使用。 |
IntegerField | 整数 |
DecimalField | 十进制浮点数, 参数max_digits表示总位数, 参数decimal_places表示小数位数,常用于表示分数和价格 Decimal(max_digits=7, decimal_places=2) ==> 99999.99~ 0.00 |
FloatField | 浮点数 |
DateField | 日期 参数auto_now表示每次保存对象时,自动设置该字段为当前时间。 参数auto_now_add表示当对象第一次被创建时自动设置当前。 参数auto_now_add和auto_now是相互排斥的,一起使用会发生错误。 |
TimeField | 时间,参数同DateField |
DateTimeField | 日期时间,参数同DateField |
FileField | 上传文件字段,django在文件字段中内置了文件上传保存类, django可以通过模型的字段存储自动保存上传文件, 但是, 在数据库中本质上保存的仅仅是文件在项目中的存储路径!! |
ImageField | 继承于FileField,对上传的内容进行校验,确保是有效的图片 |
字段约束条件:
注意:null是数据库范畴的概念,blank是表单验证范畴的
选项 | 说明 |
---|---|
null | 如果为True,表示允许为空,默认值是False。相当于python的None |
blank | 如果为True,则该字段允许为空白,默认值是False。 相当于python的空字符串,“” |
db_column | 字段的名称,如果未指定,则使用属性的名称。 |
db_index | 若值为True, 则在表中会为此字段创建索引,默认值是False。 相当于SQL语句中的key |
default | 默认值,当不填写数据时,使用该选项的值作为数据的默认值。 |
primary_key | 如果为True,则该字段会成为模型的主键,默认值是False,一般不用设置,系统默认设置。 |
unique | 如果为True,则该字段在表中必须有唯一值,默认值是False。相当于SQL语句中的unique |
外键
如一对一,一对多,多对多
在设置外键时,需要通过on_delete选项指明主表删除数据时,对于外键引用表数据如何处理,django.db.models中包含了可选常量:
- CASCADE 级联,删除主表数据时连通一起删除外键表中数据
- PROTECT 保护,通过抛出ProtectedError异常,来阻止删除主表中被外键应用的数据
- SET_NULL 设置为NULL,仅在该字段null=True允许为null时可用 ,删除该字段以后,对应表里的关联字段的值全部被改成null
- SET_DEFAULT 设置为默认值,仅在该字段设置了默认值时可用
- SET() 设置为特定值或者调用特定方法,例如: ```python from django.conf import settings from django.contrib.auth import get_user_model from django.db import models
def get_sentinel_user(): return get_user_model().objects.get_or_create(username=’deleted’)[0]
class UserModel(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET(get_sentinel_user), )
- **DO_NOTHING** 不做任何操作,如果数据库前置指明级联性,此选项会抛出**IntegrityError**异常
<a name="203076dd"></a>
### 单表操作------>生成表模型
1.创建模型<br />django会为表创建自动增长的主键列,每个模型只能有一个主键列,如果使用选项设置某个字段的约束条件为主键列(permary_key)后,django不会在自动创建自增的主键列。在django中主键的调用别名为pk
```python
在app下的models.py中创建模型
from django.db import models
class Book(models.Model):#必须继承django.db.models.Model类。
#自增,主键
# 字段名 = models.数据类型(约束选项1,约束选项2, verbose_name="注释")
id = models.AutoField(primary_key = True)
title =models.CharField(max_length = 32)
state = models.BooleanField()
pub_date = models.DateField()
price = models.DecimalField(max_digits=8,decimal_places=2)
# 一共允许8位包括小数点后的位数
publish = models.CharField(max_length=32,verbose_name='注释')
class Meta:#数据表结构信息
db_table ='book',#设置表名为book,没有指定是app名_类名
verbose_name = '学生信息表',# 在admin站点中显示的名称
verbose_name_plural = verbose_name,# 显示的复数名称
#自定义数据库操作方法
def __str__(self):
"""定义每个数据对象的显示信息"""
return "<titile" % self.title
2.配置settings文件
若想将模型转为mysql数据库中的表,需要在settings中配置:
DATABASES = {
'default':{
'ENGINE':'django.db.backends.mysql',
# 设置mysql引擎设置为mysql
'NAME':'数据库名称', #需要提前创建好
'USER':'数据库用户',
'PASSWORD':'数据库密码',
'HOST':'数据库连接地址',
'PORT':数据库端口号
}
}
最后通过两条数据库迁移命令即可在指定的数据库中创建表 :在终端页面输入
python manage.py makemigrations 应用名
# 将记录提交到本地就是应用下的migrations文件夹
python manage.py migrate
# 同步数据库操作
注意1:
NAME即数据库的名字,在mysql连接前该数据库必须已经创建,而上面的sqlite数据库下的db.sqlite3则是项目自动创建 USER和PASSWORD分别是数据库的用户名和密码,不需要修改自带的DATABASES。
设置完后,再启动我们的Django项目前,我们需要激活我们的mysql。然后,启动项目,会报错:no module named MySQLdb 。这是因为django默认你导入的驱动是MySQLdb,可是MySQLdb 对于py3有很大问题,py3对mysql的操作是通过
pymysql来完成的,所以我们需要的驱动是PyMySQL 所以,我们只需要找到项目名文件下的init文件,
在里面写入:
import pymysql
pymysql.install_as_MySQLdb()
#作用是让Django的ORM能以mysqldb的方式来调用PyMySQL。
注意2:确保settings配置文件中的INSTALLED_APPS中写入我们创建的app名称,INSTALLED_APPS中自带的是django的默认的应用,需要将自己新建的应用加进去用于激活app
注意3:如果报错如下:
django.core.exceptions.ImproperlyConfigured: mysqlclient 1.3.3 or newer is required; you have 0.7.11.None
MySQLclient目前只支持到python3.4,因此如果使用的更高版本的python,需要修改如下:
通过查找路径"C:\Programs\Python\Python36-32\Lib\site-packages\Django-2.0-py3.6.egg\
django\db\backends\mysql" 这个路径里的文件把
if version < (1, 3, 3):
raise ImproperlyConfigured("mysqlclient 1.3.3 or newer is required; you have %s" %
Database.__version__)注释掉
注意4: 如果想打印orm转换过程中的sql,需要在settings中进行如下配置:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console':{
'level':'DEBUG',
'class':'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'propagate': True,
'level':'DEBUG',
},
}
}
单表操作———>增加记录
1.在视图函数中引入models中创建数据库表的类
2.根据创建数据表的类实例化记录对象
from django.shortcuts import render
from app.models import Book
# Create your views here.
def index(request):
# 添加记录
#方式1
book_obj = Book(id="1", title="算法书", price=100, pub_data="2012-1-1", publish="人民出版社")
book_obj.save() #创建对应的sql语句并执行增加记录的操作
# 方式2 通过create方法,他的返回值是当前生成的对象记录也就是book_obj
# Book.objects.create,objects就是表的管理器,也就是生成表那个类的管理器 create是增加
# book_obj就是当前生成的对象记录,可以直接查询字段值,如:book_obj.title
book_obj = Book.objects.create(id="2", title="算法书1", price=100, pub_data="2012-1-12", publish="人民出版社")
单表操作———> 查询API
ORM中针对查询结果的限制
1. all()方法
info = BOOK.objects.all()
# 查询所有的结果返回值是Queryset类型,类似与列表,列表里面放的是查询出来的每个对象,一个记录是一个对象
# 想返回具体的信息可以通过 __str__方法,在生成表的类中定义__str__方法
# 返回值是Queryset类型 [<Book:记录信息> # model对象,<Book:记录信息>] model对象支持.操作等
# 调用者是objects
2. first() 和 last()
info = BOOK.objects.all().first()
# 取all返回值的第一个对象和最后一个对象
# 等于BOOK.objects.all()[0]
# 调用者是Queryset 返回值是model对象
3.filter()
info = BOOK.objects.filter(title='php',price=100)
# 过滤title等于php并且价格是100的那条记录
# 返回值是Queryset对象
# 调用者是objects
4.get()
info = BOOK.objects.get(title='go')
# 查询title是go的记录
# 返回值是model对象 有且只有一个查询结果的时候才有意义,超过一个或者没有都报错
# 调用者是objects
5.excelude()
info = BOOK.objects.exclude(title='go')
# 查询title不等于go的记录
# 返回值是Queryset对象
# 调用者是objects
6.order_by()
info = BOOK.objects.all().order_by('id')
# 查询所有的内容按照id字段升序排序,'-id'是降序排序'
# 返回值是Queryset对象
# 调用者是Queryset对象
7.reverse()
8.count()
info = BOOK.objects.all().count()
# 对查询的所有结果进行计数
# 返回值是整形
# 调用者是Queryset对象
9.exist()
info = BOOK.objects.all().exist()
# 判断Queryset是否包含数据,是返回True 否则返回False,只取1条
# 返回值是bool
# 调用者是Queryset对象
10.values()
info = BOOK.objects.all().values("title")
# 取值,循环取Queryset中的model对象的title值
# 返回值是Queryset,[{'title':'python'},{'title':'go'},]
# 调用者是Queryset对象、
实现原理
temp = []
for obj in BOOK.objects.all():
temp.append({"title":obj.title})
return temp
11.values_list()
info = BOOK.objects.all().values_list("title")
# 取值,循环取Queryset中的model对象的title值
# 返回值是Queryset,[('python'),('go'),]
# 调用者是Queryset对象
12.distinct()
info = BOOK.objects.all().values('price').distinct()
# 去重,对查询出来的价格字段去重
# 返回值是Queryset,
# 调用者是Queryset对象
单表操作———>模糊查询
1. __in
Book.objects.filter(price__in=[100,200,300]) # 价格等于 100 200 300的
2. __gt
Book.objects.filter(price__gt=100) # 大于
3.__lt
Book.objects.filter(price__lt=100) # 大于
4.__range
Book.objects.filter(price__range=[100,200]) # 价格在100-200 之间的
5.__contains
Book.objects.filter(title__contains="h") # 标题里面包含h的
6.__icontains
Book.objects.filter(title__icontains="h") # # 标题里面包含h的 不区分大小写
7.__startswith
Book.objects.filter(title__startswith="py") # 标题以py开头的
8.__year # __year只有data类型的可以使用,
Book.objects.filter(pub_date__year=2012) # 查询出版日期是2012年的
9.__isnull #查询字段值是否为空
Book.objects.filter(title__isnull=True) # 查询title值为空的
单表操作———>删除/修改
1. delete()
info = Book.objects.filter(price=200).delete()
# 删除价格等于200的记录,需要先查询出来再删除
# 返回值是元组,(2,{'app01.book':2}) 删除了2个记录删除的是app01.book表
# delete()的调用者是Queryset对象和model对象
info = BOOK.objects.all().first().delete()
2. update()
info = Book.objects.filter(title="php").update(title='go')
# 将查询出来的title等于php的修改为title等于go
# 返回值是元组,
# update()的调用者必须是Queryset对象
3.save()
stu = Book.objects.get(pk=1)
stu.price =88
stu.save()
#将pk=1的书籍的价格修改为88
#这种方式会对id=1的记录的所有字段重新赋值,效率低
# save之所以能提供给我们添加数据的同时,还可以更新数据的原因?
# save会找到模型的字段的主键id的值,
# 主键id的值如果是none,则表示当前数据没有被数据库,所以save会自动变成添加操作
# 主键id有值,则表示当前数据在数据库中已经存在,所以save会自动变成更新数据操作
多表操作——->生成关联表模型
一、创建模型
实例:我们来假定下面这些概念,字段和关系
作者模型:一个作者有姓名和年龄。
作者详细模型:把作者的详情放到详情表,包含生日,手机号,家庭住址等信息。作者详情模型和作者模型之间
是一对一的关系(one-to-one)
出版商模型:出版商有名称,所在城市以及email。
书籍模型: 书籍有书名和出版日期,一本书可能会有多个作者,一个作者也可以写多本书,所以作者和书籍的
关系就是多对多的关联关系(many-to-many);一本书只应该由一个出版商出版,所以出版商和书籍是一对多关联
关系(one-to-many)。
模型建立如下:
from django.db import models
# Create your models here.
class Author(models.Model):
nid = models.AutoField(primary_key=True)
name=models.CharField( max_length=32)
age=models.IntegerField()
# 与AuthorDetail表的nid字段建立一对一的关系
authorDetail=models.OneToOneField(to="AuthorDetail",to_fileld="nid",
on_delete=models.CASCADE)
class AuthorDetail(models.Model):
nid = models.AutoField(primary_key=True)
birthday=models.DateField()
telephone=models.BigIntegerField()
addr=models.CharField( max_length=64)
class Publish(models.Model):
nid = models.AutoField(primary_key=True)
name=models.CharField( max_length=32)
city=models.CharField( max_length=32)
email=models.EmailField()
class Book(models.Model):
nid = models.AutoField(primary_key=True)
title = models.CharField( max_length=32)
publishDate=models.DateField()
price=models.DecimalField(max_digits=5,decimal_places=2)
class Emp(models.Model):
name =models.CharFiled(max_length=32)
age=models.IntegerFiled()
salary=models.DecimalFiled(max_digits=8,decimal_places=2)
dep-models.CharFiled(max_length=32)
province=models.CharField(max_length=32)
# 与Publish建立一对多的关系,外键字段建立在多的一方
# 会自动建一个字段叫publish_id用于关联
# on_delete 是指删除其他表的内容时此表的关联字段设置为空 null是允许为空
# db_constraint=False 是指不建立外键约束,默认值是True
# related_name是指正向查询的时候用publish字段就可以,当然这是默认的,
但是反向查询的时候通常是表名小写_set的方式,还有一种方式就是根据related_name字段的值进行反向查询
#通过Emp.objects.publish得到的是对象类型的数据且是包含该字段关联的另一个表里的一条或多条记录
publish=models.ForeignKey(to="Publish",to_field="nid",on_delete=models.CASCADE,related_name='pb',null=True,db_constraint=False)
转换为sql相当与
publish_id int, # 自动创建的字段
foreign key (publish_id) references publish(id),
# 与Author表建立多对多的关系,ManyToManyField可以建在两个模型中的任意一个,自动创建第三张表,
这个第三张表orm告诉数据库生成后orm无法直接操作这个数据表,但是可以通过接口操作,这个接口就是生成多
对多关系的那个字段authors
#db_table是设置第三张表的表名,可以不设置
authors=models.ManyToManyField(to='Author',db_table='111')
转换为sql相当与
表名是自动拼接的
create table book_author(
id int primary key auto_increment,
book_id int,
author_id int,
foreign key (book_id) references book(id),
foreign key (author_id) references author(id),
)
注意事项:
表的名称是app名称_类名也就是表名,是根据 模型中的元数据自动生成的,也可以覆写为别的名称
id 字段是自动添加的
对于外键字段,Django 会在字段名上添加"_id" 来创建数据库中的列名
这个例子中的CREATE TABLE SQL 语句使用PostgreSQL 语法格式,要注意的是Django 会根据settings 中指定
的数据库类型来使用相应的SQL 语句。
定义好模型之后,你需要告诉Django _使用_这些模型。你要做的就是修改配置文件中的INSTALL_APPSZ中设置,
在其中添加models.py所在应用的名称。
外键字段 ForeignKey 有一个 null=True 的设置(它允许外键接受空值 NULL),你可以赋给它空值 None 。
多表操作——-> 一对多添加记录
# 单表添加记录
from app01.models import *
def add(request):
# 在出版社表添加记录,返回值就是生成的记录对象
obj=Publish.objects.create(name="人民出版社",email="123@123.com")
return HttpResponses('ok')
# 有绑定关系的表添加记录 一对多
# 方式一
# 在书籍表添加记录
book_obj=Book.objects.create(title="123",price=100,publishDate="2012-12-13",publish_id=1)
# 方式二
pub_obj =Publish.objects.filter(nid=1).first()
book_obj = Book.objects.create(title="123",price=100,publishDate="2012-12-13",publish=
pub_obj)
# create会将publish翻译成publish_id 将pub_obj的主键值取出来,组成publish_id=1
#book_obj.publish = pub_obj
方式一给publish_id传的值是1 方式二给publish传的值是一个对象
方式一在传值的时候会去找nid=1的那个出版社对象然后将这个对象赋值给publish_id,但是在取值的时候只有
方式二可以通过对象.的方式取值如:booj_obj.publish.email
多表操作——-> 多对多添加记录
# 给一本书添加多个作者
# 新增一本书的记录
book_obj = Book.objects.create(title="go",price=100,publishData="2013-13-13",)
# 从作者表里找两个作者
egon = Author.objects.get(name="egon")
alex = Author.objects.get(name="alex")
# 书籍表里的authors在原表里就是生成多对多表的实例对象,调用authors相当于操作多对多的表 ,表里面的记录是根据book_obj和alex以及egon的主键生成的
book_obj.authors.add(egon,alex)
#也就是给一本书增加了两个作者,在多对多表关系里也就是第三张表,用book_obj的主键与egon,alex的主键分别组成一条记录
解除多对多关系记录
# 找到nid为4 的那本书籍对象
book = Book.objects.filter(nid=4).first()
# 删除 book(主键为4) 与作者为 1的那条记录
book.authors.remove(1)
# 删除所有 book的记录也就是删除所有nid为4的那本书的记录
book.authors.clear()
# 获取与这本书关联的所有作者对象的集合 queryset类型的
book.authors.all()
# 查询主键为4的书籍的所有作者的名字
book.authors.all().values("name")
#重置主键为4的数据的所有作者的名字
book.authors.set([查询出来的作者对象,查询出来的作者对象])
基于对象的跨表查询——->一对多
根据模型管理器的关联字段查询,查询出来的是模型对象。
根据模型管理器的关联字段_id查询,查询出来的是具体的值
翻译成sql就是子查询的方式
查询 西游记 这本书出版社的名字
book_obj= Book.objects.filter(title="西游记").first()
book_obj.publish # 这本书关联的出版社对象
book_obj.publish.name # 这本书关联的出版社的名字
正反向查询
A表关联B表,关联字段在A表中 A查找B就是 正向查询 按字段查找 如: 根据书籍查找出版社的名字 book_obj= Book.objects.filter(title="西游记").first() book_obj.publish book_obj.publish.name B查找A就是 反向查询 按表名_set 查找 或者用related_name='XX' 如:查询出版社出版过的书籍 pub = Publish.objects.filter(name="人民出版社").first() pub.book_set.all() #queryset对象 或 用一对多中一的模型对象.related_name字段的值也就是xx,就可以进行反向查询
基于对象的跨表查询——->多对多
book表和authors表是多对多关系 关联属性在book表里
正向查询按字段
如:查询西游记这本书的所有作者的名字
book_obj=Book.objects.filter(title="西游记").first()
author_List = book_obj.authors.all() # queryset类型
for i in authors_list:
print(i.name)
反向查询按表名_set
如: 查找alex这个作者出版过的所有书籍
alex = Authors.objects.fifter(name="alex").first()
book_list = alex.book_set.all()
for i in book_list:
print(i.title)
基于对象的跨表查询——->一对一
作者表和作者详情表是一对一
正向查询按字段
如: 查找alex的手机号
alex = Authors.objects.fifter(name="alex").first()
alex.authordetail.telephone
反向查询按表名
如: 查询手机号为111的作者的年龄和名字
ad = AuthorDetail.objects.filter(telephone="111")
.first()
ad.author.name
ad.author.age
基于双下划线的跨表查询——->一对多
对应sql的联表查询 join查询
正向查询按字段 反向查找按表名用来告诉ORM引擎join哪张表
正向查询:
如:查询西游记这本书的出版社的名字
Book.object.filter(title='西游记').values("publish__name")
values里面放的就是建立关联关系的字段__后面跟需要显示的字段相当与join表的操作
(values相当与sql的 select)
在查询西游记这本书的出版社的名字的时候首先拿到西游记这本书的对象也就是Book.object.filter(title=
'西游记'),然后查询的是出版社的名字因为出版社不在book表里所以要联表也就是.values("publish__name")
,为什么联表操作要放在values里面是因为要查询的也就是要显示的出版社的名字在book里没有,
反向查询:
如:查询哪个出版社出版过西游记这本书
Publish.objects.filter(book__title="西游记").valuse("name")
filter里面是告诉orm join连接book表,查找的是(__title)等于西游记的 (filter相当于sql的while)
在查询哪个出版社出版过西游记这本书时候首先要找到出版过西游记这本书的出版社的对象,那我们就需要过滤
也就是Publish.objects.filter(book__title="西游记"),为什么此时联表操作放在filter()里面,
因为在找西游记这本书的时候在出版社表里面找不到,而我们又需要以此作为条件进行过滤所以放在filter()
里面,然后显示出版社的名字.valuse("name")
基于双下划线的跨表查询__联表的语法是可以灵活使用的,不是固定的放在某个参数里面
也可以理解为 正向查询的时候一般是需要查询被关联表里的字段,过滤条件当前表的字段就可以满足,但是显示
的时候当前表的字段是没有的所以要把联表操作放到values()里面
基于双下划线的跨表查询——->多对多
查询西游记这本书的所有作者的名字,一本书有多个作者,一个作者可以写多本书
select app01_author.name from app01_book inner join app01_book_authors on app01_book.nid =
app01_book_authors.book_id inner join app01_author on app01_book_authors.author_id =
app01_author.nid where app01_book.title ="西游记"
正向查询:
# 通过book表join与其关联的author表
# 按照字段authors通知orm引擎join book_authors表与authors,因为两个表都是借助第三张表来关联关系的
,第三张表是authors字段生成的所以用authors字段来查询
Book.objects.filter(title="西游记").values("authors__name")
反向查询:
查询哪些作者写过西游记
通过Authors表join与其关联的Book表
# 按照表名book通知orm引擎join book_authors 和 book 表
Authors.objects.filter(book__title="西游记").values("name")
基于双下划线的跨表查询——->一对一
查询alex的手机号,作者表里的一条记录只能对应作者详情表的一条记录反之也是
正向查询:
Authors_objects.filter(name="alex").vlaues("authorsdetail__telphone")
# 查询alex的手机号filter(name="alex")是过滤,__telphone是显示手机号,authorsdetail是因为alex的
手机号不在authors表里,通过authorsdetai字段与authorsdetai表进行链表
反向查询:
# 查询手机号是属于alex的
# 通过authorsdetail表join与其关联的authors表
AuthorDetail.objects.filter(author_name="alex").values("telephone")
基于双下划线的跨表查询——->连续跨表
查询手机号以151开头的作者出版过的所有的书籍名称以及出版社名称
正向查询:
# 通过book表join authordetail表,book表与authordetail无关联,所以要连续跨表 ,通过authors表进
行跨表
Book.objects.filter(authors__authordetail __telephone__startswitch="151").values("title",
"publish__name")
反向查询:
Author.objects.filter(authordetail__telephone__startswitch="151").values("book__title",
"book__publish__name ")
1.确定那个是基表
2.两个表之间是否有关系
3.没关系跨表
4.有关系的话,判断是正向还是反向
聚合查询
查询所有书籍的平均价格
select avg(price) from book
from django.db.models import Avg,Max,Min,Count
Book.objects.all().aggregate(avg12=Avg("price"))
# aggregate()是聚合统计函数 ,返回值是一个字典{"price__avg":151}
# 不加名称的时候自动拼接做为字典的键,也可以起名,avg12就是起的名字
单表下的分组查询
QuerySet对象.annotate()
查询每一个部门名称以及对应的员工数
select count(id) from 员工表 group by(部门字段)
查询每一个部门的名称以及员工的平均薪资
select dep,avg(salary) from emp group by dep
from django.db.models import Avg,Max,Min,Count
Emp.objects.values("dep").annotate(avg("salary"))
# annotate是分组统计函数 ,values里放分组的字段,语法是固定的
# 也就是.annotate前面select的字段是那个,就按照哪个字段分组
#Emp.objects.values("dep"),相当于select dep
# annotate()返回值是queryset类型<queryset[{'avg_salary':5000,'dep':教学部}],
{'avg_salary':6000,'dep':管理部}>
多表下的分组查询
# 查询每个出版社的名称以及出版的书籍个数
# 一对多关系 一本书只能被一个出版社出版,一个出版社可以出多本书
select publish.name,count("title") from book inner join Publish on book.publish_id =
publish.id group by publish.id
Publish.object.values("name").annotate(count("book__title"))
Publish.object.values("nid").annotate(c=count("book__title")).values("name","c")
# 如果不加.values("name","c") 默认显示nid字段和c字段,加上后就不显示nid和c了显示 name和c ,
values里面可以放的字段有表模型的所有字段以及统计的字段
# 查询每一个作者的名字以及出版过的书籍的最高价格
# 多对多关系 一本书有多个作者 一个作者写多本书
select max(book.price),author.name from book inner join book_authors on book.nid =
book_authoes.book_id inner join author on author.nid = book_author.author_id
group by author.nid
Authors.objects.values("nid").annotate(c=Max("book__price")).values("name",c)
# 查询每一个书籍的名称以及对应作者的个数
select Book.title,count(author.name) from book inner join book_authors on book.nid =
book_authoes.book_id inner join author on author.nid = book_author.author_id
group by Book.nid
Book.objects.values("pk").annotate(c=count("authors__name")).values("title","c")
# pk是主键的意思
跨表分组查询的语法模型
每一个后的表模型.objects.values("分组字段").annotate(聚合函数(关联表__统计字段)).values
("表模型的所有字段,以及统计字段")
F查询与Q查询
#两个属性比较的时候使用F函数
# 查询阅读数大于阅读数的书籍
from django.db.models import F
Book.objects.filter(comment_num__gt=F("red_num"))
# 将所有书籍的价格提高10元
Book.objects.all().update(price=F("price")+10)
#查询书籍名称叫红楼梦的且价格等于101的
Book.objects.filter(title="红楼梦",price=100)
#多个过滤器逐个调用表示逻辑与关系,同sql语句中where部分的and关键字。
from django.db.models import Q
#查询书籍名称叫红楼梦的或价格等于101的
Q里面是两个条件,条件之间是且就用&连接,或就是|连接
Book.objects.filter(Q(title="红楼梦") | Q(price=100))
# ~Q(title="红楼梦") title不等于红楼梦的
Book.objects.filter(Q(title="红楼梦") | Q(price=100),comment_num__gt=F("red_num"))
#多个条件时把Q条件放在前面
批量插入数据
from .models import Book
book_list = []
for i in range(100):
book =Book(title="book_%s"%i,price=i*i)#实例化一个对象此时和数据库操作还没关系
book_list.append(book)
Book.objects.bulk_create(book_list)
# 将多条数据写入到数据库相当于一条insert语句后面跟多条记录
orm进阶
queryset特性
- 可切片
使用python的切片语法来限制查询集记录的数目,等同于sql的limit和offset语法
不支持负的索引,通常,查询集 的切片返回一个新的查询集 —— 它不会执行查询
BOOK.objects.all()[:5] # (LIMIT 5)
BOOK.objects.all()[5:10] # (OFFSET 5 LIMIT 5)
- 可迭代 ```python articleList=models.Article.objects.all()
for article in articleList: print(article.title)
- **惰性查询**
查询集 是惰性执行的 —— 创建`查询集`不会带来任何数据库的访问。你可以将过滤器保持一整天,直到`查询集` 需要求值时,Django 才会真正运行这个查询
一般来说,只有在请求,查询集 的结果时才会到数据库中去获取它们。当你确实需要结果时,查询集通过访问数据库来求值。
```python
queryResult=models.Article.objects.all() # 未命中数据库
print(queryResult) # 命中数据库
for article in queryResult:
print(article.title) # 命中数据库
- 缓存机制
每个查询集都包含一个缓存来最小化对数据库的访问。理解它是如何工作的将让你编写最高效的代码。
在一个新创建的查询集中,缓存为空。首次对查询集进行求值 —— 同时发生数据库查询 ——Django 将保存查询的结果到查询集的缓存中并返回明确请求的结果(例如,如果正在迭代查询集,则返回下一个结果)。接下来对该查询集 的求值将重用缓存的结果。
请牢记这个缓存行为,因为对查询集使用不当的话,它会坑你的。例如,下面的语句创建两个查询集,对它们求值,然后扔掉它们:
queryset = Book.objects.all()
print(queryset) # 命中数据库
print(queryset) # 命中数据库
注:简单地打印查询集不会填充缓存。
这意味着相同的数据库查询将执行两次,显然倍增了你的数据库负载。同时,还有可能两个结果列表并不包含相同的数据库记录,因为在两次请求期间有可能有Article被添加进来或删除掉。为了避免这个问题,只需保存`查询集`并重新使用它:
queryset = Book.objects.all()
ret = [i for i in queryset] # 命中数据库
print(queryset) # 使用缓存
print(queryset) # 使用缓存
查询集什么时候才会被缓存?
- 遍历queryset时
- if语句(为了避免这个,可以用exists()方法来检查是否有数据)
所以单独queryset的索引或者切片都不会缓存。
queryset = Book.objects.all()
one = queryset[0] # 命中数据库
two = queryset[1] # 命中数据库
print(one)
print(two)
- exists()与iterator()方法
exists()
简单的使用if语句进行判断也会完全执行整个queryset并且把数据放入cache,虽然你并不需要这些 数据!为了避免这个,可以用exists()方法来检查是否有数据:
if queryResult.exists():
#SELECT (1) AS "a" FROM "blog_article" LIMIT 1; args=()
print("exists...")
iterator()
当queryset非常巨大时,cache会成为问题。
处理成千上万的记录时,将它们一次装入内存是很浪费的。更糟糕的是,巨大的queryset可能会锁住系统 进程,让你的程序濒临崩溃。要避免在遍历数据的同时产生queryset cache,可以使用iterator()方法 来获取数据,处理完数据就将其丢弃。
objs = Book.objects.all().iterator()
# iterator()可以一次只从数据库获取少量数据,这样可以节省内存
for obj in objs:
print(obj.title)
#BUT,再次遍历没有打印,因为迭代器已经在上一次遍历(next)到最后一次了,没得遍历了
for obj in objs:
print(obj.title)
相当于生成器
当然,使用iterator()方法来防止生成cache,意味着遍历同一个queryset时会重复执行查询。所以使 #用iterator()的时候要当心,确保你的代码在操作一个大的queryset时没有重复执行查询。
queryset的cache是用于减少程序对数据库的查询,在通常的使用下会保证只有在需要的时候才会查询数据库。 使用exists()和iterator()方法可以优化程序对内存的使用。不过,由于它们并不会生成queryset cache,可能 会造成额外的数据库查询。
总结:在使用缓存机制还是生成器机制的选择上如果是,数据量大情况主要使用生成器;数据少使用次数多的情况使用缓存机制。
中介模型
处理类似搭配 配料 和 披萨 这样简单的多对多关系时,使用标准的ManyToManyField 就可以了。但是,有时你可能需要关联数据到两个模型之间的关系上。
例如,有这样一个应用,它记录音乐家所属的音乐小组。我们可以用一个ManyToManyField 表示小组和成员之间的多对多关系。但是,有时你可能想知道更多成员关系的细节,比如成员是何时加入小组的。
对于这些情况,Django 允许你指定一个中介模型来定义多对多关系。 你可以将其他字段放在中介模型里面。源模型的ManyToManyField 字段将使用through 参数指向中介模型。对于上面的音乐小组的例子,代码如下:
也就是多对多关系的时候不自动生成第三张表,而是使用自己定义的表作为第三张表,第三张表中的字段自己定义,并且要注意外键的关系
from django.db import models
class Person(models.Model):#音乐家模型类
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Group(models.Model): #音乐组模型类
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self):
return self.name
class Membership(models.Model):#关联的模型类,也就是自定义的中介模型
person = models.ForeignKey(Person,on_delete=models.CASCADE)
group = models.ForeignKey(Group,on_delete=models.CASCADE)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
既然你已经设置好ManyToManyField 来使用中介模型(在这个例子中就是Membership),接下来你要开始创建多对多关系。也就是添加记录,要做的就是创建中介模型的实例用实例进行关系的添加,而不是通过多对对字段进行关系的添加(此处的多对多字段是group表中的members),因为通过字段我们只能添加组和人的关系,没法添加中介表中的date_joined和invite_reason字段
ringo = Person.objects.create(name="Ringo Starr") # 添加一个音乐家
paul = Person.objects.create(name="Paul McCartney") # 添加一个音乐家
beatles = Group.objects.create(name="The Beatles") #添加一个组
m1 = Membership(person=ringo, group=beatles,
date_joined=date(1962, 8, 16),
invite_reason="Needed a new drummer.") #通过中介表进行记录的添加,而不是多对多的字段
m1.save()
beatles.members.all() #查询beatles这个组下的所有的人
[<Person: Ringo Starr>]
ringo.group_set.all() #查询ringo这个人所在的组
[<Group: The Beatles>]
m2 = Membership.objects.create(person=paul, group=beatles,
date_joined=date(1960, 8, 1),
invite_reason="Wanted to form a band.") # 添加记录
m2.save()
beatles.members.all() # 查询beatles这个组下的所有的人
[<Person: Ringo Starr>, <Person: Paul McCartney>]
中介模型与普通的多对多字段不同,你不能使用多对多字段的add、 create和赋值语句(比如,beatles.members = […])来创建关系。
#这个不行
beatles.members.add(john)
#这个不行
beatles.members.create(name="George Harrison")
#这个不行
beatles.members = [john, paul, ringo, george]
为什么不能这样做? 这是因为你不能只创建 Person和 Group之间的关联关系,你还要指定 Membership模型中所需要的所有信息;而简单的add、create 和赋值语句是做不到这一点的。所以它们不能在使用中介模型的多对多关系中使用。此时,唯一的办法就是创建中介模型的实例。
remove()方法被禁用也是出于同样的原因。但是clear() 方法却是可用的。它可以清空某个实例所有的多对多关系:
#清除中介表中beatles组所对应的关系
beatles.members.clear()
数据库表反向生成模型类
众所周知,Django较为适合原生开发,即通过该框架搭建一个全新的项目,通过在修改models.py来创建新的数据库表。但是往往有时候,我们需要利用到之前的已经设计好的数据库,数据库中提供了设计好的多种表单。那么这时如果我们再通过models.py再来设计就会浪费很多的时间。所幸Django为我们提供了inspecdb的方法。他的作用即使根据已经存在对的mysql数据库表来反向映射结构到models.py中.
我们在展示django ORM反向生成之前,我们先说一下怎么样正向生成代码。
- 正向生成,指的是先创建model.py文件,然后通过django内置的编译器,在数据库如mysql中创建出符合model.py的表。
- 反向生成,指的是先在数据库中create table,然后通过django内置的编译器,生成model代码。
python manage.py inspectdb > models文件名
查询优化
- select_related()
对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用select_related 来对QuerySet进行优化。
select_related 返回一个QuerySet,当执行它的查询时它沿着外键关系查询关联的对象的数据。它会生成一个复杂的查询并引起性能的损耗,但是在以后使用外键关系时将不需要数据库查询。
简单说,在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。
下面的例子解释了普通查询和select_related() 查询的区别。
查询id=2的的书籍的出版社名称,下面是一个标准的查询:
# 命中数据库
book= models.Book.objects.get(nid=2)
# 再次命中数据库
print(book.publish.name)
使用select_related()
#相当与join表后的结果,再次使用books后就不需要取数据库查询了,而是使用join起来的这个大表
books=models.Book.objects.select_related("publish").all()
for book in books:
# 不会命中数据库
# 因为已经在上一个查询中进行了预填充
print(book.publish.name)
- 多外键查询
就是使用表中的多个一对一或外键关联字段进行join
这是针对publish的外键查询,如果是另外一个外键呢?让我们一起看下:
book=models.Book.objects.select_related("publish").get(nid=1)
print(book.authors.all())
观察logging结果,发现依然需要查询两次,所以需要改为:
book=models.Book.objects.select_related("publish","").get(nid=1)
print(book.publish)
或者:
book=models.Article.objects
.select_related("publish")
.select_related("")
.get(nid=1) # django 1.7 支持链式操作
print(book.publish)
- prefetch_related()
对于多对多字段(ManyToManyField)和一对多字段,可以使用prefetch_related()来进行优化。
prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。select_related()是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增加和内存占用的增加。若有n个对象,每个对象的多对多字段对应Mi条,就会生成Σ(n)Mi 行的结果表。
prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系。
# 查询所有文章关联的所有标签
books=models.Book.objects.all()
for book in books:
print(book.authors.all()) #4篇文章: 命中数据库 5次
#先去数据库seclect * from book 表
#每执行一次循环就去join一次authors表
改为prefetch_related:
# 查询所有文章关联的所有标签
books=models.Book.objects.prefetch_related("authors").all()
for book in books:
print(book.authors.all()) #4篇文章: 命中数据库 2次
#Book.objects.prefetch_related("authors")就是 将book表和authors表join在一起
#再循环的时候就不会去数据库查询了
#两次 是 一次是查询book数据的 另一次是join表的
- extra()
有些情况下,Django的查询语法难以简单的表达复杂的 WHERE 子句,对于这种情况, Django 提供了 extra(),QuerySet修改机制 — 它能在 QuerySet生成的SQL从句中注入新子句extra(select=None, where=None, params=None,
tables=None, order_by=None, select_params=None)
extra可以指定一个或多个 参数,例如 select, where or tables. 这些参数都不是必须的,但是你至少要使用一个!要注意这些额外的方式对不同的数据库引擎可能存在移植性问题.(因为你在显式的书写SQL语句),除非万不得已,尽量避免这样。
- 参数之select
The select 参数可以让你在 SELECT 从句中添加其他字段信息,它应该是一个字典,存放着属性名到 SQL 从句的映射。
queryResult=models.Article.objects.extra(select={'is_recent': "create_time > '2017-09-05'"})
#is_recent是自定义的名字,create_time是数据中的字段,> '2017-09-05'是条件。查询create_time > '2017-09-05'的数据
此时queryResult可以.is_recent
结果集中每个 Entry 对象都有一个额外的属性is_recent, 它是一个布尔值,表示 Article对象的create_time 是否晚于2017-09-05.
- 参数之where / tables
您可以使用where定义显式SQL WHERE子句 - 也许执行非显式连接。您可以使用tables手动将表添加到SQL FROM子句。
where和tables都接受字符串列表。所有where参数均为“与”任何其他搜索条件。
举例来讲:
queryResult=models.Article.objects.extra(where=['nid in (3,4) OR title like "py%" ','nid>2'])
Ajax请求
客户端(浏览器)向服务端发起请求的形式:
- 地址栏:GET
- 超链接标签:GET
- form表单:GET或POST
- Ajax(重要):GET或POST或PUT或DELETE
Ajax (Asynchoronous Javascript And Xml) 异步javascript和xml,就是使用javascript语言与服务器
进行异步交互传输的数据为xml现在更多使用json数据
Ajax支持get请求和post请求
同步交互:客户端发出一个请求后需要等待服务器响应结束后,才能发出第二个请求
异步交互:客户端发出一个请求后无需等待服务器响应结束就可以发出第二个请求
Ajax除了异步的特点外还有一个特点就是游览器页面局部刷新(可以让用户无感知的完成请求和响应过程 )
Ajax简单实现
基于juqer实现
<script src="jQuery cdn"></script>
<button class="Ajax">Ajax</button>
<script>
$(".ajax").click(
function(){
//发送Ajax请求
$.ajax(
{url:"请求URL",
type:"请求方式",
data:{"123":1,'456':2} # 发送到请求url的数据
success:function(data){ #回调函数 data接收的是响应的数据
console.log(data)
}
}
)
}
)
</script>
Ajax发送json数据格式
<script src="jQuery cdn"></script>
<form action="" method="post">
用户名<input type="text" name="user">
<input type="button" class="btn" value="Ajax">
</form>
<script>
$(.btn).click(
function(){
$.ajax(
{
url:"",
type:"post",
#发送json格式数据
contentType:"application/json"
#将参数转换为json格式{"a":"1","b":"2"}
data:JSON.stringify{
a:1,
b:2,
},
success:function(data){
console.log(data)
}
}
)
}
)
</script>
Ajax实现文件上传
<script src="jQuery cdn"></script>
<form action="" method="post">
用户名<input type="text" id="user">
头像<input type="file" id="tx">
<input type="button" class="btn" value="Ajax">
</form>
<script>
$(.btn).click(function(){
var formdata=new FormData(); #创建一个对象
formdata.append("user",$("#user").val()); #对这个对象添加数据
#对这个对象添加数据 $("#tx")[0]是获取id=tx这个DOM对象.files[0]就是上传的文件对象固定语法
formdata.append("tx",$("#tx")[0].files[0];
$.ajax(
{
url:"",
type:"post",
contentType:false,#不对数据编码上传文件时必须配置
processData:false,#是否要对数据做预处理上传文件时必须配置,
#如果预处理是True,则再决定用什么编码格式进行编码数据
data:FormData, #数据是创建的formdata对象
success:function(data){
console.log(data)
}
}
)
}
)
</script>
同源策略和跨域
同源策略是一种约定,它是游览器最核心也最基本的安全功能,如果缺少了同源策略则游览器的正常功能都会受到影响,可以说web是构建在同源策略基础之上的,游览器只是针对同源策略的一种实现
同源策略,是由Netscape提出的安全策略,现在所有支持javascript的游览器都会使用这个策略,所谓的同源是值 域名 协议 端口 相同,当一个游览器的两个tab分别打开百度和谷歌的页面,当游览器的百度tab页面执行一个脚本的时候会检查这个脚本属于哪个页面的,就是检查是否同源,只有和百度同源的脚本才会被执行,如果非同源,那么在请求数据时,游览器会在控制台中报一个异常,提示拒绝访问。
解决这种跨域问题的三个思路
- jsonp
- cors
- 前端代理
cors需要游览器和服务器的支持,目前所有游览器都支持该功能,IE游览器不能低于ie10.
整个cors通信过程,都是游览器自动完成的,不需要用户参与,对于开发者来说,cors通信与同源的ajax通信没有差别,代码完成一样。游览器一旦发现ajax请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但是用户不会又感觉。
因此:实现cors通信的关键是服务器,是要服务器实现了cors接口,就可以实现 跨源通信,所以服务器只要添加一个响应头,同意跨域请求,游览器就不会再拦截
response = JsonResponse(data)
response["Access-Control-Allow-Origin"] = "*"
cors有两种请求:简单请求和非简单请求
只要满足以下两大条件就是属于简单请求
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
不同时满足上面两个条件就是非简单请求,游览器对与这两种请求的处理是不同的。
简单请求:是一次请求
非简单请求:是两次请求,在发送数据之前会先发一次请求用做 预检 只有 预检通过后才再次发送一次请求用于数据传输
- 请求方式:OPTIONS
- “预检”其实做检查,检查如果通过则允许传输数据,检查不通过则不再发送真正想要发送的消息
- 如何“预检”
=> 如果复杂请求是PUT等请求,则服务端需要设置允许某请求,否则“预检”不通过
Access-Control-Request-Method
=> 如果复杂请求设置了请求头,则服务端需要设置允许某请求头,否则“预检”不通过
Access-Control-Request-Headers
支持跨域简单请求:
服务器设置响应头:Access-Control-Allow-Origin = '域名' 或 '*'
支持跨域复杂请求:
由于复杂请求时,首先会发送“预检”请求,如果“预检”成功,则发送真实数据。
“预检”请求时,允许请求方式则需服务器设置响应头:Access-Control-Request-Method
“预检”请求时,允许请求头则需服务器设置响应头:Access-Control-Request-Headers
cors在Django实现
在返回结果中加入允许信息(简单请求)
def test(request):
import json
obj=HttpResponse(json.dumps({'name':'yuan'}))
# obj['Access-Control-Allow-Origin']='*'
obj['Access-Control-Allow-Origin']='http://127.0.0.1:8004'
return obj
放到中间件中处理复杂和简单请求
from django.utils.deprecation import MiddlewareMixin
class MyCorsMiddle(MiddlewareMixin):
def process_response(self, request, response):
# 简单请求:
# 允许http://127.0.0.1:8001域向我发请求
# ret['Access-Control-Allow-Origin']='http://127.0.0.1:8001'
# 允许所有人向我发请求
response['Access-Control-Allow-Origin'] = '*'
if request.method == 'OPTIONS':
# 所有的头信息都允许
response['Access-Control-Allow-Headers'] = '*'
return response
在setting中配置即可,在中间件中的位置可以随意。
也可以通过第三方组件:pip install django-cors-headers
三方组件实现如下:
# (1)
pip install django-cors-headers
# (2)
INSTALLED_APPS = (
'corsheaders',
)
# (3)
1 MIDDLEWARE = [
2 'django.middleware.security.SecurityMiddleware',
3 'django.contrib.sessions.middleware.SessionMiddleware',
4 'django.middleware.csrf.CsrfViewMiddleware',
5 'django.contrib.auth.middleware.AuthenticationMiddleware',
6 'django.contrib.messages.middleware.MessageMiddleware',
7 'django.middleware.clickjacking.XFrameOptionsMiddleware',
8 'corsheaders.middleware.CorsMiddleware', # 按顺序
9 'django.middleware.common.CommonMiddleware', # 按顺序
10 ]
# 配置白名单
1 CORS_ALLOW_CREDENTIALS = True#允许携带cookie
2 CORS_ORIGIN_ALLOW_ALL = True
3 CORS_ORIGIN_WHITELIST = ( '*')#跨域增加忽略
4 CORS_ALLOW_METHODS = ( 'DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'VIEW', )
5 #允许的请求头
6 CORS_ALLOW_HEADERS = (
7 'XMLHttpRequest',
8 'X_FILENAME',
9 'accept-encoding',
10 'authorization',
11 'content-type',
12 'dnt',
13 'origin',
14 'user-agent',
15 'x-csrftoken',
16 'x-requested-with',
17 'Pragma',
18 )
前端项目设置请求头记得添加到CORS_ALLOW_HEADERS
分页器
from .models import Book
from django.core.paginator import Paginator # 引入分页器
from django.core.paginator import EmptyPage # 引入错误 当查询的页数不在总页数里会报EmptyPage错误
可以try一下
# 数据库插入多条数据
book_list = []
for i in range(100):
book =Book(title="book_%s"%i,price=i*i)
book_list.append(book)
Book.objects.bulk_create(book_list)
def index(request):
book_list=Book.objects.all()
Paginator_obj=Paginator(book_list,8)
#对book_list分页,每页显示8条,返回值是一个分页器对象
print(Paginator_obj.count) # 显示的是book_list里有多少条数据
print(Paginator_obj.num_pages)# 显示的是总页数
print(page_range = Paginator_obj.page_range)# 页码的列表
# 取值从客户端传过来的页码数 默认为1
current_page_num = int(request.GET.get("page",1))
# 设置页码列表只显示11个页码,也就是左边5个页码右边5个页码
if Paginator_obj.num_pages>11: # 当总页码大于11个的时候
if current_page_num -5<1: # 第一种情况 当页码数-5小于1的时候,说明当前页码数超出正常范围出现负数的情况,直接让页码为 1到11
page_range=range(1,12)
elif current_page_num +5 >Paginator_obj.num_pages: # 第二种情况,当前页码加5大于最大页码数的时候页超出了正常范围,会出现不存在的页码数,直接让页码数为最后11个,也就是总页码数减
去11到总页码数
page_range=range(Paginator_obj.num_pages-11,Paginator_obj.num_pages+1)
else:
page_range=range(current_page_num-5,current_page_num+6)
else: # 当总页码少于11个的时候直接在模板文件里循环总页码数
page_range = Paginator_obj.page_range
try:
page1 = Paginator_obj.page(1) # 取第一页的page对象支持for循环
print(page1.object_list) # 显示第一页的所有数据对象
except EmptyPage as e:
当前页默认设置为第一页 = Paginator_obj.page(1)
print(page1.has_next()) #当前页是否有下一页
print(page1.next_page_number()) #当前页下一页的页码
print(page1.has_previous()) #当前页是否有上一页返回True或False
print(page1.previous_page_number()) #当前页上一页的页码
return render(request,'index.html',locals())
#html
<ul>
{%for book in page1%}
<li>{{book.title}}:{{book.price}}</li>
</ul>
分页页码列表
from django.core import paginator
from django.shortcuts import render,HttpResponse
from django.core.paginator import Paginator
from app01.models import *
# Create your views here.
def show(request):
#当前客户端传输过来的页码数也就是要查看的页码
cu_pag_num = int(request.GET.get('page'))
# 从数据库获取所有的数据对象
fyq_all = fyq.objects.all()
# 对所有的数据对象进行分页,一页显示2条数据 得到一个分页器对象
pag_obj = Paginator(fyq_all,2)
#计算这个分页器对象一共有多少页
pag_num = int(pag_obj.num_pages)
#if判断实现的是如果分页码数过多则给前端传值的时候固定传11个页码数
if pag_num<11:
# 当总页码数少于11页的时候则直接显示1到11
pag_list= range(1,12)
if pag_num>11:
# 当总页码数大于11的时候
if cu_pag_num-5<0:
#当前页码数-5小于0的时候说明这次请求的页码数包含第一页,也就是刚好前11个页码,不能出现
小于0
pag_list= range(1,12)
elif cu_pag_num+5 > pag_num:
# 当卡页码数+5大于总页码数的时候,说明这次请求的页码包含最后一页,也就是最后11个页码,
不能出现大于最大页码
pag_list= range(pag_num-11,pag_num+1)
else:
#不是小于0页不是大于最大页码的情况,则显示当前页码-5个到当前页码+5个的页码
pag_list = range(cu_pag_num-5,cu_pag_num+6)
#获取当前页包含的数据对象
cu_pag_obj = pag_obj.page(cu_pag_num)
return render(request,'fyq.html',locals())
def dab(request): # 在数据库生成90条数据
fyq_list = []
for i in range(10,100):
fyq_obj = fyq(id=i,title="书名:{}".format(i),price=i)
fyq_list.append(fyq_obj)
fyq.objects.bulk_create(fyq_list)
#print(fyq_list)
return HttpResponse('ok')
-----------------------------------------------------------------------------------------
<!DOCTYPE html>
<html>
<!-- 使用bootstarp的分页显示组件 -->
<head>
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<!-- 可选的 Bootstrap 主题文件(一般不用引入) -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
</head>
<body>
<!-- 显示当前页的数据 -->
{%for i in cu_pag_obj%}
<p>{{i.id}},{{i.title}},{{i.price}}</p>
{%endfor%}
<!-- 分页框架是引用bootstarp -->
<nav aria-label="Page navigation">
<ul class="pagination">
<!-- if判断实现的是请求的当前页面为1的时候 则让上一页无法点击 -->
{%if cu_pag_num == 1%}
<!--class="disabled"是引用的效果就是无法点击的效果 -->
<li class="disabled">
<a href="?page={{pag_num}}" aria-label="Previous">
<span aria-hidden="true">上一页;</span>
</a>
</li>
{%else%}
<!-- 请求页不是第一页的时候上一页可以点击,上一页链接的就是当前页面-1 add是模板语法的
过滤器也就是相加的意思 加负1相当于-1 -->
<li >
<a href="?page={{cu_pag_num|add:-1}}" aria-label="Previous">
<span aria-hidden="true">上一页;</span>
</a>
</li>
{%endif%}
<!-- 生成页码列表 -->
{%for i in pag_list%}
{%if cu_pag_num == i%}
<!-- i等于请求的页面的页码数的时候给当前页码一个深色的效果class="active -->
<li class="active"><a href="?page={{i}}">{{i}}</a></li>
{%else%}
<!-- i不等于请求的页面的页码数的时候则正常显示不给与其他效果 -->
<li><a href="?page={{i}}">{{i}}</a></li>
{%endif%}
{%endfor%}
<!-- 以下实现的是当请求的页码数等于最大页码数的时候 下一页按钮则无法点击效果class="disabled"
-->
{%if cu_pag_num == pag_num %}
<li class="disabled">
<!-- 如果可以点击的话则直接跳转到第一页实现一个循环效果 -->
<a href="?page=1" aria-label="Next" >
<span aria-hidden="true">下一页;</span>
</a>
</li>
{%else%}
<!-- 当前页码不是最大页码的时候则下一页按钮可以点击也就是请求的页码数加1,模板语法add -->
<li>
<a href="?page={{cu_pag_num|add:1}}" aria-label="Next" >
<span aria-hidden="true">下一页;</span>
</a>
</li>
{%endif%}
</ul>
</nav>
</body>
</html>
forms组件
校验字段的功能
from django.db import models
from django import forms
Modes文件
class UserInfo(models.Model):
name = models.Charfield(max_length=32)
pwd = models.Charfield(max_length=32)
email = models.EmailField()
tel = models.CharField(max_length=32)
class Text(forms.ModelForm):
class Meta:#两个实例,与上面自定义的字段没有关系
models = models.UserInfo #使用模型中的UserInfo表来生成字段
fields = '__all__' #是由表中的所有字段
fields = ['name','email'] # 只使用UserInfo表中的这个两个字段
模板文件
<form action="">
<p>用户名<input type="text" name="user"></p>
<p>密码<input type="text" name="user"></p>
<p>确认密码<input type="text" name="user"></p>
<p>邮箱<input type="text" name="user"></p>
<p>手机号<input type="text" name="user"></p>
<input type="submit" value="提交">
views文件
from django import forms
class UserForm(forms.Form):
# 校验赋值的name是不是字符串类型最小长度为5
name = forms.charField(min_length=5)
email=forms.EmailField()
def reg(request):
#传参形式必须是字典形式的,字典的键必须是UserForm中定义的变量名,否则就找不到该校验哪些规则
# 如果传参的键值不在定义的规则里则不匹配这个键值,不影响正确结果
form_obj =UserForm(request.POST) # 因为request.POST也是字典形式的
form_obj =UserForm({"name"="123","email"="1234@qq.com"})
form_obj.is_vaild()#校验赋值的数据是否符合UserForm设置的规则,全部符合规则返回True,有一个不符合否则返回False
# form_obj下的cleand_data属性会存放所有校验成功的键值对,字典形式的{"name"="123",
"email"="1234@qq.com"} ,errors属性存放错误的键值,键是传入的键,值是列表形式里面放错误信息
渲染标签的功能
views文件
from django import forms
class UserForm(forms.Form):
# 校验赋值的name是不是字符串类型最小长度为5
name = forms.charField(min_length=5,label="用户名")
email=forms.EmailField()
def index(request):
obj = 从数据库中查出来的models对象
form_obj = UserForm(instance=obj) # 实例化一个对象,如果有instance在渲染标签的时候标签中会显示当前models对象每个字段的当前值,没有的话就不显示
form_obj.name.label# 获取到lable属性值,也就是用户名
return render(request,"index.html",locals())# 将实例化的对象传给模板文件
模板文件
方式1:
<form action="">
#在UserForm里面定义charfield就会渲染出input标签并且是text类型
<p>用户名 {{form_obj.name}}</p> #会自动渲染出<input type="text" name="user">
<p>邮箱{{form_obj.name}}</p>
</form>
方式2:
<form action="">
{%for i in form.obj%} # i是每一个字段对象
<p>{{i.label}} {{i}} </p> # label就是 类中字段中定义的label
{%endfor%}
</form>
方式3:
<form action="">
{{form_obj.as_p}} # 渲染出来的效果用p标签包裹起来
</form>
显示错误信息和重置字段
<form action="">
# form.name.errors[0]显示错误信息,如果form_obj是校验后的,在渲染的时候会把值渲染到input框里,
可以实现输错不会重置当前输入的效果
<p>用户名 {{form_obj.name}}</p> <span>{{form.name.errors[0]}}</span>
<p>邮箱{{form_obj.name}}</p>
</form>
forms组件参数配置
from django.forms import widgets # 引入组件配置
from django import forms
class UserForm(forms.Form):
#error_mesages={"required":"不能为空","invalid":"格式错误"} 自定义错误信息 类中有的字段都生效
#键是固定的,required是为空错误,invalid是格式错误
name = forms.charField(min_length=5,label="用户名",error_mesages={"required":"不能为空","invalid":"格式错误"}) # error_mesages只有该字段生效
#wigets.TextInput(attrs={"class":"form-contorl"}),attrs是自定义标签输入,此时是给这个html标签加上class属值为form-contorl也可以在Meta中定义
email=forms.EmailField(wigets.TextInput(attrs={"class":"form-contorl"}))
#widgets=widgets.PasswordInput()密码字段密文显示也就是密码输入框
# select输入就是selectInput()
pwd = forms.CharField(min_length=4,widgets=widgets.PasswordInput(),)
class Meta:
wigets={
'email':forms.TextInput(attrs={"class":"form-contorl"})
}
forms组件校验的钩子
from django import forms
from django.core.exceptions import NON_FIELD_ERRORS,ValidationError # 引入错误包
class UserForm(forms.Form):
name = forms.charField(min_length=5,label="用户名")
email=forms.EmailField()
def clean_name(self): #局部钩子
val = self.cleaned_data.get("name")
ret = UserInfo.objects.filter(name=val)
if nor ter:
return val
else:
raise ValidationError("该用户已经注册")
def clean(self): # 全局钩子
pwd=self.cleaned_data.get("pwd")
r_pwd=self.cleaned_data.get("r_pwd")
if pwd==r_pwd:
return self.cleaned_data
else:
raise ValidationError('两次密码不一致!')
钩子源码解析:
第一步:需要一个form类
class MyForm(forms.Form):
name = forms.CharField(max_length=6)
password = forms.CharField(max_length=8, min_length=3)
email = forms.EmailField(required=False) # form表单required默认是True
第二步:实例化form对象
form_obj=MyForm({"name":"wpr","password":"123abc","email":"123@qq.com"})
第三步:查看数据校验是否合法
🌈一切从这里开始,先留个心
form_obj.is_valid() 只有当所有的字段都校验通过才会返回True 🐷is_开头返回的都是布尔值
第四步:查看校验错误的信息
form_obj.errors ⚠️这个里面放的是所有校验未通过的字段及错误提示,⚠️🐷🌸这里要注意的是钩子函数中的报错 不会被添加到errors里面,测试是看不到结果的
第五步:查看校验通过的数据
form_obj.cleaned_data ⚠️所有符合校验规则的数据都会被放到该对象中
tips:
🌸form组件校验数据的规则:从上往下依次取值校验;
校验通过的放到cleaned_data;
校验失败的放到errors;
⚠️form中所有的字段默认都是必须传值的(required=True);
🐨 校验数据的时候可以多传数据,多传的数据不会做任何校验,不会影响form校验规则
🐷前端取消校验<form action="" method="post" novalidate>
开始对钩子函数源码详解
首先is_valid( )是校验数据的部分,将数据放入is_valid( )开始校验,合格的放在哪里,不合格的放在哪里,因此如果不执行is_valid,是不能执行后面的cleaned_data或者errors,也就是说他是循环每个字段分别去校验,而cleaned_data和errors本质上就是两个字典,用来存放正确的数据和错误的数据。
🌸总结:学form组件最核心的方法是is_valid( ),最重要的源码也是is_valid(),钩子函数也在is_valid( )中。
前期准备
from django import forms
class MyForm(forms.Form):
name = forms.CharField(max_length=6)
password = forms.CharField(max_length=8, min_length=3)
email = forms.EmailField(required=False) # form表单required默认是True
form_obj=MyForm({"name":"wpr","password":"123abc","email":"123@qq.com"})
form_obj.is_valid()
# 钩子函数,当前面的校验都通过后才会执行
# 局部钩子函数,单个字段的校验利用局部钩子函数
def clean_name(self):
name = self.cleaned_data.get('name') # 首先从校验正确的数据中获取名字
if '666' in name: # 对名字进行逻辑判断
self.add_error('name', '光喊666不行') # 对应name字段名进行提醒
return name
# 全局钩子函数,多个字段校验利用全局钩子函数
def clean(self):
password = self.cleaned_data.get('password') # 从校验通过的字典中取得密码
confirm_password = self.cleaned_data.get('confirm_password') # 从校验通过的字典中
取得确认密码
if not password == confirm_password: # 得到数据后进行逻辑判断
self.add_error('confirm_password', '两次密码不一致') # 判断后在确认密码后面进行
提醒
return self.cleaned_data
进入is_valid( )查看
def is_valid(self):
"""
Returns True if the form has no errors. Otherwise, False. If errors are
being ignored, returns False.
"""
return self.is_bound and not self.errors
详解:首先铺陈一个基础,True and True返回的是True,True and False返回的是False。这里and连接两个返回,前面的self.is_bound返回的一定是True,那么is_valid最后返回True还是False取决于errors到底是空字典还是有键值的,而当errors为空字典,说明没有任何错误,那么not 空就是True,如果errors里面有错误的键值,那么就返回False。
那么接下来就主要看self.errors里面返回的是什么
# errors默认为None
self._errors = None # Stores the errors after clean() has been called.
@property
def errors(self):
"Returns an ErrorDict for the data provided for the form"
if self._errors is None:
self.full_clean() # 因为默认为None,所以继续从这进入
return self._errors
进入full_clean( )查看
def full_clean(self):
"""
Cleans all of self.data and populates self._errors and
self.cleaned_data.
"""
self._errors = ErrorDict() # ⚠️原来错误字典是从这里来
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {} # ⚠️正确字典是从这里来
# If the form is permitted to be empty, and none of the form data has
# changed from the initial data, short circuit any validation.
if self.empty_permitted and not self.has_changed():
return
self._clean_fields()
self._clean_form()
self._post_clean()
详解:拿到两个初始变量,从逻辑上讲,接下来就是循环当前form类中的所有字段,依次判断输入进来的值和字段规则是否符合,符合就放入cleaned_data字典中,不符合就放入errors字典中。
🐷 tips:看源码时要知道自己该看什么,不要什么都看,只看我们当前逻辑关心的地方
⭕️高能!!重要!!那么接下来我们就要看clean_field( ) 校验字段里面是什么
ps:clean是校验的意思
def _clean_fields(self):
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
if field.disabled:
value = self.get_initial_for_field(field, name)
else:
value = field.widget.value_from_datadict(self.data, self.files,
self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.get_initial_for_field(field, name)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self.add_error(name, e)
详解:
1、self.fields在类实例化时完成赋值,self.fields={"name":name字段对象,"password":password字段对象
,"email":email字段对象},所以name对应的是字段字符串,field对应的是字段对象(也是规则对象),
[比如这里就是name:"name" field:name或者name:"password" field:password]。
2、往下看到value,这个value指的是传进来的字典的值(比如这里指字典中name的值wpr)。
3、接着是if isinstance(field,FileField),指的是字段对象是否为文件类型,在这里三个属性分别是
CharField,CharField,EmailField,没有涉及到文件类型,所以走value = field.clean(value)。
4、那就来分析value = field.clean(value)指的是用字段对象来校验这个value值,然后将它重新赋值
给value,校验通过后加到cleaned_data字典中,name是这个字段字符串,value是这个通过的值,但是如果这里clean校验不通过,就会抛出一个validdation的错误,由于clean是用c语言封装起来的,所以不去深究,只要知道不通过会报错即可。
5、下一句if hasattr(self, 'clean_%s' % name): ⚠️是当上面第一层校验通过后,再走第二层钩子函数的校验,判断当前类下是否有一个叫 'clean_%s' % name名字的方法,如果有就将这个方法取出加个括号来调用这个方法,这时调用第二层钩子方法,得到一个返回值(⚠️🌸 敲黑板!!注意这里就是为什么在钩子函数中也要返回的原因,但是如果不写也不会报错,这是因为他已经通过了第一层校验,cleaned_data中已经存了那个名字,所以有时不加也没事,但为了防止版本问题产生不必要的bug,还是写上返回值,严谨!!!)
🐷敲黑板:要第一层校验通过才走钩子函数,如果第一层都没通过,钩子是没用的!!!
6、无论第一次还是第二次校验不通过就会抛出异常except ValidationError as e:self.add_error(name, e),把键和错误信息放入errors中。
7、但是这时有个疑问,从逻辑上讲如果第一层通过了,cleaned_data已经存了正确的键值,那如果第二层不通过,cleaned_data就不应该有这个键值,那么关键就在这个add_error( )中。
8、那我们就进入add_error( )中去一看究竟:
注意找有用的信息!!
for field, error_list in error.items():
if field not in self.errors:
if field != NON_FIELD_ERRORS and field not in self.fields:
raise ValueError(
"'%s' has no field named '%s'." % (self.__class__.__name__, field))
if field == NON_FIELD_ERRORS:
self._errors[field] = self.error_class(error_class='nonfield')
else:
self._errors[field] = self.error_class()
self._errors[field].extend(error_list)
if field in self.cleaned_data: ⚠️# 关键就在这一句,如果字段对象已经在cleaned_data中
del self.cleaned_data[field] # 那么就从cleaned_data中删除这个字段
9、那从整体看是通过try except来控制,如果正确放入cleaned_data,如果错误放入errors中。
10、最后只要errors字典里面有键值,就返回False。
🐷 ps:可以将字段对象理解为字段规则/规则对象;
字典是是无序的(.items),但在最新版本中中将字典变成有序的了,有一个OrderedDict模块,这个字典保证我们的键值是有序的,在我们定义的时候谁是第一个键值,在我们以后用的时候他都是第一个,这就保证了我们校验的时候是有序的来,先校验第一个字段,再依次校验,如果是无序的,for循环的时候都不知道要校验哪一个;
def items(self):
"D.items() -> a set-like object providing a view on D's items"
return _OrderedDictItemsView(self) # 变成有序的了
HTTP协议无状态保存
HTTP协议是无状态保存的,也就是每个请求都是独立的,无法记录前一次的请求状态,但是在http协议中可以使用cookie来完成会话的跟踪,在web开发中使用session来完成会话跟踪,session底层以来cookie技术
cookie简介
Cookie是key-value结构,类似于一个python中的字典。随着服务器端的响应发送给客户端浏览器。然后客户端浏览器会把Cookie保存起来,当下一次再访问服务器时把Cookie再发送给服务器。 Cookie是由服务器创建,然后通过响应发送给客户端的一个键值对。客户端会保存Cookie,并会标注出Cookie的来源(哪个服务器的Cookie)。当客户端向服务器发出请求时会把所有这个服务器Cookie包含在请求中发送给服务器,这样服务器就可以识别客户端了。
cookie其实是不安全的,因为数据保存在客户端,虽然是服务端提供的
Cookie规范
Cookie大小上限为4KB;
一个服务器最多在客户端浏览器上保存20个Cookie;
一个浏览器最多保存300个Cookie;
上面的数据只是HTTP的Cookie规范,但在浏览器大战的今天,一些浏览器为了打败对手,为了展现自己的能力起见,可能对Cookie规范“扩展”了一些,例如每个Cookie的大小为8KB,最多可保存500个Cookie等!但也不会出现把你硬盘占满的可能!
注意,不同浏览器之间是不共享Cookie的。也就是说在你使用IE访问服务器时,服务器会把Cookie发给IE,
然后由IE保存起来,当你在使用FireFox访问服务器时,不可能把IE保存的Cookie发送给服务器。
Cookie与HTTP头
Cookie是通过HTTP请求和响应头在客户端和服务器端传递的:
Cookie:请求头,客户端发送给服务器端;
格式:Cookie: a=A; b=B; c=C。即多个Cookie用分号离开; Set-Cookie:响应头,服务器端发送给客户端
一个Cookie对象一个Set-Cookie: Set-Cookie: a=A Set-Cookie: b=B Set-Cookie: c=C
Cookie的覆盖
如果服务器端发送重复的Cookie那么会覆盖原有的Cookie,例如客户端的第一个请求服务器端发送的Cookie是:Set-Cookie: a=A;第二请求服务器端发送的是:Set-Cookie: a=AA,那么客户端只留下一个Cookie,
即:a=AA。
django中的cookie语法
设置cookie:
rep = HttpResponse(...) 或 rep = render(request, ...) 或 rep = redirect()
rep.set_cookie(key,value,...)
rep.set_signed_cookie(key,value,salt='加密盐',...)
#签名的cookie
#使用set_signed_cookie后如果要取cookie应该用request.get_signed_cookie('cookie键',salt='加密盐')
#会将值和加密盐进行处理后的值 与 从请求中取出来的签名进行比较
源码:
class HttpResponseBase:
def set_cookie(self, key, # 键
value='',#值
max_age=None,
#cookie的有效时间
#长时间
#cokie需要延续的时间(以秒为单位)如果参数是\ None ,
#这个cookie会延续到浏览器关闭为止。
expires=None,
#长时间
#expires默认None ,cookie失效的实际日期/时间。
path='/',
#cookie生效的路径,
#浏览器只会把cookie回传给带有该路径的页面,这样可以避免将
#cookie传给站点中的其他的应用。
#/ 表示根路径,特殊的:根路径的cookie可以被任何url的页面访问
domain=None,
#ookie生效的域名
#可用这个参数来构造一个跨站cookie。
#如, domain=".example.com"
#所构造的cookie对下面这些站点都是可读的:
#www.example.com 、 www2.example.com 和 #n.other.sub.domain.example.com 。
# 如果该参数设置为 None ,cookie只能由设置它的站点读取。
secure=False,
#果设置为 True ,浏览器将通过HTTPS来回传cookie。
httponly=False
#能http协议传输,无法被JavaScript获取
#不是绝对,底层抓包可以获取到也可以被覆盖)
): pass
获取cookie:
request.COOKIES
删除cookie:
response.delete_cookie("cookie_key",path="/",domain=name)
session
Session是服务器端技术,利用这个技术,服务器在运行时可以 为每一个用户的浏览器创建一个其独享的session对象,由于 session为用户浏览器独享,所以用户在访问服务器的web资源时 ,可以把各自的数据放在各自的session中,当用户再去访问该服务器中的其它web资源时,其它web资源再从用户各自的session中 取出数据为用户服务。
Django 提供对匿名会话(session)的完全支持。这个会话框架让你可以存储和取回每个站点访客任意数据。它在服务器端存储数据, 并以cookies的形式进行发送和接受数据。
1. 游览器第一次请求携带cookie为空{}
2. 游览器收到请求后,标识这次会话,可以设置 request.session['username']="uty"
3.设置后 会生成一个随机字符串,把响应体给客户端的时候会设置set_cookie,键是sessionid值是生成的随机字符串,(这个是django自动操作的) , 然后找到session数据表 生成一条记录 session-key是随机字符串session-data是设置的键值对
4.游览器发送第二次请求会携带cookie:{sessionid=生成的随机字符串}
5.服务器取数据的时候可以设置 request.session.get('username') 会在内部完成3个操作
1.读cookie中携带的随机字符串
2.到session数据表中过滤session-key等于随机字符串的记录对象
3.从过滤的记录对象.session_data.get('username')得到session数据
django session语法
1、设置Sessions值
request.session['session_name'] ="admin"
2、获取Sessions值
session_name = request.session["session_name"]
3、删除Sessions值
del request.session["session_name"]
4、flush()
request.flush()
删除当前的会话数据并删除会话的Cookie。
这用于确保前面的会话数据不可以再次被用户的浏览器访问
5、get(key, default=None)
fav_color = request.session.get('fav_color', 'red')
6、pop(key)
fav_color = request.session.pop('fav_color')
7、keys()
8、items()
9、setdefault()
10 用户session的随机字符串
request.session.session_key
# 将所有Session失效日期小于当前日期的数据删除
request.session.clear_expired()
# 检查 用户session的随机字符串是否在数据库中
request.session.exists("session_key")
# 删除当前用户的所有Session数据
request.session.delete("session_key")
request.session.set_expiry(value)
* 如果value是个整数,session会在些秒数后失效。
* 如果value是个datatime或timedelta,session就会在这个时间后失效。
* 如果value是0,用户关闭浏览器session就会失效。
* 如果value是None,session会依赖全局session失效策略。
session配置
Django默认支持Session,并且默认是将Session数据存储在数据库中,即:django_session 表中。
a. 配置 settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 引擎(默认)
# Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串(默认)
SESSION_COOKIE_NAME = "sessionid"
SESSION_COOKIE_PATH = "/" # Session的cookie保存的路径(默认)
SESSION_COOKIE_DOMAIN = None # Session的cookie保存的域名(默认)
SESSION_COOKIE_SECURE = False # 是否Https传输cookie(默认)
SESSION_COOKIE_HTTPONLY = True # 是否Session的cookie只支持http传输(默认)
SESSION_COOKIE_AGE = 1209600 # Session的cookie失效日期(2周)(默认)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # 是否关闭浏览器使得Session过期(默认)
SESSION_SAVE_EVERY_REQUEST = False # 是否每次请求都保存Session,默认修改之后才保存(默认)
session的更新
对于同一用户,同一个客户端,在第一次登陆的时候cookie是空的会生成session会话,并返回随机字符串下次访问的时候会带这个随机字符串,在第1次以后访问,如果服务器的sesion-data有改变,则在session数据表里根据session-key更新session-data而不会生成新的记录
对与同一用户换了一个客户端登陆,那么第一次登陆的时候会生成一个seeion会话并返回随机字符串下次访问的时候会带这个随机字符串,而不是在上一个客户端的记录上修改
总结:如果请求信息过来的时候带着session-id,如果session-data有了改变则更新,如果请求信息过来的时候没有带session-id,则生成一个新的记录
- session 在服务器端,cookie 在客户端(浏览器)
- session 默认被存在在服务器的一个文件里(不是内存)
- session 的运行依赖 session id,而 session id 是存在 cookie 中的.
- session 可以放在 文件、数据库、或内存中都可以。
- 用户验证这种场合一般会用 session
用户认证组件
django默认提供了认证系统auth模块,我们认证的时候,会使用auth模块里面给我们提供的表,认证系统包含:
- 用户管理
- 权限
- 用户组
- 密码哈希系统
- 用户登录或内容显示的表单和视图
- 一个可插拔的后台系统admin
用户认证组件的功能是:用session记录登陆验证状态,前提是有一张用户表,也就是django自带的auth_user,用户认证组件表
创建超级用户:python3 manage.py createsuperuser
Django 用户认证(Auth)组件需要导入 auth 模块
from django.contrib import auth
# 对应数据库用户表,可以继承扩展
from django.contrib.auth.models import User
1.authenticate()认证方法
提供了用户认证,即验证用户名以及密码是否正确,一般需要两个关键字参数 username password
如果认证信息有效会返回一个user对象。authenticate()会在user对象上设置一个属性标识后端认证了该用户,且该信息在后面的登陆过程中是需要的,当我们试图登陆一个从数据库中直接取出来不经过authenticate()的user对象会报错的
auth.authenticate(username="123",password="456")
# 会去auth_user表里找username="123",password="456"成功返回这个记录对象,失败返回None 将输入的密码转为密文去认证
2.用户登陆和注销
auth.login(request,user)
# 相当于 request.session['user_id']= user.pk,user_id是固定的名字
#reques是当前请求,,user是用户认证成功的记录对象,该函数接受一个HttpRequest对象,以及一个认证了的User对象。此函数使用django的session框架给某个已认证的用户附加上session id等信息。
(当前登陆对象),以后在其他视图里通过request.user取值的时候都会取到当前登陆成功的对象,如果没有登陆,会取到一个匿名用户对象,这个匿名用户对象的字段都是空的
#此函数使用django的session框架给某个已认证的用户附加上session id等信息
auth.logout(request) # request是当前请求信息
#该函数接受一个HttpRequest对象,无返回值。当调用该函数时,当前请求的session信息会全部清除。该用户即使没有登录,使用该函数也不会报错。
3.user对象
User 对象属性:username, password(必填项)password用哈希算法保存到数据库
user其实就是auth_user表
user.
create() # 创建一个普通用户,密码是明文的。
create_user() # 创建一个普通用户,密码是密文的。
create_superuser() # 与create_user() 相同,但是设置is_staff 和is_superuser 为True。
set_password(*raw_password*)
# 设置用户的密码为给定的原始字符串,并负责密码的。 不会保存User对象。当None为raw_password时,密码将设置为一个不可用的密码。
check_password(*raw_password*)
# 如果给定的raw_password是用户的真实密码,则返回True,可以在校验用户密码时使用。
user对象的 is_authenticated()
如果是真正的 User 对象,返回值恒为 True 。 用于检查用户是否已经通过了认证。
通过认证并不意味着用户拥有任何权限,甚至也不检查该用户是否处于激活状态,这只是表明用户成功的通过了认证。 这个方法很重要, 在后台用request.user.is_authenticated()判断用户是否已经登录,如果true则可以向前台展示request.user.name
要求:
1 用户登陆后才能访问某些页面,
2 如果用户没有登录就访问该页面的话直接跳到登录页面
3 用户在跳转的登陆界面中完成登陆后,自动访问跳转到之前访问的地址
方法1:
def my_view(request):
if not request.user.is_authenticated():
return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
方法2:
django已经为我们设计好了一个用于此种情况的装饰器:login_requierd()
from django.contrib.auth.decorators import login_required
@login_required
def my_view(request):
...
若用户没有登录,则会跳转到django默认的 登录URL ‘/accounts/login/ ‘ (这个值可以在settings文件中通过LOGIN_URL进行修改)。并传递 当前访问url的绝对路径 (登陆成功后,会重定向到该路径)。
创建用户
使用 create_user 辅助函数创建用户:
from django.contrib.auth.models import User
user = User.objects.create_user(username='',password='',email='')
check_password(passwd)
用户需要修改密码的时候 首先要让他输入原来的密码 ,如果给定的字符串通过了密码检查,返回 True
修改密码
使用 set_password() 来修改密码
user = User.objects.get(username='')
user.set_password(password='')
user.save
匿名用户
匿名用户
class models.AnonymousUser
django.contrib.auth.models.AnonymousUser 类实现了django.contrib.auth.models.User 接口,但具有下面几个不同点:
id 永远为None。
username 永远为空字符串。
get_username() 永远返回空字符串。
is_staff 和 is_superuser 永远为False。
is_active 永远为 False。
groups 和 user_permissions 永远为空。
is_anonymous() 返回True 而不是False。
is_authenticated() 返回False 而不是True。
set_password()、check_password()、save() 和delete() 引发 NotImplementedError。
New in Django 1.8:
新增 AnonymousUser.get_username() 以更好地模拟 django.contrib.auth.models.User。
当前登录对象
request.user
Django有一个默认中间件,叫做AuthenticationMiddleware,每次请求进来都会去session中去一个userid,取不到的话,赋值request.user = AnonymousUser() , 一个匿名用户对象。
当用户组件auth.login一旦执行,将userid赋值到session中后,再有请求进入Django,将注册的userid对应的user对象赋值给request.user,即再后面的任何视图函数中都可以从request.user中取到该客户端的登录对象。
自定义用户表
from django.contrib.auth.models import AbstractUser
设置Auth认证模块使用的用户模型为我们自己定义的用户模型,模型类必须继承AbstractUser
格式:“子应用目录名.模型类名”
在setting中配置使用的用户表
AUTH_USER_MODEL = ‘users.User’
中间件
中间件顾名思义,是介于request与response处理之间的一道处理过程,相对比较轻量级,并且在全局上改变django的输入与输出。因为改变的是全局,所以需要谨慎实用,用不好会影响到性能。
Django的中间件的定义:
Middleware is a framework of hooks into Django’s request/response processing.
It’s a light, low-level “plugin” system for globally altering Django’s input or output.
MiddleWare,是 Django 请求/响应处理的钩子框架。
它是一个轻量级的、低级的“插件”系统,用于全局改变 Django 的输入或输出。【输入指代的就是客户端像服务端django发送数据,输出指代django根据客户端要求处理数据的结果返回给客户端】
如果你想修改请求,例如被传送到view中的HttpRequest对象。 或者你想修改view返回的HttpResponse对象,这些都可以通过中间件来实现。
可能你还想在view执行之前做一些操作,这种情况就可以用 middleware来实现。
Django默认的Middleware
:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
每一个中间件都有具体的功能。
自定义中间件
中间件中一共有四个方法:
- process_request
request方法默认返回None,返回None则继续执行下一个中间件的process_request,一旦返回非None,则直接拦截并源路返回响应给客户端,不再往下执行,【原路返回是指经过的中间件的response还是会执行的】
- process_view
可以在视图操作之前,对数据进行处理,process_view如果有返回值,就不再执行
后面的process_view和视图函数,而是原路返回执行经过中间件的
process_response。
- process_exception
视图代码正常的时候不会执行process_exception,当视图中出现错误的时候才会
执行,当process_exception有返回值的时候,就不在执行后面的
process_exception和前面经过的process_exception,而是直接执行经过中间件的
response,如果没有返回值就会执行前面的process_exception
- process_response
process_response必须有一个形参response,并且必须要return response,
response接收是view函数返回的响应,可以对response进行修改但是必须返回,
就像接力棒一样最后传递给客户端
process_request,process_response
当用户发起请求的时候会依次经过所有的的中间件,这个时候的请求是process_request,最后到达views的函数中,views函数处理后,再依次穿过中间件,这个时候是process_response,最后返回给请求者。
我们也可以自己定义一个中间件,我们可以自己写一个类,但是必须继承MiddlewareMixin
需要导入该模板
from django.utils.deprecation import MiddlewareMixin
在中间件中注册自定义的中间件,是路径字符串的格式
视图函数中:
def index(request):
print("view函数...")
return HttpResponse("OK")
中间件文件 Mymiddlewares.py:
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
class Md1(MiddlewareMixin):
def process_request(self,request):
# 默认返回None,None表示可以继续往后执行中间件,如过不是返回None则拦截该请求不再继续往下执行。但是前面经过的中间件的response会继续执行,但是未经过的response则不会执行
print("Md1请求")
def process_response(self,request,response):
#必须返回response,如果是处理后的响应,则应该要返回处理后的响应给下一个中间件
#如果没处理响应,则直接返回源响应就可以
print("Md1返回")
return response
class Md2(MiddlewareMixin):
def process_request(self,request):
print("Md2请求")
def process_response(self,request,response):
print("Md2返回")
return response
结果:
Md1请求
Md2请求
view函数…
Md2返回
Md1返回
注意:如果当请求到达请求2的时候直接不符合条件返回,即return HttpResponse(“Md2中断”),程序将把请求直接发给中间件2返回,然后依次返回到请求者,结果如下:
返回Md2中断的页面,后台打印如下:
Md1请求
Md2请求
Md2返回
Md1返回
流程图如下:
process_view
process_view(self, request, callback, callback_args, callback_kwargs)
#request 接收的是请求信息
#callback 接收的是本次请求所对应的视图函数。经过所有中间件的request后进行路由控制,然后才执行process_view,在路由控制的时候就会查到是应该执行哪个函数,是根据请求中的路径决定的
#callback_args 视图函数所对应的参数
#callback_kwargs
Mymiddlewares.py修改如下
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
class Md1(MiddlewareMixin):
def process_request(self,request):
print("Md1请求")
#return HttpResponse("Md1中断")
def process_response(self,request,response):
print("Md1返回")
return response
def process_view(self, request, callback, callback_args, callback_kwargs):
print("Md1view")
class Md2(MiddlewareMixin):
def process_request(self,request):
print("Md2请求")
return HttpResponse("Md2中断")
def process_response(self,request,response):
print("Md2返回")
return response
def process_view(self, request, callback, callback_args, callback_kwargs):
print("Md2view")
结果如下:
Md1请求
Md2请求
Md1view
Md2view
view函数...
Md2返回
Md1返回
下图进行分析上面的过程:
当最后一个中间的process_request到达路由关系映射之后,返回到中间件1的process_view,然后依次往下,到达views函数,最后通过process_response依次返回到用户。
process_view可以用来调用视图函数:
class Md1(MiddlewareMixin):
def process_request(self,request):
print("Md1请求")
#return HttpResponse("Md1中断")
def process_response(self,request,response):
print("Md1返回")
return response
def process_view(self, request, callback, callback_args, callback_kwargs):
# return HttpResponse("hello")
response=callback(request,*callback_args,**callback_kwargs)
return response
结果如下:
Md1请求
Md2请求
view函数...
Md2返回
Md1返回
注意:process_view如果有返回值,就不再执行后面的process_view和视图函数,而是原路返回执行经过中间件的process_response。
process_exception
process_exception(self, request, exception)
#exception接收的是视图代码出错的错误信息
示例修改如下:
class Md1(MiddlewareMixin):
def process_request(self,request):
print("Md1请求")
#return HttpResponse("Md1中断")
def process_response(self,request,response):
print("Md1返回")
return response
def process_view(self, request, callback, callback_args, callback_kwargs):
# return HttpResponse("hello")
# response=callback(request,*callback_args,**callback_kwargs)
# return response
print("md1 process_view...")
def process_exception(self,request,exception):
print("md1 process_exception...")
class Md2(MiddlewareMixin):
def process_request(self,request):
print("Md2请求")
# return HttpResponse("Md2中断")
def process_response(self,request,response):
print("Md2返回")
return response
def process_view(self, request, callback, callback_args, callback_kwargs):
print("md2 process_view...")
def process_exception(self,request,exception):
print("md1 process_exception...")
结果如下:
Md1请求
Md2请求
md1 process_view...
md2 process_view...
view函数...
Md2返回
Md1返回
流程图如下:
当views出现错误时:
将md2的process_exception修改如下:
def process_exception(self,request,exception):
print("md2 process_exception...")
return HttpResponse("error")
结果如下:
Md1请求
Md2请求
md1 process_view...
md2 process_view...
view函数...
md2 process_exception...
Md2返回
Md1返回
应用案例
1、做IP访问频率限制
某些IP访问服务器的频率过高,进行拦截,比如限制每分钟不能超过20次。
2、URL访问过滤
如果用户访问的是login视图(放过)
如果访问其他视图,需要检测是不是有session认证,已经有了放行,没有返回login,这样就省得在多个视图函数上写装饰器了!
源码试读
作为延伸扩展内容,可以尝试着读一下以下两个自带的中间件:
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
ImageField 和 FileField
ImageField 和 FileField 可以分别对图片和文件进行上传到指定的文件夹中。
- 在下面的 models.py 中 :
picture = models.ImageField(upload_to=’avatars/‘, default=”avatars/default.png”,blank=True, null=True) 注:定义 ImageField 字段时必须制定参数 upload_to这个字段要写相对路径,
这个参数会加在 settings.py 中的 MEDIA_ROOT后面, 形成一个路径, 这个路径就是上 传图片的存放位置,默认在Django项目根路径下,也就是MEDIA_ROOT默认是Django根目录
所以要先设置好 mysite/settings.py中的 settings.py 中的 MEDIA_ROOT
class Userinfo(models.Model):
name = models.CharField(max_length=32)
avatar_img = models.FileField("avatars/")
username = request.POST.get("username")
#获取文件对象
file = request.FILES.get("file")
#插入数据,将图片对象直接赋值给字段
user = Userinfo.objects.create(name=username,avatar_img=file)
Django会在项目的根目录创建avatars文件夹,将上传文件下载到该文件夹中,avatar字段保存的是文件的相对路径。
- 在 mysite/settings.py中 :
MEDIA_ROOT = os.path.join(BASE_DIR,"media")
MEDIA_URL='/media/'
MEDIA_ROOT:存放 media 的路径, 这个值加上 upload_to的值就是真实存放上传图片文件位置
MEDIA_URL:给这个属性设值之后,静态文件的链接前面会加上这个值,如果设置这个值,则UserInfo.avatar.url自动替换成:/media/avatars/default.png,可以在模板中直接调用:
3.url.py:
from django.views.static import serve
# 添加media 配置
re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
浏览器可以直接访问http://127.0.0.1:8000/media/yuan/avatars/xxxAB.png,即我们的用户上传文件。
最后再给大家补充一个用户文件夹路径
def user_directory_path(instance, filename):
return os.path.join(instance.name,"avatars", filename)
class Userinfo(models.Model):
name = models.CharField(max_length=32)
avatar_img = models.FileField(upload_to=user_directory_path)
4.FileField 和 ImageFiled 相同。
Django3的ASGI
Web应用程序和web服务器
Web应用程序(Web)是一种能完成web业务逻辑,能让用户基于web浏览器访问的应用程序,它可以是一个实现http请求和响应功能的函数或者类,也可以是Django、Flask、sanic等这样的web框架,当然也可以是其他语言的web程序或web框架。
Web服务器(Web Server)是一种运行于网站后台(物理服务器)的软件。Web服务器主要用于提供网页浏览或文件下载服务,它可以向浏览器等Web客户端提供html网页文档,也可以提供其他类型的可展示文档,让客户端用户浏览;还可以提供数据文件下载等。目前世界上最主流的Web服务器有 Nginx 、Apache、IIS、tomcat。
问:Web服务器和Web应用程序的区别?
答:Web应用程序主要是完成web应用的业务逻辑的处理,Web服务器则主要是应对外部请求的接收、响应和转发。
需要使用web服务器启动运行,web应用程序才能被用户访问到。
而django框架中,我们之所以只有一个web应用程序就跑起来了,是因为我们在终端执行了一个命令,python manage.py runserver。这个命令启动了django框架中内置提供的测试web服务器。
网关接口
网关接口(Gateway Interface,GI)就是一种为了实现加载动态脚本而运行在Web服务器和Web应用程序中的通信接口,也可以理解为一份协议/规范。只有Web服务器和Web应用程序都实现了网关接口规范以后,双方的通信才能顺利完成。常见的网关接口协议:CGI,FastCGI,WSGI,ASGI。
1. CGI
CGI(Common Gateway Inteface): 字面意思就是通用网关接口,
它是外部应用程序与Web服务器之间的接口标准
意思就是它用来规定一个程序该如何与web服务器程序之间通信从而可以让这个程序跑在web服务器上。当然,CGI 只是一个很基本的协议,在现代常见的服务器结构中基本已经没有了它的身影,更多的则是它的扩展和更新。
FastCGI: CGI的一个扩展, 提升了性能,废除了 CGI fork-and-execute (来一个请求 fork 一个新进程处理,处理完再把进程 kill 掉)的工作方式,转而使用一种长生存期的方法,减少了进程消耗,提升了性能。
这里 FastCGI 就应用于前端 server(nginx)与后端 server(uWSGI)的通信中,制定规范等等,让前后端服务器可以顺利理解双方都在说什么(当然 uWSGI 本身并不用 FastCGI, 它有另外的协议)
2.WSGI
WSGI(Python Web Server GateWay Interface):它是用在 python web 框架编写的应用程序与后端服务器之间的规范(本例就是 Django 和 uWSGI 之间),让你写的应用程序可以与后端服务器顺利通信。在 WSGI 出现之前你不得不专门为某个后端服务器而写特定的 API,并且无法更换后端服务器,而 WSGI 就是一种统一规范, 所有使用 WSGI 的服务器都可以运行使用 WSGI 规范的 web 框架,反之亦然。
WSGI和ASGI,都是基于Python设计的网关接口(Gateway Interface,GI)。
Web服务器网关接口(Python Web Server Gateway Interface,WSGI),是Python为了解决Web服务器端与客户端之间的通信基于CGI标准而设计的。实现了WSGI协议的web服务器有:uWSGI、uvicorn、gunicorn。像django框架一般开发中就不会使用runserver来运行,而是采用上面实现了WSGI协议的web服务器来运行。
django中运行runserver命令时,其实内部就启动了wsgiref模块作为web服务器运行的。wsgiref是python内置的一个简单地遵循了wsgi接口规范的web服务器程序。
from wsgiref.simple_server import make_server
# application 由wsgi服务器调用、函数对http请求与响应的封装、使得Python专注与HTML
# environ http 请求 (dist)
# start_response 响应 (function)
def application(environ, start_response):
# 请求
if environ['REQUEST_METHOD'] == 'GET' and environ['PATH_INFO'] == '/':
# 响应
start_response('200 OK', [('Content-Type', 'text/html')])
return [b'<h1>hi, python!</h1>']
if __name__ == '__main__':
# 启动服务器 | 这个服务器负责与 wsgi 接口的 application 函数对接数据
httpd = make_server('127.0.0.1', 8000, application)
# 监听请求
httpd.serve_forever()
# 1. 监听8000端口,
# 2. 把http请求根据WSGI协议将其转换到applcation中的environ参数, 然后调用application函数.
# 3. wsgiref会把application函数提供的响应头设置转换为http协议的响应头,
# 4. 把application的返回(return)作为响应体, 根据http协议,生成响应, 返回给浏览器.
开发中,我们一般使用uWSGI或者Gunicorn作为web服务器运行django。
3.uWSGI
uWSGI 是一个快速的,自我驱动的,对开发者和系统管理员友好的应用容器服务器,用于接收前端服务器转发的动态请求并处理后发给 web 应用程序。完全由 C 编写,实现了WSGI协议,uwsgi,http等协议。注意:uwsgi 协议是一个 uWSGI服务器自有的协议,用于定义传输信息的类型,常用于uWSGI服务器与其他网络服务器的数据通信中。
uwsgi: 是uWSGI服务器实现的独有的协议, 网上没有明确的说明这个协议是用在哪里的,我个人认为它是用于前端服务器与 uwsgi 的通信规范,相当于 FastCGI的作用。
关于ASGI
历史趣谈
2019 年 12 月,Django3.0 发布,它具有一个有趣的新特性——支持 ASGI 服务器。
2003 年,诸如Zope、Quixote之类的各式各样的 Python Web 框架都附带了自己的 Web 服务器,或者拥有自己的本地接口来与流行的网络服务器(如 Apache)进行通信。
成为一名 Python Web 开发人员意味着要全身心投入到一个完整的技术栈中,但是如果需要另一个框架时,则必须重新学习所有内容。可以想象到这会导致碎片化。PEP 333,即 Python Web 服务器网关接口 v1.0,尝试通过定义一个被称为 WSGI(Web Server Gateway Interface)的简单标准接口来解决这个问题。它的卓越之处在于它的简单性。
WSGI 如此流行,以至它不仅被诸如 Django 和 Pylons 之类的大型 Web 框架所采用,还被诸如 Bottle 之类的微框架所采用。
为什么有ASGI的出现
如果我们对 WSGI 非常满意的话,为什么还要提出 ASGI 呢?如果仔细检查网络请求的整个处理流程,那么答案就非常明显了。您可以查看 在Django中网络请求如何工作 的动画。请注意,在数据库查询之后且响应发送之前,框架是如何等待的。这就是同步处理的缺点。
坦白说,直到 2009 年 Node.js 出现时,这种缺陷才变得明显或紧迫。Node.js 的创建者 Ryan Dahl 受到 C10K 问题的困扰,即为什么像 Apache 这样的流行 Web 服务器无法处理 10,000 个或更多的并发连接(给定典型的 Web 服务器硬件,内存将会耗尽)。他思考到 “查询数据库时,软件在做什么?”。
当然,答案是什么也没有发生。它正在等待数据库的响应。Ryan 认为网络服务器根本不应该去等待 I / O 活动。换言之,它应该切换为处理其他请求,并在较慢的活动完成时能得到通知。
越来越明显的是,基于异步事件的架构是解决多种并发问题的正确方法。也许这就是为什么 Python 的创建者 Guido 亲自为 Tulip 项目提供语言级别支持的原因,这个项目后来成为了 asyncio 模块。最终,Python 3.7 添加了新的关键字 async 和 await ,用于支持异步事件循环。这不仅对如何编写 Python 代码而且对代码执行也具有重大意义。
Python 的两个世界
虽然使用 Python 编写异步代码非常简单,在函数定义前添加 async 关键字,但是您必须非常小心,不要打破一个重要的规则:不要随意混合使用同步代码和异步代码。
这是因为同步代码可以阻止异步代码中的事件循环。这种情况会使您的应用程序陷入停顿。正如 Andrew Goodwin 所写:这会将您的代码分为两个世界——具有不同库和调用风格的“同步代码”和“异步代码”。
回到 WSGI,这意味着我们无法编写异步可调用对象并将其嵌入。WSGI 是为同步世界编写的。我们将需要一种新的机制来调用异步代码。但是,如果每个人都编写自己的一套机制,我们将回到最初的不兼容地狱。因此,对于异步代码,我们需要一个类似于 WSGI 的新标准。因此,ASGI 诞生了。
ASGI 还有其他一些目的。但是在此之前,让我们看一下两种类似的 Web 应用程序“Hello World”,它们分别采用 WSGI 和 ASGI 风格。
与 WSGI 一样,ASGI 可调用对象可以一个接一个地链接在一起,以处理 Web 请求(以及其他协议请求),即链式调用。实际上,ASGI 是 WSGI 的超集且可以调用 WSGI 可调用对象。ASGI 还支持长时间轮询,慢速流媒体和其他响应类型,而无需侧向加载,从而可以加快响应速度。
因此,ASGI 引入了构建异步 Web 界面和处理双向协议的新方式。客户端或服务器端都无需等待对方进行通信——这可以随时异步发生。现存且以同步代码编写的基于 WSGI 的 Web 框架将不支持这种事件驱动的工作方式。
Django 演变
同时,想将所有这些异步优势带给 Django 面临一个重大的问题 —— 所有 Django 代码都是以同步风格编写的。如果我们需要编写任何异步代码,那么需要一个采用异步风格编写的整个 Django 框架的副本。换句话说,创建两个 Django 世界。
好吧,不要惊慌——我们可能不必编写一个完整的副本,因为有聪明的方式可以在两个世界之间重用一些代码。正如领导 Django Async 项目的安德鲁·戈德温(Andrew Godwin)正确地指出的那样,“这是 Django 历史上最大规模的修订之一”。一个雄心勃勃的项目采用异步风格重新实现 ORM、请求处理程序(request handler)、模板渲染器(Template renderer)等组件。这将分阶段进行,并在多个版本中完成。如下是安德鲁的预想(这不视为已提交的时间表):
- Django 3.0-ASGI 服务器
- Django 3.1-异步视图
- Django 3.2 / 4.0-异步 ORM
您可能正在考虑其余的组件,例如模板渲染、表单和缓存等。它们可能仍然保持同步或者其异步实现会纳入到将来的技术路线中。但是以上是 Django 在异步世界中发展的关键里程碑。
asgi的使用
ASGI,是构建于WSGI接口规范之上的异步服务器网关接口,是WSGI的延伸和扩展。
A指的是Async,异步的意思。
协议,规范 | 支持的请求协议(常见,未列全) | 同步/异步 | 支持的框架 |
---|---|---|---|
CGI | HTTP | CGI程序 | |
WSGI | HTTP | 同步 | django3.0以前,Flask1.0 |
ASGI | HTTP,HTTP2,WebSocket等 | 同步/异步 | FastAPI,Quart,Sanic,Tornado,django3.0以后,flask2.0 |
在 Python3.5 之后增加 async/await 特性之后简化了协程操作以后,异步编程变得异常火爆,越来越多开发者投入异步的怀抱。
3.0版本以前,django所提供的所有内部功能都是基于同步编程的。所以,在以往django开发中,针对网络请求,数据库读取等IO操作形成的阻塞,往往会导致项目运行性能的下降。虽然等待I/O操作数微秒时,但是随着流量的增加和操作的频率上升,这一点点的阻塞就会导致整个项目运作的缓慢。而如果换成异步就不会有任何阻塞,还可以同时处理其他任务,从而以较低的延迟处理更多的请求。所以在目前python开发中,越来越多的框架开始支持了异步编程。所以,3.0版本以后,django开始支持异步编程,可以让开发者在django中使用python第三方异步模块,推出了asgi异步web服务器。3.1版本推出了异步视图,当然,目前django的异步编程还不够完善,django中只有极少的功能是支持了异步操作。
Uvicorn 是一个快速的 ASGI 服务器,Uvicorn 是基于 uvloop 和 httptools 构建的,是 Python 异步生态中重要的一员。
Uvicorn 当前支持 HTTP / 1.1 和 WebSockets,将来计划支持HTTP / 2。
安装uvicorn
pip install uvicorn
项目根目录下,运行django项目
# uvicorn 主应用目录名.asgi:application --reload
uvicorn homework.asgi:application --reload
开发中一般使用gunicorn来管理uvicorn。所以可以一并安装
pip install gunicorn
运行
# gunicorn -w 4 主应用目录名.asgi:application -k uvicorn.workers.UvicornWorker --reload
gunicorn -w 4 homework.asgi:application -k uvicorn.workers.UvicornWorker --reload