# Django 后端学习路线
推荐从上往下看。
- 官方快速安装指南(3.1 版本) (opens new window),安装的教程
- 官方中文教程(3.1 版本) (opens new window),开发的教程
- MDN 上的 Django 教程 (opens new window),和上面的教程有重叠,推荐只看会话 (opens new window)和用户认证 (opens new window)部分
- Django REST Framework (opens new window),这是一个基于 Django 的 RESTful 后端框架,为常见 RESTful 操作提供了模板,大大降低 REST API 开发量
# Django 官方教程 关键步骤
本小节记录了 官方中文教程(3.1 版本) (opens new window) 中的关键步骤。
# 1. 创建项目、项目和一个视图
- 安装:
pip install Django
- 验证安装:
python -m django --version
- 创建并初始化项目文件夹:
django-admin startproject <projectname>
- 即时预览:在
<projectname>
目录下python manage.py runserver [port]
- 创建应用:
python manage.py startapp <appname>
- 编写视图:
# polls/views.py
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world. You're at the polls index.")
- 在应用中添加写好的视图:
# polls/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
- 在站点中添加应用的视图:
# 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
- 修改项目配置文件:
# 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 数据表。
- 创建模型:
# 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
- 激活模型:
# mysite/settings.py
INSTALLED_APPS = [
'polls.apps.PollsConfig' # 添加这一项
]
- 将模型更改写入数据库:
- 根据类的更改,生成一个
迁移
(一个存储在<app_lable>/migrations
下的 py 文件,存储了变化):python manage.py makemigrations [app_label]
(文档 (opens new window)) - 将一个
迁移
应用到数据库,并迁移数据:python manage.py migrate [app_label] [migration_name]
(文档 (opens new window)) - 查看一个
迁移
将对数据库造成的影响:python manage.py sqlmigrate <app_label> <migration_name>
(文档 (opens new window)) - 一般来说,类变更以后,需要:
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.year
:Question.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
模型:
# polls/admin.py
from django.contrib import admin
from .models import Question
admin.site.register(Question)
# 3. 视图和 urls
# 3.1 添加更多视图,并用参数匹配 url
# /polls/views.py
def detail(request, question_id: int):
return HttpResponse("You're looking at question %s." % question_id)
# 这里可以做更多的事情,比如调用其他 Python 包
# /polls/url.py
urlpatterns = [
path('<int:question_id>/', views.detail, name='detail'),
]
访问 /polls/34
会返回 You're looking at question 34.
# 3.2 使用 HTML 模板
编写一个 HTML 模板:
<!-- /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()
完成
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 错误码
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()
:
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
(opens new window)
如果想要汉化 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
- 登录:
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
实例。 - 更改密码:
from django.contrib.auth.models import User
u = User.objects.get(username='john')
u.set_password('new password')
u.save()
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
字段:
$ 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
完成登录、查询管理员字段的过程:
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
:
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
(opens new window) 来修改的。
# 使用 JWT 进行身份验证
Django 自带的 Session 对于很多项目已经够用了。如果想要更高级一点的安全验证,如 Json Web Token,可以尝试 Simple JWT (opens new window) 配合 Django REST Framework 食用。文档给的示例代码很详细,有需要也可以仿照源码编写自己的 API。
# Django 定时任务
可参考 Django-crontab (opens new window)。
# 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:
python -m pip install gunicorn
在项目文件夹下运行:
gunicorn -b "127.0.0.1:8000" <projectname>.wsgi
其中 <projectname>.wsgi
也是 Python 的模块的表示方法,其表示 ./<projectname>/wsgi.py
这个模块。
可以将执行这条命令的过程写为 Systemd 服务,并实现 2 进程、每进程 3 线程,以及自动重启等:
# 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 了:
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
中配置以下几个参数
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/
是在网页中访问静态文件的路径
整个过程是这样的:
- 开发者将所需的静态文件放入
BASE_DIR/static
- 开发者运行
python3 manage.py collectstatic
,Django 将开发者提供的BASE_DIR/static
文件,和 Swagger 等 APP 提供的静态文件,一并复制进BASE_DIR/.static
- 用户在浏览器中访问
/api/static/
路径,表示用户想访问的文件夹是BASE_DIR/.static
所以还需要进行以下两步:
- 运行
python3 manage.py collectstatic
- 通过 Nginx / Apache / Caddy 等将静态文件提供给用户
Gunicorn 提供了一个 Nginx.conf 配置模板 (opens new window),我也提供一份 Caddy 的配置模板:
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
如下:
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 (opens new window)
回答也说的很清楚,只需要按照格式修改好后追加到 --access-logformat
参数即可。
我把时间、两个 -
和 127.0.0.1 去掉以后的配置如下:
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 (opens new window)。
安装 Daphne
python -m pip install daphne
在项目文件夹下运行:
daphne <projectname>.asgi:application
ExecStart=/usr/local/bin/daphne -p 8000 <projectname>.asgi:application
目前 Daphne 还不支持多进程,如需多进程,请使用 uvicorn
。
# 项目开源地址
上面提到的项目开源在 GitHub (opens new window)。