赞
踩
web通信流程
web我们这里指的就是通过浏览器去访问服务端,请求页面或者数据的通信方式,属于B/S架构。就是我们常见的网站。浏览器与服务端的通信流程:浏览器客户端发送一个请求信息(数据)发送到我们服务端,服务端接受这个请求信息进行验证,验证通过之后,构建一个页面(一组数据)响应给浏览器,浏览器拿到这个响应数据比如html页面,经过浏览器内核的渲染,最终呈现出美丽的界面。
什么是web框架?
什么是web框架?这就好比建设房子,房子主体钢结构等都为我们搭建好了,我们就是抹抹墙面,添加一些装饰,修饰一下即可。Django框架就是已经为我们搭建好的主题钢结构,剩下的根据不同的业务自定制即可。但是我们先不着急学习Django框架,今天我们先自己搭建一个web框架,自己搭建了web框架之后,你对Django框架就会比较好理解了。
自己搭建一个简易的web框架:
import socket server = socket.socket() server.bind(('127.0.0.1', 8888)) server.listen(3) conn, addr = server.accept() from_browser_data = conn.recv(1024) print(from_browser_data.decode('utf-8')) conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) conn.close() server.close()
注意:
浏览器发送请求信息时,一定带有满足http协议的请求数据。
GET / HTTP/1.1 Host: 127.0.0.1:8888 Connection: keep-alive Cache-Control: max-age=0 sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9
服务端给浏览器响应数据时,也必须满足http协议的相应数据:
conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8'))
基于B/S架构的web通信只有满足http协议规则的,才可以进行。所以,我们应该先研究http协议。
超文本传输协议(英文:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础,主要是应用于浏览器与服务端通信遵循的协议。
HTTP的发展是由蒂姆·伯纳斯-李于1989年在欧洲核子研究组织(CERN)所发起。HTTP的标准制定由万维网协会(World Wide Web Consortium,W3C)和互联网工程任务组(Internet Engineering Task Force,IETF)进行协调,最终发布了一系列的RFC,其中最著名的是1999年6月公布的 RFC 2616,定义了HTTP协议中现今广泛使用的一个版本——HTTP 1.1。
2014年12月,互联网工程任务组(IETF)的Hypertext Transfer Protocol Bis(httpbis)工作小组将HTTP/2标准提议递交至IESG进行讨论,于2015年2月17日被批准。 HTTP/2标准于2015年5月以RFC 7540正式发表,取代HTTP 1.1成为HTTP的实现标准。
HTTP是一个客户端终端(用户)和服务器端(网站)请求和应答的标准(TCP)。通过使用网页浏览器、网络爬虫或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认端口为80)。我们称这个客户端为用户代理程序(user agent)。应答的服务器上存储着一些资源,比如HTML文件和图像。我们称这个应答服务器为源服务器(origin server)。在用户代理和源服务器中间可能存在多个“中间层”,比如代理服务器、网关或者隧道(tunnel)。
尽管TCP/IP协议是互联网上最流行的应用,HTTP协议中,并没有规定必须使用它或它支持的层。事实上,HTTP可以在任何互联网协议上,或其他网络上实现。HTTP假定其下层协议提供可靠的传输。因此,任何能够提供这种保证的协议都可以被其使用。因此也就是其在TCP/IP协议族使用TCP作为其传输层。
通常,由HTTP客户端发起一个请求,创建一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如"HTTP/1.1 200 OK",以及返回的内容,如请求的文件、错误消息、或者其它信息。
HTTP协议定义Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。HTTP协议采用了请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。
以下是 HTTP 请求/响应的步骤:(重点)
客户端连接到Web服务器 建立链接。
一个HTTP客户端,通常是浏览器,与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。例如,https://www.cnblogs.com/jin-xin/。
发送HTTP请求
通过TCP套接字,客户端向Web服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据4部分组成。
服务器接受请求并返回HTTP响应
Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据4部分组成。
释放连接TCP连接
若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
客户端浏览器解析HTML内容
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。
浏览器给服务端发送的http请求信息包含四部分:请求行,请求头部,空行,请求数据。
我们修改一下我们的代码:
# 服务端 import socket server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(3) while 1: conn, addr = server.accept() while 1: try: from_browser_data = conn.recv(1024) print(from_browser_data.decode('utf-8')) conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) break except: break conn.close() server.close() # test.html: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <form action="" method="get"> # 可以切换一下post <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <input type="submit"> </form> </body> </html>
post请求和给请求的不同:
post请求: POST / HTTP/1.1 # 请求行 Host: 127.0.0.1:8080 # 请求头部 Connection: keep-alive Content-Length: 27 Cache-Control: max-age=0 sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 Origin: http://127.0.0.1:8080 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Referer: http://127.0.0.1:8080/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 # 空行 username=barry&password=123 # 请求数据 # get请求: GET /?username=minghang&password=123 HTTP/1.1 # 请求行 Host: 127.0.0.1:8080 # 请求头部 Connection: keep-alive sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Referer: http://127.0.0.1:8080/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 # 空行 # 请求数据
请求方法 URL 协议/版本号
常见的请求方法有post,get,delete put option head trace
HTTP/1.1协议中共定义了八种方法(也叫“动作”)来以不同方式操作指定的资源:
GET
向指定的资源发出“显示”请求。使用GET方法应该只用在读取数据,而不应当被用于产生“副作用”的操作中,例如在Web Application中。其中一个原因是GET可能会被网络蜘蛛等随意访问。获取资源
HEAD
与GET方法一样,都是向服务器发出指定资源的请求。只不过服务器将不传回资源的本文部分。它的好处在于,使用这个方法可以在不必传输全部内容的情况下,就可以获取其中“关于该资源的信息”(元信息或称元数据)。
POST
向指定资源提交数据,请求服务器进行处理(例如提交表单或者上传文件)。数据被包含在请求本文中。这个请求可能会创建新的资源或修改现有资源,或二者皆有。
PUT
向指定资源位置上传其最新内容。
DELETE
请求服务器删除Request-URI所标识的资源。
TRACE
回显服务器收到的请求,主要用于测试或诊断。
OPTIONS
这个方法可使服务器传回该资源所支持的所有HTTP请求方法。用’*'来代替资源名称,向Web服务器发送OPTIONS请求,可以测试服务器功能是否正常运作。
CONNECT
HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接(经由非加密的HTTP代理服务器)。
注意事项:
get请求与post请求的区别:
统一资源定位符
超文本传输协议(HTTP)的统一资源定位符将从因特网获取信息的五个基本元素包括在一个简单的地址中:
以http://www.luffycity.com:80/news/index.html?id=250&page=1 为例, 其中:
http,是协议;
www.luffycity.com,是服务器;
80,是服务器上的默认网络端口号,默认不显示;
/news/index.html,是路径(URI:直接定位到对应的资源);
?id=250&page=1,是查询。
大多数网页浏览器不要求用户输入网页中“http://”的部分,因为绝大多数网页内容是超文本传输协议文件。同样,“80”是超文本传输协议文件的常用端口号,因此一般也不必写明。一般来说用户只要键入统一资源定位符的一部分(www.luffycity.com:80/news/index.html?id=250&page=1)就可以了。
由于超文本传输协议允许服务器将浏览器重定向到另一个网页地址,因此许多服务器允许用户省略网页地址中的部分,比如 www。从技术上来说这样省略后的网页地址实际上是一个不同的网页地址,浏览器本身无法决定这个新地址是否通,服务器必须完成重定向的任务。
Url: 统一资源定位符:协议、服务器、端口、路径
http:\\ 127.0.0.1:8000\article\d1.html
路径:article\d1.html
http或者是https协议,版本号一般为1.1.
请求信息中,有很多重要的键值对封装到请求头部中:
Host: 127.0.0.1:8080 Connection: keep-alive # keep-alive值,浏览器与服务端的链接会保存一段时间一般为2s,2s内如果没有再次请求,断开连接;如果connection: close。一次请求与响应之后,直接断开连接。 Content-Length: 27 Cache-Control: max-age=0 sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 Origin: http://127.0.0.1:8080 Content-Type: application/x-www-form-urlencoded # 请求类型 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 # UA代理,告知服务端这个请求信息的来源 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Referer: http://127.0.0.1:8080/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9
空行是隔开请求头部与请求数据的作用/r/n
。
浏览器客户端向服务端发送请求时,按照需求会携带数据,post的请求方式,数据会保存在请求数据中,一般是这种形式:
name=barry&age=18&sex=1
响应信息由四部分组成:状态行,响应头部,空行,响应数据。
协议/版本号 状态码 状态描述
请求使用什么协议,响应就对应使用什么协议,一般http或者是https协议,版本号一般为1.1.
所有HTTP响应的第一行都是状态行,依次是当前HTTP版本号,3位数字组成的状态代码,以及描述状态的短语,彼此由空格分隔。
状态代码的第一个数字代表当前响应的类型:
虽然 RFC 2616 中已经推荐了描述状态的短语,例如"200 OK",“404 Not Found”,但是WEB开发者仍然能够自行决定采用何种短语,用以显示本地化的状态描述或者自定义信息。
对状态码进行一个文字描述,更加清新的告知你响应状态的内容。
响应信息的一些重要的键值对数据:
空行是隔开响应头部与响应数据的作用/r/n
。
一般是html页面数据,或者一些json形式的数据。
基于 请求-响应 的模式
HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并 返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有 接收到请求之前不会发送响应
无状态保存
HTTP是一种不保存状态,即无状态(stateless)协议。HTTP协议 自身不对请求和响应之间的通信状态进行保存。也就是说在HTTP这个级别,协议对于发送过的请求或响应都不做持久化处理。
使用HTTP协议,每当有新的请求发送时,就会有对应的新响应产 生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把HTTP协议设计成 如此简单的。可是,随着Web的不断发展,因无状态而导致业务处理变得棘手 的情况增多了。比如,用户登录到一家购物网站,即使他跳转到该站的 其他页面后,也需要能继续保持登录状态。针对这个实例,网站为了能 够掌握是谁送出的请求,需要保存用户的状态。HTTP/1.1虽然是无状态协议,但为了实现期望的保持状态功能, 于是引入了Cookie技术。有了Cookie再用HTTP协议通信,就可以管 理状态了。有关Cookie的详细内容稍后讲解。
无连接
无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间,并且可以提高并发性能,不能和每个用户建立长久的连接,请求一次相应一次,服务端和客户端就中断了。但是无连接有两种方式,早期的http协议是一个请求一个响应之后,直接就断开了,但是现在的http协议1.1版本不是直接就断开了,而是等几秒钟,这几秒钟是等什么呢,等着用户有后续的操作,如果用户在这几秒钟之内有新的请求,那么还是通过之前的连接通道来收发消息,如果过了这几秒钟用户没有发送新的请求,那么就会断开连接,这样可以提高效率,减少短时间内建立连接的次数,因为建立连接也是耗时的,默认的好像是3秒中现在,但是这个时间是可以通过咱们后端的代码来调整的,自己网站根据自己网站用户的行为来分析统计出一个最优的等待时间。
后端:
import socket server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(3) conn, addr = server.accept() from_browser_data = conn.recv(1024) print(from_browser_data.decode('utf-8')) conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) conn.close() server.close()
html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> <style> div { font-size: 30px; color: red; } </style> <script> alert('未满18岁禁止入内') </script> </head> <body> <div>欢迎来到 xxx皇家赌场 美女在线发牌</div> <ul> <li>虫子</li> <li>菜籽</li> <li>涛子</li> <li>珍子</li> </ul> </body> </html>
虽然我们的html里面有css,js代码,但是真正场景中,css,js应该分别封装到不同的文件里面。
将css,js分别写入不同的文件中,通过引入的方式进行加载。
我们再次运行,只是加载了html文件,没有加载出js,css功能,为什么?我们的后端接收一次请求,返回给浏览器一个html就结束了,浏览器接收到html页面,当读取到link标签时,link标签会发送异步请求,请求后端,请求css文件,但是此时服务端已经关闭了。同理,加载script标签时,会发送异步请求,请求js文件,服务端也已经关闭了。我们要让服务端一直运行。
import socket server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(3) while 1: conn, addr = server.accept() from_browser_data = conn.recv(1024) print(from_browser_data.decode('utf-8')) conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) conn.close() server.close()
浏览器再次请求,请求信息如下:
GET / HTTP/1.1 Host: 127.0.0.1:8080 Connection: keep-alive Cache-Control: max-age=0 sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 GET /test.css HTTP/1.1 Host: 127.0.0.1:8080 Connection: keep-alive sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108" sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 sec-ch-ua-platform: "macOS" Accept: text/css,*/*;q=0.1 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: no-cors Sec-Fetch-Dest: style Referer: http://127.0.0.1:8080/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 GET /test.js HTTP/1.1 Host: 127.0.0.1:8080 Connection: keep-alive sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108" sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 sec-ch-ua-platform: "macOS" Accept: */* Sec-Fetch-Site: same-origin Sec-Fetch-Mode: no-cors Sec-Fetch-Dest: script Referer: http://127.0.0.1:8080/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9
浏览器发送了三次请求,分别请求html文件,css文件,js文件,但是我们的后端返回给浏览器三次响应全部都是html文件。
socket服务端:
import socket server = socket.socket() server.bind(('127.0.0.1', 8088)) server.listen(3) while 1: conn, addr = server.accept() from_browser_data = conn.recv(1024) # print(from_browser_data.decode('utf-8')) request_path = from_browser_data.decode('utf-8').split('\n')[0].split()[1] # print(request_path) conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) print(request_path) if request_path.strip() == '/': with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) elif request_path.strip() == '/test.css': with open('test.css', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) elif request_path.strip() == '/test.js': with open('test.js', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) conn.close() server.close()
后端:
import socket server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(3) conn, addr = server.accept() from_browser_data = conn.recv(1024) print(from_browser_data.decode('utf-8')) conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) conn.close() server.close()
html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> <style> div { font-size: 30px; color: red; } </style> <script> alert('未满18岁禁止入内') </script> </head> <body> <div>欢迎来到 xxx皇家赌场 美女在线发牌</div> <ul> <li>虫子</li> <li>菜籽</li> <li>涛子</li> <li>珍子</li> </ul> </body> </html>
虽然我们的html里面有css,js代码,但是真正场景中,css,js应该分别封装到不同的文件里面。
将css,js分别写入不同的文件中,通过引入的方式进行加载。
我们再次运行,只是加载了html文件,没有加载出js,css功能,为什么?我们的后端接收一次请求,返回给浏览器一个html就结束了,浏览器接收到html页面,当读取到link标签时,link标签会发送异步请求,请求后端,请求css文件,但是此时服务端已经关闭了。同理,加载script标签时,会发送异步请求,请求js文件,服务端也已经关闭了。我们要让服务端一直运行。
import socket server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(3) while 1: conn, addr = server.accept() from_browser_data = conn.recv(1024) print(from_browser_data.decode('utf-8')) conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) conn.close() server.close()
浏览器再次请求,请求信息如下:
GET / HTTP/1.1 Host: 127.0.0.1:8080 Connection: keep-alive Cache-Control: max-age=0 sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 GET /test.css HTTP/1.1 Host: 127.0.0.1:8080 Connection: keep-alive sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108" sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 sec-ch-ua-platform: "macOS" Accept: text/css,*/*;q=0.1 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: no-cors Sec-Fetch-Dest: style Referer: http://127.0.0.1:8080/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 GET /test.js HTTP/1.1 Host: 127.0.0.1:8080 Connection: keep-alive sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108" sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 sec-ch-ua-platform: "macOS" Accept: */* Sec-Fetch-Site: same-origin Sec-Fetch-Mode: no-cors Sec-Fetch-Dest: script Referer: http://127.0.0.1:8080/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9
浏览器发送了三次请求,分别请求html文件,css文件,js文件,但是我们的后端返回给浏览器三次响应全部都是html文件。
socket服务端:
import socket server = socket.socket() server.bind(('127.0.0.1', 8088)) server.listen(3) while 1: conn, addr = server.accept() from_browser_data = conn.recv(1024) # print(from_browser_data.decode('utf-8')) request_path = from_browser_data.decode('utf-8').split('\n')[0].split()[1] # print(request_path) conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) print(request_path) if request_path.strip() == '/': with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) elif request_path.strip() == '/test.css': with open('test.css', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) elif request_path.strip() == '/test.js': with open('test.js', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) conn.close() server.close()
Html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> <link rel="stylesheet" href="test.css"> <script src="test.js"></script> </head> <body> <div>欢迎来到 xxx皇家赌场 美女在线发牌</div> <ul> <li>虫子</li> <li>菜籽</li> <li>涛子</li> <li>珍子</li> </ul> <img src="mn.png" alt=""> </body> </html>
由于上面的版本使用了很多if 条件比较low,我们使用函数进行优化。
py文件:
import socket server = socket.socket() server.bind(('127.0.0.1', 5000)) server.listen(3) def html(conn): with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) def css(conn): with open('test.css', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) def js(conn): with open('test.js', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) def img(conn): with open('mn.png', mode='rb') as f1: content = f1.read() conn.send(content) urlpatterns = [ ('/', html), ('/test.css', css), ('/test.js', js), ('/mn.png', img), ] while 1: conn, addr = server.accept() from_browser_data = conn.recv(1024) # print(from_browser_data.decode('utf-8')) request_path = from_browser_data.decode('utf-8').split('\n')[0].split()[1] conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) # print(request_path) for item in urlpatterns: if request_path == item[0]: item[1](conn) conn.close() server.close()
由于上面的版本都是串行,效率低,我们应该优化成并发版本。我们引入多线程
import socket from threading import Thread server = socket.socket() server.bind(('127.0.0.1', 5001)) server.listen(3) def html(conn): with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) def css(conn): with open('test.css', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) def js(conn): with open('test.js', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) def img(conn): with open('mn.png', mode='rb') as f1: content = f1.read() conn.send(content) urlpatterns = [ ('/', html), ('/test.css', css), ('/test.js', js), ('/mn.png', img), ] while 1: conn, addr = server.accept() from_browser_data = conn.recv(1024) # print(from_browser_data.decode('utf-8')) request_path = from_browser_data.decode('utf-8').split('\n')[0].split()[1] conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) # print(request_path) for item in urlpatterns: if request_path == item[0]: t1 = Thread(target=item[1], args=(conn,)) t1.start() conn.close() server.close()
其他文件不变。
目前为止,我们的html文件里面的所有的数据,都是静态数据,我们要将html页面的数据变成动态的,就是要掺杂一些后端的数据到html页面中。
静态html:html页面所有的数据都是创建html之初,写死的数据。
动态html:html页面的部分(所有的)数据是从后端获取到并渲染到html页面中,然后在将html返给浏览器。
html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> <link rel="stylesheet" href="test.css"> <script src="test.js"></script> </head> <body> <div>欢迎来到 xxx皇家赌场 美女在线发牌</div> <div>{time}</div> <ul> <li>虫子</li> <li>菜籽</li> <li>涛子</li> <li>珍子</li> </ul> <img src="mn.png" alt=""> </body> </html>
py文件:
import socket from threading import Thread from datetime import datetime server = socket.socket() server.bind(('127.0.0.1', 5004)) server.listen(3) def html(conn): time_now = datetime.now() with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() # print(content) content = content.format(time=time_now) conn.send(content.encode('utf-8')) def css(conn): with open('test.css', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) def js(conn): with open('test.js', encoding='utf-8', mode='r') as f1: content = f1.read() conn.send(content.encode('utf-8')) def img(conn): with open('mn.png', mode='rb') as f1: content = f1.read() conn.send(content) urlpatterns = [ ('/', html), ('/test.css', css), ('/test.js', js), ('/mn.png', img), ] while 1: conn, addr = server.accept() from_browser_data = conn.recv(1024) # print(from_browser_data.decode('utf-8')) request_path = from_browser_data.decode('utf-8').split('\n')[0].split()[1] conn.send('HTTP/1.1 200 ok\r\n\r\n'.encode('utf-8')) # print(request_path) for item in urlpatterns: if request_path == item[0]: t1 = Thread(target=item[1], args=(conn,)) t1.start() conn.close() server.close()
上一个版本有两个问题:
后端的项目按照整体的功能性划分是分为两部分:
服务器程序。
对请求请求信息的分析,加工,获取请求信息中的重要参数,对响应信息的分析,加工等操作,还有socket通信等等。
业务逻辑的程序。
根据浏览器访问不同的url,构建不同的代码,产生不同的数据。
服务器程序负责对socket服务器进行封装,并在请求到来时,对请求的各种数据进行整理。
应用程序则负责具体的逻辑处理。为了方便应用程序的开发,就出现了众多的Web框架,例如:Django、Flask、web.py 等。不同的框架有不同的开发方式,但是无论如何,开发出的应用程序都要和服务器程序配合,才能为用户提供服务。
这样,服务器程序就需要为不同的框架提供不同的支持。这样混乱的局面无论对于服务器还是框架,都是不好的。对服务器来说,需要支持各种不同框架,对框架来说,只有支持它的服务器才能被开发出的应用使用。最简单的Web应用就是先把HTML用文件保存好,用一个现成的HTTP服务器软件,接收用户请求,从文件中读取HTML,返回。如果要动态生成HTML,就需要把上述步骤自己来实现。不过,接受HTTP请求、解析HTTP请求、发送HTTP响应都是苦力活,如果我们自己来写这些底层代码,还没开始写动态HTML呢,就得花个把月去读HTTP规范。
正确的做法是底层代码由专门的服务器软件实现,我们用Python专注于生成HTML文档。因为我们不希望接触到TCP连接、HTTP原始请求和响应格式,所以,需要一个统一的接口协议来实现这样的服务器软件,让我们专心用Python编写Web业务。
这时候,标准化就变得尤为重要。我们可以设立一个标准,只要服务器程序支持这个标准,框架也支持这个标准,那么他们就可以配合使用。一旦标准确定,双方各自实现。这样,服务器可以支持更多支持标准的框架,框架也可以使用更多支持标准的服务器。
WSGI(Web Server Gateway Interface)就是一种规范标准,它定义了使用Python编写的web应用程序与web服务器程序之间的接口格式,实现web应用程序与web服务器程序间的解耦。
常用的WSGI服务器有uwsgi、Gunicorn。而Python标准库提供的独立WSGI服务器叫wsgiref,Django开发环境用的就是这个模块来做服务器。
WSGI是一个协议,也是标准,规定了web业务逻辑程序部分与服务器程序部分通信的接口格式。
uwsgi、Gunicorn、wsgiref遵循WSGI的标准制定的插件,他们封装好了服务器端的逻辑的程序。我们就可以直接引用,将我们的主要精力放在处理项目的业务逻辑程序部分。
接下来我们先看一看wsgiref的简单用法:
from wsgiref.simple_server import make_server # wsgiref本身就是个web框架,提供了一些固定的功能(请求和响应信息的封装,不需要我们自己写原生的socket了也不需要咱们自己来完成请求信息的提取了,提取起来很方便) #函数名字随便起 def application(environ, start_response): ''' :param environ: 是全部加工好的请求信息,加工成了一个字典,通过字典取值的方式就能拿到很多你想要拿到的信息 :param start_response: 帮你封装响应信息的(响应行和响应头),注意下面的参数 :return: ''' start_response('200 OK', [('k1','v1'),]) print(environ) print(environ['PATH_INFO']) #输入地址127.0.0.1:8000,这个打印的是'/',输入的是127.0.0.1:8000/index,打印结果是'/index' return [b'<h1>Hello, web!</h1>'] #和咱们学的socketserver那个模块很像啊 httpd = make_server('127.0.0.1', 8080, application) print('Serving HTTP on port 8080...') # 开始监听HTTP请求: httpd.serve_forever()
py文件:
from datetime import datetime from wsgiref.simple_server import make_server import getdata def html(): time_now = datetime.now() all = getdata.get_data() with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() # print(content) content = content.format(time=time_now,data=all) return content.encode('utf-8') def css(): with open('test.css', encoding='utf-8', mode='r') as f1: content = f1.read() return content.encode('utf-8') def js(): with open('test.js', encoding='utf-8', mode='r') as f1: content = f1.read() return content.encode('utf-8') def img(): with open('mn.png', mode='rb') as f1: content = f1.read() return content def ico(): with open('ico.png', mode='rb') as f1: content = f1.read() return content urlpatterns = [ ('/', html), ('/test.css', css), ('/test.js', js), ('/mn.png', img), ('/favicon.ico', ico), ] def application(environ, start_response): ''' :param environ: 是全部加工好的请求信息,加工成了一个字典,通过字典取值的方式就能拿到很多你想要拿到的信息 :param start_response: 帮你封装响应信息的(响应行和响应头),注意下面的参数 :return: ''' start_response('200 OK', [('k1', 'v1'), ]) # print(environ) request_path = environ['PATH_INFO'] data = b'' for item in urlpatterns: if request_path == item[0]: data = item[1]() return [data] # 和咱们学的socketserver那个模块很像啊 httpd = make_server('127.0.0.1', 8080, application) print('Serving HTTP on port 8080...') # 开始监听HTTP请求: httpd.serve_forever()
gatdata:
先要创建数据库,创建对应的userinfo表,插入数据:
import pymysql def get_data(): conn = pymysql.connect( host='127.0.0.1', port=3306, user='root', password='123', database='db2', charset='utf8' ) cursor = conn.cursor(pymysql.cursors.DictCursor) sql = "select * from userinfo;" cursor.execute(sql) all_data = cursor.fetchall() # print(all_data) conn.close() return all_data
html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> <link rel="stylesheet" href="test.css"> <script src="test.js"></script> </head> <body> <div>欢迎来到 xxx皇家赌场 美女在线发牌</div> <div>{time}</div> <div>{data}</div> <ul> <li>虫子</li> <li>菜籽</li> <li>涛子</li> <li>珍子</li> </ul> <img src="mn.png" alt=""> </body> </html>
我们上面将后端数据渲染到html页面中,使用的格式化输出,格式化输出只能替换数据,没有其他的功能,DJango中有专门的模板渲染功能,但是这个功能不能脱离DJango环境使用,我们用一个独立类似于Django模版渲染的功能的模块:jinja2模版渲染模块。
pip install jinja2
Py:
from datetime import datetime from wsgiref.simple_server import make_server import getdata from jinja2 import Template def html(): time_now = datetime.now() all = getdata.get_data() with open('test.html', encoding='utf-8', mode='r') as f1: content = f1.read() tem = Template(content) # 创建template对象,将content html页面数据加载到对象 render_data = tem.render({'time_now': time_now, 'userinfo': all}) # 通过对象调用render方法将数据替换到html页面数据最终返回一个渲染好的html页面数据 return render_data.encode('utf-8') def css(): with open('test.css', encoding='utf-8', mode='r') as f1: content = f1.read() return content.encode('utf-8') def js(): with open('test.js', encoding='utf-8', mode='r') as f1: content = f1.read() return content.encode('utf-8') def img(): with open('mn.png', mode='rb') as f1: content = f1.read() return content def ico(): with open('ico.png', mode='rb') as f1: content = f1.read() return content urlpatterns = [ ('/', html), ('/test.css', css), ('/test.js', js), ('/mn.png', img), ('/favicon.ico', ico), ] def application(environ, start_response): ''' :param environ: 是全部加工好的请求信息,加工成了一个字典,通过字典取值的方式就能拿到很多你想要拿到的信息 :param start_response: 帮你封装响应信息的(响应行和响应头),注意下面的参数 :return: ''' start_response('200 OK', [('k1', 'v1'), ]) # print(environ) request_path = environ['PATH_INFO'] data = b'' for item in urlpatterns: if request_path == item[0]: data = item[1]() return [data] # 和咱们学的socketserver那个模块很像啊 httpd = make_server('127.0.0.1', 8080, application) print('Serving HTTP on port 8080...') # 开始监听HTTP请求: httpd.serve_forever()
html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> <link rel="stylesheet" href="test.css"> <script src="test.js"></script> </head> <body> <div>欢迎来到 xxx皇家赌场 美女在线发牌</div> <div>{{time_now}}</div> <div>{{userinfo}}</div> <ul> {% for item in userinfo %} <li>{{item.name}}--->{{item.age}}</li> {% endfor%} </ul> <img src="mn.png" alt=""> </body> </html>
我们使用jinja2模版渲染不仅仅可以进行数据渲染,还可以加入for循环,if判断,等等各种方法。
jinja2作用:将后端的数据渲染到html字符串数据中,形成最终的html页面数据,返回给浏览器。
目前我们的目录结构很乱,都在统一目录下,我们框架肯定需要分目录开发,所有我们要仿照Django框架的目录结构,进行优化:
static静态文件
静态文件存放的就是css、js、jQuery等相关的文件,因为这些文件可以分类放置并且每种都可以有很多文件。
urls:路由分发
我们代码中有一个列表,根据不同的路径执行不同的函数,这个列表就称为路由分发,所以要单独设立一个文件。
template:html文件
template文件夹就是专门放置html文件的,你们以后的web项目html文件会非常多,所以必须用单独的一个文件夹存放。
views文件:专门放置应用程序的代码,处理业务逻辑。
manage文件:专门放置服务器程序的代码,处理http协议,socket等。启动文件
models文件:文件名自己定义,主要方式处理数据库相关逻辑。
至此,我们自己搭建的起飞版的web框架就算完成了,这个就是所有web框架的雏形,只要你把这个框架研究明白,那么接下来你无论研究什么框架,都易如反掌了。
Web服务器开发领域里著名的MVC模式,所谓MVC就是把Web应用分为模型(M),控制器©和视图(V)三层,他们之间以一种插件式的、松耦合的方式连接在一起,模型负责业务对象与数据库的映射(ORM),视图负责与用户的交互(页面),控制器接受用户的输入调用模型和视图完成用户的请求,其示意图如下所示:
Django的MTV模式本质上和MVC是一样的,也是为了各组件间保持松耦合关系,只是定义上有些许不同,Django的MTV分别是值:
除了以上三层之外,还需要一个URL分发器,它的作用是将一个个URL的页面请求分发给不同的View处理,View再调用相应的Model和Template,MTV的响应模式如下所示:
一般是用户通过浏览器向我们的服务器发起一个请求(request),这个请求回去访问视图函数,(如果不涉及到数据调用,那么这个时候视图函数返回一个模板也就是一个网页给用户),视图函数调用模型,模型去数据库查找数据,然后逐级返回,视图函数把返回的数据填充到模板中空格中,最后返回网页给用户。
Django框架是python中最大而全的框架,使用DJango框架自带了很多插件与功能,构建后端项目时,极大的提升效率,一般中大型项目需要功能比较全面的项目我们使用Django,由于只要你使用DJango框架他就会自带很多插件无论你用不用都会下载安装到本地,所以相对比较笨重,各个插件的耦合性比较强,相对不灵活。
pip3 install django==2.2.14
我们单独创建一个目录,便于我们学习django框架基础使用。
# 创建Django项目
(luffyenv) taibaijingdeMBP:django_learn barry$ django-admin startproject first_pro
补充:
项目后端分为两大部分:
服务器程序部分
应用业务逻辑部分
我们查看一下目录结构:没有应用业务逻辑部分
first_pro
| -- frist_pro # 项目的服务器的程序部分
| -- __init__.py
| -- settings.py # 项目的总配置文件
| -- urls.py # 项目的总路由分发
| -- wsgi.py # 处理的是项目的服务器的接口逻辑核心文件
| -- manage.py # 项目的启动文件(加载Django环境)
坑:
当你通过上面的命令创建一个项目时,你可能会遇到这样的错误:django-admin不是内部或者外部命令,这样是报错了,为什么? 为什么你输入python 输入pip都不回报错? python pip都是一个可执行程序,他的路径肯定是在环境变量中,所以我们要将django-admin.exe路径配置到环境变量中:
如果你在上面路径找不到这个django-admin.exe,那么就通过下面的方式寻找:
通过pip安装的大部分模块一般都在lib>site-packages里面,在找到django-admin.exe以及django-admin.py两个文件,
当前目录下会生成mysite的工程,目录结构如下:(大家注意昂,pip下载下来的django你就理解成一个模块,而不是django项目,这个模块可以帮我们创建django项目)
命令启动项目测试:
python manage.py runserver 127.0.0.1:8000
你会发现,上面没有什么view视图函数的文件啊,这里我们说一个应用与项目的关系,上面我们只是创建了一个项目,并没有创建应用,以微信来举例,微信是不是一个大的项目,但是微信里面是不是有很多个应用,支付应用、聊天应用、朋友圈、小程序等这些在一定程度上都是相互独立的应用,也就是说一个大的项目里面可以有多个应用,也就是说项目是包含应用的,它没有将view放到这个项目目录里面是因为它觉得,一个项目里面可以有多个应用,而每个应用都有自己这个应用的逻辑内容,所以他觉得这个view应该放到应用里面,比如说我们的微信,刚才说了几个应用,这几个应用的逻辑能放到一起吗,放到一起是不是就乱套啦,也不好管理和维护,所以这些应用的逻辑都分开来放,它就帮我们提炼出来了,提炼出来一个叫做应用的东西,所以我们需要来创建这个应用。
通过django命令创建的项目:他认为你的项目我已经给你创建好包含(服务器相关应用以及url等)。但是你的应用逻辑需要自己完成。也就是说咱们的项目中必须创建应用。通过manage.py 可以创建应用。
创建应用的命令:
python manage.py startapp 应用名
创建应用:
(luffyenv) taibaijingdeMBP:first_pro barry$ python manage.py startapp app01
分析目录结构:
first_pro | -- app01 # 创建的应用 | -- __init__.py | -- migrations # 存放通过python命令创建的数据库中表的临时脚本文件 | -- __init__.py | -- admin.py # 配置django自带的项目后台系统 admin | -- apps.py # 有关此应用的注册配置信息 | -- models.py # 数据模型,与数据库相关 | -- views.py # 视图函数 处理我们的应用逻辑的函数 | -- tests.py # 用于测试python代码的脚本 | -- frist_pro # 项目的服务器的程序部分 | -- __init__.py | -- settings.py # 项目的总配置文件 | -- urls.py # 项目的总路由分发 | -- wsgi.py # 处理的是项目的服务器的接口逻辑核心文件 | -- manage.py # 项目的启动文件(加载Django环境)
pycharm帮我们创建了一个templates目录
编辑启动脚本文件:
# urls.py from django.contrib import admin from django.urls import path,re_path from app01 import views urlpatterns = [ re_path('index', views.index), ] # views.py def index(request): # 等同于environ参数 ,request包含所有的请求信息 return render(request, 'index.html') # templates/index.html: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <h1>你好,世界!</h1> </body> </html>
登录验证整体是两次请求,第一次请求请求页面数据,我们用get请求,第二次请求是提交数据,一般我们用post请求。
我们通过post提交数据,django默认会阻拦,csrf跨站请求伪造的防护,我们后面会讲如何正规的处理,现在我们先将这个功能注销。
# settings.py:
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',
]
具体代码:
# urls.py: from django.urls import path,re_path from app01 import views urlpatterns = [ # path('admin/', admin.site.urls), re_path('index', views.index), re_path('login', views.login), ] # views.py: def login(request): # print(request.method) 获取请求方法 if request.method == 'GET': return render(request, 'login.html') elif request.method == 'POST': # 获取提交的数据,验证数据真实性 # print(request.POST) # <QueryDict: {'username': ['barry'], 'password': ['123']}> username = request.POST.get('username') # 获取post请求提交的数据 password = request.POST.get('password') if username == 'barry' and password == '123': return HttpResponse('登录成功') else: return HttpResponse('登录失败') # template/login.html: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <form action="" method="post"> <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <input type="submit"> </form> </body> </html>
相关命令
django-admin startproject 项目名 创建项目
python manage.py startapp 应用名 创建应用
python manage.py runserver IP:端口 启动项目
登录流程
浏览器先发送get请求,请求登录页面,(urls—> views.py —> template)返回给浏览器登录页面;输入用户名密码之后,点击登录,发送post请求,提交到后台(urls—> views.py [获取数据进行判断])最终返回给浏览器一个字符串形式的数据。
url路由分发系统,根据浏览器发送的请求的url,从上至下轮训的匹配对应的url,匹配成功之后,执行对应的views视图函数。
Django 1.x版本 from django.conf.urls import url #循环urlpatterns,找到对应的函数执行,匹配上一个路径就找到对应的函数执行,就不再往下循环了,并给函数传一个参数request,和wsgiref的environ类似,就是请求信息的所有内容 urlpatterns = [ url(正则表达式, views视图函数,参数,别名), ] Django 2.x版本 from django.contrib import admin from django.urls import path,re_path from app01 import views urlpatterns = [ # path('admin/', admin.site.urls), re_path('index', views.index), re_path('login', views.login), ]
前端给后端传递数据:
假如我们做了图书管理系统,图书管理系统是这样设置的,首页给我们展示满世界排名靠前面的书,当你想看那一年的书,你的url就应该拼接上哪一年,并且将此年份传递给后端逻辑,也就是对应的views函数中。比如你的首页为127.0.0.1:8000/book/,当你想看2003年有哪些书时,你访问的url就对应为127.0.0.1:8000/book/2003/,这也是前后端间接传递数据,那么这种需求如何完成呢?我们先写一个views函数,与对应的html。
匹配原则,注意两点:
urlpatterns = [
# path('admin/', admin.site.urls),
# re_path('by/(\d{4})', views.book_year),
# 匹配以127.0.0.1:8000/by/四位数字 为开头
# http://127.0.0.1:8000/by/2010/12
re_path('^by/(\d{4})$', views.book_year),
# 完全匹配:127.0.0.1:8000/by/四位数字
re_path('by/(\d{4})/(\d{2})', views.book_year_mouth),
]
具体代码:
# urls.py urlpatterns = [ re_path('^by/(\d{4})/$', views.book_year), # 完全匹配:127.0.0.1:8000/by/四位数字 re_path('^by/(\d{4})/(\d{2})/$', views.book_year_mouth), re_path('^by/(\d{4})/(\d{2})/(\d{2})/$', views.book_year_mouth_day), ] # views.py: def book_year(request, year): print(year, type(year)) return HttpResponse(f'查询书籍的年份{year}') def book_year_mouth(request, year, mouth): print(year, mouth) return HttpResponse(f'查询书籍的年份{year},月份{mouth}') def book_year_mouth_day(request, year, mouth, day): print(year, mouth,day) return HttpResponse(f'查询书籍的年份{year},月份{mouth},{day}号')
通过url传递数据时,给具体的匹配的数据起名字,对应的views视图函数的形参必须设置为这个名字。
re_path('^by/(?P<ye>\d{4})/$', views.book_year),
def book_year(request, ye):
print(ye, type(ye))
return HttpResponse(f'查询书籍的年份{ye}')
在有名分组这里,我们还可以设置默认值参数
re_path('^by/$', views.book_year),
re_path('^by/(?P<page>\d+)$', views.book_year),
def book_year(request, page='10'):
print(page)
return HttpResponse(f'查询书籍的页数{page}')
# http://127.0.0.1:8000/by/ 匹配的是上面的一个url,由于你没有传递page具体的数值,沿用的就是默认值 '10'
# http://127.0.0.1:8000/by/20/ 匹配的是下面的一个url,我们传递page具体的数值,使用传递的参数值 '20'
# 是否开启URL访问地址后面没有/跳转至带有/的路径的配置项
APPEND_SLASH=True
Django settings.py配置文件中默认没有 APPEND_SLASH 这个参数,但 Django 默认这个参数为 APPEND_SLASH = True。 其作用就是自动在网址结尾加’/'。其效果就是:我们定义了urls.py:
from django.conf.urls import url
from app01 import views
urlpatterns = [
url(r'^blog/$', views.blog),
]
访问 http://www.example.com/blog 时,默认将网址自动转换为 http://www.example/com/blog/ 。
如果在settings.py中设置了 APPEND_SLASH=False,此时我们再请求 http://www.example.com/blog 时就会提示找不到页面。
如果,我们在项目已经开发完毕了,但是遇到了某些需求,必须要给视图函数传递一些额外的参数,我们可以通过传递额外参数来补救。
re_path('index', views.index, {'name': 'barry'}),
def index(request,name):
print(name)
return HttpResponse('ok')
我们可以给url起一个别名,作用就是我们在视图函数中,通过reverse方法就可以借助于别名反向解析出对应的路径。
不带参数的url反向解析
re_path('home/index/base', views.index, name='base'),
from django.urls import reverse
def index(request):
# 想要获取home/index/base
# path = 'home/index/base'
path = reverse('base') # 通过这个reverse方法借助于别名可以反向解析出对应的路径
print(path) # /home/index/base
return HttpResponse('ok')
带参数的url反向解析
re_path('home/index/(\d{4})', views.index, name='index'),
def index(request, year):
path2 = reverse('index', args=('2003',))
print(path2)
return HttpResponse('ok')
一个项目中可能有多个应用,微信有漂流瓶,附近的人等等,上面只是讲了一个应用,如果有多个应用你的url如何处理呢?此时pycharm已经没有办法创建了,你需要自己加app。只能手动加上。
我们现在是一个总urls.py,这个是项目总路由,后期我们的项目都会有多个应用,include就是我们在每个子应用里面都创建一个子urls.py,通过总urls分发到不同的子应用下面的urls,子urls在将路径分发到对应的应用中的views视图函数。
我们需要创建两个应用来测试,而通过pycharm创建项目,只能自带一个应用,如果我们想在创建另一个应用,必须通过命令。
我们利用pycharm创建一个urllist项目。
我们再次通过命令创建第二个应用:
(luffyenv) taibaijingdeMBP:urllist barry$ ls
manage.py templates urllist users
(luffyenv) taibaijingdeMBP:urllist barry$ python manage.py startapp home
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qVrMKm0j-1675339555980)(F:\newschool\笔记总结\Django笔记总结\Django笔记总结.assets\image-20221228110255149.png)]
由于我们通过pycharm创建项目时,自带了一个users应用,所以pycharm帮我们将users应用配置到了urllist项目中,具体体现:
# settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'users.apps.UsersConfig', # 将users应用配置到项目中
]
而我们home应用是通过命令创建的,我们需要手动配置到项目中:
# settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'users.apps.UsersConfig',
'home.apps.HomeConfig', # 正规写法
# 'home', 简写方式
]
我们在每个应用中,创建应用的子urls:
# users应用以及home应用下面的urls:
from django.contrib import admin
from django.urls import path, re_path
urlpatterns = [
# ......
]
我们在每个应用下,创建一个对应的视图函数,然后匹配urls:
# home应用下: urlpatterns = [ re_path('index', views.home), ] def home(requests): return render(requests, 'home.html') <h1>欢迎来到home应用7的页面</h1> # users应用下: from users import views urlpatterns = [ re_path('index', views.users), ] def users(requests): return render(requests, 'users.html') <h1>欢迎来到users应用的页面</h1>
我们所有的请求到了,一定是先进行总路由,然后由总路由分发到不同的子路由的urls:
from django.contrib import admin
from django.urls import path, include,re_path
urlpatterns = [
# path('admin/', admin.site.urls),
# 127.0.0.1:8000/home/index
re_path('home/', include('home.urls')),
re_path('users/', include('users.urls')),
]
我们一般做网站,都会有个首页,首页对应的就是ip地址与端口,其实就是没有路径/。比如京东:
首页上有各种的a标签,我们通过点击a标签,进行跳转页面,在访问不同应用下面的视图函数。那么我们如何实现这个功能?我们首页的urls以及对应的视图函数写在哪里?
总urls:
from django.urls import path, include,re_path
from home import views # 借助home应用下的视图完成首页的功能。
urlpatterns = [
# path('admin/', admin.site.urls),
# 127.0.0.1:8000/home/index
re_path('^$', views.base), # 127.0.0.1:8000/
re_path('home/', include('home.urls')),
re_path('users/', include('users.urls')),
]
home/views.py:
def base(requests):
return render(requests, 'base.html')
base.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <h1>欢迎来到蒲公英皇家赌场,美男在线发牌,有癞子激情专场!</h1> <a href="home/index"> 跳转到home应用</a> <a href="users/index"> 跳转到users应用</a> </body> </html>
视图函数分为两类:
视图views.py文件主要是处理业务逻辑,其中包含数据库的交互。
Django使用请求和响应对象来通过系统传递状态。
当浏览器向服务端请求一个页面时,Django创建一个HttpRequest对象,该对象包含关于请求的元数据。然后,Django加载相应的视图,将这个HttpRequest对象作为第一个参数传递给视图函数。
每个视图负责返回一个HttpResponse对象。
Django将所有的请求信息封装到了一个HttpRuests对象中,在我们执行views视图函数时,将这个HttpRuests对象传递给视图函数的第一个形参 request(约定俗称),我们通过request参数调用各种方法,获取请求信息中的各种数据。
request.method: 获取请求方法。 'GET', 'POST', 'DELETE' 等等
request.POST: 获取post请求提交的数据。
request.GET: 获取get请求提交的数据。
from django.contrib import admin from django.urls import path from app01 import views urlpatterns = [ # path('admin/', admin.site.urls), path('index/', views.index), path('login/', views.login), ] def index(request): if request.method == 'GET': # print(request.path) # 获取请求路径(不涵盖查询参数) /index/ # print(request.get_full_path()) # 获取请求路径(涵盖查询参数) /index/?name=barry&age=18 # print(request.get_host()) # 获取 ip地址与端口 127.0.0.1:8000 # print(request.GET) # <QueryDict: {'username': ['barry'], 'password': ['123']}> # print(request.GET.get('username')) # 'barry' # print(request.META) # print(request.META.get('QUERY_STRING')) # 查询参数:username=barry&password=123 # print(request.is_ajax()) # 是否是ajax提交的请求。 print(request.body) # 获取请求信息中的第四部分,请求数据。由于get请求提交的数据作为查询参数拼接在路径后面 # 所以,get请求提交的数据,通过request.body 获取不到。b'' return HttpResponse('get请求成功') else: return HttpResponse('post请求成功') def login(request): if request.method == 'GET': return render(request, 'login.html') <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <form action="http://127.0.0.1:8000/index/" method="get"> <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <input type="submit"> </form> </body> </html>
Meta:主要键值对:
HttpRequest.META 一个标准的Python 字典,包含所有的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 键。
from django.contrib import admin from django.urls import path from app01 import views urlpatterns = [ # path('admin/', admin.site.urls), path('index/', views.index), path('login/', views.login), ] def index(request): if request.method == 'GET': # print(request.path) # 获取请求路径(不涵盖查询参数) /index/ # print(request.get_full_path()) # 获取请求路径(涵盖查询参数) /index/?name=barry&age=18 # print(request.get_host()) # 获取 ip地址与端口 127.0.0.1:8000 # print(request.GET) # <QueryDict: {'username': ['barry'], 'password': ['123']}> # print(request.GET.get('username')) # 'barry' # print(request.META) # print(request.META.get('QUERY_STRING')) # 查询参数:username=barry&password=123 # print(request.is_ajax()) # 是否是ajax提交的请求。 print(request.body) # 获取请求信息中的第四部分,请求数据。由于get请求提交的数据作为查询参数拼接在路径后面 # 所以,get请求提交的数据,通过request.body 获取不到。b'' return HttpResponse('get请求成功') else: # print(request.POST) # 获取post请求提交的数据 # print(request.META) # 获取请求头部所有的数据 # print(request.META.get('QUERY_STRING')) # 获取头部数据中的查询参数 print(request.body) # b'username=barry&password=123' return HttpResponse('post请求成功') def login(request): if request.method == 'GET': return render(request, 'login.html') <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <form action="http://127.0.0.1:8080/index/" method="post"> <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <input type="submit"> </form> </body> </html>
注意:键值对的值是多个的时候,比如checkbox类型的input标签,select标签,需要用:
request.POST.getlist("hobby")
测试:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <form action="http://127.0.0.1:8000/index/" method="post"> <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <select name="sele" id="" multiple> <option value="1">足球</option> <option value="2">篮球</option> <option value="3">乒乓球</option> <option value="4">排球</option> </select> <input type="submit"> </form> </body> </html> def index(request): if request.method == 'GET': return HttpResponse('get请求成功') else: print(request.POST.getlist('sele')) return HttpResponse('post请求成功')
响应就是给浏览器回复响应信息。
给我们的前端响应一个字符串的数据。
HttpResponse.content:响应内容
HttpResponse.charset:响应内容的编码
HttpResponse.status_code:响应的状态
def response(request):
res = HttpResponse()
res.status_code = 201
res['Content-Type'] = 'text/html; charset=GBK'
return res
# return HttpResponse('ok')
渲染一个html页面,如何进行渲染下一节模版渲染系统中详细讲解的,我们现在使用render主要是返回一个html页面数据。
def response(request):
return render(request, 'login.html', status=203,)
redirect是页面重定向,我们什么时候遇到过页面重定向?
在Django中,我们后端使用redirect方法,就可以让页面实现重定向。
登录成功跳转到首页举例说明.
urlpatterns = [ path('login/', views.login), path('base/', views.base), ] def login(request): if request.method == 'GET': return render(request, 'login.html') elif request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') if username == 'barry' and password == '123': return render(request, 'base.html') else: return HttpResponse('登录失败!请重新登录') def base(request): return render(request, 'base.html') <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <form action="" method="post"> <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <input type="submit"> </form> </body> </html>
登录页面时,登录url,登录成功之后,跳转到首页url.
我们现在实现的是登录成功之后,在登录的url这里给你渲染了base页面的内容。这个不是跳转。
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
elif request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
if username == 'barry' and password == '123':
# return render(request, 'base.html')
return redirect('/base/')
else:
return HttpResponse('登录失败!请重新登录')
上面的请求流程:
浏览器先向127.0.0.1:8000/login 发送get请求,请求登录页面。
用户填写完用户名、密码点击提交按钮,浏览器向127.0.0.1:8000/login发送post请求,提交数据,后端验证数据成功之后,返回浏览器一个redirect重定向的指令,并且携带者重定向的URL:127.0.0.1:8000/base/
浏览器接收到redirect重定向的执行,通过location.href = 127.0.0.1:8000/base/重定向到这个URL。
url别名的使用:
path('base/home/index/', views.base, name='base'), def login(request): if request.method == 'GET': return render(request, 'login.html') elif request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') if username == 'barry' and password == '123': # return render(request, 'base.html') # return redirect('/base/home/index/') return redirect('base') # 使用别名 else: return HttpResponse('登录失败!请重新登录')
FBV(function base views) 就是在视图里使用函数处理请求。之前都是FBV模式写的代码,所以就不写例子了。
CBV(class base views) 就是在视图里使用类处理请求。
Python是一个面向对象的编程语言,如果只用函数来开发,有很多面向对象的优点就错失了(继承、封装、多态)。所以Django在后来加入了Class-Based-View。可以让我们用类写View。这样做的优点主要下面两种:
urlpatterns = [ # path('admin/', admin.site.urls), path('base/', views.base, name='base'), path('login/', views.LoginView.as_view(),), ] from django.shortcuts import render, HttpResponse, redirect from django.views import View # Create your views here. class LoginView(View): def get(self, request): return render(request, 'login.html') def post(self, request): username = request.POST.get('username') password = request.POST.get('password') if username == 'barry' and password == '123': return redirect('base') else: return HttpResponse('登录失败!请重新登录') def base(request): return render(request, 'base.html')
我们接下来要研究源码,进行源码分析,源码如何实现get请求来了自动执行CBV的get方法,post请求来了如何自动执行CBV中的post方法?
path('login/', views.LoginView.as_view(),),
LoginView类名调用了as_view()方法,LoginView类没有as_view方法,从父类找,并且类名调用一个方法,该方法应该是类方法或者是静态方法。
查找View父类中的as_view方法:
第一部分,根据各种特殊原因进行报错,不影响我们的主线,可以不研究。
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError("%s() received an invalid keyword %r. as_view "
"only accepts arguments that are already "
"attributes of the class." % (cls.__name__, key))
第一个框框出来的就是各种主动抛异常,cls肯定就是我们Login类名了,http_method_names从源码中可以获取到是一个列表:
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
虽然我们现在不知道initkwargs是什么,但是我们可以大概分析出来,如果你的请求方式不是这个列表中的任意一个,我就会给你抛出一个异常。
第二部分是定义了一个view函数,但是现在还没有执行此函数的执行,所以我们直接跳过分析。
第三部分:
view.view_class = cls
view.view_initkwargs = initkwargs
# 上面两行是给函数view封装两个属性:之前我们没有说过函数也可以封装属性,python中一些皆对象,view函数也是一个对象,这个对象是从function类实例化得来的,我通过dir(view)可知,他有'__setattr__'方法,有这个方法,给view封装属性时,就会执行这个方法不报错,那么这个方法就是实现了给一个函数封装一个属性的功能,你可以随意定义一个函数试一试。
view_class = cls cls 也就是LoginView我们自己定义的类
view_initkwargs = initkwargs initkwargs目前我不确定是什么,但是我可以猜到他应该是与请求方式相关的。
# take name and docstring from class
update_wrapper(view, cls, updated=())
# 这里源码给了解释:这个update_wrapper函数就是从类中获取名字和类的描述消息
# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
# 这个大概意思就是从装饰器中获取设置的属性,
# 上面这两个函数对我们研究流程来说没有什么影响所以我们可以暂不考虑
函数的研究:
def func():
print(111)
func.name = 'barry'
# print(func.name)
print(type(func)) # 函数也是对象,函数这个对象是从function类中实例化得来的
# 函数名可以封装属性
Django按照urls路由系统的特性,就会自动执行view函数。
urlpatterns = [
# path('admin/', admin.site.urls),
path('base/', views.base, name='base'),
# path('login/', views.LoginView.as_view(),),
path('login/', views.LoginView.view,),
]
我们详细看一下view函数:
def view(request, *args, **kwargs):
self = cls(**initkwargs) # self = LoginView() 我们定义的类实例化的对象
if hasattr(self, 'get') and not hasattr(self, 'head'):
# 如果LoginView类中有get方法但是没有head方法
self.head = self.get
# self封装了一个属性head,让head属性等于LoginView类中的get方法。相当于如果我们 LoginView类中没有定义head方法,前端如果发送了head请求,则执行我们的get方法。
self.setup(request, *args, **kwargs)
if not hasattr(self, 'request'):
raise AttributeError(
"%s instance has no 'request' attribute. Did you override "
"setup() and forget to call super()?" % cls.__name__
)
return self.dispatch(request, *args, **kwargs)
我们看一下setup方法,给self对象封装了三个属性
def setup(self, request, *args, **kwargs):
"""Initialize attributes shared by all view methods."""
self.request = request
self.args = args
self.kwargs = kwargs
最后我们看一下,先要执行dispatch方法,得到返回值,放回return这个位置。
return self.dispatch(request, *args, **kwargs)
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
def dispatch(self, request, *args, **kwargs):
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
# handler = getattr(self, 'get')
else:
handler = self.http_method_not_allowed
# 上面的if else判断用意就是:如果你定义的类中有前段发送过来的请求方法对应的方法,没有则记录日志,可能会报错。
return handler(request, *args, **kwargs) # LoginView ---> get(request,)
所以,dispatch方法的核心功能就是:
最后:dispatch的返回值就是对应的执行的方法的返回值,比如此时来的是get请求,对应执行get方法,render(request, ‘login.html’) 得到一个渲染的html文件:
urlpatterns = [
# path('admin/', admin.site.urls),
path('base/', views.base, name='base'),
# path('login/', views.LoginView.view,),
path('login/', 'login.html'文件,), # 如果是get方法。
]
as_view() --> view ---> dispatch ---> 对应的视图类中那个方法的返回值
重写dispatch方法:
class LoginView(View): def dispatch(self, request, *args, **kwargs): print('请求来了') if request.method == 'GET': return HttpResponse('暂不允许get请求') ret = super().dispatch(request, *args, **kwargs) print('请求走了') return ret def get(self, request): print(111) return render(request, 'login.html') def post(self, request): print(222) username = request.POST.get('username') password = request.POST.get('password') if username == 'barry' and password == '123': return redirect('base') else: return HttpResponse('登录失败!请重新登录') def base(request): return render(request, 'base.html')
def inner(*args, **kwargs):
print('请求来了装饰器')
ret = func(*args, **kwargs)
print('请求走了装饰器')
return ret
return inner
@wrapper
def base(request):
print(1111)
return render(request, 'base.html')
def wrapper(func): def inner(*args, **kwargs): print('请求来了装饰器') print(f'args---->{args}') ret = func(*args, **kwargs) print('请求走了装饰器') return ret return inner class LoginView(View): # def dispatch(self, request, *args, **kwargs): # print('请求来了') # if request.method == 'GET': # return HttpResponse('暂不允许get请求') # ret = super().dispatch(request, *args, **kwargs) # print('请求走了') # return ret @wrapper # get = wrapper(get) def get(self, request): print(111) return render(request, 'login.html') def post(self, request): print(222) username = request.POST.get('username') password = request.POST.get('password') if username == 'barry' and password == '123': return redirect('base') else: return HttpResponse('登录失败!请重新登录')
from django.utils.decorators import method_decorator # Create your views here. def wrapper(func): def inner(*args, **kwargs): print('请求来了装饰器') print(f'args---->{args}') ret = func(*args, **kwargs) print('请求走了装饰器') return ret return inner class LoginView(View): # def dispatch(self, request, *args, **kwargs): # print('请求来了') # if request.method == 'GET': # return HttpResponse('暂不允许get请求') # ret = super().dispatch(request, *args, **kwargs) # print('请求走了') # return ret @method_decorator(wrapper) def get(self, request): print(111) return render(request, 'login.html') def post(self, request): print(222) username = request.POST.get('username') password = request.POST.get('password') if username == 'barry' and password == '123': return redirect('base') else: return HttpResponse('登录失败!请重新登录')
借助于method_decorator模块加装饰器不会传递self参数到装饰器里面。
我们想给类中的所有方法都加上装饰器,直接在子类中使用dispatch方法,充当装饰器就可以了。
class LoginView(View):
def dispatch(self, request, *args, **kwargs):
print('请求来了')
if request.method == 'GET':
return HttpResponse('暂不允许get请求')
ret = super().dispatch(request, *args, **kwargs)
print('请求走了')
return ret
静态页面
给浏览器响应的html文件所有的数据都是固定,没有从数据库或者后台获取数据渲染中html页面中。
动态页面
给浏览器响应的html文件中的某些数据是从数据库或者后台获取数据渲染中html页面中,然后在发送给客户端。
如果我们想要返回给客户端(浏览器)一个掺杂着后端数据的html页面,我们需要有一个类似于字符串格式化输出的功能,将数据先渲染到html文件数据中,然后将最终的渲染之后的html页面数据响应给浏览器。这个功能DJango自带的我们称之为模版渲染系统。
简单测试:
urlpatterns = [ # path('admin/', admin.site.urls), path('sta/', views.static_func), path('action/', views.action_func), ] def static_func(request): return render(request, 'static.html') # 获取static.html页面数据 def action_func(request): name = 'barry' return render(request, 'action.html',{'name': name}) action.html: <h1>你好,{{ name }}</h1>
动态页面的加载流程
浏览器(客户端)向指定的urlhttp://127.0.0.1:8000/action/ 发送请求,请求到了Django的urls.py文件,轮训匹配path('action/', views.action_func), 执行对应的action_func函数,render获取action.html数据(我们这里就看成一个大字符串)然后将name:barry渲染到这个字符串的指定位置,最后render将渲染完毕的最终的html页面数据返回到了path('action/', 'action.html'), 最终有path函数将这个页面数据响应给了浏览器。
这里的关键点在render方法:
获取对应的html页面数据。
将指定的后端的数据渲染到此页面数据中。
在Django的模板语言中按此语法使用:{{ 变量名 }}。
当模版引擎遇到一个变量,它将计算这个变量,然后用结果替换掉它本身。 变量的命名包括任何字母数字以及下划线 (“_”)的组合。 变量名称中不能有空格或标点符号。
深度查询据点符(.)在模板语言中有特殊的含义。当模版系统遇到点(“.”),它将以这样的顺序查询:
字典查询(Dictionary lookup)
属性或方法查询(Attribute or method lookup)
数字索引查询(Numeric index lookup)
def action_func(request):
name = 'barry'
lst = ['小雨', '高宁', '明天', '天伦']
dic = {'name': '旋哥的老舅', 'content': '路边小吃炸坤柳', 'price': '买一斤送半斤'}
return render(request, 'action.html',{'name': name, 'lst': lst, 'dd': dic})
action.html:
<h1>你好,{{ name }}</h1>
<h2>{{ lst }}</h2>
<h3>{{ dd }}</h3>
我们的容器型数据类型的对象,函数,类等对象大部分都可以通过万能的点去获取实现。
def action_func(request): name = 'barry' lst = ['小雨', '高宁', '明天', '天伦'] dic = {'name': '旋哥的老舅', 'content': '路边小吃炸坤柳', 'price': '买一斤送半斤'} def func(): return '蒲公英四期666' class Year: date = '2023/1/22' def __init__(self,name,age): self.name = name self.age = age def y_func(self): return f'{self.name}回家过年' obj = Year('姚聪', 20) return render(request, 'action.html', {'name': name, 'lst': lst, 'dd': dic, 'func': func, 'obj': obj}) <div>{{ lst.0 }}</div> <div>{{ lst.1 }}</div> <div>{{ lst.2 }}</div> <div>{{ dd.name }}</div> <div>{{ dd.content }}</div> <div>{{ dd.price }}</div> <div>{{ func }}</div> <div>{{ obj.name }}</div> <div>{{ obj.date }}</div> <div>{{ obj.y_func }}</div>
有的时候我们通过render渲染到html的数据并不是我们最终想要的数据,比如后端向前端传递的数据为hello,但是我们html想要显示为HELLO,当然这个在后端是可以提前处理的,但是诸如此类的需求我们可以通过过滤器解决,过滤器给我们提过了很多便捷的方法,加工你传递到html的数据,便于灵活开发。
过滤器与在后端直接修改想要渲染的数据的唯一区别就是:过滤器是保证在后端不用重启的前提下,对之前渲染的变量数据进行进一加工处理。
def action_func(request):
# name = 'barry'.upper() 方式一
name = 'barry' # 方式二
return render(request, 'action.html', {'name': name})
action.html:
<div>{{ name }}</div> # 方式一
<div>{{ name|upper }}</div> # 方式二
过滤器的语法: {{ value| filter_name:参数 }}
使用管道符"|"来应用过滤器。
例如:{{ name| lower }}会将name变量应用lower过滤器之后再显示它的值。lower在这里的作用是将文本全都变成小写。
注意事项:
Django的模板语言中提供了大约六十个内置过滤器。
default: 如果一个变量是false或者为空,使用给定的默认值。 否则,使用变量的值。
views:
a = '' # 没有变量a或者变量a为空
html: # 显示啥也没有
{{ value|default:"啥也没有"}}
**length:**返回值的长度,作用于字符串和列表。
views:
name_list = ['王阔', '天琪', '傻强', '志晨', '健身哥']
s = '太白金星讲师'
html:
{{ name_list|length }} # 显示为5
{{s|length }} # 显示为6
: 将值格式化为一个 “人类可读的” 文件尺寸 (例如 '13 KB'
, '4.1 MB'
, '102 bytes'
, 等等)。
views:
value = 1048576
html:
{{ value|filesizeformat }} # 显示为1.0MB
:切片,支持pyhton中可以用切片的所有数据类型
views:
name_list = ['王阔', '天琪', '傻强', '志晨', '健身哥']
s = '太白金星讲师'
html:
{{ name_list|slice:'1:3' }} # ['天琪', '傻强']
{{ s|slice:'1::2' }} # '白星师'
:时间格式化
views:
time = datetime.datetime.now()
html:
{{ t|date:"Y-m-d H:i:s" }} # 2020-02-11 07:31:29
**:**如果字符串字符多于指定的字符数量,那么会被截断。截断的字符串将以可翻译的省略号序列(“…”)结尾。
参数:截断的字符数
views:
describe = '1999年3月,马云正式辞去公职,后来被称为18罗汉的马云团队回到杭州,凑够50万元人民币'
html:
{{ describe|truncatechars:9 }} # 1999年3...
# 截断9个字符,三个点也算三个字符
**:**在一定数量的字后截断字符串,是截多少个单词。
views:
words = 'i love you my country china'
html:
{{ words|truncatewords:3 }} # i love you...
: 移除value中所有的与给出的变量相同的字符串
views:
words = 'i love you my country china'
html:
{{ words|cut:' ' }} # iloveyoumycountrychina
: 设定连接符将可迭代对象的元素连接在一起与字符串的join方法相同。
views:
name_list = ['王阔', '天琪', '傻强', '志晨', '健身哥']
dic = {'name':'太白','age': 18}
tu = (1, 2, 3)
html:
<li>{{ name_list|join:'_'}}</li>
<li>{{ dic|join:','}}</li>
<li>{{ tu|join:'+'}}</li>
'''
王阔_天琪_傻强_志晨_健身哥
name,age
1+2+3
'''
Django的模板中在进行模板渲染的时候会对HTML标签和JS等语法标签进行自动转义,原因显而易见,这样是为了安全,django担心这是用户添加的数据,比如如果有人给你评论的时候写了一段js代码,这个评论一提交,js代码就执行啦,这样你是不是可以搞一些坏事儿了,写个弹窗的死循环,那浏览器还能用吗,是不是会一直弹窗啊,这叫做xss攻击,所以浏览器不让你这么搞,给你转义了。但是有的时候我们可能不希望这些HTML元素被转义,比如我们做一个内容管理系统,后台添加的文章中是经过修饰的,这些修饰可能是通过一个类似于FCKeditor编辑加注了HTML修饰符的文本,如果自动转义的话显示的就是保护HTML标签的源文件。为了在Django中关闭HTML的自动转义有两种方式,如果是一个单独的变量我们可以通过过滤器“|safe”的方式告诉Django这段代码是安全的不必转义。
我们去network那个地方看看,浏览器看到的都是渲染之后的结果,通过network的response的那个部分可以看到,这个a标签全部是特殊符号包裹起来的,并不是一个标签,这都是django搞得事情。
a = '<a href="http://www.baidu.com">百度一下</a>'
{{ a }}
{{ a|safe }}
将日期格式设为自该日期起的时间(例如,“4天,6小时”)。
采用一个可选参数,它是一个包含用作比较点的日期的变量(不带参数,比较点为现在)。 例如,如果since_12是表示2012年6月28日日期实例,并且comment_date是2018年3月1日日期实例:
views:
year_12 = datetime.datetime.now().replace(year=2012, month=6, day=28)
year_18 = datetime.datetime.now().replace(year=2018, month=3, day=1)
html:
<li>{{ year_12|timesince:year_18}}</li>
'''
5 years, 8 months
'''
如果year_18不写,默认就是距现在的时间段。
似于timesince,除了它测量从现在开始直到给定日期或日期时间的时间。 例如,如果今天是2006年6月1日,而conference_date是保留2006年6月29日的日期实例,则{{ conference_date | timeuntil }}将返回“4周”。
使用可选参数,它是一个包含用作比较点的日期(而不是现在)的变量。 如果from_date包含2006年6月22日,则以下内容将返回“1周”:
{{ conference_date|timeuntil:from_date }}
这里简单介绍一些常用的模板的过滤器,更多详见
更多内置过滤器(此链接页面最下面的内置过滤器):https://docs.djangoproject.com/en/1.11/ref/templates/builtins/#filters
现在我们已经可以从后端通过模版系统替换掉前端的数据了,但是如果只是替换掉数据,确实不够灵活,比如,我们要想在前端页面展示可迭代对象name_list = [‘王阔’, ‘天琪’, ‘傻强’, ‘志晨’, ‘健身哥’]每个元素,你如和展示呢?
# 前端页面
<ul>
<li>{{ name_list.0 }}</li>
<li>{{ name_list.1 }}</li>
<li>{{ name_list.2 }}</li>
<li>{{ name_list.3 }}</li>
</ul>
这样写明显很low,我们要是可以用上for循环就好了。Django这么强大的框架,不可能想不到这点的,这里模版系统给我们提供了另一种标签,就是可以在html页面中进行for循环以及if判断等等。
标签看起来像是这样的: {% tag %}
。标签比变量更加复杂:一些在输出中创建文本,一些通过循环或逻辑来控制流程,一些加载其后的变量将使用到的额外信息到模版中。与python语法不同的是:一些标签需要开始和结束标签 (例如{% tag %} ...
标签 内容 … {% endtag %})。
学习下面几种标签之前,我们要重新写一个url、views以及html,方便分类学习不与上面的变量产生冲突。
基本语法:
{% for 变量 in render的可迭代对象 %}
{{ 变量 }}
{% endfor %}
例如:
{% for foo in name_list %}
{{ foo }}
{% endfor %}
遍历一个列表
lst = ['小雨', '高宁', '明天', '天伦', '菜籽']
{% for n in lst %}
{{ n }}
{% endfor %}
反向遍历一个列表
lst = ['小雨', '高宁', '明天', '天伦', '菜籽']
{% for n in lst reversed %}
{{ n }}
{% endfor %}
遍历一个字典
<div> {% for key in dic %} {{ key }} {% endfor %} </div> <div> {% for key in dic.keys %} {{ key }} {% endfor %} </div> <ul> {% for val in dic.values %} <li>{{ val }}</li> {% endfor %} </ul> <ul> {% for key,val in dic.items %} <li>{{ key }} ----> {{ val }}</li> {% endfor %} </ul>
forloop的使用
模版系统给我们的for标签还提供了forloop的功能,这个就是获取循环的次数,有多种用法:
forloop.counter 当前循环的索引值(从1开始),forloop是循环器,通过点来使用功能
forloop.counter0 当前循环的索引值(从0开始)
forloop.revcounter 当前循环的倒序索引值(从1开始)
forloop.revcounter0 当前循环的倒序索引值(从0开始)
forloop.first 当前循环是不是第一次循环(布尔值)
forloop.last 当前循环是不是最后一次循环(布尔值)
forloop.parentloop 本层循环的外层循环的对象,再通过上面的几个属性来显示外层循环的计数等
测试:
lst = ['小雨', '高宁', '明天', '天伦', '菜籽']
{% for n in lst %}
{# <div>{{ forloop.counter }}--->{{ n }}</div>#}
{# <div>{{ forloop.counter0 }}--->{{ n }}</div>#}
{# <div>{{ forloop.revcounter }}--->{{ n }}</div>#}
<div>{{ forloop.revcounter0 }}--->{{ n }}</div>
{% endfor %}
for…empty…组合
如果遍历的可迭代对象是空的或者就没有这个对象,利用这个组合可以提示用户。
lst1 = []
{% for foo in lst1 %}
<div>{{ foo }}</div>
{% empty %}
<div>啥也没有!</div>
{% endfor %}
基本语法:
{% if %}
会对一个变量求值,如果它的值是“True”(存在、不为空、且不是boolean类型的false值),对应的内容块会输出。
{% if 条件 %}
结果 <!--不满足条件,不会生成这个标签-->
{% elif 条件 %}
结果
{% else %} <!--也是在if标签结构里面的-->
结果
{% endif %}
elif和else一定要在if endif标签里面,设置多个elif或者没有elif、有没有else都可以。
测试:
age = 16 {% if age > 18 %} <div>什么都可以干了!</div> {% elif age == 18 %} <div>悠着点,刚到18岁</div> {% else %} <div>偷着干</div> {% endif %} # 可以结合过滤器 {% if lst|length > 5 %} <div>名字还是很多</div> {% else %} <div>{{ lst }}全部都是精英</div> {% endif %} 条件也可以加逻辑运算符 if语句支持 and 、or、==、>、<、!=、<=、>=、in、not in、is、is not判断,注意条件两边都有空格。 pass 与for循环配合使用 {% for foo in lst %} {% if forloop.first %} <div>{{ foo }}</div> {% else %} <div>只有第一次循环输入名字</div> {% endif %} {% endfor %} # lis = [['A', 'B'], ['C', 'D'], ['E', 'F']] {% for i in lis %} {% for j in i %} <div>{{ forloop.parentloop.counter }}---->{{ j }}</div> {% endfor %} {% endfor %}
使用一个简单地名字缓存一个复杂的变量,多用于给一个复杂的变量起别名,当你需要使用一个“昂贵的”方法(比如访问数据库)很多次的时候是非常有用的,记住!等号的左右不要加空格!!
class A: def __init__(self, age): self.age = age class B: def __init__(self, name, obj): self.name = name self.obj = obj o1 = A(20) o2 = B('barry', o1) {{ o2.obj.age }} {% with o2.obj.age as a %} {{ a }} {% endwith %}
注意事项
{% if a > b > c %}
...
{% endif %}
def xx(request):
d = {"a": 1, "b": 2, "c": 3, "items": "100"}
return render(request, "xx.html", {"data": d})
如上,我们在使用render方法渲染一个页面的时候,传的字典d有一个key是items并且还有默认的 d.items() 方法,此时在模板语言中:
{{ data.items }}
默认会取d的items key的值。
这个标签是不是非常熟悉?之前我们以post方式提交表单的时候,会报错,还记得我们在settings里面的中间件配置里面把一个csrf的防御机制给注销了啊,本身不应该注销的,而是应该学会怎么使用它,并且不让自己的操作被forbiden,通过这个标签就能搞定。
接下来我们重写构建一个流程:
path('login/', views.login), def login(request): if request.method == 'GET': return render(request, 'login.html') else: print(request.POST) return HttpResponse('登录成功') <form action="" method="post"> <div>username: <input type="text" name="username"></div> <div>password: <input type="password" name="password"></div> <input type="submit"> </form>
我们在form标签中,加入了一个csrf_token标签:render 就渲染成下面这个隐藏的input标签。
<form action="" method="post">
{% csrf_token %}
<div>username: <input type="text" name="username"></div>
<div>password: <input type="password" name="password"></div>
<input type="submit">
</form>
我们再次点击post提交,携带者这个隐藏的input标签,就可以通过csrf防跨站请求伪造的安全机制。
流程或者原理:
我们第一次进行get请求,请求我们的login页面,在视图函数的render方法将{% csrf_token %}渲染成了一个隐藏的input标签,这个隐藏的input标签的name:csrfmiddlewaretoken值对应的一堆密文(每次请求都会改变),并且将这个csrfmiddlewaretoken:密文 键值对保存在内存中,然后将渲染好的html页面数据响应给浏览器。
客户加载到登录页面时,form表单中就携带了隐藏的input标签以及csrfmiddlewaretoken:密文,然后客户输入用户名密码进行post提交,提交时,浏览器会带着用户名、密码以及csrfmiddlewaretoken:密文一并提交到后端,后端先验证此时的csrfmiddlewaretoken:密文与我之前保存的那个csrfmiddlewaretoken:密文是否一致,一致则通过验证,不一致则返回forbidden页面。
第一次get请求,他返回你一个盖戳的文件,当你在进行post请求时他会验证是不是那个盖戳的文件。他的目的就是你提交post请求时,必须是从我给你的get请求返回的页面提交的。为什么这么做呢?是因为有人登录你的页面时是可以绕过的get请求返回的页面直接进行post请求登录的,比如说爬虫。直接通过requests.post(‘/login/’)直接请求的,这样是没有csrftoken的,这样就直接拒绝了。
什么是模版继承?将这两个字拆开我们都清楚,模版就是django提供的用于html发送浏览器之前,需要在某些标签进行替换的系统,而继承我们立马就会想到这是面向对象的三大特性之一。其实模版继承就是拥有这两个特性的模版的高级用法。先不着急直接上知识点,我们用一个例子引出模版继承。
我们经常访问一些网站,你会发现只要是一个网站的多个html页面,他们有一部分是一模一样的,剩下的部分是本页面独有的。比如我们经常使用的博客园:
这个是我博客园的首页:
然后我们随机点一篇博客进去:
个人博客园设定了博客园的样式之后,你所有的博客都是按照这个样式创建的,在我选择的主题这里:导航条和侧边栏布局是一样的。再比如如果是公司内部的xx管理系统,更是如此。我们打开bootstraps网站(起步):
一般管理系统都是这样的布局,这个就是固定的导航条和侧边栏。无论我点击侧边栏里的那个按钮,这两部分不会更换只会改变中间的内容。
那么接下来我们实现一个这样的布局。
我们要准备4个html页面:base.html、menu1.html、menu2.html、menu3.html,这四个页面的导航条与左侧侧边栏一样,每个页面对应一个url。并且每个页面的左侧侧边栏菜单一、菜单二、菜单三可以实现跳转:跳转到menu1.html、menu2.html、menu3.html三个页面。而顶端导航条只是样式即可。接下来借助于Django,我们实现这四个页面并对应urls可以跑通流程。
# urls.py: urlpatterns = [ # path('admin/', admin.site.urls), path('base/', views.base), path('menu1/', views.menu1), path('menu2/', views.menu2), path('menu3/', views.menu3), ] # views.py: def base(request): return render(request, 'base.html') def menu1(request): return render(request, 'menu1.html') def menu2(request): return render(request, 'menu2.html') def menu3(request): return render(request, 'menu3.html') <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> <style> * { margin: 0; padding: 0; } .nav { width: 100%; height: 40px; background-color: grey; color: white; line-height: 40px; } .nav span{ margin-left: 50px; } .left { width: 20%; height: 1000px; background-color: #5d7eff; float: left; text-align: center; } .left ul li{ margin-top: 30px; } .left ul li a{ color: #ff98e2; text-decoration: none; } .content{ color: red; } </style> </head> <body> <div class="nav"> <span>首页</span> <span>普通足浴</span> <span>vip足浴</span> <span>全身span</span> <span>帝王服务</span> <span>检索:<input type="text"></span> </div> <div class="left"> <ul> <li><a href="/menu1/">菜单一</a></li> <li><a href="/menu2/">菜单二</a></li> <li><a href="/menu3/">菜单三</a></li> </ul> </div> <div class="content"> 欢迎来到红浪漫base首页,请先挑选技师。 </div> </body> </html>
如果我们这样去构建一个网站,前端页面出现很多的重复性的代码,太low了。我们使用母版继承。
我们将base页面设置成母版,让其他的页面继承base页面。
# 在子html文件的最顶端:
{% extends 'base.html' %}
如果我们这样使用母版继承,是完全沿用母版的样式,我们自己本页面就没有任何的样式。虽然节省代码了, 但是不具有拓展性。所以,我们既要继承母版的样式,又可以在某些位置变成自己的样式。
现在已经完成了继承母版,这个只是减少了重复代码,还没有实现每个页面自定制的一些内容,如果你想要实现自定制的内容怎么做?类似于模版系统你是不是应该在母版的具体位置做一个标识,然后在自己的页面对应的地方进行自定制?那么这个类似于%占位符的特定的标识叫做钩子。这几个页面只是在menu div不同,所以我们就在这里做一个钩子就行了。
base.html: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> <style> * { margin: 0; padding: 0; } .nav { width: 100%; height: 40px; background-color: grey; color: white; line-height: 40px; } .nav span { margin-left: 50px; } .left { width: 20%; height: 1000px; background-color: #5d7eff; float: left; text-align: center; } .left ul li { margin-top: 30px; } .left ul li a { color: #ff98e2; text-decoration: none; } {% block css %} .content { color: red; } {% endblock css %} </style> </head> <body> <div class="nav"> <span>首页</span> <span>普通足浴</span> <span>vip足浴</span> <span>全身span</span> <span>帝王服务</span> <span>检索:<input type="text"></span> </div> <div class="left"> <ul> <li><a href="/menu1/">菜单一</a></li> <li><a href="/menu2/">菜单二</a></li> <li><a href="/menu3/">菜单三</a></li> </ul> </div> <div class="content"> {% block content %} 欢迎来到红浪漫base首页,请先挑选技师。 {% endblock content %} </div> </body> </html> menu1: {% extends 'base.html' %} {% block css %} .content { color: blue; } {% endblock css %} {% block content %} 欢迎来到menu1首页。 {% endblock content %}
使用supper这个方法就可以实现。
# menu1、menu2、menu3、
{% block content %}
{{ block.super }}
欢迎来到menu1首页。
{% endblock content %}
{% extends %}
标签,它必须是模版中的第一个标签。其他的任何情况下,模版继承都将无法工作,模板渲染的时候django都不知道你在干啥。{% block %}
标签越好。请记住,子模版不必定义全部父模版中的blocks,所以,你可以在大多数blocks中填充合理的默认内容,然后,只定义你需要的那一个。多一点钩子总比少一点好。{% block %}
中。block
标签。组件就是将一组常用的功能封装起来,保存在单独的html文件中,(如导航条,页尾信息等)其他页面需要此组功能时,按如下语法导入即可。 这个与继承比较类似,但是‘格局’不同,继承是需要写一个大的母版,凡是继承母版的一些html基本上用的都是母版页面的布局,只有一部分是自己页面单独展现的,这好比多以及排布好的多个组件。
制定一个导航栏的组件:
<div class="nav">.
<span>首页</span>
<span>普通足浴</span>
<span>vip足浴</span>
<span>全身span</span>
<span>帝王服务</span>
<span>检索:<input type="text"></span>
</div>
定义一个流程:
path('component/', views.component), def component(request): return render(request, 'component.html') <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> {% include 'nav.html' %} <div>我是组件页面</div> <ul> <li>菜籽</li> <li>大雨</li> <li>小雨</li> <li>聪哥</li> </ul> </body> </html>
组件vs模板
母版属于网站整体结构的布局,格局大,一般我们的子页面嵌套在模板中构建样式。
组件属于网站的单独某个功能,格局小,一般是将组件嵌套在子页面中的构建样式。
Django的模版系统给我们提供了很多过滤器,比如切割、全大写、获取元素个数,联合join,安全性质的safe等等,这些都是对数据(尤其是字符串类型的数据)进行加工,但是这些都是模版系统提供给我们的,不能满足应对工作中的所有需求,所以Django给我们提供了口子:让我们可以自己定义过滤器,这样我们开发起来更加的灵活。接下来我们讲解如何自己定义过滤器。
在应用目录下创建templatetags文件夹(必须这样命名)
在此目录下创建一个任意命名的py文件。
在py文件中引用template模块并注册过滤器。
from django import template
# 生成过滤器对象
register = template.Library()
自定义过滤器函数。
@register.filter
def language_join(v):
pass
return # 设置返回值
测试:
path('tags/', views.tags), def tags(request): content = '下周就放假' name = '啊举' return render(request, 'tags.html', locals()) custom_tags.py: from django import template # 生成过滤器对象 register = template.Library() @register.filter def language_join(v): v += '做大做强,再创辉煌!' return v @register.filter def func(v1,v2): return v1 + v2 tags.html: {{ content|length }} {% load custom_tags %} {{ name|language_join }} {{ name|func:'sb' }}
自定义标签与自定义过滤器的构建流程几乎一模一样。前3步都一样,只有第四步不一样:
@register.simple_tag # 引用的装饰器不同
def all_join(v1,v2,...):
pass
return
测试:
path('tags/', views.tags), def tags(request): name = '啊举' content = '30岁' return render(request, 'tags.html', locals()) # custom_tags.py: @register.simple_tag def cus_tags(v1, v2, v3, v4='默认值'): return v1 + v2 + v3 + v4 # tags.html: {% load custom_tags %} {#{% cus_tags name content '666' '大帅哥' %}#} {% cus_tags '新月' '考了' '62分' '真牛逼' %}
这个标签不同于上面的两种,这个标签是将一段html代码插入到我们html页面中,有点儿类似于组件,但是比组件还灵活。上一节我们讲组件时,我们将自己设计的一个顶端的导航条nav.html作为一个组件,然后让另一个页面去使用,还记得这个例子吧?这次我们用inclusion_tag去展示一下,对比分析一下这两个有什么不同。
inclusion_tag标签:是对一段html代码或者组件进行进一步加工。
path('tags/', views.tags), def tags(request): nav_list = ['自拍偷拍', '亚洲步兵', '欧洲骑兵', '中文字幕', '动漫3D', '图片小说'] return render(request, 'tags.html', locals()) tags.html: {% load custom_tags %} {% nav_data nav_list %} custom_tags.py: @register.inclusion_tag('nav.html') def nav_data(v1): v1 += [time.strftime('%Y/%m/%d %H:%M:%S', time.localtime()), ] return {'data': v1} nav.html: <style> * { margin: 0; padding: 0; } .nav { width: 100%; height: 40px; background-color: grey; color: white; line-height: 40px; } .nav span { margin-left: 50px; } </style> <div class="nav"> {% for item in data %} <span>{{ item }}</span> {% endfor %} <span>检索:<input type="text"></span> </div>
请求到达urls---->views视图函数---->render
将nav_list列表数据传递到自定义组件函数中nav_data,当前这个nav_data函数对nav_list数据做进一步加工,完毕之后,将这个数据传递到nav.html组件中,然后对组件进行渲染,最终将渲染完毕的组件替换到
tags.html:
{% nav_data nav_list %}
最终页面呈现出上图的样式。
截止到目前为止,我们还没有应用到css、js等文件,只是使用了html页面,现在我们的页面可算是不要太丑了。所以项目中我们肯定是要引入静态文件的,什么是静态文件?静态文件就是我们之前学过的css、js、图片文件、视频文件等等,我们在html想要使用他们就是引用静态文件,之前我们写页面是需要通过标签引入静态文件,并且需要各种路径的配置,现在我们利用Django做一个简单的配置就可以直接引入静态文件了。
我们使用之前的方式引用一下css文件,是引用不到的:
path('teststatic/', views.teststatic), def teststatic(request): return render(request, 'teststatic.html') <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> <link rel="stylesheet" href="nav.css"> </head> <body> <div>过年不回家的,跟我走!</div> </body> </html> nav.css: div { color: red; }
css样式没有引用到。
我们在Django框架中,就遵循Django规范,那么Django是如何配置css。
项目目录下单独创建一个静态文件目录:将所有的静态文件(css, js,图片,下载文件等资源放置在这里面)
在settings配置文件中,进行相关的配置
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'jingtaiwenjian'),
]
在html文件中引用。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap 101 Template</title>
<link rel="stylesheet" href="/static/css/nav.css">
</head>
<body>
<div>过年不回家的,跟我走!</div>
</body>
</html>
为什么我们路径使用static替代jingtaiwenjian这个目录?
在settings配置文件的这里设置别名;
# STATIC_URL = '/static/' # 别名默认static
STATIC_URL = '/pugongying/' # 设置别名
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'jingtaiwenjian'),
]
大部分在线商城的网站的静态资源的路径都是通过别名代替真实路径:
这么做的第一个好处就是为了安全。
如果以后遇到了需求,必须将静态文件的名字改掉(比如我们本次取的名字jingtaiwenjian就不规范),如果没有别名的话,你必须将所有的引用静态文件的相关link路径都必须改掉,这样太麻烦了,但是我们有了别名的概念,无论你以后如何更改静态文件的名字,都不会造成影响的。
别名的优点:
使用别名代替真实的静态文件名字,防止有人对你的静态文件发起恶意攻击,保证静态文件的安全。
使用别名代替真实的静态文件名字,保持了代码的拓展性。
我们还可以通过第二种方式引入静态文件,第一种方式是有漏洞的,假如我们已经完成了一期项目,你的项目中已经创建了多个html文件,并且都一直通过第一种方式引入静态文件,此时你跳槽了,其他的人接收你的项目,他没有注意到你的别名为static,而当它二期项目已经进展了很长时间发现你的static别名与二期项目中的某些url冲突了,这时必须要更改你的别名才可以解决问题,那么怎么做?因为你通过第一种方式引入的静态文件,如果你把别名改了,你的很多html页面的link路径都必须要更改,所以这种方式还是有弊端的。那么接下来我介绍第二种引入静态文件的方式,就可以完美避开这个问题。
{% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> {# <link rel="stylesheet" href="/pugongying/css/nav.css">#} <link rel="stylesheet" href="{% static 'css/nav.css' %}"> </head> <body> <div>过年不回家的,跟我走!</div> </body> </html>
如果你的静态文件的路径过长,我们可以再起别名进行替换。
{% load static %} {% static 'css/nav.css' as c %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> <link rel="stylesheet" href="{{ c }}"> </head> <body> <div>过年不回家的,跟我走!</div> </body> </html>
DBA+ 应用层开发。
一般中大型公司(或者数据量巨大、读取数据的需求频繁并且追求极致效率的公司)会有专门的DBA管理数据库,编写sql语句,对于应用层开发来说,不用写sql语句,直接调用他写的接口就行。所以在这种公司一般来说,开发人员应该’供’着DBA,因为你想写入或者取出的数据需要依赖于DBA去执行,或者是你写的比较复杂的sql语句需要让DBA帮你看一下,效率行不行、是不是需要优化等等,这就需要看你们的交情或者其心情了。哈哈(开个玩笑)。
应用程序开发+sql语句编写。
这种情况多存在于小公司,没有专门设置DBA岗位,要求开发人员什么都会一些,linux、数据库、前端等等,这样成本降低并且减少由于部门之间的沟通带来的损失,提高工作流程效率。
应用程序开发+ORM.
这种模式sql不用你写,你直接写类、对象,应为你对这些更加游刃有余。然后通过你写的类、对象,等等通过相应的转换关系直接转化成对应的原生sql语句,这种转化关系就是ORM:对象-关系-映射。你直接写一个类就是创建一张表,你实例化一个对象就是增加一条数据,这样以来,既可以避开写sql语句的麻烦,而且可以提升我们的开发效率。
这样开发效率肯定是提升了,但是也有一点点缺陷就是通过ORM转化成的sql语句虽然是准确的,但是不一定是最优的。
优点:
缺点:
我们先要创建一个ormtest项目:
python manage.py makemigrations # 在migrations目录下生成一个以000x开头的脚本文件
python manage.py migrate # 执行migrations中未执行的脚本文件。
现在我们这个userinfo表已经生成,生成到哪了?生成到sqllit3数据库的客户端了。
sqlit3是一个小型的关系型数据库,可以供我们测试使用,一般项目中不用这个数据库。我们配置一下sqlit3数据库的相关配置。
我们进行拖拽操作:
我们看一下表名:Django会将你models中所有的类,通过应用名_类名的方式生成具体的表。
为什么有这么多表?
django除了我们自己创建的app01这个应用,还有其他的自带的应用:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01.apps.App01Config',
]
python manage.py makemigrations
# 执行所有应用下面的models.py里面的类,在所有应用下migrations目录下生成一个以000x开头的脚本文件
python manage.py migrate
# 执行所有应用到的migrations中未执行的脚本文件,在数据库中生成对应的表。
接下来,我们把sqlit数据库的配置以及内容删除,我们使用orm数据库进行测试。
在settings中进行相关配置:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'ormtest', # 要连接的数据库,连接前需要创建好
'USER': 'root', # 连接数据库的用户名
'PASSWORD': '123', # 连接数据库的密码
'HOST': '127.0.0.1', # 连接主机,默认本级
'PORT': 3306, # 端口 默认3306
}
}
上面这个配置是给整个项目配置了一个数据库,所有的应用都使用这个数据库。(一般我们都是一个项目一个数据库)
下面是可以给每个应用配置一个数据库。
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME':'ormtest', # 要连接的数据库,连接前需要创建好 'USER':'root', # 连接数据库的用户名 'PASSWORD':'', # 连接数据库的密码 'HOST':'127.0.0.1', # 连接主机,默认本级 'PORT':3306 # 端口 默认3306 }, 'app01': { #可以为每个app都配置自己的数据,并且数据库还可以指定别的,也就是不一定就是mysql,也可以指定sqlite等其他的数据库 'ENGINE': 'django.db.backends.mysql', 'NAME':'ormtest', # 要连接的数据库,连接前需要创建好 'USER':'root', # 连接数据库的用户名 'PASSWORD':'', # 连接数据库的密码 'HOST':'127.0.0.1', # 连接主机,默认本级 'PORT':3306 # 端口 默认3306 } } app配置单独的数据库
在MySQL中创建数据库:
create database ormtest;
Django项目连接数据库:
现在必要的参数我们都配置好了,但是我们如何让你的项目连接数据库?怎么连接呢?django默认你导入的驱动是MySQLdb,可是MySQLdb 对于py3.4+版本有很大问题,所以我们需要的驱动是PyMySQL。所以,我们只需要找到项目名文件下的__init__,在里面写入:
import pymysql
pymysql.install_as_MySQLdb()
通过输入命令生成具体的表结构:
python manage.py makemigrations
python manage.py migrate
我们配置pycharm的可视化工具,连接MySQL:
研究一下同步数据库指令的原理
在执行 python manager.py makemigrations时
Django 会在相应的 app 的migrations文件夹下面生成 一个python脚本文件
在执行 python manager.py migrate 时 Django才会生成数据库表,那么Django是如何生成数据库表的呢?
Django是根据 migrations下面的脚本文件来生成数据表的
每个migrations文件夹下面有多个脚本,那么django是如何知道该执行那个文件的呢,django有一张django-migrations表,表中记录了已经执行的脚本,那么表中没有的就是还没执行的脚本,则 执行migrate的时候就只执行表中没有记录的那些脚本。
有时在执行 migrate 的时候如果发现没有生成相应的表,可以看看在 django-migrations表中看看 脚本是否已经执行了,可以删除 django-migrations 表中的记录 和 数据库中相应的 表 ,然后重新执行
每个字段有一些特有的参数,例如,CharField需要max_length参数来指定VARCHAR
数据库字段的大小。还有一些适用于所有字段的通用参数。 这些参数在文档中有详细定义,这里我们只简单介绍一些最常用的:
<1> CharField 字符串字段, 用于较短的字符串. CharField 要求必须有一个参数 maxlength, 用于从数据库层和Django校验层限制该字段所允许的最大字符数. <2> IntegerField #用于保存一个整数. <3> DecimalField 一个浮点数. 必须 提供两个参数: 参数 描述 max_digits 总位数(不包括小数点和符号) decimal_places 小数位数 举例来说, 要保存最大值为 999 (小数点后保存2位),你要这样定义字段: models.DecimalField(..., max_digits=5, decimal_places=2) 要保存最大值一百万(小数点后保存10位)的话,你要这样定义: models.DecimalField(..., max_digits=17, decimal_places=10) #max_digits大于等于17就能存储百万以上的数了 admin 用一个文本框(<input type="text">)表示该字段保存的数据. <4> AutoField 一个 IntegerField, 添加记录时它会自动增长. 你通常不需要直接使用这个字段; 自定义一个主键:my_id=models.AutoField(primary_key=True) 如果你不指定主键的话,系统会自动添加一个主键字段到你的 model. <5> BooleanField A true/false field. admin 用 checkbox 来表示此类字段. <6> TextField 一个容量很大的文本字段. admin 用一个 <textarea> (文本区域)表示该字段数据.(一个多行编辑框). <7> EmailField 一个带有检查Email合法性的 CharField,不接受 maxlength 参数. <8> DateField 一个日期字段. 共有下列额外的可选参数: Argument 描述 auto_now 当对象被保存时(更新或者添加都行),自动将该字段的值设置为当前时间.通常用于表示 "last-modified" 时间戳. auto_now_add 当对象首次被创建时,自动将该字段的值设置为当前时间.通常用于表示对象创建时间. (仅仅在admin中有意义...) <9> DateTimeField 一个日期时间字段. 类似 DateField 支持同样的附加选项. <10> ImageField 类似 FileField, 不过要校验上传对象是否是一个合法图片.#它有两个可选参数:height_field和width_field, 如果提供这两个参数,则图片将按提供的高度和宽度规格保存. <11> FileField 一个文件上传字段. 要求一个必须有的参数: upload_to, 一个用于保存上载文件的本地文件系统路径. 这个路径必须包含 strftime #formatting, 该格式将被上载文件的 date/time 替换(so that uploaded files don't fill up the given directory). admin 用一个<input type="file">部件表示该字段保存的数据(一个文件上传部件) . 注意:在一个 model 中使用 FileField 或 ImageField 需要以下步骤: (1)在你的 settings 文件中, 定义一个完整路径给 MEDIA_ROOT 以便让 Django在此处保存上传文件. (出于性能考虑,这些文件并不保存到数据库.) 定义MEDIA_URL 作为该目录的公共 URL. 要确保该目录对 WEB服务器用户帐号是可写的. (2) 在你的 model 中添加 FileField 或 ImageField, 并确保定义了 upload_to 选项,以告诉 Django 使用 MEDIA_ROOT 的哪个子目录保存上传文件.你的数据库中要保存的只是文件的路径(相对于 MEDIA_ROOT). 出于习惯你一定很想使用 Django 提供的 get_<#fieldname>_url 函数.举例来说,如果你的 ImageField 叫作 mug_shot, 你就可以在模板中以 {{ object.#get_mug_shot_url }} 这样的方式得到图像的绝对路径. <12> URLField 用于保存 URL. 若 verify_exists 参数为 True (默认), 给定的 URL 会预先检查是否存在( 即URL是否被有效装入且 没有返回404响应). admin 用一个 <input type="text"> 文本框表示该字段保存的数据(一个单行编辑框) <13> NullBooleanField 类似 BooleanField, 不过允许 NULL 作为其中一个选项. 推荐使用这个字段而不要用 BooleanField 加 null=True 选项 admin 用一个选择框 <select> (三个可选择的值: "Unknown", "Yes" 和 "No" ) 来表示这种字段数据. <14> SlugField "Slug" 是一个报纸术语. slug 是某个东西的小小标记(短签), 只包含字母,数字,下划线和连字符.#它们通常用于URLs 若你使用 Django 开发版本,你可以指定 maxlength. 若 maxlength 未指定, Django 会使用默认长度: 50. #在 以前的 Django 版本,没有任何办法改变50 这个长度. 这暗示了 db_index=True. 它接受一个额外的参数: prepopulate_from, which is a list of fields from which to auto-#populate the slug, via JavaScript,in the object's admin form: models.SlugField (prepopulate_from=("pre_name", "name"))prepopulate_from 不接受 DateTimeFields. <13> XMLField 一个校验值是否为合法XML的 TextField,必须提供参数: schema_path, 它是一个用来校验文本的 RelaxNG schema #的文件系统路径. <14> FilePathField 可选项目为某个特定目录下的文件名. 支持三个特殊的参数, 其中第一个是必须提供的. 参数 描述 path 必需参数. 一个目录的绝对文件系统路径. FilePathField 据此得到可选项目. Example: "/home/images". match 可选参数. 一个正则表达式, 作为一个字符串, FilePathField 将使用它过滤文件名. 注意这个正则表达式只会应用到 base filename 而不是 路径全名. Example: "foo.*\.txt^", 将匹配文件 foo23.txt 却不匹配 bar.txt 或 foo23.gif. recursive可选参数.要么 True 要么 False. 默认值是 False. 是否包括 path 下面的全部子目录. 这三个参数可以同时使用. match 仅应用于 base filename, 而不是路径全名. 那么,这个例子: FilePathField(path="/home/images", match="foo.*", recursive=True) ...会匹配 /home/images/foo.gif 而不匹配 /home/images/foo/bar.gif <15> IPAddressField 一个字符串形式的 IP 地址, (i.e. "24.124.1.30"). <16> CommaSeparatedIntegerField 用于存放逗号分隔的整数值. 类似 CharField, 必须要有maxlength参数.
更多参数:
(1)null 如果为True,Django 将用NULL 来在数据库中存储空值。 默认值是 False. (1)blank 如果为True,该字段允许不填。默认为False。 要注意,这与 null 不同。null纯粹是数据库范畴的,而 blank 是数据验证范畴的。 如果一个字段的blank=True,表单的验证将允许该字段是空值。如果字段的blank=False,该字段就是必填的。 (2)default 字段的默认值。可以是一个值或者可调用对象。如果可调用 ,每有新对象被创建它都会被调用,如果你的字段没有设置可以为空,那么将来如果我们后添加一个字段,这个字段就要给一个default值 (3)primary_key 如果为True,那么这个字段就是模型的主键。如果你没有指定任何一个字段的primary_key=True, Django 就会自动添加一个IntegerField字段做为主键,所以除非你想覆盖默认的主键行为, 否则没必要设置任何一个字段的primary_key=True。 (4)unique 如果该值设置为 True, 这个数据字段的值在整张表中必须是唯一的 (5)choices 由二元组组成的一个可迭代对象(例如,列表或元组),用来给字段提供选择项。 如果设置了choices ,默认的表单将是一个选择框而不是标准的文本框,<br>而且这个选择框的选项就是choices 中的选项。 (6)db_index 如果db_index=True 则代表着为此字段设置数据库索引。 DatetimeField、DateField、TimeField这个三个时间字段,都可以设置如下属性。 (7)auto_now_add 配置auto_now_add=True,创建数据记录的时候会把当前时间添加到数据库。 (8)auto_now 配置上auto_now=True,每次更新数据记录的时候会更新该字段,标识这条记录最后一次的修改时间。
orm与实际的sql对应关系:
'AutoField': 'integer AUTO_INCREMENT', 'BigAutoField': 'bigint AUTO_INCREMENT', 'BinaryField': 'longblob', 'BooleanField': 'bool', 'CharField': 'varchar(%(max_length)s)', 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', 'DateField': 'date', 'DateTimeField': 'datetime', 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', 'DurationField': 'bigint', 'FileField': 'varchar(%(max_length)s)', 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'double precision', 'IntegerField': 'integer', 'BigIntegerField': 'bigint', 'IPAddressField': 'char(15)', 'GenericIPAddressField': 'char(39)', 'NullBooleanField': 'bool', 'OneToOneField': 'integer', 'PositiveIntegerField': 'integer UNSIGNED', 'PositiveSmallIntegerField': 'smallint UNSIGNED', 'SlugField': 'varchar(%(max_length)s)', 'SmallIntegerField': 'smallint', 'TextField': 'longtext', 'TimeField': 'time', 'UUIDField': 'char(32)',
我们进行删除表结构的操作:
修改models.py的表结构:
class UserInfo(models.Model):
"""
create table userinfo(
id int primary key auto_increment,
name varchar(16),
age int);
"""
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=16)
age = models.IntegerField()
将数据库中ormtest所有表删除,将migrations目录下面的临时脚本文件删除,重新输入同步数据库指令。
上面属于暴力删除,项目中绝对不能使用!!
orm与MySQL中的对应关系;
我们建立一个流程:
path('index/', views.index),
from django.shortcuts import render, HttpResponse
from app01 import models
# Create your views here.
def index(request):
return HttpResponse('操作成功')
ret = models.UserInfo(name='新月', age='21')
ret.save()
通过objects控制器去调用增删改查等系列方法。
# 增加并返回此对象
obj = models.UserInfo.objects.create(name='明天', age=32)
print(obj) # 他是新增的这条记录对应的orm中的表的实例化的对象。
# 我们统一称之为 model对象。
model对象:泛指models.py模块中所有的表的实例化的对象。就是对应的一条行记录。
# 增加并返回此对象
obj = models.UserInfo.objects.create(name='老舅', age=36)
print(obj) # 他是新增的这条记录对应的orm中的表的实例化的对象。
# 我们统一称之为 model对象。
print(obj.name)
我们每次打印对象,显示的都是一个名词,UserInfo object ,我们区分不出来这个对象具体是谁,所以我们需要通过打印对象.name 显示这个条数据,能否让我直接打印对象,显示它对应的名字。
class UserInfo(models.Model):
"""
create table userinfo(
id int primary key auto_increment,
name varchar(16),
age int);
"""
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=16)
age = models.IntegerField()
def __str__(self):
return self.name
批量增加,我们是否可以一下增加很多的数据。
lst = [models.UserInfo(name=f'高宁{i}', age=20 + i) for i in range(1, 21)]
models.UserInfo.objects.bulk_create(lst)
更新或者增加
models.UserInfo.objects.update_or_create(
name='高宁1',
defaults={
"age": 23,
}
)
models.UserInfo.objects.update_or_create(
name='世举',
defaults={
"age": 23,
}
)
all = models.UserInfo.objects.all()
print(all) # queryset对象
# QuerySet对象类似于一个列表,里面承载的就是一个一个model对象。
# 可以循环
for i in all:
print(i) # model对象
# 可以索引与切片
print(all[0])
print(all[:3])
QuerySet对象类似于一个列表,里面承载的就是一个一个model对象,QuerySet对象可以调用很多的查询方法
按照条件查询,返回的也是QuerySet对象
qs = models.UserInfo.objects.filter(age=23)
print(qs)
按照条件查询,返回的是一个model对象
这个get比较特殊,它返回的是model对象,通过get条件查询,查询的结果有且只有1个。
如果超过一个则报错为:get() returned more than one Student – it returned 2(有几个显示几个)!
如果没有则报错为:Student matching query does not exist.
obj = models.UserInfo.objects.get(id=1)
print(obj)
obj = models.UserInfo.objects.get(age=23)
obj = models.UserInfo.objects.get(id=50)
delete方法
通过model对象删除
obj = models.UserInfo.objects.get(id=5).delete()
print(obj) # (1, {'app01.UserInfo': 1}) 返回删除哪张表的几个记录。
通过QuerySet对象删除
obj = models.UserInfo.objects.filter(age=23).delete()
print(obj) # (2, {'app01.UserInfo': 2})
更新记录的方法只有一个就是update,此方法只能用于QuerySet集合,model对象是不可以使用的。也就是说update可以进行批量更新的操作,并且他会返回一个更新的行记录数量的返回值。
obj = models.UserInfo.objects.filter(age=22).update(
name='树宏',
age=23,
)
print(obj)
通过object控制器调用,返回QuerySet对象
all = models.UserInfo.objects.all()
print(all) # queryset对象
# QuerySet对象类似于一个列表,里面承载的就是一个一个model对象。
# 可以循环
for i in all:
print(i) # model对象
# 可以索引与切片
print(all[0])
print(all[:3])
QuerySet对象类似于一个列表,里面承载的就是一个一个model对象,QuerySet对象可以调用很多的查询方法
通过object控制器调用,返回QuerySet对象
按照条件查询
qs = models.UserInfo.objects.filter(age=23)
print(qs)
多条件查询
# 逗号是并的关系
obj = models.UserInfo.objects.filter(age=24,name='树宏')
print(obj)
obj = models.UserInfo.objects.filter(**{"age":24,"name":'树宏'})
print(obj) # <QuerySet [<UserInfo: 树宏>]>
通过object控制器调用,返回的是一个model对象
这个get比较特殊,它返回的是model对象,通过get条件查询,查询的结果有且只有1个。
如果超过一个则报错为:get() returned more than one Student – it returned 2(有几个显示几个)!
如果没有则报错为:Student matching query does not exist.
obj = models.UserInfo.objects.get(id=1)
print(obj)
obj = models.UserInfo.objects.get(age=23)
obj = models.UserInfo.objects.get(id=50)
通过object对象或者QuerySet集合调用,返回QuserySet集合。
obj = models.UserInfo.objects.filter(age=23).exclude(name='明天')
print(obj)
通过object对象或者QuerySet集合调用,返回QuserySet集合。
# object控制器调用
# qs = models.UserInfo.objects.order_by('age') # 所有的记录都排序
# print(qs)
# QuerySet调用
# qs = models.UserInfo.objects.filter(id__gt=3).order_by('-age') # id大于3的所有的记录
# print(qs)
# 条件排序
qs = models.UserInfo.objects.order_by('age', '-id') # 按照年龄从小到大,相同年龄的id从大到小
print(qs)
通过order_by返回的QuerySet集合调用,返回一个QuerySet集合。
qs = models.UserInfo.objects.order_by('age').reverse()
print(qs)
# 只能通过order_by返回的querySe进行反转
qs = models.UserInfo.objects.all().reverse()
print(qs)
qs = models.UserInfo.objects.filter(id__gt=4).reverse()
print(qs)
通过QuerySet集合调用,返回一个元素个数。
res = models.UserInfo.objects.all().count()
print(res)
qs = models.UserInfo.objects.all()
print(len(qs))
通过QuerySet集合调用,返回第一个model对象
obj = models.UserInfo.objects.filter(id__gt=3).first()
print(obj)
通过QuerySet集合调用,返回最后一个model对象
obj = models.UserInfo.objects.filter(id__gt=3).last()
print(obj)
通过QuerySet集合调用,返回bool值
bl = models.UserInfo.objects.filter(age=50).exists()
print(bl)
我们也可以通过配置settings,每次执行orm语句时都将相应的sql语句展示出来,这样可以查看一下exists().
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', }, } }
通过QuerySet集合调用,返回一个QuerySet集合
但是这个QuerySet集合比较特殊,这个里面的元素不是model对象,而是元组的形式。
# qs = models.UserInfo.objects.filter(age=23)
# print(qs)
#
# qs = models.UserInfo.objects.filter(age=23).values_list()
# print(qs) # <QuerySet [(2, '树宏', 23), (3, '明天', 23), (6, '树宏', 23), (8, '树宏', 23), (10, '树宏', 23)]>
#
#指定字段查询
qs = models.UserInfo.objects.filter(age=23).values_list('name', 'age')
print(qs) # <QuerySet [('树宏', 23), ('明天', 23), ('树宏', 23), ('树宏', 23), ('树宏', 23)]>
通过QuerySet集合调用,返回一个QuerySet集合
但是这个QuerySet集合比较特殊,这个里面的元素不是model对象,而是字典的形式。
qs = models.UserInfo.objects.filter(id__gt=3).values()
print(qs)
qs = models.UserInfo.objects.filter(id__gt=3).values('id', 'name')
print(qs)
通过QuerySet集合调用,返回一个QuerySet集合
这里要注意一点:无论是all还是filter 你对整个对象去重是没有意义的,只要有一个字段不同,都不是重复的。
# 对queryset集合中的每个model对象的所有字段进行去重。
# qs = models.UserInfo.objects.all().distinct()
# print(qs)
# 所以我们要想使用去重,先做一个筛选
# 查询userinfo表中所有的年龄段(去重)
qs = models.UserInfo.objects.all().values('age').distinct()
print(qs)
我们在使用filter方法时,一直在使用 = 条件,但是没有使用过> < >=等条件,这是因为ORM不支持这种写法,不用着急,我们可以根据另一种写法去实现。
query_objs = models.Student.objects.filter(age__gt=19) # 大于
query_objs = models.Student.objects.filter(age__gte=19) # 大于等于
query_objs = models.Student.objects.filter(age__lt=20) # 小于
query_objs = models.Student.objects.filter(age__lte=20) # 小于等于
query_objs = models.Student.objects.filter(age__range=[18, 20]) # 范围 左右都包含
query_objs = models.Student.objects.filter(name__contains='xiao') # 针对字符串类型,内容含有
query_objs = models.Student.objects.filter(name__icontains='xiao') # 针对字符串类型,内容含有 不区分大小写
query_objs = models.Student.objects.filter(name__startswith='x') # 匹配以x开头
query_objs = models.Student.objects.filter(name__istartswith='x') # 匹配以x开头,不区分大小写
query_objs = models.Student.objects.filter(name__endswith='o') # 匹配以x结尾
query_objs = models.Student.objects.filter(name__iendswith='o') # 匹配以x结尾,不区分大小写
日期这个字段查询起来比较特殊,我们单独拿出来讲解,现在我们要重新创建一个表。
class Birthday(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=16)
date = models.DateField()
def __str__(self):
return self.name
同步数据库指令。
插入一些虚拟数据:
import datetime
models.Birthday.objects.create(name='太白1', date=datetime.datetime.now())
models.Birthday.objects.create(name='太白2', date='2015-04-25')
models.Birthday.objects.create(name='太白3', date='2010-06-25')
models.Birthday.objects.create(name='太白4', date='2022-08-26')
models.Birthday.objects.create(name='太白5', date='2018-12-28')
models.Birthday.objects.create(name='太白6', date='2001-03-26')
models.Birthday.objects.create(name='太白7', date='2001-08-30')
models.Birthday.objects.create(name='太白8', date='2003-01-13')
models.Birthday.objects.create(name='太白9', date='2005-10-01')
测试:
# 查询2003年出生的人 # qs = models.Birthday.objects.filter(date__startswith='2003') 可以 # print(qs) # qs = models.Birthday.objects.filter(date__year='2005') # print(qs) # 查询 1月份出生的人 # qs = models.Birthday.objects.filter(date__month='01') # print(qs) # 查询 2010年6月份25日出生的人 # qs = models.Birthday.objects.filter(date__year='2010',date__month='06',date__day='25') # print(qs) # 查询 2022年6月份以后12月份之前出生的人 qs = models.Birthday.objects.filter(date__year='2022', date__month__gte='06', date__month__lte='11') print(qs)
一对一: 左表唯一一条记录唯一对应右表的一条记录 foreign key + unique
多对一: 单向多对一
多对多: 双向多对一
创建一个项目manytable项目,
创建model模型:
from django.db import models # Create your models here. class Author(models.Model): # 比较常用的信息放到这个表里面 name = models.CharField(max_length=32) age = models.IntegerField() # 与AuthorDetail建立一对一的关系,一对一的这个关系字段写在两个表的任意一个表里面都可以 authorDetail = models.OneToOneField(to="AuthorDetail", on_delete=models.CASCADE) # 就是foreignkey+unique,只不过不需要我们自己来写参数了,并且orm会自动帮你给这个字段名字拼上一个_id,数据库中字段名称为authorDetail_id class AuthorDetail(models.Model): # 不常用的放到这个表里面 birthday = models.DateField() telephone = models.CharField(max_length=11) addr = models.CharField(max_length=64) class Publish(models.Model): name = models.CharField(max_length=32) city = models.CharField(max_length=32) email = models.EmailField() · # 多对多的表关系,我们学mysql的时候是怎么建立的,是不是手动创建一个第三张表,然后写上两个字段,每个字段外键关联到另外两张多对多关系的表,orm的manytomany自动帮我们创建第三张表,两种方式建立关系都可以,以后的学习我们暂时用orm自动创建的第三张表,因为手动创建的第三张表我们进行orm操作的时候,很多关于多对多关系的表之间的orm语句方法无法使用#如果你想删除某张表,你只需要将这个表注销掉,然后执行那两个数据库同步指令就可以了,自动就删除了。 class Book(models.Model): title = models.CharField(max_length=32) publishDate = models.DateField() price = models.DecimalField(max_digits=5, decimal_places=2) # 与Publish建立一对多的关系,外键字段建立在多的一方,字段publish如果是外键字段,那么它自动是int类型 publish = models.ForeignKey(to=Publish,on_delete=models.CASCADE) # foreignkey里面可以加很多的参数,都是需要咱们学习的,慢慢来,to指向表,to_field指向你关联的字段,不写这个,默认会自动关联主键字段,on_delete级联删除 字段名称不需要写成publish_id,orm在翻译foreignkey的时候会自动给你这个字段拼上一个_id,这个字段名称在数据库里面就自动变成了publish_id # 与Author表建立多对多的关系,ManyToManyField可以建在两个模型中的任意一个,自动创建第三张表,并且注意一点,你查看book表的时候,你看不到这个字段,因为这个字段就是创建第三张表的意思,不是创建字段的意思,所以只能说这个book类里面有authors这个字段属性 authors = models.ManyToManyField(to='Author', ) # 注意不管是一对多还是多对多,写to这个参数的时候,最后后面的值是个字符串,不然你就需要将你要关联的那个表放到这个表的上面
一对一关系
多对一关系
现实场景中,书籍与出版社是多对多的关系,但是我们为了研究三种关系,我们这里定义书籍与出版社为多对一的关系,多本书可以对应一个出版社,多个出版社不能对应一本书。
多对多的关系
作者与书籍属于多对多的关系,多本书可以对应一个作者,多个作者可以对应一本书。
建立多对多的字段
MySQL中,建立多对多的关系是手动创建第三张表,
ORM中有两种方式创建第三张表:
方式一: 依靠一个字段ManyToManyField,orm会自动的帮你创建第三张关系表。这个第三张表可以使用orm提供的方法,但是只能包含三个字段id,外键关联book表,外键关联作者表。
方式二: 我们可以手动创建一个类,建立关系。可以添加多个字段,但是有些orm的方法使用不了。
class BookAuthor(models.Model):
book_id = models.ForeignKey(to=Book,on_delete=models.CASCADE)
author_id = models.ForeignKey(to=Author,on_delete=models.CASCADE)
一般我们采用方式一居多。
创建一个manytables数据库,配置链接这个数据库
mysql> create database manytables; Query OK, 1 row affected (0.00 sec) __init__文件: import pymysql pymysql.install_as_MySQLdb() DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'manytables', 'USER': 'root', 'PASSWORD': '123', 'HOST': '127.0.0.1', 'PORT': 3306 } }
同步数据库指令:
python manage.py makemigrations
python manage.py migrate
自动帮我们建立了id字段:
mysql> desc app01_author;
+-----------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(32) | NO | | NULL | |
| age | int(11) | NO | | NULL | |
| authorDetail_id | int(11) | NO | UNI | NULL | |
+-----------------+-------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)
其中涉及到的关系字段,Author.authorDetail
,Book.publish
字段,在生成具体的表数据时,自动拼接了_id
字符串。
引子
我们给具体的库中的表增加数据有几种大的方式:
什么是admin?翻译过来就是管理员,在django中我们代指后台管理系统,实际上就是对项目中的数据库中的数据进行的增删改查配置了可视化的工具。
我们创建一个admin用户:
将项目中需要管理的表配置到admin中:
app01/admin.py:
from django.contrib import admin
from app01 import models
# Register your models here.
admin.site.register(models.Author)
admin.site.register(models.AuthorDetail)
admin.site.register(models.Book)
admin.site.register(models.Publish)
登录admin数据库可视化工具:
http://127.0.0.1:8000/admin/login/
我们增加一些虚拟数据:
自己增加。
# 字段--->对象 # AuthorDetail = models.AuthorDetail.objects.create( # birthday='2000-01-12', # telephone='15612254367', # addr='定州' # ) # # models.Author.objects.create( # name='旋哥', # age=23, # authorDetail=AuthorDetail # ) # 字段_id ----> 对象id AuthorDetail = models.AuthorDetail.objects.create( birthday='2002-03-14', telephone='13812254367', addr='邯郸' ) models.Author.objects.create( name='改博', age=20, authorDetail_id=AuthorDetail.id )
多对一增加的方式与一对一相同。
# 字段--->对象 publish = models.Publish.objects.create( name='蒲公英出版社', city='保定', email='123123@qq.com', ) models.Book.objects.create( title='平凡的世界', publishDate='1984-02-22', price=129.89, publish=publish ) # 字段_id ----> 对象id publish = models.Publish.objects.get(pk=1) models.Book.objects.create( title='活着', publishDate='1988-04-22', price=119.89, publish_id=publish.id )
实际上是对第三张表增加数据,因为我们的第三张表是在book表中的authors=manytomanyfield()字段自动生成的,所以,我们要通过book对象调用authors字段给第三张表添加关系。
# 方式一
# 给平凡的世界这本书绑定旋哥和改博这两个作者
# book_obj = models.Book.objects.filter(title='平凡的世界').first()
# author1 = models.Author.objects.filter(name='旋哥')[0]
# author2 = models.Author.objects.filter(name='改博')[0]
# book_obj.authors.add(author1, author2) # *[author1, author2]
# 方式二
book_obj = models.Book.objects.filter(title='活着').first()
book_obj.authors.add(2, 3)
由于我们在author表中添加了级联删除。
# 删除id为4的作者,他的详情表不会删除,但是作者与书籍的第三章关系表的id=4的记录删除了
models.Author.objects.get(pk=4).delete()
# 删除id为5的作者详情记录,作者会级联删除。但是作者与书籍的第三章关系表的id=5的记录删除了
models.AuthorDetail.objects.get(pk=5).delete()
# 删除活着这本书,对应的大湿出版社不会删除
# models.Book.objects.get(pk=4).delete()
# 删除大湿出版社, 对应的书籍全部级联删除
models.Publish.objects.get(pk=1).delete()
我们删除的是第三张表的关系记录。
我们单独删除作者,第三张表的有关这个作者的对应的书籍的对应记录级联删除了;
我们单独删除书籍,第三张表的有关这个书籍的对应的作者的对应记录级联删除了。
# 删除平凡的世界对应的作者
# book_obj = models.Book.objects.get(pk=3)
# book_obj.authors.remove(3)
# 删除三体对应的两个作者1, 2
# book_obj = models.Book.objects.get(pk=6)
# book_obj.authors.remove(1,2)
# 删除鬼吹灯这本书的第三张表所有的作者记录
book_obj = models.Book.objects.get(pk=7)
book_obj.authors.clear()
# 将barry的详情改成id=3的定州
# 字段 --->对象
# author = models.Author.objects.filter(id=1)
# author.update(
# authorDetail=models.AuthorDetail.objects.get(pk=3)
# )
# 字段_id ----> 对象id
models.Author.objects.filter(id=2).update(
authorDetail_id=8
)
多对一与一对一相同。
#多对一更新
#与一对一原理相同
#方式1:常用
#将书籍id为6的对应的出版社id改为3
models.Book.objects.filter(id=6).update(
publish_id=3
)
#方式2:通过对象映射
#将书籍id为5的对应的出版社id为4
publish_obj=models.Publish.objects.get(pk=4)
models.Book.objects.filter(id=5).update(
publish=publish_obj
)
实际上是对第三张表修改数据,因为我们的第三张表是在book表中的authors=manytomanyfield()字段自动生成的,所以,我们要通过book对象调用authors字段给第三张表添加关系。
book_obj = models.Book.objects.get(pk=5)
book_obj.authors.set([2, 3])
正向查询
# 正向查询
# 查询飘哥的电话
# 对象.关联字段
# obj = models.Author.objects.get(pk=2)
# print(obj.authorDetail)
# print(obj.authorDetail.telephone)
反向查询
# 反向查询
# 查询15612254367电话对应的作者名
# 对象.小写类名
obj = models.AuthorDetail.objects.get(telephone='15612254367')
print(obj.author.name)
book与publish多对一的关系,关联字段在book表中。
正向查询
# 查询三体的出版社的名字
# 对象.小写类名
book_obj = models.Book.objects.get(title='三体')
print(book_obj.publish.name)
反向查询
# 查询网络出版社出版的所有书籍的名称
# 对象.小写类名_set.all()
publish_obj = models.Publish.objects.get(name='网络出版社')
print(publish_obj.book_set.all()) # <QuerySet [<Book: 伪装者>, <Book: 丁丁历险记>]>
book与author多对多的关系,关联字段在book表中。
正向查询
# 正向查询
# 查询book的id为5这本书对应的作者
# 对象.关联字段.all()
book_obj = models.Book.objects.get(pk=5)
print(book_obj.authors.all()) # <QuerySet [<Author: 飘哥>, <Author: 旋哥>]>
反向查询
# 查询荣轩作者出过的书
# 对象.小写类名_set.all()
obj = models.Author.objects.get(name='荣轩')
print(obj.book_set.all())
查询电话为15612232345的作者名以及出版过哪些书籍?
detail_obj = models.AuthorDetail.objects.get(telephone='15612232345')
print(detail_obj.author.name)
for book_obj in detail_obj.author.book_set.all():
print(book_obj.title)
连表操作:
# 查询barry的电话号码 # filter里面是查询条件,values里面放置的是筛选字段 obj = models.Author.objects.filter(name='barry').values('name', 'authorDetail__telephone') print(obj) # 查询barry的电话号码 obj = models.AuthorDetail.objects.filter(author__name='barry').values('telephone') print(obj) # 查询17612232134电话的作者的姓名 obj = models.AuthorDetail.objects.filter(telephone="17612232134").values('author__name') print(obj) obj = models.Author.objects.filter(authorDetail__telephone='17612232134').values('name') print(obj)
正向查询与反向查询对于双下滑线连表的操作无意义,如果已知条件字段或者要查询的字段属于直接查询的表,则直接使用,否则进行双下连接使用。
# 查询鬼吹灯这本书的出版社的名字。
obj = models.Book.objects.filter(title='鬼吹灯').values('publish__name')
print(obj)
obj = models.Publish.objects.filter(book__title='鬼吹灯').values('name')
print(obj)
book与author多对多的关系。这就涉及到三张表的查询。
# 查询青楼梦这本书的所有的作者的名字以及年龄。
obj = models.Book.objects.filter(title='青楼梦').values('authors__name', 'authors__age')
print(obj)
obj = models.Author.objects.filter(book__title='青楼梦').values('name', 'age')
print(obj)
# 查询电话为13582253227的作者名,以及出版过的书籍名称。
# 这个需求涉及到author表,authordetail表,book表。我们从哪张表开始都可以查询出来。
# 查询电话为13582253227的作者名,以及出版过的书籍名称。
obj = models.AuthorDetail.objects.filter(telephone='13582253227').values('author__name', 'author__book__title')
print(obj)
obj1 = models.Author.objects.filter(authorDetail__telephone='13582253227').values('name', 'book__title')
print(obj1)
obj2 = models.Book.objects.filter(authors__authorDetail__telephone='13582253227').values('authors__name', 'title')
print(obj2)
这个是orm表中,关联关系字段的一个属性,这个属性可以优化我们反向查询的语句。
我们这里以多对一为例,(一对一、多对多同样适用)测试:
class Book(models.Model):
......
publish = models.ForeignKey(to=Publish, on_delete=models.CASCADE,related_name='xxx')
修改了model模型中字段属性,需要同步数据库指令。
基于对象的连表查询
# 查询三体这本书的出版社的名字
# 正向不影响
# obj = models.Book.objects.get(title='三体')
# print(obj.publish.name)
# 查询蒲公英出版社出版过的书的名字
# 反向查询
obj = models.Publish.objects.get(name='蒲公英出版社')
print(obj.xxx.all()) # 由原先的小写类名_set (book_set) 变成了xxx
基于双下滑线的连表查询
# 查询三体这本书的出版社的名字
# 正常查询不影响
# obj = models.Book.objects.filter(title='三体').values('publish__name')
# print(obj)
# 反向查询 由类名__字段 变成了 xxx__字段
obj = models.Publish.objects.filter(xxx__title='三体').values('name')
print(obj)
我们可以引用一些聚合函数:max min avg sum count 我们要基于一个aggregate方法使用这些聚合函数。
from django.db.models import Min, Max, Sum, Avg, Count
# 计算所有书籍的平均价格
# res = models.Book.objects.all().aggregate(Avg('price'))
# print(res)
res = models.Book.objects.all().aggregate(Avg('price'), Max('price'), Min('price'))
print(res)
相关练习题:(写多种方式查询)
查询哪个老师的电话为17812312321。
obj = models.AuthorDetail.objects.filter(telephone='17812312321').values('author__name')
print(obj)
authordetail_obj = models.AuthorDetail.objects.get(telephone='17812312321')
print(authordetail_obj.author.name)
查询年龄为21岁的所有作者的姓名以及对应的家庭地址。
obj = models.Author.objects.filter(age=21).values('name', 'authorDetail__addr')
print(obj)
author_obj = models.Author.objects.filter(age=21)
for author_set_obj in author_obj:
print(author_set_obj, author_set_obj.authorDetail.addr)
查询正能量出版社出版的所有的书的名字以及价格。
obj = models.Publish.objects.filter(name='正能量出版社').values('xxx__title', 'xxx__price')
print(obj)
publish_obj = models.Publish.objects.get(name='正能量出版社')
book_obj = models.Book.objects.filter(publish=publish_obj.id)
for book_obj_set in book_obj:
print(book_obj_set, book_obj_set.price)
查询旋哥作者出版过的所有的书籍名以及价格。
obj = models.Book.objects.filter(authors__name='旋哥').values('title', 'price')
print(obj)
author_obj = models.Author.objects.get(name='旋哥')
for author_obj_set in author_obj.book_set.all():
print(author_obj_set.title, author_obj_set.price)
查询三体这本书的对应的作者名以及电话。
obj = models.Book.objects.filter(title='三体').values('authors__name', 'authors__authorDetail__telephone')
print(obj)
book_obj = models.Book.objects.get(title='三体')
author_obj = book_obj.authors.all()
print(author_obj[0], author_obj[0].authorDetail.telephone)
查询蒲公英出版社出版过的所有书籍名称以及作者名字以及作者电话。
obj = models.Publish.objects.filter(name='蒲公英出版社').values('xxx__title', 'xxx__authors__name', 'xxx__authors__authorDetail__telephone')
print(obj)
publish_obj = models.Publish.objects.get(name='蒲公英出版社')
book_obj = models.Book.objects.filter(publish=publish_obj.id)
for book_obj_set in book_obj:
# print(book_obj_set.authors.all())
for book_obj_set2 in book_obj_set.authors.all():
print(book_obj_set2)
mysql中分组查询使用group by方法。
# 求出每个出版社出版所有书籍的平均价格。
select avg(price) from book group by publish_id;
对于我们orm来说,它的分组查询有两种方法:
values 充当group by的作用。
from django.db.models import Min, Max, Sum, Avg, Count
# values充当的是group by的作用
# res = models.Book.objects.values('publish_id').annotate(avg_price=Avg('price'))
# print(res)
# 分组之后还可以进行筛选 fliter 替代 having方法
# 求出每个出版社出版所有书籍的平均价格大于200的出版社。
res = models.Book.objects.values('publish_id').annotate(avg_price=Avg('price')).filter(avg_price__gt=200)
print(res)
# 求出每个出版社出版所有书籍的平均价格。
# res = models.Publish.objects.annotate(a=Avg('xxx__price'))
# print(res) # 出版社对象:<QuerySet [<Publish: 蒲公英出版社>, <Publish: 正能量出版社>, <Publish: 网络出版社>, <Publish: 星光出版社>]>
# res = models.Publish.objects.annotate(a=Avg('xxx__price')).values('name',"a")
# # print(res)
# 分组完毕之后,还可以过滤
res = models.Publish.objects.annotate(a=Avg('xxx__price')).values('name',"a").filter(a__gt=200)
print(res)
F查询一般用于单表的单个字段批量操作或者字段与字段之间的比较操作。
我们修改一下表结构:
我们给已经有数据的表新增字段时,如果改字段不设置默认值或者null=True的属性,是不运行你进行新增操作的。
class Book(models.Model):
......
goods = models.IntegerField(default=1)
comment = models.IntegerField(default=1)
同步数据库指令操作。
测试:
# 给所有书籍的价格在原基础上增加20 # 不加F查询做不了 # res = models.Book.objects.filter(id__gte=1).update( # price=price + 20 # ) models.Book.objects.all().update( price=F('price') + 20, ) # 评论数比点赞数多的所有书籍 res = models.Book.objects.filter(comment__gt=F('goods')) print(res) # 评论数比点赞数多20以上的书籍 res = models.Book.objects.filter(comment__gt=F('goods') + 20) print(res)
支持逻辑运算以及嵌套的逻辑运算。
# 查询 评论数大于80且点赞数大于80的书籍 # res = models.Book.objects.filter(comment__gt=80, goods__gt=80) # print(res) # # res = models.Book.objects.filter(Q(comment__gt=80) & Q(goods__gt=80)) # print(res) # 查询 评论数大于80或点赞数大于80的书籍 res = models.Book.objects.filter(Q(comment__gt=80) | Q(goods__gt=80)) print(res) # 查询 评论数小于等于80或点赞数大于80的书籍 res = models.Book.objects.filter(Q(comment__gt=80) | Q(goods__gt=80)) print(res) # 查询 价格大于200的,评论数或者点赞数大于100的书籍 res = models.Book.objects.filter(Q(price__gt=200) & Q(Q(comment__gt=100) | Q(goods__gt=100))) print(res)
orm不足以满足所有的sql语句的转化,有时候我们需要再项目中执行原生sql语句。
方式一:我们使用哪张表作为主体,只能植入此表的sql语句。不方便。
# res = models.Book.objects.raw('select * from app01_book')
# # print(res)
# for i in res:
# # print(i)
# print(i.title, i.price)
# 不行
# res = models.Book.objects.raw(
# 'select app01_book.title,app01_publish.name from app01_book inner join app01_publish on (app01_book.publish_id = app01_publish.id)')
# for i in res:
# print(i)
方式二:
有时候raw()方法并不十分好用,很多情况下我们不需要将查询结果映射成模型,或者我们需要执行DELETE、 INSERT以及UPDATE操作。在这些情况下,我们可以直接访问数据库,完全避开模型层。我们可以直接从django提供的接口中获取数据库连接,然后像使用pymysql模块一样操作数据库。
from django.db import connection, connections
cursor = connection.cursor()
cursor.execute('select app01_book.title,app01_publish.name from app01_book inner join app01_publish on (app01_book.publish_id = app01_publish.id)')
print(cursor.fetchone())
1. 查询每个作者的姓名以及出版的书的最高价格
ret = models.Author.objects.values("name").annotate(m=Max("book__price")).values("name", "m")
print(ret)
2. 查询作者id大于2的作者的姓名以及出版的书的最高价格
res = models.Author.objects.filter(id__gt=2).annotate(m=Max('book__price')).values('name','m')
print(res)
3. 查询作业id大于2或者作者年龄大于等于20岁的女作者的姓名以及出版的书的最高价格
res = models.Author.objects.filter(Q(sex=="女")&(Q(id__gt=2) | Q(age__gte=20))).annotate(m=Max('book__price')).values('name', 'm')
print(res)
4. 查询每个作者出版的书的最高价格的平均值
res = models.Author.objects.all().annotate(m=Max('book__price')).aggregate(m2=Avg('m'))
print(res)
5. 查询每个作者出版的所有书的价格以及最高价格的那本书的名称。 !!!!
基于单表的那种形式进行构写。
查询:
显示:序号,书籍名称,书籍价格,出版日期,出版社,所有作者名字, 操作。
删除:
删除一本书,删除此本书的记录以及书籍与作者的第三张表的对应的关系记录。
增加:
出版社单选下拉框,作者:多选下拉框。
修改:将原书籍的所有内容显示到对应的input 或者select框中,作者以及出版社也是默认原选中的内容。
orm锁实际上是你设置的MySQL数据库决定的,由于MySQL数据库使用的默认引擎是innodb,所以,对MySQL来说,他是行级锁定,查询使用的是共享锁,但是增删改使用的是排它锁。这个锁是MySQL自带的,我们不用手动设置。
我们使用表级锁定的方式,做一个演示:
# 添加表级锁
lock table app01_publish write;
# 解锁
unlock tables;
什么是事务?
事务就是一组逻辑上的sql语句,要不全部执行成功,要不全部执行失败。
在Web应用中,常用的事务处理方式是将每个请求都包裹在一个事务中。这个功能使用起来非常简单,你只需要将它的配置项ATOMIC_REQUESTS设置为True。
它是这样工作的:当有请求过来时,Django会在调用视图方法前开启一个事务。如果请求却正确处理并正确返回了结果,Django就会提交该事务。否则,Django会回滚该事务。
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'mxshop', 'HOST': '127.0.0.1', 'PORT': '3306', 'USER': 'root', 'PASSWORD': '123', 'OPTIONS': { "init_command": "SET default_storage_engine='INNODB'", #'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", #配置开启严格sql模式 } "ATOMIC_REQUESTS": True, #全局开启事务,绑定的是http请求响应整个过程 "AUTOCOMMIT":False, #全局取消自动提交,慎用 }, 'other':{ 'ENGINE': 'django.db.backends.mysql', ...... } #还可以配置其他数据库
全局事务是给项目中的所有的视图函数中的所有的orm语句(除去查询语句之外)全部加上事务。一般不用。
视图函数加事务(用的不多)
from django.db import transaction
@transaction.atomic
def viewfunc(request):
# This code executes inside a transaction.
do_stuff()
with语句加事务(经常使用)
from django.db import transaction
def viewfunc(request):
# This code executes in autocommit mode (Django's default).
do_stuff()
with transaction.atomic(): #保存点
# This code executes inside a transaction.
do_more_stuff()
do_other_stuff()
引子
前端向后端发送数据有三种方式:
什么是ajax?
ajax是js实现的一个用于前端向后端发送数据并且可以接收到后端返回的数据的一个重要功能。jQuery对ajax做了进一步封装,更加好用的,简便了。一般网站的登录页面使用的都是ajax功能。
ajax不会刷新整个页面,我们使用ajax它实现的功能:异步请求,局部刷新。
局部刷新:
form表单属于全局刷新,我们使用form表单提交数据时,点击提交整个页面会刷新,再次向后端发送请求,IO,效率低,用户体验感不好。ajax就是整个页面不刷新,只是利用js将数据偷偷的发送到后端。(接收到响应可能会在页面局部更改一些内容)
同步请求与异步请求
开启一个项目:ajaxpro。
urlpatterns = [ path('admin/', admin.site.urls), path('formlogin/', views.FormLogin.as_view(),), path('home/', views.home, name='home'), ] from django.views import View # Create your views here. class FormLogin(View): def get(self, request): return render(request, 'formlogin.html') def post(self, request): username = request.POST.get('username') password = request.POST.get('password') if username == 'barry' and password == '123': return redirect('home') else: return HttpResponse('登录失败!') def home(request): return render(request, 'home.html') <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <form action="" method="post"> {% csrf_token %} <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <div><input type="submit"></div> </form> </body> </html>
每次点击提交,无论登录成功与失败全部都要刷新页面。用户体验感不好。
配置静态文件路径:
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'staticfiles'),
]
具体代码
urlpatterns = [ path('admin/', admin.site.urls), path('formlogin/', views.FormLogin.as_view(),), path('ajaxlogin/', views.AjaxLogin.as_view(),), path('home/', views.home, name='home'), ] class AjaxLogin(View): def get(self, request): return render(request, 'ajaxlogin.html') def post(self, request): username = request.POST.get('username') password = request.POST.get('password') if username == 'barry' and password == '123': return redirect('home') else: return HttpResponse('登录失败!') def home(request): return render(request, 'home.html') {% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> {% csrf_token %} <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <div><input type="submit"></div> <script src="{% static 'jQuery.js' %}"></script> <script> $(':submit').click(function () { $.ajax({ url: '/ajaxlogin/', type: 'post', data:{ 'username': $(':text').val(), 'password': $(':password').val(), 'csrfmiddlewaretoken': $('[name="csrfmiddlewaretoken"]').val(), }, success:function (response) { console.log(response) } }) }) </script> </body> </html>
我们回顾一下form表单的作用:
所以,如果我们使用ajax,获取相应数据,根据不同的响应数据通知浏览器做下一步处理。都是我们要自己动手去做。
一般ajax处理的响应数据分一下两种:
基于不同的响应数据做测试:
import json class AjaxLogin(View): def get(self, request): return render(request, 'ajaxlogin.html') def post(self, request): username = request.POST.get('username') password = request.POST.get('password') if username == 'barry' and password == '123': ret = {'redirect': 'http://127.0.0.1:8080/home'} return HttpResponse(json.dumps(ret)) else: return HttpResponse('登录失败!') {% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> {% csrf_token %} <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <div><input type="submit"></div> <script src="{% static 'jQuery.js' %}"></script> <script> $(':submit').click(function () { $.ajax({ url: '/ajaxlogin/', type: 'post', data:{ 'username': $(':text').val(), 'password': $(':password').val(), 'csrfmiddlewaretoken': $('[name="csrfmiddlewaretoken"]').val(), }, success:function (response) { {#console.log(response,typeof response)#} var res = JSON.parse(response); location.href = res['redirect']; } }) }) </script> </body> </html>
作业:
基于ajax做一个用户名密码错误的信息显示(红色显示)。
方式一:
我们上面使用的。
方式二:
$(':submit').click(function () {
$.ajax({
......
data:{
'username': $(':text').val(),
'password': $(':password').val(),
'csrfmiddlewaretoken': "{{ csrf_token }}",
},
......
})
})
方式三(后面在讲)
<script src="{% static 'js/jquery.cookie.js' %}"></script>
$.ajax({
headers:{"X-CSRFToken":$.cookie('csrftoken')},
#其实在ajax里面还有一个参数是headers,自定制请求头,可以将csrf_token加在这里,我们发contenttype类型数据的时候,csrf_token就可以这样加
{% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> {#{% csrf_token %}#} <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <div><input type="submit"></div> <script src="{% static 'jQuery.js' %}"></script> <script> $(':submit').click(function () { $.ajax({ {#url: '/ajaxlogin/',#} url: '{% url "ajaxlogin" %}', type: 'post', data:{ 'username': $(':text').val(), 'password': $(':password').val(), 'csrfmiddlewaretoken': "{{ csrf_token }}", }, success:function (response) { {#console.log(response,typeof response)#} var res = JSON.parse(response); location.href = res['redirect']; } }) }) </script> </body> </html>
项目中,html\css\js都应该是分文件开发的,所以,我们应该将ajax这个功能单独放在一个js文件中。进行构造。
{% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <div><input type="submit"></div> <script src="{% static 'jQuery.js' %}"></script> <script src="{% static 'common.js' %}"></script> <script> </script> </body> </html>
Common.js:
$(':submit').click(function () { $.ajax({ url: '/ajaxlogin/', url: '{% url "ajaxlogin" %}', type: 'post', data: { 'username': $(':text').val(), 'password': $(':password').val(), 'csrfmiddlewaretoken': "{{ csrf_token }}", }, success: function (response) { console.log(response, typeof response) var res = JSON.parse(response); location.href = res['redirect']; } }) })
运行项目我们测试:
Common.js中两个模版语言没有渲染或者渲染错了。
render的机制:
第一次发送get请求,先发送到后端的url -----> AjaxLogin里面的get方法,render将ajaxlogin.html页面进行渲染:
<script src="{% static 'jQuery.js' %}"></script>
<script src="{% static 'common.js' %}"></script>
将上面这两个标签渲染成:
<script src="/static/jQuery.js"></script>
<script src="/static/jQuery.js""></script>
然后将渲染之后的html页面响应给浏览器。一次请求与响应结束。
浏览器获取到这个ajaxlogin.html进行浏览器的渲染,当浏览器读取到
<script src="{% static 'jQuery.js' %}"></script>
<script src="{% static 'common.js' %}"></script>
会分别发送请求,请求具体的js文件:
$(':submit').click(function () { $.ajax({ url: '{% url "ajaxlogin" %}', type: 'post', data: { 'username': $(':text').val(), 'password': $(':password').val(), 'csrfmiddlewaretoken': "{{ csrf_token }}", }, success: function (response) { console.log(response, typeof response) var res = JSON.parse(response); location.href = res['redirect']; } }) })
而这个文件中的路径以及csrf_token都是模版的样式,浏览器渲染不了(应该是django的模版系统的render方法渲染的),所以,这个commom.js中的路径以及csrf_token就是上面的样式。
所以,我们点击post请求触发ajax功能,就会向’{% url “ajaxlogin” %}'错误路径发送信息,请求错误了。
如何解决?
我们django提供的响应的方法:
jsonresponse是返回json类型的字符串,使用这个方法,它的内部就自动的帮我们进行json序列化,无需我们手动在转化。
from django.http import JsonResponse
class AjaxLogin(View):
def get(self, request):
return render(request, 'ajaxlogin.html')
def post(self, request):
username = request.POST.get('username')
password = request.POST.get('password')
if username == 'barry' and password == '123':
ret = {'redirect': 'http://127.0.0.1:8080/home'}
return JsonResponse(ret)
else:
return HttpResponse('登录失败!')
看一下源码:
class JsonResponse(HttpResponse): """ An HTTP response class that consumes data to be serialized to JSON. :param data: Data to be dumped into json. By default only ``dict`` objects are allowed to be passed due to a security flaw before EcmaScript 5. See the ``safe`` parameter for more information. :param encoder: Should be a json encoder class. Defaults to ``django.core.serializers.json.DjangoJSONEncoder``. :param safe: Controls if only ``dict`` objects may be serialized. Defaults to ``True``. :param json_dumps_params: A dictionary of kwargs passed to json.dumps(). """ def __init__(self, data, encoder=DjangoJSONEncoder, safe=True, json_dumps_params=None, **kwargs): if safe and not isinstance(data, dict): raise TypeError( 'In order to allow non-dict objects to be serialized set the ' 'safe parameter to False.' ) if json_dumps_params is None: json_dumps_params = {} kwargs.setdefault('content_type', 'application/json') data = json.dumps(data, cls=encoder, **json_dumps_params) # 源码中给我们进行了json.dumps,然后调用了HttpResponse方法。 super().__init__(content=data, **kwargs)
继续研究,safe参数
path('home/', views.home, name='home'), path('index/', views.index, name='index'), def home(request): return render(request, 'home.html') def index(request): data = ['大钊', '涛哥', '小金', '荣轩', '聪哥'] return JsonResponse(data) {% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <h1>登录成功,欢迎来到首页!</h1> <script src="{% static 'jQuery.js' %}"></script> <script> $.ajax({ url: '{% url 'index' %}', type: 'get', success: function (res) { console.log(res) } }) </script> </body> </html>
会报错:
jsonResponse默认只能序列化字典类型的数据,如果想要序列化其他类型的数据,需要更改safe参数。
def index(request): data = ['大钊', '涛哥', '小金', '荣轩', '聪哥'] return JsonResponse(data, safe=False) {% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <h1>登录成功,欢迎来到首页!</h1> <ul> </ul> <script src="{% static 'jQuery.js' %}"></script> <script> $.ajax({ url: '{% url 'index' %}', type: 'get', success: function (res) { // console.log(res) $.each(res, function (k, v) { var li = '<li>'+ v + '</li>'; $('ul').append(li) }) } }) </script> </body> </html>
请求与响应一般都会携带数据,这个数据有可能是html文件,有可能是字符串,也有可能是json字符串,xml等等。每种数据都有其对应规定的类型,而响应头部以及请求头部规定数据的类型的键:content_type.
基于form表单提交的数据:
一般用于在form表单中,ajax中传递普通的键值对数据,比如用户名、密码等数据。
提交的原始数据:
基于ajax提交的数据:
原始数据:
这些数据提交到django后端:
用于传输文件类型的数据,定义的类型。文件上传时讲解。
我们前端可以构建一个json类型的数据,发送到后端,django框架默认解析不了,必须依赖于DRF框架。
form表单不能向后端发送json类型的数据,ajax可以发送。
def home(request): return render(request, 'home.html') def index(request): print(request.GET) print(request.META) return HttpResponse('ok') {% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <h1>登录成功,欢迎来到首页!</h1> <ul> </ul> <script src="{% static 'jQuery.js' %}"></script> <script> $.ajax({ url: '{% url 'index' %}', type: 'get', contentType: 'application/json', data: JSON.stringify({ "class": "蒲公英四期", "nums": 24, }), success: function (res) { console.log(res) } }) </script> </body> </html>
后端:
通过客户端(浏览器)将文件(普通文件,图片,视频,音频)等文件上传到我们的django后端,django进行接收。
我们构建一个form表单的业务流程:
path('formfile/', views.form_file, name='form_file'), def form_file(request): if request.method == 'GET': return render(request, 'formfile.html') else: # print(request.FILES.get('pic')) # 文件数据 file_obj = request.FILES.get('pic') # print(file_obj,type(file_obj)) # print(file_obj.name) # 字符串形式的文件名 # 方法一 # import os # from ajaxpro import settings # file_path = os.path.join(settings.BASE_DIR, 'staticfiles', 'img', file_obj.name) # with open(file_path, mode='wb') as f1: # for i in file_obj: # f1.write(i) # 方法二 import os from ajaxpro import settings file_path = os.path.join(settings.BASE_DIR, 'staticfiles', 'img', file_obj.name) with open(file_path, mode='wb') as f1: for chunk in file_obj.chunks(): f1.write(chunk) return HttpResponse('上传成功') <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <form action="" method="post" enctype="multipart/form-data"> {% csrf_token %} <div>用户名:<input type="text" name="username"></div> <div><input type="file" name="pic"></div> <div><input type="submit"></div> </form> </body> </html>
重要节点:
基于form表单提交文件类型的数据enctype=“multipart/form-data”
后端使用request.FILE方法接受具体的文件数据。
利用chunks方法循环获取文件数据。
具体代码:
{% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <div>用户名:<input type="text" name="username"></div> <div><input type="file" name="pic"></div> <div><input type="submit"></div> <script src="{% static 'jQuery.js' %}"></script> <script> $(':submit').click(function () { var formdata = new FormData(); formdata.append('username', $(':text').val(),); formdata.append('csrfmiddlewaretoken', "{{ csrf_token }}"); formdata.append('pic', $(':file')[0].files[0],); $.ajax({ url: '{% url "ajax_file" %}', type: 'post', data: formdata, processData: false, // 不处理数据 contentType: false, // 不设置内容类型 success: function (response) { console.log(response); {#console.log(response, typeof response);#} {#// var res = JSON.parse(response);#} {#location.href = response['redirect'];#} } }) }) </script> </body> </html>
重要节点:
基于ajax提交文件类型的数据依赖于formdata对象。
后端接收文件数据的代码不变。
登录认证
比如网站的购物车页面,个人页面,我们在未登录状态下不允许访问,一般都会跳转到登录页面,只有在登录状态下才可以访问。
http回顾
http超文本传输协议,应用层,基于tcp的请求与响应的协议。
三大特点
基于请求与响应。
短(无)连接。
浏览器与服务端建立双向链接管道,马上断开,connection:keep-alive 2s之后断开。
无状态。
http协议不会保存用户的信息,状态。
由于http协议的无状态的特点,所以,pc端默认不会记住用户的信息,从而不能实现登录认证。但是我们实际场景中,经常会用到登录认证。那么怎么去实现?现在常用的有两种实现方式:
实现登录认证的功能的两种方式
大家都知道HTTP协议是无状态的。
无状态的意思是每次请求都是独立的,它的执行情况和结果与前面的请求和之后的请求都无直接关系,它不会受前面的请求响应情况直接影响,也不会直接影响后面的请求响应情况。
一句有意思的话来描述就是人生只如初见,对服务器来说,每次的请求都是全新的。
状态可以理解为客户端和服务器在某次会话中产生的数据,那无状态的就以为这些数据不会被保留。会话中产生的数据又是我们需要保存的,也就是说要“保持状态”。因此Cookie就是在这样一个场景下诞生。
并且还有一个问题就是,你登陆我的网站的时候,我没法确定你是不是登陆了,之前我们学的django,虽然写了很多页面,但是用户不用登陆都是可以看所有网页的,只要他知道网址就行,但是我们为了自己的安全机制,我们是不是要做验证啊,访问哪一个网址,都要验证用户的身份,但是还有保证什么呢,用户登陆过之后,还要保证登陆了的用户不需要再重复登陆,就能够访问我网站的其他的网址的页面,对不对,但是http无状态啊,怎么保证这个事情呢?此时就要找cookie了。
例如:课上讲解。
首先来讲,cookie是浏览器的技术,Cookie具体指的是一段小信息,它是服务器发送出来存储在浏览器上的一组组键值对,可以理解为服务端给客户端的一个小甜点,下次访问服务器时浏览器会自动携带这些键值对,以便服务器提取有用信息。
cookie是浏览器保存在客户端的一段有用的信息数据,这些数据都是服务端提供的。以便浏览器每次访问服务端都携带着cookie这些数据,便于服务端提取信息比如登录认证等。
ookie的工作原理是:浏览器访问服务端,带着一个空的cookie,然后由服务器产生内容,浏览器收到相应后保存在本地;当浏览器再次访问时,浏览器会自动带上Cookie,这样服务器就能通过Cookie的内容来判断这个是“谁”了。
上面的数据只是HTTP的Cookie规范,但在浏览器大战的今天,一些浏览器为了打败对手,为了展现自己的能力起见,可能对Cookie规范“扩展”了一些,例如每个Cookie的大小为8KB,最多可保存500个Cookie等!但也不会出现把你硬盘占满的可能!
注意,不同浏览器之间是不共享Cookie的。也就是说在你使用IE访问服务器时,服务器会把Cookie发给IE,然后由IE保存起来,当你在使用FireFox访问服务器时,不可能把IE保存的Cookie发送给服务器。
每个服务端对应的同一个浏览器有各自的cookie表,并且不能互相访问。
我们电脑上只有谷歌浏览器(无其他浏览器),我们可以通过谷歌浏览器访问淘宝,京东,爱奇艺等,那么谷歌浏览器会在我们电脑本地保存三个cookie表,分别是淘宝、京东、爱奇艺,并且这些表之间理论上不能互相访问。
不同浏览器对应同一个服务端各自不同的cookie表,不能互相访问。
我们电脑安装了谷歌浏览器与夸克浏览器,通过两个浏览器访问同一个服务端比如淘宝,两个浏览器会在本地创建两个空间,存放各自浏览器访问过的服务端的cookie表。这两个空间不能互相访问。
一个浏览器登录成功了,另一个浏览器必须也要登录。
一般情况下, 一个浏览器只能允许登录一个用户访问同一个服务端。
创建一个cookieSeesion项目。
def index(request):
ret = HttpResponse('ok')
ret.set_cookie('is_login', True)
ret.set_cookie('username', 'barry')
ret.set_cookie('xxxid', 'fdjsalkfdjs4324dfjsafdlk')
return ret
def home(request):
c1 = request.COOKIES.get('is_login')
print(c1, type(c1)) # True <class 'str'>
c2 = request.COOKIES.get('username')
print(c2, type(c2)) # 123 <class 'str'>
c3 = request.COOKIES.get('xxxid')
print(c3)
return render(request, 'home.html')
浏览器清除cookie
先访问index:
再次访问home:
def home(request):
res = HttpResponse('删除成功')
res.delete_cookie('username')
return res
我们写三个页面,登录,购物车,个人主页。只有在登陆成功的状态下,才可以访问购物车以及个人主页。
path('login/', views.login, name='login'), path('shopping/', views.shopping, name='shopping'), path('userself/', views.user_self, name='userself'), def login(request): if request.method == 'GET': return render(request, 'login.html') if request.POST.dict().get('username') == 'barry' and request.POST.dict().get('password') == '123': res = redirect('userself') res.set_cookie('is_login', True) res.set_cookie('username', 'barry') return res else: return HttpResponse('登录失败') def auth(func): def inner(*args, **kwargs): # print('----->',args) request = args[0] if request.COOKIES.get('is_login') == 'True': ret = func(*args, **kwargs) return ret return redirect('login') return inner @auth def shopping(request): return render(request, 'shopping.html') @auth def user_self(request): return render(request, 'user_self.html')
login.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <form action="" method="post"> {% csrf_token %} <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <input type="submit"> </form> </body> </html>
Shopping.html、user_self.html:
<h1>欢迎来到购物车页面</h1>
参数:
key, 键
value=‘’, 值
max_age=None, 超时时间
expires=None, 超时时间(IE requires expires, so set it if hasn’t been already.)
path=‘/’, Cookie生效的路径,/ 表示根路径,特殊的:根路径的cookie可以被任何url的页面访问
domain=None, Cookie生效的域名
secure=False, https传输
httponly=False 只能http协议传输,无法被JavaScript获取(不是绝对,底层抓包可以获取到也可以被覆盖)
class HttpResponseBase: def set_cookie(self, key, 键 value='', 值 max_age=None, 超长时间 ,有效事件,max_age=20意思是这个cookie20秒后就消失了,默认时长是2周,这个是以秒为单位的 cookie需要延续的时间(以秒为单位) 如果参数是\ None`` ,这个cookie会延续到浏览器关闭为止。 expires=None, 超长时间,值是一个datetime类型的时间日期对象,到这个日期就失效的意思,用的不多 expires默认None ,cookie失效的实际日期/时间。 path='/', Cookie生效的路径,就是访问哪个路径可以得到cookie,'/'是所有路径都能获得cookie 浏览器只会把cookie回传给带有该路径的页面,这样可以避免将 cookie传给站点中的其他的应用。 / 表示根路径,特殊的:根路径的cookie可以被任何url的页面访问 domain=None, Cookie生效的域名 你可用这个参数来构造一个跨站cookie。 如, domain=".example.com" 所构造的cookie对下面这些站点都是可读的: www.example.com 、 www2.example.com 和an.other.sub.domain.example.com 。 如果该参数设置为 None ,cookie只能由设置它的站点读取。 secure=False, 如果设置为 True ,浏览器将通过HTTPS来回传cookie。 httponly=False 只能http协议传输,无法被JavaScript获取 (不是绝对,底层抓包可以获取到也可以被覆盖) ): pass
测试cookie的有效时间:
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
if request.POST.dict().get('username') == 'barry' and request.POST.dict().get('password') == '123':
res = redirect('userself')
res.set_cookie('is_login', True, 10)
res.set_cookie('username', 'barry')
return res
else:
return HttpResponse('登录失败')
登陆成功之后,10s内可以访问shopping、userself页面,超过10s,再次访问就会跳转到登录页面。
cookies设置中文
# 方式1 def login(request): ret = HttpResponse('ok') ret.set_cookie('k1','你好'.encode('utf-8').decode('iso-8859-1')) #取值:request.COOKIES['k1'].encode('utf-8').decode('iso-8859-1').encode('iso-8859-1').decode('utf-8') return ret 方式2 json def login(request): ret = HttpResponse('ok') import json ret.set_cookie('k1',json.dumps('你好')) #取值 json.loads(request.COOKIES['k1']) return ret
cookie的缺点
安全性低。
因为cookie保存在客户端的浏览器上,安全性低,私密性低,携带cookie通过网络传输也涉及到一些安全性的问题。
上限限制。
基于cookie以上的两个缺点,我们提供了session机制配合cookie进行操作。
之前,浏览器第一次访问服务端,带着一个空的cookie字典,然后服务端会在这个空的cookie字典中放置一些有用的键值对响应给浏览器,浏览器会保存在本地,然后通过浏览器在访问这个网站的其他页面都会带着这个存有很多有用的键值对的cookie,用以登录验证或者显示用户名等一些功能,但是这样我们从浏览器就可以查询到这些键值对,这样很不安全,现在有了session的概念,session怎么做的呢?
浏览器第一次访问服务端,带着一个空的cookie字典,然后服务端会在这个空的cookie字典中放置一个键值对,键就是session_id,值就是一个随机的加密字符串,与此同时,服务端会在相应的数据表中存储这个随机字符串,有用的键值对。然后服务器将这个cookie返回给浏览器,当浏览器访问这个网站的其他页面都会带着这个存有一个键值对的cookie,如果验证了或者需要有用的键值对了,后端就会通过session随机字符串,找到对应的这些键值对然后在使用他们。
注意:这都是django提供的方法,其他的框架就需要你自己关于cookie和session的方法了。 # 获取、设置、删除Session中数据#取值 request.session['k1'] request.session.get('k1',None) #request.session这句是帮你从cookie里面将sessionid的值取出来,将django-session表里面的对应sessionid的值的那条记录中的session-data字段的数据给你拿出来(并解密),get方法就取出k1这个键对应的值#设置值 request.session['k1'] = 123 request.session.setdefault('k1',123) # 存在则不设置 #帮你生成随机字符串,帮你将这个随机字符串和用户数据(加密后)和过期时间保存到了django-session表里面,帮你将这个随机字符串以sessionid:随机字符串的形式添加到cookie里面返回给浏览器,这个sessionid名字是可以改的,以后再说#但是注意一个事情,django-session这个表,你不能通过orm来直接控制,因为你的models.py里面没有这个对应关系 #删除值 del request.session['k1'] #django-session表里面同步删除 # 所有 键、值、键值对 request.session.keys() request.session.values() request.session.items() # 会话session的key session_key = request.session.session_key 获取sessionid的值 # 将所有Session失效日期小于当前日期的数据删除,将过期的删除 request.session.clear_expired() # 检查会话session的key在数据库中是否存在 request.session.exists("session_key") #session_key就是那个sessionid的值 # 删除当前会话的所有Session数据 request.session.delete() # 删除当前的会话数据并删除会话的Cookie。 request.session.flush() #常用,清空所有cookie---删除session表里的这个会话的记录, 这用于确保前面的会话数据不可以再次被用户的浏览器访问 例如,django.contrib.auth.logout() 函数中就会调用它。 # 设置会话Session和Cookie的超时时间 request.session.set_expiry(value) * 如果value是个整数,session会在些秒数后失效。 * 如果value是个datatime或timedelta,session就会在这个时间后失效。 * 如果value是0,用户关闭浏览器session就会失效。 * 如果value是None,session会依赖全局session失效策略。
代码测试:
同步数据库指令,创建sqllit可视化工具,拖动客户端链接服务端:
访问index视图:
path('index/', views.index),
def index(request):
request.session['is_login'] = True
request.session['username'] = 'barry'
request.session['password'] = '123'
return HttpResponse('设置成功')
执行到 request.session['is_login'] = True
代码,发生了三件事:
def home(request):
s1 = request.session.get('is_login')
print(s1, type(s1)) # True <class 'bool'>
s2 = request.session.get('username')
print(s2)
s3 = request.session.get('password')
print(s3, type(s3)) # 123 <class 'int'>
return HttpResponse('ok')
执行到s1 = request.session.get('is_login')
发生了三件事:
我们可以清除cookie,如果再次生成sessionid,就会生成一个新的sessionid.
换一个浏览器,也会生成新的sessionid.
总结:一个浏览器对应一个服务端有且只是生成一个sessionid,可能覆盖不能多赠.
#删除值
del request.session['k1'] #django-session表里面同步删除
# 删除当前会话的所有Session数据
request.session.delete()
# 删除当前的会话数据并删除会话的Cookie。
request.session.flush() #常用,清空所有cookie---删除session表里的这个会话的记录,
这用于确保前面的会话数据不可以再次被用户的浏览器访问
例如,django.contrib.auth.logout() 函数中就会调用它。
我们项目的配置文件是settings
但是我们还有一个总的配置文件,globalsettings,项目开启时,先要加载项目中settings,加载完毕之后,在加载globalsettings,相同的配置优先使用settings的,settings中没有的配置,使用globalsettings。
from django.conf import global_settings
设置过期时间
def index(request):
request.session['is_login'] = True
request.session['username'] = 'barry'
request.session['sex'] = 'male'
request.session.set_expiry(10)
return HttpResponse('设置成功')
django的session保存位置
Django中默认支持Session,其内部提供了5种类型的Session供开发者使用。
1. 数据库Session SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 引擎(默认) 2. 缓存Session SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # 引擎 SESSION_CACHE_ALIAS = 'default' # 使用的缓存别名(默认内存缓存,也可以是memcache),此处别名依赖缓存的设置 3. 文件Session SESSION_ENGINE = 'django.contrib.sessions.backends.file' # 引擎 SESSION_FILE_PATH = None # 缓存文件路径,如果为None,则使用tempfile模块获取一个临时地址tempfile.gettempdir() 4. 缓存+数据库 SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' # 引擎 5. 加密Cookie Session SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' # 引擎 其他公用设置项: SESSION_COOKIE_NAME = "sessionid" # Session的cookie保存在浏览器上时的key,即: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,默认修改之后才保存(默认)
django的中间件其中的第四个就是防跨站请求伪造的拦截功能,如果我们利用post提交数据到后端,没有设置隐藏的input标签,就会被这个中间件拦截,出现forbidden页面。我们的解决方式是post提交数据时,利用模版系统生成一个隐藏的input标签。
除了上述方法,django提供了两个装饰器,主要用于防跨站请求伪造功能。
from django.views.decorators.csrf import csrf_protect, csrf_exempt
@csrf_exempt # 强行取消csrf防跨站请求伪造的拦截功能
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
if request.POST.dict().get('username') == 'barry' and request.POST.dict().get('password') == '123':
res = redirect('userself')
res.set_cookie('is_login', True, 10)
res.set_cookie('username', 'barry')
return res
else:
return HttpResponse('登录失败')
我们将中间件的csrf拦截的注销,然后加上这个装饰器
@csrf_protect # 强行加上csrf防跨站请求伪造的拦截功能
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
if request.POST.dict().get('username') == 'barry' and request.POST.dict().get('password') == '123':
res = redirect('userself')
res.set_cookie('is_login', True, 10)
res.set_cookie('username', 'barry')
return res
else:
return HttpResponse('登录失败')
django默认给我们提供了7种中间件:settings:
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',
]
Django中间件是一种机制,可以在请求和响应之间拦截和处理请求。以下是一些常见的Django中间件及其功能: AuthenticationMiddleware:处理用户身份验证和会话管理。 SessionMiddleware:管理用户会话和跨请求的状态。 CsrfViewMiddleware:提供跨站请求伪造(CSRF)保护。 MessageMiddleware:将消息传递给下一个请求。 CommonMiddleware:处理URL重写、缓存、Gzip压缩、静态文件服务等。 SecurityMiddleware:提供安全性功能,例如HTTP头部配置、跨域资源共享(CORS)和内容安全策略(CSP)。 LocaleMiddleware:处理语言选择和翻译。 DebugToolbarMiddleware:为开发人员提供调试工具,例如Django Debug Toolbar。 MiddlewareMixin:用于创建自定义中间件的基本类。 以上是一些常用的Django中间件,当然还有其他的中间件可以自己实现。
中间件有什么作用?什么时候执行这个中间件?
所有的请求请求django后端时,一定是先经过中间件的处理,从上至下执行一遍,中间件对所有的请求进行检测,处理,拦截,加工通过之后,在执行到urls路由分发,经过视图,然后通过urls在从下至上执行完毕所有的中间件,中间件对所有的响应进行处理,加工等操作,最后发送到浏览器。
在应用下,创建一个目录(目录名自定义),在此目录下,创建一个py文件(名字自定义),我们这里以utils/mymiddlewrare:
from django.utils.deprecation import MiddlewareMixin class MD1(MiddlewareMixin): def process_request(self, request): # 规定的方法名 print('in md1 process request') def process_response(self, request, response): # 规定的方法名 print('in md1 process response') return response class MD2(MiddlewareMixin): def process_request(self, request): # 规定的方法名 print('in md2 process request') def process_response(self, request, response): # 规定的方法名 print('in md2 process response') return response
最后,在settings中进行相应的配置:
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',
'app01.utils.mymiddleware.MD1',
'app01.utils.mymiddleware.MD2',
]
访问index页面,查看执行流程:
in md1 process request
in md2 process request
in views.index
in md2 process response
in md1 process response
文字描述自己总结。
补充:
中间件的process_request方法,对request可以加工,检测,处理,拦截,我们测试一个拦截功能:
from django.utils.deprecation import MiddlewareMixin from django.shortcuts import render, HttpResponse, redirect class MD1(MiddlewareMixin): def process_request(self, request): # 规定的方法名 print('in md1 process request') def process_response(self, request, response): # 规定的方法名 print('in md1 process response') return response # 返回给了上一个中间件的process_response方法的形参。 class MD2(MiddlewareMixin): def process_request(self, request): # 规定的方法名 # print('------>',request.path) print('in md2 process request') if request.path == '/login/': return HttpResponse('禁止访问!!!') # 返回值返回给了当前中间件的process_response方法的形参。 def process_response(self, request, response): # 规定的方法名 print('in md2 process response') return response # 返回给了上一个中间件的process_response方法的形参。
执行结果:
in md1 process request
in md2 process request
in md2 process response
in md1 process response
由此可知:
在中间件的process_request方法中,
如果我们不设置return或者是return None,请求不会拦截,向下执行;
如果我们设置return的返回值为django的返回对象中的其中一种(httpresponse,render,redirect),则就会拦截,否则就会报错。
我们昨天讲的认证是基于装饰器利用cookiesession原理去实现的,我们使用装饰器去实现认证,每个需要登录认证的views视图函数都需要加上装饰器,操作比较麻烦。
我们先实现基于session的装饰器的认证:
def login(request): if request.method == 'GET': return render(request, 'login.html') if request.POST.dict().get('username') == 'barry' and request.POST.dict().get('password') == '123': request.session['is_login'] = True return redirect('userself') else: return HttpResponse('登录失败') def auth(func): def inner(*args, **kwargs): request = args[0] if request.session.get('is_login'): ret = func(*args, **kwargs) return ret return redirect('login') return inner @auth def shopping(request): return render(request, 'shopping.html') @auth def user_self(request): return render(request, 'user_self.html')
我们利用中间件实现登录认证的功能:
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import render, HttpResponse, redirect
from django.urls import reverse
class MyAuth(MiddlewareMixin):
def process_request(self, request): # 规定的方法名
if request.path == reverse('login') or request.session.get('is_login'):
return
else:
return redirect('login')
settings配置:
MIDDLEWARE = [
......
'app01.utils.mymiddleware.MyAuth',
]
继续优化:
我们项目中,并不是所有的页面都需要登录认证才可以访问的,比如淘宝:首页,其他页面,男装,女装,分类页面等等,都不需要先登录,只有购物车,个人页面,等需要登录之后访问。我们引入黑白名单。
黑名单:在名单上的禁止访问(出现),非名单上的可以访问(出现)。
白名单:在名单上的可以访问(出现),非名单上的不可以访问(出现)。
class MyAuth(MiddlewareMixin):
white_list = [reverse('home'), reverse('index'), reverse('login')]
black_list = [reverse('shopping'), reverse('userself')]
def process_request(self, request): # 规定的方法名
if request.path in self.white_list or request.session.get('is_login'):
return
else:
return redirect('login')
中间件包含5种方法,
process_request, process_response, process_view,process_exception,process_template_response.
request是HttpRequest对象。
view_func是Django即将使用的视图函数。 (它是实际的函数对象,而不是函数的名称作为字符串。)
view_args是将传递给视图的位置参数的列表.
view_kwargs是将传递给视图的关键字参数的字典。 view_args和view_kwargs都不包含第一个视图参数(request)。
Django会在调用视图函数之前调用process_view方法。
它应该返回None或一个HttpResponse对象。 如果返回None,Django将继续处理这个请求,执行任何其他中间件的process_view方法,然后在执行相应的视图。 如果它返回一个HttpResponse对象,Django不会调用对应的视图函数。 它将执行中间件的process_response方法并将应用到该HttpResponse并返回结果。
代码测试:
from django.utils.deprecation import MiddlewareMixin from django.shortcuts import render, HttpResponse, redirect from django.urls import reverse class MD1(MiddlewareMixin): def process_request(self, request): # 规定的方法名 print('in md1 process request') def process_view(self, request, view_func, view_args, view_kwargs): print('in md1 process view') # print(view_func) # print(view_func.__name__) ret = view_func(request) # 提前执行视图函数 return ret def process_response(self, request, response): # 规定的方法名 print('in md1 process response') return response class MD2(MiddlewareMixin): def process_request(self, request): # 规定的方法名 print('in md2 process request') def process_view(self, request, view_func, view_args, view_kwargs): print('in md2 process view') print(view_func) print(view_func.__name__) return def process_response(self, request, response): # 规定的方法名 print('in md2 process response') return response
process_exception(self, request, exception)
该方法两个参数:
一个HttpRequest对象
一个exception是视图函数异常产生的Exception对象。
这个方法只有在视图函数中出现异常了才执行,它返回的值可以是一个None也可以是一个HttpResponse对象。如果是HttpResponse对象,Django将调用模板和中间件中的process_response方法,并返回给浏览器,否则将默认处理异常。如果返回一个None,则交给下一个中间件的process_exception方法来处理异常。它的执行顺序也是按照中间件注册顺序的倒序执行。
代码测试:
from django.utils.deprecation import MiddlewareMixin from django.shortcuts import render, HttpResponse, redirect from django.urls import reverse class MD1(MiddlewareMixin): def process_request(self, request): # 规定的方法名 print('in md1 process request') def process_view(self, request, view_func, view_args, view_kwargs): print('in md1 process view') # print(view_func) # print(view_func.__name__) # ret = view_func(request) # 提前执行视图函数 return def process_exception(self, request, exception): print(f'视图函数报错才会执行md1 exception{exception}') def process_response(self, request, response): # 规定的方法名 print('in md1 process response') return response class MD2(MiddlewareMixin): def process_request(self, request): # 规定的方法名 print('in md2 process request') def process_view(self, request, view_func, view_args, view_kwargs): print('in md2 process view') # print(view_func) # print(view_func.__name__) return def process_exception(self, request, exception): print(f'视图函数报错才会执行md2 exception{exception}') def process_response(self, request, response): # 规定的方法名 print('in md2 process response') return response views: def index(request): print('in view index') raise Exception('主动报错!') return HttpResponse('ok')
process_template_response(self, request, response)
它的参数,一个HttpRequest对象,response是TemplateResponse对象(由视图函数或者中间件产生)。
process_template_response是在视图函数执行完成后立即执行,但是它有一个前提条件,那就是视图函数返回的对象有一个render()方法(或者表明该对象是一个TemplateResponse对象或等价方法)。
详见博客。
用于url访问过滤
登录认证,对请求信息进行分析,加工。
用于IP访问频率限制。
爬虫或者黑客,频繁地像你的url发送请求,网站一般会对这种请求做访问频次的限制。
Form组件是后端Django提供的一个功能模块,它主要实现的就是下列三个功能,有了Form组件可以提升我们的开发效率,方便我们使用。后面我们讲的DRF框架以及Flask框架,都有类似于Form组件的功能,所以我们今天学会了这个Form组件,后面的类似的功能so easy.
我们在注册时,前端需要提交用户名,密码等信息,这些信息一般都会有限制,比如用户名长度,使用的字符,密码的长度等等。如果我们提交了不满足条件的内容,一般会在页面上提示错误信息,这个校验功能谁做的?前后端都要做。
前端做校验:省去了通过网络发送到后端去验证,减轻后端压力,提升效率。
后端做校验:防止有人通过其他的技术(爬虫,黑客)绕过前端的校验,提交到后端的风险。
创建新项目:formproject,
我们实现简单的注册功能,用户名长度要在8与14之间,如果不满足,则前端页面显示红色错误提示,满足则显示登录成功。
path('register/', views.register, name='register'), def register(request): error_msg = '' if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') # 简单做一个用户名的校验 if 8 <= len(username) <= 14: return HttpResponse('注册成功') else: error_msg = '用户名长度不满足条件' return render(request, 'register.html', {'error_msg': error_msg}) return render(request, 'register.html', {'error_msg': error_msg}) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <form action="" method="post"> {% csrf_token %} <div> 用户名:<input type="text" name="username"> <span style="color: red">{{ error_msg }}</span> </div> <div>密码:<input type="password" name="password"></div> <input type="submit"> </form> </body> </html>
我们使用form组件生成前端标签:
from django.shortcuts import render, HttpResponse from django import forms # Create your views here. class RegisterForm(forms.Form): name = forms.CharField(label='用户名') password = forms.CharField(label='密码') def register(request): error_msg = '' form_obj = RegisterForm() if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') # 简单做一个用户名的校验 if 8 <= len(username) <= 14: return HttpResponse('注册成功') else: error_msg = '用户名长度不满足条件' return render(request, 'register.html', {'error_msg': error_msg}) return render(request, 'register.html', {'error_msg': error_msg, 'form_obj': form_obj}) <form action="" method="post"> {% csrf_token %} <div> 用户名:{{ form_obj.name }} <span style="color: red">{{ error_msg }}</span> </div> <div>密码:{{ form_obj.password }}</div> <input type="submit"> </form>
form组件类中的属性里面还可以设置更多的参数,这些参数大部分适用于数据校验的,也有用于标签显示的。比如label用于标签显示。
from django.shortcuts import render, HttpResponse from django import forms # Create your views here. class RegisterForm(forms.Form): name = forms.CharField(label='用户名') password = forms.CharField(label='密码') def register(request): error_msg = '' form_obj = RegisterForm() # form_obj.name 是forms.CharField这个类实例化的对象,label='用户名'对象的属性,form_obj.name.label if request.method == 'POST': username = request.POST.get('name') password = request.POST.get('password') # 简单做一个用户名的校验 if 8 <= len(username) <= 14: return HttpResponse('注册成功') else: error_msg = '用户名长度不满足条件' return render(request, 'register.html', {'error_msg': error_msg,'form_obj': form_obj}) return render(request, 'register.html', {'error_msg': error_msg, 'form_obj': form_obj}) <form action="" method="post"> {% csrf_token %} <div> <label for="{{ form_obj.name.id_for_label }}">{{ form_obj.name.label }}</label> {{ form_obj.name }} <span style="color: red">{{ error_msg }}</span> </div> <div> <label for="{{ form_obj.password.id_for_label }}">{{ form_obj.password.label }}</label> {{ form_obj.password }} </div> <input type="submit"> </form>
form组件可以生成各种input标签以及select标签,(生成的是form表单中的标签)。
text
name = forms.CharField(label='用户名')
password
password = forms.CharField(
label='密码',
widget=forms.widgets.PasswordInput(),
)
radio
{{ form_obj.sex }}
sex = forms.ChoiceField(
label='性别',
choices=((1, '男'), (2, '女'), (3, '其他'),),
widget=forms.widgets.RadioSelect(),
)
单选select
<div>
{{ form_obj.area }}
</div>
area = forms.ChoiceField(
choices=((1, '保定'), (2, '石家庄'), (3, '邢台'),),
initial=3,
widget=forms.widgets.Select(),
)
补充:initial参数是设置默认值。
多选的select
<div>
{{ form_obj.hobby }}
</div>
hobby = forms.MultipleChoiceField(
choices=((1, '嫦娥'), (2, '貂蝉'), (3, '夏侯惇'), (4, '李信')),
initial=[1, 3],
widget=forms.widgets.SelectMultiple(),
)
checkbox
多选的checkbox
<div>
{{ form_obj.hobby_food }}
</div>
hobby_food = forms.fields.MultipleChoiceField(
choices=((1, '鱼香肉丝'), (2, '宫保鸡丁'), (3, '水煮肉片'), (4, '火烧')),
widget=forms.widgets.CheckboxSelectMultiple(),
)
多选checkbox还有一个用法,就是使用单独一个,比如我们登录时,一般会遇到换一个勾选框,7天内记住我。
<div>
{{ form_obj.keep }}
<label for="{{ form_obj.keep.id_for_label }}">{{ form_obj.keep.label }}</label>
</div>
keep = forms.ChoiceField(
choices=(('True', 1),
('False', 0)),
initial=1,
label='是否七天内自动登录',
widget=forms.widgets.CheckboxInput(),
)
date
日期类型
<div>
{{ form_obj.date }}
</div>
date = forms.DateField(
widget=forms.widgets.TextInput(attrs={'type': 'date'}),
)
Field required=True, 是否允许为空 widget=None, HTML插件 label=None, 用于生成Label标签或显示内容 initial=None, 初始值 help_text='', 帮助信息(在标签旁边显示) error_messages=None, 错误信息 {'required': '不能为空', 'invalid': '格式错误'} validators=[], 自定义验证规则 localize=False, 是否支持本地化 disabled=False, 是否可以编辑 label_suffix=None Label内容后缀 CharField(Field) max_length=None, 最大长度 min_length=None, 最小长度 strip=True 是否移除用户输入空白 IntegerField(Field) max_value=None, 最大值 min_value=None, 最小值 FloatField(IntegerField) ... DecimalField(IntegerField) max_value=None, 最大值 min_value=None, 最小值 max_digits=None, 总长度 decimal_places=None, 小数位长度 BaseTemporalField(Field) input_formats=None 时间格式化 DateField(BaseTemporalField) 格式:2015-09-01 TimeField(BaseTemporalField) 格式:11:12 DateTimeField(BaseTemporalField)格式:2015-09-01 11:12 DurationField(Field) 时间间隔:%d %H:%M:%S.%f ... RegexField(CharField) regex, 自定制正则表达式 max_length=None, 最大长度 min_length=None, 最小长度 error_message=None, 忽略,错误信息使用 error_messages={'invalid': '...'} EmailField(CharField) ... FileField(Field) allow_empty_file=False 是否允许空文件 ImageField(FileField) ... 注:需要PIL模块,pip3 install Pillow 以上两个字典使用时,需要注意两点: - form表单中 enctype="multipart/form-data" - view函数中 obj = MyForm(request.POST, request.FILES) URLField(Field) ... BooleanField(Field) ... NullBooleanField(BooleanField) ... ChoiceField(Field) ... choices=(), 选项,如:choices = ((0,'上海'),(1,'北京'),) required=True, 是否必填 widget=None, 插件,默认select插件 label=None, Label内容 initial=None, 初始值 help_text='', 帮助提示 ModelChoiceField(ChoiceField) ... django.forms.models.ModelChoiceField queryset, # 查询数据库中的数据 empty_label="---------", # 默认空显示内容 to_field_name=None, # HTML中value的值对应的字段 limit_choices_to=None # ModelForm中对queryset二次筛选 ModelMultipleChoiceField(ModelChoiceField) ... django.forms.models.ModelMultipleChoiceField TypedChoiceField(ChoiceField) coerce = lambda val: val 对选中的值进行一次转换 empty_value= '' 空值的默认值 MultipleChoiceField(ChoiceField) ... TypedMultipleChoiceField(MultipleChoiceField) coerce = lambda val: val 对选中的每一个值进行一次转换 empty_value= '' 空值的默认值 ComboField(Field) fields=() 使用多个验证,如下:即验证最大长度20,又验证邮箱格式 fields.ComboField(fields=[fields.CharField(max_length=20), fields.EmailField(),]) MultiValueField(Field) PS: 抽象类,子类中可以实现聚合多个字典去匹配一个值,要配合MultiWidget使用 SplitDateTimeField(MultiValueField) input_date_formats=None, 格式列表:['%Y--%m--%d', '%m%d/%Y', '%m/%d/%y'] input_time_formats=None 格式列表:['%H:%M:%S', '%H:%M:%S.%f', '%H:%M'] FilePathField(ChoiceField) 文件选项,目录下文件显示在页面中 path, 文件夹路径 match=None, 正则匹配 recursive=False, 递归下面的文件夹 allow_files=True, 允许文件 allow_folders=False, 允许文件夹 required=True, widget=None, label=None, initial=None, help_text='' GenericIPAddressField protocol='both', both,ipv4,ipv6支持的IP格式 unpack_ipv4=False 解析ipv4地址,如果是::ffff:192.0.2.1时候,可解析为192.0.2.1, PS:protocol必须为both才能启用 SlugField(CharField) 数字,字母,下划线,减号(连字符) ... UUIDField(CharField) uuid类型
widget补充
用于定义input标签类型或者是给标签添加class、style等等属性的字段,我们实现引入Bootstrap框架,继承Bootstrap样式的功能:
{{ form_obj.name }}
class RegisterForm(forms.Form):
name = forms.CharField(
label='用户名',
initial='请输入用户名,长度在8~14位',
widget=forms.widgets.TextInput(
attrs={'class': 'form-control'}
),
)
我们可以设置参与数据校验的字段:
help_text
设置提示信息的数据。
name = forms.CharField(
help_text='用户名设置要慎重,字符在8~14之间',
)
{{ form_obj.name }}
<span>{{ form_obj.name.help_text }}</span>
用于数据校验的字段:
class RegisterForm(forms.Form): name = forms.CharField( label='用户名', required=True, # 不能为空 max_length=14, min_length=8, # 最大最小字符数 help_text='用户名设置要慎重,字符在8~14之间', ) password = forms.CharField( label='密码', required=True, # 不能为空 max_length=12, min_length=6, # 最大最小字符数 widget=forms.widgets.PasswordInput(), ) def register(request): error_obj = '' form_obj = RegisterForm() # form_obj.name 是forms.CharField这个类实例化的对象,label='用户名'对象的属性,form_obj.name.label if request.method == 'POST': """ # 自己构建的逻辑用于数据校验 username = request.POST.get('name') password = request.POST.get('password') if 8 <= len(username) <= 14: return HttpResponse('注册成功') else: error_msg = '用户名长度不满足条件' return render(request, 'register.html', {'error_msg': error_msg, 'form_obj': form_obj}) """ # form组件校验数据 form_obj = RegisterForm(request.POST) print('is_valid----->', form_obj.is_valid()) if form_obj.is_valid(): # 校验数据的方法 # 获取通过校验的提交的数据 data = form_obj.cleaned_data print('cleaned_data----->',data) # 写入数据库 else: error_obj = form_obj.errors print('error_obj---->', error_obj) return render(request, 'register.html', {'error_obj': error_obj, 'form_obj': form_obj}) return render(request, 'register.html', {'error_obj': error_obj, 'form_obj': form_obj})
前端显示:
<form action="" method="post" novalidate>
{% csrf_token %}
<div>
<label for="{{ form_obj.name.id_for_label }}">{{ form_obj.name.label }}</label>
{{ form_obj.name }}
<span style="color: red">{{ error_obj }}</span>
</div>
<div>
<label for="{{ form_obj.password.id_for_label }}">{{ form_obj.password.label }}</label>
{{ form_obj.password }}
</div>
<input type="submit">
</form>
如果提交的数据正确:
cleaned_data-----> {'name': 'barrybarry', 'password': '123456'}
如果提交的数据未通过校验:
error_obj----> <ul class="errorlist"><li>name<ul class="errorlist"><li>This field is required.</li></ul></li><li>password<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
error_message字段
上面的错误信息全部都是英文显示,我们要将其换成中文显示。借助于error_message这个字段设置。
class RegisterForm(forms.Form): name = forms.CharField( label='用户名', required=True, # 不能为空,默认不能为空 max_length=14, min_length=8, # 最大最小字符数 help_text='用户名设置要慎重,字符在8~14之间', error_messages={ 'required': '不能为空', 'max_length': '不要超过14个字符', 'min_length': '不要少于8个字符', } ) password = forms.CharField( label='密码', required=True, # 不能为空 max_length=12, min_length=6, # 最大最小字符数 widget=forms.widgets.PasswordInput(), error_messages={ 'required': '不能为空', 'max_length': '不要超过12个字符', 'min_length': '不要少于6个字符', } ) 视图函数未改变。 <form action="" method="post" novalidate> {% csrf_token %} <div> <label for="{{ form_obj.name.id_for_label }}">{{ form_obj.name.label }}</label> {{ form_obj.name }} <span style="color: red">{{ error_obj.name.0 }}</span> </div> <div> <label for="{{ form_obj.password.id_for_label }}">{{ form_obj.password.label }}</label> {{ form_obj.password }} <span style="color: red">{{ error_obj.password.0 }}</span> </div> ......
一般一个input填写内容时可能会报多个错误,我们只显示第一个错误信息即可。
Form组件配置的用于数据校验的字段都是基础的,不能满足我们项目中的所有情况,这时候我们就可以自定制错误配置。
使用RegexValidator验证器
from django.core.validators import RegexValidator
class RegisterForm(forms.Form):
name = forms.CharField(
.....
validators=[RegexValidator(r'^1[3-9]\d{9}$', '请输入正确手机号'),]
)
不满足正则的条件,则报错。
自定制校验函数
def phone_check(value): # 提交的数据 # print('value---->', value) if not re.search(r'1[3-9]\d{9}$', value): raise ValidationError('手机号码格式不正确') elif '4' in value: raise ValidationError('含有4的手机号码不允许注册') class RegisterForm(forms.Form): name = forms.CharField( label='用户名', required=True, # 不能为空,默认不能为空 max_length=14, min_length=8, # 最大最小字符数 help_text='用户名设置要慎重,字符在8~14之间', error_messages={ 'required': '不能为空', 'max_length': '不要超过14个字符', 'min_length': '不要少于8个字符', }, validators=[phone_check, ] )
我们可以设置多个校验函数。
就是钩子函数,对某个字段或者所有字段提交过来的数据进行校验。
对某个字段提交的数据进行校验。
class RegisterForm(forms.Form): name = forms.CharField( label='用户名', required=True, # 不能为空,默认不能为空 max_length=14, min_length=8, # 最大最小字符数 help_text='用户名设置要慎重,字符在8~14之间', error_messages={ 'required': '不能为空', 'max_length': '不要超过14个字符', 'min_length': '不要少于8个字符', }, ) password = forms.CharField( label='密码', required=True, # 不能为空 max_length=12, min_length=6, # 最大最小字符数 widget=forms.widgets.PasswordInput(), error_messages={ 'required': '不能为空', 'max_length': '不要超过12个字符', 'min_length': '不要少于6个字符', } ) def clean_name(self): # 局部钩子 # 获取通过基础校验之后的数据 username = self.cleaned_data.get('name') if 'sb' in username: raise ValidationError('含有非法内容sb') # 如果校验数据有问题,抛出错误 return username # 校验数据没有问题,返回校验的数据
对所有字段提交的数据进行校验。
注册时,注册密码需要再次输入密码,两次密码一致,才注册成功。
def clean(self):
p1 = self.cleaned_data.get('password')
p2 = self.cleaned_data.get('r_password')
if p1 != p2:
self.add_error('r_password', '两次密码不一致') # 将错误信息添加到指定的字段的错误属性中
raise ValidationError('两次密码不一致!')
return self.cleaned_data
前端显示:
<form action="" method="post" novalidate> {% csrf_token %} <div> <label for="{{ form_obj.name.id_for_label }}">{{ form_obj.name.label }}</label> {{ form_obj.name }} <span style="color: red">{{ error_obj.name.0 }}</span> </div> <div> <label for="{{ form_obj.password.id_for_label }}">{{ form_obj.password.label }}</label> {{ form_obj.password }} <span style="color: red">{{ error_obj.password.0 }}</span> </div> <div> <label for="{{ form_obj.r_password.id_for_label }}">{{ form_obj.r_password.label }}</label> {{ form_obj.r_password }} <span style="color: red">{{ error_obj.r_password.0 }}</span> </div>
详述CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
我们登录成功一个网站,如果这个网站是基于cookie与session原理进行登录认证的话,它会在cookie里面存放一个sessionid: 密文之外,还会存放一个csrftoken: 密文,这个密文一般不会变化。
比如,阿珍打开浏览器访问并登录了支付宝页面,支付宝进行登录验证,验证通过之后,会在你的cookie中存放一个sessionid: 密文(用于登录认证的键值对)。阿珍没有关闭支付宝的窗口,浏览了www.xxoo.com流氓网站,这个网站弹出了一个链接:阿强沐浴视频,点击链接~
当阿珍点击这个链接,这个链接跳转到支付宝的支付的页面并且给对方支付钱免密支付,由于阿珍之前没有关闭支付宝窗口,所以阿珍的浏览器的cookie中存放着用于登录支付宝认证的sessionid,如果支付宝没有做防跨站请求伪造的防护措施钱马上就会转出。
sessionid 是用于登录认证的键值对。
存在cookie中的csrftoken对应的密文是用于防跨站请求伪造的密文。
一般django后端对前端向后端发送的post、head、put请求这三种请求都会做csrftoken的处理。
解决跨站请求伪造的问题有很多种,我们django提供的基于csrf_token随机密文的模式只是其中的一种而已。
django如何解决防跨站请求伪造的?
我们通过查看源码以及文档的方式,学习了django是这样解决的。(你在学习django过程中,自己通过源码学习发现的一个亮点,借助于这个点,我们以后解决加密的需求时,可以使用。)
def _compare_salted_tokens(request_csrf_token, csrf_token): # Assume both arguments are sanitized -- that is, strings of # length CSRF_TOKEN_LENGTH, all CSRF_ALLOWED_CHARS. return constant_time_compare( _unsalt_cipher_token(request_csrf_token), _unsalt_cipher_token(csrf_token), ) def _unsalt_cipher_token(token): """ Given a token (assumed to be a string of CSRF_ALLOWED_CHARS, of length CSRF_TOKEN_LENGTH, and that its first half is a salt), use it to decrypt the second half to produce the original secret. """ salt = token[:CSRF_SECRET_LENGTH] token = token[CSRF_SECRET_LENGTH:] chars = CSRF_ALLOWED_CHARS pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in salt)) secret = ''.join(chars[x - y] for x, y in pairs) # Note negative values are ok return secret
django真正的验证csrf_token的过程:
cookie: wcefgT7czNbJLL4soJx2mZFe6Ttq5pYmS2HVC5H3QV1bPQxUODIO8AObMYSpkNvO
csrf_token: IYRNagl69YzOIKDtQE1l0QOL4ykrx45S4OktwsVXq6pgMP6Vgyc7MrXIKDJqMsCk
SECRET_KEY = '--p_4kfd@a4)^&xttd9+wh+h836slnk9gjwzqg-r+x1tn4b9-c'
我们利用ajax向后端如果发送post请求提交数据时,也需要携带着csrf密文提交到后端,我们讲过两种方式:
方式一
$.ajax({
url: "/cookie_ajax/",
type: "POST",
data: {
"username": "chao",
"password": 123456,
"csrfmiddlewaretoken": $("[name = 'csrfmiddlewaretoken']").val() // 使用jQuery取出csrfmiddlewaretoken的值,拼接到data中
},
success: function (data) {
console.log(data);
}
})
方式二
$.ajax({
data: {csrfmiddlewaretoken: '{{ csrf_token }}' },
});
无论方式一还是方式二都需要页面存在一个隐藏的input标签,获取这个标签对应的密文,封装到提交的数据中,才可以通过后端的防跨站请求伪造的验证。
方式三
我们使用ajax利用jQuery获取cookie中的csrftoken的密文,封装到数据中提交到后端,也可以通过防跨站请求伪造的验证。这种通过验证的校验原理与上图不一样。
jQuery无法直接获取到cookie中的csrftoken这个密文,我们还需要借助于一个插件,jquery.cookie.js。
{% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <div>用户名:<input type="text" name="username"></div> <div>密码:<input type="password" name="password"></div> <div><input type="submit"></div> <script src="{% static 'jQuery.js' %}"></script> <script src="{% static 'jquery.cookie.js' %}"></script> <script> $(':submit').click(function () { $.ajax({ url: '{% url "ajaxlogin" %}', type: 'post', headers: {"X-CSRFToken": $.cookie('csrftoken')}, # 获取cookie中的csrftoken data: { 'username': $(':text').val(), 'password': $(':password').val(), }, success: function (response) { console.log(response); {#console.log(response, typeof response);#} {#// var res = JSON.parse(response);#} {#location.href = response['redirect'];#} } }) }) </script> <script> </script> </body> </html>
jquery.cookie.js:
/*! * jQuery Cookie Plugin v1.4.1 * https://github.com/carhartl/jquery-cookie * * Copyright 2013 Klaus Hartl * Released under the MIT license */ (function (factory) { if (typeof define === 'function' && define.amd) { // AMD define(['jquery'], factory); } else if (typeof exports === 'object') { // CommonJS factory(require('jquery')); } else { // Browser globals factory(jQuery); } }(function ($) { var pluses = /\+/g; function encode(s) { return config.raw ? s : encodeURIComponent(s); } function decode(s) { return config.raw ? s : decodeURIComponent(s); } function stringifyCookieValue(value) { return encode(config.json ? JSON.stringify(value) : String(value)); } function parseCookieValue(s) { if (s.indexOf('"') === 0) { // This is a quoted cookie as according to RFC2068, unescape... s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); } try { // Replace server-side written pluses with spaces. // If we can't decode the cookie, ignore it, it's unusable. // If we can't parse the cookie, ignore it, it's unusable. s = decodeURIComponent(s.replace(pluses, ' ')); return config.json ? JSON.parse(s) : s; } catch(e) {} } function read(s, converter) { var value = config.raw ? s : parseCookieValue(s); return $.isFunction(converter) ? converter(value) : value; } var config = $.cookie = function (key, value, options) { // Write if (value !== undefined && !$.isFunction(value)) { options = $.extend({}, config.defaults, options); if (typeof options.expires === 'number') { var days = options.expires, t = options.expires = new Date(); t.setTime(+t + days * 864e+5); } return (document.cookie = [ encode(key), '=', stringifyCookieValue(value), options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE options.path ? '; path=' + options.path : '', options.domain ? '; domain=' + options.domain : '', options.secure ? '; secure' : '' ].join('')); } // Read var result = key ? undefined : {}; // To prevent the for loop in the first place assign an empty array // in case there are no cookies at all. Also prevents odd result when // calling $.cookie(). var cookies = document.cookie ? document.cookie.split('; ') : []; for (var i = 0, l = cookies.length; i < l; i++) { var parts = cookies[i].split('='); var name = decode(parts.shift()); var cookie = parts.join('='); if (key && key === name) { // If second argument (value) is a function it's a converter... result = read(cookie, value); break; } // Prevent storing a cookie that we couldn't decode. if (!key && (cookie = read(cookie)) !== undefined) { result[name] = cookie; } } return result; }; config.defaults = {}; $.removeCookie = function (key, options) { if ($.cookie(key) === undefined) { return false; } // Must not alter options, thus extending a fresh object... $.cookie(key, '', $.extend({}, options, { expires: -1 })); return !$.cookie(key); }; }));
同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。
同源策略,它是由Netscape提出的一个著名的安全策略。现在所有支持JavaScript 的浏览器都会使用这个策略。所谓同源是指,域名,协议,端口相同。当一个浏览器的两个tab页中分别打开来 百度和谷歌的页面当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。
默认情况下,只要是同源的两个url进行通信,浏览器(以及后端)是允许的, 如果非同源,浏览器(以及后端)默认是拦截的。
我们创建两个项目,我们实现一下同源以及非同源的通信过程。
我们创建两个项目: pro1项目(8001),pro2项目(8002),这就达到了非同源的场景。
验证,同源情况下可以进行互相通信。
我们在pro1创建两个url以及对应的视图,进行通信测试:
from app01 import views urlpatterns = [ path('admin/', admin.site.urls), path('index/', views.index, name='index'), path('base/', views.base, name='base'), ] from django.shortcuts import render from django.http import JsonResponse # Create your views here. def index(request): return render(request, 'index.html') def base(request): print(request.GET) return JsonResponse({'response': 'ok'})
html文件:
{% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bootstrap 101 Template</title> </head> <body> <h1>你好,世界!</h1> <button id="btn">点击发送</button> <script src="{% static 'jQuery.js' %}"></script> <script> $('#btn').click(function () { $.ajax({ url: '/base/', type: "get", data: { 'username': 'barry', }, success: function (res) { console.log(res) } }) }) </script> </body> </html>
验证,非同源情况下,浏览器默认不允许通信。
修改pro1的index页面: <script> $('#btn').click(function () { $.ajax({ {#url: '/base/',#} url: 'http://127.0.0.1:8002/base/', type: "get", data: { 'username': 'barry', }, success: function (res) { console.log(res) } }) }) </script> pro2: from app02 import views urlpatterns = [ path('admin/', admin.site.urls), path('base/', views.base, name='base'), ] # Create your views here. def base(request): print(request.GET) return JsonResponse({'response': 'ok'})
此时pro2项目后端已经接收到pro1发送的数据:
并且pro2的响应数据也已经响应给了浏览器,只不过浏览器进行拦截了, 所以数据没有最终发送到pro1的ajax中:
所以我们需要告知浏览器,这两个非同源的url可以互相通信:
def base(request):
print(request.GET)
ret = JsonResponse({'response': 'ok'})
ret['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8001'
# 如果我们允许多个源:
ret['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8001,http://127.0.0.1:8003..'
# 所有源:
ret['Access-Control-Allow-Origin'] = '*'
return ret
什么是请求定义为简单请求?
(1) 请求方法是以下三种方法之一:(也就是说如果你的请求方法是什么put、delete等肯定是非简单请求)
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,也就是说,如果你发送的application/json格式的数据,那么肯定是非简单请求,vue的axios默认的请求体信息格式是json的,ajax默认是urlencoded的。
必须满足以上两点要求的称之为简单请求,否则就是非简单请求。
简单请求和非简单请求的区别?
简单请求:一次请求
非简单请求:两次请求,在发送数据之前会先发一次请求用于做“预检”,只有“预检”通过后才再发送一次请求用于数据传输。
* 关于“预检”
- 请求方式:OPTIONS
- “预检”其实做检查,检查如果通过则允许传输数据,检查不通过则不再发送真正想要发送的消息
- 如何“预检”
=> 如果复杂请求是PUT等请求,则服务端需要设置允许某请求,否则“预检”不通过
Access-Control-Request-Method
=> 如果复杂请求设置了请求头,则服务端需要设置允许某请求头,否则“预检”不通过
Access-Control-Request-Headers
如果我们通过非同源发送一个非简单请求的数据,浏览器(或者后端)默认不允许访问的,也就是你的第一次遇见请求不能通过。我们可以设置让其通过。
pro1:index.html: $('#btn').click(function () { $.ajax({ {#url: '/base/',#} url: 'http://127.0.0.1:8002/base/', type: "get", contentType: 'application/json', data: JSON.stringify({ 'username': 'barry', }), success: function (res) { console.log(res) } }) }) pro2:base视图: def base(request): print(request.GET) ret = JsonResponse({'response': 'ok'}) ret['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8001' # 设置非同源的访问通过 ret['Access-Control-Allow-Headers'] = 'content-type' # 设置非简单请求的访问通过 return ret
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。