【Django】 视图层说明
【Django视图层】
视图层的主要工作是衔接HTTP请求,Python程序和HTML模板,使他们能够有机互相合作从模型层lou到数据并且反馈。说到视图层的工作就有以下几个方面要说
■ URL映射
对于一般的,通过django.conf.urls.url设置url路径,并且关联视图函数,甚至把url方法的参数写成正则表达式从而可以给视图函数传递多个参数的事情就不多说了。比如:
url(r'^single/([0-9]{4})/([0-9]{2})/([0-9]+)/$',views.single)
这个url映射就可以方便地把日期给映射到视图函数中,让其根据日期数据得到数据库中存储的数据,然后进行界面的渲染。
更高级一点的映射方式是,可以给正则表达式中的子模式添加名字。就像是format函数的{0}和{var_name}一样,在这里的正则表达式中可以写类似于r'^test/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$'这样在视图函数中就可以加上year=xxxx,month=yy这样来指定变量名了(当然request参数还是放在第一个且没有参数名)
include说得好听一点其实是Django支持分布式URL映射的一种体现。即URL映射规则不一定要写在一起,可以写在不同的模块中,通过include方法来进行统一的输出。
反向解析也是一个比较重要的点。我们知道在Django的后台Python代码中可以调用reverse(url_name)的方法来进行反向解析,然而在模板中我们也常常要使用反向解析。模板中应该写{% url 'url_name' %}来体现反向解析。反向解析通常还涉及到如何进行带参数的反向解析,前端可以: {% url 'url_name', args1,args2 %},而后台可以reverse('url_name', args=(args1,))这样。需要注意的是这里说的参数并不是像flask中url_for函数调用出来的GET请求参数,而是之前URL映射规则正则表达式中的子模式,把它看成一个参数的。
一般来说,我们使用reverse的时候都是通过一个特定url模式的name参数的值来定位的。比如有url(r'xxx', xx_view, name='test'),那么在后端的reverse('test')以及模板中的{% url 'test' %}这样来反向解析。但是有一个小问题,如果url模式很多的时候,name取名是一个问题。其实可以使用所谓的namespace,比如上述url是在app这个模块下的urls.py文件中,那么可以使用reverse('app:test'),以及,{% url 'app:test' %}来特别指出,这个test是app模块下的test,而不是其他地方name是test的url。
■ 视图函数
视图函数一定要有返回,一般可以有三种返回方式,分别是直接构造HTTP的BODY内容、用数据渲染HTML模板、返回错误或重定向。
返回HTTP内容用HttpResponse,渲染用render,重定向用redirect或者HttpResponseRedirect,而返回错误一般是HttpResponse(status=404)这样的。为了方便,Django也预定义了一些特别的类让开发者直接返回,包括:
HttpResponseNotFound 404
HttpResponseRedirect 302
HttpResponseBadRequest 400
HttpResponseForbidden 403
● ajax和django后端视图函数的配合
ajax在处理时会设定success和error等回调函数用来处理不同返回值。而在后端,通常抛出一个HTTP层面的错误可以通过上述手段或HttpResponse(status=400)之类的方法来进行。另外由于ajax请求的格式通常是JSON,所以会加上content=json.dumps({xxx}),content_type='application/json'这样子的HttpResponse参数。
如果不返回错误而是成功的话就有个小坑,如果直接写return HttpReponse()或者HttpResponse(status=200)的话,在前端ajax那里并不会进入success函数,而还是进入error函数。因为请求的是JSON格式数据而返回的是空数据。所以为了在前端ajax能顺利进入success函数执行,应该写HttpResponse(status=200,content=json.dumps({}),content_type='application/json')这样,返回一个空的JSON格式的串回去即可。
● 处理非常见类型HTTP请求
对于GET请求和POST请求已经很常见了,但是对于不那么常见的比如PUT,PATCH,HEAD之类的请求,处理方法又有点小区别。
首先1. 如果没有关闭django默认的csrftoken机制,那么PUT, PATCH这类请求也都是需要有csrftoken验证的。并且他们的token不能放在数据体中,而是要放在headers中才能被django后端识别。
换句话说,在全局ajaxSetup(data: {csrfmiddlewaretoken: {{csrf_token}})这样的操作不奏效了,得ajaxSetup({headers: {'X-CSRFTOKEN': $.cookie('csrftoken')})才有用
2. PUT, PATCH等请求虽然可以带数据体,但是数据体并不能在后端被类似于request.PUT的东西获取。而是原生在request.body中,需要我们手动取出来。做法就是
from django.http import QueryDict
putData = QueryDict(request.body)这样做了之后,可以像用request.POST那样使用putData这个东西了
■ 模板语法
Django的模板和Jinja2很类似但不完全一致。
{{}} 和Jinja2一样都是变量替换,在其中写上变量名或者方法名
{{|}} 过滤器,将过滤器放到|符号的右边,在模板层面上对传递过来的值进行进一步的修正。
说到过滤器有必要讲讲Jinja2和Django模板语法的一个很大的差别。Jinja2把模板语法调教得和Python很接近,比如方法都是直接用括号来调用的,但是Django中一般调用方法都不加括号,直接写个方法名即可。比如前端验证用户是否登录的话用{% if request.user.is_authenticated %}不用加括号。那如果一些方法比如default这个过滤器方法需要值怎么办?就这样: {% value | default:"(N/A)" %}。
过滤器多种多样,这里也不想写了,可以看下书的214页和官方文档以了解用法。
流程控制和模板继承和Jinja2类似的,都是{% for xx xxxx %}{% endfor %}、{% if xxx %}{% endif %}、{% extends "xxx.html" %}这样。
● 关于django模板的自动转义
在实际开发过程中,发现了django模板会对传递过去的内容自动进行基于html语法的转义。比如在模板中有<td>{{ description }}</td>,然后后端的description='1<br/>2'的话,最终页面显示的是1<br/>2而不是两行内容。这说明"<"和">"在模板渲染过程中被自动转义了。如果想要避免这种自动转义有两个方法:第一,就是在模板中加上safe过滤器,即<td>{{ description|safe }}</td>即可。如果需要关闭自动转义的变量比较多,一个一个写safe很麻烦的话也可以用{% autoescape off %}{% endautoescape %}这个区块来包裹这一片区域,使这一片区域中都不再自动转义。
另外在额外提一句,在上面这个页面里我还设计了一个查看更多的按钮,然而在预览界面内容是好好换行的,但是点击按钮,取的含有<br>的内容再text(content)到一个DOM中时发现就不换行又变成<br>了,这是因为用了text方法。。。应该用html(content),就可以了。
■ 自定义模板中的标签和过滤器
如果想要在django模板中自定义过滤器和标签,可以按照以下做法来做。
在某个(可以新建)app中建立一个python包,名为templatetags,这个名字是django规定好的。因为是个python包,所以必然有个__init__.py文件在其中,内容可以置空。
然后在这个python包下建立一些py文件,自定义的过滤器和标签都是写在这些文件当中的。
● 自定义过滤器
对于自定义过滤器,比如我们建立了一个custom_filters.py文件:
# custom_filter.py文件中 from django import template registser = template.Library() @register.filter #通过装饰器来自定义过滤器 def my_filter(value): ''' 自定义过滤器函数,接受一个value参数,作为模板传入函数的值 最后返回一个字符串类型 需要注意的是value不一定是字符串类型,而是看模板具体传参是什么。 ''' if type(value) is not str: value = str(value) if len(value) < 5: return '{:0>5}'.format(value) else: return value
然后在模板中:
{% load custom_filters %} {# 通过load <脚本名> 来想本模板加入自定义的内容 #} {{ '123'|my_filter }} {# 最终渲染出的效果就是00123 #}
通过装饰器来绑定函数和过滤器时,默认过滤器的名字是函数名,filter(name='xxx')可以手动重新指定名字。除了装饰器的方式,也可以通过register.filter('name',some_function) 的办法来实现。
django模板传递字符串时默认是会转义的(这也是escape过滤器存在的意义),如果自定义过滤器想要处理源字符串而不是被转义过的,可以添加is_safe=True参数,此参数默认是False。
● 自定义标签
自定义标签的编写过程比过滤器略复杂一些。首先要明白一个标签从模板中的标签变成一段HTML代码,有两个过程分别是编译和渲染。编译从源代码的字符串中提取出有效的信息,渲染对这些信息做二次加工。在这方面,这篇文章很棒可以参考。
标签有很多种,我们先从一种比较简单的入手。这种标签的语法是类似 {% tagname "arguments" %} ,它可以直接进行一些函数操作。从而为页面提供一些HTML代码或者逻辑。我觉得上面给出的那篇文章中的例子实在是太好了,就依葫芦画瓢,按照那个例子来说明一下。
比如我们要注册一个{% now %}标签,这个标签还可以接受一个参数,即格式化时间的字符串,比如{% now '%Y-%m-%d %H:%M:%S' %},最终页面上就会出现指定格式的当前的时间。
首先注册自定义标签和过滤器的方法差不多,然后自定义标签注册的函数应该是这样的:
from django import template register = template.Library() @register.tag(name='now') def get_current_time(parser,token): try: tag_name, time_format = token.split_contents() except ValueError,e: raise template.TemplateSyntaxError('%s tag only takes one argument' % token.split_contents()[0]) if not time_format[0] == time_format[-1] or time_format[0] not in ('"','\''): raise template.TemplateSyntaxError('%s should be quoted' % time_format) return CurrentTimeNode(time_format[1:-1])
接收两个参数,parser是模板解析器对象,token是一个含有模板源代码的对象,通常使用token.contents来指代源代码内容,即"now '%Y-%m-%d %H:%M:%S'"。split_contents方法是一个比较适应django模板的方法,它可以将token.contents比较只能地分成标签名+参数字符串两部分。如果使用split()就可能会把参数字符串中间的空格也分开了。另外如果有更加复杂的参数输入需求,也可以直接用正则来匹配,总之这一步做的是一个编译解析模板源代码的工作。
可以看到,在合适的地方我们抛出了template.TemplateSyntaxError这个错误来提示模板语法出错以及具体出了什么错。
下面的判断条件,其实是检查了我们的参数是否被引号合法地括起来了。如果不是也会抛出错误。在这个执行编译工作的函数的最后,返回的是一个CurrentTimeNode对象,这个对象所属的类也是我们自己定义的,称之为节点对象。其本质是一个进行渲染的工具类。这个类的实现是这样的:
from django import template import datetime class CurrentTimeNode(template.Node): def __init__(self,time_format): self.format = time_format def render(self,context): return datetime.datetime.now().strftime(self.format)
它继承自template.Node类,并且要实现render方法。编译阶段解析出来的参数可以通过__init__方法向这个类的对象传递并被render方法使用。render方法还带来一个参数context。此参数包含了渲染时用到的大部分上下文信息。比如context['request']就是我们在模板中调用的request对象,context['user']是request.user,context['xxx']是我们在后端view函数中给出的自定义的上下文。这些都可以拿来用。十分方便
导入的template模块带有Context类,它可以用来实例化一个新的上下文对象。render方法中的参数context就是它的实例,另一方面,我们也可以自己实例化一个Context的对象,来做一些事情。比如传入autoescape=False这个参数来使得这个上下文中的内容被渲染时不会被自动转义。
* 关于并发环境下的渲染
在生产环境中,django工程往往是在并发环境下的,而模板肯定只有一个。这就导致有可能在非常短的时间段内,两个不同请求分别要求渲染,如果此时自定义标签渲染的render函数中涉及到使用公共资源的话,那么有可能会发生线程不安全的情况。
上面的参考文中就提到了一种情况,比如渲染时要从一个列表['v1','v2']中滚动地取值,理想的情况是一个请求先取一次v1,再取一次v2,如此便可渲染出v1v2都有的页面。但是如果两个请求并发进来,很可能req1取的v1,req2取得v2,req1再取v1,req2再取v2。为了避免这种情况,最主要就是要在render中识别出不同的请求。比如我们可以通过一个字典,把两个请求作为两个key,value分别赋值这个列表,如此两个请求就可以稳妥地从各自的列表中取正确的值。然而这个字典显然不能是个render函数中的局部变量,解决的办法是context中的render_context,这就是个预设好的,不局部的字典,专门为了解决这种多线程或者并发场景下的冲突问题。
● 在自定义标签中调用模板变量
之前有个很纠结的事情,想要把某个模板变量如request.user作为一个参数传递给一个标签,但是{%%}里面是不能再写{{}}或者{%%}的。作为解决办法,可以使用自定义标签。这就涉及到如何在自定义标签中调用模板变量。一般来说可以这么干:
@register.tag(name='form_url') def get_form_url(parser,token): try: tag_name,user_var = token.split_contents() #将模板变量作为一个字符串解析出来 except Exception,e: raise template.TemplateSyntaxError('%s should have only one solid parameter') return FormUrlNode(user_var) #传递给渲染器类 class FormUrlNode(template.Node): def __init__(self,user_): self.user = template.Variable(user_) #将字符串变成变量 def render(self,context): user = self.user.resolve(context) #将变量和上下文关联 return reverse('userinfo_page',kwargs={'username':user.get_username()}) #使用user对象,可直接调用get_username方法 ''' 前端标签写{% form_url request.user %} '''
● simple_tag的用法
上述自定义标签方式可以说是比较完整的流程。如果是个很简单的标签也可以用simple_tag这个路子,不仅方便而且功能也基本齐全。
simple_tag是template.Library中的一个装饰器,被其修饰的函数可以接受任意多个参数并且渲染出一段内容。同时也支持从上下文中获取模板变量。是这么用的:
''' {% now '%Y-%m-%d' %} 渲染为 2018-01-01这样子 ''' @register.simple_tag(name='now') def get_current_time(time_format): return datetime.datetime.now().strftime(time_format) ''' {% userandtime '%Y-%m-%d' %} 渲染为 Frank2018-01-01 ''' @register.simple_tag(takes_context=True,name="userandtime") def get_user_and_time(context,time_format): user = context['user'].get_username() time = datetime.datetime.now().strftime(time_format) return user + time #这个函数需要注意的是当设置takes_context为True时,函数的第一个参数必须名为context才能顺利识别上下文对象,然后才能从中取值 ''' {% now '%Y-%m-%d' 2018 month='01' day='01' %} 转化为 2018-01-01(指定参数) ''' @register.simple_tag(name="now") def particular_time(time_format,*args,**kwargs): year = int(args[0]) month = int(kwargs.get('month')) day = int(kwargs.get('day')) return datetime.date(year,month,day).strftime('%Y-%m-%d')
■ 表单编程
Django自带了对表单的处理工具使得我们可以方便地构建表单。下面将进一步地说明Django的表单是如何作用的。
Django为所有继承自Form的类维护了一个bound状态,当一个表单对象在实例化的时候被赋予过数据则这个表单对象就是被绑定过的,其bound属性是True。反之当没有绑定过数据的自然就是unbound状态的表单。只有unbound状态的表单可以被赋予数据而只有bound状态的表单可以来验证数据。
● 表单数据验证
狭义上来说,表单数据验证就是指在服务器端用Python代码来验证输入数据的合法性(不包括前端的表单验证)。这种意义上的表单验证分成两种,分别是字段属性验证和自定义逻辑验证。
字段属性验证是指在创建表单类的时候就指定的字段的一些固有属性,比如CharField会指定max_length属性等等。这些验证实际上是HTML对输入的约束,不涉及到js或者django代码,而是HTML自带的一种机制。比如max_length实际上是在<input>标签中添加了maxlength属性,其表现是输入达到最大值之后再敲键盘也输入不进新内容了。不同的浏览器对这些约束的体现也不尽相同,总体而言一般简单的约束就是可以通过字段自带的一些属性来进行。
对于一些较复杂的验证逻辑(比如看输入中含不含有某些值等等),可以通过自定义逻辑来实现,这需要重载表单类的clean方法,比如:
class Moment(forms.Form): class Meta: model = Moment fields = '__all__' def clean(self): cleaned_data = super(Moment,self).clean() #先调用一次父类clean方法 content = cleaned_data.get('content') if content is None: raise ValidationError('输入不能为空') elif content.find('敏感词') >= 0: raise ValidationError('输入中含有敏感词') return cleaned_data #最终返回验证完后的数据
从中也可以看到,在验证的时候对于判断为验证通过的不用做什么特殊处理,而对于验证不通过的我们要raise forms.ValidationError来提示。如此通过raise一个错误来提示确实比较方便,不过有些丑,其效果就是在表单上方添加一个ul.errorlist里面写上我们raise出的错误信息。而且不带任何CSS,直接是一个原生的li的风格。。
如果想要改变这种丑丑的,那么可以考虑1.修改CSS,2.自己加上一些JS来处理。
其实说到自定义表单验证,其实表单类中重载clean方法究其原理还是把表单数据通过GET方法传送到后台让后台判断的。因为这个clean方法的代码一般是写在forms.py文件中的,姑且称是从表单类角度出发的验证手段吧,另外一种在views.py中也可以写验证啊,只不过这些验证代码放在这里的话会让views.py代码略显臃肿一点。但是放在views.py中有一个好处就是我们可以使用django.contrib.messages了。这个组件是类似于flask中的flash消息那样的东西。比如:
def user_login(request): form = LoginForm() if request.POST: form = loginForm(request.POST) #处理表单数据,然而再此之前可以先做验证 if form.is_valid(): username = request.POST.get('username') if 'frank' in username: messages.error(request,'frank你被禁止登录了') return redirect(reverse('psw_login')) #user = authenticated(xxxx)一些登录用户之类的操作
ctx = {} ctx.update(csrf(request)) ctx['form'] = form return render(request,'login.html',ctx)
顺便一提在前端该写的东西:
{% if messages %} {% for message in messages %} <div class="alert alert-warning"> <button class="close" type="button" data-dismiss="alert">×</button> <h4>{{ message }}</h4> </div> {% endfor %} {% endif %}
这只是个参考,精髓在{{ message }}上面。想咋用咋用。
需要注意的是,在messges.error向前端发送了警告信息之后,后面应该尽量跟return redirect到当前页面,因为messages和flash消息类似,是一个消息队列的形式,加入这次pop出去的消息没有第一时间渲染到页面上而后来又新加了几个消息的话,搞不好会在某次渲染的时候渲染出多个消息(当前队列中的所有消息)。另外把这个return换成raise ValidationError也是不行的,因为这是在views里面,函数不能没有返回值。关于messages的更多用法我想有机会在下面继续详细地说说。
■ 个性化管理员站点
Django项目都默认添加了/admin这个路径的管理台。如果认为管理台的功能不能满足当前的需求的话那么可以通过继承Django定义的管理员数据模型,模板,站点类来开发出个性化的数据模型管理功能。这部分功能其实就是我们之前那篇文章中提到过的记性类属性fields,fieldset等的设置。
● ModelAdmin模型
通过继承django.contrib.admin.ModelAdmin类可以自定义一个数据管理模型,在其中修改一些属性值来体现出和默认情况下管理台的一些不同。这部分操作一般在模块下的models.py文件中进行,ModelAdmin类可以改变的常用属性有以下这些:
empty_value_display 设置成一个字符串以改变空值的显示方式
field和exclude 上篇中提到过,通过白/黑名单方式设置一部分供编辑的字段
fieldsets 上篇中提到过,配置字段的分组以美化界面
list_editable 设置字段列表,规定模型中的哪些部分的字段在展示界面就可以编辑,没有被设置的字段将不能被编辑
list_per_page 整数,指定每页展示的实例数量最大值,默认值是100
search_fields 设置字段列表,在搜索界面中管理员可以按照这些字段的值来搜索
ordering 设置字段列表,定义管理页面中模型的排序方式
为了能够正确地显示出管理台界面,这里还需要提两点。1.正如之前所说的那样,在得到重载完成的ModelAdmin之后,需要在模块的admin.py中进行注册。不过这里的注册和一般models.py中的模型注册还不太一样。一般模型就是admin.site.register(模型名)即可。这里需要admin.site.register(关联模型名,重载ModelAdmin名)这样来做,否则会报MediaDefiningClass is not iterable之类的错误。 2. 如果启动应用还是报错的话请注意留意报错信息。比如有可能是重载后的ModelAdmin类是一定需要list_display属性等等。。
● 页面模板
如果说上面通过重载ModelAdmin类进行的修改只是小打小闹的话,直接修改页面的模板可以说是比较彻底的做法了。Django管理台相关的模板都放在了Django的模块目录(Windows的话通常在$PYTHON_HOME\site-packages\django)下的contrib/admin/templates/admin目录下。这些模板也都是按照Django的模板语法来的,所以可以根据这些模板的结构来重新定制化自己的页面。自己的页面模板可以放在templates/admin下,这个admin需要自己创建。
下面以修改login.html为示例进行演示:在项目目录下创建templates/admin/login.html模板:
{% extends 'admin/login.html' %}
{#content_title这个block是自带的login.html模板中定义的#}
{% block content_title %}
欢迎来到Django管理界面
{% endblock %}
此时再访问admin页面时,在用户名输入框的上方就有一个中文的提示语了。
● 其他修改
和ModelAdmin类似的,在django.contrib.admin中还有一个AdminSite类。这个类代表的是整个管理的模块,可以进行一些设置如set_header等,需要注意的是注册的时候就不再是简单的admin.site.register了,而是要像下面这样:
from django.contrib.admin import AdminSite class MyAdminSite(AdminSite): site_header = '我的Django' admin_site = MyAdminSite() admin_site.register(xxx,xxx) #site变成了我们自定义的站点类实例,用它来注册各种模型
另外嘛就是可以把项目的urls.py中的^admin/的admin改成其他的内容来改变管理台的url。
■ 静态文件
关于Django的静态文件做的一些简单说明。
直接访问静态文件的方法表面上django和flask很相似,都是直接访问 /static/这个pathname下面的路径。然而这个在flask中似乎是默认的,而在django中是需要自己配置的。在我的实验环境中(django 1.11.6),settings文件夹中首先要加上这两个参数:
STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR,'static')
顾名思义,这两行分别指出了通过url访问文件时的根path和实际上静态文件在服务器上的目录位置。通常是放在目录下的static目录中的,所以默认值是这样的。
在确认这两个变量有值之后,需要在项目主urls.py中添加如下内容:
from django.conf import settings from django.conf.urls.static import static urlpatterns = [ r'^$',index_page, r'^app1/$',include('app1.urls'), #...其他一些url设置 ] + static(settings.STATIC_URL,document_root=settings.STATIC_ROOT)
在添加有这些配置之后,我们就可以通过/static/这个path来访问特定位置的静态文件了。另外,js,css这些静态文件通常要在页面中被用到。模板中的静态文件的引用方法是:
{% load static %}
{# 这里之前说过是load staticfiles 不过那是比较老的写法,在新版本的官方文档中是load static,所以我们按照新的文档来 #}
{% static 'path/to/file' %}
至此,我们就可以顺利在页面中引用静态文件了。
● 关于STATIC_ROOT和STATICFILES_DIRS的联系与区别
我本来以为上面的说明就够了,然而发现了一个神奇的现象。。上面的操作あくまで是为了在linux上通过uwsgi成功部署django项目,这么做确实也奏效。然而我把服务器上代码一模一样考下来在windows上的开发调试时反倒不行了。。发现区别主要在于settings.py中的STATIC_ROOT和STATICFILES_DIRS这两条配置中。而究其linux和windows的区别的根源,在于启动方式的不同。linux上用了uwsgi的命令启动的应用,而windows上用的是类似于python manage.py runserver的方式启动的。通过uwsgi启动时主要读取STATIC_ROOT配置,另一个可有可无;通过manage.py启动时读取STATICFILES_DIRS项配置,但是不能有STATIC_ROOT否则无法顺利访问静态文件。
那么为什么两种不同的启动方式需要不同的静态文件配置项呢?这还得从django中静态文件的位置说起。
Django中的静态文件通常可以存放在以下这些地方:1.某个app目录下的static目录中;2.总项目目录下的static目录中;3.任意一个配置指定的目录中。
settings.py文件在django项目建立时通常具有STATIC_URL这个默认参数,但是没有STATICFILES_DIRS以及STATIC_ROOT,如果我们没有设置这两者,直接用默认的settings.py,并且通过python manage.py runserver 来启动应用,那么此时django会从1,也就是所有app下的static目录中把文件加载到内存里。如果我们想要从2中也加载文件进内存,此时就要用到STATICFILES_DIRS配置项,事实上这个配置项可以是个列表,其中是各种目录名,通过加载这个配置项指定的目录中的文件达到加载静态文件的目的。也就是说2,3其实属于一种情况,都是把相关的目录直接写到STATICFILES_DIRS这个配置中而已。只不过2的目录比3要特殊一些罢了。所有被加载到内存中的文件,只要通过STATIC_URL加上文件相对到各自static目录的路径即可访问。这也导致了一个小问题,就是同名(同相对路径)文件怎么办,这个就要看加载的先后顺序了,经过试验观察,发现先按照顺序加载STATICFILES_DIRS,然后加载各个app下的static目录中的内容。同名的文件以先加载的文件为准。
上面的说明不涉及到STATIC_ROOT项配置,这个配置其实是在执行python manage.py collectstatic时用的,这个命令会把上面说的侦测到的(包括1,2,3三种目录下的)所有静态文件以及默认自带的admin模块中的文件复制到指定的目录下。STATIC_ROOT配置项的值必须是绝对路径。
了解两个配置项的含义之后,再来解释为什么不同启动方式需要不同的配置就简单一些了。可以想象,通过manage.py启动,肯定不会涉及collectstatic命令(因为用的是runserver命令),所以加载STATICFILES_DIRS足矣。而通过uwsgi这类容器启动时,容器不会加载STATICFILES_DIRS,而是盯住STATIC_ROOT,只要STATIC_ROOT里面没有,容器就认为这个静态文件不存在。之前因为把STATIC_ROOT刚好设置成STATICFILES_DIRS一样的目录,所以容器启动时不用其他操作,只要把STATICFILES_DIRS注解掉再把STATIC_ROOT写回来即可。但是一般情况下两者中间还差了一步手动操作的collectstatic。所以需要注意搞清楚,用uwsgi等容器部署前是否有collectstatic过。
● DEBUG配置对于静态文件的影响
刚才又发现了一个现象,混乱了。。。刚才满足了上面条件部署仍然获取不到静态文件,原来是DEBUG配成了False,而我没有提供nginx或其他的一些web服务器来提供静态访问入口。这导致了无法访问,DEBUG改回True立马恢复。。
今天总算是解决了当DEBUG配成False时,如何通过nginx提供静态文件。在nginx的配置中,需要添加一项监听/static/的配置,然后其带有一个参数alias,指向STATIC_ROOT指出的那个目录。然后在启动应用前进行collectstatic,如此再启动的应用,我们就可以顺利访问到静态文件了。附上nginx配置参考:
server{ listen 8080; server_name 0.0.0.0; charset utf-8; access_log /home/hima/log/nginx/access.log; error_log /home/hima/log/nginx/error.log; location / { include uwsgi_params; uwsgi_connect_timeout 30; uwsgi_pass 127.0.0.1:8088; } location /static/ { alias /var/www/hima/static/; #注意这里 index index.html index.htm; } }
■ 关于获取请求数据的一些内容
django中最常见的获取请求参数的用法是request.GET.get('xxx')和request.POST.get('xxx'),两者分别用来获取GET和POST请求时的参数。大多情况下够用,但是有些时候也会失灵。比如之前说过在用rest_framework的时候会发生空数组无法上送的问题(当然后来发现这不是后端问题,而是前端发起ajax时需要将traditional设置为true并且指明contentType是json)。
● POST中的数组类型
获取POST中数组类型的参数时,首先需要明确,应该使用request.POST.getlist而不是POST.get。django为获取数组类型数据包装了getlist方法直接使用。获取到的数组值可能是None,另外我还遇到过POST变成一个空的QueryDict。获取到None很可能是因为ajax上送时没有traditional: true,导致数组名不是xxx而是xxx[],上送时加上true的traditional或者后端获取时用xxx[]来获取即可。
空QueryDict的原因可能是上传时指定的contentType不是formdata。比如像上面说的contentType是application/json的时候,数据不在request.POST中,而是在request.body中,并且是JSON字符串,还要我们手动json.loads一下才行。