• 自学Python第二十二天- Django框架(三) Admin后台、生成前端代码、cookie/session/token、AJAX、文件上传、多APP开发


    Django官方文档

    django 使用 AJAX

    django 项目中也可以使用 ajax 技术

    前端

    前端和其他 web 框架一样,需要注意的是,django 接收 POST 请求时,需要 csrf_token 进行验证,而在 ajax 中获取 csrf_token 比较麻烦。所以通常会在后端免除 csrf_token 验证。

    绑定事件方式

    {% extends 'layout.html' %}
    {% block content %}
        <div class="container">
            <h1>任务管理h1>
            <input type="button" class="btn btn-primary" value="点击" onclick="clickMe();"/>
        div>
    {% endblock %}
    {% block js %}
        <script type="text/javascript">
            function clickMe() {
                $.ajax({
                    url: "/test/ajax/",
                    type: "get",
                    data: {
                    	type:'add',
                        n1:123,
                        n2:456
                    },
                    success: function (res) {
                        console.log(res);
                    }
                })
            }
        script>
    {% endblock %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    以 jQuery 方式

    {% extends 'layout.html' %}
    {% block content %}
        <div class="container">
            <h1>任务管理h1>
            <input type="button" class="btn btn-primary" value="点击" id="btn1" />
        div>
    {% endblock %}
    {% block js %}
        <script type="text/javascript">
            $(function (){
                // 页面框架加载完成后代码自动执行
                bindBtn1Event();        // 绑定事件
            })
            function bindBtn1Event(){
                $("#btn1").click(function (){       // 绑定到 btn1 的 click 事件的函数
                    $.ajax({
                        url: "/task/ajax/",
                        type: "POST",
                        data: $("#addForm").serialize(),
                        dataType:'JSON',
                        success: function (res) {
                            console.log(res);
                            // console.log(res.res_add)
                            // console.log(res['res_add'])
                        }
                    })
                })
            }
        script>
    {% endblock %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    后端

    后端部分只要将响应请求 url 绑定视图函数,则可以在视图函数中进行处理

    def task_ajax(request):
        """测试ajax"""
        return HttpResponse('成功')
    
    • 1
    • 2
    • 3

    从请求体中获取数据

    对于 request.POST 在使用中有一些限制:

    • 只能用于 POST 请求,对于 PUT、DELETE 等非 POST 请求不能使用
    • 只能用于表单数据,即 content-typeapplication/x-www-form-urlencoded 类型的请求

    对于其他数据,必须从请求体中获取。因为 request.body 中的数据是字节型,所以需要先解析。

    import json
    
    def data_form_body(request):
    	req_dict = json.loads(request.body)
    	category = req_dict.get('category)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    返回 json 数据

    可以使用 json 的 dumps 方法将字典转为 json 并返回

    import json
    
    def task_ajax(request):
    	res_dict = {'res':'ok', 'data':{'k1': 'v1', 'k2': 'v2'}}
    	res_dict = json.dumps(res_dict)		# 可以添加indent=2参数进行缩进,添加ensure_ascii=False参数显式中文
        return HttpResponse(res_dict)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    也可以直接使用 JsonResponse 返回数据

    from django.http import JsonResponse
    
    def task_ajax(request):
    	res_dict = {'res':'ok', 'data':{'k1': 'v1', 'k2': 'v2'}}
        return JsonResponse(res_dict)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    禁用 csrf 校验检查

    前端发送 post 请求时如果不加载 csrf_token,则后端需要禁用 csrf 校验检查

    from django.views.decorators.csrf import csrf_exempt
    
    @csrf_exempt
    def task_ajax(request):
    	return HttpResponse('成功')
    
    • 1
    • 2
    • 3
    • 4
    • 5

    或在注册路由时注明

    from django.views.decorators.csrf import csrf_exempt
    
    urlpatterns = [
    	path('goods/', csrf_exempt(views.goods), name='goods'),
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    或在settings.py 的中间件中取消 csrf 中间件(不推荐)。

    使用 csrf 校验

    有时候希望在 ajax 中使用 csrf 校验,首先需要获取 csrf token 的值,然后将此值附带在请求中发给 django。获取 csrf token 的值的方法有3种:

    • 使用模板语言 {% csrf_token %},在前端创建一个 元素,其 value 就是 csrf token 的值。
    • 在定义 js 变量时直接使用 {{csrf_token}} 将值赋值给变量,然后使用。
    • 从cookie中获取,cookie name 为 csrftoken。

    附带在请求中发给 django 校验主要有2种情况需要区分:

    • 前端发送的数据不是 json,是 application/x-www-form-urlencodedmultipart/form-data 或其他,则在发送的数据中添加一个数据字段 csrfmiddlewaretoken,其值就是 csrf token。
    • 前端发送的是 json 数据,则在请求头中添加参数 X-CSRFToken,其值为 csrf token。

    Ajax 结合 ModelForm

    Ajax 结合 ModelForm 在前端实际上几乎没有改变,要注意的也就是按钮绑定 Ajax 函数不用 csrf_token

    在后端,ModelForm 其实接收到的数据也是 form 的字典格式,包含校验、保存等处理方式没有变化,只是在返回值时有了变化。

    处理重定位信息

    因为 Ajax 是接收处理数据,所以后端返回 重定位 信息是无法跳转的。如果希望跳转,则需要返回一个 json ,ajax 收到了这个特定的 json 数据后使用 js 进行页面跳转。

    处理表单错误信息

    另外返回 ModelForm 错误信息时,form.字段.errors 获取的是一个字典,可以整理成 json 格式返回给 ajax 处理。

    
    <form id="addForm" novalidate>
        <div class="clearfix">
            {% for field in form %}
                
                <div class="col-xs-6">
                    <div class="form-group">
                        <label>{{ field.label }}label>
                        {{ field }}
                        
                        { field.errors.0 }} -->
                        <span style="color: red">span>
                    div>
                div>
            {% endfor %}
            <div class="col-xs-12">
                <button type="button" id="btnAdd" class="btn btn-primary">添加button>
            div>
        div>
    form>
    <script type="text/javascript">
        $(function () {
            // 页面框架加载完成后代码自动执行
            bindBtnAddEvent();
        })
        function bindBtnAddEvent() {
            $("#btnAdd").click(function () {
            	$(".error-msg").empty();        // 清空上一次的错误信息
                $.ajax({
                    url: "/task/add/",
                    type: "POST",
                    data: $("#addForm").serialize(),
                    dataType: 'JSON',
                    success: function (res) {
                        if(res.status){
                            alert("添加成功!");
                        }else{
                            console.log(res);
                            $.each(res.error,function (name,data){      // 循环每一个错误,获取键值
                                // 拼接 id_ 和 name,获取对应文本框的 id
                                // 查找文本框元素的下一个元素(span),使其文本(text)为错误信息
                                $("#id_" + name).next().text(data[0]);
                            })
                        }
                    }
                })
            })
        }
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    文件上传

    简单的文件上传

    前端会将文件以 post 请求发送至后端,后端可以使用 request.FILES 来接收。需要注意的是 form 需要 enctype 属性。

    <form method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <input type="text" name="filename">
        <input type="file" name="file">
        <input type="submit" value="提交">
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    file_obj = request.FILES.get('file')  # 获取上传文件的对象
    filename = request.POST.get('filename', file_obj.name)      # 获取设置的文件名,如果没有则为原文件名。
    with open(filename, mode='wb') as f:
        for chunk in file_obj.chunks():  # 将上传文件分块读取
            f.write(chunk)
        f.flush()	# 文件写入完成,冲刷缓冲区
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ajax 上传文件

    ajax 上传文件和发送 json 的不同在于,发送的数据是一个 FormData 对象,创建这个对象时可以将表单 form 的 dom 对象传入。

    $("#upload").click(function () {
       var formData = new FormData($('#uploadForm')[0]);
       // 或使用 FormDate 对象添加文件对象的方式
       // var formData = new FormData();
       // formDate.append('file', this.file[0]);		//这里的 this 指向上传文件的 input 标签的dom对象
       // formDate.append('key', value);		// 可以添加其他的数据
       $.ajax({
        type: 'post',
        url: "https://****:****/fileUpload", //上传文件的请求路径必须是绝对路劲
         data: formData,
         cache: false,
         processData: false,
         contentType: false,
          }).success(function (data) {
            console.log(data);
            alert("上传成功"+data);
            filename=data;
          }).error(function () {
             alert("上传失败");
         });
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    后端接收时,注意接收字段是前端定义的 name ,文件从 requests.FILES 里获取

    uid = requests.POST.get('uid')		# 获取数据
    file = requests.FILES.get('file')	# 获取文件
    # files = requests.FILES.getlist('files')	# 获取多个文件
    
    • 1
    • 2
    • 3

    使用 Form 组件上传文件

    使用 Form 时可以定义文件字段 FileField ,定义后就能够上传文件了。

    class UpForm(forms.Form):
    	name = forms.CharField(label='姓名')
    	age = forms.IntegerField(label='年龄')
    	img = forms.FileField(label='头像')
    
    def upload_form(request):
    	form = UpForm(data=request.POST, files=request.FILES)
    	if form.is_valid():
    		print(form.cleaned_data)
    		# 文件在 form.cleaned_data 的相应字段(img)内
    		img = form.cleaned_data.get('img')
    		file_path = os.path.join('app01', 'static', 'img', img.name)	# 拼接文件路径
    		with open(file_path,'wb') as f:		# 写入文件
    			for chunk in img.chunks():
    				f.write(chunk)
    
    	else:
    		return render(request, 'upload.html', {'form': form})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    <form method="post" enctype="multipart/form-data" novalidate>
    	.
    	.
    	.
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    文件上传至上传目录 media

    通常使用的静态资源都在 static 文件夹内,如果想要用户上传文件到一个特定文件夹,例如项目根目录下的 media 文件夹,则需要在 urls.py 中进行设置启用。首先引入一些资源,再在 urls.py 文件的 urlpatterns 字段添加:

    from django.views.static import serve
    from django.urls import path, re_path
    from django.conf import settings
    
    re_path(r'^media/(?P.*)$', serve, {'document_root': settings.MEDIA_ROOT}, name='media'),
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后在 settings.py 中进行配置:

    import os
    
    MEDIA_ROOT = os.path.join(BASE_DIR, 'media')		# 项目根目录下的 media 目录
    MEDIA_URL = '/media/'
    
    • 1
    • 2
    • 3
    • 4

    此时上传文件保存的路径可以写为

    media_file_path = os.path.join('media', image_obj.name)
    
    • 1

    这样将用户上传的文件放在 media 目录下,也可以通过 /media/文件名 的 url 来访问,例如 http://127.0.0.1:8000/media/001.png

    使用 ModelForm 组件上传文件

    ModelForm 组件可以自动上传文件,并存储保存路径到数据库,不用再写相应保存的代码,且能够自动进行重名处理。只是需要设置了上传保存目录。

    需注意的是,保存到数据库中的文件路径也是 media 下的相对路径,并且不包含 media ,使用时候注意添加 media。

    # models.py
    
    class City(models.Model):
    	"""城市"""
    	name = models.CharField(verbose_name='名称', max_length=32)
    	count = models.IntegerField(verbose_name='人口')
    
    	# 本质上数据库存储的是文件路径,也是CharField,可以自动保存数据
    	# upload_to 指的就是上传文件到哪个目录,是 media 目录下的相对路径
    	img = models.FileField(verbose_name='logo', max_length=128, upload_to='city/')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    # views.py
    
    class UpModelForm(forms.ModelForm)
    	class Meta:
    		model = models.City
    		fields = '__all__'
    	
    def upload_modal_form(request):
    	form = UpModelForm(data=request.POST, files=request.FIELS)
    	if form.is_valid():
    		# 保存文件,保存 form 字段信息和文件路径并写入数据库
    		form.save()
    		return HttpResponse('成功')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    POST 请求类型之间的转换

    POST 和 GET 请求是最常用的两种请求方式。一般简单的请求使用 GET,如果需要附加大量参数、数据,都会选择 POST。之后也有将POST的功能进行具体区分,则出现了 PUT、DELETE、OPTIONS 等请求类型。

    常见的四种编码方式

    HTTP 协议规定POST 提交的数据必须放在消息主体(entity-body)中,但协议并没有规定数据必须使用什么编码方式。常见的四种编码方式有:

    1. application/x-www-form-urlencoded

    最常见的 POST提交数据的方式。浏览器的原生 form表单,如果不设置 enctype属性,那么最终就会以application/x-www-form-urlencoded方式提交数据。

    1. multipart/form-data

    另一种常见的 POST数据提交的方式。我们使用表单上传文件时,必须让 form的 enctyped 等于这个值。

    1. application/json

    application/json 就是传说中的 json 格式,实际上,现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的 JSON字符串。由于 JSON规范的流行,除了低版本 IE之外的各大浏览器都原生支持 JSON.stringify,服务端语言也都有处理 JSON的函数。

    1. text/xml

    它是一种使用 HTTP作为传输协议,XML 作为编码方式的远程调用规范。

    python中读取各类型请求中的数据

    转换分为读取和发送,实际上发送的 response 包类型基本都能被浏览器正常识别(根据请求头中的 content-type)。经常使用的有:text/javascript、text/html、application/json 和一些例如 image/png 之类的媒体类响应报文了。

    这里使用的环境就是在 django 中。

    需注意的是,如果后端返回的响应使用 json 格式,则需要使用 json.dumps() 将其序列化。

    application/x-www-form-urlencoded

    这种 form 格式虽然发送的是 POST 请求,但实际上是将数据体作为类似于 GET 请求的方式发送的,而不是对象或序列化字符。

    url: 'www.test.com/'
    body: "id=1&name=zhangsan&age=23"
    
    • 1
    • 2

    只是前端发送工具的不同会经过相应处理,例如 jquery 可以直接将 json 对象处理后发送,fetch 不进行处理。

    django 可以直接使用 request.POST.get() 方法获取,获取的 POST 数据以字典形式保存,但是字典内部不可以进行嵌套字典。

    如果前端发送的是嵌套的对象,django 会将嵌套的对象进行转换,例如:

    // 前端发送的数据
    {
    	status:true,
    	person:{
    				name:'张三',
    				age:18,
    				sex:'male'
    			}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    # django 会将数据解析成这样,类型为字典
    {	
    	'status':'true',
    	'person[name]':'张三',
    	'person[age]':'18',
    	'person[sex]':'male'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果前端进行了序列化,则后端可以反序列化获得相应的数据。方式同 json 数据的序列化与反序列化。

    multipart/form-data

    前端需要注意的是,multipart/form-data 类型前端是要发送一个 FormData 对象的,直接发送字符串文本、json对象等都无法正常处理。可以将数据对象(包括字符串或json对象)添加到 FormData 对象中发送。使用 jquery 发送时,需要设置 content-type=false ,这样会自动添加请求头为 content-type:multipart/form-data; boundary=----WebKitFormBoundaryyr7L6TwhhOAIoEyn。同时需要设置 processData=false,不处理转换数据。

    这种格式可以直接使用 request.POST.get() 方法获取各参数的值,类型是字符串类型。

    因为 request.POST 是一个 QueryDict 对象,不能直接反序列化,所以其第一级属性必须直接获取,例如 get() 方法。此方法获取到的都是字符串,哪怕字符串内容是字典类型(前端发送的数据对象)。

    如果浏览器将一个数据对象(类似于json的对象)作为 FormData 对象的一个属性值这种方式传送过来,获取的则是字符串类型的:[object Object]。这时需要前端将其(数据对象)序列化,例如使用 js 里的 JSON.stringify() 方法。后端收到这个值类型是字符串,内容是字典数据。

    后端接收到后,获取的值是序列化后的字符串,可以使用 json.loads() 来将其转为字典格式。

    上传文件也需要通过这种格式,可以使用 requests.FILES.get('file') 来获取。

    application/json

    前端发送 json 时,注意要使用 JSON.stringify() 将发送的 json 对象序列化,后端才能正常获取。

    将请求体使用 json.loads() 反序列化得到字典类型。

    if request.method == 'POST' and request.content_type == 'application/json':
    	dic = json.loads(request.body)
    	param1 = dic.get('param1')		# 如果不知道字典中是否有 param1 参数,可以使用字典的 get 方法,否则会报错
    	param2 = dic.get('param2')
    
    • 1
    • 2
    • 3
    • 4

    多 app 开发

    django 多人协作开发时,每人开发不同的 app 也可能会遇到一些问题:对于相同名称的模板或静态资源的使用问题。因为 django 搜索静态资源和模板是安装 app 注册的顺序,在各 app 目录下查找模板或静态资源文件。如果有相同名称的模板或静态资源文件,后注册的 app 就会使用先注册 app 的同名文件了。

    所以对于模板文件来说,在各自 app 下的 templates 文件夹下建立 app 名称的文件夹,再建立模板文件,使得各模板文件引用时会加入各自 app 的名称,这样就不会出现引用路径相同的问题了。

    对于静态资源文件,因为涉及公共静态资源,所以需要注册静态资源路径。

    # settings.py
    
    STATIC_URL = '/static/'		
    STATICFILES_DIRS = [
      os.path.join(BASE_DIR, "static"),			# 主静态文件目录
      os.path.join(BASE_DIR, "main", "static"),		# main app 静态文件目录
      os.path.join(BASE_DIR, "login", "static"),	# login app 静态文件目录
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后可以在每个APP下的static下建立以APP名相同的文件夹,比如在 login/static/login/ 放入样式JS CSS等

    调用时使用 static 结构,加上 app 名称

    {% static 'main/img/firefox-logo-small.jpg' %}
    
    {% static 'login/img/name.png' %}
    
    • 1
    • 2
    • 3

    另外对于静态文件的打包整合可以参考这篇文章:

    解决django多个app下的静态文件无法访问问题

    使用 iframe

    django 使用 iframe 时,可能浏览器会出现错误,根据提示信息发现是因为 X-Frame-Options=deny 导致的。

    Refused to display xxx in a frame because it set 'X-Frame-Options' to 'deny'.

    官方文档中关于点击劫持保护

    点击劫持保护

    The X-Frame-Options HTTP 响应头是用来给浏览器 指示允许一个页面 可否在 ,