TestDriven-io-博客中文翻译-二-
TestDriven.io 博客中文翻译(二)
Django 中的异步视图
编写异步代码使您能够不费吹灰之力就加快应用程序的速度。Django 版本> = 3.1 支持异步视图、中间件和测试。如果您还没有尝试过异步视图,现在是时候了解一下了。
本教程着眼于如何开始使用 Django 的异步视图。
如果您有兴趣了解更多关于异步代码背后的力量以及 Python 中线程、多处理和异步之间的差异,请查看我的文章用并发、并行和异步加速 Python。
目标
学完本教程后,您应该能够:
- 用 Django 写一个异步视图
- 在 Django 视图中发出一个非阻塞 HTTP 请求
- 用 Django 的异步视图简化基本的后台任务
- 使用
sync_to_async
在异步视图中进行同步调用 - 解释什么时候应该和不应该使用异步视图
您还应该能够回答以下问题:
- 如果在异步视图中进行同步调用会怎样?
- 如果在异步视图中进行同步和异步调用会怎么样?
- 用 Django 的异步观点芹菜还有必要吗?
先决条件
只要您已经熟悉 Django 本身,向非基于类的视图添加异步功能是非常简单的。
属国
- Python >= 3.8
- Django >= 3.1
- 独角兽企业
- HTTPX
ASGI 是什么?
ASGI 代表异步服务器网关接口。它是 WSGI 的现代异步后续,为创建基于 Python 的异步 web 应用程序提供了标准。
另一件值得一提的事情是,ASGI 向后兼容 WSGI,这是一个很好的借口,可以从像 Gunicorn 或 uWSGI 这样的 WSGI 服务器切换到像 Uvicorn 或 Daphne 这样的 ASGI 服务器,即使你没有准备好切换到编写异步应用程序。
创建应用程序
创建一个新的项目目录和一个新的 Django 项目:
`$ mkdir django-async-views && cd django-async-views
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install django
(env)$ django-admin startproject hello_async .`
你可以随意把 virtualenv 和 Pip 换成诗歌或 Pipenv 。更多信息,请查看现代 Python 环境。
如果您使用内置的开发服务器,Django 将运行您的异步视图,但它实际上不会异步运行它们,所以我们将使用 Uvicorn 运行 Django。
安装它:
`(env)$ pip install uvicorn`
要使用 Uvicorn 运行您的项目,您可以从项目的根目录使用以下命令:
`uvicorn {name of your project}.asgi:application`
在我们的例子中,这将是:
`(env)$ uvicorn hello_async.asgi:application`
接下来,让我们创建第一个异步视图。添加一个新文件来保存“hello_async”文件夹中的视图,然后添加以下视图:
`# hello_async/views.py
from django.http import HttpResponse
async def index(request):
return HttpResponse("Hello, async Django!")`
在 Django 中创建异步视图就像创建同步视图一样简单——您所需要做的就是添加关键字async
。
更新 URL:
`# hello_async/urls.py
from django.contrib import admin
from django.urls import path
from hello_async.views import index
urlpatterns = [
path("admin/", admin.site.urls),
path("", index),
]`
现在,在终端的根文件夹中,运行:
`(env)$ uvicorn hello_async.asgi:application --reload`
标志告诉 Uvicorn 观察你的文件是否有变化,如果有变化就重新加载。这可能是不言自明的。
在您最喜欢的网络浏览器中打开 http://localhost:8000/ :
不是世界上最令人兴奋的事情,但是,嘿,这是一个开始。值得注意的是,用 Django 的内置开发服务器运行这个视图会产生完全相同的功能和输出。这是因为我们实际上没有在处理程序中做任何异步的事情。
HTTPX
值得注意的是,异步支持是完全向后兼容的,因此您可以混合异步和同步视图、中间件和测试。Django 将在适当的执行上下文中执行每一个。
为了演示这一点,添加几个新视图:
`# hello_async/views.py
import asyncio
from time import sleep
import httpx
from django.http import HttpResponse
# helpers
async def http_call_async():
for num in range(1, 6):
await asyncio.sleep(1)
print(num)
async with httpx.AsyncClient() as client:
r = await client.get("https://httpbin.org/")
print(r)
def http_call_sync():
for num in range(1, 6):
sleep(1)
print(num)
r = httpx.get("https://httpbin.org/")
print(r)
# views
async def index(request):
return HttpResponse("Hello, async Django!")
async def async_view(request):
loop = asyncio.get_event_loop()
loop.create_task(http_call_async())
return HttpResponse("Non-blocking HTTP request")
def sync_view(request):
http_call_sync()
return HttpResponse("Blocking HTTP request")`
更新 URL:
`# hello_async/urls.py
from django.contrib import admin
from django.urls import path
from hello_async.views import index, async_view, sync_view
urlpatterns = [
path("admin/", admin.site.urls),
path("async/", async_view),
path("sync/", sync_view),
path("", index),
]`
安装 HTTPX :
服务器运行时,导航到http://localhost:8000/async/。您应该会立即看到响应:
`Non-blocking HTTP request`
在您的终端中,您应该看到:
`INFO: 127.0.0.1:60374 - "GET /async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>`
这里,HTTP 响应在第一次睡眠调用之前被发送回。
接下来,导航到http://localhost:8000/sync/。大约需要五秒钟才能得到响应:
转向终端:
`1
2
3
4
5
<Response [200 OK]>
INFO: 127.0.0.1:60375 - "GET /sync/ HTTP/1.1" 200 OK`
这里,HTTP 响应是在循环和对https://httpbin.org/
的请求完成后发送的。
熏肉
为了更好地模拟如何利用异步的真实场景,让我们看看如何异步运行多个操作,聚合结果,并将它们返回给调用者。
回到你的项目的 URLconf,在 smoke_some_meats
创建一个新路径:
`# hello_async/urls.py
from django.contrib import admin
from django.urls import path
from hello_async.views import index, async_view, sync_view, smoke_some_meats
urlpatterns = [
path("admin/", admin.site.urls),
path("smoke_some_meats/", smoke_some_meats),
path("async/", async_view),
path("sync/", sync_view),
path("", index),
]`
回到您的视图中,创建一个名为smoke
的新异步助手函数。这个函数有两个参数:一个名为smokables
的字符串列表和一个名为flavor
的字符串。这些分别默认为可吸烟的肉类和“糖宝·雷”的列表。
`# hello_async/views.py
async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> List[str]:
""" Smokes some meats and applies the Sweet Baby Ray's """
for smokable in smokables:
print(f"Smoking some {smokable}...")
print(f"Applying the {flavor}...")
print(f"{smokable.capitalize()} smoked.")
return len(smokables)`
for 循环异步地将风味(读:糖宝·雷的)应用到烟草(读:熏肉)上。
不要忘记重要的一点:
List
用于额外的打字功能。这不是必需的,可以很容易地省略(只需去掉“smokables”参数声明后面的: List[str]
)。
接下来,再添加两个异步助手:
`async def get_smokables():
print("Getting smokeables...")
await asyncio.sleep(2)
async with httpx.AsyncClient() as client:
await client.get("https://httpbin.org/")
print("Returning smokeable")
return [
"ribs",
"brisket",
"lemon chicken",
"salmon",
"bison sirloin",
"sausage",
]
async def get_flavor():
print("Getting flavor...")
await asyncio.sleep(1)
async with httpx.AsyncClient() as client:
await client.get("https://httpbin.org/")
print("Returning flavor")
return random.choice(
[
"Sweet Baby Ray's",
"Stubb's Original",
"Famous Dave's",
]
)`
确保添加导入:
创建使用异步函数的异步视图:
`# hello_async/views.py
async def smoke_some_meats(request):
results = await asyncio.gather(*[get_smokables(), get_flavor()])
total = await asyncio.gather(*[smoke(results[0], results[1])])
return HttpResponse(f"Smoked {total[0]} meats with {results[1]}!")`
这个视图同时调用get_smokables
和get_flavor
函数。由于smoke
依赖于来自get_smokables
和get_flavor
的结果,我们使用gather
来等待每个异步任务完成。
请记住,在常规的同步视图中,get_smokables
和get_flavor
将一次处理一个。此外,异步视图将产生执行,并允许在处理异步任务的同时处理其他请求,这允许在特定的时间内由同一进程处理更多的请求。
最后,返回一个响应,让用户知道他们美味的 BBQ 餐准备好了。
太好了。保存文件,然后返回浏览器,导航到http://localhost:8000/smoke _ some _ meats/。应该需要几秒钟才能得到响应:
`Smoked 6 meats with Sweet Baby Ray's!`
在您的控制台中,您应该会看到:
`Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable
Smoking some ribs...
Applying the Stubb's Original...
Ribs smoked.
Smoking some brisket...
Applying the Stubb's Original...
Brisket smoked.
Smoking some lemon chicken...
Applying the Stubb's Original...
Lemon chicken smoked.
Smoking some salmon...
Applying the Stubb's Original...
Salmon smoked.
Smoking some bison sirloin...
Applying the Stubb's Original...
Bison sirloin smoked.
Smoking some sausage...
Applying the Stubb's Original...
Sausage smoked.
INFO: 127.0.0.1:57501 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK`
请注意以下打印语句的顺序:
`Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable`
这就是工作中的异步性:当get_smokables
函数休眠时,get_flavor
函数完成处理。
烧焦的肉
同步呼叫
问:如果在异步视图中进行同步调用会怎样?
如果从非异步视图调用非异步函数,也会发生同样的事情。
--
为了说明这一点,在您的 views.py 中创建一个名为oversmoke
的新助手函数:
`# hello_async/views.py
def oversmoke() -> None:
""" If it's not dry, it must be uncooked """
sleep(5)
print("Who doesn't love burnt meats?")`
非常简单:我们只是同步等待五秒钟。
创建调用此函数的视图:
`# hello_async/views.py
async def burn_some_meats(request):
oversmoke()
return HttpResponse(f"Burned some meats.")`
最后,在项目的 URLconf 中连接路线:
`# hello_async/urls.py
from django.contrib import admin
from django.urls import path
from hello_async.views import index, async_view, sync_view, smoke_some_meats, burn_some_meats
urlpatterns = [
path("admin/", admin.site.urls),
path("smoke_some_meats/", smoke_some_meats),
path("burn_some_meats/", burn_some_meats),
path("async/", async_view),
path("sync/", sync_view),
path("", index),
]`
在浏览器中访问路线在http://localhost:8000/burn _ some _ meats:
请注意,最终从浏览器得到响应花了五秒钟。您还应该同时收到控制台输出:
`Who doesn't love burnt meats?
INFO: 127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK`
可能值得注意的是,不管您使用的是什么服务器,不管是基于 WSGI 还是 ASGI,都会发生同样的事情。
同步和异步呼叫
问:如果在异步视图中进行同步和异步调用会怎么样?
不要这样。
同步和异步视图往往最适合不同的目的。如果在异步视图中有阻塞功能,最好的情况也不会比仅仅使用同步视图更好。
同步到异步
如果您需要在一个异步视图中进行同步调用(例如,通过 Django ORM 与数据库交互),使用 sync_to_async 作为包装器或装饰器。
示例:
`# hello_async/views.py
async def async_with_sync_view(request):
loop = asyncio.get_event_loop()
async_function = sync_to_async(http_call_sync, thread_sensitive=False)
loop.create_task(async_function())
return HttpResponse("Non-blocking HTTP request (via sync_to_async)")`
你注意到我们将
thread_sensitive
参数设置为False
了吗?这意味着同步函数http_call_sync
将在一个新的线程中运行。查看文档了解更多信息。
将导入添加到顶部:
`from asgiref.sync import sync_to_async`
添加 URL:
`# hello_async/urls.py
from django.contrib import admin
from django.urls import path
from hello_async.views import (
index,
async_view,
sync_view,
smoke_some_meats,
burn_some_meats,
async_with_sync_view
)
urlpatterns = [
path("admin/", admin.site.urls),
path("smoke_some_meats/", smoke_some_meats),
path("burn_some_meats/", burn_some_meats),
path("sync_to_async/", async_with_sync_view),
path("async/", async_view),
path("sync/", sync_view),
path("", index),
]`
在您的浏览器中进行测试,网址为http://localhost:8000/sync _ to _ async/。
在您的终端中,您应该看到:
`INFO: 127.0.0.1:61365 - "GET /sync_to_async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>`
使用sync_to_async
,阻塞同步调用在后台线程中处理,允许 HTTP 响应在第一个睡眠调用之前被发送回。
芹菜和异步视图
问:对于 Django 的异步观点,芹菜还有存在的必要吗?
看情况。
Django 的异步视图提供了与任务或消息队列相似的功能,而没有复杂性。如果您正在使用(或者正在考虑使用)Django,并且想做一些简单的事情(并且不关心可靠性),异步视图是一个快速而简单地实现这一点的好方法。如果您需要执行更繁重、长时间运行的后台进程,您仍然会希望使用 Celery 或 RQ。
应该注意,为了有效地使用异步视图,视图中应该只有异步调用。另一方面,任务队列在不同的进程上使用工作线程,因此能够在多个服务器上的后台运行同步调用。
顺便说一下,您决不需要在异步视图和消息队列之间做出选择——您可以很容易地同时使用它们。例如:您可以使用异步视图发送电子邮件或进行一次性数据库修改,但让 Celery 在每晚的预定时间清理您的数据库或生成并发送客户报告。
何时使用
对于绿地项目,如果异步是你的事情,利用异步视图,尽可能以异步方式编写你的 I/O 过程。也就是说,如果您的大多数视图只需要调用数据库并在返回数据之前做一些基本的处理,那么您不会看到比坚持使用同步视图有太多的增加(如果有的话)。
对于棕地项目,如果你有很少或没有 I/O 进程,坚持使用同步视图。如果你有许多 I/O 进程,衡量一下用异步方式重写它们有多容易。将 sync I/O 重写为 async 并不容易,所以在尝试重写为 async 之前,您可能需要优化您的 sync I/O 和视图。另外,将同步过程与异步视图混合在一起从来都不是一个好主意。
在生产中,一定要使用 Gunicorn 来管理 uvicon,以便利用并发性(通过 uvicon)和并行性(通过 Gunicorn workers):
`gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application`
结论
总之,尽管这是一个简单的用例,但它应该让您大致了解 Django 的异步视图所带来的可能性。在异步视图中还可以尝试发送电子邮件、调用第三方 API 和读写文件。
有关 Django 新发现的异步性的更多信息,请参阅这篇优秀文章,它涵盖了相同的主题以及多线程和测试。
将 Django 应用部署到 Azure 应用服务
在本教程中,我们将看看如何安全地部署一个 Django 应用到 Azure 应用服务。
目标
学完本教程后,您应该能够:
- 解释什么是 Azure App Service 以及它是如何工作的。
- 将 Django 应用程序部署到 Azure 应用程序服务。
- 在 Azure 上运行一个 Postgres 实例。
- 用 Azure Storage 设置持久存储。
- 将自定义域链接到您的 web 应用程序,并在 HTTPS 上提供服务。
什么是 Azure App 服务?
Azure App Service 允许您快速轻松地为任何平台或设备创建企业级 web 和移动应用,并将其部署在可扩展且可靠的云基础设施上。它本身支持 Python。网,。NET Core,Node.js,Java,PHP,容器。它们具有内置的 CI/CD、安全功能和零停机部署。
Azure App Service 提供了强大的扩展能力,允许应用程序根据流量和使用模式自动扩展或缩减。Azure 还保证了 99.95%的 SLA。
如果你是新客户,你可以获得 200 美元的免费积分来测试 Azure。
为什么选择 App 服务?
- 强大的扩展能力
- 与 Visual Studio 很好地集成
- 通过 Azure Active Directory 进行认证
- 内置 SSL/TLS 证书
- 监控和警报
项目设置
在本教程中,我们将部署一个简单的图像托管应用程序,名为 django-images 。
在学习教程的过程中,通过部署您自己的 Django 应用程序来检查您的理解。
首先,从 GitHub 上的库获取代码。
创建新的虚拟环境并激活它:
`$ python3 -m venv venv && source venv/bin/activate`
安装需求并迁移数据库:
`(venv)$ pip install -r requirements.txt
(venv)$ python manage.py migrate`
运行服务器:
`(venv)$ python manage.py runserver`
打开您最喜欢的网络浏览器,导航到 http://localhost:8000 。使用右边的表格上传图像,确保一切正常。上传图像后,您应该会看到它显示在表格中:
配置 Django 项目
在本节教程中,我们将配置 Django 项目来使用 App Service。
环境变量
我们不应该在源代码中存储秘密,所以让我们利用环境变量。最简单的方法是使用名为 python-dotenv 的第三方 Python 包。首先将其添加到 requirements.txt :
随意使用不同的包来处理环境变量,如 django-environ 或 python-decouple 。
对于 Django 来说,要初始化环境更改,请更新 settings.py 的顶部,如下所示:
`# core/settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env')`
接下来,从环境中加载SECRET_KEY
、DEBUG
、ALLOWED_HOSTS
和其他设置:
`# core/settings.py
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', '0').lower() in ['true', 't', '1']
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(' ')
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(' ')
SECURE_SSL_REDIRECT = \
os.getenv('SECURE_SSL_REDIRECT', '0').lower() in ['true', 't', '1']
if SECURE_SSL_REDIRECT:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')`
数据库ˌ资料库
要使用 Postgres 代替 SQLite,我们首先需要安装数据库适配器。
将下面一行添加到 requirements.txt 中:
稍后,当我们启动 Postgres 实例时,Azure 将为我们提供一个数据库连接字符串。它将具有以下格式:
`dbname=<db_name> host=<db_host> port=5432 user=<db_user> password=<db_password>`
由于这个字符串在 Django 中使用起来相当笨拙,我们将把它分成以下几个 env 变量:DBNAME
、DBHOST
、DBUSER
、DBPASS
。
导航到 core/settings.py 并像这样更改DATABASES
:
`# core/settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DBNAME'),
'HOST': os.environ.get('DBHOST'),
'USER': os.environ.get('DBUSER'),
'PASSWORD': os.environ.get('DBPASS'),
'OPTIONS': {'sslmode': 'require'},
}
}`
Azure 应用服务也需要sslmode
,所以一定要启用它。
格尼科恩
接下来,让我们安装 Gunicorn ,这是一个生产级的 WSGI 服务器,它将用于生产,而不是 Django 的开发服务器。
添加到 requirements.txt :
Django 配置到此为止!
部署应用程序
在教程的这一部分,我们将把 web 应用部署到 Azure App Service。如果你还没有 Azure 账户,就去注册一个吧。
项目初始化
从仪表盘中,使用搜索栏搜索“Web App +数据库”。您应该会在“市场”部分看到它:
单击它,您应该会被重定向到应用程序创建表单。
使用该表单创建项目、应用程序和数据库。
项目详情
- 订阅:保留默认设置
- 资源组: django-images-group
- 地区:离你最近的地区
Web 应用程序详细信息
- 名称:姜戈图像
- 运行时堆栈: Python 3.11
数据库ˌ资料库
- 数据库引擎: PostgreSQL -灵活服务器
- 服务器名称: django-images-db
- 数据库名称: django-images
- 主持:由你决定
填写完所有详细信息后,单击“查看”按钮,然后单击“创建”。
设置大约需要五分钟。完成后,通过单击“转到资源”按钮导航到新创建的资源。
记下应用程序的 URL,因为我们将需要它来配置ALLOWED_HOSTS
、CSRF_TRUSTED_ORIGINS
和其他一些 Django 设置。
应用程序配置
为了让应用程序工作,我们需要添加我们在 Django 项目中使用的所有环境变量。
导航至您的应用服务应用,并选择边栏上的“设置>配置”。
您会注意到已经添加了一个名为AZURE_POSTGRESQL_CONNECTIONSTRING
的应用程序设置。这是因为我们使用了“Web App +数据库”来初始化项目。
连接字符串包含连接到数据库所需的所有信息。如前所述,让我们将其拆分为多个变量,并将它们作为单独的应用程序设置进行添加:
`DBNAME=<db_name>
DBHOST=<db_host>
DBUSER=<db_user>
DBPASS=<db_pass>`
确保用您的实际凭据替换占位符。
如果您的密码以
$
结尾,请确保包含它。那个$
是密码的一部分,而不是正则表达式锚。
接下来,添加以下附加变量:
`DEBUG=1 SECRET_KEY=w7a8a@lj8nax7tem0caa2f2rjm2ahsascyf83sa5alyv68vea ALLOWED_HOSTS=localhost 127.0.0.1 [::1] <your_app_url> CSRF_TRUSTED_ORIGINS=https://<your_app_url> SECURE_SSL_REDIRECT=0`
确保将<your_app_url>
替换为上一步中的应用 URL。
不用担心
DEBUG
等不安全的设定。我们以后再换!
添加完所有应用程序设置后,单击“保存”更新并重新启动应用程序服务应用程序。等待一两分钟,让重启完成,然后进入下一步。
部署代码
要将你的代码部署到 Azure App Service,你首先需要将其推送到 GitHub 、 Bitbucket 、 Azure Repos 或另一个基于 git 的版本控制系统。如果你还没有这样做,那就去做吧。
我们将在本教程中使用 GitHub。
导航至您的应用服务应用,并在侧边栏上选择“部署>部署中心”。选择您想要使用的来源,并使用您的第三方帐户进行鉴定(如果您还没有)。
接下来,填写表格:
- 来源: GitHub
- 组织:您的个人账户或组织
- 存储库:您想要部署的存储库
- 分支:您想要部署的分支
最后,点击“保存”。
Azure 现在将设置 GitHub 动作部署管道,用于部署您的应用程序。从现在开始,每次你把代码推送到选中的分支,你的 app 都会自动重新部署。
如果你导航到你的 GitHub 库,你会注意到一个名为。github/工作流”。还会有 GitHub 动作工作流运行。工作流运行完成后,尝试在您喜爱的 web 浏览器中访问您的应用程序 URL。
当您第一次访问新部署的应用程序时,应用程序服务可能需要一点时间来“唤醒”。给它几分钟,再试一次。
如果您的应用程序依赖于数据库迁移,您可能会得到一个错误,因为我们还没有迁移数据库。我们将在下一步中做它。
嘘
App Service 让我们可以轻松地从浏览器 SSH 到我们的服务器。
要使用 SSH,请导航到您的应用服务应用,然后在边栏上选择“开发工具> SSH”。接下来,单击“Go”按钮。Azure 将打开一个新的浏览器窗口,其中包含到服务器的活动 SSH 连接:
`_____
/ _ \ __________ _________ ____
/ /_\ \\___ / | \_ __ \_/ __ \
/ | \/ /| | /| | \/\ ___/
\____|__ /_____ \____/ |__| \___ >
\/ \/ \/
A P P S E R V I C E O N L I N U X
Documentation: http://aka.ms/webapp-linux
Python 3.9.16
Note: Any data outside '/home' is not persisted
(antenv) [[email protected]](/cdn-cgi/l/email-protection):/tmp/8db11d9b11cc42a#`
让我们迁移数据库并创建一个超级用户:
`(antenv)$ python manage.py migrate
(antenv)$ python manage.py createsuperuser`
不错!此时,您的 web 应用程序应该完全正常工作。
持久存储
Azure 应用服务提供了一个短暂的文件系统。这意味着您的数据不是持久的,可能会在应用程序关闭或重新部署时消失。如果你的应用程序需要保留文件,这是非常糟糕的。
要为静态和媒体文件设置持久存储,我们可以使用 Azure Storage 。
导航到你的 Azure 仪表盘,搜索“Azure Storage”。然后选择“存储帐户”。
创建存储帐户
要使用 Azure 存储,您首先需要创建一个存储帐户。单击“创建存储帐户”,并使用以下详细信息创建一个新的存储帐户:
- 订阅:保留默认设置
- 资源组: django-images-group
- 存储帐户名称:选择一个自定义名称
- 地区:与您的应用相同的地区
- 性能:由你决定
- 冗余:保留默认设置
将其他内容保留为默认值,检查并保存。记下存储帐户名,因为我们将在本教程的后面用到它。
成功创建存储帐户后,导航至该帐户。然后在侧边栏选择“安全+网络>访问密钥”并抓取其中一个密钥。
创建容器
为了更好地组织我们的存储,我们将创建两个独立的容器。
首先,导航回“概述”,然后单击“Blob 服务”。
继续创建两个容器,一个名为“静态”,另一个名为“媒体”。它们都应该将“公共访问级别”设置为“Blob(仅限 Blob 的匿名读取访问)”。
配置应用程序
接下来,导航至您的应用程序服务应用程序配置,并添加以下两个应用程序设置:
`AZURE_ACCOUNT_NAME=<your_storage_account_name>
AZURE_ACCOUNT_KEY=<your_storage_account_key>`
单击“保存”并等待您的应用程序重新启动。
配置 Django
为了利用 Azure 存储,我们将使用一个名为 django-storages 的第三方包。
将以下几行添加到 requirements.txt
`django-storages==1.13.2
azure-core==1.26.3
azure-storage-blob==12.14.1`
接下来,转到 core/settings.py 并更改静态和媒体文件设置,如下所示:
`# core/settings.py
DEFAULT_FILE_STORAGE = 'core.azure_storage.AzureMediaStorage'
STATICFILES_STORAGE = 'core.azure_storage.AzureStaticStorage'
AZURE_ACCOUNT_NAME = os.getenv('AZURE_ACCOUNT_NAME')
AZURE_ACCOUNT_KEY = os.getenv('AZURE_ACCOUNT_KEY')
AZURE_CUSTOM_DOMAIN = f'{AZURE_ACCOUNT_NAME}.blob.core.windows.net'
STATIC_URL = f'https://{AZURE_CUSTOM_DOMAIN}/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = f'https://{AZURE_CUSTOM_DOMAIN}/media/'
MEDIA_ROOT = BASE_DIR / 'mediafiles'`
因为我们使用两个独立的容器,我们需要定义自己的AzureMediaStorage
和AzureStaticStorage
。在“核心”目录下(在 settings.py 旁边)新建一个名为 azure_storage.py 的文件,内容如下:
`# core/azure_storage.py
import os
from storages.backends.azure_storage import AzureStorage
class AzureMediaStorage(AzureStorage):
account_name = os.getenv('AZURE_ACCOUNT_NAME')
account_key = os.getenv('AZURE_ACCOUNT_KEY')
azure_container = 'media'
expiration_secs = None
class AzureStaticStorage(AzureStorage):
account_name = os.getenv('AZURE_ACCOUNT_NAME')
account_key = os.getenv('AZURE_ACCOUNT_KEY')
azure_container = 'static'
expiration_secs = None`
提交您的代码并将其推送到 VCS。
在您的应用程序重新部署后,SSH 进入服务器并尝试收集静态文件:
`(antenv)$ python manage.py collectstatic
You have requested to collect static files at the destination
location as specified in your settings.
This will overwrite existing files!
Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel: yes
141 static files copied.`
为了确保静态和媒体文件正常工作,请导航到您的应用程序的/admin
并检查 CSS 是否已加载。接下来,试着上传一张图片。
自定义域
要将自定义域链接到你的应用,首先导航到你的应用仪表板,然后选择边栏上的“设置>自定义域”。之后,点击“添加自定义域”。
然后,添加一个包含以下详细信息的自定义域:
- 域提供者:所有其他域服务
- TLS/SSL 证书: App 服务托管证书
- TLS/SSL 类型:sni SSL
- 域:你的域(如 azure.testdriven.io)
- 主机名记录类型: CNAME
输入所有细节后,Azure 会要求您验证您的域所有权。为此,您需要导航到您的域名注册商的 DNS 设置,并添加一个新的“CNAME 记录”,指向您的应用程序 URL 和一个 TXT 记录,如下所示:
`+----------+--------------+------------------------------------+-----------+ | Type | Host | Value | TTL |
+----------+--------------+------------------------------------+-----------+ | CNAME | <some host> | <your_app_url> | Automatic |
+----------+--------------+------------------------------------+-----------+ | TXT | asuid.azure | <your_txt_value> | Automatic |
+----------+--------------+------------------------------------+-----------+`
示例:
`+----------+--------------+------------------------------------+-----------+ | Type | Host | Value | TTL |
+----------+--------------+------------------------------------+-----------+ | CNAME | azure | django-images.azurewebsites.net | Automatic |
+----------+--------------+------------------------------------+-----------+ | TXT | asuid.azure | BXVJAHNLY3JLDG11Y2H3B3C6KQASDFF | Automatic |
+----------+--------------+------------------------------------+-----------+`
等待几分钟,让 DNS 更改传播开来,然后单击“验证”。验证完成后,单击“添加”。
Azure 将一个自定义域链接到应用程序,并颁发 SSL 证书。您的自定义域应该以“安全”状态显示在表格中。
如果您的域的状态为“无绑定”,或者您收到错误消息“无法为创建应用程序服务管理的证书……”,单击“添加绑定”,将所有内容保留为默认设置,然后单击“验证”。如果失败,请在几分钟后重试。
我们需要做的最后一件事是改变ALLOWED_HOSTS
和CSRF_TRUSTED_ORIGINS
并启用SECURE_SSL_REDIRECT
。导航至您的应用程序服务应用程序配置,并按如下方式进行更改:
`ALLOWED_HOSTS=localhost 127.0.0.1 [::1] <your_custom_domain>
CSRF_TRUSTED_ORIGINS=https://<your_custom_domain>
SECURE_SSL_REDIRECT=1`
现在,您应该可以在 HTTPS 上的自定义域中访问您的 web 应用。
结论
在本教程中,我们已经成功地将 Django 应用部署到 Azure 应用服务。我们已经处理了 Postgres 数据库,配置了静态和媒体文件服务,链接了自定义域名,并启用了 HTTPS。现在你应该对 Azure 的工作原理有了基本的了解,并且能够部署你自己的 Django 应用了。
从 GitHub repo 中抓取最终的源代码。
未来的步骤
- 查看 Azure 应用服务监控和记录。
- 学习如何使用 Azure 命令行界面。
Django 的缓存
缓存通常是提升应用程序性能的最有效方式。
对于动态网站,在呈现模板时,您通常需要从各种来源(比如数据库、文件系统和第三方 API 等)收集数据,处理数据,并在将数据提供给客户端之前对其应用业务逻辑。最终用户将会注意到由于网络延迟造成的任何延迟。
例如,假设您必须对外部 API 进行 HTTP 调用,以获取呈现模板所需的数据。即使在完美的条件下,这也会增加渲染时间,从而增加整体加载时间。如果 API 下降或者你受到速率限制怎么办?无论哪种方式,如果数据很少更新,最好实现一种缓存机制,以避免对每个客户端请求都进行 HTTP 调用。
本文首先从整体上回顾 Django 的缓存框架,然后一步一步地详细说明如何缓存 Django 视图,来研究如何做到这一点。
依赖关系:
- Django v3.2.5
- django-redis v5.0.0 版
- python 3 . 9 . 6 版
- pymemcache 3 . 5 . 0 版
- 请求版本 2.26.0
--
Django 缓存文章:
目标
学完本教程后,您应该能够:
- 解释为什么您可能想要缓存 Django 视图
- 描述 Django 可用于缓存的内置选项
- 用缰绳遮住决哥的视线
- 使用 Apache Bench 对 Django 应用程序进行负载测试
- 用 Memcached 缓存 Django 视图
Django 缓存类型
Django 附带了几个内置缓存后端,以及对自定义后端的支持。
内置选项包括:
- Memcached : Memcached 是一个基于内存的键值存储,用于存储小块数据。它支持跨多个服务器的分布式缓存。
- 数据库:这里,缓存碎片存储在一个数据库中。为此,可以用 Django 的一个管理命令创建一个表。这不是性能最好的缓存类型,但它对于存储复杂的数据库查询很有用。
- 文件系统:缓存保存在文件系统上,每个缓存值保存在单独的文件中。这是所有缓存类型中最慢的,但是在生产环境中最容易设置。
- 本地内存:本地内存缓存,最适合您的本地开发或测试环境。虽然它几乎和 Memcached 一样快,但它不能扩展到单个服务器之外,所以它不适合用作任何使用多个 web 服务器的应用程序的数据缓存。
- Dummy :一个“虚拟”缓存,它实际上不缓存任何东西,但仍然实现缓存接口。当你不想要缓存,但又不想改变你的代码的时候,这意味着在开发或测试中使用它。
Django 缓存级别
Django 中的缓存可以在不同的层次上实现(或者站点的不同部分)。您可以缓存整个站点或不同粒度级别的特定部分(按粒度降序排列):
每个站点的缓存
这是在 Django 中实现缓存的最简单的方法。要做到这一点,您只需将两个中间件类添加到您的 settings.py 文件中:
`MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware', # NEW
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # NEW
]`
中间件的顺序在这里很重要。
UpdateCacheMiddleware
必须在FetchFromCacheMiddleware
之前。更多信息请看 Django 文档中中间件的订单。
然后,您需要添加以下设置:
`CACHE_MIDDLEWARE_ALIAS = 'default' # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = '600' # number of seconds to cache a page for (TTL)
CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same Django instance`
虽然如果您的站点只有很少或没有动态内容,缓存整个站点可能是一个不错的选择,但它可能不适合用于具有基于内存的缓存后端的大型站点,因为 RAM 非常昂贵。
每视图缓存
您可以缓存特定的视图,而不是将宝贵的内存空间浪费在缓存从快速变化的 API 获取数据的静态页面或动态页面上。这是我们将在本文中使用的方法。当你打算在 Django 应用中实现缓存时,这也是你应该开始考虑的缓存级别。
您可以使用 cache_page 装饰器直接在视图函数上或者在URLConf
内的路径中实现这种类型的缓存:
`from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def your_view(request):
...
# or
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', cache_page(60 * 15)(your_view)),
]`
缓存本身是基于 URL 的,所以对object/1
和object/2
的请求将被分别缓存。
值得注意的是,直接在视图上实现缓存使得在某些情况下禁用缓存更加困难。例如,如果您希望允许某些用户在没有缓存的情况下访问视图,该怎么办?通过URLConf
启用缓存提供了将不同的 URL 关联到不使用缓存的视图的机会:
`from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', your_view),
path('object/cache/<int:object_id>/', cache_page(60 * 15)(your_view)),
]`
模板片段缓存
如果您的模板包含根据数据经常变化的部分,您可能希望将它们从缓存中删除。
例如,您可能在模板的某个区域的导航栏中使用经过身份验证的用户的电子邮件。如果你有成千上万的用户,那么这个片段将在内存中被复制成千上万次,每个用户一个。这就是模板片段缓存发挥作用的地方,它允许您指定要缓存的模板的特定区域。
要缓存对象列表:
`{% load cache %}
{% cache 500 object_list %}
<ul>
{% for object in objects %}
<li>{{ object.title }}</li>
{% endfor %}
</ul>
{% endcache %}`
这里,{% load cache %}
为我们提供了对cache
模板标签的访问,该标签期望以秒为单位的缓存超时(500
)以及缓存片段的名称(object_list
)。
低级高速缓存 API
如果前面的选项不能提供足够的粒度,您可以使用低级 API 通过缓存键管理缓存中的单个对象。
例如:
`from django.core.cache import cache
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
objects = cache.get('objects')
if objects is None:
objects = Objects.all()
cache.set('objects', objects)
context['objects'] = objects
return context`
在本例中,当在数据库中添加、更改或删除对象时,您会希望使缓存失效(或删除)。管理这种情况的一种方法是通过数据库信号:
`from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
@receiver(post_delete, sender=Object)
def object_post_delete_handler(sender, **kwargs):
cache.delete('objects')
@receiver(post_save, sender=Object)
def object_post_save_handler(sender, **kwargs):
cache.delete('objects')`
有关使用数据库信号使缓存无效的更多信息,请查看 Django 文章中的低级缓存 API。
至此,让我们来看一些例子。
项目设置
从 cache-django-view repo 中克隆基础项目,然后检查基础分支:
`$ git clone https://github.com/testdrivenio/cache-django-view.git --branch base --single-branch
$ cd cache-django-view`
创建(并激活)虚拟环境,并满足以下要求:
`$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt`
应用 Django 迁移,然后启动服务器:
`(venv)$ python manage.py migrate
(venv)$ python manage.py runserver`
在您选择的浏览器中导航到 http://127.0.0.1:8000 以确保一切按预期运行。
您应该看到:
记下你的终端。您应该看到请求的总执行时间:
该指标来自 core/middleware.py :
`import logging
import time
def metric_middleware(get_response):
def middleware(request):
# Get beginning stats
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
# Calculate stats
total_time = end_time - start_time
# Log the results
logger = logging.getLogger('debug')
logger.info(f'Total time: {(total_time):.2f}s')
print(f'Total time: {(total_time):.2f}s')
return response
return middleware`
快速浏览一下 apicalls/views.py 中的视图:
`import datetime
import requests
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context`
这个视图通过requests
向httpbin.org发出一个 HTTP 调用。为了模拟一个长请求,来自 API 的响应被延迟了两秒钟。因此, http://127.0.0.1:8000 不仅要花两秒钟来呈现初始请求,还要呈现每个后续请求。虽然在最初的请求中,两秒钟的负载是可以接受的,但是对于后续的请求来说,这是完全不可接受的,因为数据是不变的。让我们通过使用 Django 的每视图缓存级别缓存整个视图来解决这个问题。
工作流程:
- 在初始请求中对 httpbin.org 进行完整的 HTTP 调用
- 缓存视图
- 随后的请求将从缓存中提取,绕过 HTTP 调用
- 一段时间(TTL)后使缓存失效
基线性能基准
在添加缓存之前,让我们使用 Apache Bench 快速运行一个负载测试以获得一个基准基线,从而大致了解我们的应用程序每秒可以处理多少请求。
Apache Bench 预装在 Mac 上。
如果您使用的是 Linux 系统,那么很可能它已经安装好并准备好运行了。如果没有,可以通过 APT (
apt-get install apache2-utils
)或者 YUM (yum install httpd-tools
)安装。Windows 用户需要下载并解压 Apache 二进制文件。
将 Gunicorn 添加到需求文件中:
终止 Django dev 服务器并安装 Gunicorn:
`(venv)$ pip install -r requirements.txt`
接下来,用 Gunicorn(和四个工人)提供 Django 应用程序,如下所示:
`(venv)$ gunicorn core.wsgi:application -w 4`
在新的终端窗口中,运行 Apache Bench:
`$ ab -n 100 -c 10 http://127.0.0.1:8000/`
这将模拟 10 个并发线程上的 100 个连接。一共有 100 个请求,每次 10 个。
记录每秒的请求数:
`Requests per second: 1.69 [#/sec] (mean)`
请记住,Django 调试工具栏会增加一些开销。一般来说,标杆管理很难做到完全正确。重要的是一致性。选择一个关注的指标,并在每个测试中使用相同的环境。
关闭 Gunicorn 服务器,重新启动 Django dev 服务器:
`(venv)$ python manage.py runserver`
至此,让我们看看如何缓存视图。
缓存视图
首先用@cache_page
装饰器装饰ApiCalls
视图,如下所示:
`import datetime
import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
@method_decorator(cache_page(60 * 5), name='dispatch') # NEW
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context`
因为我们使用的是基于类的视图,所以我们不能直接在类上放置装饰器,所以我们使用了一个method_decorator
并为 name 参数指定了dispatch
(作为要装饰的方法)。
本例中的缓存设置了五分钟的超时(或 TTL)。
或者,您可以在设置中这样设置:
`# Cache time to live is 5 minutes
CACHE_TTL = 60 * 5`
然后,回到视图中:
`import datetime
import requests
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
@method_decorator(cache_page(CACHE_TTL), name='dispatch')
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context`
接下来,让我们添加一个缓存后端。
里兹 vs Memcached
Memcached 和 Redis 是内存中的键值数据存储。它们易于使用,并针对高性能查找进行了优化。您可能看不出两者在性能或内存使用方面有什么不同。也就是说,Memcached 的配置稍微容易一些,因为它是为简单易用而设计的。另一方面,Redis 有更丰富的特性集,因此除了缓存之外,它还有更广泛的使用案例。例如,它通常用于存储用户会话或作为发布/订阅系统中的消息代理。由于它的灵活性,除非您已经投资了 Memcached,否则 Redis 是更好的解决方案。
有关这方面的更多信息,请查看这个堆栈溢出答案。
接下来,选择您的数据存储,让我们看看如何缓存视图。
备选案文 1:与 Django 重新谈判
下载并安装 Redis。
如果你用的是 Mac,我们建议用家酿安装 Redis:
安装完成后,在新的终端窗口中启动 Redis 服务器并确保它运行在默认端口 6379 上。当我们告诉 Django 如何与 Redis 通信时,端口号将非常重要。
对于 Django 使用 Redis 作为缓存后端,我们首先需要安装 django-redis 。
将其添加到 requirements.txt 文件中:
安装:
`(venv)$ pip install -r requirements.txt`
接下来,将自定义后端添加到 settings.py 文件中:
`CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}`
现在,当您再次运行服务器时,Redis 将被用作缓存后端:
`(venv)$ python manage.py runserver`
服务器启动并运行后,导航到 http://127.0.0.1:8000 。
第一个请求仍然需要大约两秒钟。刷新页面。页面应该几乎瞬间加载。看看你终端的加载时间。它应该接近于零:
想知道 Redis 内部的缓存数据是什么样的吗?
在新的终端窗口中以交互模式运行 Redis CLI:
您应该看到:
运行ping
以确保一切正常工作:
`127.0.0.1:6379> ping
PONG`
返回到设置文件。我们使用 Redis 数据库 1 号:'LOCATION': 'redis://127.0.0.1:6379/1',
。因此,运行select 1
来选择数据库,然后运行keys *
来查看所有键:
`127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) ":1:views.decorators.cache.cache_header..17abf5259517d604cc9599a00b7385d6.en-us.UTC"
2) ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"`
我们可以看到 Django 放入了一个 header 键和一个cache_page
键。
要查看实际缓存的数据,运行get
命令,将键作为参数:
`127.0.0.1:6379[1]> get ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"`
您应该会看到类似以下内容:
`"\x80\x05\x95D\x04\x00\x00\x00\x00\x00\x00\x8c\x18django.template.response\x94\x8c\x10TemplateResponse
\x94\x93\x94)\x81\x94}\x94(\x8c\x05using\x94N\x8c\b_headers\x94}\x94(\x8c\x0ccontent-type\x94\x8c\
x0cContent-Type\x94\x8c\x18text/html; charset=utf-8\x94\x86\x94\x8c\aexpires\x94\x8c\aExpires\x94\x8c\x1d
Fri, 01 May 2020 13:36:59 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0
bmax-age=300\x94\x86\x94u\x8c\x11_resource_closers\x94]\x94\x8c\x0e_handler_class\x94N\x8c\acookies
\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94)\x81\x94\x8c\x06closed\x94\x89\x8c
\x0e_reason_phrase\x94N\x8c\b_charset\x94N\x8c\n_container\x94]\x94B\xaf\x02\x00\x00
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Home</title>\n
<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\
"\n integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\"
crossorigin=\"anonymous\">\n\n</head>\n<body>\n<div class=\"container\">\n <div class=\"pt-3\">\n
<h1>Below is the result of the APICall</h1>\n </div>\n <div class=\"pt-3 pb-3\">\n
<a href=\"/\">\n <button type=\"button\" class=\"btn btn-success\">\n
Get new data\n </button>\n </a>\n </div>\n Results received!<br>\n
13:31:59\n</div>\n</body>\n</html>\x94a\x8c\x0c_is_rendered\x94\x88ub."`
完成后,退出交互式 CLI:
跳到“性能测试”部分。
选项 2:用 Django 实现 Memcached
首先将 pymemcache 添加到 requirements.txt 文件中:
安装依赖项:
`(venv)$ pip install -r requirements.txt`
接下来,我们需要更新 core/settings.py 中的设置,以启用 Memcached 后端:
`CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211',
}
}`
在这里,我们添加了 PyMemcacheCache 后端,并指出 Memcached 应该在本地机器的 localhost (127.0.0.1)端口 11211 上运行,这是 Memcached 的默认端口。
接下来,我们需要安装并运行 Memcached 守护进程。最简单的安装方法是通过软件包管理器,如 APT、YUM、Homebrew 或 Chocolatey,具体取决于您的操作系统:
`# linux
$ apt-get install memcached
$ yum install memcached
# mac
$ brew install memcached
# windows
$ choco install memcached`
然后,在端口 11211 上的另一个终端中运行它:
`$ memcached -p 11211
# test: telnet localhost 11211`
有关 Memcached 安装和配置的更多信息,请查看官方的 wiki 。
再次在我们的浏览器中导航至 http://127.0.0.1:8000 。第一个请求仍然需要整整两秒钟,但是所有后续请求都将利用缓存。因此,如果你刷新或按下“获取新数据按钮”,页面应该几乎立即加载。
你的终端的执行时间是怎样的?
性能测试
如果我们在 Django Debug Toolbar 中查看加载第一个请求和第二个(缓存的)请求所用的时间,它将类似于:
同样在调试工具栏中,您可以看到缓存操作:
再次旋转 Gunicorn 并重新运行性能测试:
`$ ab -n 100 -c 10 http://127.0.0.1:8000/`
每秒有哪些新请求?我机器上大概是 36!
结论
在本文中,我们研究了 Django 中不同的内置缓存选项以及不同级别的可用缓存。我们还详细介绍了如何使用 Django 的每视图缓存和 Memcached 以及 Redis 来缓存视图。
您可以在 cache-django-view repo 中找到 Memcached 和 Redis 这两个选项的最终代码。
--
一般来说,当由于数据库查询或 HTTP 调用的网络延迟而导致页面呈现缓慢时,您会希望使用缓存。
从这里开始,强烈推荐使用带有 Redis 和Per-view
类型自定义 Django 缓存后端。如果您需要更多的粒度和控制,因为不是模板上的所有数据对所有用户都是相同的,或者部分数据频繁更改,那么就跳到模板片段缓存或低级缓存 API。
--
Django 缓存文章:
用芹菜和码头工人处理 Django 的定期任务
当您构建和扩展 Django 应用程序时,您不可避免地需要定期在后台自动运行某些任务。
一些例子:
- 生成定期报告
- 清除缓存
- 发送批量电子邮件通知
- 运行夜间维护作业
这是构建和扩展不属于 Django 核心的 web 应用程序所需的少数功能之一。幸运的是,芹菜提供了一个强大的解决方案,它相当容易实现,叫做芹菜击败。
在接下来的文章中,我们将向您展示如何使用 Docker 设置 Django、Celery 和 Redis,以便使用 Celery Beat 定期运行自定义的 Django 管理命令。
姜戈+芹菜系列:
- 与 Django 和 Celery 的异步任务
- 在 Django 用芹菜和 Docker 处理周期性任务(本文!)
- 自动重试失败的芹菜任务
- 处理芹菜和数据库事务
目标
学完本教程后,您应该能够:
- 集装箱化姜戈、Celery,并与码头工重归于好
- 将芹菜集成到 Django 应用程序中并创建任务
- 编写一个定制的 Django 管理命令
- 安排一个定制的 Django 管理命令通过 Celery Beat 定期运行
项目设置
从 django-celery-beat repo 中克隆出基地项目,然后检查基地分支:
`$ git clone https://github.com/testdrivenio/django-celery-beat --branch base --single-branch
$ cd django-celery-beat`
由于我们总共需要管理四个进程(Django、Redis、worker 和 scheduler),我们将使用 Docker 来简化我们的工作流,方法是将它们连接起来,以便它们都可以通过一个命令从一个终端窗口运行。
从项目根目录,创建映像并启动 Docker 容器:
`$ docker-compose up -d --build`
接下来,应用迁移:
`$ docker-compose exec web python manage.py migrate`
一旦构建完成,导航到 http://localhost:1337 以确保应用程序按预期工作。您应该会看到以下文本:
在继续之前,快速浏览一下项目结构:
`├── .gitignore
├── docker-compose.yml
└── project
├── Dockerfile
├── core
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── entrypoint.sh
├── manage.py
├── orders
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── products.json
├── requirements.txt
└── templates
└── orders
└── order_list.html`
想学习如何构建这个项目吗?查看 Postgres、Gunicorn 和 Nginx 的博客文章。
Celery and Redis(合唱团)
现在,我们需要为芹菜、芹菜泥和 Redis 添加容器。
我们首先将依赖项添加到 requirements.txt 文件中:
`Django==3.2.4
celery==5.1.2
redis==3.5.3`
接下来,将以下内容添加到 docker-compose.yml 文件的末尾:
`redis: image: redis:alpine celery: build: ./project command: celery -A core worker -l info volumes: - ./project/:/usr/src/app/ environment: - DEBUG=1 - SECRET_KEY=dbaa1_i7%*[[email protected]](/cdn-cgi/l/email-protection)(-a_r([[email protected]](/cdn-cgi/l/email-protection)%m - DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] depends_on: - redis celery-beat: build: ./project command: celery -A core beat -l info volumes: - ./project/:/usr/src/app/ environment: - DEBUG=1 - SECRET_KEY=dbaa1_i7%*[[email protected]](/cdn-cgi/l/email-protection)(-a_r([[email protected]](/cdn-cgi/l/email-protection)%m - DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] depends_on: - redis`
我们还需要更新 web 服务的depends_on
部分:
`web: build: ./project command: python manage.py runserver 0.0.0.0:8000 volumes: - ./project/:/usr/src/app/ ports: - 1337:8000 environment: - DEBUG=1 - SECRET_KEY=dbaa1_i7%*[[email protected]](/cdn-cgi/l/email-protection)(-a_r([[email protected]](/cdn-cgi/l/email-protection)%m - DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] depends_on: - redis # NEW`
完整的 docker-compose.yml 文件现在应该是这样的:
`version: '3.8' services: web: build: ./project command: python manage.py runserver 0.0.0.0:8000 volumes: - ./project/:/usr/src/app/ ports: - 1337:8000 environment: - DEBUG=1 - SECRET_KEY=dbaa1_i7%*[[email protected]](/cdn-cgi/l/email-protection)(-a_r([[email protected]](/cdn-cgi/l/email-protection)%m - DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] depends_on: - redis redis: image: redis:alpine celery: build: ./project command: celery -A core worker -l info volumes: - ./project/:/usr/src/app/ environment: - DEBUG=1 - SECRET_KEY=dbaa1_i7%*[[email protected]](/cdn-cgi/l/email-protection)(-a_r([[email protected]](/cdn-cgi/l/email-protection)%m - DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] depends_on: - redis celery-beat: build: ./project command: celery -A core beat -l info volumes: - ./project/:/usr/src/app/ environment: - DEBUG=1 - SECRET_KEY=dbaa1_i7%*[[email protected]](/cdn-cgi/l/email-protection)(-a_r([[email protected]](/cdn-cgi/l/email-protection)%m - DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] depends_on: - redis`
在构建新容器之前,我们需要在 Django 应用程序中配置 Celery。
芹菜配置
设置
在“core”目录中,创建一个 celery.py 文件,并添加以下代码:
`import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
app = Celery("core")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()`
这里发生了什么事?
- 首先,我们为环境变量
DJANGO_SETTINGS_MODULE
设置一个默认值,以便芹菜知道如何找到 Django 项目。 - 接下来,我们创建了一个名为
core
的新 Celery 实例,并将值赋给一个名为app
的变量。 - 然后我们从
django.conf
的 settings 对象中加载 celery 配置值。我们使用了namespace="CELERY"
来防止与其他 Django 场景的冲突。换句话说,芹菜的所有配置设置都必须以CELERY_
为前缀。 - 最后,
app.autodiscover_tasks()
告诉 Celery 从settings.INSTALLED_APPS
中定义的应用程序中寻找 Celery 任务。
将以下代码添加到 core/init。py :
`from .celery import app as celery_app
__all__ = ("celery_app",)`
最后,用以下 Celery 设置更新 core/settings.py 文件,以便它可以连接到 Redis:
`CELERY_BROKER_URL = "redis://redis:6379"
CELERY_RESULT_BACKEND = "redis://redis:6379"`
构建新容器以确保一切正常工作:
`$ docker-compose up -d --build`
查看每个服务的日志,看它们是否准备好,没有错误:
`$ docker-compose logs 'web'
$ docker-compose logs 'celery'
$ docker-compose logs 'celery-beat'
$ docker-compose logs 'redis'`
如果一切顺利,我们现在有四个容器,每个都有不同的服务。
现在我们准备创建一个示例任务,看看它是否能正常工作。
创建任务
创建一个名为 core/tasks.py 的新文件,并为一个刚刚登录到控制台的示例任务添加以下代码:
`from celery import shared_task
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
@shared_task
def sample_task():
logger.info("The sample task just ran.")`
计划任务
在您的 settings.py 文件的末尾,添加以下代码来调度sample_task
每分钟运行一次,使用芹菜节拍:
`CELERY_BEAT_SCHEDULE = {
"sample_task": {
"task": "core.tasks.sample_task",
"schedule": crontab(minute="*/1"),
},
}`
这里,我们使用 CELERY_BEAT_SCHEDULE 设置定义了一个周期性任务。我们给这个任务命名为sample_task
,然后声明了两个设置:
task
声明要运行的任务。schedule
设置任务运行的时间间隔。这可以是整数、时间增量或 crontab。我们为我们的任务使用了一个 crontab 模式,告诉它每分钟运行一次。你可以在这里找到更多关于芹菜的时间安排的信息。
确保添加导入:
`from celery.schedules import crontab
import core.tasks`
重新启动容器以获取新设置:
`$ docker-compose up -d --build`
完成后,看看容器中的芹菜段:
`$ docker-compose logs -f 'celery'`
您应该会看到类似如下的内容:
`celery_1 | -------------- [queues]
celery_1 | .> celery exchange=celery(direct) key=celery
celery_1 |
celery_1 |
celery_1 | [tasks]
celery_1 | . core.tasks.sample_task`
我们可以看到,芹菜拿起了我们的样本任务,core.tasks.sample_task
。
每分钟您都应该在日志中看到一行,以“sample task 刚刚运行”结束:
`celery_1 | [2021-07-01 03:06:00,003: INFO/MainProcess]
Task core.tasks.sample_task[b8041b6c-bf9b-47ce-ab00-c37c1e837bc7] received
celery_1 | [2021-07-01 03:06:00,004: INFO/ForkPoolWorker-8]
core.tasks.sample_task[b8041b6c-bf9b-47ce-ab00-c37c1e837bc7]:
The sample task just ran.`
自定义 Django 管理命令
Django 提供了个的内置django-admin
命令,比如:
migrate
startproject
startapp
dumpdata
makemigrations
除了内置命令,Django 还让我们可以选择创建自己的定制命令:
自定义管理命令对于运行独立脚本或从 UNIX crontab 或 Windows 计划任务控制面板定期执行的脚本特别有用。
因此,我们将首先配置一个新命令,然后使用 Celery Beat 自动运行它。
首先创建一个名为orders/management/commands/my _ custom _ command . py的新文件。然后,添加运行它所需的最少代码:
`from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
help = "A description of the command"
def handle(self, *args, **options):
pass`
BaseCommand
有一些方法可以被覆盖,但是唯一需要的方法是handle
。handle
是自定义命令的入口点。换句话说,当我们运行命令时,这个方法被调用。
为了测试,我们通常只添加一个快速打印语句。然而,根据 Django 文档,建议使用stdout.write
:
当您使用管理命令并希望提供控制台输出时,您应该写入 self.stdout 和 self.stderr,而不是直接打印到 stdout 和 stderr。通过使用这些代理,测试您的定制命令变得更加容易。还要注意,您不需要用换行符结束消息,它会自动添加,除非您指定了结束参数。
所以,添加一个self.stdout.write
命令:
`from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
help = "A description of the command"
def handle(self, *args, **options):
self.stdout.write("My sample command just ran.") # NEW`
要进行测试,从命令行运行:
`$ docker-compose exec web python manage.py my_custom_command`
您应该看到:
`My sample command just ran.`
有了那个,让我们把一切都绑在一起!
使用芹菜节拍安排自定义命令
既然我们已经启动了容器,测试了我们可以安排一个任务定期运行,并编写了一个定制的 Django Admin 示例命令,那么是时候配置 Celery Beat 定期运行这个定制命令了。
设置
在这个项目中,我们有一个非常基本的应用程序,叫做订单。包含Product
和Order
两个型号。让我们创建一个自定义命令,发送当天已确认订单的电子邮件报告。
首先,我们将通过这个项目中包含的 fixture 向数据库添加一些产品和订单:
`$ docker-compose exec web python manage.py loaddata products.json`
接下来,通过 Django 管理界面添加一些样本订单。为此,首先创建一个超级用户:
`$ docker-compose exec web python manage.py createsuperuser`
出现提示时,填写用户名、电子邮件和密码。然后在网络浏览器中导航至http://127 . 0 . 0 . 1:1337/admin。使用您刚刚创建的超级用户登录并创建几个订单。确保至少有一个人有今天的confirmed_date
。
让我们为电子邮件报告创建一个新的自定义命令。
创建一个名为orders/management/commands/email _ report . py的文件:
`from datetime import timedelta, time, datetime
from django.core.mail import mail_admins
from django.core.management import BaseCommand
from django.utils import timezone
from django.utils.timezone import make_aware
from orders.models import Order
today = timezone.now()
tomorrow = today + timedelta(1)
today_start = make_aware(datetime.combine(today, time()))
today_end = make_aware(datetime.combine(tomorrow, time()))
class Command(BaseCommand):
help = "Send Today's Orders Report to Admins"
def handle(self, *args, **options):
orders = Order.objects.filter(confirmed_date__range=(today_start, today_end))
if orders:
message = ""
for order in orders:
message += f"{order} \n"
subject = (
f"Order Report for {today_start.strftime('%Y-%m-%d')} "
f"to {today_end.strftime('%Y-%m-%d')}"
)
mail_admins(subject=subject, message=message, html_message=None)
self.stdout.write("E-mail Report was sent.")
else:
self.stdout.write("No orders confirmed today.")`
在代码中,我们在数据库中查询今天的confirmed_date
订单,将订单组合成一条消息作为电子邮件正文,并使用 Django 的内置mail_admins
命令将电子邮件发送给管理员。
添加一个虚拟的管理员电子邮件,并将EMAIL_BACKEND
设置为使用控制台后端,这样电子邮件就被发送到 stdout,在设置文件中:
现在应该可以从终端运行我们的新命令了。
`$ docker-compose exec web python manage.py email_report`
输出应该如下所示:
`Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: [Django] Order Report for 2021-07-01 to 2021-07-02
From: [[email protected]](/cdn-cgi/l/email-protection)
To: [[email protected]](/cdn-cgi/l/email-protection)
Date: Thu, 01 Jul 2021 12:15:50 -0000
Message-ID: <162514175053[[email protected]](/cdn-cgi/l/email-protection)>
Order: 3947963f-1860-44d1-9b9a-4648fed04581 - product: Coffee
Order: ff449e6e-3dfd-48a8-9d5c-79a145d08253 - product: Rice
-------------------------------------------------------------------------------
E-mail Report was sent.`
芹菜搅打
我们现在需要创建一个定期任务来每天运行这个命令。
向 core/tasks.py 添加新任务:
`from celery import shared_task
from celery.utils.log import get_task_logger
from django.core.management import call_command # NEW
logger = get_task_logger(__name__)
@shared_task
def sample_task():
logger.info("The sample task just ran.")
# NEW
@shared_task
def send_email_report():
call_command("email_report", )`
因此,首先我们添加了一个call_command
导入,用于以编程方式调用 django-admin 命令。在新任务中,我们使用带有自定义命令名称的call_command
作为参数。
要调度该任务,请打开 core/settings.py 文件,并更新CELERY_BEAT_SCHEDULE
设置以包含新任务:
`CELERY_BEAT_SCHEDULE = {
"sample_task": {
"task": "core.tasks.sample_task",
"schedule": crontab(minute="*/1"),
},
"send_email_report": {
"task": "core.tasks.send_email_report",
"schedule": crontab(hour="*/1"),
},
}`
这里我们向CELERY_BEAT_SCHEDULE
添加了一个名为send_email_report
的新条目。正如我们对上一个任务所做的那样,我们声明了应该运行哪个任务——例如,core.tasks.send_email_report
——并使用 crontab 模式来设置循环。
重新启动容器以确保新设置生效:
`$ docker-compose up -d --build`
打开与celery
服务相关的日志:
`$ docker-compose logs -f 'celery'`
您应该会看到列出的send_email_report
:
`celery_1 | -------------- [queues]
celery_1 | .> celery exchange=celery(direct) key=celery
celery_1 |
celery_1 |
celery_1 | [tasks]
celery_1 | . core.tasks.sample_task
celery_1 | . core.tasks.send_email_report`
大约一分钟后,您应该看到电子邮件报告已发送:
`celery_1 | [2021-07-01 12:22:00,071: WARNING/ForkPoolWorker-8] Content-Type: text/plain; charset="utf-8"
celery_1 | MIME-Version: 1.0
celery_1 | Content-Transfer-Encoding: 7bit
celery_1 | Subject: [Django] Order Report for 2021-07-01 to 2021-07-02
celery_1 | From: [[email protected]](/cdn-cgi/l/email-protection)
celery_1 | To: [[email protected]](/cdn-cgi/l/email-protection)
celery_1 | Date: Thu, 01 Jul 2021 12:22:00 -0000
celery_1 | Message-ID: <162514212006[[email protected]](/cdn-cgi/l/email-protection)>
celery_1 |
celery_1 | Order: 3947963f-1860-44d1-9b9a-4648fed04581 - product: Coffee
celery_1 | Order: ff449e6e-3dfd-48a8-9d5c-79a145d08253 - product: Rice
celery_1 |
celery_1 |
celery_1 | [2021-07-01 12:22:00,071: WARNING/ForkPoolWorker-8] -------------------------------------------------------------------------------
celery_1 | [2021-07-01 12:22:00,071: WARNING/ForkPoolWorker-8]
celery_1 |
celery_1 | [2021-07-01 12:22:00,071: WARNING/ForkPoolWorker-8] E-mail Report was sent.`
结论
在本文中,我们指导您为芹菜、芹菜段和 Redis 设置 Docker 容器。然后,我们展示了如何创建一个定制的 Django 管理命令和一个使用 Celery Beat 自动运行该命令的周期性任务。
想要更多吗?
- 设置 Flower 来监控和管理芹菜作业和工人
- 用单元测试和集成测试来测试芹菜任务
从回购中抓取代码。
姜戈+芹菜系列:
- 与 Django 和 Celery 的异步任务
- 在 Django 用芹菜和 Docker 处理周期性任务(本文!)
- 自动重试失败的芹菜任务
- 处理芹菜和数据库事务
Django 频道简介
在本教程中,我们将使用 Django 通道构建一个实时聊天应用程序,重点关注如何将 Django 与 Django 通道集成。
为什么是另一款聊天应用?嗯,聊天 app 是最容易展现渠道力量的方式。也就是说,本教程通过实现多种请求类型、消息/聊天室持久性和私有(一对一)消息传递超越了基础知识。完成教程后,您将能够构建实时应用程序。
什么是 Django 频道?
Django Channels (或者仅仅是 Channels)扩展了 Django 的内置功能,允许 Django 项目不仅处理 HTTP,还处理需要长时间运行连接的协议,例如 WebSockets、MQTT (IoT)、聊天机器人、无线电和其他实时应用程序。除此之外,它还支持 Django 的许多核心特性,比如身份验证和会话。
基本的通道设置如下所示:
同步与异步
由于 Channels 和 Django 之间的差异,我们必须频繁地在同步和异步代码执行之间切换。例如,Django 数据库需要使用同步代码访问,而 Channels 通道层需要使用异步代码访问。
在这两者之间切换最简单的方法是使用内置的 Django asgiref ( asgrief.sync
)函数:
sync_to_async
-获取一个同步函数并返回一个包装它的异步函数async_to_sync
-获取异步函数并返回同步函数
现在还不要担心这个,我们将在教程的后面展示一个实际的例子。
项目设置
同样,我们将构建一个聊天应用程序。该应用程序将有多个房间,Django 认证的用户可以聊天。每个房间都有一个当前连接用户的列表。我们还将实现私有的一对一消息传递。
Django 项目设置
首先创建一个新目录,并建立一个新的 Django 项目:
`$ mkdir django-channels-example && cd django-channels-example
$ python3.9 -m venv env
$ source env/bin/activate
(env)$ pip install django==4.0
(env)$ django-admin startproject core .`
之后,创建一个名为chat
的新 Django 应用程序:
`(env)$ python manage.py startapp chat`
在INSTALLED_APPS
下的 core/settings.py 中注册 app:
`# core/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'chat.apps.ChatConfig', # new
]`
创建数据库模型
接下来,让我们在 chat/models.py 中创建两个 Django 模型Room
和Message
:
`# chat/models.py
from django.contrib.auth.models import User
from django.db import models
class Room(models.Model):
name = models.CharField(max_length=128)
online = models.ManyToManyField(to=User, blank=True)
def get_online_count(self):
return self.online.count()
def join(self, user):
self.online.add(user)
self.save()
def leave(self, user):
self.online.remove(user)
self.save()
def __str__(self):
return f'{self.name} ({self.get_online_count()})'
class Message(models.Model):
user = models.ForeignKey(to=User, on_delete=models.CASCADE)
room = models.ForeignKey(to=Room, on_delete=models.CASCADE)
content = models.CharField(max_length=512)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.user.username}: {self.content} [{self.timestamp}]'`
注意事项:
Room
代表聊天室。它包含一个online
字段,用于跟踪用户何时连接和断开聊天室。Message
代表发送到聊天室的消息。我们将使用这个模型来存储聊天中发送的所有消息。
运行makemigrations
和migrate
命令来同步数据库:
`(env)$ python manage.py makemigrations
(env)$ python manage.py migrate`
在 chat/admin.py 中注册模型,以便可以从 Django 管理面板访问它们:
`# chat/admin.py
from django.contrib import admin
from chat.models import Room, Message
admin.site.register(Room)
admin.site.register(Message)`
视图和 URL
web 应用程序将有以下两个 URL:
/chat/
-聊天室选择器/chat/<ROOM_NAME>/
-聊天室
将以下视图添加到 chat/views.py 中:
`# chat/views.py
from django.shortcuts import render
from chat.models import Room
def index_view(request):
return render(request, 'index.html', {
'rooms': Room.objects.all(),
})
def room_view(request, room_name):
chat_room, created = Room.objects.get_or_create(name=room_name)
return render(request, 'room.html', {
'room': chat_room,
})`
在chat
应用内创建一个 urls.py 文件:
`# chat/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index_view, name='chat-index'),
path('<str:room_name>/', views.room_view, name='chat-room'),
]`
用chat
应用程序更新项目级的 urls.py 文件:
`# core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('chat/', include('chat.urls')), # new
path('admin/', admin.site.urls),
]`
模板和静态文件
在“聊天”中名为“模板”的新文件夹中创建一个index.html文件:
`<!-- chat/templates/index.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>django-channels-chat</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/js/bootstrap.min.js"></script>
<style> #roomSelect { height: 300px; } </style>
</head>
<body>
<div class="container mt-3 p-5">
<h2>django-channels-chat</h2>
<div class="row">
<div class="col-12 col-md-8">
<div class="mb-2">
<label for="roomInput">Enter a room name to connect to it:</label>
<input type="text" class="form-control" id="roomInput" placeholder="Room name">
<small id="roomInputHelp" class="form-text text-muted">If the room doesn't exist yet, it will be created for you.</small>
</div>
<button type="button" id="roomConnect" class="btn btn-success">Connect</button>
</div>
<div class="col-12 col-md-4">
<label for="roomSelect">Active rooms</label>
<select multiple class="form-control" id="roomSelect">
{% for room in rooms %}
<option>{{ room }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<script src="{% static 'index.js' %}"></script>
</body>
</html>`
接下来,在同一个文件夹中添加room.html:
`<!-- chat/templates/room.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>django-channels-chat</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/js/bootstrap.min.js"></script>
<style> #chatLog { height: 300px; background-color: #FFFFFF; resize: none; } #onlineUsersSelector { height: 300px; } </style>
</head>
<body>
<div class="container mt-3 p-5">
<h2>django-channels-chat</h2>
<div class="row">
<div class="col-12 col-md-8">
<div class="mb-2">
<label for="chatLog">Room: #{{ room.name }}</label>
<textarea class="form-control" id="chatLog" readonly></textarea>
</div>
<div class="input-group">
<input type="text" class="form-control" id="chatMessageInput" placeholder="Enter your chat message">
<div class="input-group-append">
<button class="btn btn-success" id="chatMessageSend" type="button">Send</button>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<label for="onlineUsers">Online users</label>
<select multiple class="form-control" id="onlineUsersSelector">
</select>
</div>
</div>
{{ room.name|json_script:"roomName" }}
</div>
<script src="{% static 'room.js' %}"></script>
</body>
</html>`
为了使我们的代码更具可读性,我们将 JavaScript 代码包含在单独的文件中——分别是 index.js 和 room.js 。因为我们无法在 JavaScript 中访问 Django 上下文,所以我们可以使用 json_script 模板标签来存储room.name
,然后在 JavaScript 文件中获取它。
在“聊天”里面,创建一个名为“静态”的文件夹。然后,在“静态”内部,创建一个 index.js 和一个 room.js 文件。
index.js :
`// chat/static/index.js console.log("Sanity check from index.js."); // focus 'roomInput' when user opens the page document.querySelector("#roomInput").focus(); // submit if the user presses the enter key document.querySelector("#roomInput").onkeyup = function(e) { if (e.keyCode === 13) { // enter key document.querySelector("#roomConnect").click(); } }; // redirect to '/room/<roomInput>/' document.querySelector("#roomConnect").onclick = function() { let roomName = document.querySelector("#roomInput").value; window.location.pathname = "chat/" + roomName + "/"; } // redirect to '/room/<roomSelect>/' document.querySelector("#roomSelect").onchange = function() { let roomName = document.querySelector("#roomSelect").value.split(" (")[0]; window.location.pathname = "chat/" + roomName + "/"; }`
room.js :
`// chat/static/room.js console.log("Sanity check from room.js."); const roomName = JSON.parse(document.getElementById('roomName').textContent); let chatLog = document.querySelector("#chatLog"); let chatMessageInput = document.querySelector("#chatMessageInput"); let chatMessageSend = document.querySelector("#chatMessageSend"); let onlineUsersSelector = document.querySelector("#onlineUsersSelector"); // adds a new option to 'onlineUsersSelector' function onlineUsersSelectorAdd(value) { if (document.querySelector("option[value='" + value + "']")) return; let newOption = document.createElement("option"); newOption.value = value; newOption.innerHTML = value; onlineUsersSelector.appendChild(newOption); } // removes an option from 'onlineUsersSelector' function onlineUsersSelectorRemove(value) { let oldOption = document.querySelector("option[value='" + value + "']"); if (oldOption !== null) oldOption.remove(); } // focus 'chatMessageInput' when user opens the page chatMessageInput.focus(); // submit if the user presses the enter key chatMessageInput.onkeyup = function(e) { if (e.keyCode === 13) { // enter key chatMessageSend.click(); } }; // clear the 'chatMessageInput' and forward the message chatMessageSend.onclick = function() { if (chatMessageInput.value.length === 0) return; // TODO: forward the message to the WebSocket chatMessageInput.value = ""; };`
您最终的“聊天”应用程序目录结构应该如下所示:
`chat
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── __init__.py
├── models.py
├── static
│ ├── index.js
│ └── room.js
├── templates
│ ├── index.html
│ └── room.html
├── tests.py
├── urls.py
└── views.py`
测试
有了基本的项目设置,让我们在浏览器中进行测试。
启动 Django 开发服务器:
`(env)$ python manage.py runserver`
导航到http://localhost:8000/chat/。您将看到房间选择器:
要确保静态文件配置正确,请打开“开发人员控制台”。您应该看到健全性检查:
`Sanity check from index.js.`
接下来,在“房间名称”文本输入中输入一些内容,然后按回车键。您将被重定向到房间:
这些只是静态模板。我们稍后将为聊天和在线用户实现该功能。
添加频道
接下来,让我们连接 Django 频道。
首先安装软件包:
`(env)$ pip install channels==3.0.4`
然后,将channels
添加到 core/settings.py 内的INSTALLED_APPS
中:
`# core/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'chat.apps.ChatConfig',
'channels', # new
]`
因为我们将使用 WebSockets 而不是 HTTP 从客户端到服务器进行通信,所以我们需要用 core/asgi.py 中的 ProtocolTypeRouter 包装我们的 ASGI 配置:
`# core/asgi.py
import os
from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = ProtocolTypeRouter({
'http': get_asgi_application(),
})`
该路由器将根据使用的协议将流量路由到 web 应用程序的不同部分。
Django 版本<= 2.2 don't have built-in ASGI support. In order to get 【 running with older Django versions please refer to the 官方安装指南。
接下来,我们需要让 Django 知道我们的 ASGI 应用程序的位置。将以下内容添加到您的 core/settings.py 文件中,就在WSGI_APPLICATION
设置的下面:
`# core/settings.py
WSGI_APPLICATION = 'core.wsgi.application'
ASGI_APPLICATION = 'core.asgi.application' # new`
当您现在运行开发服务器时,您将看到 Channels 正在被使用:
`Starting ASGI/Channels version 3.0.4 development server at http://127.0.0.1:8000/`
添加通道层
一个通道层是一种通信系统,它允许我们的应用程序的多个部分交换消息,而不需要在数据库中穿梭所有的消息或事件。
我们需要一个通道层来让消费者(我们将在下一步中实现)能够相互交谈。
虽然我们可以使用 InMemoryChannelLayer 层,因为我们处于开发模式,但我们将使用一个生产就绪层, RedisChannelLayer 。
由于这一层需要 Redis ,运行下面的命令让它启动并运行 Docker :
`(env)$ docker run -p 6379:6379 -d redis:5`
该命令下载映像并在端口6379
上启动 Redis Docker 容器。
如果不想用 Docker,可以直接从官网下载 Redis。
要从 Django 连接到 Redis,我们需要安装一个名为 channels_redis 的附加包:
`(env)$ pip install channels_redis==3.3.1`
之后,在 core/settings.py 中配置图层如下:
`# core/settings.py
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}`
这里,我们让 channels_redis 知道 redis 服务器的位置。
要测试一切是否按预期运行,请打开 Django shell:
`(env)$ python manage.py shell`
然后运行:
`>>> import channels.layers
>>> channel_layer = channels.layers.get_channel_layer()
>>>
>>> from asgiref.sync import async_to_sync
>>> async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'})
>>> async_to_sync(channel_layer.receive)('test_channel')
{'type': 'hello'}`
这里,我们使用 core/settings.py 中定义的设置连接到通道层。然后,我们使用channel_layer.send
向test_channel
组发送消息,使用channel_layer.receive
读取发送给同一个组的所有消息。
注意,我们在
async_to_sync
中包装了所有的函数调用,因为通道层是异步的。
输入quit()
退出外壳。
添加频道消费者
一个消费者是渠道代码的基本单位。它们是由事件驱动的小型 ASGI 应用程序。它们类似于 Django 的观点。然而,与 Django 视图不同,消费者在默认情况下是长期运行的。一个 Django 项目可以有多个消费者,这些消费者使用通道路由进行组合(我们将在下一节中讨论)。
每个使用者都有自己的作用域,它是关于单个传入连接的一组细节。它们包含协议类型、路径、报头、路由参数、用户代理等数据。
在“聊天”中创建一个名为 consumers.py 的新文件:
`# chat/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
from .models import Room
class ChatConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
self.room_name = None
self.room_group_name = None
self.room = None
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
self.room = Room.objects.get(name=self.room_name)
# connection has to be accepted
self.accept()
# join the room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name,
)
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name,
)
def receive(self, text_data=None, bytes_data=None):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# send chat message event to the room
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
}
)
def chat_message(self, event):
self.send(text_data=json.dumps(event))`
这里,我们创建了一个ChatConsumer
,它继承了 WebsocketConsumer 。WebsocketConsumer
提供了三种方法,connect()
、disconnect()
、receive()
:
- 在
connect()
内部,我们调用了accept()
来接受连接。之后,我们将用户添加到通道层组。 - 在
disconnect()
中,我们从频道层组中移除了用户。 - 在
receive()
中,我们将数据解析为 JSON 并提取出message
。然后,我们使用group_send
将message
转发给chat_message
。
当使用通道层的
group_send
时,您的消费者必须为您使用的每个 JSON 消息type
准备一个方法。在我们的情况下,type
等于chat_message
。因此,我们添加了一个叫做chat_message
的方法。如果你在你的消息类型中使用点,当寻找一个方法时,通道会自动将它们转换成下划线——例如,
chat.message
会变成chat_message
。
由于WebsocketConsumer
是一个同步消费者,我们在处理通道层时必须调用async_to_sync
。我们决定使用同步消费者,因为聊天应用程序与 Django 紧密相连(默认情况下是同步的)。换句话说,我们不会因为使用异步消费者而获得性能提升。
默认情况下,您应该使用同步消费者。此外,只有在你完全确定你正在做的事情将从异步处理中受益(例如,可以并行完成的长时间运行的任务)并且你只使用异步本地库的情况下,才使用异步消费者。
添加通道路由
Channels 提供了不同的路由类,允许我们组合和堆叠消费者。它们类似于 Django 的 URL。
向“聊天”添加一个 routing.py 文件:
`# chat/routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]`
在 core/asgi.py 中注册 routing.py 文件:
`# core/asgi.py
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': URLRouter(
chat.routing.websocket_urlpatterns
),
})`
WebSockets(前端)
为了从前端与通道通信,我们将使用 WebSocket API 。
WebSockets 非常容易使用。首先,您需要通过提供一个url
来建立连接,然后您可以监听以下事件:
onopen
-当 WebSocket 连接建立时调用onclose
-当 WebSocket 连接被破坏时调用onmessage
-当 WebSocket 收到消息时调用onerror
-当 WebSocket 遇到错误时调用
要将 WebSockets 集成到应用程序中,请将以下内容添加到 room.js 的底部:
`// chat/static/room.js let chatSocket = null; function connect() { chatSocket = new WebSocket("ws://" + window.location.host + "/ws/chat/" + roomName + "/"); chatSocket.onopen = function(e) { console.log("Successfully connected to the WebSocket."); } chatSocket.onclose = function(e) { console.log("WebSocket connection closed unexpectedly. Trying to reconnect in 2s..."); setTimeout(function() { console.log("Reconnecting..."); connect(); }, 2000); }; chatSocket.onmessage = function(e) { const data = JSON.parse(e.data); console.log(data); switch (data.type) { case "chat_message": chatLog.value += data.message + "\n"; break; default: console.error("Unknown message type!"); break; } // scroll 'chatLog' to the bottom chatLog.scrollTop = chatLog.scrollHeight; }; chatSocket.onerror = function(err) { console.log("WebSocket encountered an error: " + err.message); console.log("Closing the socket."); chatSocket.close(); } } connect();`
建立 WebSocket 连接后,在onmessage
事件中,我们根据data.type
确定消息类型。请注意我们如何将 WebSocket 包装在connect()
方法中,以便在连接断开时能够重新建立连接。
最后,将chatMessageSend.onclickForm
中的 TODO 更改为以下内容:
`// chat/static/room.js chatSocket.send(JSON.stringify({ "message": chatMessageInput.value, }));`
完整的处理程序现在应该如下所示:
`// chat/static/room.js chatMessageSend.onclick = function() { if (chatMessageInput.value.length === 0) return; chatSocket.send(JSON.stringify({ "message": chatMessageInput.value, })); chatMessageInput.value = ""; };`
聊天的第一个版本完成了。
要进行测试,请运行开发服务器。然后,打开两个私人/匿名浏览器窗口,并在每个窗口中导航到http://localhost:8000/chat/default/。您应该能够发送一条消息:
基本功能到此为止。接下来,我们来看看身份认证。
证明
后端
Channels 附带了一个用于 Django 会话和名为AuthMiddlewareStack
的认证管理的内置类。
要使用它,我们唯一要做的就是把URLRouter
包在 core/asgi.py 里面,就像这样:
`# core/asgi.py
import os
from channels.auth import AuthMiddlewareStack # new import
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack( # new
URLRouter(
chat.routing.websocket_urlpatterns
)
), # new
})`
现在,只要经过身份验证的客户端加入,用户对象就会被添加到该范围。可以这样访问它:
`user = self.scope['user']`
如果您想使用前端 JavaScript 框架(如 Angular、React 或 Vue)运行通道,您必须使用不同的认证系统(例如令牌认证)。如果您想了解如何对通道使用令牌认证,请查看以下课程:
让我们修改ChatConsumer
来阻止未通过身份验证的用户说话,并在消息中显示用户的用户名。
将 chat/consumers.py 更改如下:
`# chat/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
from .models import Room, Message # new import
class ChatConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
self.room_name = None
self.room_group_name = None
self.room = None
self.user = None # new
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
self.room = Room.objects.get(name=self.room_name)
self.user = self.scope['user'] # new
# connection has to be accepted
self.accept()
# join the room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name,
)
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name,
)
def receive(self, text_data=None, bytes_data=None):
text_data_json = json.loads(text_data)
message = text_data_json['message']
if not self.user.is_authenticated: # new
return # new
# send chat message event to the room
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'user': self.user.username, # new
'message': message,
}
)
Message.objects.create(user=self.user, room=self.room, content=message) # new
def chat_message(self, event):
self.send(text_data=json.dumps(event))`
前端
接下来,让我们修改 room.js 来显示用户的用户名。在chatSocket.onMessage
中,添加以下内容:
`// chat/static/room.js chatSocket.onmessage = function(e) { const data = JSON.parse(e.data); console.log(data); switch (data.type) { case "chat_message": chatLog.value += data.user + ": " + data.message + "\n"; // new break; default: console.error("Unknown message type!"); break; } // scroll 'chatLog' to the bottom chatLog.scrollTop = chatLog.scrollHeight; };`
测试
创建一个超级用户,用于测试:
`(env)$ python manage.py createsuperuser`
运行服务器:
`(env)$ python manage.py runserver`
打开浏览器,使用 Django admin 登录到http://localhost:8000/admin。
然后导航到http://localhost:8000/chat/default。测试一下!
注销 Django 管理员。导航到http://localhost:8000/chat/default。尝试发布消息时会发生什么?
用户消息
接下来,我们将添加以下三种消息类型:
user_list
-发送给新加入的用户(data.users
=在线用户列表)user_join
-当用户加入聊天室时发送user_leave
-当用户离开聊天室时发送
后端
在ChatConsumer
中的connect
方法的末尾添加:
`# chat/consumers.py
def connect(self):
# ...
# send the user list to the newly joined user
self.send(json.dumps({
'type': 'user_list',
'users': [user.username for user in self.room.online.all()],
}))
if self.user.is_authenticated:
# send the join event to the room
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'user_join',
'user': self.user.username,
}
)
self.room.online.add(self.user)`
在ChatConsumer
中的disconnect
方法的末尾添加:
`# chat/consumers.py
def disconnect(self, close_code):
# ...
if self.user.is_authenticated:
# send the leave event to the room
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'user_leave',
'user': self.user.username,
}
)
self.room.online.remove(self.user)`
因为我们添加了新的消息类型,所以我们还需要添加通道层的方法。在 chat/consumers.py 末尾添加:
`# chat/consumers.py
def user_join(self, event):
self.send(text_data=json.dumps(event))
def user_leave(self, event):
self.send(text_data=json.dumps(event))`
你的 consumers.py 在这一步之后应该是这样的: consumers.py 。
前端
为了处理来自前端的消息,将以下情况添加到chatSocket.onmessage
处理程序中的 switch 语句中:
`// chat/static/room.js switch (data.type) { // ... case "user_list": for (let i = 0; i < data.users.length; i++) { onlineUsersSelectorAdd(data.users[i]); } break; case "user_join": chatLog.value += data.user + " joined the room.\n"; onlineUsersSelectorAdd(data.user); break; case "user_leave": chatLog.value += data.user + " left the room.\n"; onlineUsersSelectorRemove(data.user); break; // ...`
测试
再次运行服务器,登录,访问http://localhost:8000/chat/default。
您现在应该能够看到加入和离开消息。用户列表也应该被填充。
私人信息
Channels 包不允许直接过滤,所以没有从一个客户端向另一个客户端发送消息的内置方法。通过频道,您可以将消息发送给:
- 消费者的客户端(
self.send
) - 一个通道层组(
self.channel_layer.group_send
)
因此,为了实现私有消息传递,我们将:
- 每当客户端加入时,创建一个名为
inbox_%USERNAME%
的新组。 - 将客户添加到他们自己的收件箱组(
inbox_%USERNAME%
)。 - 当客户端断开连接时,将其从收件箱群组(
inbox_%USERNAME%
)中移除。
一旦实现,每个客户将有自己的私人邮件收件箱。然后,其他客户端可以向inbox_%TARGET_USERNAME%
发送私人消息。
后端
修改 chat/consumers.py 。
`# chat/consumers.py
class ChatConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs):
# ...
self.user_inbox = None # new
def connect(self):
# ...
self.user_inbox = f'inbox_{self.user.username}' # new
# accept the incoming connection
self.accept()
# ...
if self.user.is_authenticated:
# -------------------- new --------------------
# create a user inbox for private messages
async_to_sync(self.channel_layer.group_add)(
self.user_inbox,
self.channel_name,
)
# ---------------- end of new ----------------
# ...
def disconnect(self, close_code):
# ...
if self.user.is_authenticated:
# -------------------- new --------------------
# delete the user inbox for private messages
async_to_sync(self.channel_layer.group_discard)(
self.user_inbox,
self.channel_name,
)
# ---------------- end of new ----------------
# ...`
所以,我们:
- 将
user_inbox
添加到ChatConsumer
并在connect()
上初始化。 - 连接时将用户添加到
user_inbox
组。 - 当用户断开连接时,将用户从
user_inbox
组中移除。
接下来,修改receive()
来处理私有消息:
`# chat/consumers.py
def receive(self, text_data=None, bytes_data=None):
text_data_json = json.loads(text_data)
message = text_data_json['message']
if not self.user.is_authenticated:
return
# -------------------- new --------------------
if message.startswith('/pm '):
split = message.split(' ', 2)
target = split[1]
target_msg = split[2]
# send private message to the target
async_to_sync(self.channel_layer.group_send)(
f'inbox_{target}',
{
'type': 'private_message',
'user': self.user.username,
'message': target_msg,
}
)
# send private message delivered to the user
self.send(json.dumps({
'type': 'private_message_delivered',
'target': target,
'message': target_msg,
}))
return
# ---------------- end of new ----------------
# send chat message event to the room
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'user': self.user.username,
'message': message,
}
)
Message.objects.create(user=self.user, room=self.room, content=message)`
在 chat/consumers.py 的末尾增加以下方法:
`# chat/consumers.py
def private_message(self, event):
self.send(text_data=json.dumps(event))
def private_message_delivered(self, event):
self.send(text_data=json.dumps(event))`
你最终的 chat/consumers.py 文件应该等于这个: consumers.py
前端
为了在前端处理私有消息,在switch(data.type)
语句中添加private_message
和private_message_delivered
案例:
`// chat/static/room.js switch (data.type) { // ... case "private_message": chatLog.value += "PM from " + data.user + ": " + data.message + "\n"; break; case "private_message_delivered": chatLog.value += "PM to " + data.target + ": " + data.message + "\n"; break; // ... }`
为了使聊天更加方便,我们可以在用户点击onlineUsersSelector
中的一个在线用户时,将消息输入改为pm %USERNAME%
。将以下处理程序添加到底部:
`// chat/static/room.js onlineUsersSelector.onchange = function() { chatMessageInput.value = "/pm " + onlineUsersSelector.value + " "; onlineUsersSelector.value = null; chatMessageInput.focus(); };`
测试
就是这样!chap 应用程序现已完成。让我们最后一次测试一下。
创建两个超级用户进行测试,然后运行服务器。
打开两个不同的私人/匿名浏览器,在http://localhost:8000/admin同时登录。
然后在两个浏览器中导航到http://localhost:8000/chat/default。单击其中一个已连接的用户,向他们发送私人消息:
结论
在本教程中,我们学习了如何在 Django 中使用通道。您了解了同步和异步代码执行之间的差异,以及以下通道的概念:
- 顾客
- 通道层
- 按指定路线发送
最后,我们用 WebSockets 将所有东西捆绑在一起,构建了一个聊天应用程序。
我们的聊天远非完美。如果你想实践你所学的,你可以通过以下方法来提高它:
- 添加管理员专用聊天室。
- 当用户加入聊天室时,向用户发送最后十条消息。
- 允许用户编辑和删除消息。
- 添加“{user}”是键入“功能。
- 添加消息反应。
这些想法从最容易实现到最难实现进行排序。
您可以从 GitHub 上的 django-channels-example 存储库中获取代码。
使用 Chart.js 向 Django 添加图表
在本教程中,我们将看看如何使用 Chart.js 向 Django 添加交互式图表。我们将使用 Django 来建模和准备数据,然后使用 AJAX 从模板中异步获取数据。最后,我们将看看如何创建新的 Django 管理视图并扩展现有的管理模板,以便向 Django 管理添加自定义图表。
什么是 Chart.js?
Chart.js 是一个用于数据可视化的开源 JavaScript 库。它支持八种不同的图表类型:条形图、折线图、面积图、饼图、气泡图、雷达图、极坐标图和散点图。它灵活且高度可定制。它支持动画。最棒的是,它很容易使用。
首先,您只需要在 HTML 文件中包含 Chart.js 脚本和一个<canvas>
节点:
`<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)"></script>
<canvas id="chart" width="500" height="500"></canvas>`
然后,您可以像这样创建一个图表:
`let ctx = document.getElementById("chart").getContext("2d"); let chart = new Chart(ctx, { type: "bar", data: { labels: ["2020/Q1", "2020/Q2", "2020/Q3", "2020/Q4"], datasets: [ { label: "Gross volume ($)", backgroundColor: "#79AEC8", borderColor: "#417690", data: [26900, 28700, 27300, 29200] } ] }, options: { title: { text: "Gross Volume in 2020", display: true } } });`
此代码创建了以下图表:
见 CodePen 上 Michael Herman ( @mjhea0 )的笔 wvogmJy 。
要了解更多关于 Chart.js 的信息,请查看官方文档。
至此,我们来看看如何用 Chart.js 向 Django 添加图表。
项目设置
让我们创建一个简单的商店应用程序。我们将使用 Django 管理命令生成样本数据,然后用 Chart.js 将其可视化。
喜欢不同的 JavaScript 图表库,如 D3.js 或 Chartist ?您可以使用相同的方法将图表添加到 Django 和 Django admin 中。您只需要调整 JSON 端点中数据的格式。
工作流程:
- 使用 Django ORM 查询获取数据
- 格式化数据并通过受保护的端点返回
- 使用 AJAX 从模板中请求数据
- 初始化 Chart.js 并加载数据
首先创建一个新目录,并建立一个新的 Django 项目:
`$ mkdir django-interactive-charts && cd django-interactive-charts
$ python3.9 -m venv env
$ source env/bin/activate
(env)$ pip install django==3.1.6
(env)$ django-admin.py startproject core .`
之后,创建一个名为shop
的新应用:
`(env)$ python manage.py startapp shop`
在INSTALLED_APPS
下的 core/settings.py 中注册 app:
`# core/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'shop.apps.ShopConfig', # new
]`
创建数据库模型
接下来,创建Item
和Purchase
模型:
`# shop/models.py
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(null=True)
price = models.FloatField(default=0)
def __str__(self):
return f'{self.name} (${self.price})'
class Purchase(models.Model):
customer_full_name = models.CharField(max_length=64)
item = models.ForeignKey(to=Item, on_delete=models.CASCADE)
PAYMENT_METHODS = [
('CC', 'Credit card'),
('DC', 'Debit card'),
('ET', 'Ethereum'),
('BC', 'Bitcoin'),
]
payment_method = models.CharField(max_length=2, default='CC', choices=PAYMENT_METHODS)
time = models.DateTimeField(auto_now_add=True)
successful = models.BooleanField(default=False)
class Meta:
ordering = ['-time']
def __str__(self):
return f'{self.customer_full_name}, {self.payment_method} ({self.item.name})'`
Item
代表我们店里的一件商品Purchase
代表购买(与Item
相关联)
进行迁移,然后应用它们:
`(env)$ python manage.py makemigrations
(env)$ python manage.py migrate`
在 shop/admin.py 中注册模型:
`# shop/admin.py
from django.contrib import admin
from shop.models import Item, Purchase
admin.site.register(Item)
admin.site.register(Purchase)`
填充数据库
在创建任何图表之前,我们需要一些数据来处理。我创建了一个简单的命令,可以用来填充数据库。
在“商店”中创建一个名为“管理”的新文件夹,然后在该文件夹中创建另一个名为“命令”的文件夹。在“commands”文件夹中,创建一个名为 populate_db.py 的新文件。
`management
└── commands
└── populate_db.py`
populate_db.py :
`# shop/management/commands/populate_db.py
import random
from datetime import datetime, timedelta
import pytz
from django.core.management.base import BaseCommand
from shop.models import Item, Purchase
class Command(BaseCommand):
help = 'Populates the database with random generated data.'
def add_arguments(self, parser):
parser.add_argument('--amount', type=int, help='The number of purchases that should be created.')
def handle(self, *args, **options):
names = ['James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Charles']
surname = ['Smith', 'Jones', 'Taylor', 'Brown', 'Williams', 'Wilson', 'Johnson', 'Davies', 'Patel', 'Wright']
items = [
Item.objects.get_or_create(name='Socks', price=6.5), Item.objects.get_or_create(name='Pants', price=12),
Item.objects.get_or_create(name='T-Shirt', price=8), Item.objects.get_or_create(name='Boots', price=9),
Item.objects.get_or_create(name='Sweater', price=3), Item.objects.get_or_create(name='Underwear', price=9),
Item.objects.get_or_create(name='Leggings', price=7), Item.objects.get_or_create(name='Cap', price=5),
]
amount = options['amount'] if options['amount'] else 2500
for i in range(0, amount):
dt = pytz.utc.localize(datetime.now() - timedelta(days=random.randint(0, 1825)))
purchase = Purchase.objects.create(
customer_full_name=random.choice(names) + ' ' + random.choice(surname),
item=random.choice(items)[0],
payment_method=random.choice(Purchase.PAYMENT_METHODS)[0],
successful=True if random.randint(1, 2) == 1 else False,
)
purchase.time = dt
purchase.save()
self.stdout.write(self.style.SUCCESS('Successfully populated the database.'))`
运行以下命令来填充数据库:
`(env)$ python manage.py populate_db --amount 1000`
如果一切顺利,您应该会在控制台上看到一条Successfully populated the database.
消息。
这给数据库增加了 8 个项目和 1,000 次购买。
准备和提供数据
我们的应用将有以下端点:
- 列出我们有记录的所有年份
chart/sales/<YEAR>/
按年提取月度总量数据chart/spend-per-customer/<YEAR>/
按年度提取每位客户的月支出chart/payment-success/YEAR/
取年度支付成功数据chart/payment-method/YEAR/
获取年度支付方式数据(信用卡、借记卡、以太坊、比特币)
在添加视图之前,让我们创建几个实用函数,这将使创建图表变得更加容易。在项目根目录中添加一个名为“utils”的新文件夹。然后,向该文件夹添加一个名为 charts.py 的新文件:
`# util/charts.py
months = [
'January', 'February', 'March', 'April',
'May', 'June', 'July', 'August',
'September', 'October', 'November', 'December'
]
colorPalette = ['#55efc4', '#81ecec', '#a29bfe', '#ffeaa7', '#fab1a0', '#ff7675', '#fd79a8']
colorPrimary, colorSuccess, colorDanger = '#79aec8', colorPalette[0], colorPalette[5]
def get_year_dict():
year_dict = dict()
for month in months:
year_dict[month] = 0
return year_dict
def generate_color_palette(amount):
palette = []
i = 0
while i < len(colorPalette) and len(palette) < amount:
palette.append(colorPalette[i])
i += 1
if i == len(colorPalette) and len(palette) < amount:
i = 0
return palette`
因此,我们定义了图表颜色,并创建了以下两种方法:
- 创建一个月和值的字典,我们将用它来添加月数据。
generate_color_palette(amount)
生成一个重复的调色板,我们将把它传递给我们的图表。
视图
创建视图:
`# shop/views.py
from django.contrib.admin.views.decorators import staff_member_required
from django.db.models import Count, F, Sum, Avg
from django.db.models.functions import ExtractYear, ExtractMonth
from django.http import JsonResponse
from shop.models import Purchase
from utils.charts import months, colorPrimary, colorSuccess, colorDanger, generate_color_palette, get_year_dict
@staff_member_required
def get_filter_options(request):
grouped_purchases = Purchase.objects.annotate(year=ExtractYear('time')).values('year').order_by('-year').distinct()
options = [purchase['year'] for purchase in grouped_purchases]
return JsonResponse({
'options': options,
})
@staff_member_required
def get_sales_chart(request, year):
purchases = Purchase.objects.filter(time__year=year)
grouped_purchases = purchases.annotate(price=F('item__price')).annotate(month=ExtractMonth('time'))\
.values('month').annotate(average=Sum('item__price')).values('month', 'average').order_by('month')
sales_dict = get_year_dict()
for group in grouped_purchases:
sales_dict[months[group['month']-1]] = round(group['average'], 2)
return JsonResponse({
'title': f'Sales in {year}',
'data': {
'labels': list(sales_dict.keys()),
'datasets': [{
'label': 'Amount ($)',
'backgroundColor': colorPrimary,
'borderColor': colorPrimary,
'data': list(sales_dict.values()),
}]
},
})
@staff_member_required
def spend_per_customer_chart(request, year):
purchases = Purchase.objects.filter(time__year=year)
grouped_purchases = purchases.annotate(price=F('item__price')).annotate(month=ExtractMonth('time'))\
.values('month').annotate(average=Avg('item__price')).values('month', 'average').order_by('month')
spend_per_customer_dict = get_year_dict()
for group in grouped_purchases:
spend_per_customer_dict[months[group['month']-1]] = round(group['average'], 2)
return JsonResponse({
'title': f'Spend per customer in {year}',
'data': {
'labels': list(spend_per_customer_dict.keys()),
'datasets': [{
'label': 'Amount ($)',
'backgroundColor': colorPrimary,
'borderColor': colorPrimary,
'data': list(spend_per_customer_dict.values()),
}]
},
})
@staff_member_required
def payment_success_chart(request, year):
purchases = Purchase.objects.filter(time__year=year)
return JsonResponse({
'title': f'Payment success rate in {year}',
'data': {
'labels': ['Successful', 'Unsuccessful'],
'datasets': [{
'label': 'Amount ($)',
'backgroundColor': [colorSuccess, colorDanger],
'borderColor': [colorSuccess, colorDanger],
'data': [
purchases.filter(successful=True).count(),
purchases.filter(successful=False).count(),
],
}]
},
})
@staff_member_required
def payment_method_chart(request, year):
purchases = Purchase.objects.filter(time__year=year)
grouped_purchases = purchases.values('payment_method').annotate(count=Count('id'))\
.values('payment_method', 'count').order_by('payment_method')
payment_method_dict = dict()
for payment_method in Purchase.PAYMENT_METHODS:
payment_method_dict[payment_method[1]] = 0
for group in grouped_purchases:
payment_method_dict[dict(Purchase.PAYMENT_METHODS)[group['payment_method']]] = group['count']
return JsonResponse({
'title': f'Payment method rate in {year}',
'data': {
'labels': list(payment_method_dict.keys()),
'datasets': [{
'label': 'Amount ($)',
'backgroundColor': generate_color_palette(len(payment_method_dict)),
'borderColor': generate_color_palette(len(payment_method_dict)),
'data': list(payment_method_dict.values()),
}]
},
})`
注意事项:
- 获取所有的购买,按年份分组,从时间字段中提取年份,并在列表中返回它们。
- 获取所有购买(在特定年份)及其价格,按月分组,并计算每月价格总和。
- 获取所有购买(在特定年份)及其价格,按月分组,并计算月平均价格。
payment_success_chart()
统计特定年份中成功和不成功的采购。- 获取所有购买(在特定年份),按付款方式分组,计算购买数量,并在字典中返回。
注意,每个视图都有一个 @staff_member_required 装饰器。
资源定位符
创建以下应用程序级别的 URL:
`# shop/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('chart/filter-options/', views.get_filter_options, name='chart-filter-options'),
path('chart/sales/<int:year>/', views.get_sales_chart, name='chart-sales'),
path('chart/spend-per-customer/<int:year>/', views.spend_per_customer_chart, name='chart-spend-per-customer'),
path('chart/payment-success/<int:year>/', views.payment_success_chart, name='chart-payment-success'),
path('chart/payment-method/<int:year>/', views.payment_method_chart, name='chart-payment-method'),
]`
然后,将应用程序 URL 连接到项目 URL:
`# core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('shop/', include('shop.urls')), # new
]`
测试
现在我们已经注册了 URL,让我们测试一下端点,看看是否一切正常。
创建超级用户,然后运行开发服务器:
`(env)$ python manage.py createsuperuser
(env)$ python manage.py runserver`
然后,在您选择的浏览器中,导航到http://localhost:8000/shop/chart/filter-options/。您应该会看到类似这样的内容:
`{ "options":[ 2021, 2020, 2019, 2018, 2017, 2016 ] }`
要查看 2020 年的月度销售数据,请导航至http://localhost:8000/shop/chart/sales/2020/:
`{ "title":"Sales in 2020", "data":{ "labels":[ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "datasets":[ { "label":"Amount ($)", "backgroundColor":"#79aec8", "borderColor":"#79aec8", "data":[ 137.0, 88.0, 187.5, 156.0, 70.5, 133.0, 142.0, 176.0, 155.5, 104.0, 125.5, 97.0 ] } ] } }`
使用 Chart.js 创建图表
继续,让我们连接 Chart.js。
在“商店”中添加一个“模板”文件夹。然后,添加一个名为statistics.html的新文件:
`<!-- shop/templates/statistics.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Statistics</title>
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)"></script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/bootstrap-grid.min.css">
</head>
<body>
<div class="container">
<form id="filterForm">
<label for="year">Choose a year:</label>
<select name="year" id="year"></select>
<input type="submit" value="Load" name="_load">
</form>
<div class="row">
<div class="col-6">
<canvas id="salesChart"></canvas>
</div>
<div class="col-6">
<canvas id="paymentSuccessChart"></canvas>
</div>
<div class="col-6">
<canvas id="spendPerCustomerChart"></canvas>
</div>
<div class="col-6">
<canvas id="paymentMethodChart"></canvas>
</div>
</div>
<script> let salesCtx = document.getElementById("salesChart").getContext("2d"); let salesChart = new Chart(salesCtx, { type: "bar", options: { responsive: true, } }); let spendPerCustomerCtx = document.getElementById("spendPerCustomerChart").getContext("2d"); let spendPerCustomerChart = new Chart(spendPerCustomerCtx, { type: "line", options: { responsive: true, } }); let paymentSuccessCtx = document.getElementById("paymentSuccessChart").getContext("2d"); let paymentSuccessChart = new Chart(paymentSuccessCtx, { type: "pie", options: { responsive: true, layout: { padding: { left: 0, right: 0, top: 0, bottom: 25 } } } }); let paymentMethodCtx = document.getElementById("paymentMethodChart").getContext("2d"); let paymentMethodChart = new Chart(paymentMethodCtx, { type: "pie", options: { responsive: true, layout: { padding: { left: 0, right: 0, top: 0, bottom: 25 } } } }); </script>
</div>
</body>
</html>`
这段代码创建了 Chart.js 用来初始化的 HTML 画布。我们还将responsive
传递给每个图表的选项,以便它根据窗口大小进行调整。
将以下脚本添加到 HTML 文件中:
`<script> $(document).ready(function() { $.ajax({ url: "/shop/chart/filter-options/", type: "GET", dataType: "json", success: (jsonResponse) => { // Load all the options jsonResponse.options.forEach(option => { $("#year").append(new Option(option, option)); }); // Load data for the first option loadAllCharts($("#year").children().first().val()); }, error: () => console.log("Failed to fetch chart filter options!") }); }); $("#filterForm").on("submit", (event) => { event.preventDefault(); const year = $("#year").val(); loadAllCharts(year) }); function loadChart(chart, endpoint) { $.ajax({ url: endpoint, type: "GET", dataType: "json", success: (jsonResponse) => { // Extract data from the response const title = jsonResponse.title; const labels = jsonResponse.data.labels; const datasets = jsonResponse.data.datasets; // Reset the current chart chart.data.datasets = []; chart.data.labels = []; // Load new data into the chart chart.options.title.text = title; chart.options.title.display = true; chart.data.labels = labels; datasets.forEach(dataset => { chart.data.datasets.push(dataset); }); chart.update(); }, error: () => console.log("Failed to fetch chart data from " + endpoint + "!") }); } function loadAllCharts(year) { loadChart(salesChart, `/shop/chart/sales/${year}/`); loadChart(spendPerCustomerChart, `/shop/chart/spend-per-customer/${year}/`); loadChart(paymentSuccessChart, `/shop/chart/payment-success/${year}/`); loadChart(paymentMethodChart, `/shop/chart/payment-method/${year}/`); } </script>`
当页面加载时,这个脚本向/chart/filter-options/
发送一个 AJAX 请求,获取所有有效年份,并将它们加载到表单中。
loadChart
将 Django 端点的图表数据加载到图表中loadAllCharts
加载所有图表
注意,为了简单起见,我们使用 jQuery 来处理 AJAX 请求。请随意使用获取 API 来代替。
Inside shop/views.py 创建一个新视图:
`@staff_member_required
def statistics_view(request):
return render(request, 'statistics.html', {})`
不要忘记重要的一点:
`from django.shortcuts import render`
为视图分配 URL:
`# shop/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('statistics/', views.statistics_view, name='shop-statistics'), # new
path('chart/filter-options/', views.get_filter_options, name='chart-filter-options'),
path('chart/sales/<int:year>/', views.get_sales_chart, name='chart-sales'),
path('chart/spend-per-customer/<int:year>/', views.spend_per_customer_chart, name='chart-spend-per-customer'),
path('chart/payment-success/<int:year>/', views.payment_success_chart, name='chart-payment-success'),
path('chart/payment-method/<int:year>/', views.payment_method_chart, name='chart-payment-method'),
]`
您的图表现在可以在以下位置访问:http://localhost:8000/shop/statistics/。
向 Django 管理添加图表
关于将图表集成到 Django admin 中,我们可以:
- 创建一个新的 Django 管理视图
- 扩展现有管理模板
- 使用第三方包(即 django-admin-tools )
创建一个新的 Django 管理视图
创建一个新的 Django 管理视图是最干净和最直接的方法。在这种方法中,我们将创建一个新的管理站点,并在 settings.py 文件中对其进行更改。
首先,在“商店/模板”中,添加一个“管理”文件夹。给它添加一个statistics.html模板:
`<!-- shop/templates/admin/statistics.html -->
{% extends "admin/base_site.html" %}
{% block content %}
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)"></script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/bootstrap-grid.min.css">
<form id="filterForm">
<label for="year">Choose a year:</label>
<select name="year" id="year"></select>
<input type="submit" value="Load" name="_load">
</form>
<script> $(document).ready(function() { $.ajax({ url: "/shop/chart/filter-options/", type: "GET", dataType: "json", success: (jsonResponse) => { // Load all the options jsonResponse.options.forEach(option => { $("#year").append(new Option(option, option)); }); // Load data for the first option loadAllCharts($("#year").children().first().val()); }, error: () => console.log("Failed to fetch chart filter options!") }); }); $("#filterForm").on("submit", (event) => { event.preventDefault(); const year = $("#year").val(); loadAllCharts(year); }); function loadChart(chart, endpoint) { $.ajax({ url: endpoint, type: "GET", dataType: "json", success: (jsonResponse) => { // Extract data from the response const title = jsonResponse.title; const labels = jsonResponse.data.labels; const datasets = jsonResponse.data.datasets; // Reset the current chart chart.data.datasets = []; chart.data.labels = []; // Load new data into the chart chart.options.title.text = title; chart.options.title.display = true; chart.data.labels = labels; datasets.forEach(dataset => { chart.data.datasets.push(dataset); }); chart.update(); }, error: () => console.log("Failed to fetch chart data from " + endpoint + "!") }); } function loadAllCharts(year) { loadChart(salesChart, `/shop/chart/sales/${year}/`); loadChart(spendPerCustomerChart, `/shop/chart/spend-per-customer/${year}/`); loadChart(paymentSuccessChart, `/shop/chart/payment-success/${year}/`); loadChart(paymentMethodChart, `/shop/chart/payment-method/${year}/`); } </script>
<div class="row">
<div class="col-6">
<canvas id="salesChart"></canvas>
</div>
<div class="col-6">
<canvas id="paymentSuccessChart"></canvas>
</div>
<div class="col-6">
<canvas id="spendPerCustomerChart"></canvas>
</div>
<div class="col-6">
<canvas id="paymentMethodChart"></canvas>
</div>
</div>
<script> let salesCtx = document.getElementById("salesChart").getContext("2d"); let salesChart = new Chart(salesCtx, { type: "bar", options: { responsive: true, } }); let spendPerCustomerCtx = document.getElementById("spendPerCustomerChart").getContext("2d"); let spendPerCustomerChart = new Chart(spendPerCustomerCtx, { type: "line", options: { responsive: true, } }); let paymentSuccessCtx = document.getElementById("paymentSuccessChart").getContext("2d"); let paymentSuccessChart = new Chart(paymentSuccessCtx, { type: "pie", options: { responsive: true, layout: { padding: { left: 0, right: 0, top: 0, bottom: 25 } } } }); let paymentMethodCtx = document.getElementById("paymentMethodChart").getContext("2d"); let paymentMethodChart = new Chart(paymentMethodCtx, { type: "pie", options: { responsive: true, layout: { padding: { left: 0, right: 0, top: 0, bottom: 25 } } } }); </script>
{% endblock %}`
接下来,创建一个 core/admin.py 文件:
`# core/admin.py
from django.contrib import admin
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render
from django.urls import path
@staff_member_required
def admin_statistics_view(request):
return render(request, 'admin/statistics.html', {
'title': 'Statistics'
})
class CustomAdminSite(admin.AdminSite):
def get_app_list(self, request):
app_list = super().get_app_list(request)
app_list += [
{
'name': 'My Custom App',
'app_label': 'my_custom_app',
'models': [
{
'name': 'Statistics',
'object_name': 'statistics',
'admin_url': '/admin/statistics',
'view_only': True,
}
],
}
]
return app_list
def get_urls(self):
urls = super().get_urls()
urls += [
path('statistics/', admin_statistics_view, name='admin-statistics'),
]
return urls`
在这里,我们创建了一个名为admin_statistics_view
的人员保护视图。然后,我们创建了一个新的AdminSite
并覆盖了get_app_list
来添加我们自己的定制应用程序。我们为我们的应用程序提供了一个名为Statistics
的人工视图模型。最后,我们覆盖了get_urls
,并为我们的新视图分配了一个 URL。
在 core/apps.py 里面创建一个AdminConfig
:
`# core/apps.py
from django.contrib.admin.apps import AdminConfig
class CustomAdminConfig(AdminConfig):
default_site = 'core.admin.CustomAdminSite'`
这里,我们创建了一个新的AdminConfig
,它加载我们的CustomAdminSite
而不是 Django 的默认CustomAdminSite
。
在 core/settings.py 中用新的替换默认的AdminConfig
:
`INSTALLED_APPS = [
'core.apps.CustomAdminConfig', # replaced
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'shop.apps.ShopConfig',
]`
导航到http://localhost:8000/admin/查看新的 Django 管理视图。
最终结果:
点击“查看”查看图表:
扩展现有管理模板
您可以随时扩展管理模板并覆盖您想要的部分。
例如,如果您想在商店模型下放置图表,您可以通过覆盖 app_index.html 模板来实现。在“商店/模板/管理/商店”中添加一个新的 app_index.html 文件,然后在这里添加找到的 HTML。
最终结果:
结论
在本文中,您学习了如何使用 Django 提供数据,然后使用 Chart.js 可视化数据。
从 GitHub 上的django-interactive-chartsrepo 中获取代码。
Django 中基于类和基于函数的视图
原文:https://testdriven.io/blog/django-class-based-vs-function-based-views/
在这篇文章中,我们将看看 Django 的基于类的观点(CBV)和基于函数的观点(FBV)之间的区别。我们将比较和对比每种方法的优缺点(以及 Django 内置的基于类的通用视图)。最后,你应该很好地理解什么时候使用一个而不是另一个。
介绍
Django(以及其他任何 web 框架)的主要用途之一是响应 HTTP 请求提供 HTTP 响应。Django 允许我们使用所谓的视图来做到这一点。视图只是一个接受请求并返回响应的可调用对象。
Django 最初只支持基于函数的视图(fbv),但它们很难扩展,没有利用面向对象编程 (OOP)原则,也没有干。这就是为什么 Django 开发人员决定增加对基于类的视图(cbv)的支持。cbv 利用 OOP 原则,这允许我们使用继承,重用代码,并且通常编写更好更干净的代码。
我们需要记住,cbv 不是为了取代 fbv 而设计的。用 fbv 可以实现的任何事情,用 cbv 也可以实现。它们各有利弊。
最后,Django 提供预制或通用的 cbv,为常见问题提供解决方案。它们具有程序员友好的名称,并为显示数据、编辑数据和处理基于日期的数据等问题提供解决方案。它们可以单独使用,也可以在自定义视图中继承。
让我们看看不同的视图类型,并了解什么时候适合使用哪种视图。
基于功能的视图(fbv)
从本质上来说,fbv只是函数。它们易于阅读和操作,因为你可以清楚地看到发生了什么。由于其简单性,它们非常适合 Django 初学者。所以,如果你刚开始用 Django,建议在潜入 CBVs 之前先对 FBVs 有一些工作知识。
利弊
优点
- 显式代码流(您可以完全控制发生的事情)
- 易于实施
- 容易理解
- 非常适合独特的视图逻辑
- 易于与装饰者整合
缺点
- 大量重复的(样板)代码
- 通过条件分支处理 HTTP 方法
- 不要利用 OOP
- 更难维护
快速示例
FBV 的一个例子是这样的:
`from django.shortcuts import render, redirect
from django.views import View
def task_create_view(request):
if request.method == 'POST':
form = TaskForm(data=request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('task-list'))
return render(request, 'todo/task_create.html', {
'form': TaskForm(),
})`
这个视图接收一个request
,执行一些逻辑,然后返回一个HttpResponse
。只要看看代码,我们就可以看到第一个缺点:条件分支。对于每个 HTTP 方法,我们必须创建一个单独的分支。这会增加代码的复杂性,并导致意大利面条代码。
fbv 的下一个缺点是它们的扩展性不好。随着您的代码库变得越来越大,您会注意到许多重复的(样板)代码用于处理模型(尤其是 CRUD 操作)。试着想象一下创建文章的视图与上面的例子有多大的不同...他们会非常相似。
为了使用 fbv,我们必须将它们注册在 urls.py 中,如下所示:
`urlpatterns = [
path('create/', task_create_view, name='task-create'),
]`
当您处理高度定制的视图逻辑时,应该选择 fbv。换句话说,对于不与其他视图共享很多代码的视图来说,fbv 是一个很好的用例。使用 FBVs 的几个真实例子是:统计视图、图表视图和密码重置视图。
待办事项应用程序(使用 FBVs)
让我们来看看如何只使用 fbv 编写一个允许 CRUD 操作的简单 todo 应用程序。
首先,我们将初始化我们的项目,定义我们的模型,创建 HTML 模板,然后开始处理 views.py 。我们可能会得到这样的结果:
`# todo/views.py
from django.shortcuts import render, get_object_or_404, redirect
from .forms import TaskForm, ConfirmForm
from .models import Task
def task_list_view(request):
return render(request, 'todo/task_list.html', {
'tasks': Task.objects.all(),
})
def task_create_view(request):
if request.method == 'POST':
form = TaskForm(data=request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('task-list'))
return render(request, 'todo/task_create.html', {
'form': TaskForm(),
})
def task_detail_view(request, pk):
task = get_object_or_404(Task, pk=pk)
return render(request, 'todo/task_detail.html', {
'task': task,
})
def task_update_view(request, pk):
task = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
form = TaskForm(instance=task, data=request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('task-detail', args={pk: pk}))
return render(request, 'todo/task_update.html', {
'task': task,
'form': TaskForm(instance=task),
})
def task_delete_view(request, pk):
task = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
form = ConfirmForm(data=request.POST)
if form.is_valid():
task.delete()
return HttpResponseRedirect(reverse('task-list'))
return render(request, 'todo/task_delete.html', {
'task': task,
'form': ConfirmForm(),
})`
你可以在 GitHub 上获得完整的源代码。
我们最终得到了简单明了的视图逻辑。你不能对这段代码做太多改进。
基于类的视图(cbv)
在 Django 1.3 中引入的基于类的视图,提供了一种替代的方式来实现视图作为 Python 对象而不是函数。它们允许我们使用 OOP 原则(最重要的是继承)。我们可以使用 cbv 来概括部分代码,并将它们提取为超类视图。
cbv 还允许您使用 Django 内置的基于类的通用视图和混合视图,我们将在下一节中对此进行介绍。
利弊
优点
- 是可扩展的
- 他们利用了 OOP 概念(最重要的是继承)
- 非常适合编写 CRUD 视图
- 更干净和可重用的代码
- Django 内置的通用 CBVs
- 它们类似于 Django REST 框架视图
缺点
- 隐式代码流(许多事情发生在后台)
- 使用许多混音,这可能会令人困惑
- 更复杂,更难掌握
- 装饰者需要额外的导入或代码覆盖
更多信息,请回顾一下在 Django/Python 中使用基于类的视图的利弊?
快速示例
让我们把之前的 FBV 例子改写成 CBV:
`from django.shortcuts import render, redirect
from django.views import View
class TaskCreateView(View):
def get(self, request, *args, **kwargs):
return render(request, 'todo/task_create.html', {
'form': TaskForm(),
})
def post(self, request, *args, **kwargs):
form = TaskForm(data=request.POST)
if form.is_valid():
task = form.save()
return redirect('task-detail', pk=task.pk)
return self.get(request)`
我们可以看到,这个例子与 FBV 的方法没有太大的不同。逻辑大致相同。主要区别是代码组织。这里,每个 HTTP 方法都用一个单独的方法来处理,而不是条件分支。在 CBVs 中可以使用以下方法:get
、post
、put
、patch
、delete
、head
、options
、trace
。
这种方法的另一个好处是没有定义的 HTTP 方法会自动返回一个405 Method Not Allowed
响应。
当使用 FBVs 时,你可以使用允许的 HTTP 方法装饰器,比如
@require_http_methods
,来实现同样的事情。
因为 Django 的 URL 解析器需要一个可调用的函数,所以在 urls.py 中注册它们时,我们需要调用 as_view() :
`urlpatterns = [
path('create/', TaskCreateView.as_view(), name='task-create'),
]`
代码流
CBVs 的代码流稍微复杂一些,因为一些事情发生在后台。如果我们扩展基本视图类,将执行以下代码步骤:
- Django URL dispatcher 将一个
HttpRequest
路由到MyView
。 - Django URL 调度程序在
MyView
上调用as_view()
。 as_view()
调用setup()
和dispatch()
。dispatch()
触发特定 HTTP 方法的方法或http_method_not_allowed()
。- 返回一个
HttpResponse
。
待办事项应用程序(使用 CBVs)
现在,让我们重写 todo 应用程序,只使用 CBVs:
`# todo/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.views import View
from .forms import TaskForm, ConfirmForm
from .models import Task
class TaskListView(View):
def get(self, request, *args, **kwargs):
return render(request, 'todo/task_list.html', {
'tasks': Task.objects.all(),
})
class TaskCreateView(View):
def get(self, request, *args, **kwargs):
return render(request, 'todo/task_create.html', {
'form': TaskForm(),
})
def post(self, request, *args, **kwargs):
form = TaskForm(data=request.POST)
if form.is_valid():
task = form.save()
return redirect('task-detail', pk=task.pk)
return self.get(request)
class TaskDetailView(View):
def get(self, request, pk, *args, **kwargs):
task = get_object_or_404(Task, pk=pk)
return render(request, 'todo/task_detail.html', {
'task': task,
})
class TaskUpdateView(View):
def get(self, request, pk, *args, **kwargs):
task = get_object_or_404(Task, pk=pk)
return render(request, 'todo/task_update.html', {
'task': task,
'form': TaskForm(instance=task),
})
def post(self, request, pk, *args, **kwargs):
task = get_object_or_404(Task, pk=pk)
form = TaskForm(instance=task, data=request.POST)
if form.is_valid():
form.save()
return redirect('task-detail', pk=task.pk)
return self.get(request, pk)
class TaskDeleteView(View):
def get(self, request, pk, *args, **kwargs):
task = get_object_or_404(Task, pk=pk)
return render(request, 'todo/task_confirm_delete.html', {
'task': task,
'form': ConfirmForm(),
})
def post(self, request, pk, *args, **kwargs):
task = get_object_or_404(Task, pk=pk)
form = ConfirmForm(data=request.POST)
if form.is_valid():
task.delete()
return redirect('task-list')
return self.get(request, pk)`
另外,我们不要忘记让我们的 urls.py 调用as_view()
:
`# todo/urls.py
from django.urls import path
from .views import TaskListView, TaskDetailView, TaskCreateView, TaskUpdateView, TaskDeleteView
urlpatterns = [
path('', TaskListView.as_view(), name='task-list'),
path('create/', TaskCreateView.as_view(), name='task-create'),
path('<int:pk>/', TaskDetailView.as_view(), name='task-detail'),
path('update/<int:pk>/', TaskUpdateView.as_view(), name='task-update'),
path('delete/<int:pk>/', TaskDeleteView.as_view(), name='task-delete'),
]`
你可以在 GitHub 上获得完整的源代码。
为了更简洁的代码,我们牺牲了几行代码。我们不再使用条件分支。例如,如果我们看一下TaskCreateView
和TaskUpdateView
,我们可以看到它们几乎是一样的。我们可以通过将公共逻辑提取到父类中来进一步改进这段代码。此外,我们可以提取视图逻辑,并将其用于其他模型的视图。
Django 的基于类的通用视图
如果您遵循上一节提到的所有重构建议,您最终会得到一个模仿 Django 的一些通用的基于类的视图。Django 的通用 cbv非常适合解决常见问题,如检索、创建、修改和删除对象,以及分页和归档视图。它们也加快了开发过程。
快速示例
让我们看一个例子:
`from django.views.generic import CreateView
class TaskCreateView(CreateView):
model = Task
context_object_name = 'task'
fields = ('name', 'description', 'is_done')
template_name = 'todo/task_create.html'`
我们创建了一个名为TaskCreateView
的类,并继承了CreateView
。通过这样做,我们获得了很多功能,几乎没有代码。现在我们只需要设置以下属性:
- 定义视图使用的 Django 模型。
- Django 使用
fields
来创建表单(或者,我们可以提供form_class
)。 template_name
定义使用哪个模板(默认为/<app_name>/<model_name>_form.html
)。context_object_name
定义将模型实例传递给模板的上下文键(默认为object
)。success_url
定义用户成功时被重定向到哪里(或者,您可以在您的模型中设置get_absolute_url
)。
有关通用 cbv 的更多信息,请参考官方文档。
正如您可能已经猜到的那样,通用 cbv 的幕后还会有更多的奇迹发生。即使对于有经验的 Django 开发人员来说,它们也会令人困惑。一旦你掌握了它们的窍门,你可能会觉得自己像个巫师。
对于执行常见任务(例如 CRUD 操作)的视图,应该使用通用 cbv。如果你的视图需要做 CBVs 没有涵盖的事情,使用 mixins 或者基于函数的视图。
姜戈内置的 CBV 类型
在撰写本文时,Django 提供了大量的通用 cbv,我们可以将其分为三类:
我们将在这里看看如何使用它们的实际例子。
查看混音
每个通用视图用来解决一个问题(例如,显示信息,创建/更新某些东西)。如果您的视图需要做更多的事情,您可以使用 mixins 创建您的视图。Mixins 是提供独立功能的类,它们可以组合起来解决几乎任何问题。
你也可以选择一个通用的 CBV 作为基础,然后包括额外的混音。
甚至 Django 的通用 cbv 也是由 mixins 组成的。让我们看一下CreateView
图:
我们可以看到它利用了许多混合,比如ContextMixin
、SingleObjectMixin
和FormMixin
。它们有程序员友好的名字,所以根据它们的名字,你应该对它们的功能有一个大致的了解。
Mixins 需要花很多时间来掌握,而且经常会令人困惑。如果你刚刚开始使用 mixin,我建议你从阅读使用基于类视图的 mixin开始。
Todo 应用程序(使用 Django 的通用 CBVs)
现在,让我们使用 Django 的通用的基于类的视图最后一次重写 todo 应用程序:
`# todo/views.py
from django.views.generic import ListView, DetailView, DeleteView, UpdateView, CreateView
class TaskListView(ListView):
model = Task
context_object_name = 'tasks'
class TaskCreateView(CreateView):
model = Task
context_object_name = 'task'
fields = ('name', 'description', 'is_done')
template_name = 'todo/task_create.html'
class TaskDetailView(DetailView):
model = Task
context_object_name = 'task'
class TaskUpdateView(UpdateView):
model = Task
context_object_name = 'task'
fields = ('name', 'description', 'is_done')
template_name = 'todo/task_update.html'
class TaskDeleteView(DeleteView):
model = Task
context_object_name = 'task'
success_url = '/'`
你可以在 GitHub 上获得完整的源代码。
通过使用 Django 通用 cbv,我们将代码分成了两半。此外,代码更加清晰,更易于维护,但是如果您不熟悉一般的 cbv,那么阅读和理解起来可能会困难得多。
结论
一种视图类型并不比另一种更好。这完全取决于情况和个人喜好。有时 fbv 更好,有时 cbv 更好。当你写下一个具体的观点时,试着记住它们的优点和缺点,以便做出一个好的决定。如果你仍然不确定什么时候使用哪个,你可以通过把 fbv 转换成 cbv 或者相反来练习一下。此外,您可以使用此流程图:
我个人更喜欢在较小的项目中使用 fbv(不能用通用的 cbv 解决),在处理较大的代码库时选择 cbv。
接受姜戈和比特币基地的加密付款
在本教程中,我们将把 Django 与比特币基地商务集成起来,以接受不同的加密支付。我们来看看两种不同的方法:比特币基地收费和比特币基地结账。
什么是比特币基地商业?
比特币基地商务是由比特币基地提供的企业数字支付服务,允许商家接受不同数字货币的加密支付。在撰写本文时,支持比特币、比特币现金、戴币、以太币、莱特币、Dogecoin 和美元币。比特币基地商务很容易集成到您的 web 应用程序中,并消除了处理加密支付的麻烦。
比特币基地和比特币基地商贸不是一回事。比特币基地是一家加密交易所和钱包管理公司,而比特币基地商业是一家面向商家的数字支付服务提供商。
比特币基地商业 API 提供了两种不同的接受加密支付的方式:比特币基地收费和比特币基地结账。
比特币基地收费的优势:
- 高度可定制
- 能够以编程方式附加元数据
比特币基地收银台的优势:
- 开箱即用
- 仪表板产品管理
- 嵌入式结帐
对于大多数应用程序,我们推荐 Charges API,因为它可以定制。这比结帐 API 提供的简单性更重要。如果你在销售一个固定的产品,定制并没有发挥很大的作用,请放心使用结帐 API。
项目设置
在本教程中,我们将展示如何让这两种方法,比特币基地收费和比特币基地结帐,启动和运行。我们将从相同的项目设置开始。一旦设置好了,就可以随意遵循其中一种或两种方法。
Django 项目设置
首先创建一个新目录,并建立一个新的 Django 项目:
`$ mkdir django-coinbase && cd django-coinbase
$ python3.9 -m venv env
$ source env/bin/activate
(env)$ pip install django==3.2.8
(env)$ django-admin startproject core .`
之后,创建一个名为payments
的新应用:
`(env)$ python manage.py startapp payments`
在INSTALLED_APPS
下的 core/settings.py 中注册 app:
`# core/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'payments.apps.PaymentsConfig', # new
]`
向 payments/views.py 添加一个名为home_view
的简单功能视图:
`# payments/views.py
from django.shortcuts import render
def home_view(request):
return render(request, 'home.html', {})`
在payments
app 里面创建一个 urls.py 文件:
`# payments/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home_view, name='payments-home'),
]`
使用payments
应用程序更新项目级 URL 文件:
`# core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('', include('payments.urls')), # new
path('admin/', admin.site.urls),
]`
为主页创建一个专用的“模板”文件夹和一个文件:
`(env)$ mkdir templates
(env)$ touch templates/home.html`
然后,将以下 HTML 添加到 templates/home.html :
`<!-- templates/home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Coinbase</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<a class="btn btn-primary" href="#">Purchase</a>
</div>
</body>
</html>`
确保更新 settings.py 文件,以便 Django 知道要查找“templates”文件夹:
`# core/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates'], # new
...`
最后运行migrate
来同步数据库,运行runserver
来启动 Django 的本地 web 服务器:
`(env)$ python manage.py migrate
(env)$ python manage.py runserver`
就是这样!在自己喜欢的浏览器中打开 http://localhost:8000/ 。您应该会看到主页:
添加比特币基地商务
接下来,安装coinbase-commerce
:
`(env)$ pip install coinbase-commerce==1.0.1`
为了使用比特币基地商务 API ,你需要创建一个账户(如果你还没有的话)。创建帐户后,登录并导航至您的设置。向下滚动到“API 密钥”部分,单击“创建 API 密钥”,然后单击“显示”。将 API 密钥复制到剪贴板。
然后将密钥添加到 settings.py 文件中:
`# core/settings.py
COINBASE_COMMERCE_API_KEY = '<your coinbase api key here>'`
在进入下一步之前,让我们测试一下是否可以从 API 中检索数据。使用以下命令输入 Python shell:
`(env)$ python manage.py shell`
导入以下内容:
`>>> from core import settings
>>> from coinbase_commerce.client import Client`
初始化新客户端并列出费用:
`>>> client = Client(api_key=settings.COINBASE_COMMERCE_API_KEY)
>>> for charge in client.charge.list_paging_iter():
... print("{!r}".format(charge))`
如果一切正常,client.charge.list_paging_iter()
应该是一个空列表。只要您没有看到以下错误,就可以认为一切正常:
`coinbase_commerce.error.AuthenticationError: Request id 10780b77-1021-4b09-b53b-a590ea044380: No such API key.`
使用比特币基地 API 的有用页面:
有了这个,决定你想采取哪种方法,比特币基地收费或比特币基地结帐。
比特币基地收费
教程的这一部分展示了如何使用比特币基地收费 API 接受付款。
比特币基地收费是收集加密支付的最可定制的方法。使用这种方法,您可以完全控制费用。
工作流程:
- 使用客户端连接到比特币基地商务 API
- 创建费用(来自 JSON)
- 获取新创建的费用,并将其传递给模板
- 将用户重定向到比特币基地商务网站
- (可选)使用 webhooks 验证费用
创建一个收费
要请求加密货币支付,您需要创建一个费用。由于加密货币支付是推送支付,如果没有检测到支付,则在等待期后收费将到期。费用由一个独特的 8 字符代码识别。
将home_view
更新为创建费用:
`# payments/views.py
from coinbase_commerce.client import Client
from django.shortcuts import render
from core import settings
def home_view(request):
client = Client(api_key=settings.COINBASE_COMMERCE_API_KEY)
domain_url = 'http://localhost:8000/'
product = {
'name': 'Coffee',
'description': 'A really good local coffee.',
'local_price': {
'amount': '5.00',
'currency': 'USD'
},
'pricing_type': 'fixed_price',
'redirect_url': domain_url + 'success/',
'cancel_url': domain_url + 'cancel/',
}
charge = client.charge.create(**product)
return render(request, 'home.html', {
'charge': charge,
})`
我们首先通过向客户端传递COINBASE_COMMERCE_API_KEY
来初始化它。然后,我们创建了一个代表我们产品的 JSON 对象(我们提供了名称、描述等。).我们还将redirect_url
和cancel_url
传递给了 charge,稍后我们将在这里实现它。然后,我们解包 JSON 对象,并使用客户机创建一个费用。最后,我们将它作为上下文传递给 home 模板。
我们现在可以访问 templates/home.html 中的费用信息。像这样更新模板:
`<!-- templates/home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Coinbase Charge</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<div class="card w-25">
<div class="card-body">
<h5 class="card-title">{{ charge.name }}</h5>
<p class="card-text">
<span>{{ charge.description }}</span>
<br>
<span>${{ charge.pricing.local.amount }} {{ charge.pricing.local.currency }}</span>
</p>
<div>
<a class="btn btn-primary w-100" href="{{ charge.hosted_url }}">Purchase</a>
</div>
</div>
</div>
</div>
</body>
</html>`
请参考收费 API 文档,了解您可以在模板上显示的所有收费相关信息。
运行开发服务器:
`(env)$ python manage.py runserver`
http://localhost:8000/ 现在应该是这样的:
先不要测试。我们很快就会这么做。
创建重定向视图
让我们实现传递给 charge 的视图redirect_url
和cancel_url
。
在 payments/views.py 中创建两个新视图:
`# payments/views.py
def success_view(request):
return render(request, 'success.html', {})
def cancel_view(request):
return render(request, 'cancel.html', {})`
在 payments/urls.py 中注册新视图:
`# payments/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home_view, name='payments-home'),
path('success/', views.success_view, name='payments-success'), # new
path('cancel/', views.cancel_view, name='payments-cancel'), # new
]`
以及 templates/success.html :
`<!-- templates/success.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Coinbase Charge</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<p>Your payment has been successful.</p>
</div>
</body>
</html>`
以及 templates/cancel.html :
`<!-- templates/cancel.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Coinbase Charge</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<p>Your payment has been cancelled.</p>
</div>
</body>
</html>`
现在确认支付后,用户会被重定向到redirect_url
。如果他们取消支付(或收费超时),他们将被重定向到cancel_url
。
测试
不幸的是,比特币基地商务不支持沙盒账户测试,这意味着你需要使用真正的加密来测试你的应用。
运行服务器,导航到 http://localhost:8000/ ,点击“购买”按钮。您将被重定向到比特币基地商业托管网站:
当您选择要支付的加密货币时,将显示钱包地址(和二维码):
然后你有 60 分钟的时间在充电超时前转移密码。
在您发送加密后,矿工将需要几分钟(取决于您使用的加密货币)来确认您的交易。在交易获得所需数量的确认(基于加密货币)后,您将被重定向到与redirect_url
相关联的 URL。
付款将出现在商务仪表板的“付款”下:
跳到“用 Webhooks 确认付款”部分,了解付款确认。
比特币基地收银台
教程的这一部分展示了如何使用比特币基地结账 API 接受付款。如果您决定使用比特币基地收费,请随意跳过这一部分,转到“用 Webhooks 确认付款”部分。
比特币基地结帐是用比特币基地商业收集加密付款的最简单的方法。通过这种方式,将自动为您生成费用。这种方法适用于处理固定产品。
工作流程:
- 使用客户端连接到比特币基地商务 API
- 从比特币基地商业 API 获取一个结帐并将其传递给模板
- (可选)如果处理嵌入式签出,请将所需的 HTML 代码添加到模板中
- 将用户重定向到比特币基地商业托管网站或使用嵌入式结帐
- (可选)使用 webhooks 验证费用
创建结帐
创建签出有两种方式:
- 通过 coinbase-commerce 库进行编程
- 在比特币基地商务控制板中人工操作
为简单起见,我们将使用商务仪表板。
导航至“结帐”并点击“创建结帐”:
点击“销售产品”。输入产品名称、描述和价格。点击“下一步”。在“客户信息”下,选择“不收集任何信息”。然后,点击“完成”。
比特币基地商务生成了所有必要的 HTML 代码,您需要添加到您的主页模板。你可以在“嵌入”标签下看到这个。为了使 web 应用程序更加模块化和可重用,我们将在home_view
中获取结帐信息,而不是在模板中硬编码。为此,我们首先需要通过复制托管页面的 URL 来获取结帐 ID:
永久链接应该是这样的:
`https://commerce.coinbase.com/checkout/df2d7c68-145e-4537-86fa-1cac705eb748`
我们只对 URL 的最后一段感兴趣,它表示结账 ID:
`df2d7c68-145e-4537-86fa-1cac705eb748`
将它保存在 core/settings.py 文件中,如下所示:
`# core/settings.py
COINBASE_CHECKOUT_ID = '<your checkout id>'`
更新支付/查看. py :
`# payments/views.py
from coinbase_commerce.client import Client
from django.shortcuts import render
from core import settings
def home_view(request):
client = Client(api_key=settings.COINBASE_COMMERCE_API_KEY)
checkout = client.checkout.retrieve(settings.COINBASE_CHECKOUT_ID)
checkout_link = f'https://commerce.coinbase.com/checkout/{checkout.id}'
return render(request, 'home.html', {
'checkout': checkout,
'checkout_link': checkout_link,
})`
这段代码获取结帐并将其传递给模板。
最后,我们需要修改 home 模板来使用 checkout。您可以使用:
- 托管页面——用户被重定向到比特币基地结账网站
- 嵌入式页面-一切都发生在你的网站上
出于教育目的,我们将使用这两种方法。
更新 templates/home.html :
`<!-- templates/home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Coinbase Checkout</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<div class="card w-25">
<!-- Coinbase checkout details -->
<div class="card-body">
<h5 class="card-title">{{ checkout.name }}</h5>
<p class="card-text">
{{ checkout.description }}<br>
{{ checkout.local_price.amount }} {{ checkout.local_price.currency }}
</p>
<div>
<!-- Coinbase hosted approach (script not required) -->
<a class="btn btn-primary w-100" href="{{ checkout_link }}">Purchase (hosted)</a>
<!-- Coinbase embedded approach (script required) -->
<div class="mt-2">
<a class="btn btn-primary buy-with-crypto w-100" href="{{ checkout_link }}">Purchase (embedded)</a>
<script src="https://commerce.coinbase.com/v1/checkout.js?version=201807"></script>
</div>
</div>
</div>
</div>
</div>
</body>
</html>`
运行服务器并导航到 http://localhost:8000/ :
此时,应用程序完全正常工作。但是现在不要测试任何东西。我们将在下一节中介绍这一点。
测试
不幸的是,比特币基地商务不支持沙盒账户测试,这意味着你需要使用真正的加密来测试你的应用。
在服务器运行的情况下,导航到 http://localhost:8000/ 。您可以使用比特币基地托管的网站或嵌入式结帐测试。你的选择。
当您选择要支付的加密货币时,将显示钱包地址(和二维码):
然后你有 60 分钟的时间在充电超时前转移密码。
在您发送加密后,矿工将需要几分钟(取决于您使用的加密货币)来确认您的交易。在交易获得所需数量的确认(基于加密货币)后,您将收到一条“支付完成”消息。
付款将出现在商务仪表板的“付款”下:
用 Webhooks 确认付款
我们的应用程序在这一点上运行良好,但我们还不能以编程方式确认付款。我们只是在用户结帐后将他们重定向到成功页面,但我们不能只依赖该页面,因为支付确认是异步发生的。
在编程时,您经常要处理两种不同类型的事件:同步事件,它们会产生即时的效果和结果(例如,创建收费),异步事件,它们不会产生即时的结果(例如,确认付款)。因为支付确认是异步完成的,用户可能会在他们的支付被确认之前和我们收到他们的资金之前被重定向到成功页面。
为了在付款完成时得到通知,您需要创建一个 webhook 。我们需要在应用程序中创建一个简单的端点,每当事件发生时(即确认收费时),比特币基地商务部都会调用这个端点。通过使用 webhooks,我们可以确保支付成功。
为了使用 webhooks,我们需要:
- 在应用程序中设置 webhook 端点
- 在比特币基地商务仪表板中注册端点
配置端点
在 payments/views.py 中创建coinbase_webhook
视图:
`# payments/views.py
@csrf_exempt
@require_http_methods(['POST'])
def coinbase_webhook(request):
logger = logging.getLogger(__name__)
request_data = request.body.decode('utf-8')
request_sig = request.headers.get('X-CC-Webhook-Signature', None)
webhook_secret = settings.COINBASE_COMMERCE_WEBHOOK_SHARED_SECRET
try:
event = Webhook.construct_event(request_data, request_sig, webhook_secret)
# List of all Coinbase webhook events:
# https://commerce.coinbase.com/docs/api/#webhooks
if event['type'] == 'charge:confirmed':
logger.info('Payment confirmed.')
# TODO: run some custom code here
except (SignatureVerificationError, WebhookInvalidPayload) as e:
return HttpResponse(e, status=400)
logger.info(f'Received event: id={event.id}, type={event.type}')
return HttpResponse('ok', status=200)`
这个代码块验证请求的签名和有效负载,然后从中生成一个事件。现在,您可以检查事件类型,并根据类型执行不同的操作。
更新导入:
`import logging
from coinbase_commerce.client import Client
from coinbase_commerce.error import SignatureVerificationError, WebhookInvalidPayload
from coinbase_commerce.webhook import Webhook
from django.http import HttpResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from core import settings`
忽略
COINBASE_COMMERCE_WEBHOOK_SHARED_SECRET
还不存在的错误。我们将在下一步中添加它。
在 payments/urls.py 中注册网址:
`# payments/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home_view, name='payments-home'),
path('success/', views.success_view, name='payments-success'), # only for the Coinbase charges approach
path('cancel/', views.cancel_view, name='payments-cancel'), # only for the Coinbase charges approach
path('webhook/', views.coinbase_webhook), # new
]`
coinbase_webhook
视图现在充当我们的 webhook 端点,当事件发生时,比特币基地商务会将事件发送到该端点。
如果你遵循比特币基地结帐方法,不要包含
success/
或cancel/
URL。
识别用户
为了识别 webhook 中的用户,您需要在创建费用或开始结帐会话时传递一些元数据。然后,您将能够在 webhook 中获取这些元数据。
根据您使用的方法,传递元数据的方式会有所不同:
- 收费 API -你必须将元数据附加到产品上
- check out API——你必须将元数据作为 HTML 属性传递
使用费用 API 确定用户
要使用收费 API 识别用户,首先将以下内容添加到home_view
内的product
字典中:
`# payments/views.py
product = {
...
'metadata': {
'customer_id': request.user.id if request.user.is_authenticated else None,
'customer_username': request.user.username if request.user.is_authenticated else None,
},
...
}`
这段代码附加了经过身份验证的用户的用户 ID 和用户名。
您现在可以像这样访问coinbase_webhook
视图中的元数据:
`# payments/views.py
@csrf_exempt
@require_http_methods(['POST'])
def coinbase_webhook(request):
logger = logging.getLogger(__name__)
request_data = request.body.decode('utf-8')
request_sig = request.headers.get('X-CC-Webhook-Signature', None)
webhook_secret = settings.COINBASE_COMMERCE_WEBHOOK_SHARED_SECRET
try:
event = Webhook.construct_event(request_data, request_sig, webhook_secret)
# List of all Coinbase webhook events:
# https://commerce.coinbase.com/docs/api/#webhooks
if event['type'] == 'charge:confirmed':
logger.info('Payment confirmed.')
customer_id = event['data']['metadata']['customer_id'] # new
customer_username = event['data']['metadata']['customer_username'] # new
# TODO: run some custom code here
# you can also use 'customer_id' or 'customer_username'
# to fetch an actual Django user
except (SignatureVerificationError, WebhookInvalidPayload) as e:
return HttpResponse(e, status=400)
logger.info(f'Received event: id={event.id}, type={event.type}')
return HttpResponse('ok', status=200)`
使用结帐 API 识别用户
为了使用结帐 API 识别用户,我们需要将元数据作为一个数据属性添加到 templates/home.html 中的锚点:
例如:
`<div>
<!-- Coinbase hosted approach (script not required) -->
<a
class="btn btn-primary w-100"
{% if user.is_authenticated %}data-custom="{{ user.pk }}"{% endif %}
href="{{ checkout_link }}"
>Purchase (hosted)</a>
<!-- Coinbase embedded approach (script required) -->
<div class="mt-2">
<a
class="btn btn-primary buy-with-crypto w-100"
{% if user.is_authenticated %}data-custom="{{ user.pk }}"{% endif %}
href="{{ checkout_link }}"
>Purchase (embedded)</a>
<script src="https://commerce.coinbase.com/v1/checkout.js?version=201807"></script>
</div>
</div>`
然后,您可以在coinbase_webhook
视图中检索元数据,如下所示:
`# payments/views.py
@csrf_exempt
@require_http_methods(['POST'])
def coinbase_webhook(request):
import logging
request_data = request.body.decode('utf-8')
request_sig = request.headers.get('X-CC-Webhook-Signature', None)
webhook_secret = settings.COINBASE_COMMERCE_WEBHOOK_SHARED_SECRET
try:
event = Webhook.construct_event(request_data, request_sig, webhook_secret)
# List of all Coinbase webhook events:
# https://commerce.coinbase.com/docs/api/#webhooks
if event['type'] == 'charge:confirmed':
logger.info('Payment confirmed.')
customer_id = event['data']['metadata']['custom'] # new
# TODO: run some custom code here
# you can also use 'customer_id'
# to fetch an actual Django user
except (SignatureVerificationError, WebhookInvalidPayload) as e:
return HttpResponse(e, status=400)
logger.info(f'Received event: id={event.id}, type={event.type}')
return HttpResponse('ok', status=200)`
请记住,webhook 以纯文本形式接收
data-custom
。在使用它从数据库中获取用户之前,一定要将它解析为一个整数。
注册端点
导航至https://commerce.coinbase.com/dashboard/settings,向下滚动至“Webhook 订阅”并点击“添加端点”:
输入您的端点 URL,然后按“保存”。
该网址需要以 HTTPS 开头,这意味着您需要部署您的应用程序,然后才能测试它。
接下来,点击“显示共享密码”并复制密码:
将共享密钥添加到 settings.py 文件中,如下所示:
`# core/settings.py
COINBASE_COMMERCE_WEBHOOK_SHARED_SECRET = '<your coinbase webhook secret here>'`
测试端点
部署好应用程序后,您可以从 Commerce Dashboard 发送一个测试 webhook 请求。导航至“Webhook 订阅”,点击 webhook 端点下的“详细信息”:
然后点击“发送测试”:
最后,选择“充电:确认”并再次点击“发送测试”。您应该会看到“支付已成功”日志消息。
结论
在本教程中,您学习了如何接受比特币基地商务加密支付。您现在应该能够将其与 Django 集成,并通过 webhook 验证支付。
您可以在 GitHub 上的以下 repos 中找到代码:
在 Django 项目中期迁移到自定义用户模型
原文:https://testdriven.io/blog/django-custom-user-model-migration/
本文着眼于如何在 Django 项目中期迁移到定制用户模型。
自定义用户模型
Django 的默认用户模型带有相对较少的字段。因为这些字段对于所有用例来说都是不够的,所以很多 Django 项目转向定制用户模型。
在迁移数据库之前,切换到定制用户模型很容易,但是之后会变得更加困难,因为这会影响外键、多对多关系和迁移等等。
为了避免经历这个繁琐的迁移过程,Django 的官方文档强烈建议您在项目开始时建立一个定制的用户模型,即使默认模型已经足够了。
直到今天,仍然没有在项目中期迁移到定制用户模型的官方方法。Django 社区仍然在讨论什么是最好的迁移方式。
在本文中,我们将研究一种在项目中期迁移到定制用户模型的相对简单的方法。我们将要使用的迁移过程不像互联网上的其他迁移过程那样具有破坏性,并且不需要任何原始 SQL 执行或手动修改迁移。
有关在项目开始时创建定制用户模型的更多信息,请查看 Django 文章中的创建定制用户模型。
虚拟项目
在项目中期迁移到定制用户模型是一个潜在的破坏性行为。因此,我准备了一个虚拟项目,您可以在进入实际代码库之前使用它来测试迁移过程。
如果您想使用自己的代码库,可以跳过这一部分。
我们将要使用的虚拟项目叫做 django-custom-user 。这是一个简单的利用用户模型的 todo 应用程序。
克隆下来:
`$ git clone --single-branch --branch base [[email protected]](/cdn-cgi/l/email-protection):duplxey/django-custom-user.git
$ cd django-custom-user`
创建新的虚拟环境并激活它:
`$ python3 -m venv venv && source venv/bin/activate`
安装要求:
`(venv)$ pip install -r requirements.txt`
旋转 Postgres Docker 容器:
`$ docker run --name django-todo-postgres -p 5432:5432 \
-e POSTGRES_USER=django-todo -e POSTGRES_PASSWORD=complexpassword123 \
-e POSTGRES_DB=django-todo -d postgres`
或者,如果你愿意,你可以在 Docker 之外安装并运行 Postgres。只要确保转到 core/settings.py 并相应地更改
DATABASES
凭证。
迁移数据库:
`(venv)$ python manage.py migrate`
装载夹具:
`(venv)$ python manage.py loaddata fixtures/auth.json --app auth
(venv)$ python manage.py loaddata fixtures/todo.json --app todo`
这两个设备向数据库添加了一些用户、组和任务,并创建了一个具有以下凭证的超级用户:
`username: admin password: password`
接下来,运行服务器:
`(venv)$ python manage.py runserver`
最后,导航到位于http://localhost:8000/admin的管理面板,以超级用户身份登录,并确保数据已经成功加载。
迁移过程
我们将要使用的迁移过程假设:
- 您的项目还没有自定义用户模型。
- 您已经创建并迁移了数据库。
- 没有挂起的迁移,所有现有迁移都已应用。
- 你不想丢失任何数据。
如果您仍处于开发阶段,并且数据库中的数据不重要,则不必遵循这些步骤。要迁移到定制用户模型,您可以简单地擦除数据库,删除所有迁移文件,然后按照这里的步骤执行。
在继续之前,请完全备份您的数据库(和代码库)。在转移到生产环境之前,您还应该在分段分支/环境中尝试这些步骤。
迁移步骤
- 将
AUTH_USER_MODEL
指向 settings.py 中默认的 Django 用户。 - 相应地用
AUTH_USER_MODEL
或get_user_model()
替换所有的User
参考。 - 启动一个新的 Django app,在 settings.py 中注册。
- 在新创建的应用程序中创建一个空迁移。
- 迁移数据库,以便应用空迁移。
- 删除空的迁移文件。
- 在新创建的应用程序中创建自定义用户模型。
- 将
DJANGO_USER_MODEL
指向自定义用户。 - 运行 makemigrations。
我们开始吧!
第一步
要迁移到定制用户模型,我们首先需要去掉所有直接的User
引用。为此,首先在设置. py 中添加一个名为AUTH_USER_MODEL
的新属性,如下所示:
`# core/settings.py
AUTH_USER_MODEL = 'auth.User'`
这个属性告诉 Django 使用什么用户模型。因为我们还没有定制的用户模型,所以我们将它指向默认的 Django 用户模型。
第二步
接下来,检查您的整个代码库,确保相应地用 AUTH_USER_MODEL 或 get_user_model() 替换所有的User
引用:
`# todo/models.py
class UserTask(GenericTask):
user = models.ForeignKey(
to=AUTH_USER_MODEL,
on_delete=models.CASCADE
)
def __str__(self):
return f'UserTask {self.id}'
class GroupTask(GenericTask):
users = models.ManyToManyField(
to=AUTH_USER_MODEL
)
def __str__(self):
return f'GroupTask {self.id}'`
不要忘记在文件顶部导入AUTH_USER_MODEL
:
`from core.settings import AUTH_USER_MODEL`
还要确保你使用的所有第三方应用程序/包都是这样做的。如果他们中的任何一个直接引用了
User
模型,事情可能会被打破。你不必担心这个,因为大多数利用User
模型的流行包并不直接引用它。
第三步
接下来,我们需要启动一个新的 Django 应用程序,它将托管自定义用户模型。
我称它为用户,但是你可以选择一个不同的名字:
`(venv)$ python manage.py startapp users`
如果你愿意,你可以重用一个已经存在的应用程序,但是你需要确保这个应用程序中还没有迁移;否则,由于 Django 的限制,迁移过程将无法进行。
在 settings.py 中注册 app:
`# core/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'todo.apps.TodoConfig',
'users.apps.UsersConfig', # new
]`
第四步
接下来,我们需要欺骗 Django,让他认为用户应用程序负责auth_user
表。这通常可以用 migrate 命令和--fake
标志来完成,但在这种情况下不行,因为我们会遇到InconsistentMigrationHistory
,因为大多数迁移都依赖于auth
迁移。
无论如何,要绕过这一点,我们可以使用一个 hacky 的工作区。首先,我们将创建一个空迁移,应用它以便保存到django_migrations
表中,然后与实际的auth_user
迁移交换。
在用户应用程序中创建一个空迁移:
`(venv)$ python manage.py makemigrations --empty users
Migrations for 'users':
users\migrations\0001_initial.py`
这将创建一个名为users/migrations/0001 _ initial . py的空迁移。
第五步
迁移数据库,以便将空迁移添加到django_migrations
表中:
`(venv)$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, todo, users
Running migrations:
Applying users.0001_initial... OK`
第六步
现在,删除空的迁移文件:
`(venv)$ rm users/migrations/0001_initial.py`
第七步
转到 users/models.py ,定义自定义User
模型,如下所示:
`# users/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
class Meta:
db_table = 'auth_user'`
暂时不要添加任何自定义字段。这个模型必须是 Django 默认用户模型的直接副本,因为我们将使用它来创建初始的auth_user
表迁移。
此外,确保将其命名为User
,否则您可能会因为内容类型而遇到问题。您稍后可以更改模型的名称。
第八步
导航到您的 settings.py ,将AUTH_USER_MODEL
指向刚刚创建的定制用户模型:
`# core/settings.py
AUTH_USER_MODEL = 'users.User'`
如果你的应用不叫 users 一定要换一个。
第九步
运行makemigrations
生成初始auth_user
迁移:
`(venv)$ python manage.py makemigrations
Migrations for 'users':
users\migrations\0001_initial.py
- Create model User`
就是这样!当您第一次通过 Django 的auth
应用程序运行 migrate 时,已经应用了生成的迁移,所以再次运行 migrate 不会做任何事情。
添加新字段
一旦建立了自定义用户模型,添加新字段就很容易了。
例如,要添加一个phone
和address
字段,请将以下内容添加到自定义用户模型中:
`# users/models.py
class User(AbstractUser):
phone = models.CharField(max_length=32, blank=True, null=True) # new
address = models.CharField(max_length=64, blank=True, null=True) # new
class Meta:
db_table = 'auth_user'`
不要忘记在文件顶部导入models
:
`from django.db import models`
接下来,进行迁移并迁移:
`(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate`
要确保这些字段已经在数据库中得到反映,请将它们放入 Docker 容器:
`$ docker exec -it django-todo-postgres bash`
通过psql
连接到数据库:
`[[email protected]](/cdn-cgi/l/email-protection):/# psql -U django-todo
psql (14.5 (Debian 14.5-1.pgdg110+1))
Type "help" for help.`
并检查auth_user
表:
`django-todo=# \d+ auth_user
Table "public.auth_user"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
--------------+--------------------------+-----------+----------+----------------------------------+----------+-------------+--------------+-------------
id | integer | | not null | generated by default as identity | plain | | |
password | character varying(128) | | not null | | extended | | |
last_login | timestamp with time zone | | | | plain | | |
is_superuser | boolean | | not null | | plain | | |
username | character varying(150) | | not null | | extended | | |
first_name | character varying(150) | | not null | | extended | | |
last_name | character varying(150) | | not null | | extended | | |
email | character varying(254) | | not null | | extended | | |
is_staff | boolean | | not null | | plain | | |
is_active | boolean | | not null | | plain | | |
date_joined | timestamp with time zone | | not null | | plain | | |
phone | character varying(32) | | | | extended | | |
address | character varying(64) | | | | extended | | |`
您可以看到已经添加了名为phone
和address
的新字段。
Django 管理
要在 Django 管理面板中显示自定义用户模型,首先需要创建一个从UserAdmin
继承的新类,然后注册它。接下来,在字段集中包含phone
和address
。
最终的用户/管理员副本应该如下所示:
`# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from users.models import User
class CustomUserAdmin(UserAdmin):
fieldsets = UserAdmin.fieldsets + (
('Additional info', {'fields': ('phone', 'address')}),
)
admin.site.register(User, CustomUserAdmin)`
再次运行服务器,登录,并导航到一个随机用户。向下滚动到底部,您应该会看到一个包含新字段的新部分。
如果您希望进一步定制 Django 管理,请从官方文档中查看 Django 管理站点。
重命名用户表/模型
此时,您可以像平常一样重命名用户模型和表。
要重命名用户模型,只需更改类名,要重命名表,只需更改db_table
属性:
`# users/models.py
class User(AbstractUser): # <-- you can change me
phone = models.CharField(max_length=32, blank=True, null=True)
address = models.CharField(max_length=64, blank=True, null=True)
class Meta:
db_table = 'auth_user' # <-- you can change me`
如果您删除了db_table
属性,表名将返回到<app_name>_<model_name>
。
完成更改后,运行:
`(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate`
我一般不建议重命名任何东西,因为你的数据库结构会变得不一致。有些表格会有前缀users_
,而有些表格会有前缀auth_
。但另一方面,你可以争辩说User
模式现在是用户应用的一部分,所以它不应该有auth_
前缀。
如果您决定重命名该表,最终的数据库结构将如下所示:
`django-todo=# \dt
List of relations
Schema | Name | Type | Owner
--------+-----------------------------+-------+-------------
public | auth_group | table | django-todo
public | auth_group_permissions | table | django-todo
public | auth_permission | table | django-todo
public | django_admin_log | table | django-todo
public | django_content_type | table | django-todo
public | django_migrations | table | django-todo
public | django_session | table | django-todo
public | todo_task | table | django-todo
public | todo_task_categories | table | django-todo
public | todo_taskcategory | table | django-todo
public | users_user | table | django-todo
public | users_user_groups | table | django-todo
public | users_user_user_permissions | table | django-todo`
结论
尽管在项目中期迁移到自定义用户模型的问题已经存在了很长时间,但是仍然没有官方的解决方案。
不幸的是,许多 Django 开发人员不得不经历这个迁移过程,因为 Django 文档没有充分强调您应该在项目开始时创建一个定制的用户模型。也许他们甚至可以把它包括在教程里?
希望我在文章中介绍的迁移过程对您没有任何问题。万一有些事情对你不起作用,或者你认为有些事情可以改进,我希望听到你的反馈。
您可以从 django-custom-user repo 中获得最终的源代码。
在 Django 中创建定制用户模型
对于 Django 认证,我如何将用户名字段完全替换为电子邮件字段?
本文一步一步地解释了如何在 Django 中创建一个定制的用户模型,这样就可以使用电子邮件地址作为主要的用户标识符,而不是用户名来进行身份验证。
请记住,本文中概述的过程需要对数据库模式进行重大更改。正因为如此,它只推荐给新项目。如果您正在处理一个现有的遗留项目,您将需要遵循一组不同的步骤。有关这方面的更多信息,请查看 Django 文章中的迁移到定制用户模型中期项目。
目标
完成本文后,您应该能够:
- 描述
AbstractUser
和AbstractBaseUser
的区别 - 解释为什么在开始一个新的 Django 项目时应该建立一个定制的用户模型
- 使用定制用户模型开始一个新的 Django 项目
- 使用电子邮件地址作为主要用户标识符,而不是用户名进行身份验证
- 在实现定制用户模型时,实践测试优先开发
AbstractUser vs AbstractBaseUser
Django 中的默认用户模型使用用户名在身份验证期间惟一地标识用户。如果您更愿意使用电子邮件地址,那么您需要通过子类化AbstractUser
或AbstractBaseUser
来创建一个定制的用户模型。
选项:
AbstractUser
:如果您对用户模型上的现有字段满意,并且只想删除用户名字段,请使用此选项。- 如果你想从头开始创建你自己的,全新的用户模型,使用这个选项。
在本文中,我们将研究这两个选项,
AbstractUser
和AbstractBaseUser
。
每个的步骤都相同:
- 创建自定义用户模型和管理器
- 更新 settings.py
- 自定义
UserCreationForm
和UserChangeForm
表单 - 更新管理员
在开始一个新的 Django 项目时,强烈建议建立一个自定义用户模型。如果没有它,您将需要创建另一个模型(如
UserProfile
)并使用OneToOneField
将其链接到 Django 用户模型,如果您想要向用户模型添加新的字段的话。
项目设置
首先创建一个新的 Django 项目和一个用户应用程序:
`$ mkdir django-custom-user-model && cd django-custom-user-model
$ python3 -m venv env
$ source env/bin/activate
(env)$ pip install Django==4.1.5
(env)$ django-admin startproject hello_django .
(env)$ python manage.py startapp users`
你可以随意把 virtualenv 和 Pip 换成诗歌或 Pipenv 。更多信息,请查看现代 Python 环境。
不要应用迁移。记住:您必须在应用第一次迁移之前创建定制用户模型。
将新应用添加到 settings.py 中的INSTALLED_APPS
列表:
`INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"users", # new
]`
试验
让我们采用测试优先的方法:
`from django.contrib.auth import get_user_model
from django.test import TestCase
class UsersManagersTests(TestCase):
def test_create_user(self):
User = get_user_model()
user = User.objects.create_user(email="[[email protected]](/cdn-cgi/l/email-protection)", password="foo")
self.assertEqual(user.email, "[[email protected]](/cdn-cgi/l/email-protection)")
self.assertTrue(user.is_active)
self.assertFalse(user.is_staff)
self.assertFalse(user.is_superuser)
try:
# username is None for the AbstractUser option
# username does not exist for the AbstractBaseUser option
self.assertIsNone(user.username)
except AttributeError:
pass
with self.assertRaises(TypeError):
User.objects.create_user()
with self.assertRaises(TypeError):
User.objects.create_user(email="")
with self.assertRaises(ValueError):
User.objects.create_user(email="", password="foo")
def test_create_superuser(self):
User = get_user_model()
admin_user = User.objects.create_superuser(email="[[email protected]](/cdn-cgi/l/email-protection)", password="foo")
self.assertEqual(admin_user.email, "[[email protected]](/cdn-cgi/l/email-protection)")
self.assertTrue(admin_user.is_active)
self.assertTrue(admin_user.is_staff)
self.assertTrue(admin_user.is_superuser)
try:
# username is None for the AbstractUser option
# username does not exist for the AbstractBaseUser option
self.assertIsNone(admin_user.username)
except AttributeError:
pass
with self.assertRaises(ValueError):
User.objects.create_superuser(
email="[[email protected]](/cdn-cgi/l/email-protection)", password="foo", is_superuser=False)`
将规格添加到 users/tests.py 中,然后确保测试失败。
模型经理
首先,我们需要通过子类化BaseUserManager
来添加一个定制的管理器,它使用电子邮件作为唯一标识符,而不是用户名。
在“用户”目录下创建一个 managers.py 文件:
`from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import gettext_lazy as _
class CustomUserManager(BaseUserManager):
"""
Custom user model manager where email is the unique identifiers
for authentication instead of usernames.
"""
def create_user(self, email, password, **extra_fields):
"""
Create and save a user with the given email and password.
"""
if not email:
raise ValueError(_("The Email must be set"))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
"""
Create and save a SuperUser with the given email and password.
"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError(_("Superuser must have is_staff=True."))
if extra_fields.get("is_superuser") is not True:
raise ValueError(_("Superuser must have is_superuser=True."))
return self.create_user(email, password, **extra_fields)`
用户模型
决定你想使用哪个选项:子类化AbstractUser
或AbstractBaseUser
。
抽象用户
更新 users/models.py :
`from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
from .managers import CustomUserManager
class CustomUser(AbstractUser):
username = None
email = models.EmailField(_("email address"), unique=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
objects = CustomUserManager()
def __str__(self):
return self.email`
在此,我们:
- 创建了一个名为
CustomUser
的新类,它是AbstractUser
的子类 - 删除了
username
字段 - 使
email
字段成为必填且唯一的 - 将定义
User
型号唯一标识符的USERNAME_FIELD
设置为email
- 指定该类的所有对象都来自
CustomUserManager
AbstractBaseUser
更新 users/models.py :
`from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from .managers import CustomUserManager
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(_("email address"), unique=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(default=timezone.now)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
objects = CustomUserManager()
def __str__(self):
return self.email`
在此,我们:
- 创建了一个名为
CustomUser
的新类,它是AbstractBaseUser
的子类 - 增加了
email
、is_staff
、is_active
和date_joined
的字段 - 将定义
User
型号唯一标识符的USERNAME_FIELD
设置为email
- 指定该类的所有对象都来自
CustomUserManager
设置
将下面一行添加到 settings.py 文件中,以便 Django 知道使用新的定制用户类:
`AUTH_USER_MODEL = "users.CustomUser"`
现在,您可以创建并应用迁移,这将创建一个使用定制用户模型的新数据库。在我们这样做之前,让我们看看在没有创建迁移文件的情况下的实际迁移情况,以及 -预演标志:
`(env)$ python manage.py makemigrations --dry-run --verbosity 3`
您应该会看到类似如下的内容:
`# Generated by Django 4.1.5 on 2023-01-21 20:36
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
),
]`
如果您走的是
AbstractBaseUser
路线,您将没有first_name
或last_name
字段。为什么?
确保迁移不包括username
字段。然后,创建并应用迁移:
`(env)$ python manage.py makemigrations
(env)$ python manage.py migrate`
查看模式:
`$ sqlite3 db.sqlite3
SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.
sqlite> .tables
auth_group django_migrations
auth_group_permissions django_session
auth_permission users_customuser
django_admin_log users_customuser_groups
django_content_type users_customuser_user_permissions
sqlite> .schema users_customuser
CREATE TABLE IF NOT EXISTS "users_customuser" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"password" varchar(128) NOT NULL,
"last_login" datetime NULL,
"is_superuser" bool NOT NULL,
"first_name" varchar(150) NOT NULL,
"last_name" varchar(150) NOT NULL,
"is_staff" bool NOT NULL,
"is_active" bool NOT NULL,
"date_joined" datetime NOT NULL,
"email" varchar(254) NOT NULL UNIQUE
);`
如果你走的是
AbstractBaseUser
路线,为什么last_login
是模型的一部分?
现在,您可以使用get_user_model()
或settings.AUTH_USER_MODEL
来引用用户模型。更多信息请参考参考官方文件中的用户模型。
此外,当您创建超级用户时,应该提示您输入电子邮件而不是用户名:
`(env)$ python manage.py createsuperuser
Email address: [[email protected]](/cdn-cgi/l/email-protection)
Password:
Password (again):
Superuser created successfully.`
确保测试通过:
`(env)$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.282s
OK
Destroying test database for alias 'default'...`
形式
接下来,让我们子类化UserCreationForm
和UserChangeForm
表单,以便它们使用新的CustomUser
模型。
在“用户”中创建一个名为 forms.py 的新文件:
`from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = ("email",)
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ("email",)`
管理
通过在 users/admin.py 中子类化UserAdmin
来告诉管理员使用这些表单:
`from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = ("email", "is_staff", "is_active",)
list_filter = ("email", "is_staff", "is_active",)
fieldsets = (
(None, {"fields": ("email", "password")}),
("Permissions", {"fields": ("is_staff", "is_active", "groups", "user_permissions")}),
)
add_fieldsets = (
(None, {
"classes": ("wide",),
"fields": (
"email", "password1", "password2", "is_staff",
"is_active", "groups", "user_permissions"
)}
),
)
search_fields = ("email",)
ordering = ("email",)
admin.site.register(CustomUser, CustomUserAdmin)`
就是这样。运行服务器并登录到管理站点。您应该能够像平常一样添加和更改用户。
结论
在本文中,我们研究了如何创建自定义用户模型,以便电子邮件地址可以用作主要用户标识符,而不是用于身份验证的用户名。
你可以在django-custom-user-modelrepo 中找到两个选项AbstractUser
和AbstractBaseUser
的最终代码。最后的代码示例还包括用户认证所需的模板、视图和 URL。
想了解更多关于定制 Django 用户模型的信息吗?查看以下资源:
在 PyCharm 中调试容器化的 Django 应用程序
在 Docker 中开发你的 Django 应用会非常方便。你不必安装额外的服务,如 Postgres、Nginx 和 Redis 等。在你自己的机器上。这也使得新开发人员更容易快速上手并运行。
然而,草并不总是更绿。在 Docker 中运行 Django 会产生一些问题,让原本简单的事情变得困难。例如,如何在代码中设置断点并进行调试?
在这个快速教程中,我们将了解 PyCharm 如何通过其远程解释器和 Docker 集成来帮助调试容器化的 Django 应用程序。
本帖使用的是 PyCharm 专业版 v2021.2.2,关于 PyCharm 专业版和社区(免费)版的区别,请看一下专业版与社区版对比指南。
目标
在本教程结束时,您应该能够在 PyCharm 中执行以下操作:
- 配置 Docker 设置
- 设置远程解释器
- 创建一个运行/调试配置来调试 Docker 中运行的 Django 应用程序
PyCharm 中的坞站设置
我们需要做的第一步是告诉 PyCharm 如何连接 Docker。为此,打开 PyCharm 设置(PyCharm > Preferences
用于 Mac 用户,或者File > Settings
用于 Windows 和 Linux 用户),然后展开“构建、执行、部署”设置。单击“Docker ”,然后单击“+”按钮创建新的 Docker 配置。
对于 Mac,选择Docker for Mac
选项。然后应用更改。
配置远程解释器
现在我们已经设置了 Docker 配置,是时候将 Docker Compose 配置为远程解释器了。假设你已经打开了一个项目,再次打开设置,展开“项目:<your-project-name>
”设置,点击“Python 解释器”。单击齿轮图标,然后选择“添加”。
在下一个对话框中,在左窗格中选择“Docker Compose ”,并在“Server”字段中选择您在前面步骤中创建的 Docker 配置。“配置文件”字段应该指向 Docker 合成文件,而“服务”字段应该指向 Docker 合成文件中的 web 应用程序服务。
例如,如果您的 Docker Compose 文件如下所示,那么您会希望指向web
服务:
`version: '3.7' services: web: build: ./app command: python manage.py runserver 0.0.0.0:8000 volumes: - ./app/:/usr/src/app/ ports: - 8008:8000 env_file: - ./.env.dev depends_on: - db db: image: postgres:12.0-alpine volumes: - postgres_data:/var/lib/postgresql/data/ environment: - POSTGRES_USER=hello_django - POSTGRES_PASSWORD=hello_django - POSTGRES_DB=hello_django_dev volumes: postgres_data:`
调试器专门附加到 web 服务。当我们稍后在 PyCharm 中运行配置时,Docker Compose 文件中的所有其他服务都将启动
单击“确定”应用更改。
回到“Python 解释器”设置对话框,你现在应该看到项目有正确的远程解释器。
关闭设置。
创建运行/调试配置
既然我们已经将 PyCharm 配置为能够连接到 Docker,并基于 Docker Compose 文件创建了一个远程解释器配置,那么我们就可以创建一个运行/调试配置了。
点击“添加配置...”PyCharm 窗口顶部的按钮。
接下来点击“+”按钮并选择“Django 服务器”。
为配置命名。在这个配置对话框中,重要的是将“主机”字段设置为0.0.0.0
。
单击“确定”保存配置。我们现在可以在 PyCharm 窗口的顶部看到运行/调试配置,以及运行、调试等按钮。)已启用。
如果现在在 Django 应用程序中设置断点,并按下运行/调试配置旁边的调试按钮,就可以调试 Docker 容器中运行的 Django 应用程序了。
结论
在本教程中,我们向您展示了如何配置 PyCharm 来调试运行在 Docker 中的 Django 应用程序。有了它,您现在不仅可以调试您的视图和模型等等,还可以设置断点和调试您的模板代码。
提示:想进一步增强您的调试能力吗?PyCharm 还允许您设置条件断点!
用 VS 代码调试一个容器化的 Django 应用程序
本教程着眼于如何用 Visual Studio 代码 (VS 代码)调试一个容器化的 Django 应用。
目标
学完本教程后,您应该能够:
- 创建 VS 代码运行配置以附加到 Docker 容器
- 修改 manage.py 来启动一个Debug py(Python Tools for Visual Studio Debug Server)调试服务器
- 用 VS 代码调试一个容器化的 Django 项目
创建运行配置
如果你还没有为你的项目设置一个运行配置,添加一个。vscode/launch.json 文件:
`{ "version": "0.2.0", "configurations": [ { "name": "Run Django", "type": "python", "request": "attach", "pathMappings": [ { "localRoot": "${workspaceFolder}/app", "remoteRoot": "/usr/src/app" } ], "port": 3000, "host": "127.0.0.1", } ] }`
确保更新localRoot
和remoteRoot
值,VS 代码使用它们在您的工作区和远程主机的文件系统之间映射源文件。尽管这些值会因项目设置方式的不同而有所不同,但您通常可以从 Docker 卷配置中获得这些信息。
例如,假设您的 Docker 合成文件中有以下配置:
`volumes: - ./app/:/usr/src/app/`
本地文件夹路径./app/
是localRoot
应该设置的路径(如"${workspaceFolder}/app"
),而remoteRoot
应该设置为容器内的文件夹(如"/usr/src/app"
)。值得注意的是,容器中的这个文件夹也可能是您的工作目录:
表示我们想要将 VS 代码的调试器连接到一个已经在运行的进程。在上面的配置中,我们告诉它连接到 127.0.0.1 上的端口 3000。我们将很快配置 debugpy 在127.0.0.1:3000
上运行。
完成后,单击最左侧活动栏中的“Run”图标。您现在应该可以在侧边栏的播放按钮旁边看到Run Django
配置:
修改 manage.py
将 VS 代码设置为附加到 debugpy,让我们将它集成到我们的应用程序中。
首先,将 debugpy 包添加到您的需求文件中:
由于 debugpy 与 Django 应用程序一起运行,我们需要将其配置为在我们的 manage.py 文件中运行:
`from django.conf import settings
if settings.DEBUG:
if os.environ.get('RUN_MAIN') or os.environ.get('WERKZEUG_RUN_MAIN'):
import debugpy
debugpy.listen(("0.0.0.0", 3000))
print('Attached!')`
您的文件将类似于:
`#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
# start new section
from django.conf import settings
if settings.DEBUG:
if os.environ.get('RUN_MAIN') or os.environ.get('WERKZEUG_RUN_MAIN'):
import debugpy
debugpy.listen(("0.0.0.0", 3000))
print('Attached!')
# end new section
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()`
这里,我们首先确定项目是否在DEBUG
模式下运行。如果是这样,那么我们要确保调试器没有被附加,如果它是 Django 的重载(如果你在服务器运行时修改了一些代码)。
debugpy.listen()
方法启动调试服务器。您还可以阻塞执行,直到调试器附加了wait_for_client()
:
`from django.conf import settings
if settings.DEBUG:
if os.environ.get('RUN_MAIN') or os.environ.get('WERKZEUG_RUN_MAIN'):
import debugpy
debugpy.listen(("0.0.0.0", 3000))
debugpy.wait_for_client()
print('Attached!')`
因为 debugpy 将在端口 3000 上运行,所以您需要向主机公开该端口。如果您正在使用 Docker Compose,您可以像这样公开端口:
`version: '3.8' services: web: build: ./app command: python manage.py runserver 0.0.0.0:8000 volumes: - ./app/:/usr/src/app/ ports: - 8000:8000 - 3000:3000`
如果您没有使用 Compose,请确保在运行容器时公开端口:
`$ docker run -d -p 8000:8000 -p 3000:3000 web`
调试容器化的 Django 应用
在构建新的映像来安装 debugpy 之后,启动新的容器。
在代码中的某个地方设置断点。然后在 VS 代码中再次打开“Run”视图,确保我们之前创建的Run Django
配置被选中。单击“播放”按钮开始调试会话。
现在,您应该能够到达断点并开始调试运行在 Docker 容器中的 Django 应用程序了。
结论
在本教程中,我们向您展示了如何配置 VS 代码来调试运行在 Docker 中的 Django 应用程序。
提示:如果您使用的是 Python 3.7 或更高版本,debugpy 也支持 Python 的
breakpoint()
函数。
在 DigitalOcean 的应用平台上运行 Django
原文:https://testdriven.io/blog/django-digitalocean-app-platform/
DigitalOcean 的 App Platform 是一个平台即服务(PaaS)产品,它(很像 Heroku)允许你从 git 库部署应用程序。
本文着眼于如何将 Django 应用程序部署到 DigitalOcean 的应用程序平台。
为什么要用 DigitalOcean 的 App 平台?
DigitalOcean 的应用程序平台的目标是简化部署,以便您可以专注于应用程序开发,而不是管理基础架构。它非常适合日常的 Django 应用程序,使用 Postgres(或 MySQL)实现持久性,使用 Redis 实现芹菜消息代理。也就是说,如果你的应用确实需要你访问底层基础设施,你可能会想避开应用平台,转而使用基础设施即服务(IaaS)解决方案,如 DigitalOcean Droplet 。
积极方面:
- 不可变部署
- 零停机部署
- 内置持续部署
- 默认情况下,Cloudflare CDN
- 垂直和水平缩放(还没有自动缩放和)
- 支持 Docker
底片:
- 文档需要改进(自从这篇文章最初发表以来,它已经变得好多了)
- 与 Heroku 相比,它几乎没有扩展(比如日志和监控服务)
是的,它比 IaaS 解决方案更贵,但是如果您的应用没有复杂的基础架构要求,那么您将节省时间和金钱,因为您不需要雇用运营工程师。
项目设置
我们将部署一个基本的 Django 应用程序。如果你想跟进,你可以使用预先创建的演示应用程序或你自己的应用程序。
演示应用程序
请随意从数字海洋-应用程序-平台-django repo 的 v1 分支中克隆演示应用程序:
`$ git clone https://github.com/testdrivenio/digitalocean-app-platform-django --branch v1
$ cd digitalocean-app-platform-django
$ git checkout -b main`
确保创建一个新的 GitHub repo,并将代码推送到主分支。
你的应用
如果您有自己想要部署的 Django 应用程序,为了简单起见,请更新您的 settings.py 文件中的以下配置:
`SECRET_KEY = 'please-change-me'
DEBUG = True
ALLOWED_HOSTS = ['*']`
在初始部署之后,我们将了解如何更改其中的每一项,以便您的应用程序更加安全。
另外,添加以下静态资产配置:
`STATIC_URL = '/static/'
STATIC_ROOT = Path(BASE_DIR).joinpath('staticfiles')
STATICFILES_DIRS = (Path(BASE_DIR).joinpath('static'),)`
数字海洋
让我们设置 DigitalOcean 来使用我们的应用程序。
首先,你需要注册一个数字海洋账户(如果你还没有的话)。接下来,从数字海洋仪表盘,点击侧边栏中的“应用”链接,点击“创建应用”,然后链接你的 GitHub 账户。您可能希望只授予对本文前面创建的单个回购的访问权限,而不是对所有回购的访问权限:
完成后,选择该存储库,保持选中“自动部署”以进行连续部署,然后单击“下一步”:
在应用配置页面上,DigitalOcean 应该已经检测到该应用是基于 Python 的(基于 requirements.txt 文件的存在)。
在幕后,DigitalOcean 使用云原生构建包来运行您的应用程序。
将运行命令改为gunicorn hello_django.wsgi:application --bind 0.0.0.0:8080 --worker-tmp-dir /dev/shm
,点击“下一步”:
为您的应用程序命名并选择地区。请继续使用默认值,分别是回购名称和纽约。
点击“下一步”。
坚持“基本”计划,启动应用程序。
在下一个屏幕上,单击“转到构建日志”按钮查看实时部署日志:
构建和部署应该需要几分钟时间。完成后,单击 splash 消息中的“Live App”链接,在新的浏览器选项卡中打开您正在运行的应用程序:
您应该看到:
数据库ˌ资料库
接下来,让我们配置 Postgres,而不是使用 SQLite。
从您的应用程序的仪表板中,单击“操作”下拉菜单,然后选择“创建/附加数据库”。坚持使用名为“db”的默认“开发数据库”。单击“创建并附加”。
这将自动配置一个DATABASE_URL
环境变量并重新部署应用程序。这大约需要 10 分钟,因为需要配置数据库。部署完成后,您可以从控制台查看环境变量:
将以下变量添加到设置文件中,以读取环境变量:
`DATABASE_URL = os.getenv('DATABASE_URL', None)`
接下来,像这样更新数据库配置以使用DATABASE_URL
(如果它存在)并配置 Postgres:
`if not DATABASE_URL:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
else:
db_info = urlparse(DATABASE_URL)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'db',
'USER': db_info.username,
'PASSWORD': db_info.password,
'HOST': db_info.hostname,
'PORT': db_info.port,
'OPTIONS': {'sslmode': 'require'},
}
}`
将适当的导入添加到顶部:
`import os
from urllib.parse import urlparse`
将 psycopg2 添加到需求中:
确保它在本地有效。将您的代码提交并推送到 GitHub 以触发新的部署。
最后,部署完成后,跳回“控制台”应用您的迁移(通过python manage.py migrate
)并创建一个超级用户(通过python manage.py createsuperuser
):
您还可以验证正在使用 Postgres 配置:
`$ python manage.py shell
>>> from django.conf import settings
>>> settings.DATABASES`
环境变量
接下来,让我们通过环境变量配置以下设置来加强应用程序:
SECRET_KEY
DEBUG
ALLOWED_HOSTS
更新 settings.py 中的以下变量:
`SECRET_KEY = os.getenv('SECRET_KEY', 'please-change-me')
DEBUG = os.getenv('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '127.0.0.1,localhost').split(',')`
再次提交您的代码,并将其推送到 GitHub,以触发另一个新的部署。
部署完成后,单击“操作”下拉菜单,然后单击“管理环境变量”。然后,在“应用程序级环境变量”部分添加以下变量:
SECRET_KEY
-[[email protected]](/cdn-cgi/l/email-protection)%9$#3#)9cff)jdcl&[[email protected]](/cdn-cgi/l/email-protection)(u!y)[[email protected]](/cdn-cgi/l/email-protection)!e2s8!(jrrta_u+
DEBUG
-False
ALLOWED_HOSTS
-digitalocean-app-platform-django-3bmno.ondigitalocean.app
请务必生成一个新的密钥,并用您的域替换
digitalocean-app-platform-django-3bmno.ondigitalocean.app
。
更新后,应用程序会自动重新部署。
静态文件
我们将使用whiten noise管理静态资产。
将其添加到 requirements.txt 文件中:
更新 settings.py 中的MIDDLEWARE
列表:
`MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # new
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]`
然后,要启用压缩和缓存支持,请添加:
`STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'`
提交。推送至 GitHub。一旦部署完成,确保静态资产在管理页面上工作。
芹菜
虽然在这篇文章中我们不会演示如何设置 Celery ,但是 DigitalOcean 使配置 Redis 变得很容易,它可以被用作你的消息代理和结果后端。然后,您可以通过“操作”-“T4”-“创建资源”,将芹菜工人设置为“工人”组件:
开发工作流程
启用 autodeploy 后,任何时候对主分支的更改被添加到 GitHub 上的源代码控制中,应用程序都会自动重新部署。默认情况下启用零停机,终端用户在重新部署时不会经历任何停机。
最好使用某种风格的功能分支工作流,这样你就可以在主分支之外的另一个分支上将代码签入 GitHub,这样你就可以在修改投入生产之前测试你的应用程序。
示例:
- 创建新分支
- 对代码进行更改
- 通过 GitHub 动作将您的代码提交并推送到 GitHub 以触发 CI 构建
- 针对主要分支机构打开一个 PR
- CI 构建完成后进行合并,以更新生产
结论
这篇文章简要介绍了如何在 DigitalOcean 的应用平台上运行 Django 应用程序。
总的来说,App Platform 是一个强大的 PaaS 解决方案,您应该为您的 Django 应用程序以及其他解决方案进行评估,例如:
在这一点上,App Platform 的主要缺点是它缺少一些其他 PaaS 解决方案提供的功能和扩展。也就是说,根据这篇文章的说法,有许多新功能正在开发中,比如计划作业、部署预览、VPC 集成和部署回滚。
如果您决定使用 App 平台,请确保:
确保删除你的 Django 应用和数据库,这样你就不会产生任何额外的费用。
你可以在digital ocean-app-platform-djangorepo 中找到最终代码。
在数字海洋空间存储 Django 静态和媒体文件
数字海洋空间是一个 S3 兼容的对象存储服务,内置了内容交付网络 (CDN)。像亚马逊 S3 一样,它提供了一种简单、划算的方式来存储静态文件。
本教程展示了如何配置 Django,通过 DigitalOcean Spaces 加载和提供静态和用户上传的公共和私有媒体文件。
更喜欢用 S3?查看亚马逊 S3 上存储的 Django 静态和媒体文件。
数字海洋空间
数字海洋空间 vs S3 自动气象站?
如前所述,数字海洋空间是一种对象存储服务。与 S3 相比,数字海洋空间:
- 更容易使用
- 具有更简单、更可预测的定价模式
- 有更好的文档
也就是说,DigitalOcean Spaces 没有 S3 灵活,区域也少得多。
数字海洋空间比 S3 更容易建立和使用。添加 CDN 需要在用户界面上点击几下按钮。另外,它和 S3 一样对开发者友好。如果你已经在使用 DigitalOcean,你一定要使用它。
入门指南
首先,你需要注册一个数字海洋账户(如果你还没有的话),然后生成空间访问密钥,这样你就可以访问数字海洋空间 API 。
继续创建密钥。您应该有一个访问密钥 ID 和一个秘密访问密钥。有了它们,您可以使用 Boto3 与 Spaces API 进行交互。
示例:
`import boto3
# configure session and client
session = boto3.session.Session()
client = session.client(
's3',
region_name='nyc3',
endpoint_url='https://nyc3.digitaloceanspaces.com',
aws_access_key_id='YOUR_ACCESS_KEY_ID',
aws_secret_access_key='YOUR_SECRET_ACCESS_KEY',
)
# create new bucket
client.create_bucket(Bucket='your-bucket-name')
# upload file
with open('test.txt', 'rb') as file_contents:
client.put_object(
Bucket='your-bucket-name',
Key='test.txt',
Body=file_contents,
)
# download file
client.download_file(
Bucket='your-bucket-name',
Key='test.txt',
Filename='tmp/test.txt',
)`
更多示例,请查看使用数字海洋空间和自动气象站 S3 软件开发套件。
接下来,从数字海洋控制面板,点击右上角的“创建”,然后点击下拉菜单中的“空间”。选择一个地区,启用一个 CDN ,选择“限制文件列表”,为您的存储桶创建一个唯一的名称。然后,创建桶。
您应该会看到类似如下的内容:
Django 项目
从 GitHub 上的django-digital ocean-spacesrepo 中克隆基础项目:
`$ git clone -b base https://github.com/testdrivenio/django-digitalocean-spaces
$ cd django-digitalocean-spaces`
从项目根目录,创建映像并启动 Docker 容器:
`$ docker-compose up -d --build`
构建完成后,收集静态文件:
`$ docker-compose exec web python manage.py collectstatic`
然后,导航到 http://localhost:1337 :
您应该能够上传一个图像,然后在http://localhost:1337/media/IMAGE _ FILE _ NAME查看该图像。
公共与私有的单选按钮不起作用。我们将在本教程的后面添加这个功能。暂时忽略它们。
在继续之前,快速浏览一下项目结构:
`├── .gitignore
├── LICENSE
├── README.md
├── app
│ ├── Dockerfile
│ ├── hello_django
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── manage.py
│ ├── mediafiles
│ ├── requirements.txt
│ ├── static
│ │ └── bulma.min.css
│ ├── staticfiles
│ └── upload
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── upload.html
│ ├── tests.py
│ └── views.py
├── docker-compose.yml
└── nginx
├── Dockerfile
└── nginx.conf`
想学习如何构建这个项目吗?查看 Postgres、Gunicorn 和 Nginx 的博客文章。
Django 仓库
接下来,让我们安装 django-storages ,这样我们就可以使用 Spaces 作为主要的 django 存储后端,以及 boto3 ,以便与 Spaces API 进行交互。
更新需求文件:
`boto3==1.18.36
Django==3.2
django-storages==1.11.1
gunicorn==20.1.0`
将storages
添加到设置. py 中的INSTALLED_APPS
中:
`INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'upload',
'storages',
]`
更新图像并旋转新容器:
`$ docker-compose up -d --build`
静态文件
接下来,我们需要更新对 settings.py 中静态文件的处理:
`STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = (BASE_DIR / 'static',)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'mediafiles'`
用以下内容替换这些设置:
`USE_SPACES = os.getenv('USE_SPACES') == 'TRUE'
if USE_SPACES:
# settings
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_ENDPOINT_URL = 'https://nyc3.digitaloceanspaces.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
# static settings
AWS_LOCATION = 'static'
STATIC_URL = f'https://{AWS_S3_ENDPOINT_URL}/{AWS_LOCATION}/'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
else:
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = (BASE_DIR / 'static',)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'mediafiles'`
如果您使用的是不同于
NYC3
的地区,请务必更新AWS_S3_ENDPOINT_URL
。
注意USE_SPACES
和STATICFILES_STORAGE
:
USE_SPACES
环境变量用于打开(值为TRUE
)和关闭(值为FALSE
)空间存储。因此,您可以配置两个 Docker 合成文件:一个用于开发,不带空格,另一个用于生产,带空格。STATICFILES_STORAGE
设置配置 Django 在运行collectstatic
命令时自动将静态文件添加到 Spaces 桶中。
查看关于数字海洋和亚马逊 S3 的 django-storages 官方文档,了解更多关于上述设置和配置的信息。
向 docker-compose.yml 文件中的web
服务添加适当的环境变量:
`web: build: ./app command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000' volumes: - ./app/:/usr/src/app/ - static_volume:/usr/src/app/staticfiles - media_volume:/usr/src/app/mediafiles expose: - 8000 environment: - SECRET_KEY=please_change_me - SQL_ENGINE=django.db.backends.postgresql - SQL_DATABASE=postgres - SQL_USER=postgres - SQL_PASSWORD=postgres - SQL_HOST=db - SQL_PORT=5432 - DATABASE=postgres - USE_SPACES=TRUE - AWS_ACCESS_KEY_ID=UPDATE_ME - AWS_SECRET_ACCESS_KEY=UPDATE_ME - AWS_STORAGE_BUCKET_NAME=UPDATE_ME depends_on: - db`
不要忘记用您刚刚创建的用户密钥和
AWS_STORAGE_BUCKET_NAME
一起更新AWS_ACCESS_KEY_ID
和AWS_SECRET_ACCESS_KEY
。
要测试、重新构建和运行容器:
`$ docker-compose down -v
$ docker-compose up -d --build`
收集静态文件:
`$ docker-compose exec web python manage.py collectstatic`
由于文件正在被上传到共享空间存储桶,所以这将花费比以前长得多的时间。
http://localhost:1337 应该仍能正确渲染:
查看页面源代码以确保 CSS 样式表是从 Spaces 桶中提取的:
验证静态文件可以在数字海洋控制面板的“静态”子文件夹中看到:
媒体上传仍然会影响本地文件系统,因为我们只为静态文件配置了空间。我们将很快处理媒体上传。
最后,将USE_SPACES
的值更新为FALSE
并重新构建映像,以确保 Django 使用本地文件系统来存储静态文件。完成后,将USE_SPACES
变回TRUE
。
为了防止用户覆盖现有的静态文件,媒体文件上传应该放在桶中不同的子文件夹中。我们将通过为每种类型的存储创建自定义存储类来解决这个问题。
将名为 storage_backends.py 的新文件添加到“app/hello_django”文件夹中:
`from django.conf import settings
from storages.backends.s3boto3 import S3Boto3Storage
class StaticStorage(S3Boto3Storage):
location = 'static'
default_acl = 'public-read'
class PublicMediaStorage(S3Boto3Storage):
location = 'media'
default_acl = 'public-read'
file_overwrite = False`
对 settings.py 进行以下更改:
`USE_SPACES = os.getenv('USE_SPACES') == 'TRUE'
if USE_SPACES:
# settings
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_ENDPOINT_URL = 'https://nyc3.digitaloceanspaces.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
# static settings
AWS_LOCATION = 'static'
STATIC_URL = f'https://{AWS_S3_ENDPOINT_URL}/{AWS_LOCATION}/'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
# public media settings
PUBLIC_MEDIA_LOCATION = 'media'
MEDIA_URL = f'https://{AWS_S3_ENDPOINT_URL}/{PUBLIC_MEDIA_LOCATION}/'
DEFAULT_FILE_STORAGE = 'hello_django.storage_backends.PublicMediaStorage'
else:
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'mediafiles'
STATICFILES_DIRS = (BASE_DIR / 'static',)`
随着DEFAULT_FILE_STORAGE
设置现在被设置,所有的文件域将他们的内容上传到空间桶。继续之前,请检查其余设置。
接下来,让我们对upload
应用程序做一些修改。
app/upload/models.py :
`from django.db import models
class Upload(models.Model):
uploaded_at = models.DateTimeField(auto_now_add=True)
file = models.FileField()`
app/upload/views.py :
`from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.shortcuts import render
from .models import Upload
def image_upload(request):
if request.method == 'POST':
image_file = request.FILES['image_file']
image_type = request.POST['image_type']
if settings.USE_SPACES:
upload = Upload(file=image_file)
upload.save()
image_url = upload.file.url
else:
fs = FileSystemStorage()
filename = fs.save(image_file.name, image_file)
image_url = fs.url(filename)
return render(request, 'upload.html', {
'image_url': image_url
})
return render(request, 'upload.html')`
创建新的迁移文件,然后构建新的映像:
`$ docker-compose exec web python manage.py makemigrations
$ docker-compose down -v
$ docker-compose up -d --build
$ docker-compose exec web python manage.py migrate`
测试一下!在 http://localhost:1337 上传一张图片。图像应该上传到空间(到媒体子文件夹)和image_url
应该包括 S3 网址:
向 storage_backends.py 中添加一个新类:
`class PrivateMediaStorage(S3Boto3Storage):
location = 'private'
default_acl = 'private'
file_overwrite = False
custom_domain = False`
添加适当的设置:
`USE_SPACES = os.getenv('USE_SPACES') == 'TRUE'
if USE_SPACES:
# settings
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_ENDPOINT_URL = 'https://nyc3.digitaloceanspaces.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
# static settings
AWS_LOCATION = 'static'
STATIC_URL = f'https://{AWS_S3_ENDPOINT_URL}/{AWS_LOCATION}/'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
# public media settings
PUBLIC_MEDIA_LOCATION = 'media'
MEDIA_URL = f'https://{AWS_S3_ENDPOINT_URL}/{PUBLIC_MEDIA_LOCATION}/'
DEFAULT_FILE_STORAGE = 'hello_django.storage_backends.PublicMediaStorage'
# private media settings
PRIVATE_MEDIA_LOCATION = 'private'
PRIVATE_FILE_STORAGE = 'hello_django.storage_backends.PrivateMediaStorage'
else:
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'mediafiles'
STATICFILES_DIRS = (BASE_DIR / 'static',)`
在 app/upload/models.py 中创建新模型:
`from django.db import models
from hello_django.storage_backends import PublicMediaStorage, PrivateMediaStorage
class Upload(models.Model):
uploaded_at = models.DateTimeField(auto_now_add=True)
file = models.FileField(storage=PublicMediaStorage())
class UploadPrivate(models.Model):
uploaded_at = models.DateTimeField(auto_now_add=True)
file = models.FileField(storage=PrivateMediaStorage())`
然后,更新视图:
`from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.shortcuts import render
from .models import Upload, UploadPrivate
def image_upload(request):
if request.method == 'POST':
image_file = request.FILES['image_file']
image_type = request.POST['image_type']
if settings.USE_SPACES:
if image_type == 'private':
upload = UploadPrivate(file=image_file)
else:
upload = Upload(file=image_file)
upload.save()
image_url = upload.file.url
else:
fs = FileSystemStorage()
filename = fs.save(image_file.name, image_file)
image_url = fs.url(filename)
return render(request, 'upload.html', {
'image_url': image_url
})
return render(request, 'upload.html')`
同样,创建迁移文件,重新构建映像,并启动新容器:
`$ docker-compose exec web python manage.py makemigrations
$ docker-compose down -v
$ docker-compose up -d --build
$ docker-compose exec web python manage.py migrate`
为了测试,在 http://localhost:1337 上传一个私有镜像。像公共图像一样,图像应该上传到 Spaces(私有子文件夹)中,并且image_url
应该包括 Spaces URL 以及以下查询字符串参数:
- AWSAccessKeyId
- 签名
- 期满
本质上,我们创建了一个临时的、已签名的 URL,用户可以在特定的时间段内访问它。没有参数,您将无法直接访问它。
结论
这篇文章向您展示了如何在 DigitalOcean Spaces 上创建一个 bucket,并设置 Django 向 Spaces 上传和提供静态文件和媒体上传。
通过使用空格,您可以:
- 增加静态文件和媒体文件的可用空间
- 减轻您自己的服务器的压力,因为它不再需要提供文件
- 可以限制对特定文件的访问
- 可以利用 CDN
如果我们遗漏了什么,或者您有任何其他提示和技巧,请告诉我们。你可以在 django-digital ocean-spacesrepo 中找到最终代码。
用 Docker 将 Django 部署到 AWS,让我们加密
在本教程中,我们将使用 Docker 将 Django 应用程序部署到 AWS EC2。该应用程序将运行在 HTTPS Nginx 代理后面,该代理使用 Let's 加密 SSL 证书。我们将使用 AWS RDS 来服务我们的 Postgres 数据库,并使用 AWS ECR 来存储和管理我们的 Docker 图像。
姜戈码头系列:
目标
本教程结束时,您将能够:
- 设置新的 EC2 实例
- 在 EC2 实例上安装 Docker
- 配置和使用弹性 IP 地址
- 设置 IAM 角色
- 利用 Amazon 弹性容器注册(ECR)图像注册来存储构建的图像
- 为数据持久性配置 AWS RDS
- 配置 AWS 安全组
- 使用 Docker 将 Django 部署到 AWS EC2
- 在 HTTPS Nginx 代理后面运行 Django 应用程序,让我们加密 SSL 证书
先决条件
这篇文章建立在用 Postgres、Gunicorn 和 Nginx 和保护容器化 Django 应用程序的基础上,用 Let's Encrypt 文章。
它假设您可以:
- 将 Django 应用与 Postgres、Nginx 和 Gunicorn 一起容器化。
- 用加密 SSL 证书来保护运行在 HTTPS Nginx 代理后面的容器化 Django 应用。
- 使用 SSH 连接到远程服务器,使用 SCP 将文件复制到服务器。
AWS EC2
首先,创建一个 AWS 账户,如果你还没有的话。
接下来,导航到 EC2 控制台并点击启动实例:
使用Ubuntu Server 18.04 LTS(HVM)作为服务器镜像(AMI):
在下一步中,坚持使用 t2.micro 实例。点击下一步:配置实例详情:
在配置实例细节步骤,让一切保持原样以保持简单。然后点击下一步几次,直到你在配置安全组步骤。
选择创建新的安全组,将名称和描述设置为django-ec2
,并添加两条规则:
- HTTP ->任何地方
- HTTPS ->任何地方
颁发证书和访问应用程序需要这些规则。
安全组入站规则用于限制从互联网对您的实例的访问。除非您有一些额外的安全需求,否则您可能希望允许来自任何地方的 HTTP 和 HTTPS 流量来托管 web 应用程序。为了设置和部署,必须允许 SSH 连接到实例。
点击查看并启动。在下一个屏幕上,点击发射。
系统会提示您选择一个密钥对。您需要它来通过 SSH 连接到您的实例。选择创建一个新的密钥对,并将其命名为djangoletsencrypt
。然后点击下载密钥对。下载密钥对后,点击启动实例:
该实例将需要几分钟时间才能启动。
配置 EC2 实例
在本节中,我们将在实例上安装 Docker,添加一个弹性 IP,并配置一个 IAM 角色。
安装 Docker
导航回 EC2 控制台,选择新创建的实例,并获取公共 IP 地址:
使用我们在“AWS EC2”步骤中下载的.pem
键连接到您的 EC2 实例。
您的
.pem
可能被下载到了像~/Downloads/djangoletsencrypt . PEM 这样的路径中。如果您不确定在哪里存储它,请将它移到 ~/中。ssh 目录。您可能还需要更改权限,即chmod 400 -i /path/to/your/djangoletsencrypt.pem
。
首先安装 Docker 的最新版本和 Docker Compose 的 1.29.2 版本:
`$ sudo apt update
$ sudo apt install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
$ sudo apt update
$ sudo apt install docker-ce
$ sudo usermod -aG docker ${USER}
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker -v
Docker version 20.10.8, build 3967b7d
$ docker-compose -v
docker-compose version 1.29.2, build 5becea4c`
安装 AWS CLI
首先,安装 unzip:
下载 AWS CLI ZIP:
`$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"`
解压缩其内容:
安装 AWS CLI:
验证安装:
弹性 IP
默认情况下,实例每次启动和重新启动时都会收到新的公共 IP 地址。
弹性 IP 允许您为 EC2 实例分配静态 IP,因此 IP 始终保持不变,并且可以在实例之间重新关联。建议为您的生产设置使用一个。
导航到弹性 IP,点击分配弹性 IP 地址:
然后,点击分配:
点击关联此弹性 IP 地址:
选择您的实例并点击关联:
IAM 角色
在部署期间,我们将使用 AWS ECR 将图像从 AWS ECR 拉至我们的 EC2 实例。因为我们不允许公众访问 ECR 上的 Docker 映像,所以您需要创建一个 IAM 角色,该角色拥有从 ECR 中提取 Docker 映像并将其附加到 EC2 实例的权限。
导航到 IAM 控制台。
点击左侧工具条中的角色,然后创建角色:
选择 AWS 服务和 EC2 ,然后点击下一步:权限:
在搜索框中输入container
,选择amazonec 2 containerregistrypoweruser策略,点击下一步:标签:
点击下一页:回顾。使用django-ec2
作为名称,点击创建角色:
现在您需要将新角色附加到您的 EC2 实例。
回到 EC2 控制台,点击实例,然后选择你的实例。点击动作下拉菜单->实例设置->附加/替换 IAM 角色:
选择 django-ec2 角色,然后点击应用。
点击关闭。
添加 DNS 记录
为您正在使用的域向 DNS 添加一个 A 记录,以指向您的 EC2 实例的公共 IP。
它是您关联到实例的弹性 IP。
AWS ECR
Amazon Elastic Container Registry(ECR)是一个完全托管的 Docker 图像注册表,可以让开发者轻松存储和管理图像。对于私有映像,通过 IAM 用户和角色来管理访问。
导航到集控室控制台。点击工具条中的库,然后点击创建库:
将名称设置为django-ec2
并点击创建存储库:
AWS RDS
现在我们可以配置一个 RDS Postgres 数据库。
虽然您可以在容器中运行自己的 Postgres 数据库,但由于数据库是关键服务,添加额外的层,如 Docker,会增加生产中不必要的风险。为了简化次要版本更新、定期备份和扩展等任务,建议使用托管服务。所以,我们将使用 RDS 。
导航至 RDS 控制台。点击创建数据库:
使用自由层模板选择最新版本的 Postgres:
在设置下,设置:
- 数据库实例标识符:
djangoec2
- 主用户名:
webapp
- 选择自动生成密码
坚持使用默认设置:
- 数据库实例大小
- 储存;储备
- 可用性和耐用性
跳到连接部分,设置以下内容:
- 虚拟私有云(VPC):默认
- 子网组:默认
- 可公开访问:否
- VPC 安全组:
django-ec2
- 数据库端口:5432
让数据库认证保持原样。
打开附加配置,将初始数据库名称改为djangoec2
:
保持其他设置不变。
最后,点击创建数据库。
点击查看凭证详情查看为 webapp 用户生成的密码:
请将此密码存放在安全的地方。您需要在这里将它提供给 Django 应用程序。
该实例将需要几分钟的时间才能启动。启动后,单击新创建的数据库的 DB 标识符以查看其详细信息。记下数据库端点;你需要在你的 Django 应用中设置它。
AWS 安全组
在 EC2 控制台中,点击侧边栏中的安全组。找到并点击 django-ec2 组的 ID 以编辑其详细信息。
点击编辑入站规则:
添加允许 Postgres 连接到该安全组内实例的入站规则。为此:
- 点击添加规则
- 为规则类型选择 PostgreSQL
- 为规则源选择自定义
- 点击搜索字段并选择 django-ec2 安全组
- 点击保存规则
为了限制对数据库的访问,只允许来自同一个安全组内的实例的连接。我们的应用程序可以连接,因为我们为 RDS 和 ec2 实例设置了相同的安全组 django-ec2 。因此,不允许其他安全组中的实例进行连接。
项目配置
随着 AWS 基础设施的建立,我们现在需要在部署 Django 项目之前在本地配置它。
首先,克隆 GitHub 项目报告的内容:
`$ git clone https://github.com/testdrivenio/django-on-docker-letsencrypt django-on-docker-letsencrypt-aws
$ cd django-on-docker-letsencrypt-aws`
这个库包含了部署 Dockerized Django 和加密 HTTPS 证书所需的一切。
复合坞站
首次部署应用程序时,您应该遵循以下两个步骤来避免证书问题:
- 首先从 Let's Encrypt 的登台环境中颁发证书
- 然后,当一切按预期运行时,切换到 Let's Encrypt 的生产环境
对于测试,更新docker-compose . staging . yml .文件,如下所示:
`version: '3.8' services: web: build: context: ./app dockerfile: Dockerfile.prod image: <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:web command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000 volumes: - static_volume:/home/app/web/staticfiles - media_volume:/home/app/web/mediafiles expose: - 8000 env_file: - ./.env.staging nginx-proxy: container_name: nginx-proxy build: nginx image: <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:nginx-proxy restart: always ports: - 443:443 - 80:80 volumes: - static_volume:/home/app/web/staticfiles - media_volume:/home/app/web/mediafiles - certs:/etc/nginx/certs - html:/usr/share/nginx/html - vhost:/etc/nginx/vhost.d - /var/run/docker.sock:/tmp/docker.sock:ro depends_on: - web nginx-proxy-letsencrypt: image: jrcs/letsencrypt-nginx-proxy-companion env_file: - ./.env.staging.proxy-companion volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - certs:/etc/nginx/certs - html:/usr/share/nginx/html - vhost:/etc/nginx/vhost.d - acme:/etc/acme.sh depends_on: - nginx-proxy volumes: static_volume: media_volume: certs: html: vhost: acme:`
对于web
和nginx-proxy
服务,更新image
属性以使用来自 ECR 的图像(我们将很快添加)。
示例:
`image: 123456789.dkr.ecr.us-east-1.amazonaws.com/django-ec2:web image: 123456789.dkr.ecr.us-east-1.amazonaws.com/django-ec2:nginx-proxy`
这些值由存储库 URL ( 123456789.dkr.ecr.us-east-1.amazonaws.com
)以及图像名称(django-ec2
)和标签(web
和nginx-proxy
)组成。
为了简单起见,我们使用一个注册表来存储两个图像。我们使用
web
和nginx-proxy
来区分这两者。理想情况下,您应该使用两个注册中心:一个用于web
,一个用于nginx-proxy
。如果你喜欢,请自行更新。
除了image
属性,我们还删除了db
服务(和相关的卷),因为我们使用 RDS 而不是在容器中管理 Postgres。
环境
是时候为web
和nginx-proxy-letsencrypt
容器设置环境文件了。
首先,为web
容器添加一个 .env.staging 文件:
`DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=<YOUR_DOMAIN.COM>
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=djangoec2
SQL_USER=webapp
SQL_PASSWORD=<PASSWORD-FROM-AWS-RDS>
SQL_HOST=<DATABASE-ENDPOINT-FROM-AWS-RDS>
SQL_PORT=5432
DATABASE=postgres
VIRTUAL_HOST=<YOUR_DOMAIN.COM>
VIRTUAL_PORT=8000
LETSENCRYPT_HOST=<YOUR_DOMAIN.COM>`
注意事项:
- 将
<YOUR_DOMAIN.COM>
更改为您的实际域名。 - 更改
SQL_PASSWORD
和SQL_HOST
以匹配 RDS 部分中创建的内容。 - 将
SECRET_KEY
改为某个长的随机字符串。 nginx-proxy
容器需要VIRTUAL_HOST
和VIRTUAL_PORT
来自动创建反向代理配置。LETSENCRYPT_HOST
有没有办法让nginx-proxy-companion
为你的域名颁发加密证书。
出于测试/调试的目的,您可能希望在第一次部署时使用一个
*
来代替DJANGO_ALLOWED_HOSTS
,以简化事情。只是不要忘记在测试完成后限制允许的主机。
其次,添加一个. env . staging . proxy-companion文件,确保更新DEFAULT_EMAIL
值:
`DEFAULT_EMAIL=youremail@yourdomain.com ACME_CA_URI=https://acme-staging-v02.api.letsencrypt.org/directory NGINX_PROXY_CONTAINER=nginx-proxy`
回顾来自的【让我们加密容器化的 Django 应用程序】的让我们加密 Nginx 代理伙伴服务部分,以了解更多关于上述环境变量的信息。
构建和推送 Docker 映像
现在我们准备构建 Docker 图像:
`$ docker-compose -f docker-compose.staging.yml build`
可能需要几分钟来构建。完成后,我们就可以把图像上传到 ECR 了。
首先,假设您已经安装了AWS CLI,并且已经设置了 AWS 凭证,登录 ECR Docker 存储库:
`$ aws ecr get-login-password --region <aws-region> | docker login --username AWS --password-stdin <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com
# aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com`
您应该看到:
然后将图像推送到集控室:
`$ docker-compose -f docker-compose.staging.yml push`
打开您的 django-ec2 ECR 存储库来查看推送的图像:
运行容器
一切都为部署做好了准备。
是时候转移到 EC2 实例了。
假设您在实例上创建了一个项目目录,如/home/Ubuntu/django-on-docker,使用 SCP 复制文件和文件夹:
`$ scp -i /path/to/your/djangoletsencrypt.pem \
-r $(pwd)/{app,nginx,.env.staging,.env.staging.proxy-companion,docker-compose.staging.yml} \
[[email protected]](/cdn-cgi/l/email-protection):/path/to/django-on-docker`
接下来,通过 SSH 连接到您的实例,并移动到项目目录:
`$ ssh -i /path/to/your/djangoletsencrypt.pem [[email protected]](/cdn-cgi/l/email-protection)
$ cd /path/to/django-on-docker`
登录 ECR Docker 存储库。
`$ aws ecr get-login-password --region <aws-region> | docker login --username AWS --password-stdin <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com`
提取图像:
`$ docker pull <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:web
$ docker pull <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:nginx-proxy`
这样,您就可以开始旋转容器了:
`$ docker-compose -f docker-compose.staging.yml up -d`
一旦容器启动并运行,在浏览器中导航到您的域。您应该会看到类似这样的内容:
这是意料之中的。显示该屏幕是因为证书是从暂存环境发布的。
你怎么知道一切是否正常?
点击“高级”,然后点击“继续”。您现在应该可以看到您的应用程序了。上传图像,然后确保您可以在https://yourdomain.com/media/IMAGE_FILE_NAME
查看图像。
颁发生产证书
现在,一切都按预期运行,我们可以切换到 Let's Encrypt 的生产环境。
关闭现有容器并退出实例:
`$ docker-compose -f docker-compose.staging.yml down -v
$ exit`
回到您的本地机器,打开 docker-compose.prod.yml 并进行与您对 staging 版本所做的相同的更改:
- 更新
ìmage
属性,使之与ẁeb
和nginx-proxy
服务的 AWS ECR URLs 相匹配 - 移除
db
服务以及相关的卷
`version: '3.8' services: web: build: context: ./app dockerfile: Dockerfile.prod image: 046505967931.dkr.ecr.us-east-1.amazonaws.com/django-ec2:web command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000 volumes: - static_volume:/home/app/web/staticfiles - media_volume:/home/app/web/mediafiles expose: - 8000 env_file: - ./.env.prod nginx-proxy: container_name: nginx-proxy build: nginx image: 046505967931.dkr.ecr.us-east-1.amazonaws.com/django-ec2:nginx-proxy restart: always ports: - 443:443 - 80:80 volumes: - static_volume:/home/app/web/staticfiles - media_volume:/home/app/web/mediafiles - certs:/etc/nginx/certs - html:/usr/share/nginx/html - vhost:/etc/nginx/vhost.d - /var/run/docker.sock:/tmp/docker.sock:ro depends_on: - web nginx-proxy-letsencrypt: image: jrcs/letsencrypt-nginx-proxy-companion env_file: - ./.env.prod.proxy-companion volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - certs:/etc/nginx/certs - html:/usr/share/nginx/html - vhost:/etc/nginx/vhost.d - acme:/etc/acme.sh depends_on: - nginx-proxy volumes: static_volume: media_volume: certs: html: vhost: acme:`
接下来通过复制 .env.staging 文件创建一个 .env.prod 文件。您不需要对其进行任何更改。
最后,添加一个. env . prod . proxy-companion文件:
`DEFAULT_EMAIL=youremail@yourdomain.com NGINX_PROXY_CONTAINER=nginx-proxy`
再次构建和推送图像:
`$ docker-compose -f docker-compose.prod.yml build
$ aws ecr get-login-password --region <aws-region> | docker login --username AWS --password-stdin <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com
$ docker-compose -f docker-compose.prod.yml push`
使用 SCP 将新文件和文件夹复制到您的实例中:
`$ scp -i /path/to/your/djangoletsencrypt.pem \
$(pwd)/{.env.prod,.env.prod.proxy-companion,docker-compose.prod.yml} \
[[email protected]](/cdn-cgi/l/email-protection):/path/to/django-on-docker`
像前面一样,通过 SSH 连接到您的实例,并移动到项目目录:
`$ ssh -i /path/to/your/djangoletsencrypt.pem [[email protected]](/cdn-cgi/l/email-protection)
$ cd /path/to/django-on-docker`
再次登录您的 ECR Docker 存储库:
`$ aws ecr get-login-password --region <aws-region> | docker login --username AWS --password-stdin <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com`
提取图像:
`$ docker pull <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:web
$ docker pull <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:nginx-proxy`
最后旋转容器:
`$ docker-compose -f docker-compose.prod.yml up -d`
再次导航到您的域。您应该不会再看到警告。
恭喜你。现在,您正在为运行在 AWS EC2 上的 Django 应用程序使用一个生产证书。
想要查看证书创建过程的运行情况,请查看日志:
`$ docker-compose -f docker-compose.prod.yml logs nginx-proxy-letsencrypt`
结论
在本教程中,您将一个容器化的 Django 应用程序部署到 EC2。该应用程序运行在 HTTPS Nginx 代理后面,让我们加密 SSL 证书。您还使用了一个 RDS Postgres 实例,并将 Docker 图像存储在 ECR 上。
下一步是什么?
- 设置 S3 并配置 Django 将静态和媒体文件存储在 Docker 卷之外的桶中
- 在 GitLab 上设置 CI/CD 管道
- 配置一个运行在 EC2 实例上的容器化 Django 应用程序,将日志发送到 Amazon CloudWatch
部署愉快!
姜戈码头系列:
将 Django 与 Postgres、Gunicorn 和 Traefik 一起归档
在本教程中,我们将看看如何用 Postgres 和 Docker 设置 Django。对于生产环境,我们将添加 Gunicorn、Traefik,并进行加密。
项目设置
首先创建一个项目目录:
`$ mkdir django-docker-traefik && cd django-docker-traefik
$ mkdir app && cd app
$ python3.11 -m venv venv
$ source venv/bin/activate`
你可以随意把 virtualenv 和 Pip 换成诗歌或 Pipenv 。更多信息,请查看现代 Python 环境。
接下来,让我们安装 Django 并创建一个简单的 Django 应用程序:
`(venv)$ pip install django==4.1.6
(venv)$ django-admin startproject config .
(venv)$ python manage.py migrate`
运行应用程序:
`(venv)$ python manage.py runserver`
导航到 http://localhost:8000/ 查看 Django 欢迎屏幕。完成后,关闭服务器并退出虚拟环境。也删除虚拟环境。我们现在有了一个简单的 Django 项目。
在“app”目录下创建一个 requirements.txt 文件,并添加 Django 作为依赖项:
因为我们将转移到 Postgres,所以继续从“app”目录中删除 db.sqlite3 文件。
您的项目目录应该如下所示:
`└── app
├── config
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── requirements.txt`
码头工人
安装 Docker ,如果你还没有,那么在“app”目录下添加一个 Dockerfile :
`# app/Dockerfile
# pull the official docker image
FROM python:3.11.2-slim
# set work directory
WORKDIR /app
# set env variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .`
所以,我们从 Python 3.11.2 的slim
Docker 镜像开始。然后我们设置了一个工作目录以及两个环境变量:
PYTHONDONTWRITEBYTECODE
:防止 Python 将 pyc 文件写入磁盘(相当于python -B
选项PYTHONUNBUFFERED
:防止 Python 缓冲 stdout 和 stderr(相当于python -u
选项
最后,我们复制了 requirements.txt 文件,安装了依赖项,并复制了项目。
查看 Docker 针对 Python 开发人员的最佳实践,了解更多关于构造 Docker 文件的信息,以及为基于 Python 的开发配置 Docker 的一些最佳实践。
接下来,将一个 docker-compose.yml 文件添加到项目根:
`# docker-compose.yml version: '3.8' services: web: build: ./app command: python manage.py runserver 0.0.0.0:8000 volumes: - ./app:/app ports: - 8008:8000 environment: - DEBUG=1`
查看合成文件参考,了解该文件如何工作的信息。
建立形象:
构建映像后,运行容器:
导航到 http://localhost:8008 再次查看欢迎页面。
如果这不起作用,通过
docker-compose logs -f
检查日志中的错误。
Postgres
要配置 Postgres,我们需要向 docker-compose.yml 文件添加一个新服务,更新 Django 设置,并安装 Psycopg2 。
首先,向 docker-compose.yml 添加一个名为db
的新服务:
`# docker-compose.yml version: '3.8' services: web: build: ./app command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; python manage.py runserver 0.0.0.0:8000' volumes: - ./app:/app ports: - 8008:8000 environment: - DEBUG=1 - DATABASE_URL=postgresql://django_traefik:[[email protected]](/cdn-cgi/l/email-protection):5432/django_traefik depends_on: - db db: image: postgres:15-alpine volumes: - postgres_data:/var/lib/postgresql/data/ expose: - 5432 environment: - POSTGRES_USER=django_traefik - POSTGRES_PASSWORD=django_traefik - POSTGRES_DB=django_traefik volumes: postgres_data:`
为了在容器的生命周期之外保存数据,我们配置了一个卷。这个配置将把postgres_data
绑定到容器中的“/var/lib/postgresql/data/”目录。
我们还添加了一个环境键来定义默认数据库的名称,并设置用户名和密码。
查看 Postgres Docker Hub 页面的“环境变量”部分了解更多信息。
注意web
服务中的新命令:
`bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; python manage.py runserver 0.0.0.0:8000'`
将持续到 Postgres 完成。一旦启动,python manage.py runserver 0.0.0.0:8000
就会运行。
要配置 Postgres,添加 django-environ ,加载/读取环境变量,添加 Psycopg2 到 requirements.txt :
`Django==4.1.6
django-environ==0.9.0
psycopg2-binary==2.9.5`
初始化 config/settings.py 顶部的 environ:
`# config/settings.py
import environ
env = environ.Env()`
然后,更新DATABASES
字典:
`# config/settings.py
DATABASES = {
'default': env.db(),
}`
django-environ 将自动解析我们添加到 docker-compose.yml 中的数据库连接 URL 字符串:
`DATABASE_URL=postgresql://django_traefik:django_traefik@db:5432/django_traefik`
同样更新DEBUG
变量:
`# config/settings.py
DEBUG = env('DEBUG')`
构建新的映像并旋转两个容器:
`$ docker-compose up -d --build`
运行初始迁移:
`$ docker-compose exec web python manage.py migrate --noinput`
确保创建了默认的 Django 表:
`$ docker-compose exec db psql --username=django_traefik --dbname=django_traefik
psql (15.2)
Type "help" for help.
django_traefik=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
----------------+----------------+----------+------------+------------+-----------------------------------
django_traefik | django_traefik | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | django_traefik | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | django_traefik | UTF8 | en_US.utf8 | en_US.utf8 | =c/django_traefik +
| | | | | django_traefik=CTc/django_traefik
template1 | django_traefik | UTF8 | en_US.utf8 | en_US.utf8 | =c/django_traefik +
| | | | | django_traefik=CTc/django_traefik
(4 rows)
django_traefik=# \c django_traefik
You are now connected to database "django_traefik" as user "django_traefik".
django_traefik=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------------------------+-------+----------------
public | auth_group | table | django_traefik
public | auth_group_permissions | table | django_traefik
public | auth_permission | table | django_traefik
public | auth_user | table | django_traefik
public | auth_user_groups | table | django_traefik
public | auth_user_user_permissions | table | django_traefik
public | django_admin_log | table | django_traefik
public | django_content_type | table | django_traefik
public | django_migrations | table | django_traefik
public | django_session | table | django_traefik
(10 rows)
django_traefik=# \q`
您也可以通过运行以下命令来检查该卷是否已创建:
`$ docker volume inspect django-docker-traefik_postgres_data`
您应该会看到类似如下的内容:
`[
{
"CreatedAt": "2023-02-11T18:01:42Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "django-docker-traefik",
"com.docker.compose.version": "2.12.2",
"com.docker.compose.volume": "postgres_data"
},
"Mountpoint": "/var/lib/docker/volumes/django-docker-traefik_postgres_data/_data",
"Name": "django-docker-traefik_postgres_data",
"Options": null,
"Scope": "local"
}
]`
格尼科恩
接下来,对于生产环境,让我们将 Gunicorn ,一个生产级的 WSGI 服务器,添加到需求文件中:
`Django==4.1.6
django-environ==0.9.0
psycopg2-binary==2.9.5
gunicorn==20.1.0`
由于我们仍然希望在开发中使用 Django 的内置服务器,因此为生产创建一个名为 docker-compose.prod.yml 的新合成文件:
`# docker-compose.prod.yml version: '3.8' services: web: build: ./app command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; gunicorn --bind 0.0.0.0:8000 config.wsgi' ports: - 8008:8000 environment: - DEBUG=0 - DATABASE_URL=postgresql://django_traefik:[[email protected]](/cdn-cgi/l/email-protection):5432/django_traefik depends_on: - db db: image: postgres:15-alpine volumes: - postgres_data_prod:/var/lib/postgresql/data/ expose: - 5432 environment: - POSTGRES_USER=django_traefik - POSTGRES_PASSWORD=django_traefik - POSTGRES_DB=django_traefik volumes: postgres_data_prod:`
如果您有多个环境,您可能希望使用一个docker-compose . override . yml配置文件。使用这种方法,您可以将基本配置添加到 docker-compose.yml 文件中,然后使用 docker-compose.override.yml 文件根据环境覆盖这些配置设置。
记下默认值command
。我们运行的是 Gunicorn,而不是 Django 开发服务器。我们还从web
服务中删除了这个卷,因为我们在生产中不需要它。
将下放到开发容器(以及带有-v
标志的相关卷):
然后,构建生产映像并启动容器:
`$ docker-compose -f docker-compose.prod.yml up -d --build`
运行迁移:
`$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput`
验证django_traefik
数据库是和默认的 Django 表一起创建的。在http://localhost:8008/admin测试管理页面。静态文件加载不正确。这是意料之中的。我们会尽快解决这个问题。
同样,如果容器启动失败,通过
docker-compose -f docker-compose.prod.yml logs -f
检查日志中的错误。
生产文档
创建一个名为 Dockerfile.prod 的新 Dockerfile,用于生产构建:
`# app/Dockerfile.prod
###########
# BUILDER #
###########
# pull official base image
FROM python:3.11-slim as builder
# set work directory
WORKDIR /app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc
# lint
RUN pip install --upgrade pip
RUN pip install flake8==6.0.0
COPY . .
RUN flake8 --ignore=E501,F401 .
# install python dependencies
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.11-slim
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup --system app && adduser --system --group app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# install dependencies
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /app/requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache /wheels/*
# copy project
COPY . $APP_HOME
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app`
在这里,我们使用了一个 Docker 多阶段构建来缩小最终的图像尺寸。本质上,builder
是一个用于构建 Python 轮子的临时图像。然后车轮被复制到最终产品图像中,而builder
图像被丢弃。
您可以将多阶段构建方法更进一步,使用单个 docker 文件,而不是创建两个 docker 文件。思考在两个不同的文件上使用这种方法的利弊。
您是否注意到我们创建了一个非 root 用户?默认情况下,Docker 在容器内部以 root 用户身份运行容器进程。这是一种不好的做法,因为如果攻击者设法突破容器,他们可以获得 Docker 主机的根用户访问权限。如果您是容器中的 root 用户,那么您将是主机上的 root 用户。
更新 docker-compose.prod.yml 文件中的web
服务,用 Dockerfile.prod 构建:
`web: build: context: ./app dockerfile: Dockerfile.prod command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; gunicorn --bind 0.0.0.0:8000 config.wsgi' ports: - 8008:8000 environment: - DEBUG=0 - DATABASE_URL=postgresql://django_traefik:[[email protected]](/cdn-cgi/l/email-protection):5432/django_traefik depends_on: - db`
现在,让我们重建生产映像并启动容器:
`$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput`
Traefik
刚接触 Traefik?查看官方入门指南。
Traefik vs Nginx : Traefik 是一个现代的、HTTP 反向代理和负载平衡器。它经常被比作 Nginx ,一个网络服务器和反向代理。由于 Nginx 主要是一个网络服务器,它可以用来提供网页,也可以作为一个反向代理和负载平衡器。总的来说,Traefik 的启动和运行更简单,而 Nginx 的功能更丰富。
Traefik :
- 反向代理和负载平衡器
- 通过开箱即用的让我们加密,自动发布和更新 SSL 证书
- 将 Traefik 用于简单的、基于 Docker 的微服务
Nginx :
- Web 服务器、反向代理和负载平衡器
- 比 trafik 稍快
- 对复杂的服务使用 Nginx
添加一个名为 traefik.dev.toml 的新文件:
`# traefik.dev.toml # listen on port 80 [entryPoints] [entryPoints.web] address = ":80" # Traefik dashboard over http [api] insecure = true [log] level = "DEBUG" [accessLog] # containers are not discovered automatically [providers] [providers.docker] exposedByDefault = false`
在这里,由于我们不想公开db
服务,我们将 exposedByDefault 设置为false
。要手动公开服务,我们可以将"traefik.enable=true"
标签添加到 Docker 组合文件中。
接下来,更新 docker-compose.yml 文件,以便 Traefik 发现我们的web
服务并添加一个新的traefik
服务:
`# docker-compose.yml version: '3.8' services: web: build: ./app command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; python manage.py runserver 0.0.0.0:8000' volumes: - ./app:/app expose: # new - 8000 environment: - DEBUG=1 - DATABASE_URL=postgresql://django_traefik:[[email protected]](/cdn-cgi/l/email-protection):5432/django_traefik depends_on: - db labels: # new - "traefik.enable=true" - "traefik.http.routers.django.rule=Host(`django.localhost`)" db: image: postgres:15-alpine volumes: - postgres_data:/var/lib/postgresql/data/ expose: - 5432 environment: - POSTGRES_USER=django_traefik - POSTGRES_PASSWORD=django_traefik - POSTGRES_DB=django_traefik traefik: # new image: traefik:v2.9.6 ports: - 8008:80 - 8081:8080 volumes: - "$PWD/traefik.dev.toml:/etc/traefik/traefik.toml" - "/var/run/docker.sock:/var/run/docker.sock:ro" volumes: postgres_data:`
首先,web
服务只对端口8000
上的其他容器公开。我们还为web
服务添加了以下标签:
traefik.enable=true
使 Traefik 能够发现服务traefik.http.routers.django.rule=Host(
django.localhost)
当请求有Host=django.localhost
时,请求被重定向到该服务
记下traefik
服务中的卷:
- 将本地配置文件映射到容器中的配置文件,以便保持设置同步
"/var/run/docker.sock:/var/run/docker.sock:ro"
使 Traefik 能够发现其他容器
要进行测试,首先取下任何现有的容器:
`$ docker-compose down -v
$ docker-compose -f docker-compose.prod.yml down -v`
构建新的开发映像并启动容器:
`$ docker-compose up -d --build`
导航到http://django.localhost:8008/你应该看到 Django 的欢迎页面。
接下来,在 django.localhost:8081 查看仪表盘:
完成后,将容器和体积拿下来:
让我们加密
我们已经成功地在开发模式中创建了 Django、Docker 和 Traefik 的工作示例。对于生产,您需要配置 Traefik 来通过 Let's Encrypt 管理 TLS 证书。简而言之,Traefik 将自动联系证书颁发机构来颁发和续订证书。
因为 Let's Encrypt 不会为localhost
颁发证书,所以你需要在云计算实例(比如 DigitalOcean droplet 或 AWS EC2 实例)上运行你的生产容器。您还需要一个有效的域名。如果你没有,你可以在 Freenom 创建一个免费域名。
我们使用一个 DigitalOcean droplet 为 Docker 提供一个计算实例,并部署生产容器来测试 Traefik 配置。
假设您配置了一个计算实例并设置了一个自由域,那么现在就可以在生产模式下设置 Traefik 了。
首先将 Traefik 配置的生产版本添加到名为 traefik.prod.toml 的文件中:
`# traefik.prod.toml [entryPoints] [entryPoints.web] address = ":80" [entryPoints.web.http] [entryPoints.web.http.redirections] [entryPoints.web.http.redirections.entryPoint] to = "websecure" scheme = "https" [entryPoints.websecure] address = ":443" [accessLog] [api] dashboard = true [providers] [providers.docker] exposedByDefault = false [certificatesResolvers.letsencrypt.acme] email = "[[email protected]](/cdn-cgi/l/email-protection)" storage = "/certificates/acme.json" [certificatesResolvers.letsencrypt.acme.httpChallenge] entryPoint = "web"`
确保用您的实际电子邮件地址替换
[[email protected]](/cdn-cgi/l/email-protection)
。
这里发生了什么:
- 将我们不安全的 HTTP 应用程序的入口点设置为端口 80
- 将我们的安全 HTTPS 应用程序的入口点设置为端口 443
- 将所有不安全的请求重定向到安全端口
exposedByDefault = false
取消所有服务dashboard = true
启用监控仪表板
最后,请注意:
`[certificatesResolvers.letsencrypt.acme] email = "[[email protected]](/cdn-cgi/l/email-protection)" storage = "/certificates/acme.json" [certificatesResolvers.letsencrypt.acme.httpChallenge] entryPoint = "web"`
这是让我们加密配置的地方。我们定义了证书的存储位置(T1)和验证类型(T3),这是一个 HTTP 挑战(T5)。
接下来,假设您更新了域名的 DNS 记录,创建两个新的 A 记录,它们都指向您的计算实例的公共 IP:
django-traefik.your-domain.com
-用于网络服务dashboard-django-traefik.your-domain.com
-用于 Traefik 仪表板
确保用您的实际域名替换
your-domain.com
。
接下来,像这样更新 docker-compose.prod.yml :
`# docker-compose.prod.yml version: '3.8' services: web: build: context: ./app dockerfile: Dockerfile.prod command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; gunicorn --bind 0.0.0.0:8000 config.wsgi' expose: # new - 8000 environment: - DEBUG=0 - DATABASE_URL=postgresql://django_traefik:[[email protected]](/cdn-cgi/l/email-protection):5432/django_traefik - DJANGO_ALLOWED_HOSTS=.your-domain.com depends_on: - db labels: # new - "traefik.enable=true" - "traefik.http.routers.django.rule=Host(`django-traefik.your-domain.com`)" - "traefik.http.routers.django.tls=true" - "traefik.http.routers.django.tls.certresolver=letsencrypt" db: image: postgres:15-alpine volumes: - postgres_data_prod:/var/lib/postgresql/data/ expose: - 5432 environment: - POSTGRES_USER=django_traefik - POSTGRES_PASSWORD=django_traefik - POSTGRES_DB=django_traefik traefik: # new build: context: . dockerfile: Dockerfile.traefik ports: - 80:80 - 443:443 volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./traefik-public-certificates:/certificates" labels: - "traefik.enable=true" - "traefik.http.routers.dashboard.rule=Host(`dashboard-django-traefik.your-domain.com`)" - "traefik.http.routers.dashboard.tls=true" - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt" - "[[email protected]](/cdn-cgi/l/email-protection)" - "traefik.http.routers.dashboard.middlewares=auth" - "traefik.http.middlewares.auth.basicauth.users=testuser:$$apr1$$jIKW.bdS$$eKXe4Lxjgy/rH65wP1iQe1" volumes: postgres_data_prod: traefik-public-certificates:`
同样,确保用您的实际域名替换
your-domain.com
。
这里有什么新鲜事?
在web
服务中,我们添加了以下标签:
traefik.http.routers.django.rule=Host(
django-traefik.your-domain.com)
将主机更改为实际的域traefik.http.routers.django.tls=true
启用 HTTPStraefik.http.routers.django.tls.certresolver=letsencrypt
将证书颁发者设置为让我们加密
接下来,对于traefik
服务,我们为证书目录添加了适当的端口和一个卷。该卷确保即使容器关闭,证书仍然有效。
至于标签:
traefik.http.routers.dashboard.rule=Host(
dashboard-django-traefik.your-domain.com)
定义仪表板主机,因此可以在$Host/dashboard/
访问traefik.http.routers.dashboard.tls=true
启用 HTTPStraefik.http.routers.dashboard.tls.certresolver=letsencrypt
将证书解析器设置为“让我们加密”traefik.http.routers.dashboard.middlewares=auth
启用HTTP BasicAuth
中间件traefik.http.middlewares.auth.basicauth.users
定义用于登录的用户名和散列密码
您可以使用 htpasswd 实用程序创建新的密码哈希:
`# username: testuser
# password: password
$ echo $(htpasswd -nb testuser password) | sed -e s/\\$/\\$\\$/g
testuser:$$apr1$$jIKW.bdS$$eKXe4Lxjgy/rH65wP1iQe1`
随意使用一个env_file
来存储用户名和密码作为环境变量
`USERNAME=testuser
HASHED_PASSWORD=$$apr1$$jIKW.bdS$$eKXe4Lxjgy/rH65wP1iQe1`
接下来,更新 config/settings.py 中的ALLOWED_HOSTS
环境变量,如下所示:
`# config/settings.py
ALLOWED_HOSTS = env('DJANGO_ALLOWED_HOSTS', default=[])`
最后,添加一个名为 Dockerfile.traefik 的新 Dockerfile:
`# Dockerfile.traefik
FROM traefik:v2.9.6
COPY ./traefik.prod.toml ./etc/traefik/traefik.toml`
接下来,旋转新容器:
`$ docker-compose -f docker-compose.prod.yml up -d --build`
确保这两个 URL 有效:
此外,请确保当您访问上述网址的 HTTP 版本时,您会被重定向到 HTTPS 版本。
最后,让我们加密有效期为 90 天的证书。Treafik 将在后台自动为您处理证书更新,这样您就少了一件担心的事情!
静态文件
由于 Traefik 不提供静态文件,我们将使用whiten noise来管理静态资产。
首先,将包添加到 requirements.txt 文件中:
`Django==4.1.6
django-environ==0.9.0
psycopg2-binary==2.9.5
gunicorn==20.1.0
whitenoise==6.3.0`
像这样更新 config/settings.py 中的中间件:
`# config/settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # new
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]`
然后用STATIC_ROOT
配置静态文件的处理:
`# config/settings.py
STATIC_ROOT = BASE_DIR / 'staticfiles'`
最后,添加压缩和缓存支持:
`# config/settings.py
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'`
要进行测试,请更新图像和容器:
`$ docker-compose -f docker-compose.prod.yml up -d --build`
收集静态文件:
`$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic`
确保静态文件在https://django-traefik.your-domain.com/admin被正确提供。
结论
在本教程中,我们介绍了如何使用 Postgres 将 Django 应用程序容器化以进行开发。我们还创建了一个生产就绪的 Docker 组合文件,设置了 Traefik 和 Let's Encrypt 来通过 HTTPS 为应用程序提供服务,并启用了一个安全的仪表板来监控我们的服务。
就生产环境的实际部署而言,您可能希望使用:
你可以在 django-docker-traefik 报告中找到代码。
在 DigitalOcean Droplet 上将 Django 应用程序部署到 Dokku
在本教程中,我们将了解如何安全地将 Django 应用程序部署到 DigitalOcean droplet 上的 Dokku 。
目标
学完本教程后,您应该能够:
- 解释什么是 Dokku 以及它是如何工作的
- 创建一个数字海洋水滴,并在上面安装 Dokku
- 通过 git 将 Django 应用程序部署到 Dokku
- 创建一个由 Dokku 管理的 Postgres 数据库
- 配置 Dokku 以保持存储
- 配置 Nginx 来服务静态和媒体文件
- 通过“让我们加密并在 HTTPS 上为您的应用提供服务”获得 TLS 证书
什么是 Dokku?
Dokku 是一个开源的平台即服务(PaaS ),允许你构建和管理应用从构建到扩展的生命周期。它基本上是一个迷你的 Heroku,你可以在你的 Linux 机器上自托管。因为它使用 Heroku 的构建包,它允许你部署任何可以在 Heroku 上部署的东西。Dokku 由 Docker 提供支持,并与 git 很好地集成。
Dokku 极其轻便。其唯一的系统要求是:
- Ubuntu 18.04/20.04/22.04 或 Debian 10+操作系统
- 1 GB 系统内存(如果您没有,可以使用这种变通方法
由于其系统要求较低,你可以在 DigitalOcean droplet 上托管它,每月只需 6 美元。
为什么是多库?
- 完全免费和开源
- 允许您轻松部署应用程序
- 丰富的命令行界面
- 支持多种社区插件
- 轻量级选手
Dokku 提供了一个名为 Dokku PRO 的高级计划,它进一步简化了事情,并带有一个用户友好的界面。你可以在他们的官网了解更多。
项目设置
在本教程中,我们将部署一个简单的图像托管应用程序,名为 django-images 。
在学习教程的过程中,通过部署您自己的 Django 应用程序来检查您的理解。
首先,从 GitHub 上的库中获取代码:
创建新的虚拟环境并激活它:
`$ python3 -m venv venv && source venv/bin/activate`
安装需求并迁移数据库:
`(venv)$ pip install -r requirements.txt
(venv)$ python manage.py migrate`
运行服务器:
`(venv)$ python manage.py runserver`
打开您最喜欢的网络浏览器,导航到 http://localhost:8000 。使用右边的表格上传图像,确保一切正常。上传图像后,您应该会看到它显示在表格中:
数字海洋
让我们在数字海洋上创建一个水滴。
首先,你需要注册一个数字海洋账户(如果你还没有的话),然后生成一个访问令牌,这样你就可以访问数字海洋 API 。
将生成的令牌添加到您的环境:
`$ export DIGITAL_OCEAN_ACCESS_TOKEN=<your_digital_ocean_token>`
创建一个液滴
让我们创建一个 2 GB 的 droplet,因为我们的 Django 应用程序需要一些内存,而 Dokku 需要 1 GB 的系统内存才能工作。
创建一个水滴:
`$ curl -X POST -H 'Content-Type: application/json' \
-H 'Authorization: Bearer '$DIGITAL_OCEAN_ACCESS_TOKEN'' -d \
'{"name":"django-dokku","region":"fra1","size":"s-1vcpu-2gb","image":"ubuntu-20-04-x64"}' \
"https://api.digitalocean.com/v2/droplets"`
该命令在
frankfurt-1
区域创建一个 2 GB 内存的 droplet。请随意选择更好的系统规格并更改地区。如果你不知道数字海洋的标识符,你可以使用这个网站。
DigitalOcean 旋转一滴大约需要 3-5 分钟。
检查其状态:
`$ curl \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer '$DIGITAL_OCEAN_ACCESS_TOKEN'' \
"https://api.digitalocean.com/v2/droplets?name=django-dokku"`
如果您安装了 jq ,您可以像这样解析 JSON 响应:
`$ curl \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer '$DIGITAL_OCEAN_ACCESS_TOKEN'' \
"https://api.digitalocean.com/v2/droplets?name=django-dokku" \
| jq '.droplets[0].status'`
一旦 droplet 可用,您将收到一封电子邮件,其中包含 droplet 的 IP 地址和密码。使用该信息 SSH 到服务器。
当你第一次 SSH 进入 droplet 时,出于安全原因,你将被迫更改密码。您可以选择一个非常强的密码,因为我们将在下一步启用无密码 SSH 登录。
要生成 SSH 密钥,请运行:
将密钥保存到 /root/。ssh/id_rsa 并且不设置密码短语。这将分别生成一个公钥和私钥- id_rsa 和 id_rsa.pub 。要设置无密码 SSH 登录,请将公钥复制到 authorized_keys 文件中,并设置适当的权限:
`$ cat ~/.ssh/id_rsa.pub
$ vi ~/.ssh/authorized_keys
$ chmod 600 ~/.ssh/authorized_keys
$ chmod 600 ~/.ssh/id_rsa`
复制私钥的内容:
退出 SSH 会话,然后将密钥设置为本地计算机上的环境变量:
`export PRIVATE_KEY='-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA04up8hoqzS1+APIB0RhjXyObwHQnOzhAk5Bd7mhkSbPkyhP1
...
iWlX9HNavcydATJc1f0DpzF0u4zY8PY24RVoW8vk+bJANPp1o2IAkeajCaF3w9nf
q/SyqAWVmvwYuIhDiHDaV2A==
-----END RSA PRIVATE KEY-----'`
将密钥添加到 ssh 代理中:
`$ ssh-add - <<< "${PRIVATE_KEY}"`
要进行测试,请运行:
安装 Dokku
警告:Dokku 引导脚本应该在新安装的 Linux 上运行。
要安装最新的稳定 Dokku 版本,首先 SSH 回到 droplet,然后导航到安装最新的稳定版本并复制 bootstrap 命令。在撰写本文时,它看起来是这样的:
`wget https://raw.githubusercontent.com/dokku/dokku/v0.28.1/bootstrap.sh
sudo DOKKU_TAG=v0.28.1 bash bootstrap.sh`
这个脚本将安装 Docker、Dokku 和所有其他依赖项。安装将需要大约 5 到 10 分钟,所以在此期间您可以随意去喝杯咖啡。
安装完成后,检查 Dokku 版本:
`$ dokku version
dokku version 0.28.1`
Dokku 的部署是通过 git 处理的。每次将代码推送到远程 repo 时,您都需要通过输入 dokku 用户的密码或使用 SSH 密钥来验证自己。
为了避免基于密码的认证,将您的 SSH 密钥添加到 Dokku:
`$ cat ~/.ssh/authorized_keys | dokku ssh-keys:add admin`
如果你收到一条消息说
Duplicate ssh public key specified
,这意味着 Dokku 已经添加了你的 SSH 密钥。请随意忽略此警告。
创建 Dokku 应用程序
要创建 Dokku 应用程序,请运行:
`$ dokku apps:create django-dokku
-----> Creating django-dokku...`
检查应用程序是否已成功创建:
`$ dokku apps:list
=====> My Apps
django-dokku`
如果您在使用 Dokku 时遇到困难或忘记了某个命令,您可以在该命令后添加
:help
来查看该命令的功能及其属性。比如:
dokku apps:help
。
接下来,将您的 droplet 的 IP 地址链接到您的 Dokku 应用程序:
`$ dokku domains:add django-dokku <your_droplet_ip>`
Dokku 数据库设置
为了让 Django 工作,我们还需要一个数据库。理论上我们可以使用默认的 SQLite 数据库,但是让我们把它换成一个生产就绪的数据库: Postgres 。
默认情况下,Dokku 不会在新创建的应用程序上提供数据存储,例如 MySQL、Postgres。要用 Dokku 创建数据库服务,你首先需要安装一个社区插件——例如 dokku-postgres 、Dokku-MySQL——然后把它链接到应用程序。
从安装 dokku-postgres 开始:
`$ sudo dokku plugin:install https://github.com/dokku/dokku-postgres.git postgres`
接下来创建一个名为mydb
的 Postgres 服务,运行:
`$ dokku postgres:create mydb
Waiting for container to be ready
Creating container database
Securing connection to database
=====> Postgres container created: mydb
=====> mydb postgres service information
Config dir: /var/lib/dokku/services/postgres/mydb/data
Config options:
Data dir: /var/lib/dokku/services/postgres/mydb/data
Dsn: postgres://postgres:[[email protected]](/cdn-cgi/l/email-protection):5432/mydb
Exposed ports: -
Id: 7f8274383725b8dca9e64f0d7e3835ae4e6a16a3f5bc9bf015ff9bd32b6c5895
Internal ip: 172.17.0.3
Links: -
Service root: /var/lib/dokku/services/postgres/mydb
Status: running
Version: postgres:14.4`
Dokku 将推出一个安装了 Postgres 的新 Docker 容器。
检查码头集装箱:
`$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7f8274383725 postgres:14.4 "docker-entrypoint.s…" 1 hour ago Up 1 hour 5432/tcp dokku.postgres.mydb`
最后,将 Postgres 服务链接到 Dokku 应用程序:
`$ dokku postgres:link mydb django-dokku
-----> Setting config vars
DATABASE_URL: postgres://postgres:[[email protected]](/cdn-cgi/l/email-protection):5432/mydb
-----> Restarting app django-dokku
! App image (dokku/django-dokku:latest) not found`
这将设置一个名为DATABASE_URL
的新环境变量。这个变量是一个受十二因素应用启发的 URL,稍后将允许我们连接到数据库。
`postgres://postgres:88e242667bf9579a47c4cf5895524b8c@dokku-postgres-mydb:5432/mydb syntax: protocol://username:password@host:port/dbname`
太好了!我们的数据库现在运行在 Docker 容器中,并链接到 Dokku 应用程序。
要了解更多关于 dokku-postgres 的信息,请参考资源库的自述文件。
配置 Django 项目
在教程的这一部分,我们将准备 Django 应用程序,以便与 Dokku 一起部署。
环境变量
我们不应该在源代码中存储秘密,所以让我们利用环境变量。最简单的方法是使用名为 python-dotenv 的第三方 Python 包。首先将其添加到 requirements.txt :
随意使用不同的包来处理环境变量,如 django-environ 或 python-decouple 。
然后,在 core/settings.py 的顶部导入并初始化 python-dotenv,如下所示:
`# core/settings.py
from pathlib import Path
from dotenv import load_dotenv # new
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env') # new`
接下来,从环境中加载SECRET_KEY
、DEBUG
和ALLOWED_HOSTS
:
`# core/settings.py
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', '0').lower() in ['true', 't', '1']
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(' ')`
不要忘记重要的一点:
数据库ˌ资料库
要使用 Postgres 代替 SQLite,我们首先需要安装数据库适配器。
将下面一行添加到 requirements.txt 中:
还记得作为环境变量添加的DATABASE_URL
吗?为了在 Django 中使用它,我们可以使用一个名为 dj-database-url 的包。这个包将 URL 转换成 Django 数据库参数。
像这样添加到 requirements.txt 中:
接下来,转到 core/settings.py ,把DATABASES
改成这样:
`# core/settings.py
DATABASES = {
'default': dj_database_url.parse(os.environ.get('DATABASE_URL'), conn_max_age=600),
}`
不要忘记在文件顶部导入dj-database-url
:
格尼科恩
接下来,让我们安装 Gunicorn ,这是一个生产级的 WSGI 服务器,将用于生产,而不是 Django 的开发服务器。
添加到 requirements.txt :
轮廓
像 Heroku 一样,Dokku 也使用一个 Procfile 来指定应用程序在启动时执行的命令。
在项目根目录中创建一个 Procfile 并填充它:
`web: gunicorn core.wsgi:application release: django-admin migrate --no-input && django-admin collectstatic --no-input`
所有使用 Heroku 构建包的语言和框架都声明了一个web
进程类型,它启动应用服务器。发布步骤在发布阶段迁移数据库并收集静态文件。
runtime.txt
要指定 Dokku 应该为您的应用程序使用的 Python 版本,请在项目根目录下创建一个名为 runtime.txt 的新文件。然后像这样填充它:
要查看所有受支持的 Python 运行时列表,请查看 Heroku Python 支持。
我们的应用程序现在可以部署了。让我们添加所有文件并将它们提交给 git:
`$ git add -A
$ git commit -m "prepared the app for Dokku deployment"`
部署应用程序
在部署应用程序之前,回到 droplet 上,我们需要将 Django 设置中使用的环境变量添加到 Dokku 中:
`$ dokku config:set --no-restart django-dokku SECRET_KEY=h8710y7yaaaqopvbxtyxebdmtcewi_vuf1ah4gaxyjj4goij0u
$ dokku config:set --no-restart django-dokku DEBUG=1
$ dokku config:set --no-restart django-dokku ALLOWED_HOSTS=*`
为了使调试简单一点,我们将暂时启用调试模式并将ALLOWED_HOSTS
设置为*
。这是一个糟糕的安全实践,因为它允许你的 Django 站点在任何主机/域上服务,使你容易受到 HTTP 主机头攻击。
别担心,我们稍后会更改这两个设置。
我们添加了
--no-restart
标志,以防止 Dokku 在添加每个新的环境变量后重新启动 django-dokku 应用程序。
此外,添加DJANGO_SETTINGS_MODULE
,这是使用django-admin
命令所必需的:
`$ dokku config:set --no-restart django-dokku DJANGO_SETTINGS_MODULE=core.settings`
检查环境变量,确保所有内容都已正确添加:
`$ dokku config:show django-dokku
=====> django-dokku env vars
ALLOWED_HOSTS: *
DATABASE_URL: postgres://postgres:[[email protected]](/cdn-cgi/l/email-protection):5432/mydb
DEBUG: 1
DJANGO_SETTINGS_MODULE: core.settings
DOKKU_PROXY_PORT: 80
DOKKU_PROXY_PORT_MAP: http:80:5000
SECRET_KEY: h8710y7yaaaqopvbxtyxebdmtcewi_vuf1ah4gaxyjj4goij0u`
就是这样。
现在,让我们回到本地开发环境。
向我们的 git 存储库添加一个新的 remote,并将代码推送到 droplet:
`$ git remote add dokku [[email protected]](/cdn-cgi/l/email-protection)<your_droplet_ip_address>:django-dokku
$ git push dokku master`
当您创建一个新的 Dokku 应用程序时,也应该创建一个 git 存储库。
如果您得到一个“<app_name>似乎不是一个 git 存储库”的错误,SSH 进入您的 droplet 并运行:
dokku git:initialize django-dokku
。</app_name>
您应该会在终端中看到一个很长的输出。本质上,Dokku 是:
- 清洁环境
- 设置环境变量
- 安装 requirements.txt 中的所有需求
- 收集静态文件和迁移数据库
- 创建和配置 Nginx 实例
- 舒婷拆下旧容器,旋出一个新的
Dokku 完成部署后,您可以 SSH 到您的 droplet 并检查应用程序的状态:
最后,在浏览器中找到你的 droplet 的 IP 地址,测试应用程序,看看它是否工作。
`http://<your_droplet_ip_address>/`
不错!您的应用程序现已部署到 Dokku。每次你想更新你部署的应用程序时,提交你的更改并推送到dokku
原点。
Dokku 存储设置
Dokku 应用程序在容器中运行。因此,如果一个容器被破坏,你的文件和其他数据也会被破坏。为了在容器的生命周期之外保存数据,我们需要通过 Dokku 的持久存储插件来配置存储。
持久存储
为了持久存储,我们首先需要创建一个存储目录。多库推荐使用/var/lib/dokku/data/storage/<app_name>
。
所以,回到 droplet,运行:
`$ mkdir /var/lib/dokku/data/storage/django-dokku/
$ chown -R dokku:dokku /var/lib/dokku/data/storage/django-dokku/`
接下来,让我们挂载目录。该命令有两个参数:
- 应用程序名称
- 一个
host-path:container-path
或docker-volume:container-path
组合
`$ dokku storage:mount django-dokku /var/lib/dokku/data/storage/django-dokku/staticfiles:/app/staticfiles
$ dokku storage:mount django-dokku /var/lib/dokku/data/storage/django-dokku/mediafiles:/app/mediafiles`
重新部署 Dokku 应用程序以确保收集到文件:
`$ dokku ps:restart django-dokku`
列出目录:
`$ ls /var/lib/dokku/data/storage/django-dokku
mediafiles staticfiles`
太好了,我们的静态和媒体文件已经收集好了。
目前我们有DEBUG=1
并通过 Django 提供我们的静态和媒体文件:
`# core/urls.py
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)`
让我们禁用调试模式,并学习如何通过 Nginx 服务它们。
`$ dokku config:set django-dokku DEBUG=0`
您的服务器将重新启动。如果您访问 web 应用程序,您将无法再看到任何图像。
要将 Nginx 配置为在生产中提供静态和媒体文件,请在 Dokku 应用程序文件中创建一个名为“nginx.conf.d”的新目录。然后在这个目录中创建一个名为 static.conf 的新文件。
`$ mkdir -p /home/dokku/django-dokku/nginx.conf.d
$ vi /home/dokku/django-dokku/nginx.conf.d/static.conf`
将以下内容放入 static.conf :
`location /static/ { alias /var/lib/dokku/data/storage/django-dokku/staticfiles/; } location /media/ { alias /var/lib/dokku/data/storage/django-dokku/mediafiles/; }`
目录结构:
`└── nginx.conf.d
└── static.conf`
这创建了两条路径,一条用于存储静态文件,另一条用于媒体文件。
让 dokku 用户成为配置的所有者,并重新启动应用程序:
`$ chown -R dokku:dokku /home/dokku/django-dokku/nginx.conf.d
$ dokku ps:restart django-dokku`
应用程序重启后,在浏览器中访问它,检查媒体文件是否已恢复。
Django 管理员访问权限
要访问 Django 管理面板,我们需要创建一个超级用户。我们有两个选择:
- 使用
dokku run
执行命令。 - 创建一个 Django 命令来创建一个超级用户,并将其添加到 Procfile 中。
由于这是一次性任务,我们将使用第一种方法:
`$ dokku run django-dokku python manage.py createsuperuser`
按照提示操作,就完成了。
要确保已成功创建超级用户,请导航到管理控制面板并登录。
`http://<your_droplet_ip_address>/admin`
添加域
教程的这一部分要求您有一个域名。
需要一个便宜的域名来练习?几个域名注册商有特殊优惠。“xyz”域。或者,您可以在 Freenom 创建一个免费域名。
在 Dokku 中,有两种类型的域:
- 全局域允许您使用通配符访问特定应用。例如,如果您的域是 testdriven.io,而您的应用程序名为 myapp,您将能够通过 myapp.testdriven.io 访问您的应用程序。
- 应用领域直接指向特定应用。因此,如果您的域是 testdriven.io,那么您将能够在 testdriven.io 访问您的应用程序。
要了解更多关于 Dokku 域如何工作的信息,请参考官方文档。
由于我们只有一个应用程序,我们将利用一个应用程序域。检查当前应用程序域设置:
`$ dokku domains:report django-dokku
=====> django-dokku domains information
Domains app enabled: true
Domains app vhosts: 165.227.135.236
Domains global enabled: true
Domains global vhosts: django-dokku`
若要添加域,请前往您的域的注册商> DNS 设置,并创建一个指向您的 droplet 的 IP 地址的新“A 记录”,如下所示:
`+----------+--------------+----------------------------+-----------+ | Type | Host | Value | TTL |
+----------+--------------+----------------------------+-----------+ | A Record | <some host> | <your_droplet_ip_address> | Automatic |
+----------+--------------+----------------------------+-----------+`
示例:
`+----------+--------------+----------------------------+-----------+ | Type | Host | Value | TTL |
+----------+--------------+----------------------------+-----------+ | A Record | django-dokku | 159.89.24.5 | Automatic |
+----------+--------------+----------------------------+-----------+`
如果您不想使用子域,您可以遵循相同的步骤,但只需将 DNS 主机更改为
@
并相应地配置 Dokku。
最后,将域添加到 Dokku:
`$ dokku domains:set django-dokku django-dokku.testdriven.io -----> Set django-dokku.testdriven.io for django-dokku -----> Configuring django-dokku.testdriven.io...(using built-in template) -----> Creating http nginx.conf Reloading nginx`
确保用您的域或子域替换 django-dokku.testdriven.io。
Dokku 将配置所有的东西(包括 Nginx 配置)来与域一起工作。
再次检查域报告,您将看到一个新的应用虚拟主机:
`$ dokku domains:report django-dokku
=====> django-dokku domains information
Domains app enabled: true
Domains app vhosts: django-dokku.testdriven.io
Domains global enabled: true
Domains global vhosts: django-dokku`
尝试通过域访问您的 web 应用程序,看看它是否工作。
让我们加密的 HTTPS
在这最后一部分,我们将使用加密来获得一个 TLS 证书,然后通过 HTTPS 服务 web 应用程序。
首先,安装另一个名为 dokku-letsencrypt 的 Dokku 社区插件:
`$ sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git`
在获得证书之前,您需要添加一个带有您的电子邮件地址的 env 变量:
`$ dokku config:set --no-restart django-dokku DOKKU_LETSENCRYPT_EMAIL=<your_email>
-----> Setting config vars
DOKKU_LETSENCRYPT_EMAIL: youremail`
此电子邮件用于签发证书并通知您证书到期。
接下来,运行插件命令来启用 HTTPS:
`$ sudo dokku letsencrypt:enable django-dokku`
这个命令将检索证书,安装它,自动配置 Nginx,创建从 HTTP 到 HTTPS 的重定向,并设置 HSTS 报头。
尝试通过 HTTP 访问您的网站,您应该会被重定向到 HTTPS。
要设置自动证书续订,请运行:
`$ dokku letsencrypt:cron-job --add`
这将每天运行检查,并更新所有应该更新的证书。
最后,配置 Django 的ALLOWED_HOSTS
只允许通过 HTTPS 访问网站:
`$ dokku config:set django-dokku "ALLOWED_HOSTS=localhost 127.0.0.1 [::1] <your_domain>"`
确保用您的实际域名替换<your_domain>
。
等待服务器重启——就这样!
结论
在本教程中,我们已经成功地在 DigitalOcean droplet 上将 Django 应用程序部署到 Dokku。现在,您应该对 Dokku 的工作原理有了相当的了解,并且能够部署您自己的 Django 应用程序了。
从 django-dokku-digitalocean 回购中获取最终代码。
下一步是什么?
- 您应该通过启用和配置防火墙来使您的 droplet 更加安全。
- 将数据库放在容器中是不好的做法。考虑改用数字海洋管理的数据库或类似的服务。
- 对于媒体文件,考虑切换到 AWS S3,因为它更安全、更便宜。
Django REST 框架和 Elasticsearch
在本教程中,我们将看看如何集成 Django REST 框架 (DRF)和 Elasticsearch 。我们将使用 Django 对数据建模,使用 DRF 对数据进行序列化和服务。最后,我们将使用 Elasticsearch 索引数据,并使其可搜索。
什么是 Elasticsearch?
Elasticsearch 是一个分布式、免费和开放的搜索和分析引擎,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化数据。它以其简单的 RESTful APIs、分布式特性、速度和可伸缩性而闻名。Elasticsearch 是 Elastic Stack (也称为 ELK Stack )的核心组件,这是一套用于数据摄取、丰富、存储、分析和可视化的免费开放工具。
其使用案例包括:
- 网站搜索和应用程序搜索
- 监控和可视化您的系统指标
- 安全和业务分析
- 日志记录和日志分析
要了解更多关于 Elasticsearch 的信息,请查看什么是 Elasticsearch?来自官方文件。
弹性搜索结构和概念
在使用 Elasticsearch 之前,我们应该熟悉基本的 Elasticsearch 概念。这些从大到小排列如下:
- 集群是一个或多个节点的集合。
- 节点是运行 Elasticsearch 的单个服务器实例。在与集群通信时,它:
- 存储和索引您的数据
- 提供搜索
- 索引用于将文档存储在对应于字段数据类型的专用数据结构中(类似于 SQL 数据库)。每个索引都有一个或多个碎片和副本。
- Type 是文档的集合,它们有一些共同点(类似于 SQL 表)。
- Shard 是一个 Apache Lucene 索引。它用于拆分索引并保持大量数据的可管理性。
- 副本是一个安全机制,基本上是你的索引碎片的副本。
- 文档是可以被索引的基本信息单元(类似于 SQL 行)。它用 JSON 来表示,这是一种无处不在的互联网数据交换格式。
- 字段是 Elasticsearch 中最小的数据单元(类似于 SQL 列)。
Elasticsearch 集群具有以下结构:
好奇关系数据库概念如何与 Elasticsearch 概念相关联?
关系数据库 | 弹性搜索 |
---|---|
串 | 串 |
RDBMS 实例 | 结节 |
桌子 | 索引 |
排 | 文件 |
圆柱 | 田 |
查看跨 SQL 和 Elasticsearch 映射概念,了解 SQL 和 Elasticsearch 中的概念如何相互关联的更多信息。
Elasticsearch 与 PostgreSQL 全文搜索
关于全文搜索,Elasticsearch 和 PostgreSQL 各有利弊。在选择它们时,您应该考虑速度、查询复杂性和预算。
PostgreSQL 的优势:
- Django 支持
- 更快、更容易设置
- 不需要维护
弹性搜索优势:
- 仅为搜索而优化
- 电子搜索更快(尤其是当记录数量增加时)
- 支持不同的查询类型(叶、复合、模糊、正则表达式等等)
如果你正在做一个简单的项目,速度并不重要,你应该选择 PostgreSQL。如果性能很重要,并且您想编写复杂的查找,请选择 Elasticsearch。
关于 Django 和 Postgres 全文搜索的更多信息,请查看文章使用 Django 和 Postgres 进行基本和全文搜索。
项目设置
我们将构建一个简单的博客应用程序。我们的项目将由多个模型组成,这些模型将通过 Django REST 框架进行序列化和服务。集成 Elasticsearch 后,我们将创建一个端点,允许我们查找不同的作者、类别和文章。
为了保持代码的整洁和模块化,我们将把项目分成以下两个应用:
- 对于我们的 Django 模型、序列化器和视图集
search
-用于弹性搜索文档、索引和查询
首先创建一个新目录,并建立一个新的 Django 项目:
`$ mkdir django-drf-elasticsearch && cd django-drf-elasticsearch
$ python3.9 -m venv env
$ source env/bin/activate
(env)$ pip install django==3.2.6
(env)$ django-admin.py startproject core .`
之后,创建一个名为blog
的新应用:
`(env)$ python manage.py startapp blog`
在INSTALLED_APPS
下的 core/settings.py 中注册 app:
`# core/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog.apps.BlogConfig', # new
]`
数据库模型
接下来,在博客/models.py 中创建Category
和Article
模型:
`# blog/models.py
from django.contrib.auth.models import User
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=32)
description = models.TextField(null=True, blank=True)
class Meta:
verbose_name_plural = 'categories'
def __str__(self):
return f'{self.name}'
ARTICLE_TYPES = [
('UN', 'Unspecified'),
('TU', 'Tutorial'),
('RS', 'Research'),
('RW', 'Review'),
]
class Article(models.Model):
title = models.CharField(max_length=256)
author = models.ForeignKey(to=User, on_delete=models.CASCADE)
type = models.CharField(max_length=2, choices=ARTICLE_TYPES, default='UN')
categories = models.ManyToManyField(to=Category, blank=True, related_name='categories')
content = models.TextField()
created_datetime = models.DateTimeField(auto_now_add=True)
updated_datetime = models.DateTimeField(auto_now=True)
def __str__(self):
return f'{self.author}: {self.title} ({self.created_datetime.date()})'`
注意事项:
- 代表一个文章类别——例如,编程、Linux、测试。
Article
代表单篇文章。每篇文章可以有多个类别。文章有特定的类型-Tutorial
、Research
、Review
或Unspecified
。- 作者由默认的 Django 用户模型表示。
运行迁移
进行迁移,然后应用它们:
`(env)$ python manage.py makemigrations
(env)$ python manage.py migrate`
在 blog/admin.py 中注册模型:
`# blog/admin.py
from django.contrib import admin
from blog.models import Category, Article
admin.site.register(Category)
admin.site.register(Article)`
填充数据库
在进入下一步之前,我们需要一些数据来处理。我创建了一个简单的命令,可以用来填充数据库。
在“博客”中创建一个名为“管理”的新文件夹,然后在该文件夹中创建另一个名为“命令”的文件夹。在“commands”文件夹中,创建一个名为 populate_db.py 的新文件。
`management
└── commands
└── populate_db.py`
从 populate_db.py 中复制文件内容,粘贴到您的 populate_db.py 中。
运行以下命令来填充数据库:
`(env)$ python manage.py populate_db`
如果一切顺利,你应该会在控制台上看到一条Successfully populated the database.
消息,并且你的数据库中应该会有一些文章。
Django REST 框架
现在让我们使用 pip 安装djangorestframework
:
`(env)$ pip install djangorestframework==3.12.4`
在我们的 settings.py 中注册,就像这样:
`# core/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog.apps.BlogConfig',
'rest_framework', # new
]`
添加以下设置:
`# core/settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 25
}`
我们将需要这些设置来实现分页。
创建序列化程序
为了序列化我们的 Django 模型,我们需要为每个模型创建一个序列化器。创建依赖于 Django 模型的序列化器的最简单方法是使用ModelSerializer
类。
blog/serializer . py:
`# blog/serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
from blog.models import Article, Category
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'first_name', 'last_name')
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = '__all__'
class ArticleSerializer(serializers.ModelSerializer):
author = UserSerializer()
categories = CategorySerializer(many=True)
class Meta:
model = Article
fields = '__all__'`
注意事项:
UserSerializer
和CategorySerializer
相当简单:我们只是提供了想要序列化的字段。- 在
ArticleSerializer
中,我们需要处理好关系,以确保它们也被序列化。这就是为什么我们提供了UserSerializer
和CategorySerializer
。
想了解更多关于 DRF 连载?使用 Django REST 框架序列化器有效地检查。
创建浏览器
让我们在 blog/views.py 中为我们的每个模型创建一个视图集:
`# blog/views.py
from django.contrib.auth.models import User
from rest_framework import viewsets
from blog.models import Category, Article
from blog.serializers import CategorySerializer, ArticleSerializer, UserSerializer
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.all()
class CategoryViewSet(viewsets.ModelViewSet):
serializer_class = CategorySerializer
queryset = Category.objects.all()
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
queryset = Article.objects.all()`
在这段代码中,我们通过为每个视图集提供serializer_class
和queryset
来创建视图集。
定义 URL
为视图集创建应用程序级别的 URL:
`# blog/urls.py
from django.urls import path, include
from rest_framework import routers
from blog.views import UserViewSet, CategoryViewSet, ArticleViewSet
router = routers.DefaultRouter()
router.register(r'user', UserViewSet)
router.register(r'category', CategoryViewSet)
router.register(r'article', ArticleViewSet)
urlpatterns = [
path('', include(router.urls)),
]`
然后,将应用程序 URL 连接到项目 URL:
`# core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('blog/', include('blog.urls')),
path('admin/', admin.site.urls),
]`
我们的应用程序现在有以下网址:
/blog/user/
列出所有用户/blog/user/<USER_ID>/
获取特定用户/blog/category/
列出所有类别/blog/category/<CATEGORY_ID>/
获取特定类别/blog/article/
列出所有文章/blog/article/<ARTICLE_ID>/
获取特定的文章
测试
现在我们已经注册了 URL,我们可以测试端点,看看是否一切正常。
运行开发服务器:
`(env)$ python manage.py runserver`
然后,在您选择的浏览器中,导航到http://127 . 0 . 0 . 1:8000/blog/article/。响应应该如下所示:
`{ "count": 4, "next": null, "previous": null, "results": [ { "id": 1, "author": { "id": 3, "username": "jess_", "first_name": "Jess", "last_name": "Brown" }, "categories": [ { "id": 2, "name": "SEO optimization", "description": null } ], "title": "How to improve your Google rating?", "type": "TU", "content": "Firstly, add the correct SEO tags...", "created_datetime": "2021-08-12T17:34:31.271610Z", "updated_datetime": "2021-08-12T17:34:31.322165Z" }, { "id": 2, "author": { "id": 4, "username": "johnny", "first_name": "Johnny", "last_name": "Davis" }, "categories": [ { "id": 4, "name": "Programming", "description": null } ], "title": "Installing latest version of Ubuntu", "type": "TU", "content": "In this tutorial, we'll take a look at how to setup the latest version of Ubuntu. Ubuntu (/ʊˈbʊntuː/ is a Linux distribution based on Debian and composed mostly of free and open-source software. Ubuntu is officially released in three editions: Desktop, Server, and Core for Internet of things devices and robots.", "created_datetime": "2021-08-12T17:34:31.540628Z", "updated_datetime": "2021-08-12T17:34:31.592555Z" }, ... ] }`
手动测试其他端点。
弹性搜索设置
首先在后台安装并运行 Elasticsearch。
需要帮助启动和运行 Elasticsearch 吗?查看安装弹性搜索指南。如果你熟悉 Docker,你可以简单地运行下面的命令来拉取的官方图片,并旋转一个运行着 Elasticsearch 的容器:
`$ docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.14.0`
要将 Elasticsearch 与 Django 集成,我们需要安装以下软件包:
- elasticsearch -用于 elasticsearch 的官方低级 Python 客户端
- 用于编写和运行针对 elasticsearch 的查询的高级库
- django-elasticsearch-dsl -围绕 elasticsearch-dsl-py 的包装器,允许在 elasticsearch 中索引 django 模型
安装:
`(env)$ pip install elasticsearch==7.14.0
(env)$ pip install elasticsearch-dsl==7.4.0
(env)$ pip install django-elasticsearch-dsl==7.2.0`
启动一个名为search
的新应用,它将保存我们的弹性搜索文档、索引和查询:
`(env)$ python manage.py startapp search`
在INSTALLED_APPS
下的 core/settings.py 中注册search
和django_elasticsearch_dsl
:
`# core/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_elasticsearch_dsl', # new
'blog.apps.BlogConfig',
'search.apps.SearchConfig', # new
'rest_framework',
]`
现在我们需要让 Django 知道 Elasticsearch 在哪里运行。为此,我们将以下内容添加到我们的 core/settings.py 文件中:
`# core/settings.py
# Elasticsearch
# https://django-elasticsearch-dsl.readthedocs.io/en/latest/settings.html
ELASTICSEARCH_DSL = {
'default': {
'hosts': 'localhost:9200'
},
}`
如果您的 Elasticsearch 在不同的端口上运行,请确保相应地更改上述设置。
我们可以通过启动服务器来测试 Django 是否可以连接到 Elasticsearch:
`(env)$ python manage.py runserver`
如果您的 Django 服务器出现故障,Elasticsearch 可能无法正常工作。
创建文档
在创建文档之前,我们需要确保所有的数据都以正确的格式保存。我们在文章type
中使用CharField(max_length=2)
,这本身没有多大意义。这就是为什么我们将它转换成人类可读的文本。
我们将通过在模型中添加一个type_to_string()
方法来实现这一点,如下所示:
`# blog/models.py
class Article(models.Model):
title = models.CharField(max_length=256)
author = models.ForeignKey(to=User, on_delete=models.CASCADE)
type = models.CharField(max_length=2, choices=ARTICLE_TYPES, default='UN')
categories = models.ManyToManyField(to=Category, blank=True, related_name='categories')
content = models.TextField()
created_datetime = models.DateTimeField(auto_now_add=True)
updated_datetime = models.DateTimeField(auto_now=True)
# new
def type_to_string(self):
if self.type == 'UN':
return 'Unspecified'
elif self.type == 'TU':
return 'Tutorial'
elif self.type == 'RS':
return 'Research'
elif self.type == 'RW':
return 'Review'
def __str__(self):
return f'{self.author}: {self.title} ({self.created_datetime.date()})'`
如果没有type_to_string()
,我们的模型将会连载成这样:
`{ "title": "This is my article.", "type": "TU", ... }`
在实现type_to_string()
之后,我们的模型被序列化成这样:
`{ "title": "This is my article.", "type": "Tutorial", ... }`
现在让我们创建文档。每个文档需要有一个Index
和Django
类。在Index
类中,我们需要提供指标名称和弹性搜索指标设置。在Django
类中,我们告诉文档与哪个 Django 模型相关联,并提供我们想要索引的字段。
博客/documents.py :
`# blog/documents.py
from django.contrib.auth.models import User
from django_elasticsearch_dsl import Document, fields
from django_elasticsearch_dsl.registries import registry
from blog.models import Category, Article
@registry.register_document
class UserDocument(Document):
class Index:
name = 'users'
settings = {
'number_of_shards': 1,
'number_of_replicas': 0,
}
class Django:
model = User
fields = [
'id',
'first_name',
'last_name',
'username',
]
@registry.register_document
class CategoryDocument(Document):
id = fields.IntegerField()
class Index:
name = 'categories'
settings = {
'number_of_shards': 1,
'number_of_replicas': 0,
}
class Django:
model = Category
fields = [
'name',
'description',
]
@registry.register_document
class ArticleDocument(Document):
author = fields.ObjectField(properties={
'id': fields.IntegerField(),
'first_name': fields.TextField(),
'last_name': fields.TextField(),
'username': fields.TextField(),
})
categories = fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.TextField(),
'description': fields.TextField(),
})
type = fields.TextField(attr='type_to_string')
class Index:
name = 'articles'
settings = {
'number_of_shards': 1,
'number_of_replicas': 0,
}
class Django:
model = Article
fields = [
'title',
'content',
'created_datetime',
'updated_datetime',
]`
注意事项:
- 为了转换文章类型,我们向
ArticleDocument
添加了type
属性。 - 因为我们的
Article
模型与Category
是多对多(M:N)关系,与User
是多对一(N:1)关系,所以我们需要处理好这些关系。我们通过添加ObjectField
属性做到了这一点。
填充弹性搜索
要创建和填充 Elasticsearch 索引和映射,使用search_index
命令:
`(env)$ python manage.py search_index --rebuild
Deleting index 'users'
Deleting index 'categories'
Deleting index 'articles'
Creating index 'users'
Creating index 'categories'
Creating index 'articles'
Indexing 3 'User' objects
Indexing 4 'Article' objects
Indexing 4 'Category' objects`
每次更改索引设置时,都需要运行此命令。
django-elasticsearch-dsl 创建了适当的数据库信号,这样每次创建、删除或编辑模型实例时,您的 elasticsearch 存储都会得到更新。
弹性搜索查询
在创建适当的视图之前,让我们看看 Elasticsearch 查询是如何工作的。
我们首先必须获得Search
实例。我们通过调用文档中的search()
来实现,如下所示:
`from blog.documents import ArticleDocument
search = ArticleDocument.search()`
您可以在 Django shell 中随意运行这些查询。
一旦我们有了Search
实例,我们就可以将查询传递给query()
方法并获取响应:
`from elasticsearch_dsl import Q
from blog.documents import ArticleDocument
# Looks up all the articles that contain `How to` in the title.
query = 'How to'
q = Q(
'multi_match',
query=query,
fields=[
'title'
])
search = ArticleDocument.search().query(q)
response = search.execute()
# print all the hits
for hit in search:
print(hit.title)`
我们还可以像这样组合多个 Q 语句:
`from elasticsearch_dsl import Q
from blog.documents import ArticleDocument
"""
Looks up all the articles that:
1) Contain 'language' in the 'title'
2) Don't contain 'ruby' or 'javascript' in the 'title'
3) And contain the query either in the 'title' or 'description'
"""
query = 'programming'
q = Q(
'bool',
must=[
Q('match', title='language'),
],
must_not=[
Q('match', title='ruby'),
Q('match', title='javascript'),
],
should=[
Q('match', title=query),
Q('match', description=query),
],
minimum_should_match=1)
search = ArticleDocument.search().query(q)
response = search.execute()
# print all the hits
for hit in search:
print(hit.title)`
使用 Elasticsearch 查询的另一个重要因素是模糊性。模糊查询是允许我们处理错别字的查询。他们使用 Levenshtein 距离算法来计算数据库中的结果和查询之间的距离。
让我们看一个例子。
通过运行下面的查询,我们不会得到任何结果,因为用户拼错了“django”。
`from elasticsearch_dsl import Q
from blog.documents import ArticleDocument
query = 'djengo' # notice the typo
q = Q(
'multi_match',
query=query,
fields=[
'title'
])
search = ArticleDocument.search().query(q)
response = search.execute()
# print all the hits
for hit in search:
print(hit.title)`
如果我们像这样启用模糊性:
`from elasticsearch_dsl import Q
from blog.documents import ArticleDocument
query = 'djengo' # notice the typo
q = Q(
'multi_match',
query=query,
fields=[
'title'
],
fuzziness='auto')
search = ArticleDocument.search().query(q)
response = search.execute()
# print all the hits
for hit in search:
print(hit.title)`
用户将得到正确的结果。
全文搜索和精确匹配的区别在于,全文搜索会在文本被索引到 Elasticsearch 之前对其进行分析。文本被分解成不同的记号,这些记号被转换成它们的根形式(例如,reading - > read)。这些令牌然后被保存到倒排索引中。因此,全文搜索会产生更多的结果,但需要更长的处理时间。
Elasticsearch 有许多附加功能。要熟悉 API,请尝试实现:
你可以在这里看到所有的 Elasticsearch 搜索 API。
搜索视图
这样,让我们创建一些视图。为了让我们的代码更加简洁,我们可以在 search/views.py 中使用下面的抽象类:
`# search/views.py
import abc
from django.http import HttpResponse
from elasticsearch_dsl import Q
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.views import APIView
class PaginatedElasticSearchAPIView(APIView, LimitOffsetPagination):
serializer_class = None
document_class = None
@abc.abstractmethod
def generate_q_expression(self, query):
"""This method should be overridden
and return a Q() expression."""
def get(self, request, query):
try:
q = self.generate_q_expression(query)
search = self.document_class.search().query(q)
response = search.execute()
print(f'Found {response.hits.total.value} hit(s) for query: "{query}"')
results = self.paginate_queryset(response, request, view=self)
serializer = self.serializer_class(results, many=True)
return self.get_paginated_response(serializer.data)
except Exception as e:
return HttpResponse(e, status=500)`
注意事项:
- 要使用这个类,我们必须提供我们的
serializer_class
和document_class
并覆盖generate_q_expression()
。 - 该类除了运行
generate_q_expression()
查询、获取响应、分页并返回序列化数据之外什么也不做。
所有的视图现在都应该继承自PaginatedElasticSearchAPIView
:
`# search/views.py
import abc
from django.http import HttpResponse
from elasticsearch_dsl import Q
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.views import APIView
from blog.documents import ArticleDocument, UserDocument, CategoryDocument
from blog.serializers import ArticleSerializer, UserSerializer, CategorySerializer
class PaginatedElasticSearchAPIView(APIView, LimitOffsetPagination):
serializer_class = None
document_class = None
@abc.abstractmethod
def generate_q_expression(self, query):
"""This method should be overridden
and return a Q() expression."""
def get(self, request, query):
try:
q = self.generate_q_expression(query)
search = self.document_class.search().query(q)
response = search.execute()
print(f'Found {response.hits.total.value} hit(s) for query: "{query}"')
results = self.paginate_queryset(response, request, view=self)
serializer = self.serializer_class(results, many=True)
return self.get_paginated_response(serializer.data)
except Exception as e:
return HttpResponse(e, status=500)
# views
class SearchUsers(PaginatedElasticSearchAPIView):
serializer_class = UserSerializer
document_class = UserDocument
def generate_q_expression(self, query):
return Q('bool',
should=[
Q('match', username=query),
Q('match', first_name=query),
Q('match', last_name=query),
], minimum_should_match=1)
class SearchCategories(PaginatedElasticSearchAPIView):
serializer_class = CategorySerializer
document_class = CategoryDocument
def generate_q_expression(self, query):
return Q(
'multi_match', query=query,
fields=[
'name',
'description',
], fuzziness='auto')
class SearchArticles(PaginatedElasticSearchAPIView):
serializer_class = ArticleSerializer
document_class = ArticleDocument
def generate_q_expression(self, query):
return Q(
'multi_match', query=query,
fields=[
'title',
'author',
'type',
'content'
], fuzziness='auto')`
定义 URL
最后,让我们为视图创建 URL:
`# search.urls.py
from django.urls import path
from search.views import SearchArticles, SearchCategories, SearchUsers
urlpatterns = [
path('user/<str:query>/', SearchUsers.as_view()),
path('category/<str:query>/', SearchCategories.as_view()),
path('article/<str:query>/', SearchArticles.as_view()),
]`
然后,将应用程序 URL 连接到项目 URL:
`# core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('blog/', include('blog.urls')),
path('search/', include('search.urls')), # new
path('admin/', admin.site.urls),
]`
测试
我们的 web 应用程序完成了。我们可以通过访问以下 URL 来测试我们的搜索端点:
注意第四个请求的错别字。我们拼写了“progreming ”,但由于模糊性,仍然得到了正确的结果。
替代库
我们选择的道路并不是整合 Django 和 Elasticsearch 的唯一途径。还有一些其他的图书馆你可能想去看看:
- django-elasicsearch-dsl-drf 是围绕 Elasticsearch 和 Django REST 框架的一个包装器。它提供了视图、序列化器、过滤器后端、分页等等。它工作得很好,但是对于较小的项目来说可能有些矫枉过正。如果你需要高级的弹性搜索功能,我推荐你使用它。
- Haystack 是一些搜索后端的包装器,比如 Elasticsearch、 Solr 和 Whoosh 。它允许您编写一次搜索代码,并在不同的搜索后端重用它。它非常适合实现一个简单的搜索框。因为 Haystack 是另一个抽象层,所以会涉及更多的开销,所以如果性能真的很重要或者如果您正在处理大量数据,就不应该使用它。这也需要一些配置。
- Django REST 框架的 Haystack 是一个小库,它试图简化 Haystack 与 Django REST 框架的集成。在撰写本文时,该项目有点过时,他们的文档也写得很糟糕。我花了相当多的时间试图让它工作,但运气不好。
结论
在本教程中,您学习了使用 Django REST 框架和 Elasticsearch 的基础知识。您现在知道如何集成它们,创建 Elasticsearch 文档和查询,并通过 RESTful API 提供数据。
在将你的项目投入生产之前,考虑使用一个托管的 Elasticsearch 服务,比如 Elastic Cloud 、亚马逊 Elasticsearch 服务或 Elastic on Azure 。使用托管服务的成本将高于管理您自己的集群,但它们提供了部署、保护和运行 Elasticsearch 集群所需的所有基础设施。此外,他们将处理版本更新、定期备份和扩展。
从 GitHub 上的django-drf-elastic searchrepo 中抓取代码。
将 Django 应用程序部署到 Elastic Beanstalk
在本教程中,我们将逐步完成将生产就绪的 Django 应用程序部署到 AWS Elastic Beanstalk 的过程。
目标
本教程结束时,您将能够:
- 解释什么是弹性豆茎
- 初始化和配置弹性豆茎
- 对运行在 Elastic Beanstalk 上的应用程序进行故障排除
- 将弹性豆茎与 RDS 结合
- 为静态和媒体文件支持配置 S3
- 通过 AWS 证书管理器获取 SSL 证书
- 使用 SSL 证书在 HTTPS 上提供您的应用程序
什么是弹性豆茎?
AWS Elastic Beanstalk (EB)是一个易于使用的服务,用于部署和扩展 web 应用程序。它连接多个 AWS 服务,例如计算实例( EC2 )、数据库( RDS )、负载平衡器(应用负载平衡器)和文件存储系统( S3 ),等等。EB 允许您快速开发和部署 web 应用程序,而无需考虑底层基础设施。它支持用 Go、Java、.NET、Node.js、PHP、Python 和 Ruby。如果您需要配置自己的软件栈或部署用 EB 目前不支持的语言(或版本)开发的应用程序,EB 也支持 Docker。
典型的弹性豆茎设置:
AWS 弹性豆茎不另收费。您只需为应用程序消耗的资源付费。
要了解更多关于弹性豆茎的信息,请查看什么是 AWS 弹性豆茎?来自官方 AWS 弹性豆茎文档。
弹性豆茎概念
在开始学习教程之前,让我们先来看看与 Elastic Beanstalk 相关的几个关键概念:
- 一个 应用 是弹性 Beanstalk 组件的逻辑集合,包括环境、版本和环境配置。一个应用程序可以有多个版本。
- 一个 环境 是运行一个应用版本的 AWS 资源的集合。
- 一个 平台 是操作系统、编程语言运行时、web 服务器、应用服务器和弹性 Beanstalk 组件的组合。
这些术语将在整个教程中使用。
项目设置
在本教程中,我们将部署一个名为 django-images 的简单图像托管应用程序。
按照教程进行操作时,通过部署您自己的应用程序来检查您的理解。
首先,从 GitHub 上的库获取代码:
创建新的虚拟环境并激活它:
`$ python3 -m venv venv && source venv/bin/activate`
安装需求并迁移数据库:
`(venv)$ pip install -r requirements.txt
(venv)$ python manage.py migrate`
运行服务器:
`(venv)$ python manage.py runserver`
打开您最喜欢的网络浏览器,导航到 http://localhost:8000 。使用右边的表格上传图像,确保一切正常。上传图像后,您应该会看到它显示在表格中:
弹性豆茎 CLI
在继续之前,请务必在注册一个 AWS 帐户。通过创建一个账户,你可能也有资格加入 AWS 免费等级。
Elastic Beanstalk 命令行界面 (EB CLI)允许您执行各种操作来部署和管理您的 Elastic Beanstalk 应用程序和环境。
有两种安装 EB CLI 的方法:
建议使用安装程序(第一个选项)全局安装 EB CLI(任何特定虚拟环境之外),以避免可能的依赖冲突。更多详情请参考本解释。
安装 EB CLI 后,您可以通过运行以下命令来检查版本:
`$ eb --version
EB CLI 3.20.3 (Python 3.10.)`
如果该命令不起作用,您可能需要将 EB CLI 添加到$PATH
中。
EB CLI 命令列表及其描述可在 EB CLI 命令参考中找到。
初始化弹性豆茎
一旦我们运行了 EB CLI,我们就可以开始与 Elastic Beanstalk 交互了。让我们初始化一个新项目和一个 EB 环境。
初始化
在项目根目录(“django-images”)中,运行:
你会被提示一些问题。
默认区域
您的弹性 Beanstalk 环境的 AWS 区域(和资源)。如果您不熟悉不同的 AWS 区域,请查看 AWS 区域和可用区域。一般来说,你应该选择离你的客户最近的地区。请记住,资源价格因地区而异。
应用程序名称
这是您的弹性 Beanstalk 应用程序的名称。我建议按下回车键,使用默认设置:“django-images”。
平台和平台分支
EB CLI 将检测到您正在使用 Python 环境。之后,它会给你不同的 Python 版本和 Amazon Linux 版本供你使用。选择“运行在 64 位亚马逊 Linux 2 上的 Python 3.8”。
代码提交
CodeCommit 是一个安全的、高度可伸缩的、托管的源代码控制服务,托管私有的 Git 存储库。我们不会使用它,因为我们已经在使用 GitHub 进行源代码控制。所以说“不”。
嘘
为了稍后连接到 EC2 实例,我们需要设置 SSH。出现提示时,说“是”。
密钥对
为了连接到 EC2 实例,我们需要一个 RSA 密钥对。继续生成一个,它将被添加到您的“~/”中。ssh”文件夹。
回答完所有问题后,您会注意到项目根目录下有一个隐藏的目录,名为。elasticbeanstalk”。该目录应该包含一个 config.yml 文件,其中包含您刚才提供的所有数据。
`.elasticbeanstalk
└── config.yml`
该文件应包含类似以下内容:
`branch-defaults: master: environment: null group_suffix: null global: application_name: django-images branch: null default_ec2_keyname: aws-eb default_platform: Python 3.8 running on 64bit Amazon Linux 2 default_region: us-west-2 include_git_submodules: true instance_profile: null platform_name: null platform_version: null profile: eb-cli repository: null sc: git workspace_type: Application`
创造
接下来,让我们创建弹性 Beanstalk 环境并部署应用程序:
同样,系统会提示您几个问题。
环境名称
这表示 EB 环境的名称。我建议坚持使用默认值:“django-images-env”。
将
└-env
或└-dev
后缀添加到您的环境中被认为是一种很好的做法,这样您就可以很容易地将 EB 应用程序与环境区分开来。
DNS CNAME 前缀
您的 web 应用程序将在%cname%.%region%.elasticbeanstalk.com
可访问。同样,使用默认值。
负载平衡
负载平衡器在您的环境实例之间分配流量。选择“应用程序”。
如果您想了解不同的负载平衡器类型,请查看适用于您的弹性 Beanstalk 环境的负载平衡器。
现货车队请求
Spot Fleet 请求允许您根据自己的标准按需启动实例。我们不会在本教程中使用它们,所以说“不”。
--
有了它,环境将会旋转起来:
- 你的代码将被压缩并上传到一个新的 S3 桶。
- 之后,将创建各种 AWS 资源,如负载平衡器、安全和自动伸缩组以及 EC2 实例。
还将部署一个新的应用程序。
这将需要大约三分钟,所以请随意拿一杯咖啡。
部署完成后,EB CLI 将修改。elasticbeanstalk/config.yml 。
您的项目结构现在应该如下所示:
`|-- .elasticbeanstalk
| └-- config.yml
|-- .gitignore
|-- README.md
|-- core
| |-- __init__.py
| |-- asgi.py
| |-- settings.py
| |-- urls.py
| └-- wsgi.py
|-- db.sqlite3
|-- images
| |-- __init__.py
| |-- admin.py
| |-- apps.py
| |-- forms.py
| |-- migrations
| | |-- 0001_initial.py
| | └-- __init__.py
| |-- models.py
| |-- tables.py
| |-- templates
| | └-- images
| | └-- index.html
| |-- tests.py
| |-- urls.py
| └-- views.py
|-- manage.py
└-- requirements.txt`
状态
部署应用后,您可以通过运行以下命令来检查其状态:
`$ eb status
Environment details for: django-images-env
Application name: django-images
Region: us-west-2
Deployed Version: app-93ec-220218_095635133296
Environment ID: e-z7dmesipvc
Platform: arn:aws:elasticbeanstalk:us-west-2::platform/Python 3.8 running on 64bit Amazon Linux 2/3.3.10
Tier: WebServer-Standard-1.0
CNAME: django-images-env.us-west-2.elasticbeanstalk.com
Updated: 2022-02-18 16:00:24.954000+00:00
Status: Ready
Health: Red`
您可以看到我们环境的当前健康状况是Red
,这意味着出现了问题。暂时不要担心这个问题,我们将在接下来的步骤中解决它。
您还可以看到,AWS 为我们分配了一个 CNAME,这是我们的 EB 环境的域名。我们可以通过打开浏览器并导航到 CNAME 来访问 web 应用程序。
打开
此命令将打开您的默认浏览器并导航到 CNAME 域。你会看到502 Bad Gateway
,我们将在这里很快修复它
安慰
该命令将在您的默认浏览器中打开 Elastic Beanstalk 控制台:
同样,您可以看到环境的健康状况是“严重的”,我们将在下一步中解决这个问题。
配置环境
在上一步中,我们尝试访问我们的应用程序,它返回了502 Bad Gateway
。背后有几个原因:
- Python 需要
PYTHONPATH
来在我们的应用程序中找到模块。 - 默认情况下,Elastic Beanstalk 试图从不存在的 application.py 启动 WSGI 应用程序。
- Django 需要
DJANGO_SETTINGS_MODULE
知道使用哪些设置。
默认情况下,Elastic Beanstalk 为 Python 应用程序提供了 Gunicorn 。EB 在部署过程中自动安装 Gunicorn,因此我们不必将其添加到 requirements.txt 中。如果你想用别的东西替换 Gunicorn,看看用 Procfile 配置 WSGI 服务器的。
让我们修复这些错误。
在项目根目录下创建一个名为“”的新文件夹。ebextensions”。在新创建的文件夹中创建一个名为 01_django.config 的文件:
`# .ebextensions/01_django.config option_settings: aws:elasticbeanstalk:application:environment: DJANGO_SETTINGS_MODULE: "core.settings" PYTHONPATH: "/var/app/current:$PYTHONPATH" aws:elasticbeanstalk:container:python: WSGIPath: "core.wsgi:application"`
注意事项:
- 我们将
PYTHONPATH
设置为 EC2 实例上的 Python 路径( docs )。 - 我们将
DJANGO_SETTINGS_MODULE
指向我们的 Django 设置( docs )。 - 我们将
WSGIPath
更改为我们的 WSGI 应用程序( docs )。
EB 如何。config 文件管用吗?
- 你想要多少就有多少。
- 它们按以下顺序加载:01_x、02_x、03_x 等。
- 您不必记住这些设置;您可以通过运行
eb config
列出您的所有环境设置。如果您想了解更多关于高级环境定制的信息,请查看带有配置文件的高级环境定制。
此时,您的项目结构应该如下所示:
`|-- .ebextensions
| └-- 01_django.config
|-- .elasticbeanstalk
| └-- config.yml
|-- .gitignore
|-- README.md
|-- core
| |-- __init__.py
| |-- asgi.py
| |-- settings.py
| |-- urls.py
| └-- wsgi.py
|-- db.sqlite3
|-- images
| |-- __init__.py
| |-- admin.py
| |-- apps.py
| |-- forms.py
| |-- migrations
| | |-- 0001_initial.py
| | └-- __init__.py
| |-- models.py
| |-- tables.py
| |-- templates
| | └-- images
| | └-- index.html
| |-- tests.py
| |-- urls.py
| └-- views.py
|-- manage.py
└-- requirements.txt`
在重新部署之前,我们必须做的另一件事是将我们的 CNAME 添加到 core/settings.py 中的ALLOWED_HOSTS
:
`# core/settings.py
ALLOWED_HOSTS = [
'xyz.elasticbeanstalk.com', # make sure to replace it with your own EB CNAME
]`
或者,对于测试,您可以只使用通配符:
ALLOWED_HOSTS = ['*']
。只是不要忘记在你完成测试后改变它!
将更改提交给 git 并部署:
`$ git add .
$ git commit -m "updates for eb"
$ eb deploy`
您会注意到,如果您不提交,Elastic Beanstalk 不会检测到这些变化。这是因为 EB 与 git 集成,并且只检测提交的(更改的)文件。
部署完成后,运行eb open
看看是否一切正常
哎哟。我们修复了以前的错误,但现在又出现了新的错误:
`NotSupportedError at /
deterministic=True requires SQLite 3.8.3 or higher`
别担心。这只是 SQLite 的一个问题,无论如何都不应该在生产中使用。我们将在这里与 Postgres 交换它。
配置 RDS
Django 默认使用的数据库 SQLite 。虽然这对于开发来说是完美的,但是对于生产来说,您通常会希望迁移到更健壮的数据库,比如 Postgres 或 MySQL。此外,由于版本依赖冲突,当前的 EB 平台不能很好地与 SQLite 一起工作。因为这两点,我们将用 SQlite 替换掉 Postgres 。
本地邮政汇票
首先,让 Postgres 在本地运行。您可以从 PostgreSQL Downloads 下载它,或者启动 Docker 容器:
`$ docker run --name django-images-postgres -p 5432:5432 \
-e POSTGRES_USER=django-images -e POSTGRES_PASSWORD=complexpassword123 \
-e POSTGRES_DB=django-images -d postgres`
检查容器是否正在运行:
`$ docker ps -f name=django-images-postgres
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c05621dac852 postgres "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5432->5432/tcp django-images-postgres`
现在,让我们试着用 Django 应用程序连接它。在 core/settings.py 中,将DATABASE
配置更改为以下内容:
`# core/settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'django-images',
'USER': 'django-images',
'PASSWORD': 'complexpassword123',
'HOST': 'localhost',
'PORT': '5432',
}
}`
接下来,安装 Postgres 所需的 psycopg2-binary :
`(venv)$ pip install psycopg2-binary==2.9.3`
添加到 requirements.txt :
`Django==4.0.2
Pillow==9.0.1
django-tables2==2.4.1
django-crispy-forms==1.14.0
psycopg2-binary==2.9.3`
创建和应用迁移:
`(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate`
运行服务器:
`(venv)$ python manage.py runserver`
确保您仍然可以在 http://localhost:8000 上传图像。
如果得到一个
DisallowedHost
错误,将localhost
和127.0.0.1
添加到 core/settings.py 内的ALLOWED_HOSTS
中。
AWS RDS Postgres
要为生产设置 Postgres,首先运行以下命令打开 AWS 控制台:
单击左侧栏上的“配置”,向下滚动到“数据库”,然后单击“编辑”。
使用以下设置创建一个数据库,然后单击“应用”:
- 引擎:postgres
- 引擎版本:12.9(自 db.t2.micro 以来的旧 Postgres 版本在 13.1+版本中不可用)
- 实例类:db.t2.micro
- 存储:5 GB(应该绰绰有余)
- 用户名:选择一个用户名
- 密码:选择一个强密码
如果你想留在 AWS 免费层内,确保你选择 db.t2.micro. RDS 价格会根据你选择的实例类呈指数增长。如果你不想和
micro
一起去,一定要复习 AWS PostgreSQL 定价。
环境更新完成后,EB 会自动将以下数据库凭证传递给我们的 Django 应用程序:
`RDS_DB_NAME
RDS_USERNAME
RDS_PASSWORD
RDS_HOSTNAME
RDS_PORT`
我们现在可以使用 core/settings.py 中的这些变量来设置DATABASE
:
`# core/settings.py
if 'RDS_DB_NAME' in os.environ:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': os.environ['RDS_DB_NAME'],
'USER': os.environ['RDS_USERNAME'],
'PASSWORD': os.environ['RDS_PASSWORD'],
'HOST': os.environ['RDS_HOSTNAME'],
'PORT': os.environ['RDS_PORT'],
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'django-images',
'USER': 'django-images',
'PASSWORD': 'complexpassword123',
'HOST': 'localhost',
'PORT': '5432',
}
}`
不要忘记导入 core/settings.py 顶部的os
包:
接下来,我们必须告诉 Elastic Beanstalk 在部署新的应用程序版本时运行makemigrations
和migrate
。我们可以通过编辑来实现。EB extensions/01 _ django . config文件。将以下内容添加到文件的底部:
`# .ebextensions/01_django.config container_commands: 01_makemigrations: command: "source /var/app/venv/*/bin/activate && python3 manage.py makemigrations --noinput" leader_only: true 02_migrate: command: "source /var/app/venv/*/bin/activate && python3 manage.py migrate --noinput" leader_only: true`
现在,每当我们部署新的应用程序版本时,EB 环境都会执行上述命令。我们使用了leader_only
,所以只有第一个 EC2 实例执行它们(以防我们的 EB 环境运行多个 EC2 实例)。
弹性 Beanstalk 配置支持两个不同的命令部分,命令和容器 _ 命令。它们之间的主要区别在于它们在部署过程中的运行时间:
commands
在设置应用程序和 web 服务器以及提取应用程序版本文件之前运行。container_commands
在应用程序和 web 服务器已设置且应用程序版本存档已提取之后,但在应用程序版本部署之前(在文件从暂存文件夹移动到其最终位置之前)运行。
让我们也添加一个命令来创建超级用户。我们可以使用 Django 直观的定制命令框架来添加新命令。在“图像”应用程序中,创建以下文件和文件夹:
`└-- images
└-- management
|-- __init__.py
└-- commands
|-- __init__.py
└-- createsu.py`
create u . py:
`# images/management/commands/createsu.py
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Creates a superuser.'
def handle(self, *args, **options):
if not User.objects.filter(username='admin').exists():
User.objects.create_superuser(
username='admin',
password='complexpassword123'
)
print('Superuser has been created.')`
接下来,将第三个容器命令添加到。EB extensions/01 _ django . config:
`# .ebextensions/01_django.config container_commands: 01_makemigrations: command: "source /var/app/venv/*/bin/activate && python3 manage.py makemigrations --noinput" leader_only: true 02_migrate: command: "source /var/app/venv/*/bin/activate && python3 manage.py migrate --noinput" leader_only: true # ------------------------------------- new ------------------------------------- 03_superuser: command: "source /var/app/venv/*/bin/activate && python3 manage.py createsu" leader_only: true # --------------------------------- end of new ---------------------------------`
创建
createsu
命令的另一种方法是 SSH 到一个 EC2 实例中,然后运行 Django 的默认createsuperuser
命令。
将更改提交给 git 并部署:
`$ git add .
$ git commit -m "updates for eb"
$ eb deploy`
等待部署完成。完成后,运行eb open
在新的浏览器标签中打开你的应用。您的应用程序现在应该可以工作了。确保您可以上传图像。
文件存储的 S3
查看您的管理仪表板的部署版本。静态文件没有得到正确的服务。此外,我们不希望静态或媒体文件本地存储在 EC2 实例上,因为 EB 应用程序应该是无状态的,这使得将您的应用程序扩展到多个 EC2 实例更加容易。
虽然 AWS 提供了许多持久存储服务,但 T2 S3 T3 可以说是最受欢迎和最容易使用的。
要配置 S3,我们需要:
- 创建 S3 存储桶
- 为 S3 时段管理创建 IAM 组和用户
- 设置弹性豆茎 S3 环境变量
- 配置 Django 静态和媒体设置
创建 S3 存储桶
首先,让我们创建一个新的 S3 存储桶。导航到 AWS S3 控制台并点击“创建存储桶”。给这个桶一个惟一的名称,并设置 AWS 区域。对其他一切使用默认配置。按“创建”。
IAM 组和用户
导航到 IAM 控制台。在屏幕左侧,选择“用户组”。创建一个具有“AmazonS3FullAccess”权限的新组:
然后,创建一个具有“编程访问”权限的新用户,并将该组分配给该用户:
AWS 将为您生成认证凭证。下载提供的。csv 文件。在下一步中,我们需要将它们传递给我们的 Elastic Beanstalk 环境。
设置 EB 环境变量
接下来,我们需要设置以下环境变量:
`AWS_ACCESS_KEY_ID - your ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY - your SECRET_ACCESS_KEY AWS_S3_REGION_NAME - your selected S3 region AWS_STORAGE_BUCKET_NAME - your bucket name`
导航到您的弹性豆茎控制台。点击“配置”。然后,在“软件”类别中,单击“编辑”并向下滚动到“环境属性”部分。添加四个变量。
添加完所有变量后,点击“应用”。
接下来,为了让 Django 与我们的 S3 桶通信,我们需要安装 django-storages 和 boto3 包。
将它们添加到 requirements.txt 文件中:
`Django==4.0.2
Pillow==9.0.1
django-tables2==2.4.1
django-crispy-forms==1.14.0
psycopg2-binary==2.9.3
boto3==1.21.3
django-storages==1.12.3`
接下来,将新安装的 app 添加到 core/settings.py 中的INSTALLED_APPS
:
`# core/settings.py
INSTALLED_APPS = [
# ...
'storages',
]`
配置 django-storages 以使用由 Elastic Beanstalk 传递的环境变量:
`if 'AWS_STORAGE_BUCKET_NAME' in os.environ:
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_STORAGE_BUCKET_NAME = os.environ['AWS_STORAGE_BUCKET_NAME']
AWS_S3_REGION_NAME = os.environ['AWS_S3_REGION_NAME']
AWS_S3_ACCESS_KEY_ID = os.environ['AWS_ACCESS_KEY_ID']
AWS_S3_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY']`
最后,我们需要在部署完成后运行collectstatic
命令,因此将以下内容添加到 01_django.config 的底部:
`# .ebextensions/01_django.config # ... container_commands: # ... 04_collectstatic: command: "source /var/app/venv/*/bin/activate && python3 manage.py collectstatic --noinput" leader_only: true`
完整的文件现在应该如下所示:
`# .ebextensions/01_django.config option_settings: aws:elasticbeanstalk:application:environment: DJANGO_SETTINGS_MODULE: "core.settings" PYTHONPATH: "/var/app/current:$PYTHONPATH" aws:elasticbeanstalk:container:python: WSGIPath: "core.wsgi:application" container_commands: 01_makemigrations: command: "source /var/app/venv/*/bin/activate && python3 manage.py makemigrations --noinput" leader_only: true 02_migrate: command: "source /var/app/venv/*/bin/activate && python3 manage.py migrate --noinput" leader_only: true 03_superuser: command: "source /var/app/venv/*/bin/activate && python3 manage.py createsu" leader_only: true 04_collectstatic: command: "source /var/app/venv/*/bin/activate && python3 manage.py collectstatic --noinput" leader_only: true`
将更改提交给 git 并部署:
`$ git add .
$ git commit -m "updates for eb"
$ eb deploy`
确认静态和媒体文件现在存储在 S3 上。
如果您得到一个
Signature mismatch
错误,您可能想要将以下设置添加到 core/settings.py :AWS_S3_ADDRESSING_STYLE = "virtual"
。更多详情,请参见本期 GitHub。要了解更多关于 AWS S3 上静态和媒体文件存储的信息,请看一下亚马逊 S3 上的文章存储 Django 静态和媒体文件。
HTTPS 与证书管理器
教程的这一部分要求您有一个域名。
需要一个便宜的域名来练习?几个域名注册商有特殊优惠。“xyz”域。或者,您可以在 Freenom 创建一个免费域名。如果你没有域名,但仍然想使用 HTTPS,你可以创建并签署一个 X509 证书。
要通过 HTTPS 为您的申请提供服务,我们需要:
- 请求并验证 SSL/TLS 证书
- 把你的域名指向你的 EB CNAME
- 修改负载平衡器以服务于 HTTPS
- 修改您的应用程序设置
请求并验证 SSL/TLS 证书
导航到 AWS 证书管理器控制台。单击“申请证书”。将证书类型设置为“公共”,然后单击“下一步”。在表单输入中输入您的全限定域名,设置“验证方式”为“DNS 验证”,点击“请求”。
然后,您将被重定向到一个页面,在那里您可以看到您的所有证书。您刚刚创建的证书应该具有“待验证”状态。
为了让 AWS 颁发证书,你首先必须证明你是这个域名的所有者。在表中,单击证书以查看“证书详细信息”。注意“CNAME 的名字”和“CNAME 的价值”。要验证域的所有权,您需要在域的 DNS 设置中创建“CNAME 记录”。为此使用“CNAME 名称”和“CNAME 价值”。一旦完成,Amazon 将需要几分钟的时间来获取域更改并颁发证书。状态应该从“等待验证”更改为“已发布”。
将域名指向 EB CNAME
接下来,您需要将您的域(或子域)指向您的 EB 环境 CNAME。回到您的域名的 DNS 设置,添加另一个 CNAME 记录,其值为您的 EB CNAME -例如,django-images-dev.us-west-2.elasticbeanstalk.com
。
等待几分钟,让您的 DNS 刷新,然后在浏览器中测试您的域名的http://
风格。
修改负载平衡器以服务于 HTTPS
回到弹性豆茎控制台,点击“配置”。然后,在“负载平衡器”类别中,单击“编辑”。单击“添加监听程序”并使用以下详细信息创建监听程序:
- 端口- 443
- 议定书- HTTPS
- SSL 证书-选择您刚刚创建的证书
点击“添加”。然后,滚动到页面底部,单击“应用”。环境更新需要几分钟时间。
修改您的应用程序设置
接下来,我们需要对 Django 应用程序进行一些修改。
首先,将您的完全限定域添加到ALLOWED_HOSTS
:
`# core/settings.py
ALLOWED_HOSTS = [
# ...
'yourdomain.com',
]`
最后,我们需要将所有流量从 HTTP 重定向到 HTTPS。有多种方法可以做到这一点,但最简单的方法是将 Apache 设置为代理主机。我们可以通过在中的option_settings
末尾添加以下内容来编程实现这一点。EB extensions/01 _ django . config:
`# .ebextensions/01_django.config
option_settings:
# ...
aws:elasticbeanstalk:environment:proxy: # new
ProxyServer: apache # new`
您最终的 01_django.config 文件现在应该是这样的:
`# .ebextensions/01_django.config option_settings: aws:elasticbeanstalk:application:environment: DJANGO_SETTINGS_MODULE: "core.settings" PYTHONPATH: "/var/app/current:$PYTHONPATH" aws:elasticbeanstalk:container:python: WSGIPath: "core.wsgi:application" aws:elasticbeanstalk:environment:proxy: ProxyServer: apache container_commands: 01_makemigrations: command: "source /var/app/venv/*/bin/activate && python3 manage.py makemigrations --noinput" leader_only: true 02_migrate: command: "source /var/app/venv/*/bin/activate && python3 manage.py migrate --noinput" leader_only: true 03_superuser: command: "source /var/app/venv/*/bin/activate && python3 manage.py createsu" leader_only: true 04_collectstatic: command: "source /var/app/venv/*/bin/activate && python3 manage.py collectstatic --noinput" leader_only: true`
接下来,创建一个”。平台"文件夹中,并添加以下文件和文件夹:
`└-- .platform
└-- httpd
└-- conf.d
└-- ssl_rewrite.conf`
ssl_rewrite.conf :
`# .platform/httpd/conf.d/ssl_rewrite.conf
RewriteEngine On
<If "-n '%{HTTP:X-Forwarded-Proto}' && %{HTTP:X-Forwarded-Proto} != 'https'">
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
</If>`
您的项目结构现在应该如下所示:
`|-- .ebextensions
| └-- 01_django.config
|-- .elasticbeanstalk
| └-- config.yml
|-- .gitignore
|-- .platform
| └-- httpd
| └-- conf.d
| └-- ssl_rewrite.conf
|-- README.md
|-- core
| |-- __init__.py
| |-- asgi.py
| |-- settings.py
| |-- urls.py
| └-- wsgi.py
|-- db.sqlite3
|-- images
| |-- __init__.py
| |-- admin.py
| |-- apps.py
| |-- forms.py
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ ├── __init__.py
│ │ └── createsu.py
| |-- migrations
| | |-- 0001_initial.py
| | └-- __init__.py
| |-- models.py
| |-- tables.py
| |-- templates
| | └-- images
| | └-- index.html
| |-- tests.py
| |-- urls.py
| └-- views.py
|-- manage.py
└-- requirements.txt`
将更改提交给 git 并部署:
`$ git add .
$ git commit -m "updates for eb"
$ eb deploy`
现在,在你的浏览器中,你的应用程序的https://
风格应该工作了。试试去http://
味的。你应该被重定向到https://
风味。确保证书也正确加载:
环境变量
在生产中,最好将特定于环境的配置存储在环境变量中。使用 Elastic Beanstalk,您可以用两种不同的方式设置自定义环境变量。
通过 EB CLI 的环境变量
让我们把 Django 的SECRET_KEY
和DEBUG
设置变成环境变量。
从跑步开始:
`$ eb setenv DJANGO_SECRET_KEY='<replace me with your own secret key>' \
DJANGO_DEBUG='1'`
您可以用一个命令设置多个环境变量,用空格分隔它们。这是推荐的方法,因为它只需要对 EB 环境进行一次更新。
相应地更改 core/settings.py :
`# core/settings.py
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY',
'<replace me with your own fallback secret key>'
)
DEBUG = os.environ.get('DJANGO_DEBUG', '1').lower() in ['true', 't', '1']`
将更改提交给 git 并部署:
`$ git add .
$ git commit -m "updates for eb"
$ eb deploy`
通过 EB 控制台的环境变量
通过eb open
进入弹性豆茎控制台。导航至“配置”>“软件”>“编辑”。然后,向下滚动到“环境属性”。
完成后,单击“应用”,您的环境将会更新。
然后,您可以通过os.environ
在您的 Python 环境中访问这些变量。
例如:
`VARIABLE_NAME = os.environ['VARIABLE_NAME']`
调试弹性豆茎
当使用 Elastic Beanstalk 时,如果您不知道如何访问日志文件,那么找出问题所在会非常令人沮丧。在这一节中,我们将会看到这一点。
有两种方法可以访问日志:
- 弹性 Beanstalk CLI 或控制台
- SSH 到 EC2 实例
从个人经验来看,我已经能够用第一种方法解决所有问题。
弹性 Beanstalk CLI 或控制台
CLI:
该命令将从以下文件中获取最后 100 行:
`/var/log/web.stdout.log /var/log/eb-hooks.log /var/log/nginx/access.log /var/log/nginx/error.log /var/log/eb-engine.log`
运行
eb logs
相当于登录 EB 控制台,导航到“日志”。
我建议将日志传送到 CloudWatch 。运行以下命令来启用此功能:
`$ eb logs --cloudwatch-logs enable`
您通常会在 /var/log/web.stdout.log 或 /var/log/eb-engine.log 中找到 Django 错误。
要了解更多关于弹性 Beanstalk 日志的信息,请查看来自 Amazon EC2 实例的日志。
SSH 到 EC2 实例
要连接到运行 Django 应用程序的 EC2 实例,运行:
第一次会提示您将主机添加到已知主机。答应吧。这样,您现在就可以完全访问 EC2 实例了。请随意试验 Django 管理命令,并检查前一节中提到的一些日志文件。
请记住,Elastic Beanstalk 会自动伸缩和部署新的 EC2 实例。您在这个特定 EC2 实例上所做的更改不会反映在新启动的 EC2 实例上。一旦这个特定的 EC2 实例被替换,您的更改将被清除。
结论
在本教程中,我们介绍了将 Django 应用程序部署到 AWS Elastic Beanstalk 的过程。到目前为止,您应该对弹性豆茎的工作原理有了一个大致的了解。通过回顾本教程开头的目标,快速进行自我检查。
后续步骤:
- 你应该考虑创建两个独立的 EB 环境(
dev
和production
)。 - 查看用于您的弹性 Beanstalk 环境的自动伸缩组,了解如何配置触发器来自动伸缩您的应用程序。
要删除我们在整个教程中创建的所有 AWS 资源,首先要终止 Elastic Beanstalk 环境:
您需要手动删除 S3 桶、SSL 证书以及 IAM 组和用户。
最后,你可以在 GitHub 上的django-elastic-beanstalkrepo 中找到代码的最终版本。
将 Django 应用程序部署到 Fly.io
在本教程中,我们将看看如何部署一个 Django 应用程序到 Fly.io 。
目标
学完本教程后,您应该能够:
- 解释什么是 Fly.io,它是如何工作的。
- 将 Django 应用程序部署到 Fly.io。
- 在 Fly.io 上运行一个 PostgreSQL 实例。
- 通过 Fly Volumes 设置持久存储。
- 将域名链接到您的 web 应用程序。
- 用获得一个 SSL 证书,让我们加密并在 HTTPS 上提供您的应用程序。
Fly.io 是什么?
Fly.io 是一个流行的平台即服务(PaaS)平台,为 web 应用程序提供托管服务。与许多其他 PaaS 托管提供商不同,他们不是转售 AWS 或 GCP 服务,而是在运行于世界各地的物理专用服务器上托管你的应用。正因为如此,他们能够提供比其他 PaaS 更便宜的主机服务,比如 Heroku。他们的主要关注点是尽可能靠近他们的客户部署应用程序(在撰写本文时,你可以在 24 个地区中挑选)。Fly.io 支持三种构建器:Dockerfile、 Buildpacks ,或者预构建的 Docker 映像。
它们提供了强大的缩放和自动缩放功能。
与其他 PaaS 提供商相比,Fly.io 采用不同的方法来管理您的资源。它没有花哨的管理仪表板;相反,所有的工作都是通过他们名为 flyctl 的 CLI 来完成的。
他们的免费计划包括:
- 多达 3 个共享 cpu-1x 256 MB 虚拟机
- 3GB 永久卷存储(总计)
- 160GB 出站数据传输
这应该足够运行一些小应用程序来测试他们的平台了。
为什么要 Fly.io?
- 小型项目的免费计划
- 巨大的地区支持
- 出色的文档和完整的 API 文档
- 轻松实现水平和垂直缩放
- 相对便宜
项目设置
在本教程中,我们将部署一个简单的图像托管应用程序,名为 django-images 。
在学习教程的过程中,通过部署您自己的 Django 应用程序来检查您的理解。
首先,从 GitHub 上的库中获取代码:
创建新的虚拟环境并激活它:
`$ python3 -m venv venv && source venv/bin/activate`
安装需求并迁移数据库:
`(venv)$ pip install -r requirements.txt
(venv)$ python manage.py migrate`
运行服务器:
`(venv)$ python manage.py runserver`
打开您最喜欢的网络浏览器,导航到 http://localhost:8000 。使用右边的表格上传图像,确保一切正常。上传图像后,您应该会看到它显示在表格中:
安装 Flyctl
要使用 Fly 平台,你首先需要安装 Flyctl ,这是一个命令行界面,允许你做从创建帐户到将应用程序部署到 Fly 的所有事情。
要在 Linux 上安装它,请运行:
`$ curl -L https://fly.io/install.sh | sh`
对于其他操作系统,请看一下安装指南。
安装完成后,将flyctl
添加到PATH
:
`$ export FLYCTL_INSTALL="/home/$USER/.fly"
$ export PATH="$FLYCTL_INSTALL/bin:$PATH"`
接下来,使用您的 Fly.io 帐户进行身份验证:
`$ fly auth login
# In case you don't have an account yet:
# fly auth signup`
该命令将打开您的默认 web 浏览器,并要求您登录。登录后,单击“继续”登录 Fly CLI。
为确保一切正常,请尝试列出应用程序:
`$ fly apps list
NAME OWNER STATUS PLATFORM LATEST DEPLOY`
您应该会看到一个空表,因为您还没有任何应用程序。
配置 Django 项目
在教程的这一部分,我们将准备并对接 Django 应用程序,以便部署到 Fly.io。
环境变量
我们不应该在源代码中存储秘密,所以让我们利用环境变量。最简单的方法是使用名为 python-dotenv 的第三方 Python 包。首先将其添加到 requirements.txt :
随意使用不同的包来处理环境变量,如 django-environ 或 python-decouple 。
然后,在 core/settings.py 的顶部导入并初始化 python-dotenv,如下所示:
`# core/settings.py
from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env')`
接下来,从环境中加载SECRET_KEY
、DEBUG
、ALLOWED_HOSTS
和CSRF_TRUSTED_ORIGINS
:
`# core/settings.py
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', '0').lower() in ['true', 't', '1']
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(' ')
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(' ')`
不要忘记在文件顶部导入os
:
数据库ˌ资料库
要使用 Postgres 代替 SQLite,我们首先需要安装数据库适配器。
将下面一行添加到 requirements.txt 中:
当我们在本教程的后面创建 Postgres 实例时,一个受十二因素应用启发的名为DATABASE_URL
的环境变量将被设置并以如下格式传递给我们的 web 应用:
`postgres://USER:PASSWORD@HOST:PORT/NAME`
为了在 Django 中使用它,我们可以使用一个名为 dj-database-url 的包。这个包将 URL 转换成 Django 数据库参数。
像这样添加到 requirements.txt 中:
接下来,导航到 core/settings.py ,将DATABASES
更改如下:
`# core/settings.py
DATABASES = {
'default': dj_database_url.parse(os.environ.get('DATABASE_URL'), conn_max_age=600),
}`
不要忘记重要的一点:
格尼科恩
接下来,让我们安装 Gunicorn ,这是一个生产级的 WSGI 服务器,将用于生产,而不是 Django 的开发服务器。
添加到 requirements.txt :
Dockerfile
如简介中所述,有三种方式将应用部署到 Fly.io:
- Dockerfile
- 构建包
- 预建的 Docker 图像
在本教程中,我们将使用第一种方法,因为它是最灵活的,并且给了我们对 web 应用程序最大的控制权。它也很棒,因为它允许我们在未来轻松地切换到另一个托管服务(支持 Docker)。
首先,在项目根目录下创建一个名为 Dockerfile 的新文件,内容如下:
`# pull official base image
FROM python:3.9.6-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# create the app directory - and switch to it
RUN mkdir -p /app
WORKDIR /app
# install dependencies
COPY requirements.txt /tmp/requirements.txt
RUN set -ex && \
pip install --upgrade pip && \
pip install -r /tmp/requirements.txt && \
rm -rf /root/.cache/
# copy project
COPY . /app/
# expose port 8000
EXPOSE 8000
CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "core.wsgi:application"]`
如果您正在部署自己的 Django 应用程序,请确保相应地更改
CMD
。
接下来,创建一个。dockerignore :
`*.pyc *.pyo *.mo *.db *.css.map *.egg-info *.sql.gz .cache .project .idea .pydevproject .DS_Store .git/ .sass-cache .vagrant/ __pycache__ dist docs env logs Dockerfile`
这是一个通用的。Django 的 dockerignore 模板。如果您想从图像中排除任何其他内容,请确保对其进行更改。
部署应用程序
在本节教程中,我们将启动一个 Postgres 实例,并将我们的 Django 应用程序部署到 Fly.io。
发动
要创建和配置新的应用程序,请运行:
`$ fly launch
Creating app in /dev/django-flyio
Scanning source code
Detected a Dockerfile app
? Choose an app name (leave blank to generate one): django-images
automatically selected personal organization: Nik Tomazic
? Choose a region for deployment: Frankfurt, Germany (fra)
Created app django-images in organization personal
Wrote config file fly.toml
? Would you like to set up a Postgresql database now? Yes
? Select configuration: Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
Creating postgres cluster in organization personal
Creating app...
Setting secrets on app django-images-db...
Provisioning 1 of 1 machines with image flyio/postgres:14.4
Waiting for machine to start...
Machine 21781350b24d89 is created
==> Monitoring health checks
Waiting for 21781350b24d89 to become healthy (started, 3/3)
Postgres cluster django-images-db created
Postgres cluster django-images-db is now attached to django-images
? Would you like to set up an Upstash Redis database now? No
? Would you like to deploy now? No
Your app is ready! Deploy with `flyctl deploy``
注意事项:
- 选择一个应用名称:自定义名称或留空以生成一个随机名称
- 选择部署区域:离你最近的区域
- 是否要现在设置 Postgresql 数据库:是
- 数据库配置:开发
- 您想现在设置一个 Upstash Redis 数据库吗:否
- 是否要立即部署:否
该命令将在 Fly.io 上创建一个应用程序,启动一个 Postgres 实例,并在项目根目录中创建一个名为 fly.toml 的应用程序配置文件。
确保应用程序已成功创建:
`$ fly apps list
NAME OWNER STATUS PLATFORM LATEST DEPLOY
django-images personal pending
django-images-db personal deployed machines
fly-builder-damp-wave-89 personal deployed machines`
你会注意到三个应用程序。第一个是实际的 web 应用程序,然后是 Postgres 实例,最后是一个 Fly builder。Fly builders 用于构建您的 Docker 映像,将它们推送到容器注册表,并部署您的应用程序。
检查你的应用程序的状态:
`$ fly status
App
Name = django-images
Owner = personal
Version = 0
Status = pending
Hostname = django-images.fly.dev
Platform =
App has not been deployed yet.`
主机名告诉您 web 应用程序可以访问哪个地址。记下它,因为我们需要在教程的后面将其添加到ALLOWED_HOSTS
和CSRF_TRUSTED_ORIGINS
中。
应用程序配置
让我们稍微修改一下应用配置文件,使其能够很好地与 Django 配合使用。
首先,将端口8080
更改为 Django 的首选端口8000
:
`# fly.toml app = "django-images" kill_signal = "SIGINT" kill_timeout = 5 processes = [] [env] PORT = "8000" # new [experimental] allowed_public_ports = [] auto_rollback = true [[services]] http_checks = [] internal_port = 8000 # changed processes = ["app"] protocol = "tcp" script_checks = [] [services.concurrency] hard_limit = 25 soft_limit = 20 type = "connections"`
接下来,为了确保数据库得到迁移,添加一个部署部分,并在新创建的部分中定义一个release_command
:
`# fly.toml [deploy] release_command = "python manage.py migrate --noinput"`
在这个版本部署之前,release_command
运行在一个临时的 VM 中——使用成功构建的版本。这对于运行数据库迁移等一次性命令非常有用。
如果将来需要运行多个命令,可以在项目文件中创建一个 bash 脚本,然后像这样执行它:
`# fly.toml [deploy] release_command = "sh /path/to/your/script"`
秘密
设置我们在 Django 的 settings.py 中使用的秘密:
`$ fly secrets set DEBUG="1"
$ fly secrets set ALLOWED_HOSTS="localhost 127.0.0.1 [::1] <your_app_hostname>"
$ fly secrets set CSRF_TRUSTED_ORIGINS="https://<your_app_hostname>"
$ fly secrets set SECRET_KEY="[[email protected]](/cdn-cgi/l/email-protection)"`
确保将<your_app_hostname>
替换为您实际的应用程序主机名。例如:
`$ fly secrets set ALLOWED_HOSTS="localhost 127.0.0.1 [::1] django-images.fly.dev"
$ fly secrets set CSRF_TRUSTED_ORIGINS="https://django-images.fly.dev"`
为了使调试更容易,我们临时启用了调试模式。不要担心,因为我们将在教程的后面更改它。
确保密码设置成功:
`$ fly secrets list
NAME DIGEST CREATED AT
ALLOWED_HOSTS 06d92bcb15cf7eb1 30s ago
CSRF_TRUSTED_ORIGINS 06d92bcb15cf7eb1 21s ago
DATABASE_URL e63c286f83782cf3 5m31s ago
DEBUG 3baf154b33091aa0 45s ago
SECRET_KEY 62ac51c770a436f9 10s ago`
部署 Fly 应用程序后,每个秘密修改都会触发重新部署。如果您需要一次设置多个密码,并且不希望您的应用程序多次重新部署,您可以在一个命令中连接这些密码,如下所示:
`$ fly secrets set NAME1="VALUE1" NAME2="VALUE2"`
部署
要将应用程序部署到 Fly 平台,请运行:
`$ fly deploy
==> Verifying app config
--> Verified app config
==> Building image
Remote builder fly-builder-damp-wave-89 ready
==> Creating build context
--> Creating build context done
==> Building image with Docker
--> docker host: 20.10.12 linux x86_64
[+] Building 24.3s (11/11) FINISHED 0.0s
--> Building image done
==> Pushing image to fly
The push refers to repository [registry.fly.io/django-images]
bee487f02b7f: Pushed
deployment-01GH4AGWQEZ607T7F93RB9G4NB: digest: sha256:85309cd5c7fe58f3a59b13d50576d8568525012bc6e665ba7b5cc1df3da16a9e size: 2619
--> Pushing image done
image: registry.fly.io/django-images:deployment-01GH4AGWQEZ607T7F93RB9G4NB
image size: 152 MB
==> Creating release
--> release v2 created
==> Monitoring deployment
1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
--> v1 deployed successfully`
该命令将使用 Fly builder 构建 Docker 映像,将其推送到容器注册表,并使用它来部署您的应用程序。在部署您的应用程序之前,release_command
将在一个临时虚拟机上运行。
第一次部署你的应用大约需要五分钟,所以你可以在等待的时候喝杯咖啡。
部署应用程序后,检查其状态:
`$ fly status
App
Name = django-images
Owner = personal
Version = 0
Status = running
Hostname = django-images.fly.dev
Platform = nomad
Deployment Status
ID = 563c84b4-bf10-874c-e0e9-9820cbdd6725
Version = v1
Status = successful
Description = Deployment completed successfully
Instances = 1 desired, 1 placed, 1 healthy, 0 unhealthy
Instances
ID PROCESS VERSION REGION DESIRED STATUS HEALTH CHECKS RESTARTS CREATED
c009e8b0 app 1 fra run running 1 total, 1 passing 0 1m8s ago`
检查日志:
`$ fly logs
[info]Starting init (commit: 81d5330)...
[info]Mounting /dev/vdc at /app/data w/ uid: 0, gid: 0 and chmod 0755
[info]Preparing to run: `gunicorn --bind :8000 --workers 2 core.wsgi:application` as root
[info]2022/11/05 17:09:36 listening on [fdaa:0:c65c:a7b:86:2:bd43:2]:22 (DNS: [fdaa::3]:53)
[info][2022-11-05 17:09:36 +0000] [529] [INFO] Starting gunicorn 20.1.0
[info][2022-11-05 17:09:36 +0000] [529] [INFO] Listening at: http://0.0.0.0:8000 (529)
[info][2022-11-05 17:09:36 +0000] [529] [INFO] Using worker: sync
[info][2022-11-05 17:09:36 +0000] [534] [INFO] Booting worker with pid: 534
[info][2022-11-05 17:09:36 +0000] [535] [INFO] Booting worker with pid: 535`
一切看起来都很棒。让我们在浏览器中打开应用程序,确保它能够正常工作:
通过上传图像进行测试。
持久存储
Fly.io(以及许多其他类似的服务,如 Heroku)提供了一个短暂的文件系统。这意味着您的数据不是持久的,可能会在应用程序关闭或重新部署时消失。如果你的应用程序需要保留文件,这是非常糟糕的。
为了解决这个问题,Fly.io 为 Fly 应用程序提供了卷、持久存储。这听起来很棒,但它不是生产的最佳解决方案,因为卷被绑定到一个区域和一个服务器-这限制了您的应用程序的可伸缩性和跨不同区域的分布。
此外,Django 本身并不是为生产中的静态/媒体文件服务的。
我强烈建议你使用 AWS S3 或类似的服务,而不是 Volumes。如果您的应用程序不需要处理媒体文件,您仍然可以使用卷和白化来处理静态文件。
要了解如何使用 Django 设置 AWS S3,请看一下在亚马逊 S3 上存储 Django 静态和媒体文件的。
为了本教程的简单性和完整性,我们仍将使用飞卷。
首先,在与您的应用程序相同的区域创建一个宗卷:
`$ fly volumes create <volume_name> --region <region> --size <in_gigabytes>
# For example:
# fly volumes create django_images_data --region fra --size 1
ID: vol_53q80vdd16xvgzy6
Name: django_images_data
App: django-images
Region: fra
Zone: d7f9
Size GB: 1
Encrypted: true
Created at: 04 Nov 22 13:20 UTC`
在我的例子中,我在法兰克福地区创建了一个 1 GB 的卷。
接下来,进入 core/settings.py ,修改STATIC_ROOT
和MEDIA_ROOT
如下:
`# core/settings.py
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'data/staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'data/mediafiles'`
我们必须将静态和媒体文件放在一个子文件夹中,因为 Fly volume 只允许我们挂载一个目录。
要将目录挂载到 Fly 卷,请转到您的 fly.toml 并添加以下内容:
`# fly.toml [mounts] source="django_images_data" destination="/app/data"`
source
是您的 Fly 卷的名称destination
是您想要挂载的目录的绝对路径
重新部署你的应用程序:
最后,让我们收集静态文件。
SSH 进入 Fly 服务器,导航到“app”目录,运行collectstatic
:
`$ fly ssh console
# cd /app
# python manage.py collectstatic --noinput`
为了确保文件收集成功,请查看一下 /app/data 文件夹:
`# ls /app/data
lost+found staticfiles`
太好了!您可以通过exit
退出 SSH。
通过检查管理面板,确保已经成功收集了静态文件:
`http://<your_app_hostname>/admin`
Django 管理访问
要访问 Django 管理面板,我们需要创建一个超级用户。我们有两个选择:
- 使用
fly ssh
执行命令。 - 创建一个 Django 命令来创建一个超级用户,并将其添加到 fly.toml 中。
由于这是一次性任务,我们将使用第一种方法。
首先,使用 Fly CLI SSH 进入服务器:
`$ fly ssh console
Connecting to fdaa:0:e25c:a7b:8a:4:b5c5:2... complete`
然后,导航到我们的应用程序文件所在的 /app ,运行createsuperuser
命令:
`# cd /app
# python manage.py createsuperuser`
按照提示操作,然后运行exit
关闭 SSH 连接。
要确保已成功创建超级用户,请导航至管理控制面板并登录:
`http://<your_app_hostname>/admin`
或者使用:
然后导航至/管理。
添加域
教程的这一部分要求您有一个域名。
需要一个便宜的域名来练习?几个域名注册商有特殊优惠。“xyz”域。或者,您可以在 Freenom 创建一个免费域名。
若要将域添加到您的应用程序,您首先需要获取一个证书:
`$ fly certs add <your_full_domain_name>
# For example:
# fly certs add fly.testdriven.io`
接下来,进入你的域名注册服务商 DNS 设置,添加一个新的“CNAME 记录”指向你的应用程序的主机名,如下所示:
`+----------+--------------+----------------------------+-----------+ | Type | Host | Value | TTL |
+----------+--------------+----------------------------+-----------+ | A Record | <some host> | <your_app_hostname> | Automatic |
+----------+--------------+----------------------------+-----------+`
示例:
`+----------+--------------+----------------------------+-----------+ | Type | Host | Value | TTL |
+----------+--------------+----------------------------+-----------+ | A Record | fly | django-images.fly.dev | Automatic |
+----------+--------------+----------------------------+-----------+`
如果您不想使用子域,您可以遵循相同的步骤,但只需将 DNS 主机更改为
@
,并相应地配置 Fly.io 证书。
检查域是否已成功添加:
`$ fly certs list
Host Name Added Status
fly.testdriven.io 5 minutes ago Awaiting configuration`
检查证书是否已颁发:
`$ fly certs check <your_full_domain_name>
# For example:
# fly certs check fly.testdriven.io
The certificate for fly.testdriven.io has not been issued yet.
Your certificate for fly.testdriven.io is being issued. Status is Awaiting certificates.`
如果证书尚未颁发,请等待大约十分钟,然后重试:
`$ fly certs check fly.testdriven.io
The certificate for fly.testdriven.io has been issued.
Hostname = fly.testdriven.io
DNS Provider = enom
Certificate Authority = Let's Encrypt
Issued = rsa,ecdsa
Added to App = 8 minutes ago
Source = fly`
最后,将新的域添加到ALLOWED_HOSTS
和CSRF_TRUSTED_ORIGINS
:
`$ fly secrets set ALLOWED_HOSTS="localhost 127.0.0.1 [::1] <your_app_hostname> <your_full_domain_name>"
$ fly secrets set CSRF_TRUSTED_ORIGINS="https://<your_app_hostname> https://<your_full_domain_name>"
# For example:
# fly secrets set ALLOWED_HOSTS="localhost 127.0.0.1 [::1] django-images.fly.dev fly.testdriven.io"
# fly secrets set CSRF_TRUSTED_ORIGINS="https://django-images.fly.dev https://fly.testdriven.io"`
当你修改你的密码时,你的应用将重新启动。重新启动后,您的 web 应用程序应该可以在新添加的域中访问。
结论
在本教程中,我们已经成功地将 Django 应用程序部署到 Fly.io。我们已经处理了 PostgreSQL 数据库、通过 Fly Volumes 的持久存储,并添加了域名。现在,您应该对 Fly 平台的工作原理有了一个大致的了解,并且能够部署自己的应用程序。
下一步是什么?
- 与 AWS S3 或类似的服务交换 Fly Volumes ,以更好、更安全的方式提供静态/媒体文件。
- 设置
DEBUG=0
禁用调试模式。请记住,当启用调试模式时,应用程序仅提供静态/媒体文件。有关如何在生产中处理静态和媒体文件的更多信息,请参考在 Django 中处理静态和媒体文件。 - 看看缩放和自动缩放和区域支持。
将 Django 应用程序部署到 Google 应用程序引擎
在本教程中,我们将看看如何将一个 Django 应用安全地部署到谷歌应用引擎。
目标
学完本教程后,您应该能够:
- 解释什么是谷歌应用引擎,它是如何工作的。
- 将 Django 应用程序部署到 Google App Engine。
- 在云 SQL 上运行 Postgres 实例。
- 利用秘密管理器处理环境变量和秘密。
- 使用云存储为静态和媒体文件设置持久存储。
- 将域名链接到您的应用程序,并在 HTTPS 上提供您的应用程序。
什么是谷歌应用引擎?
Google App Engine (GAE)是一个完全托管的无服务器平台,用于大规模开发和托管网络应用。它具有强大的内置自动扩展功能,可以根据需求自动分配更多/更少的资源。GAE 原生支持用 Python、Node.js、Java、Ruby、C#、Go 和 PHP 编写的应用程序。或者,它通过定制运行时或 Dockerfiles 提供对其他语言的支持。
它具有强大的应用程序诊断功能,您可以将它与云监控和日志记录相结合,以监控您的应用程序的健康状况和性能。此外,GAE 允许你的应用扩展到零,这意味着如果没有人使用你的服务,你不用支付任何费用。
在撰写本文时,谷歌为新用户提供了300 美元的免费积分来试用他们的平台。积分将在 90 天后到期。
项目设置
在本教程中,我们将部署一个简单的图像托管应用程序,名为 django-images 。
在学习教程的过程中,通过部署您自己的 Django 应用程序来检查您的理解。
首先,从 GitHub 上的库中获取代码:
创建新的虚拟环境并激活它:
`$ python3 -m venv venv && source venv/bin/activate`
安装需求并迁移数据库:
`(venv)$ pip install -r requirements.txt
(venv)$ python manage.py migrate`
运行服务器:
`(venv)$ python manage.py runserver`
打开您最喜欢的网络浏览器,导航到 http://localhost:8000 。使用右边的表格上传图像,确保一切正常。上传图像后,您应该会看到它显示在表格中:
安装 Google Cloud CLI
要使用谷歌云平台 (GCP),首先安装谷歌云 CLI (gcloud CLI)。gcloud CLI 允许您创建和管理您的 Google 云资源和服务。
根据您的操作系统和处理器架构,安装过程会有所不同。继续按照官方安装指南为您的操作系统和 CPU 安装。
要验证安装是否成功,请运行:
`$ gcloud version
Google Cloud SDK 415.0.0
bq 2.0.84
core 2023.01.20
gcloud-crc32c 1.0.0
gsutil 5.18`
配置 Django 项目
在教程的这一部分,我们将配置 Django 项目,以便与 GAE 一起工作。
环境变量
我们不应该在源代码中存储秘密,所以让我们利用环境变量。最简单的方法是使用名为 django-environ 的第三方 Python 包。首先将其添加到 requirements.txt :
我建议您继续使用 django-environ,因为它是专门针对 django 的,并且支持数据库 URL 中的 Unix 套接字路径。
对于 Django 来说,要初始化环境更改,请更新 settings.py 的顶部,如下所示:
`# core/settings.py
import os
import environ
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env(DEBUG=(bool, False))
env_file = os.path.join(BASE_DIR, '.env')
env.read_env(env_file)`
接下来,从环境中加载SECRET_KEY
和DEBUG
:
`# core/settings.py
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG')`
为了设置ALLOWED_HOSTS
和CSRF_TRUSTED_ORIGINS
,我们可以使用来自 GAE 文档的以下代码片段:
`# core/settings.py
APPENGINE_URL = env('APPENGINE_URL', default=None)
if APPENGINE_URL:
# ensure a scheme is present in the URL before it's processed.
if not urlparse(APPENGINE_URL).scheme:
APPENGINE_URL = f'https://{APPENGINE_URL}'
ALLOWED_HOSTS = [urlparse(APPENGINE_URL).netloc]
CSRF_TRUSTED_ORIGINS = [APPENGINE_URL]
SECURE_SSL_REDIRECT = True
else:
ALLOWED_HOSTS = ['*']`
这段代码从环境中获取APPENGINE_URL
,并自动配置ALLOWED_HOSTS
和CSRF_TRUSTED_ORIGINS
。此外,它使SECURE_SSL_REDIRECT
能够执行 HTTPS。
不要忘记在文件顶部添加导入:
`from urllib.parse import urlparse`
数据库ˌ资料库
要使用 Postgres 代替 SQLite,我们首先需要安装数据库适配器。
将下面一行添加到 requirements.txt 中:
在本教程的后面,我们将创建一个 Postgres 实例,为我们提供形成一个受十二因素应用启发的数据库 URL 所需的细节。DATABASE_URL
将采用以下格式:
`postgres://USER:[[email protected]](/cdn-cgi/l/email-protection)//cloudsql/PROJECT_ID:REGION:INSTANCE_NAME/DATABASE_NAME`
为了将DATABASE_URL
用于 Django,我们可以像这样使用 django-environ 的db()
方法:
`# core/settings.py
DATABASES = {'default': env.db()}`
格尼科恩
接下来,让我们安装 Gunicorn ,这是一个生产级的 WSGI 服务器,将用于生产,而不是 Django 的开发服务器。
添加到 requirements.txt :
app.yaml
Google App Engine 的 app.yaml 配置文件用于配置 web 应用程序的运行时环境。 app.yaml 文件包含运行时、URL 处理程序和环境变量等信息。
首先在项目根目录下创建一个名为 app.yaml 的新文件,包含以下内容:
`# app.yaml runtime: python39 env: standard entrypoint: gunicorn -b :$PORT core.wsgi:application handlers: - url: /.* script: auto runtime_config: python_version: 3`
注意事项:
- 我们定义了启动 WSGI 服务器的
entrypoint
命令。 - env 有两个选项:
standard
和flexible
。我们选择了 standard,因为它更容易启动和运行,适用于较小的应用程序,并且支持 Python 3.9 开箱即用。 - 最后,
handlers
定义不同的 URL 是如何路由的。我们将在教程的后面定义静态和媒体文件的处理程序。
有关 app.yaml 的更多信息,请查看文档。
。gcloudnignore
一个。gcloudnignorefile 允许您在部署应用程序时指定不想上传到 GAE 的文件。它的工作原理类似于。gitignore 文件。
继续创建一个。项目根中的 gcloudnignore文件包含以下内容:
`# .gcloudignore .gcloudignore # Ignore local .env file .env # If you would like to upload your .git directory, .gitignore file, or files # from your .gitignore file, remove the corresponding line # below: .git .gitignore # Python pycache: __pycache__/ # Ignore collected static and media files mediafiles/ staticfiles/ # Ignore the local DB db.sqlite3 # Ignored by the build system /setup.cfg venv/ # Ignore IDE files .idea/`
部署应用程序
在本节教程中,我们将把应用程序部署到 Google App Engine。
项目初始化
如果您尚未初始化 gcloud CLI,请继续操作:
CLI 将打开您的浏览器,要求您登录并接受一些权限。
之后,你必须选择你的项目。我建议您创建一个新项目,因为删除一个项目比单独删除所有服务和资源更容易。
对于该地区,选择离您最近的地区。
创建应用程序
要创建 App Engine 应用程序,请转到项目根目录并运行:
`$ gcloud app create
You are creating an app for project [indigo-griffin-376011].
WARNING: Creating an App Engine application for a project is irreversible and the region
cannot be changed.
Please choose the region where you want your App Engine application located:
...
[13] europe-west3 (supports standard and flexible and search_api)
[14] europe-west6 (supports standard and flexible and search_api)
[15] northamerica-northeast1 (supports standard and flexible and search_api)
[16] southamerica-east1 (supports standard and flexible and search_api)
[17] us-central (supports standard and flexible and search_api)
[18] us-east1 (supports standard and flexible and search_api)
...
[24] cancel
Please enter your numeric choice: 13
Creating App Engine application in project [indigo-griffin-376011] and region [europe-west3]....done.
Success! The app is now created. Please use `gcloud app deploy` to deploy your first app.`
同样,选择离你最近的地区。
数据库ˌ资料库
规定
导航到云 SQL 仪表板并使用以下参数创建一个新的 Postgres 实例:
- 实例 ID:mydb-实例
- 密码:输入自定义密码或生成密码
- 数据库版本: PostgreSQL 14
- 配置:由你决定
- 地区:与您的应用相同的地区
- 区域可用性:由您决定
您可能还需要启用“计算引擎 API”来创建 SQL 实例。
设置数据库需要几分钟时间。同时,通过搜索“云 SQL 管理 API”并点击“启用”,继续启用云 SQL 管理 API 。我们需要启用它来测试数据库连接。
一旦提供了数据库,您应该会被重定向到数据库详细信息。记下“连接名称”:
接下来,选择侧边栏上的“Databases”并创建一个新的数据库。
最后,选择侧边栏上的“Users”并创建一个新用户。生成一个密码并记下它。
就是这样。数据库现在已经准备好了!
云 SQL 代理
为了测试数据库连接和迁移数据库,我们将使用云 SQL 身份验证代理。云 SQL 身份验证代理提供对云 SQL 实例的安全访问,无需授权网络或配置 SSL。
首先,验证并获取 API 的凭证:
`$ gcloud auth application-default login`
接下来,下载云 SQL 身份验证代理并使其可执行:
`$ wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy
$ chmod +x cloud_sql_proxy`
如果您不在 Linux 上,请遵循安装指南来安装云 SQL 代理。
安装完成后,打开一个新的终端窗口,使用您的连接详细信息启动代理,如下所示:
`$ ./cloud_sql_proxy.exe -instances="PROJECT_ID:REGION:INSTANCE_NAME"=tcp:5432
# Example:
# cloud_sql_proxy.exe -instances="indigo-35:europe-west3:mydb-instance"=tcp:5432
2023/01/30 13:45:22 Listening on 127.0.0.1:5432 for indigo-35:europe-west3:mydb-instance
2023/01/30 13:45:22 Ready for new connections
2023/01/30 13:45:22 Generated RSA key in 110.0168ms`
现在您可以像在本地机器上运行 Postgres 一样连接到localhost:5432
。
迁移数据库
因为 GAE 不允许我们在服务器上执行命令,所以我们必须从本地机器上迁移数据库。
如果您还没有安装,请继续安装这些要求:
`(venv)$ pip install -r requirements.txt`
接下来,创建一个。项目根目录中的 env 文件,带有所需的环境变量:
`# .env DEBUG=1 SECRET_KEY=+an@of0zh--q%vypb^9x@vgecoda5o!m!l9sqno)vz^n!euncl DATABASE_URL=postgres://DB_USER:DB_PASS@localhost/DB_NAME # Example `DATABASE_URL`: # DATABASE_URL=postgres://django-images:[[email protected]](/cdn-cgi/l/email-protection)/mydb`
确保用您的实际凭证替换
DB_USER
、DB_PASS
和DB_NAME
。
最后,迁移数据库:
`(venv)$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, images, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
...
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying images.0001_initial... OK
Applying sessions.0001_initial... OK`
创建超级用户
要创建超级用户,请运行:
`(venv)$ python manage.py createsuperuser`
并按照提示进行操作。
秘密经理
为了安全地管理我们的秘密和环境文件,我们将使用 Secret Manager 。
导航到秘密管理器仪表板并启用 API,如果你还没有的话。接下来,创建一个名为django_settings
的秘密,内容如下:
`DEBUG=1 SECRET_KEY=+an@of0zh--q%vypb^9x@vgecoda5o!m!l9sqno)vz^n!euncl DATABASE_URL=postgres://DB_USER:DB_PASS@//cloudsql/PROJECT_ID:REGION:INSTANCE_NAME/DB_NAME GS_BUCKET_NAME=django-images-bucket # Example `DATABASE_URL`: # postgres://django-images:[[email protected]](/cdn-cgi/l/email-protection)//cloudsql/indigo-35:europe-west3:mydb-instance/mydb`
确保相应地更改
DATABASE_URL
。PROJECT_ID:REGION:INSTANCE_NAME
等于您的数据库连接细节。你不用担心
GS_BUCKET_NAME
。这只是我们稍后要创建和使用的一个存储桶的名称。
回到您的项目,将以下内容添加到 requirements.txt :
`google-cloud-secret-manager==2.15.1`
要从 Secret Manager 加载环境变量,我们可以使用下面的官方代码片段:
`# core/settings.py
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env(DEBUG=(bool, False))
env_file = os.path.join(BASE_DIR, '.env')
if os.path.isfile(env_file):
# read a local .env file
env.read_env(env_file)
elif os.environ.get('GOOGLE_CLOUD_PROJECT', None):
# pull .env file from Secret Manager
project_id = os.environ.get('GOOGLE_CLOUD_PROJECT')
client = secretmanager.SecretManagerServiceClient()
settings_name = os.environ.get('SETTINGS_NAME', 'django_settings')
name = f'projects/{project_id}/secrets/{settings_name}/versions/latest'
payload = client.access_secret_version(name=name).payload.data.decode('UTF-8')
env.read_env(io.StringIO(payload))
else:
raise Exception('No local .env or GOOGLE_CLOUD_PROJECT detected. No secrets found.')`
不要忘记导入文件顶部的io
和secretmanager
:
`import io
from google.cloud import secretmanager`
太好了!终于到了部署我们应用的时候了。为此,请运行:
`$ gcloud app deploy
Services to deploy:
descriptor: [C:\Users\Nik\PycharmProjects\django-images-new\app.yaml]
source: [C:\Users\Nik\PycharmProjects\django-images-new]
target project: [indigo-griffin-376011]
target service: [default]
target version: [20230130t135926]
target url: [https://indigo-griffin-376011.ey.r.appspot.com]
Do you want to continue (Y/n)? y
Beginning deployment of service [default]...
#============================================================#
#= Uploading 21 files to Google Cloud Storage =#
#============================================================#
File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://indigo-griffin-376011.ey.r.appspot.com]
You can stream logs from the command line by running:
$ gcloud app logs tail -s default`
在浏览器中打开您的 web 应用程序,并测试它是否正常工作:
如果你得到一个
502 Bad Gateway
错误,你可以导航到日志浏览器来查看你的日志。如果有一个
403 Permission 'secretmanager.versions.access' denied
错误,导航到django_settings
secret permissions 并确保默认的 App Engine 服务帐户可以访问此机密。参见栈顶溢出的解决方案。
如果您尝试上传图像,您应该会看到以下错误:
`[Errno 30] Read-only file system: '/workspace/mediafiles'`
这是因为 GAE 文件是只读的。别担心。我们将在下一节中解决它。
持久存储
Google App Engine(以及许多其他类似的服务,如 Heroku)提供了一个短暂的文件系统。这意味着您的数据不是持久的,可能会在应用程序关闭或重新部署时消失。此外,GAE 文件是只读的,这使得您无法将媒体文件直接上传到 GAE。
正因为如此,我们将使用云存储来设置持久存储。
服务帐户
要使用云存储,我们首先需要创建一个专用的服务帐户,该帐户具有足够的权限来读/写和签署云存储中的文件。
导航至服务账户仪表板并点击“创建服务账户”。将其命名为“django-images-bucket ”,并将其他内容保留为默认值。提交表单后,您应该会在表格中看到一个新的服务帐户。记下您的新服务帐户的电子邮件。
接下来,单击您的服务帐户旁边的三个点,然后单击“管理详细信息”:
在导航中选择“Keys”和“Create a new key”。将其导出为 JSON。
一旦 JSON 密匙被下载,给它一个更易读的名字,比如 gcpCredentials.json ,并把它放在您的项目根目录中。
确保将该文件添加到您的中。gitignore 以防止您的服务帐户凭证意外泄露给版本控制。
水桶
导航到您的云存储桶,并使用以下详细信息创建一个新桶:
- 名称:姜戈-图像-桶
- 位置类型:地区
- 位置:与您的应用程序相同的地区
将其他内容保留为默认值,然后单击“创建”。
如果您收到一条消息说“公共访问将被阻止”,请取消选中“在此桶上实施公共访问阻止”和“确认”。我们必须允许公众访问,因为我们正在部署一个图像托管网站,并希望上传的图像可以被每个人访问。
有关更多信息,请查看公共访问防护。
要授予我们的新服务帐户对存储桶的权限,请查看您的存储桶详细信息,然后在导航中选择“权限”。之后,点击“授权访问”:
添加新主体:
- 电子邮件:您的服务帐户电子邮件
- 角色:云存储>存储管理员
太好了!我们现在有了一个存储桶和一个可以使用该存储桶的服务帐户。
配置 Django
为了利用 Django 的云存储,我们将使用一个名为 django-storages 的第三方包。
在本地安装软件包,并将下面两行添加到 requirements.txt :
`django-storages[google]==1.13.2 google-cloud-storage==2.7.0`
接下来,配置 django-storages 以使用云存储和您的服务帐户:
`# core/settings.py
GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
os.path.join(BASE_DIR, 'gcpCredentials.json')
)
DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
STATICFILES_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
GS_BUCKET_NAME = env('GS_BUCKET_NAME')`
不要忘记重要的一点:
`from google.oauth2 import service_account`
使用 app.yaml 中的处理程序使 App Engine 服务于静态和媒体文件:
`# app.yaml handlers: - url: /static # new static_dir: staticfiles/ # new - url: /media # new static_dir: mediafiles/ # new - url: /.* script: auto`
确保它们被放置在/.*
之前。
如果你正在部署自己的应用程序,确保你的
STATIC_URL
、STATIC_ROOT
、MEDIA_URL
和MEDIA_ROOT
设置正确(示例)。
收集静态文件到 GAE:
`(venv)$ python manage.py collectstatic
You have requested to collect static files at the destination
location as specified in your settings.
This will overwrite existing files!
Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel: yes
141 static files copied.`
重新部署你的应用程序:
在浏览器中打开应用程序:
导航到/admin
并确保静态文件已被收集和加载。
自定义域
要将自定义域链接到您的 web 应用,首先导航到应用引擎仪表板。在边栏中选择“设置”,然后选择“自定义域”。最后,单击“添加自定义域”。
选择您想要使用的域,或者添加并验证新域。如果您添加一个新域,请确保输入裸域名。示例:
`testdriven.io <-- good
app.testdriven.io <-- bad`
单击“继续”,然后输入要映射到你的 GAE 应用的域名和子域名。然后单击“保存映射”。
接下来,转到您的域名注册商的 DNS 设置,添加一个指向ghs.googlehosted.com
的新“CNAME 记录”,如下所示:
`+----------+--------------+----------------------------+-----------+ | Type | Host | Value | TTL |
+----------+--------------+----------------------------+-----------+ | CNAME | <some host> | ghs.googlehosted.co | Automatic |
+----------+--------------+----------------------------+-----------+`
示例:
`+----------+--------------+----------------------------+-----------+ | Type | Host | Value | TTL |
+----------+--------------+----------------------------+-----------+ | CNAME | app | ghs.googlehosted.com | Automatic |
+----------+--------------+----------------------------+-----------+`
返回到您的自定义域设置,然后单击“完成”。
您已成功添加了自定义域名。等待 DNS 更改传播,等待 Google 发布 SSL 证书。要耐心...有时需要 24 小时。
在您验证您的应用程序可通过 HTTPS 访问后,将以下内容添加到 app.yaml 的末尾:
`# app.yaml env_variables: APPENGINE_URL: <the domain you mapped> # Example: # env_variables: # APPENGINE_URL: app.testdriven.io`
这添加了一个新的环境变量,由 settings.py 用来配置ALLOWED_HOSTS
、CSRF_TRUSTED_ORIGINS
,并强制 SSL 重定向。
最后,再次重新部署您的应用:
结论
在本教程中,我们已经成功地将 Django 应用程序部署到 Google App Engine。我们已经处理了 Postgres 数据库、静态和媒体文件,添加了自定义域名,并启用了 HTTPS。现在,您应该能够将自己的应用程序部署到 App Engine。
从 django-app-engine repo 中获取最终的源代码。
如果您希望删除我们在整个教程中创建的所有服务和资源,请查看文档中的清理资源。
未来的步骤
使用 Django、htmx 和 Tailwind CSS 进行快速原型制作
在本教程中,你将学习如何用 htmx 和 Tailwind CSS 设置 Django。htmx 和 Tailwind 的目标都是简化现代 web 开发,这样您就可以设计和实现交互性,而不会离开 HTML 的舒适和方便。我们还将看看如何使用 Django Compressor 来捆绑和缩小 Django 应用程序中的静态资产。
htmx
htmx 是一个库,它允许你直接从 HTML 访问现代浏览器特性,如 AJAX、CSS 转换、WebSockets 和服务器发送的事件,而不是使用 JavaScript。它允许您直接在标记中快速构建用户界面。
htmx 扩展了浏览器已经内置的几个特性,比如发出 HTTP 请求和响应事件。例如,您可以使用 HTML 属性在任何 HTML 元素上发送 GET、POST、PUT、PATCH 或 DELETE 请求,而不仅仅是通过a
和form
元素发出 GET 和 POST 请求:
`<button hx-delete="/user/1">Delete</button>`
您还可以更新页面的某些部分来创建单页应用程序(SPA):
见 CodePen 上 Michael Herman ( @mjhea0 )的笔 RwoJYyx 。
在浏览器的开发工具中打开网络标签。当点击按钮时,一个 XHR 请求被发送到https://v2.jokeapi.dev/joke/Any?format=txt&safe-mode
端点。然后,响应被附加到具有输出的id
的p
元素。
如需更多示例,请查看官方 htmx 文档中的 UI 示例页面。
利弊
优点:
缺点:
- 库成熟度:由于库相当新,文档和示例实现很少。
- 传输数据的大小:通常,SPA 框架(如 React 和 Vue)通过在客户机和服务器之间以 JSON 格式来回传递数据来工作。然后,接收到的数据由客户端呈现。另一方面,htmx 从服务器接收呈现的 HTML,并用响应替换目标元素。呈现格式的 HTML 通常比 JSON 响应更大。
顺风 CSS
Tailwind CSS 是一个“实用优先”的 CSS 框架。它不提供预先构建的组件(像 Bootstrap 和布尔玛这样的框架专门提供这些组件),而是以实用程序类的形式提供构建模块,使人们能够快速、轻松地创建布局和设计。
例如,以下面的 HTML 和 CSS 为例:
`<style> .hello { height: 5px; width: 10px; background: gray; border-width: 1px; border-radius: 3px; padding: 5px; } </style>
<div class="hello">Hello World</div>`
这可以通过顺风实现,如下所示:
`<div class="h-1 w-2 bg-gray-600 border rounded-sm p-1">Hello World</div>`
查看 CSS 顺风转换器将原始 CSS 转换为顺风中的等效实用程序类。比较结果。
利弊
优点:
- 高度可定制:虽然 Tailwind 自带预建类,但是可以使用 tailwind.config.js 文件覆盖它们。
- 优化:你可以配置 Tailwind,通过只加载实际使用的类来优化 CSS 输出。
- 黑暗模式:轻松实现黑暗模式——比如
<div class="bg-white dark:bg-black">
。
缺点:
- 组件 : Tailwind 不提供任何官方预置的组件,比如按钮、卡片、导航条等等。组件必须从头开始创建。有一些社区驱动的组件资源,例如顺风 CSS 组件和顺风工具箱。还有一个强大的组件库,尽管是付费的,由 Tailwind 的开发者开发,名为 Tailwind UI 。
- CSS 是内联的:它将内容和设计结合在一起,增加了页面的大小,使 HTML 变得混乱。
Django 压缩机
Django Compressor 是一个扩展,用于管理(压缩/缓存)Django 应用程序中的静态资产。通过它,您可以为以下各项创建简单的资产管道:
至此,让我们来看看如何在 Django 中使用上述工具!
项目设置
首先,为我们的项目创建一个新文件夹,创建并激活一个新的虚拟环境,并安装 Django 和 Django Compressor:
`$ mkdir django-htmx-tailwind && cd django-htmx-tailwind
$ python3.10 -m venv venv
$ source venv/bin/activate
(venv)$
(venv)$ pip install Django==4.0.3 django-compressor==3.1`
接下来,让我们安装 pytailwindcss 并下载它的二进制文件:
`(venv)$ pip install pytailwindcss==0.1.4
(venv)$ tailwindcss`
创建一个新的 Django 项目和一个todos
应用:
`(venv)$ django-admin startproject config .
(venv)$ python manage.py startapp todos`
将应用添加到 config/settings.py 中的INSTALLED_APPS
列表:
`# config/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'todos', # new
'compressor', # new
]`
在项目的根目录下创建一个“模板”文件夹。然后,像这样更新TEMPLATES
设置:
`# config/settings.py
TEMPLATES = [
{
...
'DIRS': [BASE_DIR / 'templates'], # new
...
},
]`
让我们为compressor
向 config/settings.py 添加一些配置:
`# config/settings.py
COMPRESS_ROOT = BASE_DIR / 'static'
COMPRESS_ENABLED = True
STATICFILES_FINDERS = ('compressor.finders.CompressorFinder',)`
注意事项:
- COMPRESS_ROOT 定义要压缩的文件的读取和写入的绝对位置。
- COMPRESS_ENABLED 判断压缩是否会发生的布尔值。默认为
DEBUG
的相反值。 - 安装
django.contrib.staticfiles
时,必须包含 Django Compressor 的文件查找器。
初始化项目中的顺风 CSS:
该命令在项目的根目录下创建了一个 tailwind.config.js 文件。所有与顺风相关的定制都放在这个文件中。
更新 tailwind.config.js 这样:
`module.exports = { content: [ './templates/**/*.html', ], theme: { extend: {}, }, plugins: [], }`
记下部分的内容。在这里,您可以配置项目 HTML 模板的路径。顺风 CSS 将扫描你的模板,搜索顺风类名。生成的输出 CSS 文件将只包含模板文件中相关类名的 CSS。这有助于保持生成的 CSS 文件较小,因为它们将只包含实际使用的样式。
接下来,在项目根目录中,创建以下文件和文件夹:
`static
└── src
└── main.css`
然后,将以下内容添加到 static/src/main.css 中:
`/* static/src/main.css */ @tailwind base; @tailwind components; @tailwind utilities;`
这里,我们定义了来自 Tailwind CSS 的所有base
、components
和utilities
类。
就是这样。你现在有姜戈压缩机和顺风连线。接下来,我们将看看如何提供一个index.html文件来看看 CSS 的作用。
简单的例子
像这样更新 todos/views.py 文件:
`# todos/views.py
from django.shortcuts import render
def index(request):
return render(request, 'index.html')`
将视图添加到 todos/urls.py :
`# todos/urls.py
from django.urls import path
from .views import index
urlpatterns = [
path('', index, name='index'),
]`
然后,将todos.urls
添加到 config/urls.py :
`# config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('todos.urls')), # new
]`
向“模板”添加一个 _base.html 文件:
`<!-- templates/_base.html -->
{% load compress %}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Django + HTMX + Tailwind CSS</title>
{% compress css %}
<link rel="stylesheet" href="{% static 'src/output.css' %}">
{% endcompress %}
</head>
<body class="bg-blue-100">
{% block content %}
{% endblock content %}
</body>
</html>`
注意事项:
{% load compress %}
导入与 Django Compressor 配合使用所需的所有标签。{% load static %}
将静态文件载入模板。{% compress css %}
对静态/src/main.css 文件应用适当的过滤器。
此外,我们通过<body class="bg-blue-100">
给 HTML 主体添加了一些颜色。bg-blue-100
用于将背景颜色改为浅蓝色。
添加一个index.html文件:
`<!-- templates/index.html -->
{% extends "_base.html" %}
{% block content %}
<h1>Hello World</h1>
{% endblock content %}`
现在,在项目的根目录下运行以下命令,扫描模板中的类并生成一个 CSS 文件:
`(venv)$ tailwindcss -i ./static/src/main.css -o ./static/src/output.css --minify`
应用迁移并运行开发服务器:
`(venv)$ python manage.py migrate
(venv)$ python manage.py runserver`
在浏览器中导航到 http://localhost:8000 以查看结果。还要注意“static/CACHE/css”文件夹中生成的文件。
在配置了 Tailwind 之后,让我们将 htmx 添加到组合中,并构建一个在您键入时显示结果的 live search。
实时搜索示例
与其从 CDN 中获取 htmx 库,不如下载它并使用 Django Compressor 进行捆绑。
从https://unpkg.com/【邮件保护】 /dist/htmx.js 下载库,保存到 static/src/htmx.js 。
为了让我们有一些数据可以处理,将https://github . com/testdrivenio/django-htmx-tailwind/blob/master/todos/todo . py保存到一个名为 todos/todo.py 的新文件中。
现在,将实现搜索功能的视图添加到 todos/views.py :
`# todos/views.py
from django.shortcuts import render
from django.views.decorators.http import require_http_methods # new
from .todo import todos # new
def index(request):
return render(request, 'index.html', {'todos': []}) # modified
# new
@require_http_methods(['POST'])
def search(request):
res_todos = []
search = request.POST['search']
if len(search) == 0:
return render(request, 'todo.html', {'todos': []})
for i in todos:
if search in i['title']:
res_todos.append(i)
return render(request, 'todo.html', {'todos': res_todos})`
我们添加了一个新的视图search
,它搜索 todos 并呈现带有所有结果的todo.html模板。
将新创建的视图添加到 todos/urls.py :
`# todos/urls.py
from django.urls import path
from .views import index, search # modified
urlpatterns = [
path('', index, name='index'),
path('search/', search, name='search'), # new
]`
接下来,将新资产添加到 _base.html 文件中:
`<!-- templates/base.html -->
{% load compress %}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Django + HTMX + Tailwind CSS</title>
{% compress css %}
<link rel="stylesheet" href="{% static 'src/output.css' %}">
{% endcompress %}
</head>
<body class="bg-blue-100">
{% block content %}
{% endblock content %}
<!-- new -->
{% compress js %}
<script type="text/javascript" src="{% static 'src/htmx.js' %}"></script>
{% endcompress %}
<!-- new -->
<script> document.body.addEventListener('htmx:configRequest', (event) => { event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}'; }) </script>
</body>
</html>`
我们使用{% compress js %}
标签加载了 htmx 库。js
标签,默认为,应用JSMinFilter
(反过来,应用 rjsmin )。因此,这将缩小 static/src/htmx.js 并从“static/CACHE”文件夹中提供它。
我们还添加了以下脚本:
`document.body.addEventListener('htmx:configRequest', (event) => { event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}'; })`
该事件侦听器将 CSRF 令牌添加到请求标头中。
接下来,让我们添加基于每个待办事项标题的搜索功能。
更新index.html文件是这样的:
`<!-- templates/index.html -->
{% extends "_base.html" %}
{% block content %}
<div class="w-small w-2/3 mx-auto py-10 text-gray-600">
<input
type="text"
name="search"
hx-post="/search/"
hx-trigger="keyup changed delay:250ms"
hx-indicator=".htmx-indicator"
hx-target="#todo-results"
placeholder="Search"
class="bg-white h-10 px-5 pr-10 rounded-full text-2xl focus:outline-none"
>
<span class="htmx-indicator">Searching...</span>
</div>
<table class="border-collapse w-small w-2/3 mx-auto">
<thead>
<tr>
<th class="p-3 font-bold uppercase bg-gray-200 text-gray-600 border border-gray-300 hidden lg:table-cell">#</th>
<th class="p-3 font-bold uppercase bg-gray-200 text-gray-600 border border-gray-300 hidden lg:table-cell">Title</th>
<th class="p-3 font-bold uppercase bg-gray-200 text-gray-600 border border-gray-300 hidden lg:table-cell">Completed</th>
</tr>
</thead>
<tbody id="todo-results">
{% include "todo.html" %}
</tbody>
</table>
{% endblock content %}`
让我们花点时间来看看从 htmx 定义的属性:
`<input
type="text"
name="search"
hx-post="/search/"
hx-trigger="keyup changed delay:250ms"
hx-indicator=".htmx-indicator"
hx-target="#todo-results"
placeholder="Search"
class="bg-white h-10 px-5 pr-10 rounded-full text-2xl focus:outline-none"
>`
- 输入向
/search
端点发送一个 POST 请求。 - 该请求通过延迟 250 毫秒的击键事件触发。因此,如果在上一次按键之后 250 毫秒之前输入了新的按键事件,则不会触发请求。
- 来自请求的 HTML 响应显示在
#todo-results
元素中。 - 我们还有一个指示器,它是一个加载元素,在发送请求后出现,在响应返回后消失。
添加模板/todo.html 文件:
`<!-- templates/todo.html -->
{% for todo in todos %}
<tr
class="bg-white lg:hover:bg-gray-100 flex lg:table-row flex-row lg:flex-row flex-wrap lg:flex-no-wrap mb-10 lg:mb-0">
<td class="w-full lg:w-auto p-3 text-gray-800 text-center border border-b block lg:table-cell relative lg:static">
{{todo.id}}
</td>
<td class="w-full lg:w-auto p-3 text-gray-800 text-center border border-b block lg:table-cell relative lg:static">
{{todo.title}}
</td>
<td class="w-full lg:w-auto p-3 text-gray-800 text-center border border-b block lg:table-cell relative lg:static">
{% if todo.completed %}
<span class="rounded bg-green-400 py-1 px-3 text-xs font-bold">Yes</span>
{% else %}
<span class="rounded bg-red-400 py-1 px-3 text-xs font-bold">No</span>
{% endif %}
</td>
</tr>
{% endfor %}`
该文件呈现了与我们的搜索查询相匹配的待办事项。
生成一个新的 src/output.css 文件:
`(venv)$ tailwindcss -i ./static/src/main.css -o ./static/src/output.css --minify`
使用python manage.py runserver
运行应用程序,并再次导航到 http://localhost:8000 进行测试。
结论
在本教程中,我们学习了如何:
- 设置 Django 压缩机和顺风 CSS
- 使用 Django、Tailwind CSS 和 htmx 构建一个实时搜索应用程序
htmx 可以在不重新加载页面的情况下呈现元素。最重要的是,您无需编写任何 JavaScript 就可以实现这一点。虽然这减少了客户端所需的工作量,但从服务器发送的数据可能会更多,因为它发送的是渲染的 HTML。
像这样提供部分 HTML 模板在 21 世纪初很流行。htmx 为这种方法提供了一种现代的变形。总的来说,由于 React 和 Vue 等框架的复杂性,提供部分模板再次变得流行起来。您还可以将 WebSockets 添加到组合中,以交付实时更改。著名的 Phoenix LiveView 也使用了同样的方法。你可以在阅读更多关于 HTML over WebSockets 的内容,Web 软件的未来是 HTML-Over Web sockets和 HTML Over WebSockets 。
图书馆还年轻,但未来看起来很光明。
Tailwind 是一个强大的 CSS 框架,专注于开发人员的生产力。虽然这个教程没有涉及到,但是 Tailwind 是高度可定制的。查看以下资源了解更多信息:
当使用 Django 时,一定要将 htmx 和 Tailwind 与 Django Compressor 耦合,以简化静态资产管理。
完整的代码可以在 django-htmx-tailwind 资源库中找到。
用加密保护容器化的 Django 应用程序
如何为 Django 应用程序设置 SSL 证书?
在本教程中,我们将看看如何使用加密 SSL 证书来保护运行在 HTTPS Nginx 代理后面的容器化 Django 应用程序。
本教程建立在用 Postgres、Gunicorn 和 Nginx 构建 Django 的基础上。它假设您了解如何将 Django 应用程序与 Postgres、Nginx 和 Gunicorn 一起容器化。
如今,你根本无法让你的应用程序运行在 HTTP 之上。没有 HTTPS,你的网站就不那么安全和可信。“让我们加密”简化了获取和安装 SSL 证书的过程,再也没有理由不使用 HTTPS 了。
姜戈码头系列:
先决条件
要学习本教程,您需要:
需要一个便宜的域名来练习?几个域名注册商有特殊优惠。“xyz”域。或者,您可以在 Freenom 创建一个免费域名。
方法
有许多不同的方法来保护 HTTPS 的容器化 Django 应用程序。可以说,最流行的方法是向 Docker Compose 文件添加一个新的服务,该服务利用 Certbot 来发布和更新 SSL 证书。虽然这是完全正确的,但我们将采用稍微不同的方法,并使用以下项目:
- nginx-proxy -用于自动构建运行容器的 nginx 代理配置,其中每个容器都被视为单个虚拟主机
- Let sencrypt-nginx-proxy-companion-用于为 nginx-proxy 代理的每个容器颁发和更新加密 SSL 证书
总之,这些项目简化了 Nginx 配置和 SSL 证书的管理。
另一种选择是使用 Traefik 而不是 Nginx。简而言之,Traefik 使用 Let's Encrypt 来发布和更新证书。更多信息,请查看Dockerizing Django with Postgres、Gunicorn 和 Traefik 。
让我们加密
首次部署应用程序时,您应该遵循以下两个步骤来避免证书问题:
- 首先从 Let's Encrypt 的登台环境中颁发证书
- 然后,当一切按预期运行时,切换到 Let's Encrypt 的生产环境
为什么?
为了保护他们的服务器,让我们对他们的生产验证系统实施速率限制:
- 每个帐户、每个主机名每小时 5 次验证失败
- 每个域每周可以创建 50 个证书
如果您在域名或 DNS 条目或任何类似的地方输入错误,您的请求将失败,这将计入您的费率限制,您将不得不尝试颁发新的证书。
为了避免速率受限,在开发和测试期间,您应该使用 Let's Encrypt 的登台环境来测试他们的验证系统。在分段环境中,速率限制要高得多,这对测试来说更好。请注意,暂存中颁发的证书并不公开可信,因此一旦一切正常,您应该切换到他们的生产环境。
项目设置
首先,克隆 GitHub 项目报告的内容:
`$ git clone https://github.com/testdrivenio/django-on-docker django-on-docker-letsencrypt
$ cd django-on-docker-letsencrypt`
这个库包含了部署 Dockerized Django 应用程序所需的一切,但不包括 SSL 证书,我们将在本教程中添加 SSL 证书。
Django 配置
首先,要在 HTTPS 代理后面运行 Django 应用程序,您需要将 SECURE_PROXY_SSL_HEADER 设置添加到 settings.py :
`SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")`
在这个元组中,当X-Forwarded-Proto
被设置为https
时,请求是安全的。
复合坞站
是时候配置 Docker Compose 了。
让我们添加一个新的 Docker Compose 文件用于测试,名为Docker-Compose . staging . yml:
`version: '3.8' services: web: build: context: ./app dockerfile: Dockerfile.prod command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000 volumes: - static_volume:/home/app/web/staticfiles - media_volume:/home/app/web/mediafiles expose: - 8000 env_file: - ./.env.staging depends_on: - db db: image: postgres:13.0-alpine volumes: - postgres_data:/var/lib/postgresql/data/ env_file: - ./.env.staging.db nginx-proxy: container_name: nginx-proxy build: nginx restart: always ports: - 443:443 - 80:80 volumes: - static_volume:/home/app/web/staticfiles - media_volume:/home/app/web/mediafiles - certs:/etc/nginx/certs - html:/usr/share/nginx/html - vhost:/etc/nginx/vhost.d - /var/run/docker.sock:/tmp/docker.sock:ro depends_on: - web nginx-proxy-letsencrypt: image: jrcs/letsencrypt-nginx-proxy-companion env_file: - ./.env.staging.proxy-companion volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - certs:/etc/nginx/certs - html:/usr/share/nginx/html - vhost:/etc/nginx/vhost.d - acme:/etc/acme.sh depends_on: - nginx-proxy volumes: postgres_data: static_volume: media_volume: certs: html: vhost: acme:`
为db
容器添加一个 .env.staging.db 文件:
`POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod`
更改
POSTGRES_USER
和POSTGRES_PASSWORD
的值,以匹配您的用户和密码。
我们已经在之前的教程中看到了web
和db
服务,所以让我们深入研究一下nginx-proxy
和nginx-proxy-letsencrypt
服务。
数据库是关键服务。增加额外的层,例如 Docker,会增加生产中不必要的风险。为了简化小版本更新、定期备份和扩展等任务,建议使用托管服务,如 AWS RDS 、 Google Cloud SQL 或 DigitalOcean 托管数据库。
Nginx 代理服务
对于这个服务, nginx-proxy 项目用于为使用虚拟主机进行路由的web
容器生成反向代理配置。
请务必查看关于 nginx-proxy repo 的自述文件。
一旦启动,与nginx-proxy
相关联的容器自动检测设置了VIRTUAL_HOST
环境变量的容器(在同一网络中),并动态更新其虚拟主机配置。
继续为web
容器添加一个 .env.staging 文件:
`DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=<YOUR_DOMAIN.COM>
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
VIRTUAL_HOST=<YOUR_DOMAIN.COM>
VIRTUAL_PORT=8000
LETSENCRYPT_HOST=<YOUR_DOMAIN.COM>`
注意事项:
- 将
<YOUR_DOMAIN.COM>
更改为您的实际域,并将SQL_USER
和SQL_PASSWORD
的默认值更改为匹配POSTGRES_USER
和POSTGRES_PASSWORD
(来自 .env.staging.db )。 - 如上所述,
nginx-proxy
需要VIRTUAL_HOST
(和VIRTUAL_PORT
)来自动创建反向代理配置。 LETSENCRYPT_HOST
有没有办法让nginx-proxy-companion
为你的域名颁发加密证书。- 由于 Django 应用程序将监听端口 8000,我们还设置了
VIRTUAL_PORT
环境变量。 - docker-compose . staging . yml中的
/var/run/docker.sock:/tmp/docker.sock:ro
卷用于监听新注册/注销的容器。
出于测试/调试的目的,您可能希望在第一次部署时使用一个
*
来代替DJANGO_ALLOWED_HOSTS
,以简化事情。只是不要忘记在测试完成后限制允许的主机。
因此,对指定域的请求将由代理到容器,该容器将域设置为VIRTUAL_HOST
环境变量。
接下来,让我们更新“nginx”文件夹中的 Nginx 配置。
首先,添加名为“vhost.d”的目录。然后,在该目录中添加一个名为 default 的文件来提供静态和媒体文件:
`location /static/ {
alias /home/app/web/staticfiles/;
add_header Access-Control-Allow-Origin *;
}
location /media/ {
alias /home/app/web/mediafiles/;
add_header Access-Control-Allow-Origin *;
}`
符合任何这些模式的请求将从静态或媒体文件夹中得到服务。它们不会被代理到其他容器。web
和nginx-proxy
容器共享静态和媒体文件所在的卷:
`static_volume:/home/app/web/staticfiles
media_volume:/home/app/web/mediafiles`
将一个 custom.conf 文件添加到“nginx”文件夹中,以保存自定义代理范围的配置:
`client_max_body_size 10M;`
更新 nginx/Dockerfile :
`FROM jwilder/nginx-proxy:0.9
COPY vhost.d/default /etc/nginx/vhost.d/default
COPY custom.conf /etc/nginx/conf.d/custom.conf`
移除 nginx.conf 。
您的“nginx”目录现在应该如下所示:
`└── nginx
├── Dockerfile
├── custom.conf
└── vhost.d
└── default`
让我们加密 Nginx 代理配套服务
当nginx-proxy
服务处理路由时,nginx-proxy-letsencrypt
(通过lets Encrypt-nginx-proxy-companion)处理代理 Docker 容器的证书的创建、更新和使用。
要为代理容器颁发和更新证书,需要为每个容器添加LETSENCRYPT_HOST
环境变量(我们已经完成了)。它还必须具有与VIRTUAL_HOST
相同的值。
该容器必须与nginx-proxy
共享以下卷:
certs:/etc/nginx/certs
存储证书、私钥和 ACME 帐户密钥html:/usr/share/nginx/html
写入 http-01 挑战文件vhost:/etc/nginx/vhost.d
更改虚拟主机的配置
有关更多信息,请查看官方文档。
添加一个. env . staging . proxy-companion文件:
`DEFAULT_EMAIL=youremail@yourdomain.com ACME_CA_URI=https://acme-staging-v02.api.letsencrypt.org/directory NGINX_PROXY_CONTAINER=nginx-proxy`
注意事项:
DEFAULT_EMAIL
是我们将用来向您发送证书(包括续订)通知的电子邮件。ACME_CA_URI
是用于颁发证书的 URL。再次,使用阶段直到你 100%确定一切正常。NGINX_PROXY_CONTAINER
是nginx-proxy
容器的名称。
运行容器
一切准备就绪,随时可以部署。
是时候转移到您的 Linux 实例了。
假设在您的实例上创建了一个项目目录,如/home/my user/django-on-docker,用 SCP 复制文件和文件夹:
`$ scp -r $(pwd)/{app,nginx,.env.staging,.env.staging.db,.env.staging.proxy-companion,docker-compose.staging.yml} [[email protected]](/cdn-cgi/l/email-protection):/path/to/django-on-docker`
通过 SSH 连接到您的实例,并移动到项目目录:
这时,您就可以构建映像并旋转容器了:
`$ docker-compose -f docker-compose.staging.yml up -d --build`
一旦容器启动并运行,在浏览器中导航到您的域。您应该会看到类似这样的内容:
这是意料之中的。显示该屏幕是因为证书是从暂存环境中颁发的,该环境同样没有与生产环境相同的速率限制。它类似于一个自签名的 HTTPS 证书。始终使用试运行环境,直到您确定一切都按预期运行。
你怎么知道一切是否正常?
点击“高级”,然后点击“继续”。您现在应该可以看到您的应用程序了。上传图像,然后确保您可以在https://yourdomain.com/mediafiles/IMAGE_FILE_NAME
查看图像。
颁发生产证书
现在,一切都按预期运行,我们可以切换到 Let's Encrypt 的生产环境。
关闭现有容器并退出实例:
`$ docker-compose -f docker-compose.staging.yml down -v
$ exit`
回到您的本地机器上,更新docker-composite . product . yml:
`version: '3.8' services: web: build: context: ./app dockerfile: Dockerfile.prod command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000 volumes: - static_volume:/home/app/web/staticfiles - media_volume:/home/app/web/mediafiles expose: - 8000 env_file: - ./.env.prod depends_on: - db db: image: postgres:13.0-alpine volumes: - postgres_data:/var/lib/postgresql/data/ env_file: - ./.env.prod.db nginx-proxy: container_name: nginx-proxy build: nginx restart: always ports: - 443:443 - 80:80 volumes: - static_volume:/home/app/web/staticfiles - media_volume:/home/app/web/mediafiles - certs:/etc/nginx/certs - html:/usr/share/nginx/html - vhost:/etc/nginx/vhost.d - /var/run/docker.sock:/tmp/docker.sock:ro depends_on: - web nginx-proxy-letsencrypt: image: jrcs/letsencrypt-nginx-proxy-companion env_file: - ./.env.prod.proxy-companion volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - certs:/etc/nginx/certs - html:/usr/share/nginx/html - vhost:/etc/nginx/vhost.d - acme:/etc/acme.sh depends_on: - nginx-proxy volumes: postgres_data: static_volume: media_volume: certs: html: vhost: acme:`
与docker-compose . staging . yml相比,这里唯一的区别是我们使用了不同的环境文件。
.env.prod :
`DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=<YOUR_DOMAIN.COM>
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
VIRTUAL_HOST=<YOUR_DOMAIN.COM>
VIRTUAL_PORT=8000
LETSENCRYPT_HOST=<YOUR_DOMAIN.COM>`
.env.prod.db :
`POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod`
. env . prod . proxy-companion:
`DEFAULT_EMAIL=youremail@yourdomain.co NGINX_PROXY_CONTAINER=nginx-proxy`
适当地更新它们。
你发现了不同版本的区别了吗?没有设置ACME_CA_URI
环境变量,因为默认情况下letsencrypt-nginx-proxy-companion
映像使用 Let's Encrypt 的生产环境。
使用 SCP 将新文件和文件夹复制到您的实例中:
`$ scp $(pwd)/{.env.prod,.env.prod.db,.env.prod.proxy-companion,docker-compose.prod.yml} [[email protected]](/cdn-cgi/l/email-protection):/path/to/django-on-docker`
像前面一样,通过 SSH 连接到您的实例,并移动到项目目录:
构建图像并旋转容器:
`$ docker-compose -f docker-compose.prod.yml up -d --build`
再次导航到您的域。您应该不会再看到警告。
恭喜你。您现在使用的是生产加密证书。
想要查看证书创建过程的运行情况,请查看日志:
`$ docker-compose -f docker-compose.prod.yml logs nginx-proxy-letsencrypt`
结论
总之,一旦您将 Docker Compose 配置为运行 Django,要设置 HTTPS,您需要在 Docker Compose 文件中添加(并配置)服务nginx-proxy
和nginx-proxy-letsencrypt
。现在,您可以通过配置VIRTUAL_HOST
(路由)和LETSENCRYPT_HOST
(证书)环境变量来添加更多的容器。和往常一样,一定要先用 Let's Encrypt 的登台环境进行测试。
你可以在 django-on-docker-letsencryptrepo 中找到代码。
姜戈码头系列:
使用 Django、Docker 和 CloudWatch 进行集中日志记录
让我们看看如何配置运行在 EC2 实例上的容器化 Django 应用程序,以便将日志发送到 Amazon CloudWatch 。
步骤:
- 创建 IAM 角色
- 将 IAM 角色附加到 EC2 实例
- 创建云观察日志组
- 在 EC2 实例上安装 CloudWatch 日志代理
- 配置 Django 日志记录
- 复合更新坞站
创建 IAM 角色
首先创建一个新的 IAM 角色,并附加CloudWatchAgentServerPolicy策略,供 Docker 守护进程用来写入 CloudWatch。
从 IAM 控制台中,选择“角色”并点击“创建角色”。在“常见用例”下选择“EC2”服务:
单击下一步按钮。
在“权限”页面上,搜索“CloudWatchAgentServerPolicy”策略,并选中复选框以附加它:
单击几次“下一步”。在审查页面上,输入一个角色名称-即CloudWatchAgentRole
-然后创建该角色。
附加 IAM 角色
要将角色附加到 EC2 实例,导航到 EC2 仪表板并选择实例。单击“操作”下拉菜单,选择“实例设置”,然后单击“附加/替换 IAM 角色”:
搜索并选择您刚刚创建的 IAM 角色,然后单击“应用”。
您也可以从命令行附加角色,如下所示:
$ aws ec2 associate-iam-instance-profile \ --instance-id <YOUR_INSTANCE_ID> \ --iam-instance-profile Name=CloudWatchAgentRole
创建云观察日志组
既然 Docker 守护进程已经获得了写入 CloudWatch 的权限,那么让我们创建一个要写入的日志组。在 CloudWatch 控制台内,创建一个新的日志组。向新创建的组中添加一个新的日志流。
安装云观察日志代理
SSH 到 EC2 实例,直接从 S3 下载并安装 CloudWatch 日志代理。
示例:
`# download
$ curl https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
# install
$ sudo dpkg -i -E ./amazon-cloudwatch-agent.deb`
配置 Django 日志记录
因为 Docker 容器向 stdout 和 stderr 输出流发出日志,所以您需要配置 Django 日志记录,通过 StreamHandler 将所有内容记录到 stderr。
例如:
`LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler'
},
},
'loggers': {
'': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}`
复合更新坞站
最后,我们可以在单个容器中使用 Docker 的 awslogs 日志驱动程序。
例如:
`version: "3.8" services: api: build: ./project command: gunicorn core.wsgi:application --bind 0.0.0.0:8000 --log-level=debug logging: driver: "awslogs" options: awslogs-region: "us-east-1" awslogs-group: "your-log-group" awslogs-stream: "your-log-stream"`
不使用 compose?假设您已经构建并标记了图像,像这样旋转容器:
`$ docker run \
--log-driver="awslogs" \
--log-opt awslogs-region="use-east-1" \
--log-opt awslogs-group="your-log-group" \
--log-opt awslogs-stream="your-log-group" \
your-image-tag`
Django 中的低级缓存 API
在之前的文章中,我们查看了 Django 中缓存的概述,并深入研究了如何缓存 Django 视图以及使用不同的缓存后端。本文更深入地研究了 Django 中的低级缓存 API。
--
Django 缓存文章:
目标
完成本文后,您应该能够:
- 将 Redis 设置为 Django 缓存后端
- 使用 Django 的低级缓存 API 来缓存模型
- 使用 Django 数据库信号使缓存无效
- 利用 Django 生命周期简化缓存失效
- 与低级缓存 API 交互
Django 低级高速缓存
Django 中的缓存可以在不同的层次上实现(或者站点的不同部分)。您可以缓存整个站点或不同粒度级别的特定部分(按粒度降序排列):
有关 Django 中不同缓存级别的更多信息,请参考 Django 文章中的缓存。
如果 Django 的每站点或每视图缓存不够精细,无法满足您的应用程序需求,那么您可能希望利用低级缓存 API 来管理对象级的缓存。
如果您需要缓存不同的:
- 对以不同间隔变化的对象进行建模
- 登录用户的数据相互分离
- 计算负载繁重的外部资源
- 外部 API 调用
所以,当你需要更多的粒度和对缓存的控制时,Django 的低级缓存是很好的。它可以存放任何可以安全腌制的物品。要使用低级缓存,你可以使用内置的django.core.cache.caches
,或者,如果你只是想使用在 settings.py 文件中定义的默认缓存,通过django.core.cache.cache
。
项目设置
从 GitHub 上的django-低级缓存 repo 中克隆基础项目:
`$ git clone -b base https://github.com/testdrivenio/django-low-level-cache
$ cd django-low-level-cache`
创建(并激活)虚拟环境,并满足以下要求:
`$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt`
应用 Django 迁移,将一些产品数据加载到数据库中,并启动服务器:
`(venv)$ python manage.py migrate
(venv)$ python manage.py seed_db
(venv)$ python manage.py runserver`
在您的浏览器中导航到 http://127.0.0.1:8000 以检查一切是否按预期工作。
缓存后端
我们将使用 Redis 作为缓存后端。
下载并安装 Redis。
如果你用的是 Mac,我们建议用家酿安装 Redis:
安装完成后,在新的终端窗口中启动 Redis 服务器并确保它运行在默认端口 6379 上。当我们告诉 Django 如何与 Redis 通信时,端口号将非常重要。
对于 Django 使用 Redis 作为缓存后端,需要 django-redis 依赖关系。它已经安装好了,所以你只需要将自定义后端添加到 settings.py 文件中:
`CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}`
现在,当您再次运行服务器时,Redis 将被用作缓存后端:
`(venv)$ python manage.py runserver`
翻到代码。 products/views.py 中的HomePageView
视图简单地列出了数据库中的所有产品:
`class HomePageView(View):
template_name = 'products/home.html'
def get(self, request):
product_objects = Product.objects.all()
context = {
'products': product_objects
}
return render(request, self.template_name, context)`
让我们为产品对象添加对低级缓存 API 的支持。
首先,将导入添加到 products/views.py 的顶部:
`from django.core.cache import cache`
然后,向视图添加用于缓存产品的代码:
`class HomePageView(View):
template_name = 'products/home.html'
def get(self, request):
product_objects = cache.get('product_objects') # NEW
if product_objects is None: # NEW
product_objects = Product.objects.all()
cache.set('product_objects', product_objects) # NEW
context = {
'products': product_objects
}
return render(request, self.template_name, context)`
这里,我们首先检查默认缓存中是否有名为product_objects
的缓存对象:
- 如果是这样,我们只是将它返回给模板,而没有进行数据库查询。
- 如果在我们的缓存中没有找到,我们查询数据库并用键
product_objects
将结果添加到缓存中。
服务器运行时,在浏览器中导航至 http://127.0.0.1:8000 。点击 Django 调试工具栏右侧菜单中的“缓存”。您应该会看到类似如下的内容:
有两个缓存调用:
- 第一个调用试图获取名为
product_objects
的缓存对象,由于该对象不存在,导致缓存未命中。 - 第二个调用使用相同的名称设置缓存对象,结果是所有产品的 queryset。
还有一个 SQL 查询。总的来说,页面加载大约需要 313 毫秒。
在浏览器中刷新页面:
这一次,您应该看到一个缓存命中,它获得名为product_objects
的缓存对象。此外,没有 SQL 查询,页面加载大约需要 234 毫秒。
尝试添加新产品、更新现有产品和删除产品。您将不会在 http://127.0.0.1:8000 看到任何更改,直到您通过按下“Invalidate cache”按钮手动使缓存无效。
使缓存失效
接下来让我们看看如何自动使缓存失效。在之前的文章中,我们研究了如何在一段时间(TTL)后使缓存失效。在本文中,我们将研究如何在模型发生变化时使缓存失效——例如,当一个产品被添加到 products 表中时,或者当一个现有产品被更新或删除时。
使用姜戈信号
对于这个任务,我们可以使用数据库信号:
Django 包括一个“信号调度程序”,当框架中的其他地方发生动作时,它可以帮助解耦的应用程序得到通知。简而言之,信号允许某些发送者通知一组接收者某个动作已经发生。当许多代码可能对相同的事件感兴趣时,它们特别有用。
保存和删除
要设置处理缓存失效的信号,首先更新 products/apps.py ,如下所示:
`from django.apps import AppConfig
class ProductsConfig(AppConfig):
name = 'products'
def ready(self): # NEW
import products.signals # NEW`
接下来,在“产品”目录中创建一个名为 signals.py 的文件:
`from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from .models import Product
@receiver(post_delete, sender=Product, dispatch_uid='post_deleted')
def object_post_delete_handler(sender, **kwargs):
cache.delete('product_objects')
@receiver(post_save, sender=Product, dispatch_uid='posts_updated')
def object_post_save_handler(sender, **kwargs):
cache.delete('product_objects')`
这里,我们使用来自django.dispatch
的receiver
装饰器来装饰两个函数,当添加或删除产品时,这两个函数分别被调用。让我们来看看论点:
- 第一个参数是修饰函数绑定到的信号事件,可以是
save
或delete
。 - 我们还指定了一个发送者,即接收信号的
Product
模型。 - 最后,我们传递了一个字符串作为
dispatch_uid
到防止重复信号。
因此,当针对Product
模型进行保存或删除时,缓存对象上的delete
方法被调用来删除product_objects
缓存的内容。
要查看实际效果,请启动或重启服务器,并在浏览器中导航到 http://127.0.0.1:8000 。打开 Django 调试工具栏中的“缓存”标签。您应该会看到一次缓存未命中。刷新,您应该没有缓存未命中和一个缓存命中。关闭调试工具栏页面。然后,单击“新产品”按钮添加新产品。单击“保存”后,您应该会被重定向回主页。这一次,您应该看到一次缓存未命中,这表明信号起作用了。此外,你的新产品应该出现在产品列表的顶部。
更新
更新一下怎么样?
如果您像这样更新一个项目,就会触发post_save
信号:
`product = Product.objects.get(id=1)
product.title = 'A new title'
product.save()`
但是,如果您通过QuerySet
对模型执行update
,则post_save
不会被触发:
`Product.objects.filter(id=1).update(title='A new title')`
记下ProductUpdateView
:
`class ProductUpdateView(UpdateView):
model = Product
fields = ['title', 'price']
template_name = 'products/product_update.html'
# we overrode the post method for testing purposes
def post(self, request, *args, **kwargs):
self.object = self.get_object()
Product.objects.filter(id=self.object.id).update(
title=request.POST.get('title'),
price=request.POST.get('price')
)
return HttpResponseRedirect(reverse_lazy('home'))`
因此,为了触发post_save
,让我们覆盖 queryset update()
方法。首先创建一个自定义QuerySet
和一个自定义Manager
。在 products/models.py 的顶部,添加以下几行:
`from django.core.cache import cache # NEW
from django.db import models
from django.db.models import QuerySet, Manager # NEW
from django.utils import timezone # NEW`
接下来,让我们将下面的代码添加到 products/models.py 的Product
类的正上方:
`class CustomQuerySet(QuerySet):
def update(self, **kwargs):
cache.delete('product_objects')
super(CustomQuerySet, self).update(updated=timezone.now(), **kwargs)
class CustomManager(Manager):
def get_queryset(self):
return CustomQuerySet(self.model, using=self._db)`
这里,我们创建了一个自定义Manager
,它只有一个任务:返回我们的自定义QuerySet
。在我们的定制QuerySet
中,我们重写了update()
方法,首先删除缓存键,然后执行常规的QuerySet
更新。
为了让我们的代码使用它,您还需要像这样更新Product
:
`class Product(models.Model):
title = models.CharField(max_length=200, blank=False)
price = models.CharField(max_length=20, blank=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
objects = CustomManager() # NEW
class Meta:
ordering = ['-created']`
完整文件:
`from django.core.cache import cache
from django.db import models
from django.db.models import QuerySet, Manager
from django.utils import timezone
class CustomQuerySet(QuerySet):
def update(self, **kwargs):
cache.delete('product_objects')
super(CustomQuerySet, self).update(updated=timezone.now(), **kwargs)
class CustomManager(Manager):
def get_queryset(self):
return CustomQuerySet(self.model, using=self._db)
class Product(models.Model):
title = models.CharField(max_length=200, blank=False)
price = models.CharField(max_length=20, blank=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
objects = CustomManager()
class Meta:
ordering = ['-created']`
测试一下。
使用 Django 生命周期
您可以不使用数据库信号,而是使用一个名为 Django Lifecycle 的第三方包,这有助于使缓存失效更容易、更易读:
这个项目提供了一个@hook 装饰器以及一个基本模型和 mixin 来为 Django 模型添加生命周期挂钩。Django 提供生命周期挂钩的内置方法是 Signals。然而,我的团队经常发现信号引入了不必要的间接性,并且与 Django 的“胖模型”方法不一致。
要切换到使用 Django 生命周期,关闭服务器,然后像这样更新 products/app.py :
`from django.apps import AppConfig
class ProductsConfig(AppConfig):
name = 'products'`
接下来,将 Django 生命周期添加到 requirements.txt :
`Django==3.1.13 django-debug-toolbar==3.2.1 django-lifecycle==0.9.1 # NEW django-redis==5.0.0 redis==3.5.3`
安装新要求:
`(venv)$ pip install -r requirements.txt`
要使用生命周期挂钩,请像这样更新 products/models.py :
`from django.core.cache import cache
from django.db import models
from django.db.models import QuerySet, Manager
from django_lifecycle import LifecycleModel, hook, AFTER_DELETE, AFTER_SAVE # NEW
from django.utils import timezone
class CustomQuerySet(QuerySet):
def update(self, **kwargs):
cache.delete('product_objects')
super(CustomQuerySet, self).update(updated=timezone.now(), **kwargs)
class CustomManager(Manager):
def get_queryset(self):
return CustomQuerySet(self.model, using=self._db)
class Product(LifecycleModel): # NEW
title = models.CharField(max_length=200, blank=False)
price = models.CharField(max_length=20, blank=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
objects = CustomManager()
class Meta:
ordering = ['-created']
@hook(AFTER_SAVE) # NEW
@hook(AFTER_DELETE) # NEW
def invalidate_cache(self): # NEW
cache.delete('product_objects') # NEW`
在上面的代码中,我们:
- 首先从 Django 生命周期中导入必要的对象
- 然后继承自
LifecycleModel
而不是django.db.models
- 创建了一个删除
product_object
缓存键的invalidate_cache
方法 - 使用了
@hook
decorator 来指定我们想要“挂钩”的事件
通过以下方式在您的浏览器中进行测试-
- 导航到 http://127.0.0.1:8000
- 在调试工具栏中刷新并验证是否有缓存命中
- 添加产品并验证现在是否存在缓存缺失
与django signals
一样,如果我们像前面提到的例子那样通过QuerySet
更新,钩子不会触发:
`Product.objects.filter(id=1).update(title="A new title")`
在这种情况下,我们仍然需要创建一个自定义的Manager
和QuerySet
,就像我们之前展示的那样。
测试编辑和删除产品。
低级高速缓存 API 方法
到目前为止,我们已经使用了cache.get
、cache.set
和cache.delete
方法来获取、设置和删除(无效)缓存中的对象。让我们从django.core.cache.cache
中来看看一些更多的方法。
cache.get_or_set
获取指定的键(如果存在)。如果它不存在,它设置关键点。
语法
cache.get_or_set(key, default, timeout=DEFAULT_TIMEOUT, version=None)
timeout
参数用于设置缓存的有效时间(以秒为单位)。将其设置为None
将永久缓存该值。省略它将使用在CACHES
设置中的setting.py
中设置的超时(如果有的话)
许多缓存方法还包括一个version
参数。使用此参数,您可以设置或访问同一缓存键的不同版本。
例子
`>>> from django.core.cache import cache
>>> cache.get_or_set('my_key', 'my new value')
'my new value'`
我们可以在视图中使用它,而不是使用 if 语句:
`# current implementation
product_objects = cache.get('product_objects')
if product_objects is None:
product_objects = Product.objects.all()
cache.set('product_objects', product_objects)
# with get_or_set
product_objects = cache.get_or_set('product_objects', product_objects)`
cache.set_many
用于通过传递一个键值对字典来一次设置多个键。
语法
cache.set_many(dict, timeout)
例子
`>>> cache.set_many({'my_first_key': 1, 'my_second_key': 2, 'my_third_key': 3})`
cache.get_many
用于一次获取多个缓存对象。它返回一个字典,其中的键被指定为方法的参数,只要它们存在并且没有过期。
语法
cache.get_many(keys, version=None)
例子
`>>> cache.get_many(['my_key', 'my_first_key', 'my_second_key', 'my_third_key'])
OrderedDict([('my_key', 'my new value'), ('my_first_key', 1), ('my_second_key', 2), ('my_third_key', 3)])`
cache.touch
如果您想要更新某个密钥的到期时间,可以使用此方法。超时值以秒为单位在超时参数中设置。
语法
cache.touch(key, timeout=DEFAULT_TIMEOUT, version=None)
例子
`>>> cache.set('sample', 'just a sample', timeout=120)
>>> cache.touch('sample', timeout=180)`
cache.incr 和 cache.decr
这两种方法可以用来增加或减少一个已经存在的键值。如果在不存在的缓存键上使用这些方法,它将返回一个ValueError
。
在未指定 delta 参数的情况下,该值将增加/减少 1。
语法
`cache.incr(key, delta=1, version=None)
cache.decr(key, delta=1, version=None)`
例子
`>>> cache.set('my_first_key', 1)
>>> cache.incr('my_first_key')
2
>>>
>>> cache.incr('my_first_key', 10)
12`
cache.close()
要关闭到缓存的连接,可以使用close()
方法。
语法
cache.close()
例子
cache.close()
cache.clear
要一次删除缓存中的所有键,可以使用这个方法。请记住,这将从缓存中删除所有的东西,而不仅仅是你的应用程序设置的键。
语法
cache.clear()
例子
cache.clear()
结论
在本文中,我们研究了 Django 中的低级缓存 API。我们扩展了一个演示项目以使用低级缓存,并且使用 Django 的数据库信号和 Django 生命周期钩子第三方包使缓存无效。
我们还提供了 Django 低级缓存 API 中所有可用方法的概述,以及如何使用它们的示例。
你可以在django-低级缓存 repo 中找到最终代码。
--
Django 缓存文章:
整合 Mailchimp 和 Django
在本文中,我们将了解如何将 Mailchimp 与 Django 集成,以便处理新闻订阅和发送事务性电子邮件。
目标
在本文结束时,您将能够:
- 解释 Mailchimp 的营销和交易电子邮件 API 之间的区别
- 将 Mailchimp 的营销 API 与 Django 集成
- 管理 Mailchimp 受众和联系人
- 设置并使用合并字段发送个性化活动
- 使用 Mailchimp 的事务性电子邮件 API 发送事务性电子邮件
Mailchimp 是什么?
Mailchimp 是一个营销自动化平台,允许您创建、发送和分析电子邮件和广告活动。此外,它还允许您管理联系人、创建自定义电子邮件模板和生成报告。这是企业最常用的电子邮件营销解决方案之一。
Mailchimp 为开发人员提供了以下 API:
营销 API 与交易电子邮件 API
营销和交易电子邮件 API 都可以用于发送电子邮件...那么有什么区别呢?
营销 API 用于发送批量电子邮件,通常用于营销目的。其用途包括时事通讯、产品促销和欢迎系列。
另一方面,事务性电子邮件 API 用于在电子邮件触发操作后向单个收件人发送电子邮件。其用途包括帐户创建电子邮件、订单通知和密码重置电子邮件。
如需详细解释,请查看官方文档。
在本文的第一部分,我们将使用营销 API 来创建一个时事通讯。之后,我们将演示如何通过事务性电子邮件 API 发送电子邮件。
项目设置
创建一个新的项目目录以及一个名为djangomailchimp
的新 Django 项目:
`$ mkdir django-mailchimp && cd django-mailchimp
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install django==4.1
(env)$ django-admin startproject djangomailchimp .`
你可以随意把 virtualenv 和 Pip 换成诗歌或 Pipenv 。更多信息,请查看现代 Python 环境。
接下来,迁移数据库:
`(env)$ python manage.py migrate`
这就是基本的设置。
Mailchimp 营销 API
在本节中,我们将使用营销 API 来创建一份时事通讯。
有了一个免费的 Mailchimp 账户,你可以拥有多达 2000 个联系人,每月发送多达 10000 封电子邮件(每天最多 2000 封)。
设置
出于组织目的,让我们创建一个名为marketing
的新 Django 应用程序:
`(env)$ python manage.py startapp marketing`
将 app 添加到 settings.py 中的INSTALLED_APPS
配置中:
`# djangomailchimp/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'marketing.apps.MarketingConfig', # new
]`
接下来,用marketing
应用程序更新项目级的 urls.py :
`# djangomailchimp/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('marketing/', include('marketing.urls')), # new
]`
在marketing
应用程序中创建一个 urls.py 文件并填充它:
`# marketing/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.subscribe_view, name='subscribe'),
path('success/', views.subscribe_success_view, name='subscribe-success'),
path('fail/', views.subscribe_fail_view, name='subscribe-fail'),
path('unsubscribe/', views.unsubscribe_view, name='unsubscribe'),
path('unsubscribe/success/', views.unsubscribe_success_view, name='unsubscribe-success'),
path('unsubscribe/fail/', views.unsubscribe_fail_view, name='unsubscribe-fail'),
]`
记下网址。它们应该是不言自明的:
- 索引将显示订阅表单。
- 如果用户成功订阅,他们将被重定向到
/success
,否则将被重定向到/fail
。 - 取消订阅网址的工作方式相同。
在处理视图之前,让我们创建一个名为 forms.py 的新文件:
`# marketing/forms.py
from django import forms
class EmailForm(forms.Form):
email = forms.EmailField(label='Email', max_length=128)`
我们创建了一个简单的EmailForm
,用于在用户订阅时事通讯时收集联系数据。
随意添加您想要收集的可选信息,如
first_name
、last_name
、address
、phone_number
等。
现在将以下内容添加到 views.py 中:
`# marketing/views.py
from django.http import JsonResponse
from django.shortcuts import render, redirect
from djangomailchimp import settings
from marketing.forms import EmailForm
def subscribe_view(request):
if request.method == 'POST':
form = EmailForm(request.POST)
if form.is_valid():
form_email = form.cleaned_data['email']
# TODO: use Mailchimp API to subscribe
return redirect('subscribe-success')
return render(request, 'subscribe.html', {
'form': EmailForm(),
})
def subscribe_success_view(request):
return render(request, 'message.html', {
'title': 'Successfully subscribed',
'message': 'Yay, you have been successfully subscribed to our mailing list.',
})
def subscribe_fail_view(request):
return render(request, 'message.html', {
'title': 'Failed to subscribe',
'message': 'Oops, something went wrong.',
})
def unsubscribe_view(request):
if request.method == 'POST':
form = EmailForm(request.POST)
if form.is_valid():
form_email = form.cleaned_data['email']
# TODO: use Mailchimp API to unsubscribe
return redirect('unsubscribe-success')
return render(request, 'unsubscribe.html', {
'form': EmailForm(),
})
def unsubscribe_success_view(request):
return render(request, 'message.html', {
'title': 'Successfully unsubscribed',
'message': 'You have been successfully unsubscribed from our mailing list.',
})
def unsubscribe_fail_view(request):
return render(request, 'message.html', {
'title': 'Failed to unsubscribe',
'message': 'Oops, something went wrong.',
})`
接下来,让我们为这些视图提供 HTML 模板。在根目录下创建一个“模板”文件夹。然后添加以下文件...
subscribe.html:
`<!-- templates/subscribe.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Mailchimp</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<h1>Subscribe</h1>
<p>Enter your email address to subscribe to our mailing list.</p>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Subscribe</button>
</form>
<p class="mt-2"><a href="{% url "unsubscribe" %}">Unsubscribe form</a></p>
</div>
</body>
</html>`
unsubscribe.html:
`<!-- templates/unsubscribe.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Mailchimp</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<h1>Unsubscribe</h1>
<p>Enter your email address to unsubscribe from our mailing list.</p>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-danger">Unsubscribe</button>
</form>
<p class="mt-2"><a href="{% url "subscribe" %}">Subscribe form</a></p>
</div>
</body>
</html>`
message.html:
`<!-- templates/message.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Mailchimp</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<h1>{{ title }}</h1>
<p>{{ message }}</p>
</div>
</body>
</html>`
确保更新 settings.py 文件,以便 Django 知道要查找“模板”文件夹:
`# djangomailchimp/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates'], # new
...`
最后,运行runserver
命令启动 Django 的本地 web 服务器:
`(env)$ python manage.py runserver`
在浏览器中导航至http://localhost:8000/marketing/。您应该会看到订阅表单:
通过点击“退订表单”锚,您将被重定向到退订表单。
太好了!我们完成了姜戈的设置。
添加 Mailchimp 营销客户端
首先,安装 mailchimp-marketing 包,这是 Mailchimp Marketing API 的官方客户端库:
`(env)$ pip install mailchimp-marketing==3.0.75`
接下来,我们需要创建/获取一个 API 密钥。
如果你还没有 Mailchimp 账户,那就去注册。
登录 Mailchimp 帐户后,通过点击链接或点击您的帐户(左下角)>“帐户&计费”,导航至帐户 API 键:
然后单击“附加功能”>“API 密钥”:
最后,单击“创建密钥”以生成 API 密钥:
获得 API 密钥后,复制它。
要使用营销 API,我们还需要了解我们所在的地区。最简单的方法是查看 Mailchimp URL 的开头。
例如:
`https://us8.admin.mailchimp.com/
^^^`
在我这里,地区是:us8
。
将 Mailchimp API 密钥和区域存储在 settings.py 的底部,如下所示:
`# djangomailchimp/settings.py
MAILCHIMP_API_KEY = '<your mailchimp api key>'
MAILCHIMP_REGION = '<your mailchimp region>'`
接下来,我们将在 marketing/views.py 中初始化营销 API 客户端,并创建一个 ping API 的端点:
`# marketing/views.py
mailchimp = Client()
mailchimp.set_config({
'api_key': settings.MAILCHIMP_API_KEY,
'server': settings.MAILCHIMP_REGION,
})
def mailchimp_ping_view(request):
response = mailchimp.ping.get()
return JsonResponse(response)`
不要忘记在文件顶部导入Client
:
`from mailchimp_marketing import Client`
在 marketing/urls.py 中注册新创建的端点:
`# marketing/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('ping/', views.mailchimp_ping_view), # new
path('', views.subscribe_view, name='subscribe'),
path('success/', views.subscribe_success_view, name='subscribe-success'),
path('fail/', views.subscribe_fail_view, name='subscribe-fail'),
path('unsubscribe/', views.unsubscribe_view, name='unsubscribe'),
path('unsubscribe/success/', views.unsubscribe_success_view, name='unsubscribe-success'),
path('unsubscribe/fail/', views.unsubscribe_fail_view, name='unsubscribe-fail'),
]`
再次运行服务器,访问http://localhost:8000/marketing/ping/看看能否 ping 通 API。
`{ "health_status": "Everything's Chimpy!" }`
如果你看到上面的消息,那么一切正常。
创造观众
要创建简讯,我们需要使用受众。受众(或列表)是您可以向其发送活动电子邮件的联系人列表。
Mailchimp 的免费计划只允许你有一个(默认)受众。如果您有付费计划,请随意创建专门针对此演示新闻稿的受众。你可以通过网络界面或编程来创建一个。
导航至您的 Mailchimp 仪表盘。在“受众”下,点击“所有联系人”。然后点击【设置】>【受众名称及默认】:
在屏幕右侧,复制“观众 ID”:
粘贴在 settings.py 的末尾,API 键和区域下:
`# djangomailchimp/settings.py
MAILCHIMP_API_KEY = '<your mailchimp api key>'
MAILCHIMP_REGION = '<your mailchimp region>'
MAILCHIMP_MARKETING_AUDIENCE_ID = '<your mailchimp audience id>' # new`
在下一部分中,我们将向受众添加用户。
订阅视图
转到 marketing/views.py ,用以下代码替换subscribe_view
:
`# marketing/views.py
def subscribe_view(request):
if request.method == 'POST':
form = EmailForm(request.POST)
if form.is_valid():
try:
form_email = form.cleaned_data['email']
member_info = {
'email_address': form_email,
'status': 'subscribed',
}
response = mailchimp.lists.add_list_member(
settings.MAILCHIMP_MARKETING_AUDIENCE_ID,
member_info,
)
logger.info(f'API call successful: {response}')
return redirect('subscribe-success')
except ApiClientError as error:
logger.error(f'An exception occurred: {error.text}')
return redirect('subscribe-fail')
return render(request, 'subscribe.html', {
'form': EmailForm(),
})`
不要忘记像这样导入ApiClientError
:
`from mailchimp_marketing.api_client import ApiClientError`
另外,添加记录器:
`import logging
logger = logging.getLogger(__name__)`
因此,我们首先创建了一个字典,其中包含了我们想要存储的所有用户信息。字典需要包含email
和status
。我们可以使用多种状态类型,但最重要的是:
subscribed
-立即添加联系人pending
-用户将在被添加为联系人之前收到一封验证电子邮件
如果我们想要附加额外的用户信息,我们必须使用合并字段。合并字段之后,我们可以发送个性化的电子邮件。它们由一个name
和一个type
组成(如text
、number
、address
)。
默认的合并字段有:ADDRESS
、BIRTHDAY
、FNAME
、LNAME
、PHONE
。
如果您想使用自定义合并字段,您可以使用 Mailchimp 仪表板或通过 Marketing API 添加它们。
让我们将用户的名字和姓氏硬连接到member_info
:
`member_info = {
'email_address': form_email,
'status': 'subscribed',
'merge_fields': {
'FNAME': 'Elliot',
'LNAME': 'Alderson',
}
}`
发送活动时,您可以通过占位符访问合并字段,例如,*|FNAME|*
将替换为Elliot
。
如果您在第一步中向
EmailForm
添加了额外的字段,请随意将它们添加为合并字段,如示例所示。关于合并字段的更多信息可以在正式文档中找到。
我们已经完成了订阅视图。我们来测试一下。
运行服务器并导航到http://localhost:8000/marketing/。然后,使用订阅表单进行订阅。您应该被重定向到success/
:
接下来,打开你的 Mailchimp 仪表盘,导航至“观众”>“所有联系人”。您应该可以看到您的第一个简讯订阅者:
取消订阅视图
要启用取消订阅功能,请用以下代码替换unsubscribe_view
:
`# marketing/views.py
def unsubscribe_view(request):
if request.method == 'POST':
form = EmailForm(request.POST)
if form.is_valid():
try:
form_email = form.cleaned_data['email']
form_email_hash = hashlib.md5(form_email.encode('utf-8').lower()).hexdigest()
member_update = {
'status': 'unsubscribed',
}
response = mailchimp.lists.update_list_member(
settings.MAILCHIMP_MARKETING_AUDIENCE_ID,
form_email_hash,
member_update,
)
logger.info(f'API call successful: {response}')
return redirect('unsubscribe-success')
except ApiClientError as error:
logger.error(f'An exception occurred: {error.text}')
return redirect('unsubscribe-fail')
return render(request, 'unsubscribe.html', {
'form': EmailForm(),
})`
在此,我们:
- 通过表单获取用户的电子邮件。
- 使用
md5
散列用户的电子邮件并生成订户散列。哈希允许我们操作用户数据。 - 用我们想要更改的所有数据创建了一个名为
member_update
的字典。在我们的例子中,我们只是改变了状态。 - 将所有这些数据传递给
lists.update_list_member()
——瞧!
您可以使用相同的方法来更改其他用户数据,如合并字段。
添加导入:
让我们测试一下是否一切正常。
再次运行服务器,导航到http://localhost:8000/marketing/unsubscribe/并使用表单取消订阅。打开“所有联系人”,您会注意到状态从“已订阅”变为“未订阅”:
获取订阅信息
以下是如何获取用户订阅状态的代码示例:
`member_email = '[[email protected]](/cdn-cgi/l/email-protection)'
member_email_hash = hashlib.md5(member_email.encode('utf-8').lower()).hexdigest()
try:
response = mailchimp.lists.get_list_member(
settings.MAILCHIMP_MARKETING_AUDIENCE_ID,
member_email_hash
)
print(f'API call successful: {response}')
except ApiClientError as error:
print(f'An exception occurred: {error.text}')`
正如我们在上一节中看到的,每当我们想要获取/修改某个用户时,我们需要散列他们的电子邮件并将其提供给 API。
响应看起来会像这样:
`{ "id": "f4ce663018fefacfe5c327869be7485d", "email_address": "[[email protected]](/cdn-cgi/l/email-protection)", "unique_email_id": "ec7bdadf19", "contact_id": "67851f34b33195292b2977590007e965", "full_name": "Elliot Alderson", "web_id": 585506089, "email_type": "html", "status": "unsubscribed", "unsubscribe_reason": "N/A (Unsubscribed by admin)", "consents_to_one_to_one_messaging": true, "merge_fields": { "FNAME": "Elliot", "LNAME": "Alderson", "ADDRESS": "", "PHONE": "", "BIRTHDAY": "" }, ... }`
--
我们的时事通讯差不多完成了。我们创建了一个受众,并启用了订阅和取消订阅功能。现在,剩下唯一要做的就是获得一些实际用户,并开始发送竞选电子邮件!
Mailchimp 事务 API
在本节中,我们将演示如何使用 Mailchimp 事务性电子邮件 API 来发送事务性电子邮件。
有了一个免费的 Mailchimp 交易账户 / Mandrill 账户,你可以发送多达 500 封测试邮件。不过,测试邮件只能发送到经过验证的域名。
要使用 Mailchimp 事务性电子邮件 API,您需要拥有一个域名,并有权访问其高级 DNS 设置。
设置
出于组织目的,让我们创建另一个名为transactional
的 Django 应用程序:
`(env)$ python manage.py startapp transactional`
将 app 添加到 settings.py 中的INSTALLED_APPS
配置中:
`# djangomailchimp/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'marketing.apps.MarketingConfig',
'transactional.apps.TransactionalConfig', # new
]`
用transactional
app 更新项目级 urls.py :
`# djangomailchimp/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('marketing/', include('marketing.urls')),
path('transactional/', include('transactional.urls')), # new
]`
现在,在transactional
应用程序中创建一个 urls.py 文件:
`# transactional/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('send/', views.send_view, name='mailchimp-send'),
]`
接下来,在 transactional/views.py 中创建一个send_view
:
`# transactional/views.py
from django.http import JsonResponse
def send_view(request):
return JsonResponse({
'detail': 'This view is going to send an email.',
})`
运行服务器并导航到http://localhost:8000/transactional/send/。您应该会得到这样的回应:
`{ "detail": "This view is going to send an email." }`
添加交易电子邮件客户端
接下来,让我们安装 mailchimp-transactional 包:
`pip install mailchimp-transactional==1.0.47`
这个包使得向事务性电子邮件 API 发送请求变得很容易。
如果你还没有 Mailchimp 账户,那就去注册。
我们现在需要创建一个新的事务 API 键。
登录您的 Mailchimp 帐户,进入“自动操作”>“交易电子邮件”,然后按“启动”(在窗口的右上角):
在那之后,你将被重定向到 Mandrill,在那里你将被询问你是否想用你的帐户登录。按“使用 Mailchimp 登录”。
然后导航至“设置”并点击“添加 API 密钥”:
生成密钥后,复制它:
将生成的密钥存储在 settings.py 的底部,如下所示:
`# djangomailchimp/settings.py
MAILCHIMP_MARKETING_API_KEY = '<your mailchimp marketing api key>'
MAILCHIMP_MARKETING_REGION = '<your mailchimp marketing region>'
MAILCHIMP_MARKETING_AUDIENCE_ID = '<your mailchimp audience id>'
MAILCHIMP_TRANSACTIONAL_API_KEY = '<your mailchimp transactional api key>' # new`
接下来,回到 Mandrill 设置,点击“域”,然后“发送域”。
添加您的域名,并通过 TXT 记录或电子邮件验证所有权。验证域所有权后,还应该为 DKIM 和 SPF 设置添加 TXT 记录。接下来,点击“测试 DNS 设置”,看看是否一切正常:
应该只有绿色的扁虱。
接下来,让我们在 transactional/views.py 中初始化 Mailchimp 事务电子邮件客户端,并创建一个端点来测试我们是否可以成功 ping 通 API:
`import mailchimp_transactional
from django.http import JsonResponse
from mailchimp_transactional.api_client import ApiClientError
from djangomailchimp import settings
mailchimp = mailchimp_transactional.Client(
api_key=settings.MAILCHIMP_TRANSACTIONAL_API_KEY,
)
def mailchimp_transactional_ping_view(request):
try:
mailchimp.users.ping()
return JsonResponse({
'detail': 'Everything is working fine',
})
except ApiClientError as error:
return JsonResponse({
'detail': 'Something went wrong',
'error': error.text,
})`
确保不要忘记任何进口。
在 urls.py 中注册新创建的 URL:
`# transactional/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('ping/', views.mailchimp_transactional_ping_view), # new
path('send/', views.send_view, name='mailchimp-send'),
]`
再次运行服务器,访问http://localhost:8000/transactional/ping/查看 ping 是否通过。
`{ "detail": "Everything is working fine" }`
如果你看到上面的消息,那么一切正常。
发送交易电子邮件
用以下代码替换我们的send_view
:
`def send_view(request):
message = {
'from_email': '<YOUR_SENDER_EMAIL>',
'subject': 'My First Email',
'text': 'Hey there, this email has been sent via Mailchimp Transactional API.',
'to': [
{
'email': '<YOUR_RECIPIENT_EMAIL>',
'type': 'to'
},
]
}
try:
response = mailchimp.messages.send({
'message': message,
})
return JsonResponse({
'detail': 'Email has been sent',
'response': response,
})
except ApiClientError as error:
return JsonResponse({
'detail': 'Something went wrong',
'error': error.text,
})`
确保用您的实际电子邮件地址替换
<YOUR_SENDER_EMAIL>
和<YOUR_RECIPIENT_EMAIL>
。也可以随意改变主题和/或文字。
在此,我们:
- 创建了一个包含所有电子邮件信息的字典。
- 将新创建的字典传递给
mailchimp.messages.send
。
你应该一直把它包在袋子里,以防出错。
如果你使用的是免费的 Mailchimp 计划,而你的电子邮件被拒绝
'reject_reason': 'recipient-domain-mismatch'
,你很可能试图发送一封电子邮件到一个未经验证的域名。如果您只验证了一个域,您只能向该域发送电子邮件。
运行服务器,访问http://localhost:8000/transactional/send/。如果一切顺利,您应该会看到以下响应:
`{ "detail": "Email has been sent" }`
接下来,检查你的收件箱,邮件应该在那里。
这只是一个如何发送交易邮件的演示。在现实世界的应用程序中,您可以使用该代码发送电子邮件进行密码重置和电子邮件验证等。
结论
在本文中,我们研究了如何利用 Mailchimp 的营销和交易电子邮件 API。我们创建了一个简单的时事通讯,并学习了如何发送交易电子邮件。现在,您应该对 Mailchimp APIs 的工作原理以及如何在您的应用程序中实现其他 API 端点有了相当好的理解。
Django 中的分页
分页是将大量数据分割成多个独立网页的过程。您可以定义希望每页显示的单个记录的数量,然后发送回与用户请求的页面相对应的数据,而不是将所有数据都转储给用户。
使用这种技术的优点是它改善了用户体验,尤其是当有数千条记录需要检索时。在 Django 中实现分页相当容易,因为 Django 提供了一个 Paginator 类,您可以使用它将内容分组到不同的页面上。
根据开发人员的配置方式,分页可以有不同的风格。也就是说,在本文中,我们将研究如何使用三种不同的 UI 风格将分页与基于函数和基于类的视图结合起来。
示例项目可以在 GitHub 上的django-pagination-examplerepo 中找到。
目标
完成本文后,您将能够:
- 解释什么是分页,以及为什么你可能想使用它。
- 使用 Django 的
Paginator
类和Page
对象。 - 用函数和基于类的视图在 Django 中实现分页。
Django 建筑
在 Django 中实现分页时,您将使用以下结构,而不是重新发明分页所需的逻辑:
让我们看一些简单的例子。
分页器
`from django.contrib.auth.models import User
for num in range(43):
User.objects.create(username=f"{num}")`
这里,我们创建了 43 个用户对象。
接下来,我们将导入Paginator
类并创建一个新实例:
`from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
print(paginator.num_pages) # => 5`
Paginator
类有四个参数:
object_list
-任何具有count()
或__len__()
方法的对象,如列表、元组或查询集per_page
-一页中包含的最大项目数orphans
(可选)-用于防止最后一页项目很少,默认为0
allow_empty_first_page
(可选)——顾名思义,如果您通过将参数设置为False
,默认为True
,不允许首页为空,那么您可以引发一个EmtpyPage
错误
因此,在上面的例子中,我们将用户分成十个页面(或块)。前四页将有十个用户,而最后一页将有三个用户。
Paginator
类具有以下属性:
count
-物体总数num_pages
-总页数page_range
-页码范围迭代器
为了获得一致的分页结果,应该对查询集或模型进行排序。
如果您不希望在最后一个页面上只有三个用户,您可以像这样使用孤儿参数来将最后三个用户添加到前一个页面:
`from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10, orphans=3)
print(paginator.num_pages) # => 4`
因此,当最后一页剩余对象的数量小于或等于orphans
的值时,这些对象将被添加到前一页。
页
在 Django 查询集被分解成Page
个对象之后。然后我们可以使用page()
方法通过传递页码来访问每个页面的数据:
`from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
page_obj = paginator.page(1)
print(page_obj) # => <Page 1 of 5>`
这里,page_obj
给了我们一个 page 对象,表示结果的第一页。这可以在你的模板中使用。
注意,我们并没有真的创建一个
Page
实例。相反,我们从 Paginator 类获得了实例。
如果页面不存在会怎么样?
`from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
page_obj = paginator.page(99)`
您应该看到:
`raise EmptyPage(_('That page contains no results'))
django.core.paginator.EmptyPage: That page contains no results`
因此,像这样捕捉一个EmptyPage
异常是一个好主意:
`from django.contrib.auth.models import User
from django.core.paginator import EmptyPage, Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
try:
page_obj = paginator.page(99)
except EmptyPage:
# Do something
pass`
您可能还想捕捉一个PageNotAnInteger
异常。
有关这方面的更多信息,请查看分页器文档中的异常部分。
也就是说,如果您不想显式处理EmptyPage
或PageNotAnInteger
异常,您可以使用 get_page() 方法来代替page()
:
`from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
page_obj = paginator.get_page(99)
print(page_obj) # => <Page 5 of 5>`
所以,即使数字99
超出范围,也将返回最后一页。
同样,如果页面不是一个有效的数字,默认情况下get_page()
将返回第一页:
`from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
page_obj = paginator.get_page('foo')
print(page_obj) # => <Page 1 of 5>`
因此,page()
或get_page()
这两种方法都可以根据您的喜好使用。本文中展示的例子将使用page()
。
number
-显示给定页面的页码paginator
-显示相关的Paginator
对象has_next()
-如果有下一页,返回True
has_previous()
- -如果有上一页,返回True
next_page_number()
-返回下一页的页码previous_page_number()
-返回上一页的页码
基于功能的视图
接下来,让我们看看如何在基于函数的视图中使用分页:
`from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render
from . models import Employee
def index(request):
object_list = Employee.objects.all()
page_num = request.GET.get('page', 1)
paginator = Paginator(object_list, 6) # 6 employees per page
try:
page_obj = paginator.page(page_num)
except PageNotAnInteger:
# if page is not an integer, deliver the first page
page_obj = paginator.page(1)
except EmptyPage:
# if the page is out of range, deliver the last page
page_obj = paginator.page(paginator.num_pages)
return render(request, 'index.html', {'page_obj': page_obj})`
在此,我们:
- 从 URL 定义了一个
page_num
变量。 - 实例化了
Paginator
类,向其传递所需的参数、employees
QuerySet 和每页包含的雇员数。 - 生成了一个名为
page_obj
的页面对象,它包含分页的员工数据以及用于导航到上一页和下一页的元数据。
https://github . com/testdrivenio/django-pagination-example/blob/main/employees/views . py
基于类的视图
在基于类的视图中实现分页的示例:
`from django.views.generic import ListView
from . models import Employee
class Index(ListView):
model = Employee
context_object_name = 'employees'
paginate_by = 6
template_name = 'index.html'`
https://github . com/testdrivenio/django-pagination-example/blob/main/employees/views . py
模板
在模板中使用分页开始变得有趣起来,因为有几种不同的实现。在本文中,我们将研究三种不同的实现,每一种都展示了导航到上一页和下一页的不同方式。
你可以在 GitHub 的django-pagination-examplerepo 上的 templates 文件夹中找到每个例子的代码。
风味 1
这是实现分页 UI 的第一种风格。
因此,在本例中,我们有“上一页”和“下一页”链接,最终用户可以单击这些链接在页面之间移动。
index.html:
`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css">
<title>Pagination in Django</title>
</head>
<body>
<div class="container">
<h1 class="text-center">List of Employees</h1>
<hr>
<ul class="list-group list-group-flush">
{% for employee in page_obj %}
<li class="list-group-item">{{ employee }}</li>
{% endfor %}
</ul>
<br><hr>
{% include "pagination.html" %}
</div>
</body>
</html>`
pagination.html:
`<div>
<span>
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
<span>
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
</span>
</div>`
请记住,pagination.html模板可以跨许多模板重用。
风味 2
pagination.html:
`{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% else %}
<a>Previous</a>
{% endif %}
{% for i in page_obj.paginator.page_range %}
{% if page_obj.number == i %}
<a href="#">{{ i }} </a>
{% else %}
<a href="?page={{ i }}">{{ i }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
{% else %}
<a>Next</a>
{% endif %}`
这种风格在 UI 中显示所有的页码,使得导航到不同的页面更加容易。
风味 3
pagination.html:
`{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">« Previous page</a>
{% if page_obj.number > 3 %}
<a href="?page=1">1</a>
{% if page_obj.number > 4 %}
<span>...</span>
{% endif %}
{% endif %}
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<a href="?page={{ num }}">{{ num }}</a>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<a href="?page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
{% if page_obj.number < page_obj.paginator.num_pages|add:'-3' %}
<span>...</span>
<a href="?page={{ page_obj.paginator.num_pages }}">{{ page_obj.paginator.num_pages }}</a>
{% elif page_obj.number < page_obj.paginator.num_pages|add:'-2' %}
<a href="?page={{ page_obj.paginator.num_pages }}">{{ page_obj.paginator.num_pages }}</a>
{% endif %}
<a href="?page={{ page_obj.next_page_number }}">Next Page »</a>
{% endif %}`
如果你有大量的页面,你可能想看看这第三种也是最后一种风格。
结论
关于在 Django 中实现分页的文章到此结束。以下是需要记住的要点:
- 在 Django 中实现分页非常容易,因为有现成的
Paginator
和Page
助手类。 - 一旦创建了视图,您只需传回带有分页数据的 page 对象,以便在模板中使用。
Django 中的自动化性能测试
低效的数据库查询是 Django 最常见的性能缺陷之一。尤其是 N+1 查询会在早期对应用程序的性能产生负面影响。当您使用针对每个记录的单独查询从关联表中选择记录,而不是在单个查询中获取所有记录时,就会出现这种情况。不幸的是,这种低效很容易被 Django ORM 引入。也就是说,它们是可以通过自动化测试快速发现和预防的。
这篇文章着眼于如何:
- 测试一个请求执行的查询数量以及查询的持续时间
- 使用 nplusone 包防止 N+1 次查询
N+1 个查询
我们将在这篇文章中使用的示例应用程序可以在 GitHub 上找到。
比方说,您正在使用一个 Django 应用程序,它有以下模型:
`# courses/models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Course(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
def __str__(self):
return self.title`
现在,如果您的任务是创建一个新视图,用于返回所有课程的 JSON 响应,包括标题和作者姓名,您可以编写以下代码:
`# courses/views.py
from django.http import JsonResponse
from courses.models import Course
def all_courses(request):
queryset = Course.objects.all()
courses = []
for course in queryset:
courses.append(
{"title": course.title, "author": course.author.name}
)
return JsonResponse(courses, safe=False)`
这段代码可以工作,但是效率非常低,因为它会进行太多的数据库查询:
- 1 获取所有课程的查询
- 在每次迭代中获取分支的 n 次查询
在解决这个问题之前,让我们看看进行了多少次查询,并测量执行时间。
度量中间件
您会注意到该项目包含了定制的中间件,它计算并记录每个请求的执行时间:
`# core/middleware.py
import logging
import time
from django.db import connection, reset_queries
def metric_middleware(get_response):
def middleware(request):
reset_queries()
# Get beginning stats
start_queries = len(connection.queries)
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
end_queries = len(connection.queries)
# Calculate stats
total_time = end_time - start_time
total_queries = end_queries - start_queries
# Log the results
logger = logging.getLogger("debug")
logger.debug(f"Request: {request.method} {request.path}")
logger.debug(f"Number of Queries: {total_queries}")
logger.debug(f"Total time: {(total_time):.2f}s")
return response
return middleware`
运行数据库种子命令,向数据库添加 10 名作者和 100 门课程:
`$ python manage.py seed_db`
Django 开发服务器启动并运行后,在浏览器中导航到http://localhost:8000/courses/。您应该会看到 JSON 响应。回到您的终端,记下指标:
`Request: GET /courses/
Number of Queries: 101
Total time: 0.10s`
这是一个很大的疑问!这是非常低效的。每增加一个作者和课程都需要一个额外的数据库查询,所以随着数据库的增长,性能会继续下降。幸运的是,解决这个问题非常简单:您可以添加一个select_related
方法来创建一个 SQL join,它将在初始数据库查询中包含作者。
`queryset = Course.objects.select_related("author").all()`
在进行任何代码更改之前,让我们先从一些测试开始。
性能测试
从下面的测试开始,它使用 django _ assert _ num _ queriespy test fixture 来确保当数据库中存在一个或多个作者和课程记录时,数据库只被命中一次:
`import json
import pytest
from faker import Faker
from django.test import override_settings
from courses.models import Course, Author
@pytest.mark.django_db
def test_number_of_sql_queries_all_courses(client, django_assert_num_queries):
fake = Faker()
author_name = fake.name()
author = Author(name=author_name)
author.save()
course_title = fake.sentence(nb_words=4)
course = Course(title=course_title, author=author)
course.save()
with django_assert_num_queries(1):
res = client.get("/courses/")
data = json.loads(res.content)
assert res.status_code == 200
assert len(data) == 1
author_name = fake.name()
author = Author(name=author_name)
author.save()
course_title = fake.sentence(nb_words=4)
course = Course(title=course_title, author=author)
course.save()
res = client.get("/courses/")
data = json.loads(res.content)
assert res.status_code == 200
assert len(data) == 2`
不使用 pytest?使用 assertNumQueries 测试方法代替
django_assert_num_queries
。
此外,我们还可以使用 nplusone 来防止引入未来的 N+1 个查询。在安装完包并将其添加到设置文件之后,您可以使用@override_settings
装饰器将它添加到您的测试中:
`...
@pytest.mark.django_db
@override_settings(NPLUSONE_RAISE=True)
def test_number_of_sql_queries_all_courses(client, django_assert_num_queries):
...`
或者,如果您想在整个测试套件中自动启用 nplusone,那么将以下内容添加到您的测试根 conftest.py 文件中:
`from django.conf import settings
def pytest_configure(config):
settings.NPLUSONE_RAISE = True`
回到示例应用程序,然后运行测试。您应该会看到以下错误:
`nplusone.core.exceptions.NPlusOneError: Potential n+1 query detected on `Course.author``
现在,进行推荐的更改——添加select_related
方法——然后再次运行测试。他们现在应该通过了。
结论
这篇文章介绍了如何使用 nplusone 包在代码库中自动阻止 N+1 个查询,并使用django_assert_num_queries
pytest fixture 测试执行的查询数量。
随着应用程序的增长和用户的增加,这应该有助于防止性能瓶颈。如果您将它添加到现有的代码库中,您可能需要花费一些时间来修复中断的查询,以便它们具有恒定的数据库命中次数。如果在修复和优化之后仍然遇到性能问题,您可能需要添加额外的缓存层,对数据库的某些部分进行反规范化,和/或配置数据库索引。
Django 的权限
Django 自带强大的权限系统。
在本文中,我们将了解如何为用户和组分配权限,以便授权他们执行特定的操作。
目标
完成本文后,您将能够:
- 解释 Django 的权限和组是如何工作的
- 利用 Django 内置许可系统的力量
身份验证与授权
这篇文章是关于授权的。
- 认证是确认用户是否有权访问系统的过程。通常,用户名/电子邮件和密码用于验证用户。
- 授权:与“认证”用户在系统中可以做什么有关。
换句话说,认证回答了“你是谁?”而授权回答‘你能做什么?’。
用户级权限
当django.contrib.auth
被添加到 settings.py 文件中的INSTALLED_APPS
设置中时,Django 自动为创建的每个 Django 模型创建add
、change
、delete
和view
权限。
Django 中的权限遵循以下命名顺序:
{app}.{action}_{model_name}
注意事项:
app
是相关模型所在的 Django 应用的名称action
:是add
、change
、delete
还是view
model_name
:小写的型号名称
让我们假设我们在一个名为“博客”的应用程序中有以下模型:
`from django.db import models
class Post(models.Model):
title = models.CharField(max_length=400)
body = models.TextField()`
默认情况下,Django 将创建以下权限:
blog.add_post
blog.change_post
blog.delete_post
blog.view_post
然后,您可以检查用户(通过 Django 用户对象)是否拥有使用has_perm()
方法的权限:
`from django.contrib.auth import get_user_model
from django.contrib.auth.models import User, Permission
from django.contrib.contenttypes.models import ContentType
from blog.models import Post
content_type = ContentType.objects.get_for_model(Post)
post_permission = Permission.objects.filter(content_type=content_type)
print([perm.codename for perm in post_permission])
# => ['add_post', 'change_post', 'delete_post', 'view_post']
user = User.objects.create_user(username="test", password="test", email="[[email protected]](/cdn-cgi/l/email-protection)")
# Check if the user has permissions already
print(user.has_perm("blog.view_post"))
# => False
# To add permissions
for perm in post_permission:
user.user_permissions.add(perm)
print(user.has_perm("blog.view_post"))
# => False
# Why? This is because Django's permissions do not take
# effect until you allocate a new instance of the user.
user = get_user_model().objects.get(email="[[email protected]](/cdn-cgi/l/email-protection)")
print(user.has_perm("blog.view_post"))
# => True`
超级用户将始终拥有设置为True
的权限,即使该权限不存在:
`from django.contrib.auth.models import User
superuser = User.objects.create_superuser(
username="super", password="test", email="[[email protected]](/cdn-cgi/l/email-protection)"
)
# Output will be true
print(superuser.has_perm("blog.view_post"))
# Output will be true even if the permission does not exists
print(superuser.has_perm("foo.add_bar"))`
超级用户是 Django 中拥有系统中所有权限的用户类型。无论是自定义权限还是 Django 创建的权限,超级用户都可以访问每一个权限。
staff 用户就像系统中的任何其他用户一样,但是具有能够访问 Django 管理界面的额外优势。Django 管理界面只对超级用户和职员用户开放。
组级权限
每次都必须给用户分配权限,这很繁琐,而且不可伸缩。在某些情况下,您可能希望向一组用户添加新的权限。这里是 Django 组织发挥作用的地方。
什么是团体?
- 英语定义:组是被分类在一起的一组对象。
- Django 定义:组模型是一种对用户进行分类的通用方法,因此您可以对这些用户应用权限或其他标签。用户可以属于任意数量的组。
使用 Django,您可以创建组来对用户进行分类,并为每个组分配权限,因此在创建用户时,您可以将用户分配到一个组,反过来,用户拥有该组的所有权限。
要创建一个组,您需要来自django.contrib.auth.models
的Group
模型。
让我们为以下角色创建组:
Author
:可以查看和添加帖子Editor
:可以查看、添加、编辑帖子Publisher
:可以查看、添加、编辑、删除帖子
代码:
`from django.contrib.auth.models import Group, User, Permission
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
from blog.models import Post
author_group, created = Group.objects.get_or_create(name="Author")
editor_group, created = Group.objects.get_or_create(name="Editor")
publisher_group, created = Group.objects.get_or_create(name="Publisher")
content_type = ContentType.objects.get_for_model(Post)
post_permission = Permission.objects.filter(content_type=content_type)
print([perm.codename for perm in post_permission])
# => ['add_post', 'change_post', 'delete_post', 'view_post']
for perm in post_permission:
if perm.codename == "delete_post":
publisher_group.permissions.add(perm)
elif perm.codename == "change_post":
editor_group.permissions.add(perm)
publisher_group.permissions.add(perm)
else:
author_group.permissions.add(perm)
editor_group.permissions.add(perm)
publisher_group.permissions.add(perm)
user = User.objects.get(username="test")
user.groups.add(author_group) # Add the user to the Author group
user = get_object_or_404(User, pk=user.id)
print(user.has_perm("blog.delete_post")) # => False
print(user.has_perm("blog.change_post")) # => False
print(user.has_perm("blog.view_post")) # => True
print(user.has_perm("blog.add_post")) # => True`
强制权限
除了 Django Admin 之外,权限通常是在视图层执行的,因为用户是从请求对象获得的。
要在基于类的视图中实施权限,您可以使用来自django.contrib.auth.mixins
的permissionrequiredminix,如下所示:
`from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import ListView
from blog.models import Post
class PostListView(PermissionRequiredMixin, ListView):
permission_required = "blog.view_post"
template_name = "post.html"
model = Post`
permission_required
可以是单一权限,也可以是可重复的权限。如果使用 iterable,用户必须拥有所有权限才能访问视图:
`from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import ListView
from blog.models import Post
class PostListView(PermissionRequiredMixin, ListView):
permission_required = ("blog.view_post", "blog.add_post")
template_name = "post.html"
model = Post`
对于基于函数的视图,使用permission_required
装饰器:
`from django.contrib.auth.decorators import permission_required
@permission_required("blog.view_post")
def post_list_view(request):
return HttpResponse()`
您还可以在 Django 模板中检查权限。使用 Django 的 auth context 处理器,当你渲染你的模板时,一个 perms 变量是默认可用的。perms
变量实际上包含了 Django 应用程序中的所有权限。
例如:
`{% if perms.blog.view_post %}
{# Your content here #}
{% endif %}`
模型级权限
您还可以通过 model Meta 选项向 Django 模型添加定制权限。
让我们给Post
模型添加一个is_published
标志:
`from django.db import models
class Post(models.Model):
title = models.CharField(max_length=400)
body = models.TextField()
is_published = models.Boolean(default=False)`
接下来,我们将设置一个名为set_published_status
的自定义权限:
`from django.db import models
class Post(models.Model):
title = models.CharField(max_length=400)
body = models.TextField()
is_published = models.Boolean(default=False)
class Meta:
permissions = [
(
"set_published_status",
"Can set the status of the post to either publish or not"
)
]`
为了实施这个权限,我们可以在视图中使用 Django 提供的 mixin,这给了我们显式检查用户是否拥有所需权限的灵活性。
下面是一个基于类的视图,它检查用户是否有权限设置帖子的发布状态:
`from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import render
from django.views.generic import View
from blog.models import Post
class PostListView(UserPassesTestMixin, View):
template_name = "post_details.html"
def test_func(self):
return self.request.user.has_perm("blog.set_published_status")
def post(self, request, *args, **kwargs):
post_id = request.POST.get('post_id')
published_status = request.POST.get('published_status')
if post_id:
post = Post.objects.get(pk=post_id)
post.is_published = bool(published_status)
post.save()
return render(request, self.template_name)`
因此,使用UserPassesTestMixin
,您需要覆盖该类的test_func
方法并添加您自己的测试。请注意,该方法的返回值必须始终是布尔值。
对象级权限
如果您使用的是 Django REST 框架,它已经将对象级权限内置到基本权限类中。BasePermission
有has_permission
,主要用于列表视图,还有has_object_permission
,用于检查用户是否有权限访问单个模型实例。
有关 Django REST 框架中权限的更多信息,请查看 Django REST 框架中的权限。
如果您没有使用 Django REST 框架,要实现对象级权限,您可以使用第三方,比如:
更多与权限相关的包,请查看 Django 包。
结论
在本文中,您已经学习了如何向 Django 模型添加权限并检查权限。如果您有一定数量的用户类型,您可以将每个用户类型创建为一个组,并向该组授予必要的权限。然后,对于添加到系统和所需组中的每个用户,权限会自动授予每个用户。
部署 Django 应用程序进行渲染
在本教程中,我们将看看如何部署一个 Django 应用程序来渲染。
目标
学完本教程后,您应该能够:
- 解释什么是渲染以及它是如何工作的。
- 部署一个 Django 应用程序来呈现。
- 渲染时加速 PostgreSQL 实例。
- 了解如何在渲染时提供静态和媒体文件。
- 添加一个自定义域并在 HTTPS 上提供您的 web 应用程序。
什么是渲染?
Render 是一个易于使用的平台即服务 (PaaS)解决方案,非常适合构建和运行您的所有应用程序和网站。它于 2019 年推出,此后越来越受欢迎。Render 允许您托管静态站点、web 服务、PostgreSQL 数据库和 Redis 实例。
它极其简单的用户界面/UX 和强大的 git 集成让你可以在几分钟内启动并运行一个应用。它具有对 Python、Node.js、Ruby、Elixir、Go 和 Rust 的原生支持。如果这些都不适合你,Render 还可以通过一个 Dockerfile 进行部署。
Render 的自动缩放功能将确保你的应用程序总是以合适的价格拥有必要的资源。此外,Render 上托管的所有内容也可以获得免费的 TLS 证书。
参考他们的官方文档以获得更多关于他们定价的信息。
为什么渲染?
- 非常适合初学者
- 轻松设置和部署应用
- 基于实时 CPU 和内存使用情况的自动扩展
- 免费层(包括 web 服务、PostgreSQL、Redis)——非常适合原型开发
- 良好的客户支持
项目设置
在本教程中,我们将部署一个简单的图像托管应用程序,名为 django-images 。
在学习教程的过程中,通过部署您自己的 Django 应用程序来检查您的理解。
首先,从 GitHub 上的库中获取代码:
创建新的虚拟环境并激活它:
`$ python3 -m venv venv && source venv/bin/activate`
安装需求并迁移数据库:
`(venv)$ pip install -r requirements.txt
(venv)$ python manage.py migrate`
运行服务器:
`(venv)$ python manage.py runserver`
打开您最喜欢的网络浏览器,导航到 http://localhost:8000 。使用右边的表格上传图像,确保一切正常。上传图像后,您应该会看到它显示在表格中:
配置 Django 项目
在本节教程中,我们将准备 Django 项目,以便进行渲染部署。
环境变量
我们不应该在源代码中存储秘密,所以让我们利用环境变量。最简单的方法是使用名为 python-dotenv 的第三方包。首先将其添加到 requirements.txt :
随意使用不同的包来处理环境变量,如 django-environ 或 python-decouple 。
接下来,导航到您的 settings.py 并在文件顶部初始化 python-dotenv,如下所示:
`# core/settings.py
from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env')`
接下来,从环境中加载SECRET_KEY
、DEBUG
和ALLOWED_HOSTS
:
`# core/settings.py
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', '0').lower() in ['true', 't', '1']
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(' ')`
不要忘记在文件顶部导入os
:
数据库ˌ资料库
让我们把 Django 的默认 SQLite 换成 PostgreSQL。
通过将下面一行添加到 requirements.txt 来安装数据库适配器:
稍后,当我们启动 PostgreSQL 数据库时,Render 将为我们提供一个DATABASE_URL
。这是一个受十二因素应用启发的环境变量,包括连接数据库所需的所有参数。它将采用以下格式:
`postgres://USER:PASSWORD@HOST:PORT/NAME`
为了在 Django 中使用它,我们可以使用一个名为 dj-database-url 的包。这个包允许我们将数据库 URL 转换成 Django 数据库参数。
像这样添加到 requirements.txt 中:
接下来,导航到 core/settings.py ,将DATABASES
更改如下:
`# core/settings.py
DATABASES = {
'default': dj_database_url.parse(os.environ.get('DATABASE_URL'), conn_max_age=600),
}`
不要忘记重要的一点:
格尼科恩
接下来,让我们安装 Gunicorn ,这是一个生产级的 WSGI 服务器,将用于生产,而不是 Django 的开发服务器。
添加到 requirements.txt :
构建脚本
为了收集静态文件和迁移数据库,我们将创建一个构建脚本。构建脚本允许我们在部署应用程序之前运行一系列命令。
在项目根目录下创建一个 build.sh 文件,内容如下:
`#!/usr/bin/env bash
set -o errexit # exit on error
pip install -r requirements.txt
python manage.py collectstatic --no-input
python manage.py migrate`
将所有更改提交给 git,并推送到您的遥控器。
部署
在本节教程中,我们将启动一个 PostgreSQL 实例,并部署 Django 应用程序进行渲染。
如果你还没有渲染账户,就去注册吧。
数据库ˌ资料库
在渲染面板中,点击屏幕右上角的“新建”,然后点击“PostgreSQL”。然后,使用以下参数创建一个 PostgreSQL 实例:
- 名称:自定义名称
- 数据库:留空
- 用户:留空
- 地区:离你最近的地区
- PostgreSQL 版本: 15
- 数据狗 API 键:留空
- 计划类型:适合您需求的计划
请记住,免费渲染帐户有以下限制:
- 如果你不升级你的帐户,免费的 PostgreSQL 数据库会在 90 天后被删除。
- Render 只为免费的 PostgreSQL 数据库提供 1 GB 的存储空间。
等待数据库状态从“正在创建”变为“可用”,然后向下滚动到“连接”部分。记下“内部数据库 URL”。
太好了。数据库部分到此为止。
网络服务
接下来,让我们创建一个 web 服务。
再次单击屏幕右上角的“New ”,但这次选择“Web Service”。
将您的渲染帐户连接到 GitHub 或 GitLab 帐户。确保向您想要部署的存储库授予呈现权限。连接后,选择您的存储库。
输入以下详细信息:
- 名称:自定义名称
- 区域:与您的数据库相同的区域
- 分支:您的主要分支(例如,主/主要)
- 根目录:留空
- 环境: Python 3
- 构建命令:
sh build.sh
- 开始命令
gunicorn core.wsgi:application
- 计划类型:适合您需求的计划
打开“高级”下拉菜单,添加以下环境变量:
PYTHON_VERSION
:3.9.9
SECRET_KEY
:点击“生成”DEBUG
:1
ALLOWED_HOSTS
:*
DATABASE_URL
:<your_internal_database_url>
我们需要设置PYTHON_VERSION
,因为 Render 的默认 Python 版本是3.7
,而 Django 4 需要3.8
或更高版本。我们还临时启用了调试模式,并允许所有主机。不要担心这一点,因为我们将在教程的后面更改它。
最后,单击“创建 Web 服务”。
Render 将检查您的源代码,准备环境,运行 build.sh ,生成容器,并部署它。
等待几分钟,让部署状态变为“实时”,然后通过上传图像来测试应用程序。您的 web 应用程序的 URL 显示在 web 服务名称下(左上角)。
每次您将代码签入遥控器时,Render 都会自动重新部署您的应用程序。
静态文件
为了在生产中提供静态文件,我们可以使用一个名为whiten noise的包。WhiteNoise 使我们的应用程序成为一个独立的单元,可以部署在任何地方,而不依赖于 Nginx、Apache 或任何其他外部服务。此外,它通过使用 gzip 和 Brotli 格式来压缩我们的内容。
首先将以下两个包添加到 requirements.txt 中:
`whitenoise==6.2.0
Brotli==1.0.9`
添加 Brotli 支持是可选的,但是 Whitenoise 推荐的以及 Render。
接下来,将 WhiteNoise 的中间件添加到 settings.py 中的MIDDLEWARE
列表中。它应该放在除 Django 的SecurityMiddleware
之外的所有其他中间件之上:
`# core/settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
# ...
]`
最后,向下滚动到 settings.py 的底部,添加:
`# core/settings.py
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'`
就是这样。下次 Render 部署新版本的应用程序时,我们的静态文件将由 WhiteNoise 提供服务。
Render(以及许多其他类似的服务,如 Heroku)提供了一个短暂的文件系统。这意味着当应用程序关闭或重新部署时,您的数据不会持久,可能会消失。如果你的应用程序需要保留文件,这是非常糟糕的。
要启用持久存储,您可以使用:
- 渲染磁盘:带自动每日快照的高性能固态硬盘
- 云对象存储,如 AWS S3 或类似的服务
我建议你设置 AWS S3,因为渲染磁盘不允许水平缩放,而且有点贵。
要了解如何使用 Django 设置 AWS S3,请看一下在亚马逊 S3 上存储 Django 静态和媒体文件的。
然而,为了本教程的完整性,让我们设置渲染磁盘。
导航到您的渲染仪表板并选择您的 web 服务。单击边栏上的“Disks ”,并使用以下详细信息创建一个新磁盘:
- 名称:选择一个自定义名称
- 挂载路径:/opt/render/project/src/media files
- 尺寸:适合你的最小尺寸
点击“创建”就完成了。您的媒体文件现在将保留。
Django 管理访问
创建超级用户有两种方法:
- SSH 进入服务器并运行
createsuperuser
命令。 - 创建一个 Django 命令来创建超级用户,并将其添加到 build.sh 。
我们将使用第二种方法,因为它允许我们自动化部署,而且 Render 不允许免费用户 SSH 到他们的 web 服务。
首先,在“images”应用程序中创建以下目录结构:
`└-- images
└-- management
|-- __init__.py
└-- commands
|-- __init__.py
└-- createsu.py`
将以下内容放入 createsu.py 中:
`# images/management/commands/createsu.py
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Creates a superuser.'
def handle(self, *args, **options):
if not User.objects.filter(username='admin').exists():
User.objects.create_superuser(
username='admin',
password='complexpassword123'
)
print('Superuser has been created.')`
如果您不想在源代码中暴露您的超级用户凭证,请考虑从环境变量中加载它们。
将createsu
命令添加到 build.sh 的末尾,如下所示:
`#!/usr/bin/env bash
set -o errexit # exit on error
pip install -r requirements.txt
python manage.py collectstatic --no-input
python manage.py migrate
python manage.py createsu # new`
最后,提交并推送代码。
Render 将自动重新部署您的应用程序。完成后,导航到您的 web 应用的管理仪表板并尝试登录。
自定义域
导航到您的渲染仪表板并选择您的 web 服务。选择边栏上的“设置”,然后向下滚动到“自定义域”部分。点击“添加”,输入您的域名,然后点击“保存”。
接下来,进入你的域名注册服务商 DNS 设置,添加一个新的“CNAME 记录”指向你的应用程序的主机名,如下所示:
`+----------+--------------+-----------------------------------+-----------+ | Type | Host | Value | TTL |
+----------+--------------+-----------------------------------+-----------+ | CNAME | <some host> | <your_app_hostname> | Automatic |
+----------+--------------+-----------------------------------+-----------+`
示例:
`+----------+--------------+-----------------------------------+-----------+ | Type | Host | Value | TTL |
+----------+--------------+-----------------------------------+-----------+ | CNAME | render | django-images-374w.onrender.com | Automatic |
+----------+--------------+-----------------------------------+-----------+`
等待几分钟,让 DNS 更改传播开来,然后单击“验证”。Render 将为您的域颁发 TLS 证书,您的应用现在可以在 HTTPS 上的自定义域中访问。
为确保其有效,请尝试访问您的 web 应用,网址为:
`https://<your_custom_domain>
Example:
https://render.testdriven.io`
将ALLOWED_HOSTS
环境变量改为<your_custom_domain>
(例如render.testdriven.io
)。然后,触发最新提交的手动部署。
等待您的应用程序重新部署,您就大功告成了!
结论
在本教程中,我们已经成功地部署了一个 Django 应用程序来进行渲染。我们已经处理了 PostgreSQL 数据库、静态和媒体文件,添加了自定义域名并启用了 HTTPS。现在,您应该对 Render 的工作原理有了一个大致的了解,并且能够部署自己的应用程序了。
下一步是什么?
- 考虑 AWS S3 或类似的服务,以更好、更安全的方式提供媒体文件。
- 设置
DEBUG=0
禁用调试模式。请记住,该应用程序仅在启用调试模式时提供媒体文件。有关如何在生产中处理静态和媒体文件的更多信息,请参考在 Django 中处理静态和媒体文件。 - 看看 Render 的缩放特性。
Django REST 框架教程
描述
Django REST Framework (DRF)是一个广泛使用的全功能 API 框架,旨在用 Django 构建 RESTful APIs。在其核心,DRF 集成了 Django 的核心特性——模型、视图和 URLs 使得创建 RESTful HTTP 资源变得简单而无缝。
DRF 由以下部分组成:
- 序列化器用于将 Django 查询集和模型实例转换为 JSON(以及许多其他数据呈现格式,如 XML 和 YAML )(序列化)和(反序列化)。
- 视图(以及视图集),类似于传统的 Django 视图,处理 RESTful HTTP 请求和响应。视图本身使用序列化程序来验证传入的有效负载,并包含返回响应的必要逻辑。视图与路由器耦合,路由器将视图映射回公开的 URL。
TestDriven.io 上的文章和教程是比较中级到高级的,涵盖了权限、序列化器和 Elasticsearch。
深入了解 Django REST 框架最强大的视图 ViewSets。
使用 Django REST Framework 的通用视图来防止一遍又一遍地重复某些模式。
深入探讨 Django REST 框架的视图是如何工作的,以及它最基本的视图 APIView。
- 由 发布尼克·托马齐奇
- 最后更新于2021 年 8 月 31 日
查看如何将 Django REST 框架与 Elasticsearch 集成。
如何在 Django REST 框架中构建自定义权限类?
Django REST 框架中内置权限类的工作方式。
Django REST 框架中权限的工作方式。
深入研究 Django REST 框架(DRF)序列化程序。
使用 Django 和 Postgres 进行基本和全文搜索
与关系数据库不同,全文搜索不是标准化的。有几个开源选项,如 ElasticSearch、Solr 和 Xapian。ElasticSearch 可能是最流行的解决方案;但是,设置和维护起来很复杂。此外,如果您没有利用 ElasticSearch 提供的一些高级功能,您应该坚持使用许多关系数据库(如 Postgres、MySQL、SQLite)和非关系数据库(如 MongoDB 和 CouchDB)提供的全文搜索功能。Postgres 尤其适合全文搜索。 Django 也支持开箱即用。
对于绝大多数 Django 应用程序,在寻找更强大的解决方案如 ElasticSearch 或 Solr 之前,你至少应该从利用 Postgres 的全文搜索开始。
在本教程中,您将学习如何使用 Postgres 向 Django 应用程序添加基本的全文搜索。您还将通过添加搜索向量字段和数据库索引来优化全文搜索。
这是一个中级教程。它假设您熟悉 Django 和 Docker。查看使用 Postgres、Gunicorn 和 Nginx 的教程以了解更多信息。
目标
本教程结束时,您将能够:
- 使用 Q 对象模块在 Django 应用程序中设置基本的搜索功能
- 向 Django 应用程序添加全文搜索
- 使用词干、排名和加权技术,按相关性对全文搜索结果进行排序
- 向您的搜索结果添加预览
- 使用搜索向量字段和数据库索引优化全文搜索
项目设置和概述
从 django-search repo 中克隆出 base 分支;
`$ git clone https://github.com/testdrivenio/django-search --branch base --single-branch
$ cd django-search`
您将使用 Docker 和 Django 来简化 Postgres 的设置和运行。
从项目根目录,创建映像并启动 Docker 容器:
`$ docker-compose up -d --build`
接下来,应用迁移并创建超级用户:
`$ docker-compose exec web python manage.py makemigrations
$ docker-compose exec web python manage.py migrate
$ docker-compose exec web python manage.py createsuperuser`
完成后,导航到http://127 . 0 . 0 . 1:8011/quotes/以确保应用程序按预期工作。您应该看到以下内容:
想学习如何与 Django 和 Postgres 合作吗?查看关于 Django 与 Postgres、Gunicorn 和 Nginx 的文章。
注意 quotes/models.py 中的Quote
型号:
`from django.db import models
class Quote(models.Model):
name = models.CharField(max_length=250)
quote = models.TextField(max_length=1000)
def __str__(self):
return self.quote`
接下来,运行以下管理命令,将 10,000 个报价添加到数据库中:
`$ docker-compose exec web python manage.py add_quotes`
这将需要几分钟时间。完成后,导航到http://127 . 0 . 0 . 1:8011/quotes/查看数据。
视图的输出被缓存了五分钟,所以您可能想要注释掉 quotes/views.py 中的
@method_decorator
来加载报价。确保完成后删除评论。
在quotes/templates/quote . html文件中,您有一个带有搜索输入字段的基本表单:
`<form action="{% url 'search_results' %}" method="get">
<input
type="search"
name="q"
placeholder="Search by name or quote..."
class="form-control"
/>
</form>`
提交时,表单将数据发送到后端。使用了一个GET
请求而不是一个POST
请求,这样我们就可以在 URL 和 Django 视图中访问查询字符串,允许用户以链接的形式共享搜索结果。
在继续之前,快速浏览一下项目结构和代码的其余部分。
基本搜索
当使用 Django 进行搜索时,您通常会通过使用contains
或icontains
执行搜索查询来进行精确匹配。 Q 对象也可以用来添加 AND ( &
)或 OR ( |
)逻辑运算符。
例如,使用 or 操作符,在报价/视图. py 中覆盖SearchResultsList
的默认QuerySet
,如下所示:
`class SearchResultsList(ListView):
model = Quote
context_object_name = "quotes"
template_name = "search.html"
def get_queryset(self):
query = self.request.GET.get("q")
return Quote.objects.filter(
Q(name__icontains=query) | Q(quote__icontains=query)
)`
这里,我们使用了过滤器方法来过滤name
或quote
字段。此外,我们还使用了 icontains 扩展来检查查询是否出现在name
或quote
字段中(不区分大小写)。如果找到匹配,将返回肯定的结果。
不要忘记重要的一点:
`from django.db.models import Q`
尝试一下:
对于小型数据集,这是向应用程序添加基本搜索功能的好方法。如果您正在处理一个大型数据集,或者想要一个类似于互联网搜索引擎的搜索功能,那么您将需要使用全文搜索。
全文搜索
我们前面看到的基本搜索有几个限制,尤其是当您想要执行复杂的查找时。
如上所述,使用基本搜索,您只能执行精确匹配。
另一个限制是停止字的限制。停用词是诸如“一个”、“一个”、“该”之类的词。这些词很常见,没有足够的意义,因此应该忽略。要进行测试,请尝试搜索前面带有“the”的单词。假设你搜索了“中间”。在这种情况下,您将只看到“中间”的结果,因此您不会看到任何包含单词“中间”而前面没有“the”的结果。
比方说你有这两句话:
- 我在中间。
- 你不喜欢中学。
每种类型的搜索都会返回以下内容:
询问 | 基本搜索 | 全文搜索 |
---|---|---|
“中间” | 1 | 1 和 2 |
“中间” | 1 和 2 | 1 和 2 |
另一个问题是忽略相似的单词。使用基本搜索,只返回完全匹配的内容。但是,使用全文搜索时,会考虑相似的单词。要测试,试着找一些类似“pony”和“ponies”的词。使用基本搜索,如果你搜索“小马”,你不会看到包含“小马”的结果,反之亦然。
说你有这两句话。
- 我是一匹小马。
- 你不喜欢小马
每种类型的搜索都会返回以下内容:
询问 | 基本搜索 | 全文搜索 |
---|---|---|
“小马” | 1 | 1 和 2 |
“小马” | 2 | 1 和 2 |
有了全文搜索,这两个问题都得到了缓解。但是,请记住,根据您的目标,全文搜索实际上可能会降低精度(质量)和召回(相关结果的数量)。通常,全文搜索不如基本搜索精确,因为基本搜索产生完全匹配。也就是说,如果您要搜索包含大量文本的大型数据集,那么全文搜索是首选,因为它通常要快得多。
全文搜索是一种高级搜索技术,它会在尝试匹配搜索标准时检查每个存储文档中的所有单词。此外,使用全文搜索,您可以对被索引的单词使用特定语言的词干。例如,单词“驱动”、“被驱动”和“被驱动”将被记录在单个概念单词“驱动”下。词干化是将单词缩减为词干、词根或词根形式的过程。
可以说全文搜索并不完美。很可能检索到许多与预期搜索查询不相关(误报)的文档。然而,有一些基于贝叶斯算法的技术可以帮助减少这样的问题。
要利用 Django 的 Postgres 全文搜索,请将django.contrib.postgres
添加到您的INSTALLED_APPS
列表中:
`INSTALLED_APPS = [
...
"django.contrib.postgres", # new
]`
接下来,我们来看两个快速的全文搜索示例,分别针对单个字段和多个字段。
单一字段搜索
像这样更新SearchResultsList
视图功能下的get_queryset
功能:
`class SearchResultsList(ListView):
model = Quote
context_object_name = "quotes"
template_name = "search.html"
def get_queryset(self):
query = self.request.GET.get("q")
return Quote.objects.filter(quote__search=query)`
这里,我们针对单个字段 quote 字段设置了全文搜索。
如你所见,它考虑了相似的单词。在上面的例子中,“ponies”和“pony”被视为相似的词。
多字段搜索
要搜索多个字段和相关模型,可以使用SearchVector
类。
再次更新SearchResultsList
:
`class SearchResultsList(ListView):
model = Quote
context_object_name = "quotes"
template_name = "search.html"
def get_queryset(self):
query = self.request.GET.get("q")
return Quote.objects.annotate(search=SearchVector("name", "quote")).filter(
search=query
)`
为了搜索多个字段,您可以使用SearchVector
对查询集进行注释。vector 是您正在搜索的数据,它已经被转换成易于搜索的形式。在上面的例子中,这些数据是数据库中的name
和quote
字段。
确保添加导入:
`from django.contrib.postgres.search import SearchVector`
尝试一些搜索。
词干和排序
在这一节中,您将结合几种方法,如 SearchVector 、 SearchQuery 和 SearchRank 来生成一个非常健壮的搜索,它使用词干和排名。
同样,词干化是将单词缩减为词干、词根或词根形式的过程。使用词干,像“child”和“children”这样的单词将被视为相似的单词。另一方面,排名允许我们根据相关性对结果进行排序。
更新SearchResultsList
:
`class SearchResultsList(ListView):
model = Quote
context_object_name = "quotes"
template_name = "search.html"
def get_queryset(self):
query = self.request.GET.get("q")
search_vector = SearchVector("name", "quote")
search_query = SearchQuery(query)
return (
Quote.objects.annotate(
search=search_vector, rank=SearchRank(search_vector, search_query)
)
.filter(search=search_query)
.order_by("-rank")
)`
这里发生了什么事?
SearchVector
-再次使用搜索向量搜索多个字段。数据被转换成另一种形式,因为您不再像使用icontains
时那样只是搜索原始文本。因此,有了这个,你将能够很容易地搜索复数。例如,搜索“flask”和“flask”会得到相同的搜索结果,因为它们基本上是相同的东西。SearchQuery
-翻译表单中作为查询提供给我们的单词,通过词干算法传递它们,然后寻找所有结果词的匹配。SearchRank
-允许我们根据相关性对结果进行排序。它考虑了查询术语在文档中出现的频率、术语与文档的接近程度以及它们在文档中出现的位置有多重要。
添加导入:
`from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank`
将基本搜索的结果与全文搜索的结果进行比较。有明显的区别。在全文搜索中,最先显示结果最多的查询。这就是SearchRank
的力量。组合SearchVector
、SearchQuery
和SearchRank
是一种比基本搜索更强大、更精确的快速搜索方式。
添加重量
全文搜索使我们能够为数据库中的某些字段添加比其他字段更重要的内容。我们可以通过给查询增加权重来实现这一点。
权重应该是以下字母 D、C、B、a 中的一个,默认情况下,这些权重分别指的是数字 0.1、0.2、0.4、1.0。
更新SearchResultsList
:
`class SearchResultsList(ListView):
model = Quote
context_object_name = "quotes"
template_name = "search.html"
def get_queryset(self):
query = self.request.GET.get("q")
search_vector = SearchVector("name", weight="B") + SearchVector(
"quote", weight="A"
)
search_query = SearchQuery(query)
return (
Quote.objects.annotate(rank=SearchRank(search_vector, search_query))
.filter(rank__gte=0.3)
.order_by("-rank")
)`
这里,您使用name
和quote
字段向SearchVector
添加了权重。权重 0.4 和 1.0 分别应用于名称和报价字段。因此,引用匹配将优先于名称内容匹配。最后,过滤结果,只显示大于 0.3 的结果。
向搜索结果添加预览
在本节中,您将通过 SearchHeadline 方法添加一点搜索结果的预览。这将突出显示搜索结果查询。
再次更新SearchResultsList
:
`class SearchResultsList(ListView):
model = Quote
context_object_name = "quotes"
template_name = "search.html"
def get_queryset(self):
query = self.request.GET.get("q")
search_vector = SearchVector("name", "quote")
search_query = SearchQuery(query)
search_headline = SearchHeadline("quote", search_query)
return Quote.objects.annotate(
search=search_vector,
rank=SearchRank(search_vector, search_query)
).annotate(headline=search_headline).filter(search=search_query).order_by("-rank")`
SearchHeadline
接收您想要预览的字段。在这种情况下,这将是查询的quote
字段,以粗体显示。
确保添加导入:
`from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank, SearchHeadline`
在尝试一些搜索之前,更新quotes/templates/search . html中的<li></li>
,如下所示:
`<li>{{ quote.headline | safe }} - <b>By <i>{{ quote.name }}</i></b></li>`
现在,不再像以前那样显示报价,而是只显示完整报价字段的预览以及突出显示的搜索查询。
提升性能
全文搜索是一个密集的过程。要解决性能低下的问题,您可以:
- 用 SearchVectorField 将搜索向量保存到数据库中。换句话说,我们将创建一个单独的数据库字段,包含已处理的搜索向量,并在任何时候对
quote
或name
字段进行插入或更新时更新该字段,而不是动态地将字符串转换为搜索向量。 - 创建一个数据库索引,这是一个提高数据库数据检索过程速度的数据结构。因此,它加快了查询速度。Postgres 给了你几个索引,可能适用于不同的情况。可以说,基尼指数是最受欢迎的。
要了解关于全文搜索性能的更多信息,请查看 Django 文档中的性能一节。
搜索矢量场
首先向 quotes/models.py 中的Quote
模型添加一个新的 SearchVectorField 字段:
`from django.contrib.postgres.search import SearchVectorField # new
from django.db import models
class Quote(models.Model):
name = models.CharField(max_length=250)
quote = models.TextField(max_length=1000)
search_vector = SearchVectorField(null=True) # new
def __str__(self):
return self.quote`
创建迁移文件:
`$ docker-compose exec web python manage.py makemigrations`
现在,只有当数据库中已经存在quote
或name
对象时,才能填充该字段。因此,每当quote
或name
字段被更新时,我们需要添加一个触发器来更新search_vector
字段。为此,在“报价/迁移”中创建一个名为0003 _ search _ vector _ trigger . py的定制迁移文件:
`from django.contrib.postgres.search import SearchVector
from django.db import migrations
def compute_search_vector(apps, schema_editor):
Quote = apps.get_model("quotes", "Quote")
Quote.objects.update(search_vector=SearchVector("name", "quote"))
class Migration(migrations.Migration):
dependencies = [
("quotes", "0002_quote_search_vector"),
]
operations = [
migrations.RunSQL(
sql="""
CREATE TRIGGER search_vector_trigger
BEFORE INSERT OR UPDATE OF name, quote, search_vector
ON quotes_quote
FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(
search_vector, 'pg_catalog.english', name, quote
);
UPDATE quotes_quote SET search_vector = NULL;
""",
reverse_sql="""
DROP TRIGGER IF EXISTS search_vector_trigger
ON quotes_quote;
""",
),
migrations.RunPython(
compute_search_vector, reverse_code=migrations.RunPython.noop
),
]`
根据您的项目结构,您可能需要更新
dependencies
中先前迁移文件的名称。
应用迁移:
`$ docker-compose exec web python manage.py migrate`
要使用新字段进行搜索,请像这样更新SearchResultsList
:
`class SearchResultsList(ListView):
model = Quote
context_object_name = "quotes"
template_name = "search.html"
def get_queryset(self):
query = self.request.GET.get("q")
return Quote.objects.filter(search_vector=query)`
再次更新quotes/templates/search . html中的<li></li>
:
`<li>{{ quote.quote | safe }} - <b>By <i>{{ quote.name }}</i></b></li>`
索引
最后,我们来设置一个函数索引, GinIndex 。
更新Quote
型号:
`from django.contrib.postgres.indexes import GinIndex # new
from django.contrib.postgres.search import SearchVectorField
from django.db import models
class Quote(models.Model):
name = models.CharField(max_length=250)
quote = models.TextField(max_length=1000)
search_vector = SearchVectorField(null=True)
def __str__(self):
return self.quote
# new
class Meta:
indexes = [
GinIndex(fields=["search_vector"]),
]`
最后一次创建和应用迁移:
`$ docker-compose exec web python manage.py makemigrations
$ docker-compose exec web python manage.py migrate`
测试一下。
结论
在本教程中,指导您向 Django 应用程序添加基本的全文搜索。我们还研究了如何通过添加搜索向量字段和数据库索引来优化全文搜索功能。
从 django-search repo 中获取完整代码。
向 Django 添加社会认证
本教程着眼于如何使用 Django Allauth 向 Django 应用程序添加社交认证(也称为社交登录或社交登录)。您还将配置 GitHub 和 Twitter 认证,以及带有用户名和密码的常规认证。
社交登录是一种单点登录形式,使用脸书、推特或谷歌等社交网络服务的现有信息登录第三方网站,而不是专门为该网站创建新的登录帐户。它旨在简化最终用户的登录,并为 web 开发人员提供更可靠的人口统计信息。
使用社交认证有其优势。您不需要为您的 web 应用程序设置 auth,因为它是由第三方 OAuth 提供者处理的。此外,由于像谷歌、脸书和 GitHub 这样的提供商执行广泛的检查来防止对其服务的未经授权的访问,利用社交认证而不是滚动您自己的认证机制可以提高您的应用程序的安全性。
OAuth
社交认证通常是通过 OAuth 来实现的,OAuth 是一种开放的标准认证协议,由第三方认证提供商来验证用户的身份。
典型流程:
- 用户试图使用第三方身份验证提供商的帐户登录您的应用程序
- 它们被重定向到身份验证提供者进行验证
- 验证后,它们会被重定向回你的应用程序
- 然后他们登录,这样他们就可以访问受保护的资源
有关 OAuth 的更多信息,请查看OAuth 2 的介绍。
为什么要利用 OAuth 而不是滚动自己的 Auth 呢?
优点:
- 提高安全性。
- 由于无需创建和记住用户名或密码,登录流程更加简单快捷。
- 在安全漏洞的情况下,不会发生第三方损害,因为认证是无密码的。
缺点:
- 您的应用程序现在依赖于您控制之外的另一个应用程序。如果提供商关闭,用户将无法登录。
- 人们往往会忽略 OAuth 提供者请求的权限。
- 在您配置的提供商中没有帐户的用户将无法访问您的应用程序。最好的方法是同时实现用户名、密码和社交认证,让用户自己选择。
django Allauth vs . Python Social Auth
Django Allauth 和 Python Social Auth 是在 Django 中实现社会认证的两个最流行的包。你应该用哪一个?
姜戈·阿劳斯
优点:
- Django Allauth 是最受欢迎的 Django 包之一。
- 它支持 50 多个身份验证提供商(例如 GitHub、Twitter、Google)。
- 除了社交认证,它还提供常规的用户名和密码认证。
- Django Allauth 使得定制授权流程中使用的表单变得很容易。
缺点:
- 尽管这个包很受欢迎,但是文档结构很差,不适合初学者。
- 注册 OAuth 提供者需要相当多的初始设置,这对初学者来说可能很困难。
- GitHub 上有 310 多个问题(截至发稿时)。
Python 社交认证
优点:
-
Python Social Auth 提供了对几个 Python web 框架的支持,如 Django、Flask、Webpy、Pyramid 和 Tornado。
-
它支持近 50 个 OAuth 提供者。
-
它支持 Django ORM 和 MongoEngine ODM。
-
它提供了一个存储接口,允许用户添加更多的 ORM。
例如,要了解如何使用存储接口来处理 SQLAlchemy ORM,请在这里查看代码。查看官方文档,了解更多关于如何使用存储接口的信息。
缺点:
- 文档稍微简单一点,但是仍然需要做一些与组织相关的工作。
- 同样,注册 OAuth 提供者需要相当多的初始设置,这对初学者来说可能很困难。
- GitHub 上有将近 125 个未解决的问题(截至发稿时)。
这两种包装都有其起伏。然而,本教程主要关注 Django Allauth,因为它更受欢迎,并且支持通过用户名和密码进行社交认证和常规认证。
Django Setup
让我们创建一个新的 Django 项目并配置 Django Allauth。
创建新的 Django 项目
首先创建一个虚拟环境并安装 Django:
`$ mkdir django-social-auth && cd django-social-auth
$ python3.11 -m venv .venv
$ source .venv/bin/activate
(.venv)$ pip install Django==4.1.3`
随意把 venv 和 Pip 换成诗歌或 Pipenv 。更多信息,请查看现代 Python 环境。
现在创建一个新项目,应用迁移,并运行服务器:
`(.venv)$ django-admin startproject social_app .
(.venv)$ python manage.py migrate
(.venv)$ python manage.py runserver`
导航到 http://127.0.0.1:8000 。您应该会看到以下屏幕:
Configure Django Allauth
接下来,让我们为 Django 应用程序设置 Django Allauth。
`(.venv)$ pip install django-allauth==0.51.0`
为了让 Django Allauth 使用我们的 Django 应用程序,更新 settings.py 文件中的INSTALLED_APPS
,如下所示:
`# social_app/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sites", # new
# 3rd party
"allauth", # new
"allauth.account", # new
"allauth.socialaccount", # new
# social providers
"allauth.socialaccount.providers.github", # new
"allauth.socialaccount.providers.twitter", # new
]`
首先,我们添加了 Django“站点”框架,这是 Allauth 正常工作所必需的。然后我们添加了核心的 Allauth 应用程序:allauth
、allauth.account
和allauth.socialaccount
。
现在将以下内容添加到 settings.py 的底部:
`# social_app/settings.py
AUTHENTICATION_BACKENDS = (
"allauth.account.auth_backends.AuthenticationBackend",
)
SITE_ID = 1
ACCOUNT_EMAIL_VERIFICATION = "none"
LOGIN_REDIRECT_URL = "home"
ACCOUNT_LOGOUT_ON_GET = True`
这里,我们定义了以下内容:
- 我们添加了
allauth
作为认证后端。所有登录和退出(通过 OAuth 或常规用户名和密码)现在将由 Allauth 处理。 SITE_ID
,Django Allauth 需要它才能运行。ACCOUNT_EMAIL_VERIFICATION = "none"
关闭验证邮件。Django 自动建立了一个电子邮件验证工作流程。我们现在不需要这个功能。- 成功登录后,将用户重定向到主页。
ACCOUNT_LOGOUT_ON_GET = True
当通过 GET 请求点击注销按钮时,直接将用户注销。这跳过了确认注销页面。
更新 urls.py 以包含 Django Allauth:
`from django.contrib import admin
from django.urls import path, include # new
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")), # new
]`
应用与 Django Allauth 相关的迁移文件:
`(.venv)$ python manage.py migrate`
迁移在这里很重要,因为 Allauth 需要大量新表。别忘了这一步!
创建超级用户:
`(.venv)$ python manage.py createsuperuser`
模板
新建一个名为“templates”的文件夹,添加两个名为 _base.html 和【home.html】T2 的文件:
`(.venv)$ mkdir templates && cd templates
(.venv)$ touch _base.html home.html`
更新 settings.py 中的TEMPLATES
,以便 Django 知道在哪里可以找到模板:
`# social_app/settings.py
TEMPLATES = [
{
...
"DIRS": [str(BASE_DIR.joinpath("templates"))],
...
},
]`
templates/_base.html :
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link
href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Django Social Login</title>
</head>
<body>
{% block content %} {% endblock content %}
</body>
</html>`
templates/home.html
`{% extends '_base.html' %} {% load socialaccount %}
{% block content %}
<div class="container" style="text-align: center; padding-top: 10%;">
<h1>Django Social Login</h1>
<br /><br />
{% if user.is_authenticated %}
<h3>Welcome {{ user.username }} !!!</h3>
<br /><br />
<a href="{% url 'account_logout' %}" class="btn btn-danger">Logout</a>
{% endif %}
</div>
{% endblock content %}`
创建一个视图来提供home.html模板:
`# social_app/views.py
from django.views.generic import TemplateView
class Home(TemplateView):
template_name = "home.html"`
添加新的 URL:
`# social_app/urls.py
from django.contrib import admin
from django.urls import path, include
from .views import Home # new
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")),
path("", Home.as_view(), name="home"), # new
]`
就是这样!Django Allauth 已配置好,可以测试了。现在,您应该能够通过用户名和密码登录。运行服务器。导航到http://127 . 0 . 0 . 1:8000/accounts/log in/。确保您可以使用超级用户凭据登录。
您可能想要覆盖默认模板来应用 CSS 样式等等。查看官方文档中的模板页面了解更多信息。
GitHub 提供商
现在 Django 和 Django Allauth 都准备好了,让我们连接我们的第一个社会认证提供者:GitHub。
应用
首先,我们需要创建一个 OAuth 应用程序,并从 GitHub 获取 OAuth 密钥。登录你的 GitHub 账户,然后导航到https://github.com/settings/applications/new创建一个新的 OAuth 应用:
`Application name: Testing Django Allauth
Homepage URL: http://127.0.0.1:8000
Callback URL: http://127.0.0.1:8000/accounts/github/login/callback`
点击“注册申请”。你将被重定向至你的应用。记下客户端 ID 和客户端密码:
如果没有生成客户端密码,请点按“生成新的客户端密码”。
接下来,我们需要在 Django 管理面板中添加 GitHub 提供者。
运行服务器:
`(.venv)$ python manage.py runserver`
在http://127 . 0 . 0 . 1:8000/admin登录管理员。然后,在“社交应用”下,点击“添加社交应用”:
- 选择 GitHub 作为提供商
- 添加姓名
- 将先前获得的客户端 ID 和客户端秘密添加到秘密密钥中
- 将 example.com 添加为所选地点之一
我们已经成功地将 GitHub 整合为一个社交认证提供商。现在,让我们更新一下 templates/home.html 模板来测试一下:
`{% extends '_base.html' %} {% load socialaccount %}
{% block content %}
<div class="container" style="text-align: center; padding-top: 10%;">
<h1>Django Social Login</h1>
<br /><br />
{% if user.is_authenticated %}
<h3>Welcome {{ user.username }} !!!</h3>
<br /><br />
<a href="{% url 'account_logout' %}" class="btn btn-danger">Logout</a>
{% else %}
<!-- GitHub button starts here -->
<a href="{% provider_login_url 'github' %}" class="btn btn-secondary">
<i class="fa fa-github fa-fw"></i>
<span>Login with GitHub</span>
</a>
<!-- GitHub button ends here -->
{% endif %}
</div>
{% endblock content %}`
运行应用程序。你现在应该可以通过 GitHub 登录了。
登录后,您应该会在http://127 . 0 . 0 . 1:8000/admin/auth/user/看到用户,以及在http://127 . 0 . 0 . 1:8000/admin/socialaccount/socialaccount/看到关联的社交账户。如果你查看社交账户,你会看到与 GitHub 账户相关的所有公开数据。这些数据(称为范围)可以用于你在 Django 上的用户资料。为此,建议使用定制的用户模型。更多信息,请查看在 Django 中创建定制用户模型。
您是否需要对 Allauth 默认范围之外的用户信息进行读写访问?从官方文件中查看变更提供商范围。
设置 Twitter 提供者类似于 GitHub:
- 在 Twitter 上创建 OAuth 应用程序
- 在 Django admin 中注册提供者
- 更新home.html模板
从申请一个 Twitter 开发者账户开始。创建完成后,导航至项目和应用,点击“创建应用”。
为应用程序命名,并记下 API 密钥和 API 密钥。然后,在“认证设置”下,打开“启用 3 脚 OAuth”和“向用户请求电子邮件地址”。添加回拨、网站、服务条款和隐私政策 URL:
`Callback URL: http://127.0.0.1:8000/accounts/twitter/login/callback
Website URL: http://example.com
Terms of service: http://example.com
Privacy policy: http://example.com`
让我们在 Django 管理中添加提供者。
运行服务器:
`(.venv)$ python manage.py runserver`
在http://127 . 0 . 0 . 1:8000/admin登录管理员。然后,在“社交应用”下,点击“添加社交应用”:
- 选择 Twitter 作为提供商
- 添加姓名
- 将先前获得的 API 密钥(添加到客户端 id)和 API 秘密密钥(添加到秘密密钥)
- 将 example.com 添加为所选地点之一
记住保护您的 API 密钥和令牌。
最后,在 templates/home.html 中添加一个“用 Twitter 登录”按钮:
`{% extends '_base.html' %} {% load socialaccount %}
{% block content %}
<div class="container" style="text-align: center; padding-top: 10%;">
<h1>Django Social Login</h1>
<br /><br />
{% if user.is_authenticated %}
<h3>Welcome {{ user.username }} !!!</h3>
<br /><br />
<a href="{% url 'account_logout' %}" class="btn btn-danger">Logout</a>
{% else %}
...
<!-- Twitter button starts here -->
</a>
<a href="{% provider_login_url 'twitter' %}" class="btn btn-primary">
<i class="fa fa-twitter fa-fw"></i>
<span>Login with Twitter</span>
</a>
<!-- Twitter button ends here -->
{% endif %}
</div>
{% endblock content %}`
导航到 http://127.0.0.1:8000 以测试身份验证工作流。
结论
本教程详细介绍了如何用 Django 和 Django Allauth 设置社交认证。现在,您应该对如何连接新的社交认证提供商有了深入的了解:
- 将适当的 Allauth 应用程序添加到设置文件中的
INSTALLED_APPS
- 在提供商的开发人员站点上创建一个 OAuth 应用程序,并记录令牌/密钥/秘密
- 在 Django 管理中注册应用程序
- 将 URL 添加到模板中
尽管本教程关注的是 Django Allauth,但这并不意味着它应该在每种情况下都用于 Python Social Auth。探索这两个包。尝试实现自定义表单并链接多个社交帐户。
从 GitHub 上的 django-social-auth 库获取代码。
Django 基于会话的单页应用授权
在本文中,我们将看看如何使用基于会话的认证来认证单页面应用程序 (SPAs)。我们将使用 Django 作为后端,而前端将使用 React 构建,这是一个为构建用户界面而设计的 JavaScript 库。
请随意将 React 替换为不同的工具,如 Angular、Vue 或 Svelte。
会话与基于令牌的身份验证
它们是什么?
使用基于会话的身份验证,会生成一个会话,并将 ID 存储在 cookie 中。
登录后,服务器会验证凭据。如果有效,它生成一个会话,存储它,然后将会话 id 发送回浏览器。浏览器将会话 ID 存储为 cookie,每当向服务器发出请求时,就会发送该 cookie。
基于会话的身份验证是有状态的。每当客户端请求服务器时,服务器必须在内存中定位会话,以便将会话 ID 绑定到相关用户。
另一方面,与基于会话的身份验证相比,基于令牌的身份验证相对较新。随着水疗和 RESTful APIs 的兴起,它获得了牵引力。
登录后,服务器验证凭据,如果有效,则创建一个签名令牌并发送回浏览器。大多数情况下,令牌存储在 localStorage 中。然后,当向服务器发出请求时,客户端会将令牌添加到报头中。假设请求来自授权来源,服务器解码令牌并检查其有效性。
令牌是对用户信息进行编码的字符串。
例如:
`// token header { "alg": "HS256", "typ": "JWT" } // token payload { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }`
令牌可以被验证和信任,因为它是使用秘密密钥或公钥/私钥对进行数字签名的。最常见的令牌类型是 JSON Web 令牌 (JWT)。
由于令牌包含服务器验证用户身份所需的所有信息,因此基于令牌的身份验证是无状态的。
有关会话和令牌的更多信息,请查看 Stack Exchange 中的会话认证与令牌认证。
安全漏洞
如前所述,基于会话的身份验证在 cookie 中维护客户端的状态。虽然 JWT 可以存储在 localStorage 或 cookie 中,但是大多数基于令牌的 auth 实现都将 JWT 存储在 localStorage 中。这两种方法都存在潜在的安全问题:
CSRF 是一种针对 web 应用程序的攻击,攻击者试图欺骗经过身份验证的用户执行恶意操作。大多数 CSRF 攻击的目标是使用基于 cookie 的身份验证的 web 应用程序,因为 web 浏览器包括与每个请求的特定域相关联的所有 cookie。因此,当发出恶意请求时,攻击者可以很容易地利用存储的 cookies。
要了解更多关于 CSRF 和如何在烧瓶中预防它,请查看烧瓶中的 CSRF 保护文章。
XSS 攻击是一种注入类型,恶意脚本被注入客户端,通常是为了绕过浏览器的同源策略。在 localStorage 中存储令牌的 Web 应用程序容易受到 XSS 攻击。打开浏览器并导航到任何站点。在开发者工具中打开控制台,输入JSON.stringify(localStorage)
。按回车键。这应该以 JSON 序列化的形式打印 localStorage 元素。脚本访问 localStorage 就是这么容易。
关于在哪里存储 jwt 的更多信息,请查看在哪里存储 jwt——cookie 与 HTML5 Web 存储。
设置基于会话的身份验证
本教程涵盖了以下将 Django 与前端库或框架相结合的方法:
- 通过 Django 模板提供框架
- 在同一个域上独立于 Django 提供框架
- 与 Django 分开提供框架,Django REST 框架在同一个域中
- 在不同的域上独立于 Django 提供框架
同样,你也可以随意替换 React 作为你选择的前端——例如,棱角分明的,脆弱的,苗条的。
Django 提供前台服务
使用这种方法,我们将直接从 Django 提供 React 应用程序。这种方法是最容易建立的。
后端
让我们首先为我们的项目创建一个新目录。在目录中,我们将创建并激活一个新的虚拟环境,安装 Django,并创建一个新的 Django 项目:
`$ mkdir django_react_templates && cd django_react_templates
$ python3.9 -m venv env
$ source env/bin/activate
(env)$ pip install django==3.1.4
(env)$ django-admin.py startproject djangocookieauth .`
之后,创建一个名为api
的新应用:
`(env)$ python manage.py startapp api`
在INSTALLED_APPS
下的djangookieauth/settings . py中注册 app:
`# djangocookieauth/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api.apps.ApiConfig', # new
]`
我们的应用将拥有以下 API 端点:
/api/login/
允许用户通过提供用户名和密码登录/api/logout/
注销用户/api/session/
检查会话是否存在/api/whoami/
获取已验证用户的用户数据
对于视图,在这里获取完整的代码,并将其添加到 api/views.py 文件中。
向“api”添加一个 urls.py 文件,并定义以下 URL:
`# api/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('login/', views.login_view, name='api-login'),
path('logout/', views.logout_view, name='api-logout'),
path('session/', views.session_view, name='api-session'),
path('whoami/', views.whoami_view, name='api-whoami'),
]`
现在,让我们将我们的应用程序 URL 注册到基础项目:
`# djangocookieauth/urls.py
from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include # new import
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')), # new
]`
我们后端的代码现在差不多完成了。运行 migrate 命令并创建一个超级用户以供将来测试:
`(env)$ python manage.py migrate
(env)$ python manage.py createsuperuser`
最后,更新djangookieauth/settings . py中的以下安全设置:
`CSRF_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_HTTPONLY = False # False since we will grab it via universal-cookies
SESSION_COOKIE_HTTPONLY = True
# PROD ONLY
# CSRF_COOKIE_SECURE = True
# SESSION_COOKIE_SECURE = True`
注意事项:
- 将
CSRF_COOKIE_SAMESITE
和SESSION_COOKIE_SAMESITE
设置为True
可以防止任何外部请求发送 cookies 和 CSRF 令牌。 - 将
CSRF_COOKIE_HTTPONLY
和SESSION_COOKIE_HTTPONLY
设置为True
会阻止客户端 JavaScript 访问 CSRF 和会话 cookies。我们将CSRF_COOKIE_HTTPONLY
设置为False
,因为我们将通过 JavaScript 访问 cookie。
如果你在制作中,你应该通过 HTTPS 服务你的网站并启用
CSRF_COOKIE_SECURE
和SESSION_COOKIE_SECURE
,这将只允许 cookies 通过 HTTPS 发送。
前端
在你开始前端工作之前,确保你已经安装了 Node.js 和 npm (或者 Yarn )。
我们将使用 Create React App 来搭建一个新的 React 项目:
`$ npx create-react-app frontend
$ cd frontend
$ npm start`
这将在端口 3000 上启动我们的应用程序。访问 http://localhost:3000 以确保其工作正常:
您可以通过删除所有文件和文件夹来简化前端,除了:
`├── README.md
├── node_modules
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
└── src
├── App.js
├── index.css
└── index.js`
接下来,让我们添加 Bootstrapfrontend/public/index . html:
`<!-- frontend/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
<!-- new -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" crossorigin="anonymous">
<!-- end of new -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>`
接下来,我们将使用 universal-cookie 将 cookie 加载到 React 应用程序中。
从“前端”文件夹安装它:
`$ npm install universal-cookie`
抓取App
组件的完整代码在这里,并将其添加到 frontend/src/App.js 文件中。
这只是一个简单的带有表单的前端应用程序,由 React state 处理。在页面加载时,compontentDidMount()
被调用,它获取会话并将isAuthenticated
设置为true
或false
。
我们使用universal-cookie
获得了 CSRF 令牌,并在我们的请求中将其作为报头传递给了X-CSRFToken
:
`import Cookies from "universal-cookie"; const cookies = new Cookies(); login = (event) => { event.preventDefault(); fetch("/api/login/", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRFToken": cookies.get("csrftoken"), }, credentials: "same-origin", body: JSON.stringify({username: this.state.username, password: this.state.password}), }) .then(this.isResponseOk) .then((data) => { console.log(data); this.setState({isAuthenticated: true, username: "", password: "", error: ""}); }) .catch((err) => { console.log(err); this.setState({error: "Wrong username or password."}); }); }`
请注意,对于每个请求,我们都使用了credentials: same-origin
。这是必需的,因为如果 URL 与调用脚本来源相同,我们希望浏览器在每个 HTTP 请求中传递 cookies。
更新 frontend/src/index.js :
`// frontend/src/index.js import React from "react"; import ReactDOM from "react-dom"; import App from "./App.js"; import "./index.css"; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") );`
上菜反应
首先,构建前端应用程序:
这个命令将生成“build”文件夹,我们的后端将使用它来提供 React 应用程序。
接下来,我们必须让 Django 知道我们的 React 应用程序在哪里:
`# djangocookieauth/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR.joinpath('frontend')], # new
'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',
],
},
},
]
...
STATIC_URL = '/static/'
STATICFILES_DIRS = (
BASE_DIR.joinpath('frontend', 'build', 'static'), # new
)`
如果你使用的是 Django 的旧版本,确保导入
os
并使用 os.path.join 而不是 joinpath 。
让我们为我们的应用程序创建索引视图:
`# djangocookieauth/urls.py
from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include
# new
def index_view(request):
return render(request, 'build/index.html')
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
path('', index_view, name='index'), # new
]`
因为 Django 最终提供前端服务,所以 CSRF cookie 将被自动设置。
从项目根目录,使用runserver
命令运行 Django 服务器,如下所示:
`(env)$ python manage.py runserver`
打开浏览器,导航到 https://localhost:8000/ 。您的 React 应用程序现在通过 Django 模板提供服务。
在加载时,设置 CSRF cookie,用于后续的 AJAX 请求。如果用户输入了正确的用户名和密码,它会对他们进行身份验证,并将sessionid
cookie 保存到他们的浏览器中。
您可以用之前创建的超级用户来测试它。
从 GitHub 获取该方法的完整代码: django_react_templates 。
单独提供前端服务(同一域)
使用这种方法,我们将构建前端,并在同一个域中独立于 Django 应用程序提供它。我们将使用 Docker 和 Nginx 在本地同一域上提供这两个应用程序。
模板方法和这种方法的主要区别在于,我们必须在加载时手动获取 CSRF 令牌。
首先创建一个项目目录:
`$ mkdir django_react_same_origin && cd django_react_same_origin`
后端
首先,为 Django 项目创建一个名为“backend”的新目录:
`$ mkdir backend && cd backend`
接下来,创建并激活一个新的虚拟环境,安装 Django,并创建一个新的 Django 项目:
`$ python3.9 -m venv env
$ source env/bin/activate
(env)$ pip install django==3.1.4
(env)$ django-admin.py startproject djangocookieauth .`
之后,创建一个名为api
的新应用:
`(env)$ python manage.py startapp api`
在INSTALLED_APPS
下的djangookieauth/settings . py中注册 app:
`# backend/djangocookieauth/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api.apps.ApiConfig', # new
]`
我们的应用将拥有以下 API 端点:
- 将生成一个 CSRF 令牌并以 JSON 的形式返回
/api/login/
允许用户通过提供用户名和密码登录/api/logout/
注销用户/api/session/
检查会话是否存在/api/whoami/
获取已验证用户的用户数据
对于视图,在这里获取完整代码,并将其添加到后端/api/views.py 文件中。
向“后端/api”添加一个 urls.py 文件,并定义以下特定于应用程序的 URL:
`# backend/api/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('csrf/', views.get_csrf, name='api-csrf'),
path('login/', views.login_view, name='api-login'),
path('logout/', views.logout_view, name='api-logout'),
path('session/', views.session_view, name='api-session'),
path('whoami/', views.whoami_view, name='api-whoami'),
]`
现在,让我们将我们的应用程序 URL 注册到基础项目:
`# backend/djangocookieauth/urls.py
from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include # new import
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')), # new
]`
让我们更改一下back end/djangookieauth/settings . py中的一些安全设置:
`CSRF_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_HTTPONLY = True
# PROD ONLY
# CSRF_COOKIE_SECURE = True
# SESSION_COOKIE_SECURE = True`
注意事项:
- 启用
CSRF_COOKIE_SAMESITE
和SESSION_COOKIE_SAMESITE
可防止任何外部请求发送 cookies 和 CSRF 令牌。 - 启用
CSRF_COOKIE_HTTPONLY
和SESSION_COOKIE_HTTPONLY
会阻止客户端 JavaScript 访问 CSRF 和会话 cookies。
如果你在制作中,你应该通过 HTTPS 服务你的网站并启用
CSRF_COOKIE_SECURE
和SESSION_COOKIE_SECURE
,这将只允许 cookies 通过 HTTPS 发送。
创建一个后端/需求. txt 文件:
前端
在你开始前端工作之前,确保你已经安装了 Node.js 和 npm (或者 Yarn )。
我们将使用 Create React App 来搭建一个新的 React 项目。
从项目根目录运行:
`$ npx create-react-app frontend
$ cd frontend
$ npm start`
这将在端口 3000 上启动我们的应用程序。访问 http://localhost:3000 以确保其工作正常:
您可以通过删除所有文件和文件夹来简化前端,除了:
`├── README.md
├── node_modules
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
└── src
├── App.js
├── index.css
└── index.js`
接下来,让我们添加 Bootstrapfrontend/public/index . html:
`<!-- frontend/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
<!-- new -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" crossorigin="anonymous">
<!-- end of new -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>`
抓取App
组件的完整代码在这里,并将其添加到 frontend/src/App.js 文件中。
这只是一个简单的带有表单的前端应用程序,由 React state 处理。在页面加载时,compontentDidMount()
被调用,它执行两个 API 调用:
- 首先,它通过调用
/api/session/
检查用户是否被认证,并将isAuthenticated
设置为true
或false
。 - 如果用户没有通过认证,它从
/api/csrf/
获取 CSRF 令牌,并将其保存到状态。
请注意,对于每个请求,我们都使用了credentials: same-origin
。这是必需的,因为如果 URL 与调用脚本来源相同,我们希望浏览器在每个 HTTP 请求中传递 cookies。
更新 frontend/src/index.js :
`// frontend/src/index.js import React from "react"; import ReactDOM from "react-dom"; import App from "./App.js"; import "./index.css"; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") );`
码头工人
接下来,让我们对两个应用程序进行 Dockerize。
后端
`# backend/Dockerfile
# pull official base image
FROM python:3.9.0-slim-buster
# set working directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# add app
COPY . .
# start app
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]`
前端
`# frontend/Dockerfile
# pull official base image
FROM node:15.2.0-alpine
# set working directory
WORKDIR /usr/src/app
# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH
# install and cache app dependencies
COPY package.json .
COPY package-lock.json .
RUN npm ci
RUN npm install [[email protected]](/cdn-cgi/l/email-protection) -g --silent
# start app
CMD ["npm", "start"]`
Nginx
为了在同一个域上运行这两个应用程序,让我们为 Nginx 添加一个作为反向代理的容器。在项目根目录下创建一个名为“nginx”的新文件夹。
`# nginx/Dockerfile
FROM nginx:latest
COPY ./nginx.conf /etc/nginx/nginx.conf`
同样添加一个 nginx/nginx.conf 配置文件。你可以在这里找到它的代码。
注意两个位置块:
`# nginx/nginx.conf location /api { proxy_pass http://backend:8000; ... } location / { proxy_pass http://frontend:8000; ... }`
对/
的请求将被转发到 http://frontend:8000 (frontend 是 Docker Compose 文件中服务的名称,我们稍后将添加它),而对/api
的请求将被转发到 http://backend:8000 (backend 是 Docker Compose 文件中服务的名称)。
复合坞站
在项目根目录中创建一个 docker-compose.yml 文件,并添加以下内容:
`# docker-compose.yml version: '3.8' services: backend: build: ./backend volumes: - ./backend:/usr/src/app expose: - 8000 frontend: stdin_open: true build: ./frontend volumes: - ./frontend:/usr/src/app - /usr/src/app/node_modules expose: - 3000 environment: - NODE_ENV=development depends_on: - backend reverse_proxy: build: ./nginx ports: - 81:80 depends_on: - backend - frontend`
您的项目结构现在应该如下所示:
`├── backend
│ ├── Dockerfile
│ ├── api
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ ├── urls.py
│ │ └── views.py
│ ├── djangocookieauth
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── manage.py
│ └── requirements.txt
├── docker-compose.yml
├── frontend
│ ├── Dockerfile
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── manifest.json
│ │ └── robots.txt
│ └── src
│ ├── App.js
│ ├── index.css
│ └── index.js
└── nginx
├── Dockerfile
└── nginx.conf`
使用 Docker 运行
构建图像并旋转容器:
`$ docker-compose up -d --build`
如果你遇到“服务前端构建失败”,你的 package-lock.json 可能会丢失。移动到前端文件夹,运行
npm install --package-lock
生成。
运行迁移并创建超级用户:
`$ docker-compose exec backend python manage.py makemigrations
$ docker-compose exec backend python manage.py migrate
$ docker-compose exec backend python manage.py createsuperuser`
您的应用程序应该可以在: http://localhost:81 访问。通过使用您刚刚创建的超级用户登录来测试它。
从 GitHub 获取该方法的完整代码: django_react_same_origin 。
姜戈 DRF +前端分开服务(同一域)
这种方法或多或少与前面的方法“前端分开服务(同一个域)”相同。下面列出了一些小的区别。
当使用这种方法时,你必须使用 pip 安装djangorestframework
或将其添加到 requirements.txt (如果用 Docker 构建)。安装后,你需要在设置中的INSTALLED_APPS
下注册它。
`# djangocookieauth/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api.apps.ApiConfig',
'rest_framework', # new
]`
要启用SessionAuthentication
,您必须将以下内容添加到您的设置中。py :
`# backend/djangocookieauth/settings.py
# Django REST framework
# https://www.django-rest-framework.org/api-guide/settings/
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
}`
还建议将
JSONRenderer
设置为DEFAULT_RENDERER_CLASSES
来禁用 DRF 导航和那个花哨的显示。
创建 session 和 whoami 视图时,使用从rest_framework
导入的APIView
,并显式设置authentication_classes
和permission_classes
:
`# backend/api/views.py
from django.http import JsonResponse
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
class SessionView(APIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [IsAuthenticated]
@staticmethod
def get(request, format=None):
return JsonResponse({'isAuthenticated': True})
class WhoAmIView(APIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [IsAuthenticated]
@staticmethod
def get(request, format=None):
return JsonResponse({'username': request.user.username})`
注册 URL 时,请像这样注册:
`# backend/api/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('csrf/', views.get_csrf, name='api-csrf'),
path('login/', views.login_view, name='api-login'),
path('logout/', views.logout_view, name='api-logout'),
path('session/', views.SessionView.as_view(), name='api-session'), # new
path('whoami/', views.WhoAmIView.as_view(), name='api-whoami'), # new
]`
从 GitHub 获取该方法的完整代码:django _ react _ drf _ same _ origin。
单独提供前端服务(跨域)
使用这种方法,我们将构建前端,并在不同的域上独立于 Django 应用程序提供它。我们将不得不通过使用 django-cors-headers 允许来自前端的跨域请求来稍微放宽安全性。
首先创建一个项目目录:
`$ mkdir django_react_cross_origin && cd django_react_cross_origin`
后端
首先,为 Django 项目创建一个名为“backend”的新目录:
`$ mkdir backend && cd backend`
接下来,创建并激活一个新的虚拟环境,安装 Django,并创建一个新的 Django 项目:
`$ python3.9 -m venv env
$ source env/bin/activate
(env)$ pip install django==3.1.4
(env)$ django-admin.py startproject djangocookieauth .`
之后,创建一个名为api
的新应用:
`(env)$ python manage.py startapp api`
在INSTALLED_APPS
下的back end/djangookieauth/settings . py中注册 app:
`# backend/djangocookieauth/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api.apps.ApiConfig', # new
]`
我们的应用将拥有以下 API 端点:
- 将生成一个 CSRF 令牌并以 JSON 的形式返回
/api/login/
允许用户通过提供用户名和密码登录/api/logout/
注销用户/api/session/
检查会话是否存在/api/whoami/
获取已验证用户的用户数据
对于视图,在这里获取完整代码,并将其添加到后端/api/views.py 文件中。
向“api”添加一个 urls.py 文件,并定义以下 URL:
`# backend/api/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('csrf/', views.get_csrf, name='api-csrf'),
path('login/', views.login_view, name='api-login'),
path('logout/', views.logout_view, name='api-logout'),
path('session/', views.session_view, name='api-session'),
path('whoami/', views.whoami_view, name='api-whoami'),
]`
现在,让我们将我们的应用程序 URL 注册到基础项目:
`# backend/djangocookieauth/urls.py
from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include # new import
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')), # new
]`
现在,我们必须放松一些安全设置,以便我们的请求能够通过。让我们首先在back end/djangookieauth/settings . py中设置我们的 cookie 设置:
`CSRF_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_HTTPONLY = True
# PROD ONLY
# CSRF_COOKIE_SECURE = True
# SESSION_COOKIE_SECURE = True`
注意事项:
- 将
CSRF_COOKIE_SAMESITE
和SESSION_COOKIE_SAMESITE
设置为Lax
允许我们在外部请求中发送 CSRF cookie。 - 启用
CSRF_COOKIE_HTTPONLY
和SESSION_COOKIE_HTTPONLY
会阻止客户端 JavaScript 访问 CSRF 和会话 cookies。
如果你在制作中,你应该通过 HTTPS 服务你的网站并启用
CSRF_COOKIE_SECURE
和SESSION_COOKIE_SECURE
,这将只允许 cookies 通过 HTTPS 发送。
为了允许跨源 cookie 保存,我们还需要更改一些 CORS 设置。为此我们将使用django-cors-headers
。让我们从使用以下命令安装它开始:
`(env)$ pip install django-cors-headers==3.5.0`
将它添加到您已安装的应用程序中,并添加一个新的中间件类:
`# backend/djangocookieauth/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api.apps.ApiConfig',
'corsheaders', # new
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # new
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]`
配置 CORS:
`# backend/djangocookieauth/settings.py
CORS_ALLOWED_ORIGINS = [
'http://localhost:3000',
'http://127.0.0.1:3000',
]
CORS_EXPOSE_HEADERS = ['Content-Type', 'X-CSRFToken']
CORS_ALLOW_CREDENTIALS = True`
注意事项:
- 我们在
CORS_ALLOWED_ORIGINS
列表中设置了允许的原点。值得注意的是,出于测试目的,不使用CORS_ALLOWED_ORIGINS
设置,而是将CORS_ALLOW_ALL_ORIGIN
设置为True
,以便允许任何来源发出请求。但是不要在生产中使用它。 CORS_EXPOSE_HEADERS
是暴露给浏览器的 HTTP 头列表。- 将
CORS_ALLOW_CREDENTIALS
设置为True
允许 cookies 随跨来源请求一起发送。
我们后端的代码现在差不多完成了。让我们运行 migrate 命令并创建一个超级用户,以便将来进行测试:
`(env)$ python manage.py migrate
(env)$ python manage.py createsuperuser`
前端
在你开始前端工作之前,确保你已经安装了 Node.js 和 npm (或者 Yarn )。
我们将使用 Create React App 来搭建一个新的 React 项目。
从项目根目录运行:
`$ npx create-react-app frontend
$ cd frontend
$ npm start`
这将在端口 3000 上启动我们的应用程序。访问 http://localhost:3000 以确保其工作正常:
您可以通过删除所有文件和文件夹来简化前端,除了:
`├── README.md
├── node_modules
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
└── src
├── App.js
├── index.css
└── index.js`
接下来,让我们添加 Bootstrapfrontend/public/index . html:
`<!-- frontend/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
<!-- new -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" crossorigin="anonymous">
<!-- end of new -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>`
抓取App
组件的完整代码在这里,并将其添加到 frontend/src/App.js 文件中。
这只是一个简单的带有表单的前端应用程序,由 React state 处理。在页面加载时,compontentDidMount()
被调用,它执行两个 API 调用:
- 首先,它通过调用
/api/session/
检查用户是否被认证,并将isAuthenticated
设置为true
或false
。 - 如果用户没有通过认证,它从
/api/csrf/
获取 CSRF 令牌,并将其保存到状态。
请注意,对于每个请求,我们都使用了credentials: include
。这是必需的,因为我们希望浏览器通过每个 HTTP 请求传递 cookies,即使 URL 与调用脚本的来源不同。请记住,我们在后端更改了一些 CORS 设置以允许这样做。
更新 frontend/src/index.js :
`// frontend/src/index.js import React from "react"; import ReactDOM from "react-dom"; import App from "./App.js"; import "./index.css"; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") );`
运行应用程序
移动到您的后端文件夹,使用以下命令运行 Django:
`(env)$ python manage.py runserver`
您的后端应该可以访问: http://localhost:8000 。
打开一个新的终端窗口,导航到“frontend”文件夹并启动 React with npm:
您应该能够在 http://localhost:3000 访问您的应用程序。
通过使用您之前创建的超级用户登录来测试您的应用程序。
从 GitHub 获取该方法的完整代码: django_react_same_origin
结论
本文详细介绍了如何使用 Django 和 React 为单页面应用程序设置基于会话的身份验证。无论您使用会话 cookie 还是令牌,当客户端是浏览器时,最好使用 cookie 进行身份验证。虽然最好从同一个域提供这两个应用程序,但您可以通过放宽跨域安全设置,在不同的域上提供它们。
我们研究了将 Django 与前端框架结合起来处理基于会话的授权的四种不同方法:
方法 | 前端 | 后端 |
---|---|---|
Django 提供前台服务 | 使用universal-cookies 获取 CSRF 令牌,并在请求中使用credentials: "same-origin" 。 |
将CSRF_COOKIE_SAMESITE 、SESSION_COOKIE_SAMESITE 设置为"Strict" 。启用SESSION_COOKIE_HTTPONLY ,禁用CSRF_COOKIE_HTTPONLY 。 |
单独提供前端服务(同一域) | 获取 CSRF 令牌并在获取请求中使用credentials: "same-origin" 。 |
添加一个路由处理程序,用于生成在响应头中设置的 CSRF 令牌。将SESSION_COOKIE_HTTPONLY 、CSRF_COOKIE_HTTPONLY 设置为True ,将SESSION_COOKIE_SAMESITE 、CSRF_COOKIE_SAMESITE 设置为"Strict" 。 |
前端与 DRF 分开服务(同一域) | 获取 CSRF 令牌并在获取请求中使用credentials: "same-origin" 。 |
添加一个路由处理程序,用于生成在响应头中设置的 CSRF 令牌。将SESSION_COOKIE_HTTPONLY 、CSRF_COOKIE_HTTPONLY 设置为True ,将SESSION_COOKIE_SAMESITE 、CSRF_COOKIE_SAMESITE 设置为"Strict" 。 |
单独提供前端服务(跨来源) | 获取 CSRF 令牌并在获取请求中使用credentials: "include" 。 |
启用 CORS 并添加路由处理程序,以生成在响应标头中设置的 CSRF 令牌。将SESSION_COOKIE_HTTPONLY 、CSRF_COOKIE_HTTPONLY 设置为True ,将SESSION_COOKIE_SAMESITE 、CSRF_COOKIE_SAMESITE 设置为"Lax" 。添加 django-cors-headers 包并配置CORS_ALLOWED_ORIGINS 、CORS_EXPOSE_HEADERS 和CORS_ALLOW_CREDENTIALS 设置。 |
从 django-spa-cookie-auth 存储库中获取代码。
在 Django 中使用静态和媒体文件
本文着眼于如何在 Django 项目中,在本地和生产中处理静态和媒体文件。
目标
完成本文后,您将能够:
- 描述你通常会在 Django 项目中找到的三种不同类型的文件
- 解释静态文件和媒体文件的区别
- 在本地和生产中处理静态和媒体文件
文件类型
Django 是一个固执己见的全栈 web 应用框架。它配有许多电池,你可以用它们来构建一个全功能的网络应用,包括静态和媒体文件管理。
在我们看如何之前,我们先来看一些定义。
什么是静态和媒体文件?
首先,您通常会在 Django 项目中找到这三种类型的文件:
- 源代码:这些是组成每个 Django 项目的核心 Python 模块和 HTML 文件,您可以在其中定义模型、视图和模板。
- 静态文件:这些是你的 CSS 样式表、JavaScript 文件、字体和图片。由于不涉及任何处理,这些文件非常节能,因为它们可以按原样提供。它们也更容易缓存。
- 媒体文件:这些是用户上传的文件。
本文主要关注静态和媒体文件。尽管名称不同,但两者都表示常规文件。最大的区别是静态文件保存在版本控制中,并在部署过程中与源代码文件一起发布。另一方面,媒体文件是您的最终用户(内部和外部)上传或由您的应用程序动态创建的文件(通常是一些用户操作的副作用)。
为什么要区别对待静态文件和媒体文件?
- 您不能信任最终用户上传的文件,因此媒体文件需要区别对待。
- 您可能需要对用户上传的媒体文件进行处理,以便更好地提供服务,例如,您可以优化上传的图像以支持不同的设备。
- 您不希望用户上传的文件意外地替换了静态文件。
附加说明:
- 静态和媒体文件有时被称为静态和媒体资产。
- Django admin 附带了一些静态文件,它们存储在 GitHub 的版本控制中。
- 更容易混淆静态文件和媒体文件的是,Django 文档本身并没有很好地区分这两者。
静态文件
Django 为处理静态文件提供了强大的功能,被恰当地命名为 staticfiles 。
如果您是 staticfiles 应用程序的新手,请快速浏览 Django 文档中的如何管理静态文件(例如,图像、JavaScript、CSS) 指南。
Django 的 staticfiles 应用程序提供了以下核心组件:
- 设置
- 管理命令
- 存储类别
- 模板标签
设置
根据您的环境,您可能需要配置许多设置:
- STATIC_URL :用户可以在浏览器中访问您的静态文件的 URL。默认值是
/static/
,这意味着您的文件将在开发模式下的http://127.0.0.1:8000/static/
可用——例如http://127.0.0.1:8000/static/css/main.css
。 - STATIC _ ROOT:Django 应用程序服务静态文件的目录的绝对路径。当您运行 collectstatic 管理命令时(稍后会有更多介绍),它会找到所有静态文件并将它们复制到这个目录中。
- STATICFILES_DIRS :默认情况下,静态文件在
<APP_NAME>/static/
存储在 app 级。collectstatic 命令将在这些目录中查找静态文件。您还可以用STATICFILES_DIRS
告诉 Django 在其他位置寻找静态文件。 - STATICFILES_STORAGE :您想要使用的文件存储类,它控制静态文件的存储和访问方式。文件通过静态文件存储存储在文件系统中。
- STATICFILES_FINDERS :该设置定义了用于自动查找静态文件的文件查找器后端。默认情况下,使用
FileSystemFinder
和AppDirectoriesFinder
查找器:FileSystemFinder
-使用STATICFILES_DIRS
设置查找文件。AppDirectoriesFinder
-在项目内的每个 Django 应用程序的“静态”文件夹中查找文件。
管理命令
staticfiles 应用程序提供了以下管理命令:
collectstatic
是一个管理命令,从不同的位置收集静态文件-即<APP_NAME>/static/
和在STATICFILES_DIRS
设置中找到的目录-并将它们复制到STATIC_ROOT
目录。findstatic
是调试时使用的一个非常有用的命令,因此您可以确切地看到特定文件的来源- 启动一个轻量级的开发服务器来运行开发中的 Django 应用程序。
注意事项:
- 不要将任何静态文件放在
STATIC_ROOT
目录中。运行collectstatic
后,静态文件会自动复制到那里。相反,总是将它们放在与STATICFILES_DIRS
设置或<APP_NAME>/static/
相关的目录中。 - 不要在生产中使用开发服务器。请改用生产级的 WSGI 应用服务器。稍后会有更多内容。
findstatic
命令的快速示例:假设你有两个 Django 应用,
app1
和app2
。每个应用程序都有一个名为“静态”的文件夹,在每个文件夹中,都有一个名为 app.css 的文件。来自 settings.py 的相关设置:`STATIC_ROOT = 'staticfiles' INSTALLED_APPS = [ ... 'app1', 'app2', ]`
运行
python manage.py collectstatic
时,将创建“staticfiles”目录,并将适当的静态文件复制到其中:`$ ls staticfiles/ admin app.css`
只有一个 app.css 文件,因为当存在多个同名文件时,staticfiles finder 将使用第一个找到的文件。要查看哪个文件被复制,您可以使用
findstatic
命令:`$ python manage.py findstatic app.css Found 'app.css' here: /app1/static/app.css /app2/static/app.css`
由于只收集第一个遇到的文件,要检查复制到“staticfiles”目录的 app.css 的源代码,请运行:
`$ python manage.py findstatic app.css --first Found 'app.css' here: /app1/static/app.css`
存储类别
当运行collectstatic
命令时,Django 使用存储类来决定如何存储和访问静态文件。同样,这是通过 STATICFILES_STORAGE 设置来配置的。
默认的存储类是静态文件存储。在后台,StaticFilesStorage
使用 FileSystemStorage 类在本地文件系统上存储文件。
您可能希望在生产中偏离默认设置。例如, django-storages 为不同的云/CDN 提供商提供了一些定制的存储类。您还可以使用文件存储 API 编写自己的存储类。查看云服务或 CDN 提供的静态文件,了解更多相关信息。
存储类可用于执行后处理任务,如缩小。
要在模板文件中加载静态文件,您需要:
- 将
{% load static %}
添加到模板文件的顶部 - 然后,为您想要链接的每个文件添加
{% static %}
模板标签
例如:
`{% load static %}
<link rel="stylesheet" href="{% static 'base.css' %}">`
这些标签共同生成一个完整的 URL——例如,/static/base.css
——基于 settings.py 文件中的静态文件配置。
您应该总是以这种方式加载静态文件,而不是直接硬编码 URL,这样您就可以更改您的静态文件配置并指向不同的STATIC_URL
,而不必手动更新每个模板。
关于这些模板标签的更多信息,请查看内置模板标签和过滤器中的静态部分。
开发模式下的静态文件
在开发过程中,只要你将DEBUG
设置为TRUE
并且使用 staticfiles 应用,你就可以使用 Django 的开发服务器提供静态文件。你甚至不需要运行collecstatic
命令。
典型开发配置:
`# settings.py
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static',]
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'`
生产中的静态文件
在生产中处理静态文件并不像您的开发环境那样容易,因为您将使用 WSGI(如 Gunicorn )或 ASGI(如 Uvicorn )兼容的 web 应用服务器,它们用于提供动态内容——即您的 Django 源代码文件。
在生产中有许多不同的方法来处理静态文件,但是最流行的两种方法是:
- 使用像 Nginx 这样的 web 服务器将静态文件的流量直接路由到静态根目录(通过
STATIC_ROOT
配置) - 使用whiten noise直接从 WSGI 或 ASGI web 应用服务器提供静态文件
不管选择哪一种,您可能都希望利用一个 CDN 。
有关这些选项的更多信息,请查看如何部署静态文件。
Nginx
Nginx 配置示例:
`upstream hello_django { server web:8000; } server { listen 80; location / { proxy_pass http://hello_django; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_redirect off; } location /static/ { alias /home/app/web/staticfiles/; } }`
简而言之,当一个请求被发送到/static/
——例如/static/base.css
——Nginx 将尝试从“/home/app/web/staticfiles/”文件夹中提供文件。
好奇上面的 Nginx 配置是如何工作的?查看使用 Postgres、Gunicorn 和 Nginx 的教程。
其他资源:
- 更喜欢把你的静态文件存储在亚马逊 S3 上?查看在亚马逊 S3 上存储 Django 静态和媒体文件的。
- 更喜欢将您的静态文件存储在数字海洋空间上?查看在数字海洋空间存储 Django 静态和媒体文件。
白噪声
您可以使用whiten noise从 WSGI 或 ASGI web 应用服务器提供静态文件。
最基本的设置很简单。安装完软件包后,将 WhiteNoise 添加到除了django.middleware.security.SecurityMiddleware
之外的所有其他中间件之上的MIDDLEWARE
列表中:
`MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # <---- WhiteNoise!
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]`
然后,为了支持压缩和缓存,像这样更新STATICFILES_STORAGE
:
`STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'`
就是这样!关闭调试模式,运行collectstatic
命令,然后运行 WSGI 或 ASGI web 应用服务器。
有关配置 WhiteNoise 与 Django 配合使用的更多信息,请参考结合 Django 使用 white noise指南。
同样,媒体文件是您的最终用户(内部和外部)上传或由您的应用程序动态创建的文件(通常是一些用户操作的副作用)。它们通常不在版本控制中。
几乎总是,与文件字段或图像字段模型字段相关联的文件应该被视为媒体文件。
与静态文件一样,媒体文件的处理在 settings.py 文件中配置。
处理媒体文件的基本配置设置:
- MEDIA_URL :类似于
STATIC_URL
,这是用户可以访问媒体文件的 URL。 - MEDIA _ ROOT:Django 应用程序提供媒体文件的目录的绝对路径。
- DEFAULT_FILE_STORAGE :您想要使用的文件存储类,它控制媒体文件的存储和访问方式。默认为文件系统存储。
典型开发配置:
`MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'uploads'`
不幸的是,Django 开发服务器默认不提供媒体文件。幸运的是,有一个非常简单的解决方法:您可以将媒体根作为静态路径添加到项目级 URL 中的ROOT_URLCONF
。
示例:
`from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# ... the rest of your URLconf goes here ...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)`
当涉及到在生产中处理媒体文件时,您的选择比静态文件少,因为您不能使用 WhiteNoise 来提供媒体文件。因此,你通常会希望使用 Nginx 和 django-storages 来存储本地文件系统之外的媒体文件,应用程序在本地文件系统中运行。
Nginx 配置示例:
`upstream hello_django { server web:8000; } server { listen 80; location / { proxy_pass http://hello_django; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_redirect off; } location /media/ { alias /home/app/web/mediafiles/; } }`
因此,当一个请求被发送到/media/
——例如/media/upload.png
——Nginx 将尝试从“/home/app/web/mediafiles/”文件夹中提供文件。
好奇上面的 Nginx 配置是如何工作的?查看使用 Postgres、Gunicorn 和 Nginx 的教程。
其他资源:
- 更喜欢把你的媒体文件储存在亚马逊 S3 上?查看在亚马逊 S3 上存储 Django 静态和媒体文件的。
- 更喜欢将您的媒体文件存储在 DigitalOcean Spaces 上?查看在数字海洋空间存储 Django 静态和媒体文件。
结论
静态文件和媒体文件是不同的,为了安全起见,必须区别对待。
在本文中,您看到了如何在开发和生产中提供静态和媒体文件的示例。此外,文章还涵盖了:
- 两种类型文件的不同设置
- Django 如何用最少的配置处理它们
您可以在这里找到一个简单的 Django 项目,其中包含为开发和生产中的静态文件以及开发中的媒体文件提供服务的示例。
本文只带您了解如何在 Django 中处理静态和媒体文件。它没有讨论静态文件的预处理/后处理,如缩小和捆绑。对于这样的任务,你必须使用像 Rollup 、packet或者 webpack 这样的工具来建立复杂的构建过程。
Django 条纹订阅
本教程着眼于如何用 Django 和 Stripe 处理订阅支付。
需要接受一次性付款?查看 Django 条纹教程。
条带订阅支付选项
有多种方法可以实现和处理条带订阅,但最常见的两种方法是:
在这两种情况下,您都可以使用 Stripe Checkout (这是一个 Stripe 托管的 Checkout 页面)或 Stripe Elements (这是一组用于构建支付表单的定制 UI 组件)。如果您不介意将您的用户重定向到 Stripe 托管的页面,并希望 Stripe 为您处理大部分支付流程(例如,创建客户和支付意向等),请使用 Stripe Checkout。),否则使用条纹元素。
固定价格方法更容易建立,但是你不能完全控制计费周期和支付。通过使用这种方法,Stripe 将在成功结账后的每个结算周期自动开始向您的客户收费。
固定价格步骤:
- 将用户重定向至条带检验(使用
mode=subscription
) - 创建一个监听
checkout.session.completed
的网络钩子 - 调用 webhook 后,将相关数据保存到数据库中
未来付款方式更难设置,但这种方式可以让您完全控制订阅。您提前收集客户详细信息和付款信息,并在未来某个日期向客户收费。这种方法还允许您同步计费周期,以便您可以在同一天向所有客户收费。
未来付款步骤:
- 将用户重定向到条带结帐(使用
mode=setup
)以收集支付信息 - 创建一个监听
checkout.session.completed
的网络钩子 - 调用 webhook 后,将相关数据保存到数据库中
- 从那里,您可以在将来使用付款意向 API 对付款方式收费
在本教程中,我们将使用带条纹结帐的固定价格方法。
计费周期
在开始之前,值得注意的是 Stripe 没有默认的计费频率。每个条带订阅的记账日期由以下两个因素决定:
- 计费周期锚点(订阅创建的时间戳)
- 重复间隔(每天、每月、每年等。)
例如,每月订阅设置为在每月 2 日循环的客户将始终在 2 日计费。
如果一个月没有锚定日,订阅将在该月的最后一天计费。例如,从 1 月 31 日开始的订阅在 2 月 28 日(或闰年的 2 月 29 日)计费,然后是 3 月 31 日、4 月 30 日等等。
要了解有关计费周期的更多信息,请参考 Stripe 文档中的设置订阅计费周期日期页面。
项目设置
让我们首先为我们的项目创建一个新目录。在目录中,我们将创建并激活一个新的虚拟环境,安装 Django,并使用 django-admin 创建一个新的 Django 项目:
`$ mkdir django-stripe-subscriptions && cd django-stripe-subscriptions
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install django
(env)$ django-admin startproject djangostripe .`
之后,创建一个名为subscriptions
的新应用:
`(env)$ python manage.py startapp subscriptions`
在INSTALLED_APPS
下的 djangostripe/settings.py 中注册 app:
`# djangostripe/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'subscriptions.apps.SubscriptionsConfig', # new
]`
创建一个名为home
的新视图,它将作为我们的主索引页面:
`# subscriptions/views.py
from django.shortcuts import render
def home(request):
return render(request, 'home.html')`
通过向 subscriptions/urls.py 添加以下内容,为视图分配一个 URL:
`# subscriptions/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='subscriptions-home'),
]`
现在,让我们告诉 Django,subscriptions
应用程序在主应用程序中有自己的 URL:
`# djangostripe/urls.py
from django.contrib import admin
from django.urls import path, include # new
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('subscriptions.urls')), # new
]`
最后,在名为“模板”的新文件夹中创建一个名为home.html的新模板。将以下 HTML 添加到模板中:
`<!-- templates/home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Subscriptions</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
</div>
</body>
</html>`
确保更新 settings.py 文件,以便 Django 知道要查找“模板”文件夹:
`# djangostripe/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates'], # new
...`
最后运行migrate
来同步数据库,运行runserver
来启动 Django 的本地 web 服务器。
`(env)$ python manage.py migrate
(env)$ python manage.py runserver`
在您选择的浏览器中访问 http://localhost:8000/ 。您应该看到“Subscribe”按钮,我们稍后将使用该按钮将客户重定向到 Stripe Checkout 页面。
添加条纹
准备好基础项目后,让我们添加 Stripe。安装最新版本:
`(env)$ pip install stripe`
接下来,注册一个 Stipe 账户的(如果你还没有注册的话)并导航到仪表板。单击“Developers ”,然后从左侧栏的列表中单击“API keys ”:
每个条带帐户有四个 API 密钥:两个用于测试,两个用于生产。每一对都有一个“秘密密钥”和一个“可公开密钥”。不要向任何人透露密钥;可发布的密钥将被嵌入到任何人都可以看到的页面上的 JavaScript 中。
目前右上角的“查看测试数据”开关表示我们正在使用测试键。这就是我们想要的。
在你的 settings.py 文件的底部,添加下面两行,包括你自己的测试秘密和可发布的密钥。确保在实际的键周围包含''
字符。
`# djangostripe/settings.py
STRIPE_PUBLISHABLE_KEY = '<enter your stripe publishable key>'
STRIPE_SECRET_KEY = '<enter your stripe secret key>'`
最后,您需要在https://dashboard.stripe.com/settings/account的“帐户设置”中指定一个“帐户名称”。
创造产品
接下来,让我们创建一个要销售的订阅产品。
单击“产品”,然后单击“添加产品”。
添加产品名称和描述,输入价格,然后选择“重复”:
点击“保存产品”。
接下来,获取价格的 API ID:
将 ID 保存在 settings.py 文件中,如下所示:
`# djangostripe/settings.py
STRIPE_PRICE_ID = '<enter your stripe price id>'`
证明
为了将 Django 用户与 Stripe 客户相关联,并在将来实现订阅管理,我们需要在允许客户订阅服务之前强制执行用户身份验证。我们可以通过向所有需要认证的视图添加一个@login_required
装饰器来实现这一点。
先来保护一下home
视图:
`# subscriptions/views.py
from django.contrib.auth.decorators import login_required # new
from django.shortcuts import render
@login_required # new
def home(request):
return render(request, 'home.html')`
现在,当未经认证的用户试图访问home
视图时,他们将被重定向到 settings.py 中定义的LOGIN_REDIRECT_URL
。
如果您有首选的身份验证系统,现在就设置并配置LOGIN_REDIRECT_URL
,否则跳到下一部分安装 django-allauth 。
django-allauth(可选)
django-allauth 是最流行的 django 包之一,用于解决认证、注册、帐户管理和第三方帐户认证。我们将使用它来配置一个简单的注册/登录系统。
首先,安装软件包:
`(env)$ pip install django-allauth`
像这样更新 djangostripe/settings.py 中的INSTALLED_APPS
:
`# djangostripe/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites', # new
'allauth', # new
'allauth.account', # new
'allauth.socialaccount', # new
'subscriptions.apps.SubscriptionsConfig',
]`
接下来,将以下 django-allauth 配置添加到 djangostripe/settings.py :
`# djangostripe/settings.py
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',
# `allauth` specific authentication methods, such as login by e-mail
'allauth.account.auth_backends.AuthenticationBackend',
]
# We have to set this variable, because we enabled 'django.contrib.sites'
SITE_ID = 1
# User will be redirected to this page after logging in
LOGIN_REDIRECT_URL = '/'
# If you don't have an email server running yet add this line to avoid any possible errors.
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'`
注册 allauth URLs:
`# djangostripe/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('subscriptions.urls')),
path('accounts/', include('allauth.urls')), # new
]`
应用迁移:
`(env)$ python manage.py migrate`
通过运行服务器并导航到http://localhost.com:8000/来测试 auth。您应该会被重定向到注册页面。创建一个帐户,然后登录。
数据库模型
为了正确处理客户和订阅,我们需要在数据库中存储一些信息。让我们创建一个名为StripeCustomer
的新模型,它将存储 Stripe 的customerId
和subscriptionId
,并将其关联回 Django auth 用户。这将允许我们从 Stripe 获取我们的客户和订阅数据。
理论上,我们可以在每次需要时从 Stripe 中获取
customerId
和subscriptionId
,但这将极大地增加我们被 Stripe 限制速率的机会。
让我们在 subscriptions/models.py 中创建我们的模型:
`# subscriptions/models.py
from django.contrib.auth.models import User
from django.db import models
class StripeCustomer(models.Model):
user = models.OneToOneField(to=User, on_delete=models.CASCADE)
stripeCustomerId = models.CharField(max_length=255)
stripeSubscriptionId = models.CharField(max_length=255)
def __str__(self):
return self.user.username`
在 subscriptions/admin.py 中向管理员注册:
`# subscriptions/admin.py
from django.contrib import admin
from subscriptions.models import StripeCustomer
admin.site.register(StripeCustomer)`
创建和应用迁移:
`(env)$ python manage.py makemigrations && python manage.py migrate`
获取可发布密钥
JavaScript 静态文件
首先创建一个新的静态文件来保存我们所有的 JavaScript:
`(env)$ mkdir static
(env)$ touch static/main.js`
向新的 main.js 文件添加快速健全检查:
`// static/main.js console.log("Sanity check!");`
然后更新 settings.py 文件,这样 Django 就知道在哪里可以找到静态文件:
`# djangostripe/settings.py
STATIC_URL = 'static/'
# for django >= 3.1
STATICFILES_DIRS = [Path(BASE_DIR).joinpath('static')] # new
# for django < 3.1
# STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # new`
在 HTML 模板中添加静态模板标签和新的脚本标签:
`<!-- templates/home.html -->
{% load static %} <!-- new -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Subscriptions</title>
<script src="https://js.stripe.com/v3/"></script> <!-- new -->
<script src="{% static 'main.js' %}"></script> <!-- new -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
</div>
</body>
</html>`
再次运行开发服务器。导航到 http://localhost:8000/ ,打开 JavaScript 控制台。您应该会看到控制台内部的健全性检查。
视角
接下来,向 subscriptions/views.py 添加一个新视图来处理 AJAX 请求:
`# subscriptions/views.py
from django.conf import settings # new
from django.contrib.auth.decorators import login_required
from django.http.response import JsonResponse # new
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt # new
@login_required
def home(request):
return render(request, 'home.html')
# new
@csrf_exempt
def stripe_config(request):
if request.method == 'GET':
stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
return JsonResponse(stripe_config, safe=False)`
也添加一个新的 URL:
`# subscriptions/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='subscriptions-home'),
path('config/', views.stripe_config), # new
]`
AJAX 请求
接下来,使用获取 API 向 static/main.js 中的新/config/
端点发出 AJAX 请求:
`// static/main.js console.log("Sanity check!"); // new // Get Stripe publishable key fetch("/config/") .then((result) => { return result.json(); }) .then((data) => { // Initialize Stripe.js const stripe = Stripe(data.publicKey); });`
来自fetch
请求的响应是一个可读流。result.json()
返回一个承诺,我们将它解析为一个 JavaScript 对象——即data
。然后我们使用点符号来访问publicKey
以获得可发布的密钥。
创建签出会话
接下来,我们需要将一个事件处理程序附加到按钮的 click 事件,该事件将向服务器发送另一个 AJAX 请求,以生成一个新的结帐会话 ID。
视角
首先,添加新视图:
`# subscriptions/views.py
@csrf_exempt
def create_checkout_session(request):
if request.method == 'GET':
domain_url = 'http://localhost:8000/'
stripe.api_key = settings.STRIPE_SECRET_KEY
try:
checkout_session = stripe.checkout.Session.create(
client_reference_id=request.user.id if request.user.is_authenticated else None,
success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
cancel_url=domain_url + 'cancel/',
payment_method_types=['card'],
mode='subscription',
line_items=[
{
'price': settings.STRIPE_PRICE_ID,
'quantity': 1,
}
]
)
return JsonResponse({'sessionId': checkout_session['id']})
except Exception as e:
return JsonResponse({'error': str(e)})`
这里,如果请求方法是GET
,我们定义了一个domain_url
,将条带密钥分配给stripe.api_key
(因此当我们请求创建一个新的签出会话时,它将被自动发送),创建了签出会话,并在响应中发回了 ID。注意success_url
和cancel_url
。在成功支付或取消的情况下,用户将分别被重定向回这些 URL。我们将很快设置这些视图。
不要忘记重要的一点:
完整的文件现在应该如下所示:
`# subscriptions/views.py
import stripe
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http.response import JsonResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
@login_required
def home(request):
return render(request, 'home.html')
@csrf_exempt
def stripe_config(request):
if request.method == 'GET':
stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
return JsonResponse(stripe_config, safe=False)
@csrf_exempt
def create_checkout_session(request):
if request.method == 'GET':
domain_url = 'http://localhost:8000/'
stripe.api_key = settings.STRIPE_SECRET_KEY
try:
checkout_session = stripe.checkout.Session.create(
client_reference_id=request.user.id if request.user.is_authenticated else None,
success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
cancel_url=domain_url + 'cancel/',
payment_method_types=['card'],
mode='subscription',
line_items=[
{
'price': settings.STRIPE_PRICE_ID,
'quantity': 1,
}
]
)
return JsonResponse({'sessionId': checkout_session['id']})
except Exception as e:
return JsonResponse({'error': str(e)})`
AJAX 请求
注册结帐会话 URL:
`# subscriptions/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='subscriptions-home'),
path('config/', views.stripe_config),
path('create-checkout-session/', views.create_checkout_session), # new
]`
将事件处理程序和后续 AJAX 请求添加到 static/main.js :
`// static/main.js console.log("Sanity check!"); // Get Stripe publishable key fetch("/config/") .then((result) => { return result.json(); }) .then((data) => { // Initialize Stripe.js const stripe = Stripe(data.publicKey); // new // Event handler let submitBtn = document.querySelector("#submitBtn"); if (submitBtn !== null) { submitBtn.addEventListener("click", () => { // Get Checkout Session ID fetch("/create-checkout-session/") .then((result) => { return result.json(); }) .then((data) => { console.log(data); // Redirect to Stripe Checkout return stripe.redirectToCheckout({sessionId: data.sessionId}) }) .then((res) => { console.log(res); }); }); } });`
在这里,在解析了result.json()
承诺之后,我们调用了 redirectToCheckout ,其中的结帐会话 ID 来自解析的承诺。
导航到 http://localhost:8000/ 。点击按钮后,您将被重定向到一个 Stripe Checkout 实例(一个 Stripe 托管页面,用于安全收集支付信息),其中包含订阅信息:
我们可以使用 Stripe 提供的几个测试卡号中的一个来测试表单。还是用4242 4242 4242 4242
吧。请确保到期日期在未来。为 CVC 添加任意 3 个数字,为邮政编码添加任意 5 个数字。输入任何电子邮件地址和名称。如果一切顺利,付款应该被处理,您应该订阅,但重定向将失败,因为我们还没有设置/success/
URL。
用户重定向
接下来,我们将创建成功和取消视图,并在结帐后将用户重定向到适当的页面。
视图:
`# subscriptions/views.py
@login_required
def success(request):
return render(request, 'success.html')
@login_required
def cancel(request):
return render(request, 'cancel.html')`
创建success.html和cancel.html模板。
成功:
`<!-- templates/success.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Subscriptions</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<p>You have successfully subscribed!</p>
<p><a href="{% url "subscriptions-home" %}">Return to the dashboard</a></p>
</div>
</body>
</html>`
取消:
`<!-- templates/cancel.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Subscriptions</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-5">
<p>You have cancelled the checkout.</p>
<p><a href="{% url "subscriptions-home" %}">Return to the dashboard</a></p>
</div>
</body>
</html>`
在 subscriptions/urls.py 中注册新视图:
`# subscriptions/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='subscriptions-home'),
path('config/', views.stripe_config),
path('create-checkout-session/', views.create_checkout_session),
path('success/', views.success), # new
path('cancel/', views.cancel), # new
]`
如果支付成功,用户将被重定向到/success
,如果支付失败,用户将被重定向到cancel/
。测试一下。
条纹网钩
我们的应用程序在这一点上运行良好,但我们仍然不能以编程方式确认付款。我们也没有在客户成功订阅时向StripeCustomer
模型添加新客户。我们已经在用户结帐后将他们重定向到成功页面,但我们不能只依赖该页面(作为确认),因为支付确认是异步发生的。
一般来说,在条带和编程中有两种类型的事件。同步事件,具有即时的效果和结果(例如,创建一个客户),异步事件,没有即时的结果(例如,确认付款)。因为支付确认是异步完成的,用户可能会在他们的支付被确认之前和我们收到他们的资金之前被重定向到成功页面。
当付款通过时,最简单的通知方法之一是使用回调或所谓的 Stripe webhook。我们需要在应用程序中创建一个简单的端点,每当事件发生时(例如,当用户订阅时),Stripe 将调用这个端点。通过使用 webhooks,我们可以绝对肯定支付成功。
为了使用 webhooks,我们需要:
- 设置 webhook 端点
- 使用条带 CLI 测试端点
- 用条带注册端点
端点
创建一个名为stripe_webhook
的新视图,每次有人订阅我们的服务时都会创建一个新的StripeCustomer
:
`# subscriptions/views.py
@csrf_exempt
def stripe_webhook(request):
stripe.api_key = settings.STRIPE_SECRET_KEY
endpoint_secret = settings.STRIPE_ENDPOINT_SECRET
payload = request.body
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
event = None
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError as e:
# Invalid payload
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return HttpResponse(status=400)
# Handle the checkout.session.completed event
if event['type'] == 'checkout.session.completed':
session = event['data']['object']
# Fetch all the required data from session
client_reference_id = session.get('client_reference_id')
stripe_customer_id = session.get('customer')
stripe_subscription_id = session.get('subscription')
# Get the user and create a new StripeCustomer
user = User.objects.get(id=client_reference_id)
StripeCustomer.objects.create(
user=user,
stripeCustomerId=stripe_customer_id,
stripeSubscriptionId=stripe_subscription_id,
)
print(user.username + ' just subscribed.')
return HttpResponse(status=200)`
stripe_webhook
现在作为我们的 webhook 端点。这里,我们只寻找每当结帐成功时调用的checkout.session.completed
事件,但是您可以对其他条带事件使用相同的模式。
对导入进行以下更改:
`# subscriptions/views.py
import stripe
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User # new
from django.http.response import JsonResponse, HttpResponse # updated
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from subscriptions.models import StripeCustomer # new`
要使端点可访问,剩下唯一要做的事情就是在 urls.py 中注册它:
`# subscriptions/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='subscriptions-home'),
path('config/', views.stripe_config),
path('create-checkout-session/', views.create_checkout_session),
path('success/', views.success),
path('cancel/', views.cancel),
path('webhook/', views.stripe_webhook), # new
]`
测试 webhook
我们将使用 Stripe CLI 来测试 webhook。
一旦下载并安装了,在新的终端窗口中运行以下命令,登录到您的 Stripe 帐户:
此命令应生成一个配对代码:
`Your pairing code is: peach-loves-classy-cozy
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser (^C to quit)`
通过按 Enter,CLI 将打开您的默认 web 浏览器,并请求访问您的帐户信息的权限。请继续并允许访问。回到您的终端,您应该看到类似于以下内容的内容:
`> Done! The Stripe CLI is configured for Django Test with account id acct_<ACCOUNT_ID>
Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.`
接下来,我们可以开始侦听条带事件,并使用以下命令将它们转发到我们的端点:
`$ stripe listen --forward-to localhost:8000/webhook/`
这也将生成一个 webhook 签名密码:
`> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)`
为了初始化端点,将秘密添加到 settings.py 文件中:
`# djangostribe/settings.py
STRIPE_ENDPOINT_SECRET = '<your webhook signing secret here>'`
Stripe 现在会将事件转发到我们的端点。要测试,通过4242 4242 4242 4242
运行另一个测试支付。在您的终端中,您应该会看到<USERNAME> just subscribed.
消息。
一旦完成,停止stripe listen --forward-to localhost:8000/webhook/
过程。
注册端点
最后,在部署你的应用程序后,你可以在 Stripe 仪表板中注册端点,在开发者> Webhooks 下。这将生成一个 webhook 签名密码,用于您的生产应用程序。
例如:
获取订阅数据
我们的应用程序现在允许用户订阅我们的服务,但我们仍然没有办法获取他们的订阅数据并显示出来。
更新home
视图:
`# subscriptions/views.py
@login_required
def home(request):
try:
# Retrieve the subscription & product
stripe_customer = StripeCustomer.objects.get(user=request.user)
stripe.api_key = settings.STRIPE_SECRET_KEY
subscription = stripe.Subscription.retrieve(stripe_customer.stripeSubscriptionId)
product = stripe.Product.retrieve(subscription.plan.product)
# Feel free to fetch any additional data from 'subscription' or 'product'
# https://stripe.com/docs/api/subscriptions/object
# https://stripe.com/docs/api/products/object
return render(request, 'home.html', {
'subscription': subscription,
'product': product,
})
except StripeCustomer.DoesNotExist:
return render(request, 'home.html')`
这里,如果存在一个StripeCustomer
,我们使用subscriptionId
从 Stripe API 获取客户的订阅和产品信息。
修改home.html模板,向订阅用户显示当前计划;
`<!-- templates/home.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Subscriptions</title>
<script src="https://js.stripe.com/v3/"></script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
<script src="{% static 'main.js' %}"></script>
</head>
<body>
<div class="container mt-5">
{% if subscription.status == "active" %}
<h4>Your subscription:</h4>
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">{{ product.name }}</h5>
<p class="card-text">
{{ product.description }}
</p>
</div>
</div>
{% else %}
<button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
{% endif %}
</div>
</body>
</html>`
我们的订阅客户现在将看到他们当前的订阅计划,而其他人仍将看到订阅按钮:
限制用户访问
如果您想要将对特定视图的访问限制为只有订阅的用户,那么您可以像我们在上一步中所做的那样获取订阅,并检查subscription.status == "active"
。通过执行这项检查,您将确保订阅仍然有效,这意味着它已经支付,并没有被取消。
其他可能的订阅状态有
incomplete
、incomplete_expired
、trialing
、active
、past_due
、canceled
或unpaid
。
结论
我们已经成功地创建了一个 Django web 应用程序,允许用户订阅我们的服务并查看他们的计划。我们的客户也将每月自动计费。
这只是最基本的。您仍然需要:
- 允许用户管理/取消其当前计划
- 处理未来的付款失败
您还想为domain_url
、API 密钥和 webhook 签名密码使用环境变量,而不是硬编码它们。
从 GitHub 上的django-stripe-subscriptionsrepo 中获取代码。
姜戈条纹教程
在本教程中,我将演示如何从头开始配置一个新的 Django 网站,以接受带有 Stripe 的一次性付款。
需要处理订阅付款?查看 Django Stripe 订阅。
条纹支付选项
目前有三种方式接受 Stripe 的一次性付款:
你应该用哪一个?
- 如果您想快速启动并运行,请使用 Checkout。如果你熟悉 Checkout 的旧版本模态版本,这是正确的方法。它提供了大量开箱即用的功能,支持多种语言,并包括一个实现定期支付的简单途径。最重要的是,Checkout 为您管理整个付款过程,因此您甚至无需添加任何表单就可以开始接受付款!
- 如果您希望为最终用户提供更加定制的体验,请使用付款意向 API。
虽然您仍然可以使用 Charges API,但是如果您是 Stripe 的新手,请不要使用它,因为它不支持最新的银行法规(如 SCA )。你会看到很高的下降率。如需了解更多信息,请查看官方 Stripe 文档中的费用与付款意向 API页面。
还在用收费 API?如果你的大多数客户都在美国或加拿大,你还不需要迁移。查看结帐迁移指南指南了解更多信息。
项目设置
创建一个新的项目目录以及一个名为djangostripe
的新 Django 项目:
`$ mkdir django-stripe-checkout && cd django-stripe-checkout
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install django==3.2.9
(env)$ django-admin startproject djangostripe .`
你可以随意把 virtualenv 和 Pip 换成诗歌或 Pipenv 。更多信息,请查看现代 Python 环境。
接下来,创建一个名为payments
的新应用:
`(env)$ python manage.py startapp payments`
现在将新应用添加到 settings.py 中的INSTALLED_APPS
配置中:
`# djangostripe/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Local
'payments.apps.PaymentsConfig', # new
]`
用payments
app 更新项目级 urls.py 文件:
`# djangostripe/urls.py
from django.contrib import admin
from django.urls import path, include # new
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('payments.urls')), # new
]`
在新应用中也创建一个 urls.py 文件:
`(env)$ touch payments/urls.py`
然后按如下方式填充它:
`# payments/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.HomePageView.as_view(), name='home'),
]`
现在添加一个 views.py 文件:
`# payments/views.py
from django.views.generic.base import TemplateView
class HomePageView(TemplateView):
template_name = 'home.html'`
并为我们的主页创建一个专用的“模板”文件夹和文件。
`(env)$ mkdir templates
(env)$ touch templates/home.html`
然后,添加以下 HTML:
`<!-- templates/home.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Checkout</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
</head>
<body>
<section class="section">
<div class="container">
<button class="button is-primary" id="submitBtn">Purchase!</button>
</div>
</section>
</body>
</html>`
确保更新 settings.py 文件,以便 Django 知道要查找“模板”文件夹:
`# djangostripe/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates'], # new
...`
最后运行migrate
来同步数据库,运行runserver
来启动 Django 的本地 web 服务器。
`(env)$ python manage.py migrate
(env)$ python manage.py runserver`
就是这样!查看 http://localhost:8000/ 你会看到主页:
添加条纹
条纹时间到了。从安装开始:
`(env)$ pip install stripe==2.63.0`
接下来,注册一个 Stripe 账户的(如果你还没有这样做的话)并导航到仪表板。点击“开发者”:
然后在左侧栏中点击“API keys”:
每个条带帐户有四个 API 密钥:两个用于测试,两个用于生产。每一对都有一个“秘密密钥”和一个“可公开密钥”。不要向任何人透露密钥;可发布的密钥将被嵌入到任何人都可以看到的页面上的 JavaScript 中。
目前右上角的“查看测试数据”开关表示我们正在使用测试键。这就是我们想要的。
在您的 settings.py 文件的底部,添加以下两行,包括您自己的测试秘密和测试可发布密钥。确保在实际的键周围包含''
字符。
`# djangostripe/settings.py
STRIPE_PUBLISHABLE_KEY = '<your test publishable key here>'
STRIPE_SECRET_KEY = '<your test secret key here>'`
最后,您需要在https://dashboard.stripe.com/settings/account的“帐户设置”中指定一个“帐户名称”:
创造产品
接下来,我们需要创造一个产品来销售。
单击“产品”,然后单击“添加产品”:
添加产品名称,输入价格,然后选择“一次性”:
点击“保存产品”。
用户流量
在用户点击购买按钮后,我们需要做以下事情:
-
获取可发布密钥
- 从客户端向服务器发送请求可发布密钥的 XHR 请求
- 用键回应
- 使用键创建 Stripe.js 的新实例
-
创建签出会话
- 向服务器发送另一个 XHR 请求,请求新的结帐会话 ID
- 生成新的签出会话并发回 ID
- 重定向到用户完成购买的结帐页面
-
适当地重定向用户
- 成功付款后重定向到成功页面
- 取消付款后重定向到取消页面
-
用条纹网钩确认付款
- 设置 webhook 端点
- 使用条带 CLI 测试端点
- 用条带注册端点
获取可发布密钥
JavaScript 静态文件
让我们首先创建一个新的静态文件来保存我们所有的 JavaScript:
`(env)$ mkdir static
(env)$ touch static/main.js`
向新的 main.js 文件添加快速健全检查:
`// static/main.js console.log("Sanity check!");`
然后更新 settings.py 文件,这样 Django 就知道在哪里可以找到静态文件:
`# djangostripe/settings.py
STATIC_URL = '/static/'
# for django >= 3.1
STATICFILES_DIRS = [BASE_DIR / 'static'] # new
# for django < 3.1
# STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # new`
在 HTML 模板中添加静态模板标签和新的脚本标签:
`<!-- templates/home.html -->
{% load static %} <!-- new -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Checkout</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/css/bulma.min.css">
<script src="{% static 'main.js' %}"></script> <!-- new -->
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
</head>
<body>
<section class="section">
<div class="container">
<button class="button is-primary" id="submitBtn">Purchase!</button>
</div>
</section>
</body>
</html>`
再次运行开发服务器。导航到 http://localhost:8000/ ,打开 JavaScript 控制台。您应该看到健全性检查:
视角
接下来,向 payments/views.py 添加一个新视图来处理 XHR 请求:
`# payments/views.py
from django.conf import settings # new
from django.http.response import JsonResponse # new
from django.views.decorators.csrf import csrf_exempt # new
from django.views.generic.base import TemplateView
class HomePageView(TemplateView):
template_name = 'home.html'
# new
@csrf_exempt
def stripe_config(request):
if request.method == 'GET':
stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
return JsonResponse(stripe_config, safe=False)`
也添加一个 URL:
`# payments/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.HomePageView.as_view(), name='home'),
path('config/', views.stripe_config), # new
]`
XHR 请求
接下来,使用获取 API 向 static/main.js 中的新/config/
端点发出 XHR (XMLHttpRequest)请求:
`// static/main.js console.log("Sanity check!"); // new // Get Stripe publishable key fetch("/config/") .then((result) => { return result.json(); }) .then((data) => { // Initialize Stripe.js const stripe = Stripe(data.publicKey); });`
来自fetch
请求的响应是一个可读流。result.json()
返回一个承诺,我们将其解析为一个 JavaScript 对象——例如data
。然后我们使用点符号来访问publicKey
以获得可发布的密钥。
将 Stripe.js 包含在 templates/home.html 中像这样:
`<!-- templates/home.html -->
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Checkout</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/css/bulma.min.css">
<script src="https://js.stripe.com/v3/"></script> <!-- new -->
<script src="{% static 'main.js' %}"></script>
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
</head>
<body>
<section class="section">
<div class="container">
<button class="button is-primary" id="submitBtn">Purchase!</button>
</div>
</section>
</body>
</html>`
现在,在页面加载之后,将调用/config/
,它将使用 Stripe publish key 进行响应。然后,我们将使用这个键创建 Stripe.js 的新实例。
流量:
-
获取可发布密钥从客户端向服务器发送 XHR 请求,请求可发布密钥用键响应使用键创建 Stripe.js 的新实例
-
创建签出会话
- 向服务器发送另一个 XHR 请求,请求新的结帐会话 ID
- 生成新的签出会话并发回 ID
- 重定向到用户完成购买的结帐页面
-
适当地重定向用户
- 成功付款后重定向到成功页面
- 取消付款后重定向到取消页面
-
用条纹网钩确认付款
- 设置 webhook 端点
- 使用条带 CLI 测试端点
- 用条带注册端点
创建签出会话
接下来,我们需要为按钮的 click 事件附加一个事件处理程序,该事件将向服务器发送另一个 XHR 请求,以生成新的结帐会话 ID。
视角
首先,添加新视图:
`# payments/views.py
@csrf_exempt
def create_checkout_session(request):
if request.method == 'GET':
domain_url = 'http://localhost:8000/'
stripe.api_key = settings.STRIPE_SECRET_KEY
try:
# Create new Checkout Session for the order
# Other optional params include:
# [billing_address_collection] - to display billing address details on the page
# [customer] - if you have an existing Stripe Customer ID
# [payment_intent_data] - capture the payment later
# [customer_email] - prefill the email input in the form
# For full details see https://stripe.com/docs/api/checkout/sessions/create
# ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
checkout_session = stripe.checkout.Session.create(
success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
cancel_url=domain_url + 'cancelled/',
payment_method_types=['card'],
mode='payment',
line_items=[
{
'name': 'T-shirt',
'quantity': 1,
'currency': 'usd',
'amount': '2000',
}
]
)
return JsonResponse({'sessionId': checkout_session['id']})
except Exception as e:
return JsonResponse({'error': str(e)})`
这里,如果请求方法是GET
,我们定义了一个domain_url
,将条带密钥分配给stripe.api_key
(因此当我们请求创建一个新的签出会话时,它将被自动发送),创建了签出会话,并在响应中发回了 ID。注意success_url
和cancel_url
。在成功支付或取消的情况下,用户将分别被重定向回这些 URL。我们将很快设置这些视图。
不要忘记重要的一点:
添加 URL:
`# payments/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.HomePageView.as_view(), name='home'),
path('config/', views.stripe_config),
path('create-checkout-session/', views.create_checkout_session), # new
]`
XHR 请求
将事件处理程序和后续的 XHR 请求添加到 static/main.js :
`// static/main.js console.log("Sanity check!"); // Get Stripe publishable key fetch("/config/") .then((result) => { return result.json(); }) .then((data) => { // Initialize Stripe.js const stripe = Stripe(data.publicKey); // new // Event handler document.querySelector("#submitBtn").addEventListener("click", () => { // Get Checkout Session ID fetch("/create-checkout-session/") .then((result) => { return result.json(); }) .then((data) => { console.log(data); // Redirect to Stripe Checkout return stripe.redirectToCheckout({sessionId: data.sessionId}) }) .then((res) => { console.log(res); }); }); });`
在这里,在解析了result.json()
承诺之后,我们调用了 redirectToCheckout ,其中的结帐会话 ID 来自解析的承诺。
导航到 http://localhost:8000/ 。点击按钮后,您将被重定向到 Stripe Checkout 实例(一个 Stripe 托管页面,用于安全收集支付信息),其中包含 t 恤产品信息:
我们可以使用 Stripe 提供的几个测试卡号中的一个来测试表单。还是用4242 4242 4242 4242
吧。请确保到期日期在未来。为 CVC 添加任意 3 个数字,为邮政编码添加任意 5 个数字。输入任何电子邮件地址和名称。如果一切顺利,付款应该被处理,但是重定向将失败,因为我们还没有设置/success/
URL。
流量:
-
获取可发布密钥从客户端向服务器发送 XHR 请求,请求可发布密钥用键响应使用键创建 Stripe.js 的新实例
-
创建结账会话向服务器发送另一个 XHR 请求,请求新的结账会话 ID生成新的结账会话并发回 ID重定向到用户完成购买的结账页面
-
适当地重定向用户
- 成功付款后重定向到成功页面
- 取消付款后重定向到取消页面
-
用条纹网钩确认付款
- 设置 webhook 端点
- 使用条带 CLI 测试端点
- 用条带注册端点
适当地重定向用户
最后,让我们连接用于处理成功和取消重定向的模板、视图和 URL。
成功模板:
`<!-- templates/success.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Checkout</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
</head>
<body>
<section class="section">
<div class="container">
<p>Your payment succeeded.</p>
</div>
</section>
</body>
</html>`
已取消的模板:
`<!-- templates/cancelled.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Stripe Checkout</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[[email protected]](/cdn-cgi/l/email-protection)/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
</head>
<body>
<section class="section">
<div class="container">
<p>Your payment was cancelled.</p>
</div>
</section>
</body>
</html>`
视图:
`# payments/views.py
class SuccessView(TemplateView):
template_name = 'success.html'
class CancelledView(TemplateView):
template_name = 'cancelled.html'`
URL:
`# payments/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.HomePageView.as_view(), name='home'),
path('config/', views.stripe_config),
path('create-checkout-session/', views.create_checkout_session),
path('success/', views.SuccessView.as_view()), # new
path('cancelled/', views.CancelledView.as_view()), # new
]`
好的,在刷新网页 http://localhost:8000/ 。点击支付按钮,再次使用信用卡号4242 4242 4242 4242
和其他虚拟信息。提交付款。你应该被重定向回http://localhost:8000/success/。
要确认实际收费,请返回“支付”下的 Stripe 仪表盘:
回顾一下,我们使用密钥在服务器上创建一个惟一的签出会话 ID。这个 ID 随后被用来创建一个结帐实例,最终用户在点击支付按钮后被重定向到这个实例。收费发生后,他们会被重定向回成功页面。
流量:
-
获取可发布密钥从客户端向服务器发送 XHR 请求,请求可发布密钥用键响应使用键创建 Stripe.js 的新实例
-
创建结账会话向服务器发送另一个 XHR 请求,请求新的结账会话 ID生成新的结账会话并发回 ID重定向到用户完成购买的结账页面
-
适当地重定向用户支付成功后重定向至成功页面取消付款后重定向至取消页面
-
用条纹网钩确认付款
- 设置 webhook 端点
- 使用条带 CLI 测试端点
- 用条带注册端点
用条纹网钩确认付款
我们的应用程序在这一点上工作得很好,但我们仍然不能以编程方式确认支付,或者在支付成功时运行一些代码。我们已经在用户结帐后将他们重定向到成功页面,但是我们不能只依赖那个页面,因为付款确认是异步发生的。
Stripe 和一般编程中有两种类型的事件:同步事件,具有即时效果和结果(例如,创建一个客户),异步事件,没有即时结果(例如,确认付款)。因为支付确认是异步完成的,用户可能会在他们的支付被确认之前和我们收到他们的资金之前被重定向到成功页面。
当支付完成时,获得通知的最简单的方法之一是使用回调或所谓的 Stripe webhook 。我们需要在应用程序中创建一个简单的端点,每当事件发生时(例如,当用户购买 t 恤时),Stripe 就会调用这个端点。通过使用 webhooks,我们可以绝对肯定支付成功。
为了使用 webhooks,我们需要:
- 设置 webhook 端点
- 使用条带 CLI 测试端点
- 用条带注册端点
这一部分由尼克·托马兹奇撰写。
端点
创建一个名为stripe_webhook
的新视图,它会在每次付款成功时打印一条消息:
`# payments/views.py
@csrf_exempt
def stripe_webhook(request):
stripe.api_key = settings.STRIPE_SECRET_KEY
endpoint_secret = settings.STRIPE_ENDPOINT_SECRET
payload = request.body
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
event = None
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError as e:
# Invalid payload
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return HttpResponse(status=400)
# Handle the checkout.session.completed event
if event['type'] == 'checkout.session.completed':
print("Payment was successful.")
# TODO: run some custom code here
return HttpResponse(status=200)`
stripe_webhook
现在作为我们的 webhook 端点。这里,我们只寻找每当结帐成功时调用的checkout.session.completed
事件,但是您可以对其他条带事件使用相同的模式。
确保将HttpResponse
导入添加到顶部:
`from django.http.response import JsonResponse, HttpResponse`
要使端点可访问,唯一要做的就是在 urls.py 中注册它:
`# payments/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.HomePageView.as_view(), name='home'),
path('config/', views.stripe_config),
path('create-checkout-session/', views.create_checkout_session),
path('success/', views.SuccessView.as_view()),
path('cancelled/', views.CancelledView.as_view()),
path('webhook/', views.stripe_webhook), # new
]`
测试 webhook
我们将使用 Stripe CLI 来测试 webhook。
一旦下载并安装了,在新的终端窗口中运行以下命令,登录到您的 Stripe 帐户:
此命令应生成一个配对代码:
`Your pairing code is: peach-loves-classy-cozy
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser (^C to quit)`
通过按 Enter,CLI 将打开您的默认 web 浏览器,并请求访问您的帐户信息的权限。请继续并允许访问。回到您的终端,您应该看到类似于以下内容的内容:
`> Done! The Stripe CLI is configured for Django Test with account id acct_<ACCOUNT_ID>
Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.`
接下来,我们可以开始侦听条带事件,并使用以下命令将它们转发到我们的端点:
`$ stripe listen --forward-to localhost:8000/webhook/`
这也将生成一个 webhook 签名密码:
`> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)`
为了初始化端点,将秘密添加到 settings.py 文件中:
`# djangostripe/settings.py
STRIPE_ENDPOINT_SECRET = '<your webhook signing secret here>'`
Stripe 现在会将事件转发到我们的端点。要测试,通过4242 4242 4242 4242
运行另一个测试支付。在您的终端中,您应该会看到Payment was successful.
消息。
一旦完成,停止stripe listen --forward-to localhost:8000/webhook/
过程。
如果您想识别进行购买的用户,可以使用 client_reference_id 将某种用户标识符附加到条带会话。
例如:
# payments/views.py @csrf_exempt def create_checkout_session(request): if request.method == 'GET': domain_url = 'http://localhost:8000/' stripe.api_key = settings.STRIPE_SECRET_KEY try: checkout_session = stripe.checkout.Session.create( # new client_reference_id=request.user.id if request.user.is_authenticated else None, success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}', cancel_url=domain_url + 'cancelled/', payment_method_types=['card'], mode='payment', line_items=[ { 'name': 'T-shirt', 'quantity': 1, 'currency': 'usd', 'amount': '2000', } ] ) return JsonResponse({'sessionId': checkout_session['id']}) except Exception as e: return JsonResponse({'error': str(e)})
注册端点
最后,在部署你的应用程序后,你可以在 Stripe 仪表板中注册端点,在开发者> Webhooks 下。这将生成一个 webhook 签名密码,用于您的生产应用程序。
例如:
流量:
-
获取可发布密钥从客户端向服务器发送 XHR 请求,请求可发布密钥用键响应使用键创建 Stripe.js 的新实例
-
创建结账会话向服务器发送另一个 XHR 请求,请求新的结账会话 ID生成新的结账会话并发回 ID重定向到用户完成购买的结账页面
-
适当地重定向用户支付成功后重定向至成功页面取消付款后重定向至取消页面
-
用条纹网钩确认付款设置 webhook 端点使用条带 CLI 测试端点用条带注册端点
下一步是什么
在实时网站上,需要有 HTTPS,这样你的连接是安全的。此外,虽然为了简单起见,我们硬编码了 API 密钥和 webhook 签名密码,但它们实际上应该存储在环境变量中。您可能还想将domain_url
存储为一个环境变量。
从 GitHub 上的django-stripe-check outrepo 中获取代码。
2023 年 Django vs. Flask:选择哪个框架
根据 2021 JetBrains Python 开发者调查,Django 和 Flask 是目前最流行的两个 Python web 框架。虽然 Django 传统上是最受欢迎的 Python web 框架,但 Flask 在几年前超过了 Django,占据了头把交椅,考虑到 web 开发行业在过去七年左右的时间里一直倾向于更小的框架、微服务和“无服务器”平台,这并不奇怪。
或者这可能与行业趋势关系不大,而与 JetBrains 用户关系更大?
Django 和 Flask 拥有成熟的社区,得到了广泛的支持和欢迎,并提供了高效的应用程序开发方法,让您可以将时间和精力集中在应用程序的独特部分,而不是核心构架上。最后,这两个框架都用于开发 web 应用程序。关键的区别在于他们如何实现这个目标。把 Django 想象成一辆汽车,把 Flask 想象成一辆自行车。两者都可以把你从 A 点带到 B 点,但是他们的方法是完全不同的。每种都有自己的最佳用例。姜戈和弗拉斯克也一样。
在本文中,我们将从教育和开发的角度来看 Django 和 Flask 的最佳用例,以及它们的独特之处。
哲学
Django 和 Flask 都是免费的、开源的、基于 Python 的 web 框架,旨在构建 web 应用程序。
与 Flask 相比,Django 支持稳定性以及“包含电池”的方法,即开箱即用地提供大量电池(例如,工具、模式、特性和功能)。在稳定性方面,Django 通常有更长、更严格的发布周期。因此,虽然 Django 版本的新特性较少,但它们往往具有更强的向后兼容性。
基于 Werkzeug ,Flask 很好的处理了核心脚手架。开箱即用,您可以获得 URL 路由、请求和错误处理、模板、cookies、单元测试支持、调试器和开发服务器。因为大多数 web 应用程序需要更多的东西(比如 ORM、认证和授权等等),所以您可以决定如何构建您的应用程序。无论是利用第三方扩展还是自己定制代码,Flask 都不会干涉。比 Django 灵活多了。如果你需要打开引擎盖查看源代码,那么暴露在攻击面前的表面区域也少得多,需要审查的代码也少得多。
我强烈建议你阅读并回顾一下 Flask 源代码。干净、清晰、简洁——这是结构良好的 Python 代码的一个很好的例子。
特征
接下来,让我们根据核心框架附带的特性来比较 Flask 和 Django。
数据库ˌ资料库
Django 包括一个简单而强大的 ORM(对象关系映射),支持许多现成的关系数据库(T2 ): SQLite、PostgreSQL、MySQL、MariaDB 和 Oracle。ORM 为生成和管理数据库迁移提供支持。基于数据模型创建表单、视图和模板也相当容易,这对于典型的 CRUD web 应用程序来说是完美的。虽然它确实有一些缺点,但对于大多数网络应用来说已经足够好了。
Flask 没有假设数据是如何存储的,但是有大量的库和扩展可以帮助你:
总之,如果您正在使用关系数据库,Django 会让您更容易上手,因为它有一个内置的 ORM 和迁移管理工具。但是,如果您正在使用非关系数据库,或者想要使用不同的 ORM,比如 SQLAlchemy,Django 几乎会在每一步都与您作对。另外,您很可能无法利用 Django 管理、模型表单或 DRF 模型序列化程序。
Flask 不碍事,让您可以自由挑选最适合您的应用程序的 ORM(或 ODM)。不过,自由是有代价的:学习曲线更高,出错的空间也更大,因为你要自己管理这些部分。
你自己做的越多,你犯的错误就越多,尤其是当事情扩大的时候。
作家(author 的简写)
既然大部分网络应用都需要认证(你是谁?)和授权(你被允许做什么?),Django 提供了这一功能以及账户管理和会话支持(通过用户模型)。Flask 支持基于 cookie 的会话,但是您必须求助于扩展网络来进行帐户管理、认证和授权。
管理
Django 附带了一个功能性的管理面板,这是一个 web 应用程序,它提供了一个用户界面来管理基于您的模型的数据。这是 Django 的另一个亮点。它允许您在构建应用程序时快速对模型执行 CRUD 操作,而无需编写任何额外的代码。再说一次,Flask 没有附带这样的东西,但是 Flask-Admin 扩展提供了所有相同的功能以及更多的功能:
Django 自动做很多事情。
Flask 哲学略有不同——显式比隐式好。如果某个东西应该初始化,那么它应该由开发人员来初始化。
Flask-Admin 遵循这个约定。作为一名开发人员,应该由您来告诉 Flask-Admin 应该显示什么以及如何显示。
有时这将需要编写一些样板代码,但将来会有回报,尤其是如果您必须实现一些定制逻辑的话。
Flask-Admin 支持一定数量的数据库后端,比如 SQLAlchemy、Peewee、MongoEngine 等等。你也可以添加你自己的后端。它还可以与(或不与)流行的 Flask auth 扩展一起使用:
路由和视图
这两个框架都允许您将 URL 映射到视图,并支持基于函数和类的视图。
姜戈
当一个请求匹配一个 URL 模式时,保存 HTTP 请求信息的请求对象被传递给一个视图,然后这个视图被调用。任何时候你需要访问请求对象,你必须显式地传递它。
URL 和视图分别在单独的文件中定义- urls.py 和 views.py 。
烧瓶
在其核心,Flask 使用 Werkzeug ,它提供 URL 路由和请求/响应处理。
请求对象在 Flask 中是全局的,所以您可以更容易地访问它(只要您导入它)。URL 通常与视图一起定义(通过一个装饰器,但是它们可以被分离出来到一个类似 Django 模式的集中位置。
你注意到 Django 和 Flask 处理请求对象的不同了吗?一般来说,Flask 倾向于更明确地处理事情,但在这种情况下,情况正好相反:Django 强迫你明确地传递请求对象,而 Flask 的请求对象只是神奇地可用。这是 Flask 的难点之一,尤其是对于那些来自类似风格框架的新手来说,比如 Express.js 。
形式
表单是大多数 web 应用程序的另一个重要组成部分,与 Django 打包在一起。这包括输入处理、客户端和服务器端验证以及各种安全问题的处理,如跨站点请求伪造(CSRF)、跨站点脚本(XSS)和 SQL 注入。它们可以从数据模型中创建(通过模型表单)并与管理面板很好地集成。
Flask 默认不支持表单,但是强大的 Flask-WTF 扩展集成了 Flask 和 WTForms 。 WTForms-Alchemy 可以用来自动创建基于 SQLAlchemy 模型的表单,在表单和 ORM 之间架起一座桥梁,就像 Django 的 ModelForm 一样。
可重用组件
关于项目结构,随着你的应用程序变得越来越复杂,这两个框架都让你很容易通过将相关的文件组合在一起,展示相似的功能来分解它们。因此,例如,您可以将所有与用户相关的功能组合在一起,其中可以包括路由、视图、表单、模板和静态资产。
Django 有一个应用程序的概念,而 Flask 有 T2 的蓝图。
Django 应用程序比 Flask blueprints 更复杂,但是一旦设置好,它们更容易使用和重用。另外,由于 urls.py 、 models.py 和 views.py 约定一致的项目结构!-您可以相当容易地向 Django 项目添加新的开发人员。与此同时,蓝图更简单,更容易启动和运行。
模板和静态文件
模板引擎允许您从后端动态地将信息注入到 HTML 页面中。Flask 默认使用 Jinja2 ,而 Django 有自己的模板引擎。它们在语法和功能集方面非常相似。也可以用 Django 搭配 Jinja2。
两个框架都支持静态文件处理:
- 姜戈
- 烧瓶
Django 附带了一个方便的管理命令,用于收集所有静态文件,并将它们放在一个中心位置进行生产部署。
异步视图
随着 Django 3.1 的引入,Django 支持异步处理程序。使用关键字async
可以使视图异步。异步支持也适用于中间件。如果需要在异步视图中进行同步调用,可以使用 sync_to_async 函数/装饰器。这可以用来与 Django 中还不支持异步的其他部分进行交互,比如 ORM 和缓存层。
异步 web 服务器,包括但不限于达芙妮、 Hypercorn 、uvicon,应该与 Django 一起使用,以充分利用异步视图的能力。
Flask 2.0 增加了对异步路由/视图、错误处理程序、请求函数之前和之后以及拆卸回调的内置支持!
关于 Django 和 Flask 中异步视图的更多信息,请分别查阅 Django 中的异步视图和 Flask 2.0 中的异步视图。
测试
两个框架都有内置的测试支持。
对于单元测试,它们都利用了 Python 的 unittest 框架。它们中的每一个都支持一个测试客户机,您可以向它发送请求,然后检查和验证响应的一部分。
更多信息,请分别参见测试烧瓶应用和在 Django 的测试。
在扩展方面,如果你喜欢 unittest 框架的工作方式,请查看烧瓶测试。另一方面, pytest-flask 延长件为烧瓶增加了 pytest 支架。对于 Django,请查看 pytest-django 。
其他功能
Django 还有几个没有提到的特性,但 Flask 没有:
安全性
如前所述,Django 内置了针对一些常见攻击媒介的保护,如 CSRF、XSS 和 SQL 注入。这些安全措施有助于防止代码中的漏洞。Django 开发团队还主动披露并快速修复已知的安全漏洞。另一方面,Flask 的代码库要小得多,因此暴露在攻击面前的空间也更小。然而,当你手工制作的应用程序代码出现安全漏洞时,你需要解决并修复它们。
归根结底,你的安全取决于你最薄弱的一环。因为 Flask 更依赖于第三方扩展,所以应用程序的安全性将取决于最不安全的扩展。这给开发团队带来了更大的压力,他们需要通过评估和监控第三方库和扩展来维护安全性。保持它们最新是最重要的(也是最困难的)事情,因为每个扩展都有自己的开发团队、文档和发布周期。在某些情况下,可能只有一两个开发人员维护一个特定的扩展。当评估一个扩展优于另一个扩展时,一定要回顾 GitHub 的问题,看看维护人员通常需要多长时间来应对关键问题。
这并不意味着 Django 天生就比 Flask 更安全;在应用程序的生命周期中,前期保护和维护更容易。
资源:
灵活性
从设计上来说,Flask 比 Django 灵活得多,而且它应该被扩展。因此,Flask 通常需要更长的设置时间,因为您必须根据业务需求添加适当的扩展——例如 ORM、权限、认证等等。这种前期成本为不符合标准 Django 模型的应用程序带来了更大的灵活性。
不过,小心点。灵活性给了开发人员更多的自由和控制,但是这会减慢开发速度,特别是对于大型团队,因为需要做出更多的决策。
开发人员喜欢自由地做他们想做的任何事情来解决问题。由于 Flask 没有提供许多关于如何开发应用程序的约束或意见,开发者可以介绍他们自己的。结果是,两个功能上可以互换的 Flask 应用程序通常会有不同的结构。因此,你需要一个更成熟的团队,理解设计模式、可伸缩性和测试的重要性,来处理这样的灵活性。
教育
学习模式,而不是语言或框架。
不管你的最终目标是学习 Flask 还是 Django,先从 Flask 开始。这是学习 web 开发基础和最佳实践以及几乎所有框架都通用的 web 框架核心部分的一个很好的工具。
- Flask 比 Django 更轻,也更露骨。因此,如果您是 web 开发的新手,但不熟悉 Python,您会发现在 Flask 中开发要容易得多,因为这感觉就像您在用普通的 Python 定义请求处理程序和视图等等。
- 姜戈开销很大。从项目结构到设置,再到安装一些你一无所知的螺母和螺栓,你会迷失方向,最终学到的更多的是 Django 本身,而不是实际的基础知识。
几乎所有情况下,都建议先学 Flask 再学 Django。你唯一应该偏离这一点的时候是当你只需要让一个应用程序快速运行来满足一些外部利益相关者的时候。一定要回到 Flask,在某个时候学习基础知识。
开放源码
Django 和 Flask 有强大的开源社区。
截至 2023 年 1 月 30 日的 GitHub 统计:
公制的 | 姜戈 | 烧瓶 |
---|---|---|
第一次提交 | 2005 | 2010 |
贡献者 | 2,337 | 690 |
用户* | 1,138,608 | 1,290,243 |
观察者 | 2,264 | 2,149 |
明星 | 68,431 | 61,711 |
*依赖项被其他存储库使用的次数
更多信息,请查看 Open Hub 的 Django 和 Flask 的开源比较。
截至 2023 年 1 月 30 日堆栈溢出问题:
--
我们在这里能得出什么结论?
- 两个社区都非常活跃
- Django 更老,有更多的贡献者
- Flask 被更多的项目使用
- Django 上有更多的内容
要从开源的角度真正比较这些框架(或生态系统),您必须考虑 Jinja2 和 Werkzeug 以及一些核心 Flask 库和扩展,如 SQLAlchemy / Flask-SQLAlchemy、Alembic / Flask-Alembic 和 WTForms / Flask-WTF。
由于核心 Flask 功能分散在多个项目中,社区很难创建和开发跨项目的必要协同来保持势头。例如,Flask 没有一个单一的、事实上的扩展来创建 RESTful APIs 截至 2023 年 1 月,有(可以说)四种流行的扩展:
更重要的是,为了找到这些扩展,你需要有相当扎实的信息检索技能。您必须过滤掉所有未维护的扩展以及引用它们的文章。事实上,RESTful APIs 有如此多不同的扩展项目,以至于开发自己的开源项目通常更容易。在这一点上,你可能会保持一段时间,但最终它会成为问题的一部分,而不是一个解决方案。
注意事项:
- 这不是对 Flask 社区的抨击。这在整个开源中是一个问题,特别是在微型 web 框架中,您经常不得不拼凑由不同开发人员在不同发布周期维护的大量项目,这些项目具有不同的文档质量水平。如果您想看到这一点的极致,请前往 JavaScript 社区。
- Django 社区也不能幸免。这只是一个小问题,因为它几乎处理了构建和保护开箱即用的标准 web 应用程序所需的一切。
关于这篇综述的更多信息,请参见 Django vs Flask 的“开源动力”部分:实践者的视角:
由于没有统一战线,跨推广的协同努力的机会无法实现,从而产生了漏洞百出的推广。这就让开发人员去填补那些已经在工作的全包功能的空白,如果他们只是选择了一个不同的工具来完成这项工作。
雇用
尽管 Python 和 Django 都很受欢迎,但很难雇佣 Django 开发人员。他们很难找到和留住,因为他们的需求很高,而且他们通常更高端,所以他们可能相当昂贵。也没有很多新的、有抱负的 web 开发人员学习 Django,因为这个行业更关注较小的框架,而框架本身很难学习。
- 行业趋势:由于微服务的兴起,有抱负的 web 开发人员通常会学习更小、更轻量级的框架。此外,由于客户端 JavaScript 框架 Angular、React 和 Vue 的流行,越来越多的 web 开发人员选择 JavaScript 而不是 Python 或 Ruby 作为他们的第一语言。
- 难以学习:令人惊讶的是,缺乏适合初学者的 Django 教程。即使是 Django 文档,它非常全面,还有臭名昭著的民意测验教程也不是为初学者设计的。
Flask 也很难租到,但它比 Django 容易,因为它是一个轻量级框架,抽象层更少。一个在不同语言的类似框架中有经验的优秀开发者,比如 Express.js 或 Sinatra ,可以很快熟悉 Flask 应用。当雇佣这样的开发人员时,把你的搜索重点放在那些理解设计模式和基本软件原则的人身上,而不是他们知道的语言或框架。
用例
当你决定一个框架时,一定要考虑到你的项目的个人需求。由于 Django 提供了许多额外的功能,您应该好好利用它们。如果你对姜戈处理事情的方式有强烈的不同意见,你可以选择弗拉斯克。如果您不想利用 Django 提供的结构和工具,也可以这么说。
让我们看一些例子。
数据库ˌ资料库
如果您的应用程序使用 SQLite、PostgreSQL、MySQL、MariaDB 或 Oracle,您应该仔细研究一下 Django。另一方面,如果您使用 NoSQL 或者根本没有数据库,那么 Flask 是一个可靠的选择。
项目规模和预期寿命
Flask 更适合于较小的、不太复杂的项目,这些项目有明确的范围和较短的预期生命周期。
因为 Django 强制使用一致的应用程序结构,而不管项目的大小,几乎所有的 Django 项目都有相似的结构。因此,Django 可以更好地处理更大的项目(更大的团队),这些项目具有更长的生命周期和更大的增长潜力,因为你很可能会不时地雇佣新的开发人员。
应用类型
您正在构建什么样的应用程序?
Django 擅长用服务器端模板创建全功能的 web 应用程序。如果您只是开发一个静态网站或 RESTful web 服务来支持您的 SPA 或移动应用程序,Flask 是一个可靠的选择。Django 结合 Django REST 框架在后一种情况下也能很好地工作。
RESTful APIs
设计 RESTful API?
Django REST 框架 (DRF),最流行的第三方 Django 包之一,是一个用于通过 RESTful 接口公开 Django 模型的框架。它提供了您需要的一切(视图、序列化器、验证、授权)和更多(可浏览 API、版本控制、缓存)来快速轻松地构建 API。也就是说,请记住,与 Django ORM 一样,它也是为了与关系数据库相结合。
Flask 也有许多很好的扩展:
一定要看看连接,它将视图、序列化和验证功能组合到一个包中!
--
回顾一下这个堆栈交换答案,了解您在选择框架时可能需要考虑的许多其他需求。
表演
Flask 的性能稍好,因为它更小,层数更少。不过,这里的差别可以忽略不计,尤其是当你考虑 I/O 的时候。
结论
那么,您应该使用哪个框架呢?一如既往,视情况而定。选择使用一种框架或语言或工具,几乎完全取决于当前的环境和问题。
Django 功能齐全,因此您或您的团队需要做的决策更少。那样你可能会走得更快。但是,如果您不同意 Django 为您做出的选择之一,或者您有独特的应用程序需求,这限制了您可以利用的特性的数量,那么您可能希望使用 Flask。
总会有权衡和妥协。
考虑项目限制,如时间、知识和预算。你的应用有哪些关键特性?什么是你不能妥协的?你需要快速行动吗?你的应用需要很大的灵活性吗?回答这些问题时,尽量把你的意见放在一边。
最终,这两个框架都降低了构建 web 应用程序的门槛,使它们的开发变得更加容易和快捷。
我希望这有所帮助。要了解更多信息,请查看以下两个优秀资源:
- Django vs Flask:一个实践者的视角
- Flask vs. Django:选择你的 Python Web 框架
Django 教程
描述
Django 是一个基于 Python 的 web 框架,旨在缓解开发人员在构建现代 web 应用程序时遇到的许多棘手问题。它具有一个易于遵循的 MTV(模型、模板、视图)架构,一个现代的数据库关系管理器,以及一个强大的管理系统来帮助快速构建数据(这对快速原型开发非常有用)。Django 应用程序通常速度很快,易于扩展,并且遵循最佳实践时,各种技能水平的开发人员都可以使用。
虽然 Django 是在 2005 年发布的,但是它活跃而热情的社区一直努力保持它与新框架的竞争力。例如,开发单页面应用程序(SPAs)的开发者会发现 Django REST 框架(DRF)非常适合,几乎所有流行的 JavaScript 框架都与 Django 结合使用,没有失败过。一旦你熟悉了,你会发现我们的文章和教程是扩展你构建生产级 Django 应用程序技能的一个很好的方式。
此外,本节中的大多数文章和教程都是比较中级到高级的,涵盖了测试、缓存、最佳实践、部署、容器化、安全和实现 Celery。
- 由 发布尼克·托马齐奇
- 最后更新于2023 年 2 月 28 日
将 Django 应用程序部署到 Azure App Service。
- 发帖者 发帖者阿玛尔沙姬
- 最后更新于2023 年 2 月 16 日
配置 Django 与 Postgres、Gunicorn、Traefik 和 Let's Encrypt 一起运行在 Docker 上。
- 发帖者 发帖者阿玛尔沙姬
- 最后更新于2023 年 2 月 10 日
常用的 web 身份验证方法。
- 由 发布尼克·托马齐奇
- 最后更新于2023 年 2 月 8 日
使用 Django 通道创建一个实时应用程序。
- 由 发布尼克·托马齐奇
- 最后更新于2023 年 2 月 7 日
将 Django 应用程序部署到 Google App Engine。
在本文中,我们将从教育和开发的角度来看 Django 和 Flask 的最佳用例,以及它们的独特之处。
配置 Django,通过一个亚马逊 S3 桶加载和提供公共和私有的静态和媒体文件。
本文逐步解释了如何在 Django 中创建定制用户模型。
在 Python 应用程序中启用多区域支持。
本文着眼于如何在 Django 应用程序中配置 Celery 来处理长时间运行的任务。
- 由 发布尼克·托马齐奇
- 最后更新于2022 年 12 月 15 日
部署一个 Django 应用程序来呈现。
- 发帖者 发帖者阿玛尔沙姬
- 最后更新于2022 年 11 月 11 日
与 Django 和 Django Allauth 建立社会认证。
- 由 发布尼克·托马齐奇
- 最后更新于2022 年 11 月 9 日
将 Django 应用程序部署到 Fly.io。
- 由 发布尼克·托马齐奇
- 最后更新于2022 年 10 月 21 日
本文一步一步地解释了如何在 Django 项目中期迁移到定制用户模型。
- 由 发布尼克·托马齐奇
- 最后更新于2022 年 10 月 10 日
在 DigitalOcean droplet 上将 Django 应用程序部署到 Dokku。
- 由 发布尼克·托马齐奇
- 最后更新于2022 年 9 月 2 日
配置 GitHub 动作,以持续地将 Django 和 Docker 应用程序部署到 Linode。
- 由 发布尼克·托马齐奇
- 最后更新于2022 年 8 月 10 日
将 Mailchimp 与 Django 集成,用于时事通讯和交易电子邮件。
姜戈基于阶级的观点(CBV)和基于功能的观点(FBV)之间的差异。
本教程着眼于如何用 Django 和 Stripe 处理订阅付款。
开始使用 Django 的异步视图。
研究如何利用 Django 的默认权限系统为用户和组分配权限。
- 发帖者 发帖者阿玛尔沙姬
- 最后更新于2022 年 4 月 3 日
在 Django 中加入 htmx 和 Tailwind CSS,提高开发者生产力。
- 由 发布尼克·托马齐奇
- 最后更新于2022 年 2 月 28 日
将 Django 应用程序部署到 AWS Elastic Beanstalk。
如何将 Django 应用程序部署到 DigitalOcean 的 App 平台。
- 发帖者 发帖者阿玛尔沙姬
- 最后更新于2022 年 2 月 8 日
如何在 Django 项目中,在本地和生产中处理静态和媒体文件。
查看如何向 Django 项目添加分页。
- 发帖者 迈克尔·尹
- 最后更新于2021 年 12 月 13 日
让 Celery 很好地与 Django 数据库事务一起工作。
- 发帖者 迈克尔·尹
- 最后更新于2021 年 12 月 11 日
自动重试失败的芹菜任务。
使用 Terraform 将 Django 应用程序部署到 AWS ECS。
快速添加 Stripe 以接受 Django/Python 网站上的支付。
如何通过 Heroku 容器运行时使用 Docker 将 Django 应用程序部署到 Heroku。
配置 VS 代码来调试运行在 Docker 中的 Django 应用程序。
配置一个在 EC2 实例上运行的容器化 Django 应用程序,将日志发送到 CloudWatch。
- 由 发布尼克·托马齐奇
- 最后更新于2021 年 10 月 14 日
接受 Django 和比特币基地商业部的加密付款。
使用 Postgres 为 Django 应用程序添加基本和全文搜索。
深入了解 Django REST 框架最强大的视图 ViewSets。
向您展示了如何配置 PyCharm 来调试运行在 Docker 中的 Django 应用程序。
使用 Django REST Framework 的通用视图来防止一遍又一遍地重复某些模式。
深入探讨 Django REST 框架的视图是如何工作的,以及它最基本的视图 APIView。
配置 Django,通过 DigitalOcean Spaces 加载和提供公共和私有的静态和媒体文件。
- 由 发布尼克·托马齐奇
- 最后更新于2021 年 8 月 31 日
查看如何将 Django REST 框架与 Elasticsearch 集成。
用 Docker 部署一个 Django app 到 AWS EC2,让我们加密。
本教程详细介绍了如何配置 Django 与 Postgres、Nginx 和 Gunicorn 一起在 Docker 上运行。
使用加密 SSL 证书来保护运行在 HTTPS Nginx 代理后面的容器化 Django 应用程序。
如何使用 Django 中的低级缓存 API?
用 Fetch API 和 jQuery 在 Django 中执行 GET、POST、PUT 和 DELETE AJAX 请求。
向 Django 项目添加多语言支持。
详细看看 Django 的内置缓存选项。
配置 GitLab CI 以持续地将 Django 和 Docker 应用程序部署到 AWS EC2。
如何在 Django REST 框架中构建自定义权限类?
这篇文章着眼于如何管理 Django、Celery 和 Docker 的周期性任务。
配置 GitHub 操作,以持续将 Django 和 Docker 应用程序部署到 DigitalOcean。
Django REST 框架中内置权限类的工作方式。
通过使用 Django 和 Aloe 编写一个示例特性,引导您完成行为驱动开发(BDD)开发周期。
配置 GitLab CI 以持续将 Django 和 Docker 应用程序部署到 DigitalOcean。
Django REST 框架中权限的工作方式。
这篇文章着眼于如何建立自动化的性能测试来发现和防止低效的数据库查询。
本文着眼于实现单页面应用程序(SPA)的新方法——基于 WebSockets 的 HTML。
深入研究 Django REST 框架(DRF)序列化程序。
- 由 发布尼克·托马齐奇
- 最后更新于2021 年 2 月 22 日
使用 Chart.js 向 Django 添加交互式图表。
- 由 发布尼克·托马齐奇
- 最后更新于2021 年 2 月 2 日
将 Pydantic 与 Django 应用程序集成。
简化在 Heroku 上部署、维护和扩展生产级 Django 应用的流程。
- 由 发布尼克·托马齐奇
- 最后更新于2020 年 12 月 14 日
将基于会话的身份验证添加到由 Django 和 React 支持的单页面应用程序(SPA)中。
本教程着眼于如何将 Stripe Connect 集成到 Django 应用程序中。
面向 Python 开发者的 Docker 最佳实践
本文着眼于编写 Docker 文件和使用 Docker 时需要遵循的一些最佳实践。虽然列出的大多数实践适用于所有开发人员,不管是哪种语言,但是有一些只适用于开发基于 Python 的应用程序的人员。
--
Dockerfiles :
- 使用多阶段构建
- 对 Dockerfile 命令进行适当排序
- 使用小型 Docker 基本图像
- 最小化层数
- 使用非特权容器
- 更喜欢复制而不是添加
- 将 Python 包缓存到 Docker 主机
- 每个容器仅运行一个流程
- 优先使用数组而不是字符串语法
- 了解入口点和 CMD 的区别
- 包含健康检查指令
图像:
奖励提示
码头文件
使用多阶段构建
利用多阶段构建创建更精简、更安全的 Docker 映像。
多阶段 Docker 构建允许你将你的 Docker 文件分成几个阶段。例如,您可以有一个用于编译和构建应用程序的阶段,然后可以将该阶段复制到后续阶段。因为只有最后一个阶段用于创建映像,所以与构建应用程序相关的依赖项和工具都被丢弃了,留下了一个精简的模块化生产就绪映像。
Web 开发示例:
`# temp stage
FROM python:3.9-slim as builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# final stage
FROM python:3.9-slim
WORKDIR /app
COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt .
RUN pip install --no-cache /wheels/*`
在这个例子中, GCC 编译器是安装某些 Python 包所必需的,所以我们添加了一个临时的构建时阶段来处理构建阶段。因为最终的运行时映像不包含 GCC,所以它更轻便、更安全。
尺寸比较:
`REPOSITORY TAG IMAGE ID CREATED SIZE
docker-single latest 8d6b6a4d7fb6 16 seconds ago 259MB
docker-multi latest 813c2fa9b114 3 minutes ago 156MB`
数据科学示例:
`# temp stage
FROM python:3.9 as builder
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /wheels jupyter pandas
# final stage
FROM python:3.9-slim
WORKDIR /notebooks
COPY --from=builder /wheels /wheels
RUN pip install --no-cache /wheels/*`
尺寸比较:
`REPOSITORY TAG IMAGE ID CREATED SIZE
ds-multi latest b4195deac742 2 minutes ago 357MB
ds-single latest 7c23c43aeda6 6 minutes ago 969MB`
总之,多阶段构建可以减小生产映像的大小,帮助您节省时间和金钱。此外,这将简化您的生产容器。此外,由于更小的尺寸和简单性,潜在的攻击面也更小。
正确排序 Dockerfile 命令
密切注意 Dockerfile 命令的顺序,以利用层缓存。
Docker 将每个步骤(或层)缓存在一个特定的 docker 文件中,以加速后续的构建。当一个步骤发生变化时,不仅该特定步骤的缓存会失效,所有后续步骤的缓存也会失效。
示例:
`FROM python:3.9-slim
WORKDIR /app
COPY sample.py .
COPY requirements.txt .
RUN pip install -r /requirements.txt`
在这个 over 文件中,我们在安装需求之前复制了应用程序代码。现在,每次我们改变 sample.py ,构建将重新安装软件包。这是非常低效的,尤其是当使用 Docker 容器作为开发环境时。因此,将频繁更改的文件放在 docker 文件的末尾是至关重要的。
您还可以通过使用来帮助防止不必要的缓存失效。dockerignore 文件来排除不必要的文件被添加到 Docker 构建上下文和最终映像中。稍后会有更多相关内容。
因此,在上面的 docker 文件中,您应该将COPY sample.py .
命令移到底部:
`FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r /requirements.txt
COPY sample.py .`
注意事项:
- 始终将可能发生变化的层放在 Dockerfile 文件中尽可能低的位置。
- 组合
RUN apt-get update
和RUN apt-get install
命令。(这也有助于减小图像尺寸。我们很快会谈到这一点。) - 如果您想关闭特定 Docker 版本的缓存,添加
--no-cache=True
标志。
使用小型 Docker 基本图像
较小的 Docker 映像更加模块化和安全。
对于较小的图像,构建、推和拉图像会更快。它们也更安全,因为它们只包含运行应用程序所需的必要库和系统依赖项。
您应该使用哪个 Docker 基础映像?
不幸的是,这要看情况。
下面是 Python 的各种 Docker 基本图像的大小比较:
`REPOSITORY TAG IMAGE ID CREATED SIZE
python 3.9.6-alpine3.14 f773016f760e 3 days ago 45.1MB
python 3.9.6-slim 907fc13ca8e7 3 days ago 115MB
python 3.9.6-slim-buster 907fc13ca8e7 3 days ago 115MB
python 3.9.6 cba42c28d9b8 3 days ago 886MB
python 3.9.6-buster cba42c28d9b8 3 days ago 886MB`
虽然基于 Alpine Linux 的 Alpine 版本是最小的,但是如果你找不到可以使用它的编译过的二进制文件,它通常会导致编译时间的增加。结果,您可能不得不自己构建二进制文件,这会增加映像大小(取决于所需的系统级依赖项)和构建时间(由于必须从源代码编译)。
请参考Python 应用的最佳 Docker 基础映像和使用 Alpine 会使 Python Docker 编译速度慢 50 倍,了解为什么最好避免使用基于 Alpine 的基础映像。
归根结底,这都是为了平衡。当你有疑问时,从一个*-slim
风格开始,尤其是在开发模式下,当你构建你的应用程序时。当您添加新的 Python 包时,您希望避免不断更新 Dockerfile 文件来安装必要的系统级依赖项。当您强化您的应用程序和 docker 文件以用于生产时,您可能希望探索使用 Alpine 作为多阶段构建的最终映像。
此外,不要忘记定期更新您的基本映像,以提高安全性和性能。当一个新版本的基础映像发布时——即
3.9.6-slim
->-3.9.7-slim
——你应该拉新的映像并更新你的运行容器以获得所有最新的安全补丁。
尽量减少层数
这是一个好主意,尽量结合使用RUN
、COPY
和ADD
命令,因为它们可以创建图层。每一层都增加了图像的大小,因为它们被缓存。因此,随着层数的增加,尺寸也增加。
您可以使用docker history
命令对此进行测试:
`$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dockerfile latest 180f98132d02 51 seconds ago 259MB
$ docker history 180f98132d02
IMAGE CREATED CREATED BY SIZE COMMENT
180f98132d02 58 seconds ago COPY . . # buildkit 6.71kB buildkit.dockerfile.v0
<missing> 58 seconds ago RUN /bin/sh -c pip install -r requirements.t… 35.5MB buildkit.dockerfile.v0
<missing> About a minute ago COPY requirements.txt . # buildkit 58B buildkit.dockerfile.v0
<missing> About a minute ago WORKDIR /app
...`
注意尺寸。只有RUN
、COPY
和ADD
命令可以增加图像的大小。您可以通过尽可能组合命令来减小图像大小。例如:
`RUN apt-get update
RUN apt-get install -y netcat`
可以组合成一个单独的RUN
命令:
`RUN apt-get update && apt-get install -y netcat`
因此,创建单个层而不是两个层,这减小了最终图像的大小。
虽然减少层数是个好主意,但更重要的是,减少层数本身不是目标,而是减少图像大小和构建时间的副作用。换句话说,更多地关注前面的三个实践——多阶段构建、Dockerfile 命令的顺序和使用小型基础映像——而不是试图优化每一个命令。
注意事项:
RUN
、COPY
和ADD
各自创建层。- 每一层都包含与前一层的不同之处。
- 图层增加了最终图像的大小。
小贴士:
- 组合相关命令。
- 在创建它们的同一个运行
step
中删除不必要的文件。 - 尽量减少运行
apt-get upgrade
的次数,因为它会将所有包升级到最新版本。 - 对于多阶段构建,不要太担心过度优化临时阶段中的命令。
最后,为了提高可读性,最好按字母数字顺序对多行参数进行排序:
`RUN apt-get update && apt-get install -y \
git \
gcc \
matplotlib \
pillow \
&& rm -rf /var/lib/apt/lists/*`
使用无特权的容器
默认情况下,Docker 在容器内部以 root 用户身份运行容器进程。但是,这是一种不好的做法,因为在容器中作为根用户运行的进程在 Docker 主机中也是作为根用户运行的。因此,如果攻击者获得了对您的容器的访问权限,他们就可以访问所有的根权限,并可以对 Docker 主机执行多种攻击,比如-
- 将敏感信息从主机的文件系统复制到容器
- 执行远程命令
为了防止这种情况,请确保使用非 root 用户运行容器进程:
`RUN addgroup --system app && adduser --system --group app
USER app`
您可以更进一步,删除 shell 访问,并确保没有主目录:
`RUN addgroup --gid 1001 --system app && \
adduser --no-create-home --shell /bin/false --disabled-password --uid 1001 --system --group app
USER app`
验证:
`$ docker run -i sample id
uid=1001(app) gid=1001(app) groups=1001(app)`
这里,容器中的应用程序在非 root 用户下运行。但是,请记住,Docker 守护进程和容器本身仍然以 root 权限运行。请务必查看以非根用户身份运行 Docker 守护程序,以获得以非根用户身份运行守护程序和容器的帮助。
首选复制而非添加
使用COPY
,除非你确定你需要ADD
附带的额外功能。
COPY
和ADD
有什么区别?
这两个命令都允许您将文件从特定位置复制到 Docker 映像中:
`ADD <src> <dest>
COPY <src> <dest>`
虽然它们看起来服务于相同的目的,但ADD
还有一些额外的功能:
COPY
用于将本地文件或目录从 Docker 主机复制到镜像。ADD
可用于下载外部文件。还有,如果你用的是压缩文件(tar,gzip,bzip2 等。)作为<src>
参数,ADD
会自动将内容解压到给定的位置。
`# copy local files on the host to the destination
COPY /source/path /destination/path
ADD /source/path /destination/path
# download external file and copy to the destination
ADD http://external.file/url /destination/path
# copy and extract local compresses files
ADD source.file.tar.gz /destination/path`
将 Python 包缓存到 Docker 主机
当需求文件发生变化时,需要重新构建映像来安装新的包。前面的步骤将被缓存,正如在中提到的最小化层数。在重建映像时下载所有软件包会导致大量网络活动,并花费大量时间。每次重新构建花费相同的时间来跨构建下载公共包。
您可以通过将 pip 缓存目录映射到主机上的一个目录来避免这种情况。因此,对于每次重新构建,缓存的版本会持续存在,并可以提高构建速度。
将一个卷作为-v $HOME/.cache/pip-docker/:/root/.cache/pip
添加到 docker 运行中,或者作为 Docker 编写文件中的一个映射。
以上目录仅供参考。确保映射缓存目录,而不是站点包(构建包所在的位置)。
将缓存从 docker 映像移动到主机可以节省最终映像中的空间。
如果您正在利用 Docker BuildKit ,使用 BuildKit 缓存挂载来管理缓存:
`# syntax = docker/dockerfile:1.2
...
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
...`
每个容器仅运行一个流程
为什么建议每个容器只运行一个流程?
让我们假设您的应用程序堆栈由两个 web 服务器和一个数据库组成。虽然您可以轻松地从一个容器中运行所有这三个服务,但是您应该在一个单独的容器中运行每个服务,以便于重用和扩展每个服务。
- 扩展 -每个服务都在一个单独的容器中,你可以根据需要水平扩展你的一个 web 服务器来处理更多的流量。
- 可重用性——也许您有另一个需要容器化数据库的服务。您可以简单地重用同一个数据库容器,而不会带来两个不必要的服务。
- 日志——耦合容器使得日志更加复杂。我们将在本文后面更详细地讨论这个问题。
- 可移植性和可预测性 -当可用的表面积减少时,制作安全补丁或调试问题就容易多了。
优先使用数组而不是字符串语法
您可以在 docker 文件中以数组(exec)或字符串(shell)格式编写CMD
和ENTRYPOINT
命令:
`# array (exec)
CMD ["gunicorn", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "main:app"]
# string (shell)
CMD "gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app"`
两者都是正确的,并实现几乎相同的事情;但是,您应该尽可能使用 exec 格式。来自 Docker 文档:
- 确保在 docker 文件中使用 exec 格式的
CMD
和ENTRYPOINT
。 - 例如,使用
["program", "arg1", "arg2"]
而不是"program arg1 arg2"
。使用字符串形式会导致 Docker 使用 bash 运行您的进程,而 bash 不能正确处理信号。Compose 总是使用 JSON 格式,所以如果您覆盖了 Compose 文件中的命令或入口点,也不用担心。
因此,由于大多数 shell 不处理发送给子进程的信号,如果使用 shell 格式,CTRL-C
(它生成一个SIGTERM
)可能不会停止子进程。
示例:
`FROM ubuntu:18.04
# BAD: shell format
ENTRYPOINT top -d
# GOOD: exec format
ENTRYPOINT ["top", "-d"]`
这两个都试试。注意,使用 shell 格式时,CTRL-C
不会终止进程。相反,你会看到^C^C^C^C^C^C^C^C^C^C^C
。
另一个警告是,shell 格式携带 shell 的 PID,而不是进程本身。
`# array format
[[email protected]](/cdn-cgi/l/email-protection):/app# ps ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 python manage.py runserver 0.0.0.0:8000
7 ? Sl 0:02 /usr/local/bin/python manage.py runserver 0.0.0.0:8000
25 pts/0 Ss 0:00 bash
356 pts/0 R+ 0:00 ps ax
# string format
[[email protected]](/cdn-cgi/l/email-protection):/app# ps ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 /bin/sh -c python manage.py runserver 0.0.0.0:8000
8 ? S 0:00 python manage.py runserver 0.0.0.0:8000
9 ? Sl 0:01 /usr/local/bin/python manage.py runserver 0.0.0.0:8000
13 pts/0 Ss 0:00 bash
342 pts/0 R+ 0:00 ps ax`
理解入口点和 CMD 的区别
我应该使用 ENTRYPOINT 还是 CMD 来运行容器进程?
在容器中运行命令有两种方式:
`CMD ["gunicorn", "config.wsgi", "-b", "0.0.0.0:8000"]
# and
ENTRYPOINT ["gunicorn", "config.wsgi", "-b", "0.0.0.0:8000"]`
两者本质上做同样的事情:用 Gunicorn 服务器在config.wsgi
启动应用程序,并将其绑定到0.0.0.0:8000
。
CMD
很容易被覆盖。如果您运行docker run <image_name> uvicorn config.asgi
,上面的 CMD 将被新的参数替换,例如uvicorn config.asgi
。然而要覆盖ENTRYPOINT
命令,必须指定--entrypoint
选项:
`docker run --entrypoint uvicorn config.asgi <image_name>`
在这里,很明显我们覆盖了入口点。因此,建议使用ENTRYPOINT
而不是CMD
来防止意外覆盖命令。
它们也可以一起使用。
例如:
`ENTRYPOINT ["gunicorn", "config.wsgi", "-w"]
CMD ["4"]`
像这样一起使用时,运行来启动容器的命令是:
`gunicorn config.wsgi -w 4`
如上所述,CMD
很容易被覆盖。因此,CMD
可以用来将参数传递给ENTRYPOINT
命令。工人的数量可以很容易地这样改变:
`docker run <image_name> 6`
这将启动六个 Gunicorn 工人的集装箱,而不是四个。
包括健康检查指令
使用一个HEALTHCHECK
来确定在容器中运行的进程是否不仅启动并运行,而且还“健康”。
Docker 公开了一个 API,用于检查容器中运行的进程的状态,这提供了比进程是否“正在运行”更多的信息,因为“正在运行”包括“它已启动并正在工作”、“仍在启动”,甚至“陷入了某种无限循环错误状态”。您可以通过 HEALTHCHECK 指令与该 API 进行交互。
例如,如果您正在提供一个 web 应用程序,那么您可以使用下面的方法来确定/
端点是否启动并可以处理服务请求:
`HEALTHCHECK CMD curl --fail http://localhost:8000 || exit 1`
如果运行docker ps
,可以看到HEALTHCHECK
的状态。
健康的例子:
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
09c2eb4970d4 healthcheck "python manage.py ru…" 10 seconds ago Up 8 seconds (health: starting) 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp xenodochial_clarke`
不健康的例子:
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
09c2eb4970d4 healthcheck "python manage.py ru…" About a minute ago Up About a minute (unhealthy) 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp xenodochial_clarke`
您可以更进一步,设置一个仅用于健康检查的自定义端点,然后配置HEALTHCHECK
来测试返回的数据。例如,如果端点返回一个 JSON 响应{"ping": "pong"}
,您可以指示HEALTHCHECK
验证响应体。
以下是使用docker inspect
查看健康检查状态的方式:
`❯ docker inspect --format "{{json .State.Health }}" ab94f2ac7889
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [
{
"Start": "2021-09-28T15:22:57.5764644Z",
"End": "2021-09-28T15:22:57.7825527Z",
"ExitCode": 0,
"Output": "..."`
这里,输出被裁剪,因为它包含整个 HTML 输出。
您还可以将运行状况检查添加到 Docker 撰写文件中:
`version: "3.8" services: web: build: . ports: - '8000:8000' healthcheck: test: curl --fail http://localhost:8000 || exit 1 interval: 10s timeout: 10s start_period: 10s retries: 3`
选项:
test
:要测试的命令。interval
:测试的时间间隔,即每x
个时间单位测试一次。timeout
:等待响应的最长时间。start_period
:何时开始健康检查。当在容器准备好之前执行附加任务时,如运行迁移,可以使用它。retries
:将测试指定为failed
之前的最大重试次数。
如果您使用的是 Docker Swarm 之外的编排工具,例如 Kubernetes 或 AWS ECS,那么该工具很可能有自己的内部系统来处理健康检查。添加
HEALTHCHECK
指令前,请参考特定工具的文档。
形象
版本 Docker 图像
尽可能避免使用latest
标签。
如果你依赖于latest
标签(它不是真正的“标签”,因为当一个图像没有被显式标记时,它被默认应用),你不能根据图像标签来判断你的代码运行的是哪个版本。这使得回滚变得很困难,并且很容易被覆盖(无论是意外的还是恶意的)。标签,就像你的基础设施和部署,应该是不可变的 T2。
无论您如何处理您的内部映像,您都不应该对基本映像使用latest
标记,因为您可能会无意中部署一个对生产环境有重大更改的新版本。
对于内部映像,使用描述性标记可以更容易地判断代码运行的版本,处理回滚,并避免命名冲突。
例如,您可以使用以下描述符来组成标签:
- 时间戳
- Docker 图像 id
- Git 提交哈希
- 语义版本
要了解更多选项,请查看“正确版本化 Docker 图像”堆栈溢出问题的答案。
例如:
`docker build -t web-prod-a072c4e5d94b5a769225f621f08af3d4bf820a07-0.1.4 .`
这里,我们使用以下内容来构成标记:
- 项目名称:
web
- 环境名称:
prod
- Git 提交哈希:
a072c4e5d94b5a769225f621f08af3d4bf820a07
- 语义版本:
0.1.4
选择一个标记方案并与之保持一致是很重要的。由于提交散列使得将图像标签快速绑定到代码变得容易,所以强烈建议将它们包含在您的标签方案中。
不要在图像中存储秘密
秘密是敏感的信息片段,例如密码、数据库凭证、SSH 密钥、令牌和 TLS 证书等等。这些不应该在没有加密的情况下放进你的图像中,因为获得图像访问权的未授权用户只能检查图层来提取秘密。
不要以纯文本形式将秘密添加到 Docker 文件中,尤其是如果您将图像推送到像 Docker Hub 这样的公共注册表中:
`FROM python:3.9-slim
ENV DATABASE_PASSWORD "SuperSecretSauce"`
相反,应该通过以下途径注射:
- 环境变量(运行时)
- 构建时参数(在构建时)
- 像 Docker Swarm(通过 Docker secrets)或 Kubernetes(通过 Kubernetes secrets)这样的编排工具
此外,您还可以通过将常用机密文件和文件夹添加到您的中来帮助防止泄密。dockerignore 文件:
最后,明确哪些文件将被复制到映像,而不是递归地复制所有文件:
`# BAD
COPY . .
# GOOD
copy ./app.py .`
明确也有助于限制缓存破坏。
环境变量
您可以通过环境变量传递秘密,但是它们将在所有子进程、链接容器和日志中可见,也可以通过docker inspect
传递。更新它们也很困难。
`$ docker run --detach --env "DATABASE_PASSWORD=SuperSecretSauce" python:3.9-slim
d92cf5cf870eb0fdbf03c666e7fcf18f9664314b79ad58bc7618ea3445e39239
$ docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' d92cf5cf870eb0fdbf03c666e7fcf18f9664314b79ad58bc7618ea3445e39239
DATABASE_PASSWORD=SuperSecretSauce
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=C.UTF-8
GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568
PYTHON_VERSION=3.9.7
PYTHON_PIP_VERSION=21.2.4
PYTHON_SETUPTOOLS_VERSION=57.5.0
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/c20b0cfd643cd4a19246ccf204e2997af70f6b21/public/get-pip.py
PYTHON_GET_PIP_SHA256=fa6f3fb93cce234cd4e8dd2beb54a51ab9c247653b52855a48dd44e6b21ff28b`
这是最直接的秘密管理方法。虽然它不是最安全的,但它会让诚实的人保持诚实,因为它提供了一层薄薄的保护,有助于保持秘密不被好奇的眼睛发现。
使用共享卷传递秘密是一个更好的解决方案,但它们应该通过 Vault 或 AWS 密钥管理服务 (KMS)进行加密,因为它们保存在光盘上。
构建时参数
你可以在构建时使用构建时参数传递秘密,但是那些通过docker history
访问映像的人可以看到它们。
示例:
`FROM python:3.9-slim
ARG DATABASE_PASSWORD`
构建:
`$ docker build --build-arg "DATABASE_PASSWORD=SuperSecretSauce" .`
如果您只需要在构建过程中临时使用密码(例如,用于克隆私有回购或下载私有包的 SSH 密钥),则应该使用多阶段构建,因为构建器历史记录在临时阶段会被忽略:
`# temp stage
FROM python:3.9-slim as builder
# secret
ARG SSH_PRIVATE_KEY
# install git
RUN apt-get update && \
apt-get install -y --no-install-recommends git
# use ssh key to clone repo
RUN mkdir -p /root/.ssh/ && \
echo "${PRIVATE_SSH_KEY}" > /root/.ssh/id_rsa
RUN touch /root/.ssh/known_hosts &&
ssh-keyscan bitbucket.org >> /root/.ssh/known_hosts
RUN git clone [[email protected]](/cdn-cgi/l/email-protection):testdrivenio/not-real.git
# final stage
FROM python:3.9-slim
WORKDIR /app
# copy the repository from the temp image
COPY --from=builder /your-repo /app/your-repo
# use the repo for something!`
多阶段构建仅保留最终图像的历史记录。请记住,您可以将此功能用于您的应用程序所需的永久机密,如数据库凭证。
您还可以使用 Docker build 中新的--secret
选项将秘密传递给 Docker 映像,这些秘密不会存储在映像中。
`# "docker_is_awesome" > secrets.txt
FROM alpine
# shows secret from default secret location:
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret`
这将从secrets.txt
文件中挂载秘密。
建立形象:
`docker build --no-cache --progress=plain --secret id=mysecret,src=secrets.txt .
# output
...
#4 [1/2] FROM docker.io/library/alpine
#4 sha256:665ba8b2cdc0cb0200e2a42a6b3c0f8f684089f4cd1b81494fbb9805879120f7
#4 CACHED
#5 [2/2] RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
#5 sha256:75601a522ebe80ada66dedd9dd86772ca932d30d7e1b11bba94c04aa55c237de
#5 0.635 docker_is_awesome#5 DONE 0.7s
#6 exporting to image`
最后,查看历史记录,看看秘密是否泄露:
`❯ docker history 49574a19241c
IMAGE CREATED CREATED BY SIZE COMMENT
49574a19241c 5 minutes ago CMD ["/bin/sh"] 0B buildkit.dockerfile.v0
<missing> 5 minutes ago RUN /bin/sh -c cat /run/secrets/mysecret # b… 0B buildkit.dockerfile.v0
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:aad4290d27580cc1a… 5.6MB`
有关构建时秘密的更多信息,请查看不要泄露 Docker 映像的构建秘密。
码头工人的秘密
如果你正在使用 Docker Swarm ,你可以用 Docker secrets 来管理秘密。
例如,初始化 Docker 群模式:
创建 docker 机密:
`$ echo "supersecretpassword" | docker secret create postgres_password -
qdqmbpizeef0lfhyttxqfbty0
$ docker secret ls
ID NAME DRIVER CREATED UPDATED
qdqmbpizeef0lfhyttxqfbty0 postgres_password 4 seconds ago 4 seconds ago`
当一个容器被授予访问上述秘密的权限时,它将在/run/secrets/postgres_password
挂载。这个文件将以明文形式包含秘密的实际值。
使用不同的业务流程工具?
- AWS EKS - 通过 Kubernetes 使用 AWS Secrets Manager secrets】
- 数字海洋 Kubernetes - 保护数字海洋 Kubernetes 集群的推荐步骤
- Google Kubernetes 引擎- 与其他产品一起使用 Secret Manager】
- Nomad - 保险库集成和检索动态机密
使用. dockerignore 文件
我们已经提到使用一个 。dockerignore 文件已经好几次了。该文件用于指定您不希望添加到发送到 Docker 守护进程的初始构建上下文中的文件和文件夹,Docker 守护进程随后将构建您的映像。换句话说,您可以使用它来定义您需要的构建上下文。
构建 Docker 映像时,在评估COPY
或ADD
命令之前,整个 Docker 上下文——即项目的根——被发送到 Docker 守护进程。这可能非常昂贵,尤其是如果您在项目中有许多依赖项、大型数据文件或构建工件的话。另外,Docker CLI 和守护程序可能不在同一台机器上。因此,如果守护进程在远程机器上执行,您应该更加注意构建上下文的大小。
你应该给添加什么?dockerignore 文件?
- 临时文件和文件夹
- 构建日志
- 地方机密
- 本地开发文件,如 docker-compose.yml
- 版本控制文件夹,如“.git“,”。hg”,和”。svn "
示例:
`**/.git
**/.gitignore
**/.vscode
**/coverage
**/.env
**/.aws
**/.ssh
Dockerfile
README.md
docker-compose.yml
**/.DS_Store
**/venv
**/env`
总而言之,一个结构合理的。dockerignore 可以帮助:
- 减小 Docker 图像的大小
- 加快构建过程
- 防止不必要的缓存失效
- 防止泄密
Lint 和扫描您的 docker 文件和图像
林挺是检查您的源代码的程序和风格错误以及可能导致潜在缺陷的不良实践的过程。就像编程语言一样,静态文件也可以被链接。特别是对于 docker 文件,linters 有助于确保它们的可维护性,避免不推荐使用的语法,并遵循最佳实践。林挺:你的形象应该成为你 CI 渠道的一个标准部分。
Hadolint 是最受欢迎的 Dockerfile linter:
`$ hadolint Dockerfile
Dockerfile:1 DL3006 warning: Always tag the version of an image explicitly
Dockerfile:7 DL3042 warning: Avoid the use of cache directory with pip. Use `pip install --no-cache-dir <package>`
Dockerfile:9 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
Dockerfile:17 DL3025 warning: Use arguments JSON notation for CMD and ENTRYPOINT arguments`
你可以在 https://hadolint.github.io/hadolint/的网站上看到它的运行。还有一个 VS 代码扩展。
您可以将林挺与扫描图像和容器的漏洞相结合。
一些选项:
- Snyk 是 Docker 本地漏洞扫描的独家提供商。您可以使用
docker scan
CLI 命令扫描图像。 - Trivy 可以用来扫描容器映像、文件系统、git 库和其他配置文件。
- Clair 是一个开源项目,用于静态分析应用程序容器中的漏洞。
- Anchore 是一个开源项目,为集装箱图像的检查、分析和认证提供集中服务。
总之,lint 和扫描您的 docker 文件和图像,找出任何偏离最佳实践的潜在问题。
签名并验证图像
您如何知道用于运行生产代码的图像没有被篡改?
篡改可以通过中间人 (MITM)攻击通过网络进行,也可以来自被完全破坏的注册表。
Docker 内容信任 (DCT)支持来自远程注册中心的 Docker 图像的签名和验证。
要验证图像的完整性和真实性,请设置以下环境变量:
现在,如果您尝试提取未签名的图像,您将收到以下错误:
`Error: remote trust data does not exist for docker.io/namespace/unsigned-image:
notary.docker.io does not have trust data for docker.io/namespace/unsigned-image`
您可以从使用 Docker 内容信任签名图像文档中了解签名图像。
从 Docker Hub 下载图像时,请确保使用官方图像或来自可信来源的验证图像。较大的团队应该考虑使用他们自己的内部私有容器注册中心。
额外提示
使用 Python 虚拟环境
是否应该在容器中使用虚拟环境?
在大多数情况下,只要坚持每个容器只运行一个进程,虚拟环境就是不必要的。由于容器本身提供了隔离,包可以在系统范围内安装。也就是说,您可能希望在多阶段构建中使用虚拟环境,而不是构建 wheel 文件。
带轮子的例子:
`# temp stage
FROM python:3.9-slim as builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# final stage
FROM python:3.9-slim
WORKDIR /app
COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt .
RUN pip install --no-cache /wheels/*`
virtualenv 示例:
`# temp stage
FROM python:3.9-slim as builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install -r requirements.txt
# final stage
FROM python:3.9-slim
COPY --from=builder /opt/venv /opt/venv
WORKDIR /app
ENV PATH="/opt/venv/bin:$PATH"`
设置内存和 CPU 限制
限制 Docker 容器的内存使用是一个好主意,尤其是当您在一台机器上运行多个容器时。这可以防止任何容器使用所有可用的内存,从而降低其余容器的性能。
限制内存使用的最简单方法是在 Docker cli 中使用--memory
和--cpu
选项:
`$ docker run --cpus=2 -m 512m nginx`
上面的命令将容器的使用限制为 2 个 CPU 和 512 兆的主内存。
您可以在 Docker 合成文件中做同样的事情,如下所示:
`version: "3.9" services: redis: image: redis:alpine deploy: resources: limits: cpus: 2 memory: 512M reservations: cpus: 1 memory: 256M`
记下reservations
字段。它用于设置一个软限制,当主机内存或 CPU 资源不足时,该限制优先。
其他资源:
记录到 stdout 或 stderr
在 Docker 容器中运行的应用程序应该将日志消息写入标准输出(stdout)和标准错误(stderr ),而不是文件。
然后,您可以配置 Docker 守护进程将您的日志消息发送到一个集中的日志记录解决方案(如 CloudWatch Logs 或 Papertrail )。
更多信息,请查看来自的将日志视为事件流、来自的十二因素应用以及来自 Docker 文档的配置日志驱动程序。
为 Gunicorn Heartbeat 使用共享内存挂载
Gunicorn 使用基于文件的心跳系统来确保所有分叉的工作进程都是活动的。
在大多数情况下,心跳文件位于“/tmp”中,通常通过 tmpfs 存储在内存中。因为 Docker 默认情况下不利用 tmpfs,所以文件将存储在磁盘支持的文件系统中。这会导致问题,比如随机冻结,因为心跳系统使用os.fchmod
,如果目录实际上在磁盘支持的文件系统上,这可能会阻塞一个工作进程。
幸运的是,有一个简单的修复方法:通过--worker-tmp-dir
标志将 heartbeat 目录更改为内存映射目录。
`gunicorn --worker-tmp-dir /dev/shm config.wsgi -b 0.0.0.0:8000`
结论
本文研究了几个最佳实践,使您的 docker 文件和图像更干净、更精简、更安全。
其他资源:
--
Dockerfiles :
- 使用多阶段构建
- 对 Dockerfile 命令进行适当排序
- 使用小型 Docker 基本图像
- 最小化层数
- 使用非特权容器
- 更喜欢复制而不是添加
- 将 Python 包缓存到 Docker 主机
- 每个容器仅运行一个流程
- 优先使用数组而不是字符串语法
- 了解入口点和 CMD 的区别
- 包含健康检查指令
图像:
奖励提示