Summary of Django Framework Knowledge
Preface
最近做蓝鲸SaaS开发课程的理论考试,感觉自己对Django的原理了解得不够深入,故作此文聊以总结。
主要参考资料是Django官方文档,蓝鲸SaaS开发课程为辅。
Project Init
安装好Django后,可以通过如下命令构建Django项目:
django-admin startproject [project_name]
cd [project_name]
python manage.py startapp [app_name]
python manage.py migrate # 初始化数据库
其他常用的指令如下:
python manage.py craetesuperuser # 创建管理员
python manage.py runserver {port} # 运行Django服务
python manage.py makemigrations # 创建数据配置文件,显示并记录所有数据的改动
python manage.py migrate # 将表结构的改动更新到数据库
关于django-admin
和manage.py
常见用法,可以看这篇帖子。
Django Life Cycle
也叫请求处理流程,指的是用户从浏览器上输入URL到用户看到网页的这个时间段内,Django后台所发生的事情。流程如下:
-
用户(浏览器)发起一个请求。
-
请求经由uwsgi处理,转发到请求中间件(Request Middleware),请求中间件会对请求进行预处理(或直接返回请求)。
-
请求到达由
settings.py
中ROOT_URLCONF
指定的路由文件(默认为urls.py
),通过URL匹配至对应的view。 -
请求到达视图中间件(View Middleware),视图中间件对请求做一些预处理(或直接返回请求)。
-
请求到达视图文件,调用对应的视图方法,进行逻辑处理。
-
View中的方法可以用个Model访问持久层的数据。
-
Model访问db的操作由managers进行控制。
-
View从Model中获取数据后,生成上下文(Context)并给Template传递数据。
-
Template获取Context后,使用Tags和Filters来渲染数据。
-
渲染完成后的Template被返回给View,View通过render函数生成一个HttpResponse对象,并把此对象发送给请求中间件。
-
请求中间件会对HttpResponse做一点处理后再返回给浏览器,呈现给用户。
Django Core Model Functions
settings.py
settings.py
是项目的全局配置文件,比较常用的是DB配置。
# USE FOLLOWING SQL TO CREATE THE DATABASE
# SQL: CREATE DATABASE `[projectName]` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'projectName',
'USER': 'root',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '3306',
},
}
Django也支持一个项目使用多个数据库,配置方式如下:
-
在
settings.py
里的DATABASES
配置多个DB配置。 -
编写一个路由文件以定义路由规则,给每个APP指定数据库,并在DATABASE_ROUTERS中配置路由文件。
此外,Django也支持手工在具体的CURD中指定数据库。
urls.py
path([expresstion], [mapping_function], [kwargs], [alias]) # 精准匹配
pre_path([regular_expresstion], [mapping_function]) # 使用正则表达式来实现模糊匹配
views.py
Django的views.py
有两种模式:基于函数的视图(Function based view, FBV)和基于类的视图(Class based view, CBV)。
FBV
FBV即一条路由规则对应一个函数,通过一个函数去处理请求。一旦路由映射表的其中一条路由规则匹配成功,就执行view函数中对应的函数名。这里给一个完整的例子:
# urls.py
from app_name import views
urlpatterns = [
path('', views.mainpage)
]
# views.py
from django.shortcuts import HttpResponse
def mainpage:
return HttpResponse('Hello and welcome!')
这里再特别解释下HttpRequest
和HttpResponse
:
HttpRequest.xxx:
body: 请求报文的主体
path: 请求的路径
method: 请求的http方法
encoding: 提交数据的编码方式
GET: 包含HTTP GET的所有参数
POST: 包含HTTP POST的所有参数
META: HTTP首部信息
HttpResponse.xxx:
content: 响应内容
charset: 响应内容的编码
status_code: 响应的状态码
CBV
即一条路由规则对应一个类。用户发给服务器的请求包含URL和method(均为str类型),然后服务器通过URL或者method去匹配类中的方法来处理请求。
服务器通过路由映射表匹配成功后会自动去找dispatch方法,然后Django会通过dispatch反射的方式找到类中对应的方法,并执行之。类中的方法执行完毕之后,会把客户端想要的数据返回给dispatch方法,由dispatch方法把数据返回给客户端。
仍然举一个完整的例子:
# urls.py
from app_name import views
urlpatterns = [
path('demo/', views.Demos.as_view())
]
# views.py
from django.views import View
from django.shortcuts import HttpResponse
class Demo(View):
"""
a simple CBV class
"""
# 处理GET请求
def get(self, request):
return HttpResponse('GET method!')
# 处理POST请求
def post(self, request):
return HttpResponse('POST method!')
注意到路由配置中,调用了views.Demo
类的as_view()
方法,且views.py
中的Demo类继承了View类。as_view()
方法是基类View中定义的一个classonlymethod
方法,每个请求都要经过这个方法进行处理,然后通过其定义的dispatch()
方法将请求分发到对应的函数去处理。
使用CBV有一定的优点:可以使用面向对象的技术,提高代码的可复用性;可以针对不用的HTTP方法,使用不同的函数进行处理,提高代码可读性。
models.py
Model负责与db打交道,我们可以在Model中定义db的表结构,通过两行指令同步到db。Django还为我们提供了Model的版本管理,可以方便地追溯每一次db表结构的变更。依然是举一个完整的例子:
# models.py
from django.db import models
class Book(models.Model):
"""
Information of book
"""
AUTHOR_CHOICE = [
('author1', 'Tom'),
('author2', 'Bob'),
('author3', 'Jack')
]
book_name = models.CharField(maxlength=64, unique=True)
author = models.CharField(maxlength=64, null=True, blank=True, choices=AUTHOR_CHOICE)
create_time = models.DateTimeField(auto_now_add=True)
update_time = models.DateTimeField(auto_now=True, null=True, blank=True)
class Meta:
app_label = 'app_name' # 定义model所属app
db_table = 'table_name' # 定义表名
ordering = ['-id'] # 默认排序方式
indexes = [
models.Index(fields=['id', 'id'])
]
至此,可以通过以下两行指令将表结构同步到db:
python manage.py makemigrations [app_name] # 创建脚本
python manage.py migrate [app_name] # 数据迁移
执行makemigrations将在app下自动创建一个migrations目录,并在此目录下创建一个0001_initial.py
文件,文件中是即将被执行的脚本内容。
执行migrate会去读取db中django_migrations表(这张表在初始化数据库中就已经被创建)中执行migrations脚本的记录,将未执行的migration脚本都执行一遍,并将记录记入django_migrations表。
此后,每次更改models.py
后,执行python manage.py makemigrations [app_name]
时,Django都会将models.py
和最近一次生成的migrations脚本中的内容做对比,生成“差异脚本”。
MTV Framework
从总体上看,Django框架分为三个模块:Model(模型)、Template(模板)和View(视图),与传统的MVC架构区别不大。其中:
-
Model用于建立数据模型,存储数据。
-
Template用于提供静态页面并渲染从后端获取的数据,位于
/template
目录下。 -
View用于控制业务逻辑,通过调用Model和Template存取或展现数据。一般来说,业务逻辑会存放
view.py
文件中,通过urls.py
中定义的路由规则将页面请求分发给view.py
进行处理。
Model
Django对数据库的表做了一层抽象,Model存储着数据及其对应行为。在model.py
文件里,一个类对应着数据库的一张表。
Model有以下三个特点:
-
每个类都是django.db.models.Model的派生类。
-
每个类的属性代表了一个数据字段。
-
Django提供了自动生成的数据库调用API。
注意:
-
表名是自动从模型元数据派生出来的,但可以被覆盖。
-
每张表自带一个非空id字段,且为主键。如果你不希望使用id作为主键,可以把其他字段设置为primary_key=True,Django不会添加id字段到表中。
-
Model对应哪个数据库的SQL语法,取决于
settings.py
里选择了哪个数据库。
一旦决定使用某个APP的Model,就应当往settings.py
的INSTALLED_APPS添加APP_NAME。
Model Fields
通常来说,这一部分没什么好讲的,用到再查文档就可以了。但是有些东西值得注意一下。
Model的每一个字段都应该是Field类的一个实例,Django使用字段类来决定以下属性:
-
列类型,展示当前列元素的类型。
-
呈现表单字段时,默认使用的HTML部件。
-
最低的验证要求。这一功能用于Django管理员和自动生成的表单中。
字段选项根据Model Fields来决定,比如CharField就需要设定max_length
这一属性。关于字段选项,可以看这里。
一个比较特殊的选项是choices
,它由一系列的二元组组成。如果设定这一属性,则默认的表单组件会从文本字段变成选择框。这里给出一个例子:
# models.py
from django.db import models
class Person(models.Model):
SHIRT_SIZES = (
('S', 'Small'),
('M', 'Medium'),
('L', 'Large')
)
name = models.CharField(max_length=60)
shirt_size = models.CharField(max_length=1, choices=SHIRT_SIZES)
每个tuple的第一个元素会存储在数据库中,第二个元素则被表单组件显示。第二个元素可以通过如下途径得到:
>>> p = Person(name="Tom", shirt_size="L")
>>> p.save() # 保存p这一对象
>>> p.shirt_size
'L'
>>> p.get_shirt_size_display() ## 通过调用instance_name.get_FOO_display()方法得到
'Large'
另一个值得一谈的选项是primary_key。如果在创建Model时没有指定任何主键,则Django会自动添加一项名为id的IntergerField属性,作为该表的主键。主键字段是只读的。
此外,还可以使用verbose_name来指定属性的详细名称。
Model Relationship
关系模型一直以来都是关系型数据库的重点,在Django中也有与之相关的属性。Django提供了三种常见关系类型:多对一、多对多和一对一。
要定义一个多对一关系,可以使用ForeignKey(django.db.models.ForeignKey)。ForeignKey要求一个位置参数,指定与哪一个Model关联。
在定义ForeignKey和一对一关系时,需要加上on_delete选项。此参数是为了表示删除关联表中的数据时,关联表与其关联字段的操作,解决两个表里的数据不一致的问题,不传这个参数会报错。on_delete有CASCADE/ PROTECT/ SET_NULL/ SET_DEFAULT/ SET()五个可选择的值,含义如下:
- CASCADE: 级联删除
- PROTECT: 报完整性错误
- SET_NULL: 在ForeignKey允许的情况下将其设置为空
- SET_DEFAULT: 设置为ForeignKey的默认值
- SET(): 调用外面的值
考察下面这个例子。每一辆车都有其对应的制造商(Manufacturer),每一个制造商可以生产多种不同的汽车:
from django.db import models
class Manufacturer(models.Model):
pass
class Car(models.Model):
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
Django官方文档提出一个建议:ForeignKey名称应该为所依赖Model名称的小写(就像上面Car类的manufacturer那样写)。
要定义一个多对多关系,可以使用ManyToManyField。和ForeignKey一样,ManyToManyField也要求一个未知参数,指定与哪一个Model关联。
考察下面这个例子。一个披萨需要很多配料(Topping),一种配料可以用于制作很多披萨:
from django.db import models
class Topping(models.Model):
pass
class Pizza(models.Model):
toppings = models.ManyToManyFields(Topping)
对于Pizza和Topping这两个Model来说,谁拥有ManyToManyFields字段并不是很重要,但有一点必须要保证:只能把该字段放到其中一个Model中。虽然我们认为谁拥有ManyToManyFields字段并不重要,但一般来说,把ManyToManyFields字段放到表单要编辑的Model中会更加自然。考虑上面的例子,一般我们会认为“披萨对应很多配料”而不是“配料对应很多披萨”。
当我们只需要处理一个简单的多对多关系时,一个ManyToManyFields就已经够用了。但当我们把数据与两个Model的关系关联起来时,情况就变得复杂起来。考虑如下例子:一个APP用于跟踪音乐家所属音乐团体的情况。“音乐家”与“音乐团体”之间存在多对多关系,可以使用ManyToManyFields字段。但我们可能还需要其他数据,比如某个音乐家何时加入了某个音乐团体。对于这种需求,Django允许指定Model来管理多对多关系,然后把额外的属性绑定到该Model中。代码片段如下:
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
当设置类似上面Membership这样的中介模型时,需要为多对多关系中设计的模型明确指定ForeignKey,该显式声明定义了两个模型之间的关系。
对于中介模型,有如下限制:
-
中介模型必须有且仅有一个到源模型的ForeignKey,或者使用ManyToManyField.through_fields明确指定Django用于关系的ForeignKey。如果有多个ForeignKey且未指定through_fields,将引发验证错误。类似的限制适用于目标模型的ForeignKey(比如上面的Person类)。
-
对于通过中介模型且与自身有多对多关系的模型,允许使用同一模型的两个ForeignKey,但它们被视为多对多关系的两个不同方面的东西,也需要指定through_fields,否则也会引发验证错误。
对于上面的例子,可以用如下操作来验证其正确性:
>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles,
... date_joined=date(1962, 8, 16),
... invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>]>
>>> ringo.group_set.all()
<QuerySet [<Group: The Beatles>]>
>>> m2 = Membership.objects.create(person=paul, group=beatles,
... date_joined=date(1960, 8, 1),
... invite_reason="Wanted to form a band.")
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>
要定义一对一关系,使用OneToOneField这一字段。同样地,也需要给定一个Model名称来指定与哪一个Model关联。这一关系在某个对象“拓展”到另一对象时最为有用。OneToOneField曾经用于自动生成Model的主键,现在已经不再使用这种方法。因此,可以在单个Model中放置多个OneToOneField类型的字段。
考察如下例子:建立了一个名为places的数据库,并在其中存储地址、电话号码等。但如果希望在places的基础上建立restaurant数据库,则没有必要再把已有的数据复制一遍,只需要在原有的数据库中加入一个OneToOneField即可。
Model Meta options
可以通过在Model中声明内部类Meta来给Model赋予元信息,比如:
from django.db import models
class Ox(models.Model):
horn_length = models.IntegerField()
class Meta:
ordering = ['horn_length']
verbost_name_plural = 'oxen'
Model元信息是“不在字段里的数据”,比如排序选项、数据库表名、数据库表详细名称。没有一项是必需的。
View
Django通过urls.py
和views.py
两个文件来实现逻辑控制。其中,urls.py
文件创建了一张从URL到view函数的映射表;views.py
定义了View函数,具体体现为业务逻辑控制代码。
Procedure of process request in Django
-
Django使用一个模块作为基本URLconf路由模块,使用哪个模块取决于
settings.py
文件里的ROOT_URLCONF
值。如果进入的Http请求有一个urlconf属性(被HttpRequest中间件加上去的),那么就使用这个urlconf属性。 -
Django加载URLconf路由模块,并在模块中按顺序寻找合适的urlpattern。
-
一旦找到匹配的urlpattern,Django引入并调用对应的视图函数。视图函数传入以下参数:
- 一个HttpRequest实例。
- 如果匹配的urlpattern包含匿名组,那么把匹配的正则表达式作为位置参数。
- 关键字参数由提供路径表达式匹配的所有命名部分组成。
在这个过程中,如果没有任何url被匹配或引发了任何异常,Django会调用响应的错误处理视图函数。这里给出一个简单的例子:
from django.urls import path from . import views urlpatterns = [ path('articles/2003/', views.special_case_2003), path('articles/<int:year>/', views.year_archive), path('articles/<int:year>/<int:month>/', views.month_archive), path('articles/<int:year>/<int:month>/<slug:slug>/', views.article_detail), ]
在上面的例子中:
/articles/2005/03/
会使用到第三条匹配规则。Django会调用这样的视图函数:vies.month_archive(request, year=2005, month=03)
。/articles/2003/
会使用到第一条匹配规则,而/articles/2003
不会使用到任何匹配规则。
注意:
- 要从url捕获值,使用尖括号(像html那样)。
- 捕获的值可以是转换器类型,比如使用<int: name>捕获整数参数。如果不包含转换器,则匹配
/
以外的任何字符串。 - 不需要加前导斜杠,因为每个URL都有后置斜杠。
若使用正则表达式做路由匹配,只需把path()
换成re_path()
即可。
关于URLconf搜索的内容,值得详细解释一下。它的搜索法则不包含GET或POST参数,也不包含域名。比如https://www.example.com/myapp/
,URLconf会搜到myapp/
。对于https://www.example.com/myapp/?page=3
,URLconf会搜到myapp/
。URLconf也不关心请求方法,所有请求方法都会被路由到相同的视图函数。
View function
View函数是一种python函数,它能获取Web Request对象,并返回一个Web Response对象。Web Response可以是HTML内容、网页、重定向或其他东西。View函数包含返回Response所需的逻辑。Django约定把所有的View函数放置在views.py
文件中。
这里给出一个以HTML文档的形式,返回当前日期和时间的View函数:
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It's now %s.</body></html>" % now
return HttpResponse(html)
Django提供返回Http错误码的功能,它们都是HttpResponse
的子类。举个例子:
from django.http import HttpResponse, HttpResponseNotFound
def my_view(request):
# ...
if foo:
return HttpResponseNotFound('<h1>Page not found</h1>')
else:
return HttpResponse('<h1>Page was found</h1>')
也可以通过如下形式来自定义Http状态码:
return HttpResponse(status=404)
对于404,有更简便的处理办法,可以看这里。
View decorator
这个东西功能很强大,且使用简单。举个例子,我们可以通过装饰器来实现限制Http方法来调用View函数:
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET", "POST"])
def my_view(request):
# I can assume now that only GET or POST requests make it this far
pass
Shortcut function
django.shortcuts
包含了多个跨越MVC架构多层的辅助函数或类。换句话来说,这些辅助函数和类引入了受控耦合。
Render
render(request, template_name, context=None, content_type=Node, status=None, using=None)
render函数将给定的模板和上下文结合在一起,并返回呈现该内容的HttpResponse对象。以下两个代码片段是等价的:
from django.shortcuts import render
def my_view(request):
return render(request, 'myapp/index.html', {
'foo': 'bar'
}, content_type='applicatoin/xhtml+xml')
from django.http import HttpResponse
from django.template import loader
def my_view(request):
# 获取模板对象后,手动调用render方法
t = loader.get_template('myapp/index.html')
c = {'foo': 'bar'}
return HttpResponse(t.render(c, request), content_type='application/xhtml+xml')
Redirect
redirect(to, *args, permanent=False, **kwargs)
redirect函数返回一个HttpResponseRedirect
对象给适当的URL。参数可以是:
- 一个Model。该Model的
get_absolute_url()
方法会被调用。 - 一个可能带有参数的View。会自动调用
reverse()
方法来反向解析名称。 - 一个绝对或相对URL。
有好几种调用redirect()
的方法:
from django.shortcuts import redirect
def my_view(request):
...
obj = ModelName.objects.get(...)
return redirect(obj)
# 如果加上permanent=True参数,则变成永久重定向
# return redirect(obj, permanaent=True)
def my_view(request):
...
return redirect('some-view-name', foo='bar')
def my_view(request):
...
return redirect('/some/url/')
# or you can do this
# return redirect('https://example.com/')
View的种类繁多,关于更多信息可以看这里。
Request Object
要理解request对象,我们先来看一个简单的实例:
# urls.py
from django.conf.urls import url
from django.contrib import admin
from cmdb import views
urlpatterns = [
url(r'^login/',views.login.as_view())
]
# views.py
from django.shortcuts import render
from django.views.generic import View
class login(View):
"""
A simple class achieved login function
"""
def get(self, request):
return render(request, 'login.html')
def post(self, request):
USER_INPUT = []
user = request.POST.get('user', None)
email = request.POST.get('email', None)
temp = {
'user': user,
'email': email,
}
USER_INPUT.append(temp)
return render(request, 'login.html', {
'data': USER_INPUT
})
{# login.html #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<Body>
{# 把表单的数据以post方式提交给login视图处理 #}
<form action="login/" method="post">
<input type="text" name="user">
<input type="text" name="email">
<input type="submit" value="Submit">
</form>
{# 将表单内容在这里展示 #}
<h1>数据展示</h1>
<table border="1">
{% for item in data %}
<tr>
<td>{{ item.user }}</td>
<td>{{ item.email }}</td>
</tr>
{% endfor %}
</table>
</Body>
</html>
流程分析:
- 浏览器输入
http://127.0.0.1/login
,浏览器把浏览器信息、客户端地址、访问地址等信息封装到request里,以GET方式发送给Django。经过中间件处理请求后,Django在urls.py
里找到对应的urlpattern,调用CBV对应的get方法来处理请求,直接返回login.html
,完成登录页面的访问。 - 用户在登录页面输入用户名和邮箱,点击登录。form表单把输入的信息、浏览器信息、客户端地址、访问地址等信息封装到request对象里,以POST方式发送给Django。同样调用CBV对应的post方法来处理请求,渲染页面后返回
login.html
,完成登录页面的渲染工作。
Request对象的属性如下:
Attribute name | Function |
---|---|
scheme | 代表请求的方案,http/https |
path | 代表请求路径,比如’127.0.0.1/regist/check’,这个值就是‘/regist/check’ |
method | 表示请求使用的Http方法 |
encoding | 表示提交数据的编码方式 |
GET | 获取GET请求 |
POST | 获取POST请求,例如request.POST.get()获取前端提交的数据 |
COOKIES | 包含浏览器所有的cookie |
user | 一个django.contrib.auth.models.User类型的对象,表示当前登录的用户。如果当前用户没有登录,user将设置为django.contrib.auth.models.AnonymouUser的一个实例 |
session | 一个既可读又可写的类似于dict的对象,表示当前会话 |
关于user
这个属性还可以再多说一点。一般在Model里创建自定义用户类时(比如蓝鲸SaaS开发课程Demo里的WeChatUser类),通常会设置一个名为user的属性,并把它设置为user = models.OneToOneField(User, models.CASCADE)
。这里的User
就是django.contrib.auth.models.User
。这样做的好处在于我们可以在View函数里通过ModelName.object.get(user=request.user)
轻松地获取当前登录用户的数据项。
Middleware
中间件是有关Django处理request/response的钩子函数的一套框架,适用于处理Django全局输入输出。每个中间件组件都被设定为完成某些特定工作。
Customized Middleware
一个中间件工厂(middleware factory)可以被写成一个传入函数并返回中间件的函数(听上去就像是装饰器)。写法大概像这样:
def simple_middleware(get_response):
# One-time configuration and initialization.
def middleware(request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
return middleware
或者你还可以写成类的形式,实现__call__
方法:
class SimpleMiddleware:
"""
a simple middleware factory
"""
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
get_response
函数可能是实际视图,也可能是下一个中间件。当前中间件无需关心get_response
到底是谁。实际上,get_response
链中的最后一个中间件的可调用对象并不是实际视图,而且处理程序中的包装方法,该方法负责应用视图中间件,使用适当的URL参数调用视图以及应用template-response
或exception
中间件。
中间件工厂必须接收一个get_response
参数,还可以初始化中间件的某些全局状态,但有两点需要注意:
- Django仅使用
get_response
参数初始化中间件,不能往__init__
方法传入其他参数。 __init__
仅在Django服务器启动时被调用一次,与__call__
这种每传一个response
就调用一次的方法不一样。
在处理HttpRequest时,与在urls.py
中寻找合适的urlpattern匹配一样,Django按照settings.py
中MIDDLEWARE
给定的顺序调用中间件。
Other middleware hooks
还可以往基于类的中间件添加三个特殊的方法。
process_view()
process_view(request, view_func, view_args, view_kwargs)
process_view
在Django调用对应的视图函数之前就被调用,它要么返回None,要么返回一个HttpResponse对象。如果不返回东西,Django会继续处理这一request,执行其他中间件的process_view()
函数,再应用对应的View函数。如果它返回了一个HttpResponse对象,Django不会中断调用View函数的过程,还会应用中间件的响应结果,返回该HttpResponse对象。
process_exception()
process_exception(request, exception)
这里的exception是在View函数里抛出的。当View函数抛出一个异常时,Django会调用process_exception()
这一方法,该方法也应返回None或一个HttpResponse对象。当它返回一个HttpResponse对象时,则应用模板响应和响应中间件,并把响应结果返回给用户浏览器。
process_template_response()
process_template_response(request, response)
与上面的函数不同,process_template_response()
传入一个被View函数或middleware返回的TemplateResponse
对象。process_template_response()
在view函数执行完毕后调用,如果响应实例有render()
方法,就说明响应实例是一个TemplateResponse
对象。之所以强调render()
方法的存在,是因为process_template_response()
必须返回一个拥有render()
方法的响应对象。
还有一件值得注意的事情:响应是不需要显式渲染的,当所有的模板响应中间件都被调用过后,响应会被自动渲染。
Template
Basic skills
作为一个web框架,Django需要一种方便的能动态生成HTML页面的做法,比较通用的做法是通过模板来实现这个功能。模板是一个纯文本文件,它既包含了静态的部分,也包含描述如何插入动态内容的表达式(这里的表达式又分为两类,一类是区块标签,另一类是变量)。Django依赖模板引擎来渲染模板,可选的模板引擎有两种:一种是预设的模板引擎,另一种是Jinja2,模板引擎的设置可以在settings.py
里找到。
“区块标签”是在模板里起作用的标记,形式为{% xxx %}
,这里的“起作用”定义很广泛,可以是生成内容、作为控制结构、获取数据库内容或访问其他模板标签。“变量”是一个在模板里用来输出值的标记,形式为{{ xxx }}
,还可以搭配filter来实现更多的显示效果。光有标签还不够,我们还需要从View函数得到特定的上下文(Context),才能获取变量标签对应的值并渲染模板。Context是一个传递给模板的“从名称到值的映射”,它的本质是一个dict。术语渲染(render)
指的是从context中获取值来替换模板中的变量标签,执行所有区块标签,并生成最终的HTML页面的一个过程。
Render的一个比较经典的做法是构造Template和Context对象,并调用Template的render方法:
>>> from django.template import Template, Context
>>> t = Template('My name is {{ name }}.')
>>> c = Context({'name': 'Tom'})
>>> t.render(c)
'My name is Tom.'
当然现在基本不这么做了,可以调用我们之前在View部分谈到的render()
函数,直接传入request、html页面路径和包含数据的dict,就自动完成了HTML页面的渲染工作。另外,render()
与render_to_response()
这两个函数在不同的Django版本中用法会有所不同,使用时需要仔细查看文档。
在渲染模板的时候,可以用locals()
稍微偷懒一下。让我们来考察两个功能相同的View函数:
# 不使用locals()
def current_datetime(request):
now = datetime.datetime.now()
return render_to_response('current_datetime.html', {
'current_date': now,
})
# 使用locals()
def current_datetime(request):
current_date = datetime.datetime.now()
return render_to_response('current_datetime.html', locals())
使用locals()
的优点在于能让代码看起来更加简明,适合在函数中大部分变量都被用于创建Context的时候使用。
Template inheritance
一般来说,一个HTML页面代码都是非常冗长复杂的,有没有办法能复用HTML页面的代码呢?一种做法是通过{% include %}
来解决这个问题,这种做法就是把一个网页嵌入到另一个网页中(跟C++ #include
的做法非常像)。但现在Django会使用模板继承这一策略来解决这个问题。
本质上来说,模板继承就是先构造一个基础框架模板,然后在其子模板中对它所包含的公用部分进行重载。让我们来看一个基础模板长什么样子:
<!-- base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<h1>This is my helpful timestamp site!</h1>
{% block content %}{% endblock %}
{% block footer %}
<hr>
<p>Thanks for visiting my site!</p>
{% endblock %}
</body>
</html>
这个叫做base.html
的文件定义了一个简单的HTML框架文档,而子模板的作用就是重载、添加或保留那些block的内容。{% block %}
这一标签告诉模板引擎:子模板可以重载这些部分。现在我们有了一个基础模板,我们试着写一个子模板来使用它:
<!-- current_datetime.html -->
{% extends "base.html" %}
{% block title %}Current time{% endblock %}
{% block content %}
<p>It's {{ current_date }} now.</p>
{% endblock %}
这样就不用写一大堆相同的代码来实现构建两个几乎一样的页面了。模板引擎在加载current_datetime.html
时,首先读取到{% extends "base.html" %}
这一标签,注意到该页面是一个子模板。模板引擎会立即装载其父模板,并用子模板中的{% block %}
内容来替换父模板中{% block %}
的内容。注意到子模板中并没有定义{% block footer %}
,模板引擎此时会使用父模板中{% block footer %}
的内容。
继承并不会改变Context的工作方式,而且可以根据实际需要,使用多层继承(注意是“多层”而不是“多重”,这种处理继承的思想类似于Java,而不是C++或Python)。
使用模板继承有一些需要注意的地方:
- 如果使用模板继承,必须保证{% extend %}为模板中的第一个模板标记,否则将不会实现模板继承的效果。
- 一般来说,模板中的
{% block %}
越多越好(也就是说钩子越多越好),但这其实需要根据实际情况决定。 - 如果想使用父模板中的内容,可以使用
{{ block.super }}
变量,这是一个很漂亮的设计(个人认为)。 - 不能在同一个模板中定义多个同名的
{% block %}
,这是因为block标签的工作方式是双向的。也就是说,block标签不仅挖了一个要填的坑,也定义了在父模板中这个坑要填充的内容。想象一下有这样的一个继承关系:A.html -> B.html -> C.html
,其中A.html
为“根模板”。对于在B.html
里出现的{% block %}
,它既定义了要在A.html
中要填充的内容,也定义了C.html
中未定义{% block %}
的备选内容。如果存在多个同名{% block %}
,模板引擎将不知道使用哪一个block的内容来渲染页面。 - 多数情况下,
{% extends %}
参数应该是一个字符串。不过这个参数也可以是个变量(如果直到运行时才能确定父模板名称),这个特性可以让模板实现多态(暂时没有发现有人使用“多态”这种说法,但这种功能跟C++的多态实现原理实在是太像了,姑且先这么叫着,方便理解)。
Some other questions
What are wsgi, uwsgi and uWSGI?
wsgi(web server gateway interface,web服务器网关接口)是Python在处理Http请求时规定的一种处理方式(也可以叫一套协议),用于接收用户请求,并将请求交给web框架。比如一个Http Request过来了,那么就有一个相应的处理函数来进行处理和返回结果。wsgi就是规定这个处理函数的参数和它的返回结果长什么样子。简单而言,wsgi规定了处理函数的输入和输出格式。
uwsgi与wsgi一样,是一套通信协议。它是uWSGI服务器的独占协议,用于定义传输信息的类型。
uWSGI是一个web服务器,它实现了wsgi、uwsgi、http三个协议。
Django insider components
Admin,form,modelform,model
Ways to transfer data from backend to frontend
render
from django.shortcuts import render
def main_page(request):
data = [1,2,3,4]
return render(request, 'index.html', {'data': data})
# 在html页面中,使用{{ }}来获取数据
<div>{{ data[0] }}</div>
render + 数据json序列化
import json
from django.shortcuts import render
def main_page(request):
list = ['view', 'Json', 'JS']
return render(request, 'index.html', {
'list': json.dumps(list) # 序列化操作
})
# JavaScript部分需要添加safe过滤
let List = {{ list|safe }}
传递数据到Ajax,使用HttpResponse且返回json序列化字符串
import json
def scene_update_view(request):
if request.method == 'POST':
name = request.POST.get('name')
status = 0
result = 'Error!'
return HttpResponse(json.dumps({
"status": status,
"result": result
}))
# ajax中json字符串转成对象用JSON.parse(data)
Difference between render and HttpResponse
- render是将整个html字符串返回并渲染成网页。
- HttpResponse只是返回字符串,不能渲染html。
Difference between render and render_to_response
render是render_to_response的一个新的快捷方式,render会自动使用RequestContext,而render_to_response必需coding出来。render更简洁。
When to create request object in Django?
class WSGIHandler(base.BaseHandler):
...
request = self.request_class(environ)
请求走到WSGIHandler类的时候,执行cell
方法,将environ封装为request。
Difference between filter and exclude
两者都是返回QuerySet对象。filter是找满足条件的项,exclude是排除满足条件的项。
Difference between cookie and session
- cookie是保存在浏览器端的键值对,可用于做用户认证。
- session将用户的会话信息保存在服务端,key值是随机产生的字符串,value值是session的内容,依赖于cookie将每个用户的随机字符串保存到用户浏览器上。
- 考虑安全性应使用cookie,考虑服务器负载应使用cookie。
Advantages and disadvantages of ORM
优点
- ORM使得与数据库交互变得简单。
- 避免写SQL所带来的性能与安全问题。
缺点
- 性能没有直接写SQL优化那么强。
- 对于个别查询做不到。
Three ways to write SQL in ORM
- execute,直接访问数据库。
- extra。
- raw。
How Django realize csrf?
- Django第一次响应来自某个客户端的请求时,后端随机产生一个token,把这个token保存在session状态中;同时,后端把这个token放到cookie中并交给前端页面。
- 下次前端需要发起请求的时候,把这个token加入到请求数据或者头信息中,一起传给后端。
- 后端校验前端请求带过来的token和session里的token是否一致。
How to take csrf token when sending post request with using ajax
-
后端将csrf token传到前端,发送post请求时携带这个值发送。
data:{ 'csrfmiddlewaretoken': '{{ csrf_token }}' }
-
获取form中隐藏标签的csrf token值,加入到请求数据中并传给后端。
data: { 'csrfmiddlewaretoken': $('[name="csrfmiddlewaretoken"]').val() }
-
cookie中存在csrf token,将csrf token值放到请求头中。
headers: { "X-CSRFtoken": $.cookie("csrftoken") }
Difference between runserver and uWSGI
- runserver方法是调试Django时经常用到的运行方式,它使用Django自带的WSGI Server运行,主要在测试和开发中使用,并且runserver开启的方式也是单进程。
- uWSGI是一个web服务器,它实现了WSGI、http、uwsgi等协议。注意uwsgi是一种通信协议,而uWSGI是实现uwsgi协议和WSGI协议的web服务器。uWSGI具有高性能、低内存占用和多app管理等优点,搭配Nginx就是一个生产环境,能够将用户访问请求与应用app隔离开,实现真正的部署。优点在于:支持的并发量更高,方便管理多进程,发挥多核优势,提升性能。
How Django realize web socket
官方推荐用channels。channels通过升级http协议到websocket协议,保证实时通讯。也就是说我们完全可以用channels实现即时通讯,而不是使用长轮询和计时器方式来保证伪实时通讯。
Ways of Model inherit
- 抽象基类。比如
django.db.model.Models
。 - 多表继承。
Difference between get and filter
- get返回一个model对象,只能用于找到某个确切的结果。如果有多个或没有对象满足条件,都会报错。
- filter返回一个QuerySet对象(本质上是个list),能用于找到某些结果。
Difference between Django、Flask and Tornado
- Django倾向于大而全,提高开发效率。但其性能扩展有限,通常要提高性能只能重构项目。
- Flask是轻量级的框架,可扩展性强。
- Tornado倾向于少而精,性能优越。