第 4 章:公司网站¶
在本章中,我们将构建第三个项目——一个公司网站,同时深入学习更多模板知识、引入基于类的视图,并集成更高级的测试。这是我们转向数据库和 Django 模型之前的最后一个项目,因此这是一个巩固以往学习成果的机会,并探索 Django 的其他三个部分——视图、URL 和模板——能做什么。
初始设置¶
我们的初始设置现在应该开始变得熟悉了,包含以下步骤:
- 为代码创建一个名为 company 的新目录,并导航进入该目录
- 创建一个名为
.venv的新虚拟环境并激活它 - 安装 Django 和 Black
- 创建一个名为
django_project的新 Django 项目 - 创建一个名为
pages的新应用
在命令行中,确保你没有在已有的虚拟环境中工作。如果命令行提示符前有文本——Windows 上是 >,macOS 上是 %——那么你就在虚拟环境中!确保输入 deactivate 来退出它。
在新的命令行 Shell 中,导航到桌面上的 code 文件夹,创建一个名为 company 的新文件夹,进入该目录,并激活一个名为 .venv 的新 Python 虚拟环境。
# Windows
$ cd onedrive\desktop\code
$ mkdir company
$ cd company
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $
# macOS
$ cd ~/desktop/code
$ mkdir company
$ cd company
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $
接下来,安装 Django 和 Black,创建一个名为 django_project 的新项目,并创建一个名为 pages 的新应用。我们之所以将所有应用称为"pages",是因为它们都用于相对静态的页面。在未来的项目中,我们将从数据库填充页面内容,应用名称将反映这种新的动态特性。
(.venv) $ python -m pip install django~=5.0.0
(.venv) $ python -m pip install black
(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py startapp pages
请记住,即使我们添加了一个新应用,Django 也不会识别它,直到将其明确添加到 django_project/settings.py 中的 INSTALLED_APPS 设置。打开你的文本编辑器,现在将其添加到底部:
# django_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"pages", # new
]
使用 migrate 初始化数据库,并使用 runserver 启动本地 Web 服务器。
(.venv) $ python manage.py migrate
(.venv) $ python manage.py runserver
然后导航到 http://127.0.0.1:8000/ 查看 Django 欢迎页面。
项目级模板¶
我们之前看到,Django 期望模板文件位于应用内名为 templates 的目录中,并且最佳实践是通过再次添加目录名称来进一步命名空间化。换句话说,pages 应用的模板文件将位于 pages/templates/pages/ 目录中。
然而,许多 Django 开发者倾向于另一种方法:创建一个单独的项目级 templates 目录,并将所有模板放在其中。这使得在一个位置查找和更新所有模板更加容易。通过调整 django_project/settings.py 文件,我们还可以告诉 Django 在该目录中查找模板。
首先,使用 Control+c 命令退出正在运行的服务器。然后,创建一个名为 templates 的目录。
(.venv) $ mkdir templates
接下来,我们需要更新 django_project/settings.py 来告诉 Django 新的 templates 目录在哪里。这需要对 TEMPLATES 下的 "DIRS" 配置进行一行修改。
# django_project/settings.py
TEMPLATES = [
{
...
"DIRS": [BASE_DIR / "templates"], # new
...
},
]
我们将在本书的其余部分使用这种方法来组织模板。在 templates 目录中创建一个名为 home.html 的新文件。你可以在文本编辑器中完成此操作:在 Visual Studio Code 中,点击屏幕左上角的"File",然后点击"New File"。确保在正确的位置命名并保存文件。
目前,home.html 文件将包含一个简单的标题。
<!-- templates/home.html -->
<h1>Company Homepage</h1>
我们的模板完成了!下一步是配置 URL 和视图文件。
基于函数的视图和 URL¶
开发者自行决定先编写视图还是先编写 URL。最终,我们需要两者来显示网页,因此决定执行顺序随着时间的推移会变成个人偏好。在本例中,我们将从 pages 应用中的视图开始。
# pages/views.py
from django.shortcuts import render
def home_page_view(request): # new
return render(request, "home.html")
这段代码应该看起来很熟悉,和上一章的一样。我们使用在顶部导入的 render() 快捷函数。然后创建视图 home_page_view,并将其第一个参数——HttpRequest 对象——命名为 request。我们返回 request 对象并指定正确的模板文件 home.html。
接下来是 URL 配置,包括项目级的 urls.py 文件(作为网站的入口点)和应用级的 urls.py 文件(包含主页的特定路由和视图)。
django_project/urls.py 文件是所有 URL 请求的初始入口点。我们必须在顶部导入 include 函数,然后使用它将 pages 应用的 URL 路由包含进来,该路由将设置为空字符串 ""。
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include # new
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("pages.urls")), # new
]
应用级的 pages/urls.py 文件导入视图 home_page_view,并将其设置为空字符串 "" 的 URL 路径。
# pages/urls.py
from django.urls import path
from .views import home_page_view
urlpatterns = [
path("", home_page_view),
]
使用 runserver 命令启动开发 Web 服务器。
(.venv) $ python manage.py runserver
如果你导航到 http://127.0.0.1:8000/,现在可以看到主页了。

很简单,对吧?到目前为止,唯一引入的新概念是项目级 templates 目录。
模板上下文、标签和过滤器¶
让我们为主页视图添加模板上下文,然后尝试一些 Django 的内置标签和过滤器。标签执行更复杂的操作,如循环、条件判断和模板继承。同时,过滤器用于执行更简单的转换来修改变量的显示方式,例如格式化日期、截断文本或将字符串转换为大写。标签和过滤器太多了,无法全部记住;知道对于几乎任何内容显示,都有大量原生解决方案可用是很有帮助的。
模板上下文具有键值对的字典结构。为了演示,我们可以添加两个:一个包含三个 widget 的 inventory_list 和一个故意混合大小写的 greeting 文本字符串。
# pages/views.py
from django.shortcuts import render
def home_page_view(request):
context = { # new
"inventory_list": ["Widget 1", "Widget 2", "Widget 3"],
"greeting": "THAnk you FOR visitING.",
}
return render(request, "home.html", context)
使用以下代码更新 home.html 模板文件。now 标签使用 DATE_FORMAT 显示当前日期和/或时间,这是多种显示选项之一。接下来,使用适用于字符串和列表的 length 过滤器显示 inventory_list 中的项目数量。
然后,使用 for 标签循环遍历每个项目。一般语法是 {% for item in item_list %},其中 item 是代表循环中当前项目的变量名,item_list 是我们正在循环遍历的序列。同样关键的是要包含 {% endfor %} 标签来结束任何 for 循环。在本例中,序列名为 inventory_list。我们可以将变量命名为任何喜欢的名称,但像 item 这样的描述性名称是常见选择,使代码更易于理解。最后,我们使用 title 过滤器将字符串转换为标题大小写,其中每个单词以大写字母开头,后跟小写字母。
<!-- templates/home.html -->
<h1>Company Homepage</h1>
<p>The current date and time is: {% now "DATETIME_FORMAT" %}</p>
<p>There are {{ inventory_list|length }} items of inventory.
<ul>
{% for item in inventory_list %}
<li>{{ item }}</li>
{% endfor %}
</ul>
<p>{{ greeting| title }}</p>
{% comment %}Add more content here!{% endcomment %}
本地 Web 服务器应该仍在后台运行着 runserver 命令,所以你只需刷新网页即可看到变化。

这里的目的不是用大量你需要记住的新 Django 功能来让你应接不暇。标签和过滤器太多了,根本记不完。相反,目的是强调对于你想到的几乎任何 Web 开发任务,Django 可能都有内置解决方案,这就是为什么官方文档对于专业 Django 开发者的日常开发不可或缺。
基于类的视图和通用基于类的视图¶
Django 社区中最接近"宗教辩论"的话题是关于基于函数的视图——我们到目前为止一直在使用的——和基于类的视图。早期版本的 Django 只提供基于函数的视图,由于它们模拟了 HTTP 请求/响应周期,因此 arguably 比基于类的对应物更容易理解。正是因为这个原因,本书一开始只使用基于函数的视图。
基于函数的视图确实有其缺点。它们缺乏简单的继承方式,这意味着开发者必须在每个视图中反复重复相同的代码片段。这违反了 Django 的总体 DRY(Don't Repeat Yourself,不要重复自己)原则。但即使不重复相同的代码,基于函数的视图在实际项目中通常会变得很长,因此难以理解。经常会看到包含十行、二十行甚至更多行逻辑的视图,这变得难以理解。
通用基于函数的视图在 Django 开发的早期被引入,用于抽象常见模式并避免代码重复。示例包括:
- 编写一个显示单个模板的视图(就像我们刚刚在这里做的)
- 编写一个列出数据库模型中所有对象的视图
- 编写一个仅显示模型中一个详细项目的视图
- 编写一个创建、更新或删除对象的视图
通用基于函数的视图的问题在于没有简单的方法来扩展或自定义它们。随着项目的增长,这越来越成为一个问题。
Django 添加了基于类的视图和通用基于类的视图来帮助代码可重用性,同时保留了基于函数的视图。类是 Python 的基本组成部分,依赖于面向对象编程(OOP)和继承,因此一个类可以从另一个类继承属性和方法。这意味着我们不必将所有视图逻辑放在一个地方,而是可以抽象常见模式,然后根据需要自定义或扩展它们。关于 Python 类和 OOP 的详细讨论超出了本书的范围。不过,如果你需要入门或复习,我建议查阅官方 Python 文档,其中有关于类及其用法的优秀教程。
一旦你使用通用基于类的视图一段时间,它们就会变成优雅高效的代码编写方式。你通常可以修改其中一个方法来实现自定义行为,而不是从头重写所有内容,这使得理解他人的代码更加容易。然而,这是以复杂性为代价的,需要一定的信仰飞跃,因为理解它们底层的工作原理需要很长时间。整个网站 Classy Class-Based Views 专门致力于帮助 Django 开发者解读通用基于类的视图。
Django 代码库本身已转向主要使用基于类的视图和通用基于类的视图。通用基于函数的视图在 Django 1.3 中被弃用,并在 1.5 版本中被完全移除。
多年来 Django 这些变化的结果是,现在在 Django 中有三种不同的编写视图的方式:基于函数的、基于类的或通用基于类的。这对初学者来说 understandably 非常令人困惑。
本书的早期版本完全专注于通用基于类的视图,但这个版本同时包含了两者。Django 开发者需要理解每种方法的工作原理,即使他们随着时间的推移无疑会有个人偏好。
TemplateView¶
让我们为公司网站创建第二个网页,这次使用 TemplateView,一个通用基于类的视图。这将用于一个关于页面,它也利用了模板上下文和 Django 模板语言。
在 views.py 文件的顶部,从 django.views.generic 模块导入 TemplateView。然后创建一个类 AboutPageView,它扩展 TemplateView 并指定模板 about.html。在 Python 中,类的命名约定是使用"CamelCase"(驼峰命名法),即单词的首字母大写,单词之间没有下划线。
# pages/views.py
from django.shortcuts import render
from django.views.generic import TemplateView # new
def home_page_view(request):
context = {
"inventory_list": ["Widget 1", "Widget 2", "Widget 3"],
"greeting": "THAnk you FOR visitING.",
}
return render(request, "home.html", context)
class AboutPageView(TemplateView): # new
template_name = "about.html"
接下来,更新 pages/urls.py 文件以显示新视图。我们导入 AboutPageView 并设置到 about/ 的路由,同时指定 AboutPageView 作为视图。
# pages/urls.py
from django.urls import path
from .views import home_page_view, AboutPageView # new
urlpatterns = [
path("about/", AboutPageView.as_view()), # new
path("", home_page_view),
]
注意添加了 as_view() 方法,它返回一个可调用的视图。在为基于类的视图与基于函数的视图配置 URL 时,唯一的基本区别是必须添加此方法。
最后一步是创建我们的模板文件 about.html。使用文本编辑器,在现有的 templates 目录中添加这个新文件,包含以下代码:
<!-- templates/about.html -->
<h1>Company About Page</h1>
现在确保本地服务器正在运行,并在 Web 浏览器中导航到 127.0.0.1:8000/about/。

很简单,对吧?
get_context_data()¶
Django 中最强大、最有用、最常用的方法之一是 get_context_data()。它是在通用基于类的视图中更新模板上下文的推荐方法。让我们现在用它来为关于页面添加上下文数据。
# pages/views.py
...
class AboutPageView(TemplateView):
template_name = "about.html"
def get_context_data(self, **kwargs): # new
context = super().get_context_data(**kwargs)
context["contact_address"] = "123 Main Street"
context["phone_number"] = "555-555-5555"
return context
...
首先,我们覆盖现有的 get_context_data() 方法。第一个参数是 self,第二个是 **kwargs,允许我们传入关键字参数。这就是我们如何向上下文添加键/值对的方式。
下一步是设置一个名为 context 的变量,其中包含上下文的现有值。我们怎么做呢?对 get_context_data 调用 super() 并包含任何关键字参数。然后我们添加两个键 contact_address 和 phone_number,以及它们对应的值。最后一步始终是显式返回已更新的 context。
要在模板中渲染上下文变量,我们使用双花括号 {{ }}。
<!-- templates/about.html -->
<h1>Company About Page</h1>
<p>The company address is {{ contact_address }} and the phone number is
{{ phone_number }}.</p>
在 Web 浏览器中刷新关于页面以查看显示的信息。

使用通用基于类的视图目前可能看起来没有必要,但它们的真正威力将在下一章我们开始使用数据库时变得显而易见。
模板继承¶
本章全部关于模板和视图。我们已经涵盖了很多信息:模板上下文、模板标签和过滤器,以及基于类的视图。然而,模板还有一个更强大的特性,那就是它们可以被扩展。
如果你想想大多数网站,相同的内容出现在每个页面上(页眉、页脚等)。如果我们作为开发者,能有一个权威的地方存放页眉代码,让所有其他模板都继承它,那该多好啊!嗯,我们可以做到!
在 templates 目录中,创建一个 base.html 文件,包含带有指向主页和关于页面链接的页眉。这是我们的父模板,所有其他子模板都将从它继承。为了定义哪些区域可以被覆盖,我们将在语法中使用 block 标签 {% block content %} 和 {% endblock %}。block 标签内的任何内容都可以在子模板中被覆盖。
<!-- templates/base.html -->
<header>
<a href="/">Home</a> |
<a href="/about">About</a>
</header>
{% block content %}{% endblock %}
extends 标签允许我们通过指定父模板来建立模板之间的父/子关系。将其添加到 home.html 和 about.html 模板的顶部。然后用 {% block content %} 和 {% endblock %} 标签定义我们的子模板内容。
<!-- templates/home.html -->
{% extends "base.html" %}
{% block content %}
<h1>Company Homepage</h1>
<p>The current date and time is: {% now "DATETIME_FORMAT" %}</p>
<p>There are {{ inventory_list|length }} items of inventory.
<ul>
{% for item in inventory_list %}
<li>{{ item }}</li>
{% endfor %}
</ul>
<p>{{ greeting| title }}</p>
{% comment %}Add more content here!{% endcomment %}
{% endblock %}
<!-- templates/about.html -->
{% extends "base.html" %}
{% block content %}
<h1>Company About Page</h1>
<p>The company address is {{ contact_address }} and the phone number is
{{ phone_number }}.</p>
{% endblock %}
在浏览器中刷新每个网页以查看结果:


每个页面现在都包含带有主页和关于页面导航链接的 base.html 页眉。
命名 URL¶
有经验的 Web 开发者可能已经注意到我们当前页面链接方式的一个问题。我们在 views.py 和 urls.py 文件中硬编码了 URL 路径。在每个地方,我们为首页指定了 /,为关于页面指定了 about/。如果我们在一个地方更改了 URL 路径但在另一个地方没有更改会怎样?我们会得到页面未找到的 404 错误。
Django 认真对待为逻辑设置权威位置的理念。在本例中,我们希望引用一个 URL 及其关联的视图一次且仅一次。为此,我们可以在 URL 中添加 name。path() 函数接受以下参数:path(route, view, kwargs=None, name=None)。默认情况下,kwargs 和 name 设置为 None,但我们可以在这里更新 name。
# pages/urls.py
from django.urls import path
from .views import home_page_view, AboutPageView
urlpatterns = [
path("about/", AboutPageView.as_view(), name="about"), # new
path("", home_page_view, name="home"), # new
]
现在,每当我们想要引用特定的 URL 路径时,都可以通过内置的 url 模板标签在模板中使用命名 URL 来实现。使用下面的代码更新 base.html 文件。
<!-- templates/base.html -->
<header>
<a href="{% url 'home' %}">Home</a> |
<a href="{% url 'about' %}">About</a>
</header>
{% block content %}{% endblock %}
如果你在浏览器中刷新网站,两个页面及其链接都能像以前一样正常工作。URL 路径现在只在一个位置设置——在 urls.py 文件中。命名 URL 使你的项目更易于维护和修改,因为对 URL 模式的更改不需要在代码的多个位置进行修改。它们是一种最佳实践,应在所有 Django 项目中使用。
测试¶
在上一章中,我们为每个网页编写了一个单元测试,检查它是否返回了 HTTP 200 状态码。让我们在添加更强大的测试之前,先快速回顾一下。使用以下代码更新 pages/tests.py 文件:
# pages/tests.py
from django.test import SimpleTestCase
class HomepageTests(SimpleTestCase):
def test_url_exists_at_correct_location(self):
response = self.client.get("/")
self.assertEqual(response.status_code, 200)
class AboutpageTests(SimpleTestCase):
def test_url_exists_at_correct_location(self):
response = self.client.get("/about/")
self.assertEqual(response.status_code, 200)
然后,使用 Control+c 退出本地 Web 服务器,在命令行中输入 python manage.py test 来运行测试。
(.venv) $ python manage.py test
Found 2 test(s).
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.003s
OK
到目前为止一切顺利。公司网站与上一章的个人网站相比有什么变化?我们现在每个 URL 路由都有 URL 名称,所以我们应该检查它们是否按预期工作。我们可以使用方便的 Django 工具函数 reverse。它不是直接访问 URL 路径,而是查找 URL 名称。硬编码 URL 通常是个坏主意,尤其是在模板中。我们可以通过使用 reverse 来避免这种情况。在文档中可以查看进一步说明,我们将在后续章节中更多地使用 reverse。
在文本编辑器中打开现有的 pages/tests.py 文件,添加以下代码:
# pages/tests.py
from django.test import SimpleTestCase
from django.urls import reverse # new
class HomepageTests(SimpleTestCase):
def test_url_exists_at_correct_location(self):
response = self.client.get("/")
self.assertEqual(response.status_code, 200)
def test_url_available_by_name(self): # new
response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
class AboutpageTests(SimpleTestCase):
def test_url_exists_at_correct_location(self):
response = self.client.get("/about/")
self.assertEqual(response.status_code, 200)
def test_url_available_by_name(self): # new
response = self.client.get(reverse("about"))
self.assertEqual(response.status_code, 200)
在顶部,我们导入了 SimpleTestCase,因为我们没有使用数据库,然后导入了 reverse 函数。有两个测试类分别对应每个网页。HomepageTests 检查位于 / 的主页是否返回 200 状态码,然后检查对名为"home"的 URL 调用 reverse 是否也能达到同样效果。这两个测试的模式在 AboutpageTests 类中为关于页面重复了一遍。
运行测试以确认它们正常工作。
(.venv) $ python manage.py test
Found 4 test(s).
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 4 tests in 0.005s
OK
我们已经测试了 URL 位置和名称,但还没有测试模板。让我们确保在每个页面上使用了正确的模板 home.html 和 about.html,并且它们分别包含预期的文本 <h1>Company Homepage</h1> 和 <h1>Company About Page</h1>。我们可以使用 assertTemplateUsed 和 assertContains 来实现这一点。
# pages/tests.py
from django.test import SimpleTestCase
from django.urls import reverse
class HomepageTests(SimpleTestCase):
def test_url_exists_at_correct_location(self):
response = self.client.get("/")
self.assertEqual(response.status_code, 200)
def test_url_available_by_name(self):
response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
def test_template_name_correct(self): # new
response = self.client.get(reverse("home"))
self.assertTemplateUsed(response, "home.html")
def test_template_content(self): # new
response = self.client.get(reverse("home"))
self.assertContains(response, "<h1>Company Homepage</h1>")
class AboutpageTests(SimpleTestCase):
def test_url_exists_at_correct_location(self):
response = self.client.get("/about/")
self.assertEqual(response.status_code, 200)
def test_url_available_by_name(self):
response = self.client.get(reverse("about"))
self.assertEqual(response.status_code, 200)
def test_template_name_correct(self): # new
response = self.client.get(reverse("about"))
self.assertTemplateUsed(response, "about.html")
def test_template_content(self): # new
response = self.client.get(reverse("about"))
self.assertContains(response, "<h1>Company About Page</h1>")
最后一次运行测试以检查我们的新工作。一切都应该通过。
(.venv) $ python manage.py test
Found 8 test(s).
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 8 tests in 0.006s
OK
有经验的程序员可能会看我们的测试代码并注意到它是重复的。例如,我们在所有八个测试中都设置了 response。通常,遵循 DRY(不要重复自己)编程是个好主意,但单元测试在它们是独立的且高度详细的情况下效果最好。随着测试套件的扩展,出于性能原因,将多个断言合并到更少的测试中可能更有意义。
我们将来会在测试方面做更多的工作,尤其是在开始使用数据库之后。目前,重要的是要看到每次向 Django 项目添加新功能时添加测试是多么容易和重要。
Git 和 GitHub¶
现在是时候使用 Git 跟踪我们的更改并将它们推送到 GitHub 了。我们首先初始化目录并检查更改的状态。
(.venv) $ git init
(.venv) $ git status
然后,创建一个 .gitignore 文件来指示 Git 不要跟踪哪些内容。我们将关注三个方面:包含虚拟环境的 .venv 目录、包含编译字节码的 __pycache__ 目录,以及数据库文件 db.sqlite3。
# .gitignore
.venv/
__pycache__/
db.sqlite3
下一步是创建一个 requirements.txt 文件,列出虚拟环境的内容。
(.venv) $ pip freeze > requirements.txt
(.venv) $ git status
最后一步是再次运行 git status 以确认 requirements.txt 已包含在内,而 .gitignore 文件中的三个项目被忽略了。然后,添加所有预期的文件和目录,并附带初始提交消息。
(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "initial commit"
在 GitHub 上创建一个名为 company-website 的新仓库,确保选择"Private"单选按钮。然后点击"Create repository"按钮。
在下一个页面上,向下滚动到"or push an existing repository from the command line"的位置。将那里的两个命令复制粘贴到你的终端中。
它应该看起来像下面的内容,当然用户名不是 wsvincent,而是你的 GitHub 用户名。
(.venv) $ git remote add origin https://github.com/wsvincent/company-website.git
(.venv) $ git branch -M main
(.venv) $ git push -u origin main
结论¶
恭喜你构建并部署了第三个 Django 项目!这次,我们同时使用了基于函数的视图和通用基于类的视图来构建网站。我们整合了模板继承、命名 URL,并添加了更高级的测试。本章的完整源代码可在 GitHub 上获取,供你参考。在下一章中,我们将转向第一个数据库驱动的项目——一个留言板网站,并看看 Django 真正大放异彩的地方。