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-adminmanage.py常见用法,可以看这篇帖子

Django Life Cycle

也叫请求处理流程,指的是用户从浏览器上输入URL到用户看到网页的这个时间段内,Django后台所发生的事情。流程如下:

  1. 用户(浏览器)发起一个请求。

  2. 请求经由uwsgi处理,转发到请求中间件(Request Middleware),请求中间件会对请求进行预处理(或直接返回请求)。

  3. 请求到达由settings.pyROOT_URLCONF指定的路由文件(默认为urls.py),通过URL匹配至对应的view。

  4. 请求到达视图中间件(View Middleware),视图中间件对请求做一些预处理(或直接返回请求)。

  5. 请求到达视图文件,调用对应的视图方法,进行逻辑处理。

  6. View中的方法可以用个Model访问持久层的数据。

  7. Model访问db的操作由managers进行控制。

  8. View从Model中获取数据后,生成上下文(Context)并给Template传递数据。

  9. Template获取Context后,使用Tags和Filters来渲染数据。

  10. 渲染完成后的Template被返回给View,View通过render函数生成一个HttpResponse对象,并把此对象发送给请求中间件。

  11. 请求中间件会对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也支持一个项目使用多个数据库,配置方式如下:

  1. settings.py里的DATABASES配置多个DB配置。

  2. 编写一个路由文件以定义路由规则,给每个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!')

这里再特别解释下HttpRequestHttpResponse:

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.pyviews.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不会使用到任何匹配规则。

    注意:

    1. 要从url捕获值,使用尖括号(像html那样)。
    2. 捕获的值可以是转换器类型,比如使用<int: name>捕获整数参数。如果不包含转换器,则匹配/以外的任何字符串。
    3. 不需要加前导斜杠,因为每个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-responseexception中间件。

中间件工厂必须接收一个get_response参数,还可以初始化中间件的某些全局状态,但有两点需要注意:

  1. Django仅使用get_response参数初始化中间件,不能往__init__方法传入其他参数。
  2. __init__仅在Django服务器启动时被调用一次,与__call__这种每传一个response就调用一次的方法不一样。

在处理HttpRequest时,与在urls.py中寻找合适的urlpattern匹配一样,Django按照settings.pyMIDDLEWARE给定的顺序调用中间件。

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是排除满足条件的项。

  • 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?

  1. Django第一次响应来自某个客户端的请求时,后端随机产生一个token,把这个token保存在session状态中;同时,后端把这个token放到cookie中并交给前端页面。
  2. 下次前端需要发起请求的时候,把这个token加入到请求数据或者头信息中,一起传给后端。
  3. 后端校验前端请求带过来的token和session里的token是否一致。

How to take csrf token when sending post request with using ajax

  1. 后端将csrf token传到前端,发送post请求时携带这个值发送。

    data:{
    	'csrfmiddlewaretoken': '{{ csrf_token }}'
    }
    
  2. 获取form中隐藏标签的csrf token值,加入到请求数据中并传给后端。

    data: {
    	'csrfmiddlewaretoken': $('[name="csrfmiddlewaretoken"]').val()
    }
    
  3. cookie中存在csrf token,将csrf token值放到请求头中。

    headers: {
    	"X-CSRFtoken": $.cookie("csrftoken")
    }
    

Difference between runserver and uWSGI

  1. runserver方法是调试Django时经常用到的运行方式,它使用Django自带的WSGI Server运行,主要在测试和开发中使用,并且runserver开启的方式也是单进程。
  2. 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倾向于少而精,性能优越。
posted @ 2020-04-28 16:12  JHSeng  阅读(333)  评论(0编辑  收藏  举报