Django 后端学习路线

推荐从上往下看。

Django 官方教程 关键步骤

本小节记录了 官方中文教程(3.1 版本) 中的关键步骤。

1. 创建项目、项目和一个视图

  • 安装:pip install Django
  • 验证安装:python -m django --version
  • 创建并初始化项目文件夹:django-admin startproject <projectname>
  • 即时预览:在 <projectname> 目录下 python manage.py runserver [port]
  • 创建应用:python manage.py startapp <appname>
  • 编写视图:
1
2
3
4
5
# polls/views.py
from django.http import HttpResponse

def index(request):
return HttpResponse("Hello, world. You're at the polls index.")
  • 在应用中添加写好的视图:
1
2
3
4
5
6
7
# polls/urls.py
from django.urls import path
from . import views

urlpatterns = [
path('', views.index, name='index'),
]
  • 在站点中添加应用的视图:
1
2
3
4
5
6
7
8
# mysite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
path('polls/', include('polls.urls')),
path('admin/', admin.site.urls),
]

2. 数据库使用、管理员

2.1 配置数据库

  • 安装 mysqlclient:pip install mysqlclient
  • 修改项目配置文件:
1
2
3
4
5
6
7
8
9
10
11
12
# mysite/settings.py

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'django_learn',
'USER': 'root',
'PASSWORD': 'yourpassword',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}

2.2 创建模型并迁移至数据库

一个 Django 模型对于一个 SQL 数据表。

  • 创建模型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# polls/models.py
from django.db import models


class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
def __str__(self):
return self.question_text


class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
def __str__(self):
return self.choice_text
  • 激活模型:
1
2
3
4
# mysite/settings.py
INSTALLED_APPS = [
'polls.apps.PollsConfig' # 添加这一项
]
  • 将模型更改写入数据库:
    • 根据类的更改,生成一个 迁移(一个存储在 <app_lable>/migrations 下的 py 文件,存储了变化):python manage.py makemigrations [app_label]文档
    • 将一个 迁移 应用到数据库,并迁移数据:python manage.py migrate [app_label] [migration_name]文档
    • 查看一个 迁移 将对数据库造成的影响:python manage.py sqlmigrate <app_label> <migration_name>文档
    • 一般来说,类变更以后,需要:python manage.py makemigrations && python manage.py migrate
    • 第一次部署的时候,需要 python manage.py makemigrations <app1> <app2> <...appn> && python manage.py migrate

对了,migrations 文件夹应当加入 .gitignore,否则不同开发者的 migrations 就要冲突啦。

2.3 数据库 API

  • 进入 Python 命令行:python manage.py shell
  • 使用前先引入类:from polls.models import Choice, Question

对于一个数据表:

  • 一个表的所有元素:Question.objects.all()
  • 以成员筛选记录:Question.objects.filter(id=1)
  • pub_date 成员的 year 成员筛选(成员方法同理):pub_date.yearQuestion.objects.filter(pub_date__year)

对于一个记录:

  • 构造一个新记录:q = Question(question_text="What's new?", pub_date=timezone.now())
  • 将记录插入表:q.save()
  • 查询、修改记录的属性(同理可调用其方法):q.question_text
  • 删除一个记录:q.delete()

对于一个外键(Choice 存在外键,为 Question):

  • 查询一个 Choice 对应的 Question:c.qeustion
  • 查询一个 Question 对应的 Choice:q.choice_set.all()
  • 为 Question 创建一个 Choice:q.choice_set.create(choice_text='Not much', votes=0)

2.4 管理员相关

  • 创建管理员:python manage.py createsuperuser
  • 管理员登录界面:http://127.0.0.1:8000/admin/
  • 在管理员页面中添加 Question 模型:
1
2
3
4
5
6
# polls/admin.py
from django.contrib import admin

from .models import Question

admin.site.register(Question)

3. 视图和 urls

3.1 添加更多视图,并用参数匹配 url

1
2
3
4
# /polls/views.py
def detail(request, question_id: int):
return HttpResponse("You're looking at question %s." % question_id)
# 这里可以做更多的事情,比如调用其他 Python 包
1
2
3
4
# /polls/url.py
urlpatterns = [
path('<int:question_id>/', views.detail, name='detail'),
]

访问 /polls/34 会返回 You're looking at question 34.

3.2 使用 HTML 模板

编写一个 HTML 模板:

1
2
3
4
5
6
7
8
9
10
<!-- /polls/templates/polls/index.html -->
{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}

再在视图中:加载模板、用数据渲染、然后转为 HTTP Response,三步使用 django.shortcuts.render() 完成

1
2
3
4
5
6
from django.shortcuts import render

def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'latest_question_list': latest_question_list}
return render(request, 'polls/index.html', context)

3.3 抛出 404 错误码

1
2
3
4
5
6
7
8
from django.http import Http404

def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, 'polls/detail.html', {'question': question})

也可以使用 django.shortcuts.get_object_or_404。该函数在 object 不存在会 raise Http404()

1
2
3
4
5
from django.shortcuts import get_object_or_404, render

def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})

也有 get_list_or_404() 函数,工作原理和 get_object_or_404() 一样,除了 get() 函数被换成了 filter() 函数。如果列表为空的话会抛出 Http404 异常。

3.4 使用 name 替代 URL 中的硬编码、为 URL 名称添加命名空间(app_name)

https://docs.djangoproject.com/zh-hans/3.1/intro/tutorial03/#removing-hardcoded-urls-in-templates

https://docs.djangoproject.com/zh-hans/3.1/intro/tutorial03/#namespacing-url-names

4. 编写一个简单的表单

因为我想用 Django 做纯 REST 后端,所以这部分略。

https://docs.djangoproject.com/zh-hans/3.1/intro/tutorial04/

5. 测试

https://docs.djangoproject.com/zh-hans/3.1/intro/tutorial05/

关于测试还是值得单独拿一个章节出来的:测试

6. 插入静态文件

https://docs.djangoproject.com/zh-hans/3.1/intro/tutorial06/

7. 修改 Admin 页面

https://docs.djangoproject.com/zh-hans/3.1/intro/tutorial07/

如果想要修改某元素对应外键的信息(而不是修改其外键),可以参考 django.contrib.admin.StackedInline

如果想要汉化 Admin 页面,可以参考:https://blog.csdn.net/aaazz47/article/details/78666099

Django 用户认证

Django 用户认证(后端篇)

MDN 教程:https://developer.mozilla.org/zh-CN/docs/Learn/Server-side/Django/Authentication
文档:https://docs.djangoproject.com/zh-hans/3.1/topics/auth/default/

  • 创建用户:user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword')
  • 创建超级用户:在命令行中 python manage.py createsuperuser
  • 登录:
1
2
3
4
5
6
7
8
9
10
11
12
13
from django.contrib.auth import authenticate, login

def my_view(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
# Redirect to a success page.
...
else:
# Return an 'invalid login' error message.
...
  • 判断用户身份:可以通过 request.user.is_authenticated==False 表示为匿名者;否则 request.user 会被设置为 User 实例。
  • 更改密码:
1
2
3
4
5
from django.contrib.auth.models import User

u = User.objects.get(username='john')
u.set_password('new password')
u.save()
1
2
3
4
from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
  • 登出:django.contrib.auth.logout(request)

Django 用户认证(前端篇)

Django 的用户认证是用 Session 实现的,和其他的 Session 应该是类似的。但对于零基础前后端开发的我,不清楚这之中究竟发生了什么。于是我简单测试了一下。

在登录成功后,应答的 headers 中就会出现 Set-Cookies 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ http POST http://127.0.0.1:8000/api/accounts/login/ <<< '{"username":"lyh543@outlook.com", "password":"xxxxxxxx"}'
HTTP/1.1 200 OK
Allow: OPTIONS, POST
Connection: close
Content-Length: 493
Content-Type: application/json
Date: Tue, 09 Feb 2021 05:34:41 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.9.1
Set-Cookie: csrftoken=sJQyvoxpJ7nIwFpbgXSKKiBIoo7GxogKKTmsFwJshfyFMBIEyPlhQrvl8OK6FlQR; expires=Tue, 08 Feb 2022 05:34:40 GMT; Max-Age=31449600; Path=/; SameSite=Lax
Set-Cookie: sessionid=kmst16goqdwof54ycuynbz7wzk1scboc; expires=Tue, 23 Feb 2021 05:34:40 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
Vary: Accept, Cookie, Origin
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

前一个 csrftoken 是防止跨站请求的,如果项目是前后端分离的话,就需要进行配置;
后一个 sessionid 就是登录成功后的 sessionid 了。如果我们在下次请求中的 headers 中加入了这个 sessionid,服务器就能识别到我们。对于 Django 来说,就是 request.user 为登录的这个用户。

对于浏览器、requests.sessions.Session 等,会自动设置 Cookie。下面是利用 requests.sessions.Session 完成登录、查询管理员字段的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
In [2]: import requests

In [3]: s = requests.Session()

In [20]: r1 = s.post("http://localhost:8000/api/accounts/login/", data={"username":"lyh543@outlook.com", "password":"xxxxxxxx"})

In [21]: r1
Out[21]: <Response [200]>

In [22]: dict(s.cookies)
Out[22]:
{'csrftoken': 's1aepuw0A08k9PsFtTWn6CbCkLU24MU6tTuk3DieW4uj1b6PAXwSjrfHqfCvufz3',
'sessionid': 'f9ofrqwar9rs0phbg4p89647pwryowrs'}

In [23]: r1.request.headers
Out[23]: {'User-Agent': 'python-requests/2.18.4', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '47', 'Content-Type': 'application/x-www-form-urlencoded'}

In [25]: r1.headers
Out[25]: {'Date': 'Tue, 09 Feb 2021 03:48:32 GMT', 'Server': 'WSGIServer/0.2 CPython/3.9.1', 'Content-Type': 'application/json', 'Vary': 'Accept, Cookie, Origin', 'Allow': 'OPTIONS, POST', 'X-Frame-Options': 'DENY', 'Content-Length': '493', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'same-origin', 'Set-Cookie': 'csrftoken=s1aepuw0A08k9PsFtTWn6CbCkLU24MU6tTuk3DieW4uj1b6PAXwSjrfHqfCvufz3; expires=Tue, 08 Feb 2022 03:48:32 GMT; Max-Age=31449600; Path=/; SameSite=Lax, sessionid=f9ofrqwar9rs0phbg4p89647pwryowrs; expires=Tue, 23 Feb 2021 03:48:32 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax', 'Connection': 'close'}

In [26]: r1 = s.get("http://localhost:8000/api/activities/1/admin/")

In [27]: r1
Out[27]: <Response [200]>

而对于非登录操作、或登录失败,应答中的字段就不会有 Set-Cookies 字段,requests.sessions.Session 也不会设置 Cookies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [2]: import requests

In [3]: s = requests.Session()

In [7]: r1 = s.get("http://localhost:8000/api/activities/1/admin/")

In [8]: r1
Out[8]: <Response [403]>

In [9]: s.cookies
Out[9]: <RequestsCookieJar[]>

In [10]: list(s.cookies)
Out[10]: []

Django Session 的过期时间也是可以通过修改 SESSION_COOKIE_AGE 来修改的。

使用 JWT 进行身份验证

Django 自带的 Session 对于很多项目已经够用了。如果想要更高级一点的安全验证,如 Json Web Token,可以尝试 Simple JWT 配合 Django REST Framework 食用。文档给的示例代码很详细,有需要也可以仿照源码编写自己的 API。

Django 定时任务

可参考 Django-crontab

Django REST Framework

这部分就另开一篇博文来写了。

Django 项目部署

诚然,python manage.py runserver 8000 然后将 8000 端口交给 Nginx / Apache / Caddy 反向代理到 80(http) / 443(https),是最简单且最直接的方法。但是,其替代方案有多线程、占用内存小等优势。

Django 的管理命令 startproject 生成了一个最小化的默认 WSGI 配置,你可以按照自己项目的需要去调整这个配置,任何兼容 WSGI 的应用程序服务器都可以直接使用。

而其中一个 WSGI 应用程序服务器的方案,就是使用 Gunicorn。由于细节比较多,各位先不要急着实践,建议先通读这部分,再决定是否采用这种方式还是直接 startproject

安装 Gunicorn:

1
python -m pip install gunicorn

在项目文件夹下运行:

1
gunicorn -b "127.0.0.1:8000" <projectname>.wsgi

其中 <projectname>.wsgi 也是 Python 的模块的表示方法,其表示 ./<projectname>/wsgi.py 这个模块。

可以将执行这条命令的过程写为 Systemd 服务,并实现自动重启等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# djangoproject.service
[Unit]
Description=Django Project
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=1
User=root
WorkingDirectory=/path/to/<projectname>/
ExecStart=/usr/local/bin/gunicorn -b "127.0.0.1:8000" --access-logfile - <projectname>.wsgi

[Install]
WantedBy=multi-user.target

命令的 --access-logfile - 表示将 log 输出在控制台,在 Systemd 中即表示可以通过 systemctl status djangoproject 查询日志。

然后就是将这项服务复制到 /etc/systemd/system/,然后 enable 和 start 了:

1
2
3
4
sudo cp ./djangoproject.service /etc/systemd/system/
sudo systemctl enable djangoproject # 激活
sudo systemctl start djangoproject # 启动
sudo systemctl status djangoproject # 查询状态

但是!这并没有完成部署。访问 localhost:8000 时,可以看到 Django 有正常响应,但是所有静态文件全部失效,Swagger 文档生成也失效了。

为了解决这个问题,需要配置静态文件。

<projectname>/settings.py 中配置以下几个参数

1
2
3
4
5
6
7
import os
STATIC_ROOT = os.path.join(BASE_DIR, '.static')
STATIC_URL = '/api/static/'

STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]

三个参数的意义如下:

  • BASE_DIR/static 是开发中静态文件所在文件夹
  • BASE_DIR/.static 是项目生成后静态文件所在文件夹,应当加入 .gitignore
  • /api/static/ 是在网页中访问静态文件的路径

整个过程是这样的:

  1. 开发者将所需的静态文件放入 BASE_DIR/static
  2. 开发者运行 python3 manage.py collectstatic,Django 将开发者提供的 BASE_DIR/static 文件,和 Swagger 等 APP 提供的静态文件,一并复制进 BASE_DIR/.static
  3. 用户在浏览器中访问 /api/static/ 路径,表示用户想访问的文件夹是 BASE_DIR/.static

所以还需要进行以下两步:

  1. 运行 python3 manage.py collectstatic
  2. 通过 Nginx / Apache / Caddy 等将静态文件提供给用户

Gunicorn 提供了一个 Nginx.conf 配置模板,我也提供一份 Caddy 的配置模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
example.com {
handle /api/static/* {
uri strip_prefix /api/static
root * /etc/uestcmsc_webapp/backend/.static
file_server
}

handle /api/* {
reverse_proxy localhost:8000
}

handle {
root * /etc/uestcmsc_webapp/frontend
try_files {path} /index.html
file_server
}
}

需要注意的是,这种配置的前提是所有 REST API 放在了 /api/ 下,这种方法使用的 <projectname>/urls.py 如下:

1
2
3
4
5
6
7
8
9
10
11
api_urlpatterns = [
url(r'^docs(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
url(r'^docs/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
path('admin/', admin.site.urls),
# ...
]

urlpatterns = [
url('api/', include(api_urlpatterns))
]

项目开源地址

上面提到的项目开源在 GitHub