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 是防止跨站请求的,如果项目是前后端分离的话,就需要进行配置(关于 CSRF,可以看 和 CSRF 与 CORS 斗智斗勇);
后一个 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 项目部署 (WSGI)

Django 部署可以采用 WSGI,也可以使用 ASGI。WSGI 是为同步 Web Server 编写的,而 ASGI 是为异步 Web Server 编写的。虽然可以混用,但是同步函数和异步函数可以混用,但是会有约 1ms 的用于线程切换的性能损失。

。如果你主要使用的是异步函数,你可以快进到下一章,进行 ASGI 的部署。看完以后,再回来看看如何 处理静态文件

Gunicorn

诚然,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 服务,并实现 2 进程、每进程 3 线程,以及自动重启等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 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" \
--workers=2 \
--threads=3 \
--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))
]

Gunicorn 日志 ip 总是显示 127.0.0.1

出现这个问题,我第一反应是 Caddy 反代的锅,第二反应是 Django 的锅,最后查了一下才发现是 Gunicorn 的锅。

Gunicorn doesn’t log real ip from nginx - Stack Overflow

回答也说的很清楚,只需要按照格式修改好后追加到 --access-logformat 参数即可。

我把时间、两个 -和 127.0.0.1 去掉以后的配置如下:

1
2
3
4
5
6
ExecStart=/usr/local/bin/gunicorn -b "127.0.0.1:8000" \
--workers=2 \
--threads=3 \
--access-logfile - \
--access-logformat "%({X-Real-IP}i)s \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\"" \
<projectname>.wsgi

Django 项目部署 (ASGI)

Django 主推的 ASGI 部署方式,应该是它自己维护的 Daphne

安装 Daphne

1
python -m pip install daphne

在项目文件夹下运行:

1
daphne <projectname>.asgi:application
1
ExecStart=/usr/local/bin/daphne -p 8000 <projectname>.asgi:application

目前 Daphne 还不支持多进程,如需多进程,请使用 uvicorn

项目开源地址

上面提到的项目开源在 GitHub