django框架安装部署应用

Django

django

web框架介绍

1. 什么是web框架

应该叫web应用框架(web application framework)是一种开发框架。

通俗点来说,就是用来开发web应用的程序。

2.为什么要使用框架来开发

互联网行业流行一句话叫“不要重复造轮子”,特别是对于新手和应用层开发人员。对于web开发来说,要提高开发效率,我们需要站在前人的肩膀上,利用成熟的web框架,把精力放在业务逻辑的实现上,不关心底层建设。

本质上来讲就是一个字快。

开发一个软件项目好比盖房子:

方案一、

生产砖块、木头、水泥、钢筋、
打地基
盖房子
装修
购置家具
方案二、

找开发商买毛坯房
装修
购置家具
方案三、

找开发商购买精装修房子
购置家具
很显然如果所有的工作都从头做起的方案一的效率最低,而站在前人的肩膀上的方案4是效率最高的。从方案二开始就好比是封装了底层建设的框架,不同的框架的封装程度不同,自由度和开发效率各不同。

3. 如何选择框架

在这里插入图片描述
选择web框架好比买房子选择有实力的开发商。python最流行三大框架是Django,Flask,Tornado。

  • Django
    大而全
    入门简单
    开发效率极高
    最流行
    类似于方案三(精装修的房子)
    Flask

  • 轻量级
    自由度高
    流行
    有一定难度
    类似于方案二(毛坯房)

  • Tornado
    轻量级
    自由度高
    异步
    难度较大
    应用在高并发的场景
    如何选择框架要根据项目的用户量,需求变动频率,开发周期等综合考虑,一般有系统架构师来决定。

4. Django是什么?

Django是一个高级的Python Web框架,它的宗旨是:Django makes it easier to build better Web apps more quickly and with less code.

  • 发音[`dʒæŋɡəʊ]
  • 开发效率极高
  • 功能全面开箱即用
  • 安全放心
  • 社区活跃,插件丰富

因为django框架功能全面,可以看到web应用开发的全貌,学习难度也较低,推荐新手先学习。

python虚拟环境

在python项目开发过程中会下载很多第三方库,有时不同项目对同一个库的依赖版本不同,如果所有项目都使用同一个python环境就会起冲突不便于管理。
因此,实际开发中会为每一个项目都单独创建一个python的虚拟环境。这里的虚拟环境本质上是对系统python环境的一个拷贝,它依赖系统环境又和系统环境隔离。
流行的python虚拟环境管理工具有很多最常用的是virtualenv (opens new window)。

virtualenv

安装
pip install virtualenv
创建虚拟环境

cd到你想要创建虚拟环境的目录,然后执行命令:

virtualenv env # env是虚拟环境的名字

运行完这个命令后,会在当前目录创建一个名为env的文件夹,这个文件中的内容就是环境的文件。
如果系统上有多个python环境可以通过-p参数指定不同python版本来创建对应的虚拟环境

virtualenv -p C:\Python39\python.exe env  # 指定对应版本的python创建虚拟环境
使用虚拟环境

windows

venv\scripts\activate

linux

source venv/bin/activate

进入虚拟环境后会在命令行的用户名前加上(虚拟环境名)。
注意
创建虚拟环境的目录路径中不能有中文和标点符号
进入虚拟环境后只在当前终端中有效,新开启的终端需要再次重新进入。如果关闭了终端会退出虚拟环境,再次进入的时候,需要重新进入虚拟环境。

退出虚拟环境

1、如果是切换,直接进入其他虚拟环境即可
2、主动退出当前虚拟环境
运行命令

deactivate
提示:
使用pycharm创建项目时会默认通过virtualenv在项目根目录下创建虚拟环境,点击terminal后会自动进入对应的虚拟环境。

创建django项目与应用

创建Django项目

安装Django

首先需要安装django,进入虚拟环境中运行命令:

pip install django==3.2.11  # 指定3.2的版本
创建项目

安装django后,会安装一个django-admin程序,用来创建和管理django项目。
要创建一个django项目可以运行如下命令:

django-admin startproject <project_name> [project_path]

其中startproject是创建子命令,<project_name>是项目名必须提供,project_path是创建项目的路径,如果省略会把项目创建在当前目录下。
例如运行命令:

django-admin startproject study_django

这会在当前目录创建名为study_django的目录,目录中的内容如下:

study_django/								# 项目根目录,目录名可以随意更换
	manage.py								# 管理django项目命令行工具
    study_django/							# 项目目录,python包,
        __init__.py
        settings.py           				# 项目配置文件
        urls.py								# 项目根路由文件
        asgi.py								# 兼容asgi协议的web服务器入口文件
        wsgi.py								# 兼容wsgi协议的web服务器入口文件

但是这样项目根目录文件名和项目目录名相同,有时候我们不需要创建最外层的目录,只把项目生成在当前目录下可以运行如下命令:

django-admin startproject study_project .  # 路径参数是. 代表当前目录
运行项目

在项目根目录下运行如下命令:

python manage.py runserver ip:端口

命令不带ip和端口服务将默认运行在127.0.0.1:8000。
运行成功后在浏览器访问http://127.0.0.1:8000/将会看到欢迎页面。
注意
这只是一个Django 自带的用于开发的简易服务器,它为开发而设计,不要应用在生产环境中。

修改时区和语言

上一步中的欢迎页面看起来是英文的,django框架做了国际化,我们只需要在settings.py中修改如下配置

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

创建django应用

什么是应用

在Django中有项目和应用的概念。每一个应用都是一个 Python 包,并且遵循着相同的约定和结构。Django 自带一个工具,可以帮你生成应用的基础目录结构,这样你就能专心写代码,而不是创建目录了。
项目和应用有什么区别?
应用是一个专门做某件事的网络应用程序——比如博客系统,或者公共记录的数据库,或者小型的投票程序。 项目则是一个网站使用的配置和应用的集合。项目可以包含很多个应用。应用可以被很多个项目使用。

提示:
也可以简单理解为一个项目即是一个网站,一个应用就是这个网站的某一个功能板块。
创建应用

在项目根目录下运行如下命令:

python manage.py startapp crm

这将会创建一个 crm 目录,它的目录结构大致如下:

crm/
    __init__.py
    admin.py              # django后台站点配置入口
    apps.py               # 应用信息配置入口
    migrations/           # 数据库迁移历史信息目录
        __init__.py
    models.py             # 数据模型模块
    tests.py              # 单元测试
    views.py              # 应用视图模块

这个目录结构包括了crm应用的全部内容。

第一个试图

打开crm/views.py,编写如下代码:

from django.http import HttpResponse

def index(request):
    return HttpResponse("我是首页面")

这是 Django 中最简单的视图。如果想要看见效果,需要将一个url映射到它。

添加路由

在crm目录下创建子路由模块urls.py,编写如下代码:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index)
]

再到主路由模块study_django/urls.py中添加子路由如下:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    ...
    path('crm/', include('crm.urls'))  # 添加一条include
]

做完上面的操作之后,重新运行服务,就可以通过urlhttp://127.0.0.1:8000/crm/访问在上面一步创建的视图。

django中的路由系统

django中路由的作用和路由器类似,当一个用户请求Django站点的一个页面时,是路由系统通过对url的路径部分进行匹配,一旦匹配成功就导入并执行对应的视图来返回响应。

django如何处理请求

1、当一个请求来到时,django首先到项目中查找根URLconf模块,在其中查找路由匹配规则。
2、根URLconf模块,就是项目文件目录下的urls.py文件。在这个文件中定义了一个变量urlpatterns。				   它是一个列表,其中每一个元素都是一个url模式,定义了url和视图函数的对应关系。
3、django按顺序运行每个url模式,并在与请求的url匹配的第一个模式停止。
4、一旦去中一个url模式匹配,django将导入并调用该视图。
5、如果没有匹配的模式,或者在此过程中任何时候引发异常,django调用错误处理视图。、

在这里插入图片描述

路由模块

在django中路由模块一般命名为url.py。
每一个路由模块中都会包含一个urlpatterns变量,它是一个django.urls.path()或者django.urls.re_path()实例的列表。

根路由模块

最外层的路由模块,路由解析的入口。
django通过设置ROOT_URLCONF来确定主路由模块,通常是项目目录下的urls.py模块

子路由

主路由包含的其他路由都是子路由。
一般都是各自应用目录下的urls.py模块。

path()
  • path(route,view, kwargs=None,name=None)
  • path函数返回一个对象,表示一个路由规则
  • route:一个字符串,表示url规则
  • view: 一个视图
  • kwargs:一个字典,需要传入的额外参数
  • name:url的命名

案例:

上一节,我们在crm应用中定义了如下路由:

path('index/', views.index)

将’index/'和视图views.index进行映射。

include()
  • include(module, namespace=None)
  • 将一个子路由导入到一个URLconf模块中
  • module: URLconf模块(或模块名称)

案例:
上一节,我们在根路由中,通过include包含了crm子路由:

path('crm/', include('crm.urls'))

将crm/和子路由crm.urls进行映射。

URLconf在什么上查找

请求的url会被看做是一个普通的Python字符串,URLconf在其上查找并匹配。
进行匹配时不包含GET或POST请求方式的参数以及域名。
例如,https://www.example.com/myapp/ 请求中,URLconf 将查找 myapp/
在 https://www.example.com/myapp/?page=3 请求中,URLconf 仍将查找 myapp/ 。
URLconf不检查使用哪种请求方法。
换句话说,所有请求方法,对同一个URL无论是POST请求,GET请求等等,都将路由到相同的视图函数。
案例:
所以在浏览器中访问地址http://127.0.0.1/crm/index/时,URLconf将查找crm/index/。
第一步在跟路由中进行匹配,可以匹配到:

path('crm/', include('crm.urls'))

第二步当遇到include()时,它会将匹配到该点的URL部分cmr/切掉,并将剩余字符串index/发送到包含的crm.urls模块中进一步匹配
第三/index在crm.urls路由中可以匹配到:

path('index/', views.index)

没有下一步匹配,直接执行对应的视图函数views.index。
注意,这个过程会递归的进行,中间遇到任何一条匹配的路由就会返回。

在URL中捕获参数

django允许在url中捕获值,若要从URL中捕获值,请使用尖括号。

尖括号定义变量名,捕获的值传递给视图函数相同名称的参数。格式如下:

'<参数名>'

案例:
写一个学生详情接口,通过crm/students/n/返回id为n的学生的信息。
视图代码:

# crm/views.py
def detail(request, pk):
    return HttpResponse('学生id为{}的详情'.format(pk))		# 模拟返回对应学生的详情

路由代码:

# crm
urlpatterns = [
    ...
    path('students/<pk>/', views.detail)
]

现在,在浏览器中访问:http://127.0.0.1:8000/crm/students/2/会返回页面:
在这里插入图片描述
注意看路由部分的,这里的pk对应视图函数的pk形参。django会自动匹配url中这部分的字符串2然后传递给detail函数的pk形参。

路径转换器

上面的案例有个漏洞,如果我们在浏览器中输入http://127.0.0.1:8000/crm/students/aaa/,我们发现它依然可以访问。
在实际开发中,这显然不对,id不可能是个字符串。当然可以在view中进行类型转换,但是不够通用。django中设计了路径转换器,能够在路由匹配时,自动进行转换。
以下路径转换器在默认情况下是可用的:

  • str: 匹配除了路径分隔符/之外的任何非空字符串。如果表达式中不包含转换器,默认为字符串转换器。
  • int: 匹配0或任何整数。返回一个整数类型
  • slug: 匹配任何由ASCII字符或数字组成的slug字符串,加上连字符和下划线。
  • uuid:匹配格式化的UUID。为了防止多个url映射到同一个页面,必须包含破折号并且必须是小写字母。例如:075194d3-6885-417e-a8a8-6c931e272f00
  • path: 匹配任何非空字符串,包括路径分隔符/。这允许匹配完整的URL路径,而不是像str那样仅匹配url路径部分。
    路径转换器的使用方式非常简单,只需要在捕获符号<>中,以以下语法即可:
'<转换器:参数名>'

我们可以将上面的案例修改为:

path('students/<int:pk>/', views.detail)

然后,我们再次访问`http://127.0.0.1:8000/crm/students/aaa/,结果是404。
当然,我们也可以捕获多个值,看如下案例:
视图代码:

# crm/views.py
def student_list(request, year, month):
    return HttpResponse('{}年{}月创建的学生列表'.format(year, month))

路由代码:

# crm/urls.py
urlpatterns = [
    ...
    path('students/<int:year>-<int:month>/', views.student_list),
    path('students/<int:year>/<int:month>/', views.student_list)
]

那么通过urlhttp://127.0.0.1:8000/crm/students/2022/01和http://127.0.0.1:8000/crm/students/2022-01/会得到相同的结果。
在这里插入图片描述
在这里插入图片描述
但是如果访问http://127.0.0.1:8000/crm/students/9527-100/也会得到结果:
在这里插入图片描述
这显然又是bug,路径转换器只能进行简单的类型转换和匹配,还需要更强大的匹配功能,需要用到re_path()函数。

re_path()

  • re_path(route,view, kwargs=None,name=None)函数返回一个对象,表示一条路由规则。
  • route: 一个字符串,表示一个url规则
  • view:一个视图
  • kwargs: 一个字典,需要传入的额外参数
  • name: url命名

与path()不同的是,route部分包含正则表达式。
当进行匹配时,从正则表达式中捕获的组会被传递到视图中。
如果组是命名的,则作为命名参数,否则作为位置参数。值以字符串的形式专递,不进行任何类型转换。
命名正则表达式分组的语法是:(?Ppattern),其中name是组的名称,pattern是要匹配的某个模式。
下面是前面例子中的路由,使用正则表达式重写:

re_path(r'^students/(?P<year>\d{4})-(?P<month>[1-9]|1[0-2])/$', views.student_list),
re_path(r'^students/(?P<year>\d{4})/(?P<month>[1-9]|1[0-2])/$', views.student_list)

这样写和之前的路由匹配一致,只是:

  • 匹配的url会受到限制,例如:100月将不再匹配,因为月份整数被限制为1-12。
  • 捕获的每个参数都以字符串的形式发送到视图。
  • 当从使用path()切换到re_path()或相反时,特别重要的是注意视图参数的类型会发生变化,因此可能需要调整视图。
使用没有命名分组的正则表达式

除了命名组语法,例如(?P\d{4}),还可以使用较短的未命名组,例如(\d{4})。这种写法并不特别推荐,因为它更容易在匹配的预期含义和视图参数之间意外的一如错误。

注意在实际使用中建议只使用一种,要么命名,要么不命名,因为当两种方式混合使用时,会忽略未命名的组,只将命名的组传递给视图函数。

URL命名

path(), re_path()还有一个参数,那就是name,这个参数可以给我们的url命一个名。
那它有什么作用呢?
在实际开发中,经常需要获取最终形式的url,比如嵌入的页面链接和服务端导航(重定向)。
我们来模拟一个登录过程,创建一个登录的函数视图如下:

# crm/views.py
from django.shortcuts import redirect
...

def login(request):
    return redirect('/crm/index/')

配置好url

# crm/urls.py
path('login/', views.login)

当我们访问/crm/login/时会发现页面被重定向到了/crm/index/,但是这里有一个问题:重定向这里的url是硬编码的,万一将来我们要修改这个url(这个几率很大),那么我们需要在代码中修改所有硬编码的url部分。这显然不利于维护。所以,强烈建议不要硬编码URL(这是一个费力,不能扩展,容易出错的注意)。
django提供一个django.shortcuts.reverse()函数,它接受一个url的命名,能反向解析出url的绝对路径。
给crm应用的每条路由都添加一个name

urlpatterns = [
    path('index/', views.index, name='index'),
    path('students/<int:pk>/', views.detail, name='student_detail'),
    path('students/<int:year>/<int:month>/', views.student_list, name='student_list'),
    path('login/', views.login, name='login')
]

然后修改登录视图如下:

from django.shortcuts import reverse
def login(request):
    url = reverse('index')
    return redirect(url)

这样不管怎么修改url,reverse都可以动态的解析出url。
当有url参数时,可以通过args,kwargs进行传递,例如:

reverse('student_list', kwargs={'year': 2021, 'month': 12})
# 或
reverse('student_list', args=(2021,12))
# 都可以解析出 /crm/students/2021/12/

注意:在reverse中args,kwargs两个参数不能同时使用。

app_name

将url命名为index,login非常常见,一个项目中,不同的app给url相同的命名,那怎么区分不同应用相同名称的url呢?
非常简单,在应用下的urls.py文件中定义一个app_name变量,给它赋值为引用的名称,例如在crm/urls.py中定义app_name变量如下:

# crm/urls.py
...
app_name = 'crm'			# 一般和引用同名
...

定义app_name之后,再解析url时,需同时传入app_name,格式如下:

'app_name:url_name'

那么要解析index的url的代码如下:

reverse('crm:index')

web框架设计模式

认识web框架的设计模式对学习web框架非常重要。本章只是介绍web框架常见的设计模式,不会深入讨论如何设计web框架。

最常见的web框架设计模式有两种:

  • MVC
  • MTV

MVC

经典的MVC模式中:

  • M(model) 业务模型,代表一个存储数据的对象
  • V(view) 用户界面,代表模型数据的可视化
  • C(controller) 控制器,作用于模型和视图上。它控制数据流向,并在数据变化时更新视图。它使视图与模式解耦。

MTV

django的设计模式称为MTV,本质上跟MVC一样,只是叫法不同。

  • M全拼为Model,模型,与MVC中的M功能相同,负责和数据库交互,进行数据处理
  • V全拼为View,视图,与MVC中的C功能相同,接收请求,进行业务处理,返回响应
  • T全拼为Template, 模板,与MVC中的V功能相同,负责构造要返回的html页面
    MTV框架图如下:
    在这里插入图片描述
    django中数据流和请求流程如下:
  • 用户通过浏览器发起请求
  • view根据请求和业务逻辑向model获取或者发送数据
  • model根据视图的要求和数据库进行交互
  • view收到model返回的结果后将结果发送给template
  • template将收到的数据渲染成最终的html返回给view
  • view将最终包含结果数据的html返回给用户

模版

模板

之前的案例中,能够返回简单的字符串信息给浏览器。那如果想要返回html页面给浏览器该怎么做呢?

当然,我们可以这么写:

def index(request):
    return HttpResponse('<h1 style="color:red">我是硬编码的</h1>')

这样显然,不便维护,也不高效。
django提供了一套模板渲染的机制,将html源码写在模板文件中,然后通过方法将数据渲染后返回给客户端。

模板路径设置

在项目根目录下创建一个templates文件夹用来存放模板文件,然后将这个文件的路径配置到配置项TEMPLATES的DIRS中。

# study_django/settings.py
...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [str(BASE_DIR / 'templates')],		# 项目模板文件路径
        'APP_DIRS': True,							# 查找目录时是否在应用目录下查找
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

注意要填写模板文件夹的绝对路径,通过代码str(BASE_DIR / ‘templates’)可以动态生成。

模板渲染

模板渲染本质上就是将数据替换到模板文件的插槽中,和字符串替换一样。

模板变量

最简单的渲染是将变量替换到模板中。

在模板中,模板变量的语法是:

{{ 变量名 }}

在templates文件中再创建一个crm文件夹,然后在其中创建一个index.html,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <p>{{ msg }}</p>
</body>
</html>

添加了一个模板变量msg,这个变量需要从视图传递一个对应的变量进行替换。修改crm.index视图如下:

# crm/views.py

from django.http import HttpResponse
from django.template.loader import get_template


def index(request):
    msg = '我是首页面'
    t = get_template('crm/index.html')      # 获取模板
    html = t.render(context={'msg': msg})   # 渲染html
    return HttpResponse(html)               # 返回响应

上面的代码非常简单,做了如下工作:

  • 在视图中定义了一个变量msg,
  • 然后根据路径crm/index.html获取对应的模板,
  • 再将变量msg传递给模板进行渲染,
  • 最后将渲染好的html返回。

访问这个视图,返回页面如下:
在这里插入图片描述
上面的代码还可以通过一个快捷函数简化:

from django.shortcuts import render

def index(request):
    msg = '我是首页面'
    return render(request, 'crm/index.html', context={'msg': msg})

render函数的第一个参数是请求request,第二个参数是模板路径,第三个参数context是要传递给模板的数据,是一个字典,其中key是模板上对应的变量名,值是实际要渲染的数据。

模板变量的解析规则

模板变量按照如下流程解析替换:

  • 当模板引擎遇到模板变量时,它会计算该变量,并将其替换为结果
  • 当模板引擎在变量中遇到.时(x.y的形式),它会按以下顺序尝试查找:
    • 字典键值查找
    • 属性或方法查找
    • 数字索引查找
  • 如果结果是可调用的,则调用它时不带参数。调用结果成为模板值。

模板标签

只能简单渲染变量显然不满足需求,django的模板系统中还提供了模板标签来实现更多的渲染逻辑,例如判断,循环等。

下面列出几个常用的模板标签:

for

循环浏览数组中的每个项目,使该项目可以在上下文变量中可用。例如,要显示student_list中提供的学生列表:

<ul>
{% for student in student_list %}
    <li>{{ student.name }}</li>
{% endfor %}
</ul>
if

{% if %} 标签会判断给定的变量,当变量为 True 时(比如存在、非空、非布尔值 False),就会输出块内的内容:

{% if student_list %}
    学生的数量: {{ student_list|length }}
{% else %}
    没有学生
{% endif %}

静态文件引用

django项目开发过程中,通过django.contrib.staticfiles提供静态文件服务,所以确保它在INSTALLED_APPS配置中。

路径设置

静态文件的设置有两个:

1、静态文件目录

在项目根目录下创建static文件夹,然后配置STATICFILES_DIRS

# settings.py
...
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]		# 静态文件目录
...
2、静态文件引用前缀

再配置一个STATIC_URL项

STATIC_URL = '/static/'  # 最后一定要带个`/`

静态文件引用

在static目录下创建文件夹crm,再在crm下创建文件夹css,再在其中创建base.css文件,内容如下:

* {
    coler: red
}

在模板中引用静态文件有两种方法:

1、硬编码

不推荐使用。
在index.html中添加如下引用

<link rel="stylesheet" href="/static/crm/css/base.css">

/static/就是上面设置的STATIC_URL的值 crm/css/base.css就是我们想要引入的静态文件的路径。

2、模板标签引入

推荐使用。
在index.html中添加如下代码

{% load static %} #  写在模板头

...
<link rel="stylesheet" href="{% static 'crm/css/base.css' %}">

利用内置模板标签static动态解析。
如果一切正常,当再次访问http://127.0.0.1/crm/index/时,会看到所有的字都变成红色了。

web应用开发模式

web应用的开发主要有两种模式:

  • 前后端不分离
  • 前后端分离

前后端不分离

在互联网早期,web应用开发采用前后端不分离的方式。
它是以后端直接渲染模板完成响应的一种开发模式。
以前后端不分离的方式开发的web应用的架构图如下:
在这里插入图片描述
浏览器向服务器发起请求,服务器接收到请求后去数据库中获取数据,然后渲染html模板并返回渲染后的html数据,或者返回一个重定向。
绝大部分工作都在后端进行处理,客户端(浏览器)只负责页面的展示和用户交互。
这种开发方式有如下特点:

  • 一般一个应用只需要一个后台服务器
  • 客户端只需要请求不同的页面,服务器会完成页面上的所有数据逻辑,所以http请求次数较少
  • 每个请求都是一个html,会有大量的冗余数据
  • 开发时,前后端代码耦合高,出了问题责任不明确
  • 在开发单纯的网站时,效率非常高,有利于seo
  • 响应数据是html,只能适应单一客户端,当需要多端支持时如要单独开发
    在这里插入图片描述

前后端分离

随着ajax技术的出现,可以在不刷新页面向服务器发送http请求,所以又出现了前后端分离的开发模式。
后端只要开发接口即可,前端可以通过发起ajax请求,拿到后端的数据,渲染和怎样调用接口的事情,交给前端。
以前后端分离方式开发的web应用的结构图如下:
在这里插入图片描述
这种开发方式有如下特点:

  • 是目前web开发的主流模式
  • 需要静态文件服务器和后端接口服务器
  • 后台服务器只提供的数据接口的服务,响应的往往是json数据
  • 页面是动态渲染的,爬虫无法爬取页面的有效信息,不利于seo
  • 开发时,前后端责任分工明确
  • 前后端解耦合,可以同时开发,提高开发效率
  • 一个后台即可满足网站、app、小程序等多种应用的需要
    在这里插入图片描述

如何选择web开发模式

  • 对于主要功能是展示,没有复杂交互的网站,并且需要良好的seo,选择前后端不分离
  • 后端管理项目,交互性较强,不考虑seo,可以选择前后端分离
  • 另外,也可以结合业务,混合使用

请求和响应

web框架本质就是处理用户发起的请求,然后返回响应结果。请求,和响应就是框架中的数据流。

请求

当页面被请求时,django会创建一个HttpRequest对象,该对象包含关于请求的元数据。然后django加载适当的视图,将HttpRequest对象作为第一个参数传递给视图函数(这就是为什么所有的视图的第一个参数都命名为request的原因)。每个视图负责返回一个HttpResponse对象

HttpRequest对象

常用属性
  • .body

    原始请求的body,类型是字节类型。

  • .method
    代表请求中使用的http方法的字符串,保证是大写字母。例如在函数视图中通过判断request.method来处理不同的逻辑:

if request.method == 'GET':
    do_something()
elif request.method == 'POST':
    do_something_else()
  • .GET
    一个类似字典的对象,包含所有给定的HTTP GET参数(查询参数)。
  • .POST
    一个类似字典的对象,包含所有给定的表单格式的HTTP POST参数。
  • .FILES
    一个类似字典额对象,包含所有上传的文件。

请求参数类型

利用HTTP协议向服务器传参有几种途径?
1、查询字符串参数

  • url中?后面的key=value形式的参数
  • 在django中通过request.GET属性获取
    2、请求体参数
  • post方法的form表单
    • 在django中通过request.POST属性获取
  • json格式和put,patch,delete方法传递的form表单
    • 在django中通过request.body属性获取
  • 文件
    • 上传的文件通过request.FILES属性获取
      3、路径参数
  • 在django中通过路由规则传递给视图

响应

与django自动创建请求对象不同,创建响应对象是你的责任。你写的每个视图都要负责实例化,填充和返回一个响应对象。

HttpResponse
传入字符串

典型的用法是将页面的内容以字符串、或字节字符串的形式传递给 HttpResponse 构造函数:

>>> from django.http import HttpResponse
>>> response = HttpResponse("Here's the text of the Web page.")
>>> response = HttpResponse("Text only, please.", content_type="text/plain")
>>> response = HttpResponse(b'Bytestrings are also accepted.')

也可以增量的形式添加内容,你可以使用response作为一个类似文件的对象:

>>> response = HttpResponse()
>>> response.write("<p>Here's the text of the Web page.</p>")
>>> response.write("<p>Here's another paragraph.</p>")
设置头字段

使用HttpResponse.headers设置和删除一个字段。

>>> response = HttpResponse()
>>> response.headers['Age'] = 120
>>> del response.headers['Age']
设置状态码

直接传递参数status给构造函数

>>> from django.http import HttpResponse
>>> response = HttpResponse("Here's the text of the Web page.", status=200)
JsonResponse对象
  • class JsonResponse(data,encoder=DjangoJSONEncoder, safe=True,
    json_dumps_params=None, kwargs)

一个HttpResponse的子类,用来创建json编码的响应。默认的Content-Type为application/json。第一个参数data应该是字典实例。如果safe参数设置为False,它可以是任何json可序列化的对象。
典型的用法:

>>> from django.http import JsonResponse
>>> response = JsonResponse({'foo': 'bar'})
>>> response.content
b'{"foo": "bar"}'

序列化非字典对象:
为了对 dict 以外的对象进行序列化,你必须将 safe 参数设置为 False:

>>> response = JsonResponse([1, 2, 3], safe=False)

如果没有传递 safe=False,将引发一个 TypeError (opens new window)。

视图

定义

在django中,视图是一个可调用的对象,能够接受用户的请求并返回响应。在视图中通过模型操作数据库,通过模板构造返回数据。
约定将视图放在名为views.py的文件里,这个文件放置在项目或者应用目录里。

django中视图有两种:

  • 函数视图
  • 类视图

函数视图

最简单的视图,就是一个普通的python函数,它接受web请求并返回一个web响应。
例如:

from django.http import HttpResponse

def index(request):
    return HttpResponse("我是首页面")

特点:

  1. 逻辑清晰好理解
  2. 复用性较差

类视图

类视图是以类的形式组织视图,类视图并不能替代基于函数的视图,与函数视图相比,类视图有如下优势:

  • 与特定的HTTP方法关联 ,能通过单独的方法替代条件分支的方式处理不同的HTTP请求
  • 面向对象技术,可用于将代码分解为可重用的组件

类视图的基本使用

本质上来说,基于类的视图允许你使用不同的实例方法响应不同 HTTP 请求方法,而不是在单个视图函数里使用有条件分支的代码。

因此在视图函数里处理 HTTP GET 的代码应该像下面这样:

from django.http import HttpResponse

def my_view(request):
    if request.method == 'GET':
        # <view logic>
        return HttpResponse('result')

而在基于类的视图里,会变成:

from django.http import HttpResponse
from django.views import View

class MyView(View):
    def get(self, request):
        # <view logic>
        return HttpResponse('result')

类视图的路由

Django的路由系统会使用请求和相关参数来调动一个函数而不是一个类。基于类的视图有一个as_view()类方法,这个方法会返回一个函数。

# urls.py
from django.urls import path
from myapp.views import MyView

urlpatterns = [
    path('about/', MyView.as_view()),
]

ORM与模型

ORM

对象关系映射(Object Relational Mapping,简称ORM)!简单的说就是用面向对象的方式,描述数据库,操作数据库,达到不用编写SQL语句就能对数据库进行增删改查。

django内置了一套ORM框架,它的映射关系是:
在这里插入图片描述
然后通过对类,类属性,实例的各种操作,达到操作数据库的功能,底层是生成原生sql语句进行数据库操作。

安装数据库

我们的项目选择使用MariaDB,它是MySQL的一个分支,开源免费,越来越多的web项目开始使用它。直接安装数据库相对比较麻烦和难以维护,推荐使用docker安装数据库。安装docker的方案如下:

  • 方案一:
    windows直接安装Docker Desktop,不推荐,个人感觉还是很影响系统的使用,且需要开启虚拟服务,会与某些软件冲突。
    mac可以直接安装Docker Desktop。
  • 方案二:
    windows系统,安装虚拟机,然后安装linux的虚拟机,再到虚拟机中安装docker环境。
    虚拟机软件有,virtualbox(免费开源),vimware(收费),安装简单,使用稍复杂,资源占用大。
    用过虚拟机的童靴可以选择此方案,从来没用过的可以忽略。
    mac不建议。
  • 方案三:
    买一台云服务器,在云服务器中安装docker。
    阿里云,百度云,腾讯云,华为云,都有新用户优惠,几十块钱一年,推荐使用。
    最后项目的部署也会使用云服务器。
    阿里云新人优惠连接(opens new window)
    注意:系统选择ubuntu或者centos。
    docker命令:
docker run --name mariadb --restart=always -d -v mariadb:/var/lib/mysql -e MARIADB_ROOT_PASSWORD=pythonvip -p 4000:3306 -e MARIADB_DATABASE=easytest mariadb:latest

上面这条命令会根据mariadb数据库镜像mariadb:latest,创建一个名为mariadb的容器,并创建一个名为easytest的数据库,设置root账号的密码为pythonvip,映射3306端口到宿主机4000端口。

django配置数据库

安装驱动

django官方支持一下数据库:

  • PostgreSQL(opens new window)
  • MariaDB(opens new window)
  • MySQL(opens new window)
  • Oracle(opens new window)
  • SQLite

不同的数据库都需要对应的python数据库驱动程序。
我们接下来使用MariaDB数据库,它是MySQL的一个分支,在Django中它的配置和MySQL一致。
django推荐使用mysqlclient作为MySQL或MariaDB的数据库驱动

windows环境

在网站https://www.lfd.uci.edu/~gohlke/pythonlibs/#mysqlclient (opens new window)下载与python版本对应的mysqlclient本地安装文件,再使用pip命令安装,例如:

pip install mysqlclient‑1.4.6‑cp38‑cp38‑win_amd64.whl  # py3.8 64位
mac环境

mac环境下依赖mysql客户端。

$ brew install mysql-client
$ echo 'export PATH="/usr/local/opt/mysql-client/bin:$PATH"' >> ~/.bash_profile
$ source .bash_profile
$ pip install mysqlclient
Linux环境

linux环境下需要对应的依赖,根据环境不同依赖有所不同,下面的只是基本的步骤,不能保证在所有的环境上都有效。

Debian/Ubuntu

$ sudo apt-get install python3-dev default-libmysqlclient-dev build-essential
$ pip install mysqlclient

Red Hat /CentOS

sudo yum install python3-devel mysql-devel
pip install mysqlclient
连接配置

安装好数据库和数据库驱动后,还需要在settings.py配置模块中进行连接配置:
1、直接配置

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',		# 对应数据库引擎
        'NAME': 'easytest',							# 数据库名
        'USER': 'root',								# 用户名
        'PASSWORD': '12345678',						# 密码
        'HOST': '127.0.0.1',						# 主机
        'PORT': '3306',								# 端口
    }
}

2、通过配置文件配置

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '/path/to/my.cnf',		# 配置文件路径
        },
    }
}

# my.cnf
database = easytest				# 数据库名
user = root						# 用户名
password = 12345678				# 密码
host = 127.0.0.1				# 主机
port = 3306						# 端口

配置好后,运行python manage.py runserver如果可以正常运行说明数据库配置成功,项目成功连接数据库

模型

Django中模型准确且唯一的描述了数据。它包含储存的数据的重要字段和行为。一般来说,每一个模型都映射一张数据库表。

  • 每个模型都是一个 Python 的类,这些类继承django.db.models.Model
  • 模型类的每个属性都相当于一个数据库的字段
  • 利用这些,Django 提供了一个自动生成访问数据库的 API
创建模型

在创建模型前,先为我们的crm系统设计一张student表,表结构如下:

 `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NUll,
  `age` tinyint(1) DEFAULT NULL,
  `sex` tinyint(1) DEFAULT 1,
  `phone` varchar(20) DEFAULT NULL UNIQUE,
  `c_time` datetime(6) NOT NULL

然后根据表设计,在crm/models.py中编写如下模型类:

# models.py
class Student(models.Model):
    name = models.CharField('姓名', max_length=20, help_text='姓名')
    age = models.SmallIntegerField('年龄', null=True, blank=True, help_text='年龄')
    sex = models.SmallIntegerField('性别', default=1, help_text='性别')
    phone = models.CharField('手机号码', max_length=20, null=True, blank=True, unique=True, help_text='手机号码')
    c_time = models.DateTimeField('创建时间', auto_now_add=True)
    
    class Meta:
        db_table = 'tb_student'
        verbose_name = '学生信息'
        verbose_name_plural = verbose_name
        ordering = ['-c_time']
        
    def __str__(self):
		return self.name

上面的代码很简单,Student类就是一个模型,表示一张数据库表。类中有几个变量,每个变量表示数据库中的一个字段。

字段类型

每个字段由一个字段类的实例表示。
例如:字符字段使用CharField,日期时间使用DateTimeField。
每个字段实例的名称(例如,name,age)就是字段的名称。数据库将会使用它作为列名。
下面是常用字段类型和数据库字段类型的映射关系:
在这里插入图片描述

字段参数

每个字段在实例化时可以接收多个参数,用来提供不同的功能。常用的字段参数有:

  • verbose_name
    人类可读的字段详细名称,如果没有给定会使用字段的属性名自动创建。
  • primary_key
    主键设置,如果为True,则将该字段设置为模型的主键。如果编写模型是没有设置任何字段为主键,django会自动添加一个id字段作为主键。
  • unique
    唯一索引,如果为True,这个字段会在数据库层面创建唯一索引
  • blank
    如果为True,该字段允许为空。默认为False,注意该字段只与验证相关
  • null
    如果为True,django将在数据库存储空值NULL。默认为False。注意如果希望表单中允许控制,还需要设置blank=True,因为null参数只影响数据库的存储
  • default
    默认值。可以是一个值或者一个调用对象。当创建模型实例且没有为该字段提供值时,使用默认值
  • help_text
    额外的帮助文本,会随表单控件一同显示。它对生成文档也很有用
  • db_conlumn
    这个参数可以设置字段的数据库列名。如果不设置默认使用字段名作为数据库列名
  • validators
    该字段运行的验证器列表
  • error_messages
    这个参数可以覆盖字段引发的默认错误信息,传入一个与你想覆盖的错误信息相匹配的键值的字典
  • max_length
    CharField类型字段必须传递的参数,表示该字段的最大长度(以字符为单位)。max_length会在数据库层面强制执行。
自动设置主键

默认情况下,django给每个模型一个自动递增的主键,其类型在AppConfig.default_auto_field中指定,或者在DEFAULT_AUTO_FILED配置中全局指定。例如:

id = models.BigAutoField(primary_key=True)

如果你想自己指定主键,在你想要设置为主键的字段上设置参数primary_key=True。
注意每个模型都需要一个主键字段。

Meta选项

在模型内定义Meta类来给模型赋予元数据。
在我们的例子中db_table=‘tb_project’表示创建表的时候指定表名为tb_project,不使用默认的表名。默认会用应用名_模型名小写作为表名。
verbose_name和verbose_name_plural表示模型的可读名称的单数和复数,主要在admin站点中使用。
ordering=[’-c_time’]表示查询数据时默认按照c_time字段降序排列。
Meta选项较多,且和很多高级功能有关,在后面的课程中用到再详细讲解。

__str__方法

每个模型都应该定义一个__str__方法,当打印模型对象时,它的值友好的展示一个对象。

数据库迁移

编写模型后,django会将模型的修改应用至数据库进行关联,还需要将模型和数据库进行关联,这个操作称为迁移。

安装应用

要进行模型数据库迁移,首先需要将应用安装到项目中。在配置项INSTALLED_APPS中添加要安装的应用的设置类。例如,安装crm应用到项目中的配置如下:

study_django/settings.py
INSTALLED_APPS = [
    ...
    'crm.apps.CrmConfig'
]

应用的设置类是自动生成的,在应用根目录的apps.py模块中,安装应用时,使用点式路径,例如crm.apps.CrmConfig。

生成迁移文件

在命令行输入如下命令:

python mamage.py makemigrations crm

你将会看到类似于下面这样的输出:

Migrations for 'crm':
  crm\migrations\0001_initial.py
    - Create model Student

通过运行makemigrations命令,django会检测你对模型文件的修改(当前是创建),并把修改的部分存储为一次迁移。迁移是django对模型定义的变化的记录。上面的命令会在crm/migrations目录中生成0001_initial.py,这就是迁移文件。感兴趣可以打开阅读它们,别担心,不需要每次都阅读迁移文件,但是它被设计成人类可阅读的形式,这是便于手动调整django的修改方式。

执行迁移

生成迁移文件之后,还需要执行迁移,同步数据库。django通过命令migrate来执行迁移。
在运行迁移之前,我们可以看看,迁移会执行哪些SQL语句。
命令sqlmigrate接收一个迁移的名称,然后返回对应的SQl:

python manage.py sqlmigrate crm 0001

输出如下:

--
-- Create model Student
--
CREATE TABLE "tb_student" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(20) NOT NULL, "age" smallint N
ULL, "sex" smallint NOT NULL, "phone" varchar(20) NULL UNIQUE, "c_time" datetime NOT NULL);

输出格式跟你使用的数据库有关。
现在,运行migrate命令,在数据库里创建新定义的模型的数据表:

python .\manage.py migrate crm
Operations to perform:
  Apply all migrations: crm
Running migrations:        
  Applying crm.0001_initial... OK

django通过在数据库中创建一个特殊的表django_migrations来跟踪执行过哪些迁移。
migrate命令会执行所有没有执行过的迁移,将模型的修改同步到数据库结构上。
迁移时非常强大的功能,是的开发过程中持续的改变数据库结构而不需要重新删除和创建表,它专注于使用数据库平滑升级而不丢失数据。
记住,改变模型需要这三步:

  • 编辑models.py文件,修改模型
  • 运行python manage.py makemigrations为模型的改变生成迁移文件
  • 运行python manage.py migrate来应用数据库迁移
表关系

显然,关系型数据库的强大之处在于各表之间的关联关系。 Django 提供了定义三种最常见的数据库关联关系的方法:多对一,多对多,一对一。

表设计

为我们的crm系统设计学生表(tb_student),学生详情表(tb_student_detail),渠道表(tb_channel),课程表(tb_course),报名表(tb_entry),表关系和字段如下图:
在这里插入图片描述

多对一

上图中的学生表和渠道表,一个学生会对应一个渠道,一个渠道对应多个学生,学生表中的一条数据和渠道表中的一条数据对应,渠道表中的一条数据与学生表中的多条数据对应,学生表和渠道表形成多对一的关系。

在django中要表达多对一的关系需要使用django.db.models.ForeignKey字段,创建一个渠道模型,然后在Student模型中添加一个外键字段如下:

from django.db import models


class Student(models.Model):
	...
    channel = models.ForeignKey('Channel', null=True, on_delete=models.SET_NULL, help_text='渠道')
    ...
    
class Channel(models.Model):
    title = models.CharField('名称', max_length=20, help_text='渠道名称')

    class Meta:
        db_table = 'tb_channel'
        verbose_name = '渠道表'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.title

外键字段定义在多的一方,所以Student模型中定义了channel字段,这个字段名一般使用关系模型的小写名。

外键字段的第一个参数是一个位置参数,即需要关联的模型,可以是模型本身,也可以是模型的字符串形式的导入路径(当引用后定义的模型时很有用)。

在数据库层面,django会在字段名后附加_id来创建数据库列名。所以Student模型的数据库表将有一个channel_id列,然后会为这个列创建一个外键约束,被引用表为tb_channel,被引用字段为tb_channel.id。

注意:有时候为了效率,在数据库不创建外键约束,而是通过代码逻辑来保证数据的完整性。在django中可以在ForeiginKey字段中指定db_constraint=False来控制不创建外键约束。

级联操作

当一个由ForeignKey引用的对象被删除时,django通过on_delete参数指定的方法实现数据库级联操作。
on_delete的可能值有:

  • CASCADE
    级联删除
  • PROTECT
    通过引发ProtectedErro防止删除被引用字段
  • RESTRICT
    通过引发RestrictedError方式删除被引用字段
  • SET_NULL
    设置外键为空,只有null为True是才能
  • SET_DEFAULT
    将外键设置为默认值,必须为外键设置默认值

注意:外键字段必须指定参数on_delete。
当删除一个渠道时,在业务层面不大可能会删除对应的学生,应该是设置channel_id字段为null,所以这个外键字段的级联操作设置为null=True, on_delete=models.SET_NULL

多对多

数据库中多对多的关系通过第三张表来表示。

一个学生可以报名多门课程,一个课程可以被多名学生报名。在我们的案例中学生表和课程表通过报名表实现多对多的关系。

在django中要表达多对多的关系要使用django.db.models.ManyToManyField字段。

对于多对多关系的两个模型,可以在其中任意一个模型中定义多对多字段,但只能选择一个模型设置该字段,不能同时在两个模型中添加该字段。

一般来说应该把多对多字段放到需要在表单中编辑的对象中,或者是在业务中需要查询更多的模型中。

在我们的案例中,编辑学生对象,或者编辑课程对象时都不需要彼此,而在查询时,“报名了某个课程的学生有哪些”,这个需求会更多,所以把多对多的字段定义在课程表中。定义Course模型如下:

class Course(models.Model):
    name = models.CharField('名称', max_length=20, help_text='课程名称')
    students = models.ManyToManyField('Student', help_text='报名学生')

    class Meta:
        db_table = 'tb_course'
        verbose_name = '课程表'
        verbose_name_plural = verbose_name

多对多字段名一般设置为关系模型名的复数形式,表示关系模型的对象集合,所以Course模型中的多对多字段名为students。多对多字段的第一个参数是一个位置参数,既需要关联的模型,可以是模型本身,也可以是模型的字符串形式的导入路径(当引用后定义的模型时很有用)。

在数据库层面,django会自动创建一个中间表来表示多对多关系。默认情况下这个表名使用创建多对多字段的模型名和字段名生成,上面的例子会生成表名tb_course_students。然后包含两个字段,分别是两个模型的名字和_id组成(student_id,course_id),并创建外键引用对应表的id,还会对这两个字段创建联合唯一的约束。

自定义中间表

虽然django会自动创建第三张表,但是不能提供额外字段。如果中间表需要包含其他字段,就需要自定义中间表,然后在定义多对多字段时通过through参数指定第三张表。

所以创建一个Entry模型如下:

class Entry(models.Model):
    student = models.ForeignKey(Student, verbose_name='学生', help_text='学生', on_delete=models.PROTECT)
    course = models.ForeignKey(Course, verbose_name='课程', help_text='课程', on_delete=models.PROTECT)
    c_time = models.DateTimeField('报名时间', help_text='报名时间', auto_now_add=True)

    def __str__(self):
        return '{}-{}'.format(self.student.name, self.course.name)

    class Meta:
        db_table = 'tb_entry'
        verbose_name = '报名表'
        verbose_name_plural = verbose_name

在其中定义student和course外键字段分别和对应的模型形成多对一的关系。再定义c_time字段,用来记录报名时间。学生表,课程表通过报名来表达多对多关系。现在创建多对多字段主要是为了查询方便,可以在课程对象和学生对象上彼此关联,所以修改Course模型中的students字段如下:

class Course(models.Model):
    ...
    students = models.ManyToManyField('Student', help_text='报名学生', through='Entry')
	...
一对一

在django中要表达一对一的关系需要使用django.db.models.OneToOneField字段,概念上,这类似于ForeignKey与unique=True的结合。

在crm中,学生详情与学生表就是一对一的关系,创建模型如下:

class StudentDetail(models.Model):
    STATION_CHOICES = [
        ('功能测试工程师', '功能测试工程师'),
        ('自动化测试工程师', '自动化测试工程师'),
        ('测试开发工程师', '测试开发工程师'),
        ('测试组长', '测试组长'),
        ('测试经理', '测试经理')
    ]

    class SalaryChoice(models.TextChoices):
        FIRST = '5000以下', '5000以下'
        SECOND = '5000-10000', '5000-10000'
        THIRD = '10000-15000', '10000-15000'
        FOURTH = '15000-20000', '15000-20000'
        FIFTH = '20000以上', '20000以上'
    student = models.OneToOneField('Student', verbose_name='学生', on_delete=models.CASCADE, help_text='学生')
    city = models.CharField('城市', max_length=20, help_text='所在城市', null=True)
    company = models.CharField('就职公司', max_length=64, help_text='就职公司', null=True)
    station = models.CharField('岗位', choices=STATION_CHOICES, max_length=10, default='功能测试工程师', help_text='岗位')
    salary = models.CharField('薪资水平', choices=SalaryChoice.choices, max_length=20, default=SalaryChoice.THIRD, help_text='薪资水平')

    class Meta:
        db_table = 'tb_student_detail'
        verbose_name = '学生详情表'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.student.name

在数据库层面会创建student_id列,然后为这个列创建一个外键约束,引用表为tb_student,字段为tb_student.id。与多对一不同,这个列还会创建一个唯一约束,形成一对一的关系。其他的级联操作与多对一一样。

数据库操作

一旦创建 数据模型后,Django 自动给予你一套数据库抽象 API,允许你创建(create),检索(retrieve),更新(update)和删除(delete)对象。

为了方便调试我们通过下面的命令进入交互式python命令行:

python manage.py shell

我们使用这个命令而不是简单的使用python是因为 manage.py 会设置 DJANGO_SETTINGS_MODULE 环境变量,这个变量会让 Django 根据项目配置文件来设置 Python 包的导入路径。

queryset对象的query属性可以看到执行的sql,但是只能对queryset对象使用,所以insert、update就不能使用。

在日志等级debug=True的情况下,下面的代码可以打印出所有执行过的sql语句。

from django.db import connection
print(connection.queries)

创建对象

创建一个模型对象,可以直接通过关键字参数实例化,然后调用save()方法将其存入数据库。

from crm.models import Student
s = Student(name='心蓝', age=18)
s.save()

这在幕后执行了 INSERT SQL 语句。

django在你显示的调用save()才会操作数据库。save()方法没有返回值。

还有以一种创建对象并一步到位的保存方法是create。

# 使用create方法,create方法直接写入数据库
st = Student.objects.create(name='赵六')

还可以使用get_or_create方法,先查询,没有才创建,但是如果找到了多个会引发MultipleObjectsReturned错误。

 Student.objects.get_or_create(name='xinlan')
 (<Student: Student object (1)>, False)

返回值是一个元组,第一个元素是模型对象,如果是查询返回的,第二个元素为False,如果是新创建则为True。

批量创建

批量创建对象可以利用上面的方法通过for循环来实现,但是会执行多条sql语句,不够高效。

bulk_create(objs, batch_size=None, ignore_conflicts=False)这个方法可以有效的将提供的对象插入数据表(一般来说,不管多少个对象,只需进行一次查询),并以列表形式返回创建的对象,顺序与提供的相同:

Students.objects.bulk_create([
	Student(name='张三'),
	Student(name='李四')
])

但是需要注意:

  1. 模型的save()方法不会被调用,pre_save和post_save信号将不会被发送
  2. 在多表继承的模式下,它不能与子模型一起工作
  3. 如果模型的主键是一个AutoField,主键属性只能在某些数据库(目前是PostgreSQL,MariaDB 10.5+,和 SQLite 3.35+)上检索到。在其他数据库中,它将不会被设置。
  4. 对于多对多的关系,它行不通。

batch_size 参数控制在一次查询中创建多少对象。默认情况是在一个批次中创建所有对象,但 SQLite 除外,默认情况是每个查询最多使用 999 个变量。

在支持它的数据库上(除了Oracle),将 ignore_conflicts 参数设置为 True 告诉数据库忽略插入任何不合格的约束条件的行,如重复的唯一值。启用该参数会禁用在每个模型实例上设置主键(如果数据库正常支持的话)。

更新对象

要将修改保存至数据库中已有的某个对象,使用save()方法。

有一个已经存入数据库中的Student实例s,修改其年龄,并在数据库中更新其记录:

s.age = 19
s.save()
指定要更新的字段

直接调用save()方法更新时,在sql中会更新所有字段。

可以在调用save()方法时传递给参数update_fields一个字段名列表,那么只有列表中命名的字段才会被更新,而不是所有的字段都被更新,可以轻微提升性能优势。

update_fields参数可以是任何包含字符串的可迭代对象。一个空的update_fields可迭代对象将跳过保存。值为None将对所有字段进行更新。

s.name = '心蓝'
s.save(update_fields=['name']) # 在update语句上只会更新name字段
一次更新多个对象

有时候,你想统一设置查询集中的所有对象的某个字段,你可以通过update()方法。例如:

Student.object.all().update(sex=1)

方法 update() 立刻被运行,并返回匹配查询调节的行数(若某些行早已是新值,则可能不等于实际匹配的行数)。

要认识到 update() 方法是直接转为 SQL 语句的。这是一种用于直接更新的批量操作。它并不会调用模型的save()方法,或发射pre_save或post_save信号,或使用auto_now字段选项。

若想保存查询集中的每项,并确保调用了每个实例的save()方式,你并不需要任何特殊的函数来处理问题。迭代它们,调用它们的save()方法:

for item in my_queryset:
    item.save()
检索对象
管理器

要从数据库检索对象,要通过模型类的Manager构建一个QuerySet。

每个模型至少有一个Manager,默认名称是objects。像这样直接通过模型类使用它们:

>>> Student.objects
<django.db.models.manager.Manager at 0x1066850a0>
>>> s = Student(name='xinlan', age=18)
>>> s.objects
Traceback:
    ...
AttributeError: "Manager isn't accessible via Student instances."
QuerySet

一个QuerySet代表来自数据库中对象的一个集合,它可以迭代,支持切片,不支持负索引,可以通过list将其转换成列表。在SQL的层面上,QuerySet对应SELECT语句。

检索全部对象

从数据库中检索对象最简单的方式就是检索全部。只需要在Manager上调用all()方法:

all_students = Student.objects.all()

方法all()返回一个包含数据库表中所有对象的QuerySet对象。

可以通过打印query属性,查看当前QuerySet对象将执行的查询语句。

>>> print(all_studnets.query)
SELECT "tb_student"."id", "tb_student"."name", "tb_student"."age", "tb_student"."sex", "tb_student"."phone", "tb_student"
."c_time", "tb_student"."channel_id" FROM "tb_student" ORDER BY "tb_student"."c_time" DESC
过滤

all()返回的QuerySet包含了数据表中所有的对象。通过下面的两个方法可以对QuerySet进行过滤。

filter(**kwargs)

返回一个新的QuerySet,包含的对象满足给定查询参数。

exclude(**kwargs)

返回一个新的QuerySet,包含的对象不满足给定查询参数。

例如查询所有叫心蓝的学生

Student.objects.filter(name="心蓝")
# 或者
Student.objects.all().filter(name='心蓝')
检索单个对象

filter()总是返回一个QuerySet,即便只有一个对象满足查询条件——这种请情况下,QuerySet只包含一个元素。

如果知道只有一个对象满足查询条件,可以在管理器上使用get()方法,它会直接返回这个对象:

s = Student.objects.get(pk=1)  # 如果不存在会抛出异常

除了get方法之外,还有一些快捷方法获取单个对象。

  • first()

    Student.objects.first()  # 获取第一个对象
    
  • last

    Student.objects.last()   # 获取最后一个对象
    
排序

order_by(*fields),会根据给定字段排序,默认QuerySet按照模型类中的Meta类中定义的来排序。管理器和QuerySet上都可以调用。

Student.objects.all().order_by('name')  # 根据名字升序排序
Student.objects.order_by('-name') # 根据名字降序排序
切片

使用python列表的切片的语法来获取部分数据,它等价于SQL中LIMIT与OFFSET子句。

Student.objects.all()[:5]   # 获取前5条 等价于  LIMIT 5
Student.objects.all()[2:5]   # 等价于 LIMIT 3 OFFSET 2
选择字段
  • values(fields)
    返回一个QuerySet,这个QuerySet返回一个字典列表,而不是数据对象。参数fields指定了select语句中我们想要限制查询的字段。返回的字典中只会包含我们指定的字段。如果不指定,则包含所有字段。

    Student.objects.values('name')
    <QuerySet [{'name': '张柏芝'}, {'name': '刘德华'}, {'name': '心蓝'}]>
    
  • only(fields)
    返回一个QuerySet,这个QuerySet返回一个对象列表。参数fields指定了select语句中我们想要限制查询的字段。注意,only一定会包含主键字段。

    Student.objects.only('name')
    <QuerySet [<Student: 张柏芝>, <Student: 刘德华>, <Student: 心蓝>]>
    
  • defer(fields)
    返回一个QuerySet,这个QuerySet返回一个对象列表。参数fields指定了select语句中我们想要限制查询的字段。注意,only一定会包含主键字段。

    Student.objects.only('name')
    <QuerySet [<Student: 张柏芝>, <Student: 刘德华>, <Student: 心蓝>]>
    
  • defer(fields)
    返回一个QuerySet,这个QuerySet返回一个对象列表。参数fields指定了select语句中我们想要排除的查询的字段。注意,defer一定会包含主键字段。

    Student.objects.defer('c_time')
    

    only和defer返回的对象还是可以正常访问没有包含在select语句中的字段,只是会再次查询数据库

条件查询

在filter,exclude,get中可以接收参数实现各种比较条件的查询。

  • exact
    准确匹配.如果给的值是None,它会被解释成SQL NULL 看下面的案例

    Student.objects.get(id__exact=14)
    Student.objects.get(qq__exact=None)  
    

id__exact=14等价于id = 14,默认情况不带exact

  • iexact 不分大小写的匹配。

    Blog.objects.get(name__iexact='beatles blog')
    Blog.objects.get(name__iexact=None)
    
  • in
    在一个给定的可迭代对象中,通常是一个列表,元组,或queryset。虽然不是经常用但是字符串也可以

    Entry.objects.filter(id__in=[1, 3, 4])
    Entry.objects.filter(headline__in='abc')
    
  • gt
    大于

    Entry.objects.filter(id__gt=4)
    
  • gte 大于等于

  • lt 小于

  • lte 小于等于

  • range
    范围区间

    Student.objects.filter(age__range=(18, 20))
    
条件组合
  • AND
    使用 SQL AND 操作符将两个 QuerySet 组合起来。

    以下的都是相同的

    Model.objects.filter(x=1) & Model.objects.filter(y=2)
    Model.objects.filter(x=1, y=2)
    from django.db.models import Q
    Model.objects.filter(Q(x=1) & Q(y=2))
    

    SQL 等价于:

    SELECT ... WHERE x=1 AND y=2
    
  • OR
    使用 SQL OR 操作符将两个 QuerySet 组合起来。

    以下的都是相同的:

    Model.objects.filter(x=1) | Model.objects.filter(y=2)
    from django.db.models import Q
    Model.objects.filter(Q(x=1) | Q(y=2))
    

    SQL 等价于:

    SELECT ... WHERE x=1 OR y=2
    
聚合查询
  • count 统计数量

     # 统计所有学生的数量
     Students.objects.count()
     # 统计所有男生的数量
     Students.objects.filter(sex=1).count()
    
  • Avg 平均值

     # 计算同学们的年龄平均值
     from django.db.models import Avg
     Students.objects.aggregate(age_avg=Avg('age'))
    
  • Max 最大值

     # 找到最大年龄的学生
     from django.db.models import Max
     Students.objects.aggregate(Max('age'))
    
  • Min 最小值

     # 找到最小年龄的学生
     from django.db.models import Min
     Students.objects.aggregate(Min('age'))
    
  • Sum 求和

     # 计算缴费总金额
     from django.db.models import Sum
     Enroll.objects.aggregate(Sum('pay'))
    
分组

分组,聚合,需要结合values,annotate和聚合方法看下面的案例

# 查询男生女生多少人
Student.objects.values('sex').annotate(Count('sex'))  # annotate 默认按照主键分组
删除对象

通常,删除方法被命名为 delete()。该方法立刻删除对象,并返回被删除的对象数量和一个包含了每个被删除对象类型的数量的字典。例子:

s = Student.objects.get(pk=1)
s.delete()
关联对象操作
多对一
正向

一个模型如果有一个外键字段,通过这个模型对外键进制操作叫做正向

  • 更新,修改

    # 给学生设置渠道属性
    # 1 通过属性赋值的方式
    ch = Channel('百度')
    ch.save()
    s1 = Student(name='心蓝', age=18)
    s1.channel = ch
    s1.save()
    # 2 通过主键的方式
    s2 = Student(name='小明', age=19)
    s2.channel_id = ch.id
    s2.save()
    
  • 删除
    如果一个外键字段有null=True的设置(即,它允许空值),您可以指定None来删除关系。例如:

    s1.channel = None
    s1.save()
    
  • # 查询所有百度渠道的学生
    Student.objects.filter(channel__name='百度') #  跨表查询
    channel = Channel.objects.get(name='百度')
    channel.student_set.all()  # 反向查询
    

外键字段对象的属性可以通过两个下划线来获取

反向

一个模型如果被另外一个模型外键关联,通过这个模型对关联它的模型进行操作叫做反向。

如果一个模型有一个ForeignKey,那么这个外键模型的实例将可以访问一个返回第一个模型的所有实例的管理器。默认情况下,这个管理器名为foo_set,其中foo是源模型名,小写。这个管理器返回queryset,可以按照前面检索对象一节中描述的那样对其进行过滤和操作。

通过在定义字段的时候设置参数 related_name 可以替代上面的管理器名

  • # 1.通过主表创建从表数据
    new_student = ch.student_set.create(name='韩梅梅', age=16, sex=0) 
    # 2.增加多条数据
    ch.student_set.add(s1,s2,s3)
    # 会将s1,s2,s3全部加入关联对象集合
    
  • # 1. 从相关对象中移除指定的模型对象
    # 清除某个渠道中的某些学生
    ch.student_set.remove(s1, s2, ...)
    # 2. 从相关对象中删除所有的 对象
    # 清除某个渠道中的所有学生
    ch.student_set.clear()
    
  • # 替换对象集
    ch.student_set.set([s1, s2])
    # 如果clear 可以调用 先clear再添加,如果过不行就直接添加
    
  • # 1. 查询所有
    ch.student_set.all()
    # 2. 条件查询
    ch.student_set.filter(name='心蓝')
    # 和objects一样的使用
    
多对多

多对多两端都可以获得另一端的自动API访问。该API的工作原理类似上面的反向多对一关系,都是一个多对多的管理器对象。

定义ManyToManyField的模型使用该字段本身的名称,反向模型使用关系模型的名称小写加上_set。看下面的例子更容易理解:

# 先创建几个学生s1,s2,s3 几个课程 c1,c2,c3
# 学生s1报名课程c1,c2,c3
s1.course_set.add(c1,c2,c3)
# 学生s1,s2,s3报名课程c1
c1.students.add(s1,s2,s3)
# 学生s1报名的课程有哪些
s1.course_set.all()
# 课程c1有哪些学生报名

和ForeignKey一样ManyToMany也可以指定related_name。在上面的案例中,如果定义在Course中的ManyToManyField指定了related_name=‘courses’,那么每一个Student对象都会有一个Courses的属性替代course_set。

和一对多关系不同的是,除了模型实例外,多对多关系上的add(),set(),和remove()方法还接受主键值。例如这些set()调用的工作方式是相同的:

c1.students.set([s1,s2,s3])
c1.students.set([s1.pk,s2.pk,s3,pk])

当多对多的关系指定了中间表的时候,还可以通过中间表对象像普通的多对一关系一样操作。看下面的案例:

# 创建一个报名记录,s1学员报名了c1课程
e = Enroll()
e.course = c1
e.student = s1
e.save()

你可能会问,既然这样,那指定中间表还要多对多字段干什么?因为查询非常方便,看下面的案例:

# 查询报名某课程的学生
c1.students.all()
# 查询某学生报名的课程
s1.course_set.all()

使用ManyToManyField之后,这两个表可以有api直接访问

一对一

一对一关系非常类似于多对一关系。如果模型上定义了一对一字段,那么该模型的实例将可以通过模型的一个简单属性访问相关对象。例如:

# 给某学生添加学生详情
s1 = Student.objects.first()
sd = StudentDetail.objects.create(student=s1,city='北京',salary=50000)
<Student: yoyo>

不同之处在于反向查询。一对一关系中的相关模型可以通过相关模型的小写属性名访问一个Manager对象,但是这个Manager只是一个对象,而不是一个对象集合:

s1.studentdetail
<StudentDetail: yoyo>

如果没有给这个关系分配对象,Django将抛出一个DoesNotExist异常

跨表查询

Django提供了一种强大而直观的方法,可以在查询中“跟踪”关系,在幕后自动处理为SQL连接。要跨越关系,只需使用跨模型的相关字段的字段名,以双下划线分隔,直到您到达您想要的字段为止。

例如,查询男生都报名了什么课程

Course.objects.filter(students__sex=1).distinct() 

这个关系要多深就可以有多深。

它也向后工作。要引用反向关系,只需使用模型的小写名称。

这个例子查询所有报名了python课程的学员:

Student.objects.filter(course__name__contains='python') 

我们再看一个例子:

查询所有报名了python的百度渠道的学员

Student.objects.filter(course__name__contains='python', channel__name='百度')
执行原生SQL
1. 执行原生查询并返回模型实例

在管理器上调用raw()方法用于执行远程SQL查询,就会返回模型实例,语法格式如下:

Manager.raw(raw_query, params=(), translations=None)

该方法接受一个原生 SQL 查询语句,执行它,并返回一个 django.db.models.query.RawQuerySet 实例。这个 RawQuerySet 能像普通的 QuerySet一样被迭代获取对象实例。

for stu in Student.objects.raw('select * from crm_student'):
    print(stu)
2. 执行原生查询

直接调用原生数据驱动执行SQL。

from django.db import connection

def my_custom_sql():
    with connection.cursor() as cursor:
        cursor.execute("select * from crm_student where age= %s", [18])
        row = cursor.fetchone()

    return row

项目实战一

需求

以前后端不分离的方式实现学生的增删改查操作

学生列表功能

接口设计

url:/students/

请求方法:get

参数:

  • 格式:查询参数
参数名类型是否必传说明
pageint页码,默认为1
sizeinit每页数据条数默认为10
namestr根据姓名过滤
ageint根据年龄过滤
sexint根据性别过滤
phonestr根据手机过滤
channel_idint根据渠道过滤

响应:html

代码
试图
import math
from urllib.parse import urlencode


def student_list(request):
    """学生列表视图"""

    # 1. 获取查询参数
    query_params = {key: value for key, value in request.GET.items()}
    # 2. 获取分页参数
    page = int(query_params.pop('page', 1))
    size = int(query_params.pop('size', 10))
    # 3. 获取查询集
    queryset = Student.objects.all()
    for key, value in query_params.items():
        try:
            queryset = queryset.filter(**{key: value})
        except:
            pass
    # 4. 分页处理
    # 数据总条数
    total_num = queryset.count()
    # 总页数
    total_page = math.ceil(total_num/size)
    # 下一页
    if page < total_page:
        query_params.update({'page': page+1, 'size': size})
        next_page_query_params = urlencode(query_params)
    else:
        next_page_query_params = None

    # 上一页
    if page > 1:
        query_params.update({'page': page - 1, 'size': size})
        pre_page_query_params = urlencode(query_params)
    else:
        pre_page_query_params = None
    # 分页过滤
    queryset = queryset[(page-1)*size:page*size]

    # 5. 渲染模板并返回响应
    return render(request, 'crm/student_list.html', context={
        'queryset': queryset,
        'pre_page': pre_page_query_params,
        'next_page': next_page_query_params,
        'page': page,
        'total_num': total_num,
        'total_page': total_page
    })
路由
path('students/', views.student_list, name='student-list'),
模板
<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">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>学生列表</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
          integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">

    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
    <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
    <![endif]-->
</head>
<body>
<div class="container">
    <h1>学生列表</h1>
    <div style="width: 800px">
        {% if queryset %}
        <a class="btn btn-success" style="float: right" href="{% url 'student-create' %}">添加</a>
        <table class="table table-hover table-bordered table-condensed">
            <thead>
            <tr>
                <th>序号</th>
                <th>姓名</th>
                <th>年龄</th>
                <th>性别</th>
                <th>电话</th>
                <th>渠道</th>
                <th>创建时间</th>
                <th>操作</th>
            </tr>
            </thead>
            <tbody>
            {% for obj in queryset %}
                <tr>
                    <td>{{ forloop.counter }}</td>
                    <td>{{ obj.name }}</td>
                    <td>{{ obj.age | default:"" }}</td>
                    <td>{% if obj.sex == 1 %}男{% else %}女{% endif %}</td>
                    <td>{{ obj.phone | default:"" }}</td>
                    <td>{% if obj.channel %}{{ obj.channel.title }}{% endif %}</td>
                    <td>{{ obj.c_time }}</td>
                    <td><a class="btn btn-primary" href="{% url 'student-update' obj.id %}">编辑</a>
                        <a class="btn btn-danger" href="{% url 'student-delete' obj.id %}">删除</a></td>
                </tr>
            {% endfor %}
            </tbody>
        </table>
        <nav aria-label="...">
            <ul class="pager">
                <li class="previous {% if not pre_page %}disabled{% endif %}"><a
                        href="{% if pre_page %}?{{ pre_page }}{% else %}#{% endif %}"><span
                        aria-hidden="true">&larr;</span> 上一页</a></li>
                <li class="next {% if not next_page %}disabled{% endif %}"><a
                        href="{% if next_page %}?{{ next_page }}{% else %}#{% endif %}">下一页 <span aria-hidden="true">&rarr;</span></a>
                </li>
            </ul>
        </nav>
        {% else %}
            <p>查不到数据...</p>
        {% endif %}
    </div>
</div>


<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
{#    <script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>#}
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
{#    <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>#}
</body>
</html>

访问http://127.0.0.1/crm/students/效果图如下:
在这里插入图片描述

学生添加功能

学生添加页面
接口设计

url:/students/create/

请求方法:get

响应:html页面

代码

1、试图

class StudentCreateView(View):
    """
    学生添加视图
    """
    def get(self, request):
        """学生添加页面"""
        # 1. 获取渠道对象
        channels = Channel.objects.all()
        return render(request, 'crm/student_detail.html', context={'channels': channels})

2、路由

path('students/create/', views.StudentCreateView.as_view(), name='student-create')

3、模板

<!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">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>{% if obj %}修改{% else %}添加{% endif %}学生</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">

    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
    <![endif]-->
  </head>
  <body>
    <div class="container">
       <div style="width: 800px">
    <h1>学生{% if obj %}修改{% else %}添加{% endif %}页面</h1>
    <form class="form-horizontal" method="post">
  <div class="form-group">
    <label for="name" class="col-sm-2 control-label">姓名</label>
    <div class="col-sm-10">
      <input type="text" class="form-control" id="name" name="name" placeholder="姓名" value="{{ obj.name }}">
    </div>
  </div>
  <div class="form-group">
    <label for="age" class="col-sm-2 control-label">年龄</label>
    <div class="col-sm-10">
      <input type="text" class="form-control" id="age" name="age" placeholder="年龄" value="{{ obj.age|default:"" }}">
    </div>
  </div>
  <div class="form-group">
    <label for="sex" class="col-sm-2 control-label">性别</label>
    <div class="col-sm-10">
        <select name="sex" id="sex" class="form-control">
            <option value="1" {% if obj.sex == 1%}selected{% endif %}>男</option>
            <option value="0" {% if obj.sex == 0%}selected{% endif %}>女</option>
        </select>
    </div>
  </div>
  <div class="form-group">
    <label for="phone" class="col-sm-2 control-label">电话号码</label>
    <div class="col-sm-10">
        <input type="text" class="form-control" id="phone" name="phone" placeholder="phone" value="{{ obj.phone|default:"" }}">
    </div>
  </div>
  <div class="form-group">
    <label for="channel" class="col-sm-2 control-label">渠道</label>
    <div class="col-sm-10">
        <select name="channel" id="channel" class="form-control">
            <option value="">--------</option>
            {% for ch in channels %}
                <option value="{{ ch.id }}" {% if obj.channel.title == ch.title %}selected{% endif %}>{{ ch.title }}</option>
            {% endfor %}
        </select>
    </div>
  </div>

  <div class="form-group">
    <div class="col-sm-offset-2 col-sm-10">
      <button type="submit" class="btn btn-default">{% if obj %}修改{% else %}添加{% endif %}</button>
{#      <button type="submit" class="btn btn-default">添加</button>#}
    </div>
  </div>
</form>
    </div>
    </div>


    <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
    <script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
    <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
  </body>
</html>

访问http://127.0.0.1/crm/students/create/效果图如下:
在这里插入图片描述

学生添加
接口设计

url:/students/create/

请求方法:post

参数:

  • 格式:form
参数名类型是否必传说明
namestr姓名
ageint年龄
sexint性别
phonestr手机
channel_idint渠道id

响应:重定向到学生列表页面

代码

1、视图

class StudentCreateView(View):
    """
    学生添加视图
    """
	...
    def post(self, request):
        """添加学生"""
        # 1. 获取前端传递的数据
        data = {key: value for key, value in request.POST.items() if value}
        if data.get('channel', None):
            data['channel_id'] = data.pop('channel')
        # 2. 创建学生
        try:
            obj = Student.objects.create(**data)
        except Exception as e:
            # 粗糙的处理现在就够了
            return HttpResponse(str(e), status=400)
        # 3. 返回响应
        return redirect(reverse('student-list'))

2、路由
同上

学生修改功能

学生修改页面
接口设计

url:/students/update/

请求方法:get

响应:html页面

代码

1、试图

from django.shortcuts import get_object_or_404


class StudentUpdateView(View):
    """
    学生更新视图
    """

    def get_obj(self, pk):
        obj = get_object_or_404(Student, pk=pk)
        return obj

    def get(self, request, pk):
        # 1. 获取修改对象
        obj = self.get_obj(pk)
        # 2. 获取渠道对象
        channels = Channel.objects.all()
        # 2. 渲染并返回修改页面
        return render(request, 'crm/student_detail.html', context={'channels': channels, 'obj': obj})

2、路由

path('students/update/<int:pk>/', views.StudentUpdateView.as_view(), name='student-update')

3、模板

同添加功能模板
学生修改
接口设计

url:/students/update/pk/

请求方法:post

参数:

  • 路径参数
  • 格式:form
参数名类型是否必传说明
namestr姓名
ageint年龄
sexint性别
phonestr手机
channel_idint渠道id

响应:重定向到学生列表页面

代码

1、视图

class StudentUpdateView(View):
    """
    学生更新视图
    """
    ...

    def post(self, request, pk):
        # 1. 获取修改对象
        obj = self.get_obj(pk)
        # 2. 获取前端传递的数据
        data = {key: value for key, value in request.POST.items() if value}
        if data.get('channel', None):
            data['channel_id'] = data.pop('channel')
        # 3. 更新学生
        for key, value in data.items():
            setattr(obj, key, value)
        try:
            obj.save(update_fields=data.keys())
        except Exception as e:
            # 粗糙的处理现在就够了
            return HttpResponse(str(e), status=400)
        # 4. 返回响应
        return redirect(reverse('student-list'))

2、路由
同上

学生删除功能
接口设计

url:/students/delete/pk/

请求方法:get

参数:

  • 路径参数
    响应:重定向到学生列表页面
代码

1、试图

def student_delete(request, pk):
    # 1. 获取对象
    obj = get_object_or_404(Student, pk=pk)
    # 2. 删除对象
    obj.delete()
    # 3. 返回响应
    return redirect(reverse('student-list'))

2、路由

path('students/delete/<int:pk>', views.student_delete, name='student-delete')

表单

在之前的案例中,每次我们需要提交表单数据的时候。我们都需要去手动编辑html表单,根据不同的字段,字段名,进行编码。做了很多重复的部分,所以django提供了一个专门用来处理表单的类,django.forms.Form。

通过它,我们不仅能够自动生成前端页面,也可以用来验证数据的合法性。我们通过改写添加修改学生的表单来学习它

创建表单

在app根目录下,创建一个forms.py的模块,代码如下:

from django import forms
from .models import Channel


class StudentForm(forms.Form):
    name = forms.CharField(label='姓名', max_length=20)
    age = forms.IntegerField(label='年龄', required=False)
    sex = forms.ChoiceField(label='性别', choices=((1, '男'), (0, '女')))
    phone = forms.CharField(label='手机号码', required=False, max_length=20)
    channel = forms.ModelChoiceField(label='渠道', required=False, queryset=Channel.objects.all())

每一个模型表单,都是forms.Form的一个子类,类属性与模型的类属性类似,都表示不同类型的字段。不同的字段,将会渲染成不同的input类型。字段名与每一个input标签的name属性对应。

每一个字段都是一个字段类的实例,其中label参数渲染成label标签的内容。max_length用来限制用户输入字符长度。required参数表示该字段是否必填,默认为True,要指定一个字段是不必填的,设置required=False

在模板中使用表单

只需要将表单实例放到模板上下文就可以通过模板变量使用表单。

渲染表单对象

修改学生添加页面视图如下:

from .forms import StudentForm


class StudentCreateView(View):
    """
    学生添加视图
    """
    def get(self, request):
        """学生添加页面"""
        # 1. 获取渠道对象
        channels = Channel.objects.all()
        form = StudentForm()
        return render(request, 'crm/student_detail.html', context={'channels': channels, 'form': form})

在视图中,实例化了一个表单对象,然后传递变量form给了模板。那么在模板中通过{{ form }}将会渲染对应的

<tr><th><label for="id_name">姓名:</label></th><td><input type="text" name="name" maxlength="20" required id="id_name"></td></tr>
<tr><th><label for="id_age">年龄:</label></th><td><input type="number" name="age" id="id_age"></td></tr>
<tr><th><label for="id_sex">性别:</label></th><td><select name="sex" id="id_sex">
  <option value="1"></option>
  <option value="0"></option>
  <option value="1">百度</option>
  <option value="2">抖音</option>
  <option value="3">b站</option>
</select></td></tr>

我们看到表单对象默认渲染了表格格式的字段,所以需要在模板中提供外层标签和submit控件。

那么在模板中可以按照如下方式渲染:

<form >
    <talbe>
    	{{ form }}
    </talbe>
    <input type="submit" value="添加" />
</form>    

对于表单字段的渲染,还有如下格式:

  • {{ form.as_table }} 字段会渲染成表格元素<tr>
  • {{ form.as_p }} 字段会渲染成<p>标签
  • {{ form.as_ul }} 字段会渲染成<li>标签
    注意
    记得提供外层的<table>或<ul>元素
手动渲染字段

直接渲染表单对象,不是太灵活,我们可以手动处理。每个字段都可以用{{ form.name_of_field }}作为表单的一个属性,并被相应的渲染在模板中。例如:

{{ form.non_field_errors }}
<div class="fieldWrapper">
    {{ form.subject.errors }}
    <label for="{{ form.subject.id_for_label }}">Email subject:</label>
    {{ form.subject }}
</div>
<div class="fieldWrapper">
    {{ form.message.errors }}
    <label for="{{ form.message.id_for_label }}">Your message:</label>
    {{ form.message }}
</div>
<div class="fieldWrapper">
    {{ form.sender.errors }}
    <label for="{{ form.sender.id_for_label }}">Your email address:</label>
    {{ form.sender }}
</div>
<div class="fieldWrapper">
    {{ form.cc_myself.errors }}
    <label for="{{ form.cc_myself.id_for_label }}">CC yourself?</label>
    {{ form.cc_myself }}
</div>

完整的

<div class="fieldWrapper">
    {{ form.subject.errors }}
    {{ form.subject.label_tag }}
    {{ form.subject }}
</div>
渲染表单错误信息

表单的错误信息分两种,一种是{{ form.name_of_field.errors }}显示对应字段的错误信息列表,它默认被渲染成为无序列表,看起来如下:

<ul class="errorlist">
    <li>Sender is required.</li>
</ul>

该列表有一个CSS class errorlist ,允许自定义样式。如果想要进一步定义错误信息的显示,可以通过遍历来实现:

{% if form.subject.errors %}
    <ol>
    {% for error in form.subject.errors %}
        <li><strong>{{ error|escape }}</strong></li>
    {% endfor %}
    </ol>
{% endif %}

第二种是{{ form.non_field_errors }}显示非字段验证错误信息,它渲染后看起来如下

<ul class="errorlist nonfield">
    <li>Generic validation error</li>
</ul>

该列表会额外带上一个classnonfield以便与字段验证错误信息区分

遍历表单字段

如果表单字段使用相同的结构,可以对表单对象进行迭代:

{% for field in form %}
    <div class="fieldWrapper">
        {{ field.errors }}
        {{ field.label_tag }} {{ field }}
        {% if field.help_text %}
        <p class="help">{{ field.help_text|safe }}</p>
        {% endif %}
    </div>
{% endfor %}

有用的字段属性:

  • {{ field.id_for_label }}

    该字段的id,用于手动构建label

  • {{ field.value }}

    该字段的值

  • {{ field.html_name }}

    字段名称,用于输入元素的name属性中。如果设置了表单前置,它也会被加进去。

  • {{ field.help_text }}

    与该字段关联的帮助文本

  • {{ field.errors }}

    输出错误信息列表

  • {{ field.is_hidden }}

    如果该字段是隐藏字段,这个属性是True,否则为False

部件

每一个表单字段,都会有一个对应的HTML元素与之对应。部件用来处理HTML渲染,以及从对应的GET/POST字典中提取数据

指定部件

每一个表单字段,django都会使用一个默认的部件来显示数据类型。要想知道哪个字段使用哪个部件,请查看内置Field类 (opens new window)。

有时候我们可能需要修改默认的部件,通过字段参数widget来处理。例如:

from django import forms

class CommentForm(forms.Form):
    name = forms.CharField()
    url = forms.URLField()
    comment = forms.CharField(widget=forms.Textarea)

字段comment将会使用Textarea部件,而不是默认的TextInput部件。

样式化部件实例

默认情况下,部件渲染的表单标签没有css类,没有额外属性。可以通过attrs参数进行设置:

class CommentForm(forms.Form):
    name = forms.CharField(widget=forms.TextInput(attrs={'class': 'special'}))
    url = forms.URLField()
    comment = forms.CharField(widget=forms.TextInput(attrs={'size': '40'}))

也可以在表单定义中修改部件:

class CommentForm(forms.Form):
    name = forms.CharField()
    url = forms.URLField()
    comment = forms.CharField()

    name.widget.attrs.update({'class': 'special'})
    comment.widget.attrs.update(size='40')

或者如果该字段没有直接在表单上声明(比如模型表单字段),可以使用Form.fields属性:

class CommentForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['name'].widget.attrs.update({'class': 'special'})
        self.fields['comment'].widget.attrs.update(size='40')

Django会将这些属性包含在渲染的输出中:

>>> f = CommentForm(auto_id=False)
>>> f.as_table()
<tr><th>Name:</th><td><input type="text" name="name" class="special" required></td></tr>
<tr><th>Url:</th><td><input type="url" name="url" required></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" size="40" required></td></tr>

表单的校验

django中的表单除了渲染html外,还有一个很重要的作用就是校验数据。

看添加学生的视图案例:

class StudentCreateView(View):
    """
    学生添加视图
    """

    def post(self, request):
        """添加学生"""
        form = StudentForm(request.POST)
        if form.is_valid():
            obj = Student.objects.create(**form.cleaned_data)
            return redirect(reverse('student-list'))
        return render(request, 'crm/student_detail.html', context={'form': form})

实例化表单时,可以将GET/POST参数传入,然后调用表单对象的is_valid()方法进行校验。如果校验通过,这个方法会返回True,否则返回False。

校验通过后通过cleaned_data属性访问干净的数据。

指定字段校验

定义表单时,可以定义方法clean_()方法对指定的字段进行校验,该方法不接受参数。在方法中通过self.cleaned_data获取该字段的值。

如果校验不通过需要触发一个ValidationError的异常,校验通过请return该值。

在学生创建的逻辑中,我们没有验证电话号码的格式,在表单中编写一个校验方法如下:

import re

from django import forms
from django.core.exceptions import ValidationError

from .models import Channel, Student

class StudentForm(forms.ModelForm):
    class Meta:
        model = Student  # 指定要生成表单的模型
        exclude = ['c_time']    # 指定不需要生成的字段

    def clean_phone(self):
        phone = self.cleaned_data.get('phone')
        if phone is not None:
            if not re.match(r'1[3-9]\d{9}$', phone):
                raise ValidationError('手机号码格式不正确!')
        return phone
验证相互依赖的字段

有时候需要同时校验多个字段,比如注册时,校验密码和重复密码。这时复写clean()方法是一个很好的办法:

from django import forms
from django.core.exceptions import ValidationError

class RegistorForm(forms.Form):
    # Everything as before.
    
    def clean(self):
        cleaned_data = super().clean()
		password = cleaned_data.get('password')
        password_confirm = cleaned_data.get('password_confirm')
        if not password == password_confirm:
            raise ValidationError('输入的密码不一致!')
        return cleaned_data

在表单的clean()方法被调用前,上一节中的单字段校验方法都会先被运行。clean()方法中如果出现验证错误,在模板中使用{{form.non_field_errors}}显示。

模型表单

django提供了一个辅助类,可以从一个模型创建一个Form类,而不需要重复定义字段。

修改学生表单如下:

from django import forms
from .models import Channel, Student


class StudentForm(forms.ModelForm):
    class Meta:
        model = Student  # 指定要生成表单的模型
        exclude = ['c_time']    # 指定不需要生成的字段

每一个模型表单都是forms.ModelForm的一个子类,和普通表单不同。由于模型已经定义了字段,在模型表单中,只需要在Meta类中指定模型和字段。

字段可以通过属性fields=[‘field1’, ‘field2’, …]指定需要的字段,fields='all’表示生成所有的字段, 也可以通过exclude = [‘field1’, ‘field2’, …]排除字段。

save()

模型表单与普通的表单还有一个不同就是save()方法。在校验过的表单实例上调用save()方法,会自动调用对应的模型在数据库中创建数据或修改数据。

学生添加案例:

class StudentCreateView(View):
    """
    学生添加视图
    """

    def post(self, request):
        """添加学生"""
        # 实例化表单
        form = StudentForm(request.POST)
        # 校验
        if form.is_valid():
            # 保存数据
            form.save()
            return redirect(reverse('student-list'))
        return render(request, 'crm/student_detail.html', context={'form': form})

上面的代码中,如果表单校验通过,执行form.save()会创建返回Student实例并保存到数据库。

学生更新案例:

class StudentUpdateView(View):
    """
    学生更新视图
    """

    def get_obj(self, pk):
        obj = get_object_or_404(Student, pk=pk)
        return obj

    def get(self, request, pk):
        # 1. 获取修改对象
        obj = self.get_obj(pk)
        # 2. 实例化表单对象,并填充模型对象
        form = StudentForm(instance=obj)
        # 2. 渲染并返回修改页面
        return render(request, 'crm/student_detail.html', context={'form': form})

    def post(self, request, pk):
        # 1. 获取修改对象
        obj = self.get_obj(pk)
        # 2. 实例化表单对象,填充前端传递的数据和模型对象
        form = StudentForm(request.POST, instance=obj)
        # 3. 校验
        if form.is_valid():
            form.save()     # 保存更新
            return redirect(reverse('student-list'))
        return render(request, 'crm/student_detail.html', context={'form': form})

上面的代码中实例化表单时传递POST参数,同时把要更新的模型对象传给instance参数,在校验通过后,执行form.save()会使用校验后的参数更新模型对象。

学生创建,更新视图案例

表单
# crm/froms.py
import re

from django import forms
from django.core.exceptions import ValidationError

from .models import Student


class StudentForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for name in self.fields:
            self.fields[name].widget.attrs.update({'class': 'form-control'})

    class Meta:
        model = Student  # 指定要生成表单的模型
        exclude = ['c_time']    # 指定不需要生成的字段

    def clean_phone(self):
        phone = self.cleaned_data.get('phone')
        if not re.match(r'1[3-9]\d{9}$', phone):
            raise ValidationError('手机号码格式不正确!')
        return phone
视图
# crm/views.py
class StudentCreateView(View):
    """
    学生添加视图
    """
    def get(self, request):
        """学生添加页面"""
        # 1. 获取渠道对象
        channels = Channel.objects.all()
        form = StudentForm()
        return render(request, 'crm/student_detail.html', context={'channels': channels, 'form': form})

    def post(self, request):
        """添加学生"""
        form = StudentForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect(reverse('student-list'))
        return render(request, 'crm/student_detail.html', context={'form': form})


class StudentUpdateView(View):
    """
    学生更新视图
    """

    def get_obj(self, pk):
        obj = get_object_or_404(Student, pk=pk)
        return obj

    def get(self, request, pk):
        # 1. 获取修改对象
        obj = self.get_obj(pk)
        # 2. 实例化表单对象,并填充模型对象
        form = StudentForm(instance=obj)
        # 2. 渲染并返回修改页面
        return render(request, 'crm/student_detail.html', context={'form': form})

    def post(self, request, pk):
        # 1. 获取修改对象
        obj = self.get_obj(pk)
        # 2. 实例化表单对象,填充前端传递的数据和模型对象
        form = StudentForm(request.POST, instance=obj)
        # 3. 校验
        if form.is_valid():
            form.save()     # 保存更新
            return redirect(reverse('student-list'))
        return render(request, 'crm/student_detail.html', context={'form': form})
模板
<!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">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>{% if obj %}修改{% else %}添加{% endif %}学生</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
          integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">

    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
    <![endif]-->
</head>
<body>
<div class="container">
    <div style="width: 800px">
        <h1>学生{% if obj %}修改{% else %}添加{% endif %}页面</h1>
        <form class="form-horizontal" method="post">
            {% for field in form %}
                <div class="form-group {% if field.errors %}has-error{% endif %}">
                    <label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label>
                    <div class="col-sm-10">
                        {{ field }}
                        {% for error in field.errors %}
                            <span class="help-block">{{ error }}</span>
                        {% endfor %}
                    </div>
                </div>
            {% endfor %}

            <div class="form-group">
                <div class="col-sm-offset-2 col-sm-10">
                    <button type="submit" class="btn btn-default">{% if form.instance %}修改{% else %}
                        添加{% endif %}</button>
                </div>
            </div>
        </form>
    </div>
</div>


<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"
        integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"
        crossorigin="anonymous"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"
        integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd"
        crossorigin="anonymous"></script>
</body>
</html>

RESTful API

什么是RESTful

REST一词,是Roy Thomas Fielding (opens new window)在他2000年的博士论文 (opens new window)中提出的。

Fielding是一个非常重要的人,他是HTTP协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席。所以,他的这篇论文一经发表,就引起了关注,并且立即对互联网开发产生了深远的影响。

在这篇论文中,Fielding将他对互联网软件的架构原则,定名为REST,即Representational State Transfer的缩写,翻译为表述性状态转移

如果一个架构符合REST原则,就称它为RESTful架构。

要理解RESTful,最好的办法就是理解Representational State Transfer这个词组的意思

资源(Resources)

资源,就是网络上的一个实体,它是一个具体的信息,可以是一段文本,一张图片,一首歌,一个种服务。可以通过一个URI指向它。要获取这个资源,就可以通过它的URI访问它,每一个资源的URI都是独一无二的标识符。

REST的作用对象就是资源,也即是资源的表述性状态转移

表现层(Representation)

资源是一种信息实体,它可以有多种的表现形式。资源的具体呈现形式,叫做它的表现层。

比如,文本可以是txt格式表现,也可以是HTML格式,XML格式,JSON格式表现等;图片可以是JPG格式表现,也可以是PNG格式表现

状态转移

访问一个资源,是一个客户端通过HTTP协议与服务端的互动过程。在这个过程中,势必涉及数据和状态的变化。客户端通过不同的HTTP请求,操作资源,让其发生状态(创建,更新,删除等)的改变。

HTTP协议里,有四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种操作:GET获取资源,POST创建资源,PUT更新资源,DELETE删除资源。

总结

综上,我们总结一下什么是RESTful架构:

每个URI代表一种资源
客户端和服务端之间,传递这种资源的某种表现形式
客户端通过HTTP方法,对服务器资源进行操作,实现状态的变化

RESTful API

网络应用程序,分为客户端和服务端两部分,或者前端后后端。当前的发展趋势,就是前端设备层出不穷(个人电脑,手机,平板,各种移动设备…)。

因此,必须有一种统一的机制,方便不同的客户端与后端进行通信。所以导致了API架构的流行,RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。

不同的开发人员对理论的理解以及根据实际项目的不同并没有形成一套非常明确的标准,下面列出的是得到普遍公认和行之有效的设计。

版本

应该将API的版本号放入URL.

http://api.example.com/1.0/
路径

路径又称"终点"(endpoint),表示API的具体网址。

在RESTful架构中,每个网址代表一种资源,所以网址中不应该有动词,只能有名词,而且所有的名词往往与数据库表的表名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。

举例来说,一个crm系统的api,包含学生和课程信息,则它的路径应该设计成下面这样

http://api.example.com/1.0/students/
http://api.example.com/1.0/courses/
http动词

对于资源的具体操作类型,由http动词表示。

常用的http动词有下面五个(括号里是对应的SQL命令)。

GET(SELECT):从服务器取出资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性局部更新)。
DELETE(DELETE):从服务器删除资源。

还有两个不常用的HTTP动词。

HEAD:获取资源的元数据。
OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

看下面的例子:

GET /students:列出所有学生
POST /students: 新建一个学生
GET /students/pk: 获取某个学生的信息
PUT /students/pk: 更新某个学生的全部信息
PATCH /students/pk: 更新某个学生的部分信息
DELET /students/pk: 删除某个学生
过滤信息

如果记录数量很多,服务器不可能一次将它们全部返回给用户。API应该提供参数,过滤返回结果。

下面是场景的参数。

?limit=10:指定返回记录的数量
?offset=10:指定返回记录的开始位置。
?page=2&page_size=100:指定第几页,以及每页的记录数。
?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
?sex=1:指定筛选条件
状态码

服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。

200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

项目实战二

需求

以前后端分离的方式实现学生的增删改查操作

学生列表接口

url:/students/
请求方法:get
参数:

  • 格式:查询参数
参数名类型是否必传说明
pageint页码,默认为1
sizeinit每页数据条数默认为10
namestr根据姓名过滤
ageint根据年龄过滤
sexint根据性别过滤
phonestr根据手机过滤
channelint根据渠道过滤

响应:

  • 状态码:200
  • 格式:json
  • 响应示例:
{
    "total": 7,
    "page": 1,
    "next_page": null,
    "pre_page": null,
    "results": [
        {
            "id": 8,
            "name": "yaya",
            "age": 18,
            "sex": 1,
            "phone": null,
            "channel": "",
            "c_time": "2022-09-01T06:30:48.417Z"
        },
        {
            "id": 7,
            "name": "yaya",
            "age": 18,
            "sex": 1,
            "phone": null,
            "channel": "",
            "c_time": "2022-09-01T06:10:41.869Z"
        },
        {
            "id": 6,
            "name": "心蓝",
            "age": 20,
            "sex": 1,
            "phone": "15873061798",
            "channel": "",
            "c_time": "2022-08-31T12:21:04.068Z"
        },
        {
            "id": 5,
            "name": "小简",
            "age": 16,
            "sex": 0,
            "phone": null,
            "channel": "抖音",
            "c_time": "2022-08-23T13:10:05.317Z"
        },
        {
            "id": 3,
            "name": "张柏芝",
            "age": null,
            "sex": 1,
            "phone": null,
            "channel": "",
            "c_time": "2022-08-23T08:00:15.165Z"
        },
        {
            "id": 2,
            "name": "刘德华",
            "age": null,
            "sex": 1,
            "phone": null,
            "channel": "",
            "c_time": "2022-08-23T08:00:08.035Z"
        },
        {
            "id": 1,
            "name": "心蓝",
            "age": null,
            "sex": 1,
            "phone": null,
            "channel": "",
            "c_time": "2022-08-23T07:59:29.417Z"
        }
    ]
}

学生详情接口

url:/students/pk/
请求方法:get
参数:

  • 格式:路径参数

响应数据:

  • 状态码:200
  • 格式:json
  • 响应实例:
{
    "id": 1,
    "name": "心蓝",
    "age": null,
    "sex": 1,
    "phone": null,
    "channel": "",
    "c_time": "2022-08-23T07:59:29.417Z"
}

学生添加接口

url:/students/
请求方法:post
参数:

  • 格式:json
参数名类型是否必传说明
namestr姓名
ageint年龄
sexint性别
phonestr手机
channelint渠道id
  • 请求实例:
{
    "name": "刘德华",
    "age": 60,
    "sex": 1,
    "phone": '13888888888',
    "channel": 1
},

响应:

  • 状态码:201
  • 格式:json
  • 响应示例:
{
    "id": 8,
    "name": "yaya",
    "age": 18,
    "sex": 1,
    "phone": null,
    "channel": "",
    "c_time": "2022-09-01T06:10:41.869Z"
}

学生修改接口

url:/students/pk/
请求方法:put/patch
参数:

  • 格式:json,路径
参数名类型是否必传说明
namestr姓名
ageint年龄
sexint性别
phonestr手机
channelint渠道id
  • 请求实例:
{
    "name": "刘德华",
    "age": 60,
    "sex": 1,
    "phone": '13888888888',
    "channel": 1
},

响应:

  • 状态码:200
  • 格式:json
  • 响应示例:
{
    "id": 8,
    "name": "yaya",
    "age": 18,
    "sex": 1,
    "phone": null,
    "channel": "",
    "c_time": "2022-09-01T06:10:41.869Z"
}

学生删除接口

url:/students/pk/
请求方法:delete
参数:

  • 格式:路径

响应:

  • 状态码:204
  • 格式:无响应内容

后端代码

视图
class StudentView(View):

    def serialize(self, item):

        return {
                'id': item.id,
                'name': item.name,
                'age': item.age,
                'sex': item.sex,
                'phone': item.phone,
                'channel': item.channel.title if item.channel else '',
                'c_time': item.c_time
            }

    def get(self, request, pk=None):
        if pk is not None:
            #  查看学生详情
            obj = self.get_obj(pk)
            data = self.serialize(obj)
            return JsonResponse(data)

        # 1. 获取查询参数
        query_params = {key: value for key, value in request.GET.items()}
        # 2. 获取分页参数
        page = int(query_params.pop('page', 1))
        size = int(query_params.pop('size', 10))
        # 3. 获取查询集
        queryset = Student.objects.all()
        for key, value in query_params.items():
            try:
                queryset = queryset.filter(**{key: value})
            except:
                pass
        # 4. 分页处理
        # 数据总条数
        total_num = queryset.count()
        # 总页数
        total_page = math.ceil(total_num / size)
        # 下一页
        absolute_url = self.request.build_absolute_uri()
        next_url = None
        pre_url = None
        if page < total_page:

            if 'page' in absolute_url:
                next_url = re.sub(r'page=\d*', 'page={}'.format(page+1), absolute_url)
            else:
                next_url = absolute_url + '&page={}'.format(page+1)
        # 上一页
        if page > 1:

            if 'page' in absolute_url:
                pre_url = re.sub(r'page=\d*', 'page={}'.format(page-1), absolute_url)
            else:
                pre_url = absolute_url + '&page={}'.format(page-1)
        # 分页过滤
        queryset = queryset[(page - 1) * size:page * size]
        # 5.序列化
        students = [
            self.serialize(item) for item in queryset
        ]
        data = {
            'total': total_num,
            'page': page,
            'next_page': next_url,
            'pre_page': pre_url,
            'results': students
        }
        # 6. 返回响应
        return JsonResponse(data)

    def post(self, request):
        # 1.接受参数
        create_data = json.loads(request.body)
        # 2.实例化表达
        form = StudentForm(create_data)
        # 3.校验
        if form.is_valid():
            instance = form.save()
            # 4.序列化
            data = self.serialize(instance)
            return JsonResponse(data, status=201)
        else:
            # 5.错误信息
            data = {'errors': form.errors}
            return JsonResponse(data, status=400)

    def get_obj(self, pk):
        obj = get_object_or_404(Student, pk=pk)
        return obj

    def put(self, request, pk):
        # 1. 获取对象
        obj = self.get_obj(pk)
        # 2. 接收参数
        update_data = json.loads(request.body)
        # 2. 实例化表单
        form = StudentForm(update_data, instance=obj)
        # 3. 校验
        if form.is_valid():
            instance = form.save()
            # 4. 序列化
            data = self.serialize(instance)
            return JsonResponse(data, status=200)
        else:
            # 5.错误信息
            data = {'errors': form.errors}
            return JsonResponse(data, status=400)

    def delete(self, request, pk):
        # 1. 获取对象
        obj = self.get_obj(pk)
        # 2. 删除对象
        try:
            obj.delete()
            return HttpResponse(status=204)
        except Exception as e:
            return JsonResponse(data={'errors': str(e)}, status=400)
路由
path('students/', views.StudentView.as_view(), name='student-list-create'),
    path('students/<int:pk>/', views.StudentView.as_view(), name='student-retrieve-update-delete')

前端代码

列表页面
<!-- student_list_single.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">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>Bootstrap 101 Template</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
          integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">

    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
    <script src="https://fastly.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
    <script src="https://fastly.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
    <![endif]-->
    <style>
        [v-cloak] {
            display: none;
        }
    </style>
</head>
<body>
<div class="container" style="width: 1000px" id="app">
    <h1>学生列表</h1>
    <a class="btn btn-success" style="float: right" href="./student_detail_single.html">添加</a>
    <table class="table table-hover table-bordered table-condensed" v-cloak>
        <thead>
        <tr>
            <th>序号</th>
            <th>姓名</th>
            <th>性别</th>
            <th>年龄</th>
            <th>phone</th>
            <th>渠道</th>
            <th>创建时间</th>
            <th>操作</th>
        </tr>
        </thead>
        <tbody>
        <tr v-for="(stu, index) in students">
            <td>{{ index }}</td>
            <td>{{ stu.name }}</td>
            <td>{{ stu.sex }}</td>
            <td>{{ stu.age }}</td>
            <td>{{ stu.phone }}</td>
            <td>{{ stu.channel }}</td>
            <td>{{ stu.c_time }}</td>
            <td style="text-align: center;padding: 0.75em 0"><a :href="'./student_detail_single.html?id='+stu.id"
                                                                class="btn btn-primary btn-sm">编辑</a>&nbsp;
                <button class="btn btn-danger btn-sm" @click="delStudent(stu.id, index)">删除</button>
            </td>
        </tr>
        </tbody>
    </table>
    <nav aria-label="...">
        <ul class="pager">
            <li class="previous" :class="{ disabled: !pre_url }"><a
                    href="{{ students.pre_url }}"><span
                    aria-hidden="true">&larr;</span> 上一页</a></li>
            <li class="next" :class="{ disabled: !next_url }"><a
                    href="{{ students.next_url }}">下一页 <span
                    aria-hidden="true">&rarr;</span></a>
            </li>
        </ul>
    </nav>
</div>


<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://fastly.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"
        integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"
        crossorigin="anonymous"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"
        integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd"
        crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    let baseUrl = 'http://127.0.0.1:8000'
    const app = new Vue({
        el: '#app',
        data: {
            students: [],
            next_url: null,
            pre_url: null,
        },
        computed: {

        },
        methods: {
            delStudent(sid, index) {
                axios.delete(baseUrl + '/crm/students/' + sid + '/').then(res => {
                    this.students.splice(index, 1)
                })
            }
        },
        created() {
            axios.get(baseUrl + '/crm/students/').then(res => {
                this.students = res.data.results
                this.next_url = res.data.next_url
                this.pre_url = res.data.pre_url
            })
        }
    })
</script>
</body>
</html>
详情页
<!-- student_detail_single.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">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>Bootstrap 101 Template</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
          integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">

    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
    <script src="https://fastly.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
    <script src="https://fastly.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
    <![endif]-->
    <style>
        [v-cloak]{
              display: none;
          }
    </style>
</head>
<body>
<div class="container" style="width: 800px" id="app" v-cloak>
    <h1>{{ title }}</h1>
    <form class="form-horizontal">
        <div class="form-group">
            <label for="name" class="col-sm-2 control-label">姓名</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="name" name="name" v-model="stu.name" placeholder="姓名">
            </div>
        </div>
        <div class="form-group">
            <label for="sex" class="col-sm-2 control-label">性别</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="sex" name="sex" v-model="stu.sex" placeholder="性别">
            </div>
        </div>
        <div class="form-group">
            <label for="age" class="col-sm-2 control-label">年龄</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="age" name="age" v-model="stu.age" placeholder="年龄">
            </div>
        </div>
        <div class="form-group">
            <label for="qq" class="col-sm-2 control-label">qq</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="qq" name="qq" v-model="stu.qq" placeholder="qq">
            </div>
        </div>
        <div class="form-group">
            <label for="phone" class="col-sm-2 control-label">手机号码</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="phone" name="phone" v-model="stu.phone"
                       placeholder="手机号码">
            </div>
        </div>
        <div class="form-group">
            <label for="channel" class="col-sm-2 control-label">渠道</label>
            <div class="col-sm-10">
                <select name="channel" id="channel" class="form-control" v-model="stu.channel">
                    <option value="">--------</option>
                    <option v-for="channel in channels" :value="channel.id">{{ channel.name }}</option>
                </select>
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-offset-2 col-sm-10">
                <button class="btn btn-info float-right"
                        @click.prevent="btnSubmit(stu)">{{ btnName }}</button>
            </div>
        </div>


    </form>
</div>

<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://fastly.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"
        integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"
        crossorigin="anonymous"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"
        integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd"
        crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    let baseUrl = 'http://127.0.0.1:8000'
    const app = new Vue({
        el: '#app',
        data: {
            title: '',
            stu: {},
            channels: [{name: '百度', id: 1}, {name: '抖音', id: 2}],
            btnName: ''
        },
        methods: {
            delStudent(sid, index) {
                axios.delete(baseUrl + '/crm/students/' + sid + '/' ).then(res => {
                    this.students.splice(index, 1)
                })
            },
            getParams() {
                let query = window.location.search.substring(1)
                let vars = query.split("&");
                let data = {}
                for (let v of vars) {
                    v = v.split('=');
                    data[v[0]] = v[1]
                }
                return data

            },
            saveStu(sid, stu){
                axios.put(baseUrl + '/crm/students/' + sid + '/' , stu).then(res=>{
                    location.href = 'student_list_single.html'
                })
            },
            createStu(stu){
                axios.post( baseUrl + '/crm/students/', stu).then(res=>{
                    location.href = 'student_list_single.html'
                })
            },
            btnSubmit(stu){
                let sid = this.getParams().id;
                if (sid){
                    this.saveStu(sid, stu)
                }else {
                    this.createStu(stu)
                }
            }

        },
        created() {
            let sid = this.getParams().id;
            if (sid) {
                this.title = '学生详情';
                this.btnName = '保存';
                axios.get(baseUrl + '/crm/students/' + sid + '/' ).then(res => {
                    this.stu = res.data
                })
            } else {
                this.title = '添加学生';
                this.btnName = '添加'
            }

        }
    })
</script>
</body>
</html>

CORS

同源策略

同源策略是一个重要的安全策略,它是浏览器最核心最基本的安全功能。
它限制web应用程序只能从加载应用程序的同一个域请求HTTP资源。
当向不同的域请求HTTP资源时就发生了跨域,默认请情况下浏览器会阻止跨域的请求。
那如何判断是否同源呢?
如果两个URL的协议,端口和主机都相同的话,则这两个URL是同源。
例如以下所有资源都具有相同的来源:

http://example.com/
http://example.com:80/
http://example.com/path/file

每个url都有相同的协议,主机和端口号。
而以下每个资源都与其他不同源:

http://example.com/
 http://example.com:8080/
 http://www.example.com/
 https://example.com:80/
 https://example.com/
 http://example.org/
 http://ietf.org/

所以所谓的同源策略简单的理解就是,打开某个页面后,这个页面上的ajax请求默认只能向和页面同源的url发送http请求。
同源策略固然保证了安全,但同时也限制了应用的灵活性,所以出现了CORS

什么是CORS

CORS是一个W3C标准,全称是"跨域资源共享"(cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于前端开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信

CORS原理
跨域请求

浏览器将跨域请求分为两类:简单请求和非简单请求。

只要同时满足一下两个条件,就属于简单请求:

1、请求方法是一下三种方法之一:

  • head
  • get
  • post

2、http请求的头信息不超出以下几种字段:

  • accept
  • accept-language
  • content-language
  • Last-Event-ID
  • Content-Type的值仅限于下列三者之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理是不一样的

简单请求CORS步骤

对于简单请求CORS的基本流程如下:

第一步:客户端(浏览器)请求

当浏览器发出跨域请求时,该浏览器会添加一个包含当前源(协议,主机和端口)的Origin头。
在这里插入图片描述

第二步:服务器响应

在服务器,当服务器看到Origin头并想要允许访问时,就需要在响应中加入一个Access-Control-Allow-Origin响应头来指定请求源(例如加入*表示允许任何源)

在这里插入图片描述

第三步:浏览器接受响应

当浏览器看到带有相应Access-Control-Allow-Origin响应头的响应时,即允许与客户端网站共享响应数据。否则抛出CORS异常。

注意同源策略只是浏览器遵守的规则,使用别的工具进行请求不会遵循同源策略的影响。

复杂请求CORS步骤
第一步:发送预检请求

浏览器会根据需要创建预检请求。该请求是一个options请求,会在实际请求消息之前被发送。
在这里插入图片描述
预检请求中关键请求头是origin表示请求来自哪个源。除了origin字段,预检请求头的信息还包含两个特殊字段

1、access-control-request-method
该字段是必须的,用来列出接下来的CORS请求会用到哪些HTTP方法,上面图片中的是PATCH
2、access-control-request-headers
这个字段是一个逗号分隔的字符串,指定接下来的CORS请求还会携带哪些额外的字段,上面图片中的是content-type

第二步:响应预检请求

服务器收到预检请求后,检查origin,access-control-request-method,access-control-request-headers字段后,就可以返回响应
在这里插入图片描述
响应中的access-control-allow-origin字段表示允许跨域的源,*表示允许任意跨域请求。其他CORS相关响应头如下:
1、Access-Control-Allow-Methods
逗号分隔的一个字符串,表明服务器允许的跨域请求方法
2、Access-Control-Allow-Headers
逗号分隔的一个字符串,表明服务器支持的头字段
3、Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上图中的有效期是一天(86400秒),在此期间不用发出另一条预检请求。
注意:如果服务器否定了预检请求,也会返回一个正常的HTTP响应,但是不包含任何CORS相关的响应头

发送跨域请求

一旦服务器通过了预检请求,以后每次浏览器正常的CORS请求,就跟简单请求一样,会有一个origin头字段。服务器的回应,也会有一个access-control-allow-origin头信息字段。
在这里插入图片描述

cookie跨域

出于隐私原因,CORS请求默认不带cookie。如果想要在使用CORS时发送cookie,就需要发送请求时携带cookie并且服务器也同意。

请求

ajax请求需要打开withCredentials属性才可以携带cookie:

const request = axios.create({
    baseURL: 'http://127.0.0.1:8000',
    timeout: 5000,
    withCredentials: true // 设置为true 跨域时会携带cookie
})
响应

如果要接受cookie跨域,access-control-allow-origin就不能设置为星号,必须指定明确,并且响应头中必须包含字段Access-Control-Allow-Credentials,值为true。

同时cookie依然遵循同源策略,只有服务器指明的域名的cookie才会上传。

在这里插入图片描述

django-cors-headers

在django项目中要实现CORS可以手写(重复造轮子),也可以使用成熟插件(推荐)。

django-cors-headers是一个处理跨域资源共享(CORS)所需服务器器头信息的django应用。

它的使用非常简单

安装
pip install django-cors-headers
添加到apps
INSTALLED_APPS = [
 ...,
 "corsheaders",
 ...,
]
设置中间件

Django-cors-headers是通过中间件实现cors头设置的,所以需要设置对应的中间件

MIDDLEWARE = [
 ...,
 "corsheaders.middleware.CorsMiddleware",
 "django.middleware.common.CommonMiddleware",
 ...,
]

CorsMiddleware应该放在尽可能高的位置,特别是在任何可以生成响应的中间件之前,比如django的CommonMiddleware。否则无法将CORS头添加到这些响应中。

配置

要使用CORS,还需要在settings.py模块中添加如下配置:

# CORS设置
# 允许跨域的域名列表
CORS_ALLOWED_ORIGINS = [
 'http://localhost:8080'
]
CORS_ALLOW_ALL_ORIGINS = True # 表示y
# 允许cookies跨域
CORS_ALLOW_CREDENTIALS = True

djang oadmin

安装django-admin

为数据进行增删改查是一个枯燥,有没有创造性的工作。django提供了一个内置的app,我们只需要做简单的配置就可以自动生成一个功能强大的管理后台。

要使用django admin非常简单:

1、首先你需要在INSTALL_APPS中注册django.contrib.admin,然后它的依赖,
在这里插入图片描述
2、模板设置
在这里插入图片描述
3、如果你自定义了MIDDLEWARE,必须包含

  • django.contrib.auth.middleware.AuthenticationMiddleware
  • django.contrib.messages.middleware.MessageMiddleware
    4、在根路由中配置admin
path('admin/', admin.site.urls),

当你做完这些,你就可以通过’/admin’(默认)访问admin站点。

在这里插入图片描述
默认情况下,创建的项目会自动安装admin,不需要做任何操作即可使用django-admin

创建管理员用户

要使用django-admin,你还需要创建一个用户来登录站点。通过python manage.py createsuperuser命令
在这里插入图片描述

管理模型

接下来,我们就需要为我们要管理的模型定义ModelAdmin。

app里有一个admin.py,在这个文件中我们注册我们想要管理的模型,代码如下:

from django.contrib import admin

# Register your models here.
from .models import Student, StudentDetail

admin.site.register(Student)
admin.site.register(StudentDetail)

然后访问admin站点,通过用刚才创建的超级管理员账号登录,你会看到如下页面
在这里插入图片描述
接下来我们就可以对这些模型的数据进行增删改查

自定义设置

django admin 的强大之处在于,可以通过简单的设置,可以定义管理页面的显示方式。

如果我们要自定义设置,我们需要在admin.py中定义admin.ModeAdmin的子类:

class StudentAdmin(admin.ModelAdmin):
    list_display = ['id', 'name', 'sex', 'qq', 'phone']
    list_display_links = ['name']
    list_filter = ['sex']
    search_fields = ['name', 'qq', 'name', 'c_time']
    list_per_page = 1

    # fields = ['sex', 'name']
    fieldsets = [
        (None, {'fields': ['name', 'sex']}),
        ('详细信息', {'fields': ['qq', 'phone', 'grade']}),
        ('设置', {'fields': ['is_delete']})

    ]


admin.site.register(Student, StudentAdmin)
admin.site.register(StudentDetail)

列表页属性:

  • list_display:显示字段,可以点击列头进行排序
  • list_filter:过滤字段,过滤框会出现在右侧
  • search_fields:搜索字段,搜索框会出现在上侧
  • list_per_page:分页,分页框会出现在下侧
  • list_display_links: 定义点击哪些字段可以调到修改增加页面

添加、修改页属性:

  • fields:属性的先后顺序
  • fieldsets:属性分组

注意:上面两个属性,二者选一。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值