第 3 章:个人网站

在本章中,我们将构建一个包含首页和关于页面的个人网站,同时深入学习 Django 的模板、基于函数的视图(FBV)和测试。模板是控制数据如何展示的表现层,同时它们还支持继承和基本逻辑,这符合 Django 的 DRY(Don’t Repeat Yourself,不要重复自己)设计理念。视图将 URL 和模板结合起来,并在数据上增加大量逻辑;它们通常被认为是 Django 应用中的逻辑层。第三个基本概念是测试,这对任何 Web 项目都至关重要,并且在 Django 中得到了很好的支持。到本章结束时,你将学会如何使用简单的基于函数的视图和模板,并编写你的第一个测试。

初始设置

我们的初始设置与上一章类似,包含以下步骤:

  • 为代码创建一个名为 personal_website 的新目录并进入该目录
  • 创建一个名为 .venv 的新虚拟环境并激活它
  • 安装 Django 和 Black
  • 创建一个名为 django_project 的新 Django 项目
  • 创建一个名为 pages 的新应用

在新的命令行终端中,导航到桌面上的 code 文件夹,创建一个名为 personal_website 的新文件夹。在 Windows 的命令行提示符 > 或 macOS 的 % 之前,你应该看不到表示活跃虚拟环境的 (.venv)。如果你看到了,说明你还在现有的虚拟环境中,请输入 deactivate 退出它。然后进入 personal_website 目录,创建一个新的虚拟环境并激活它。

# Windows
$ cd onedrive\desktop\code
$ mkdir personal_website
$ cd personal_website
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $

# macOS
$ cd ~/desktop/code
$ mkdir personal_website
$ cd personal_website
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $

接下来,安装 Django 和 Black,创建一个名为 django_project 的新项目,并创建一个名为 pages 的新应用。到本书结束时,这些命令会变得非常熟悉,唯一变化的是你的项目名或应用名。

(.venv) $ python -m pip install django~=5.0.0 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",  # 新增
]

使用 migrate 初始化数据库)/),然后用 runserver 启动本地 Web 服务器。

(.venv) $ python manage.py migrate
(.venv) $ python manage.py runserver

然后,导航到 http://127.0.0.1:8000/ 查看 Django 欢迎页面。只用了几条命令,我们就拥有了一个在虚拟环境中运行的全新 Django 项目,安装了 Black,以及一个 pages 应用。

首页

Django 的”视图”是一个接受 Web 请求并返回 Web 响应的 Python 函数。当请求一个 Web 页面时,Django 会自动创建一个包含请求元数据的 HttpRequest 对象。视图则返回一个 HttpResponse 对象。

我们将从重复上一章的步骤开始,仅使用视图和 URL 调度器创建一个首页。然后,我们将使用模板和更复杂的视图逻辑创建一个关于页面。

先从视图开始。Django 在应用内预填充的 views.py 文件包含以下代码:

# pages/views.py
from django.shortcuts import render

# Create your views here.

我们将在关于页面中使用 render,所以保留该导入即可。对于首页,我们将重复上一章的步骤:导入 HttpResponse 类,创建一个名为 home_page_view 的视图,将第一个参数(始终是 HttpRequest 对象!)命名为 request,然后返回文本 "Homepage"

# pages/views.py
from django.http import HttpResponse
from django.shortcuts import render

def home_page_view(request):
    return HttpResponse("Homepage")

下一步是在 pages 应用中创建一个 urls.py 文件,从 django.urls 导入 path,从本地目录的 views.py 文件中导入 home_page_view,并在空字符串 "" 处设置路由,调用 home_page_view

# pages/urls.py
from django.urls import path
from .views import home_page_view

urlpatterns = [
    path("", home_page_view),
]

最后一步是更新项目级别的 urls.py 文件,这是所有 URL 请求的入口点。我们导入 include 函数来包含其他 URL 配置,并为 pages 应用设置一个 "" 的 URL 路由。

# django_project/urls.py
from django.contrib import admin
from django.urls import path, include  # 新增

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("pages.urls"),  # 新增
]

这样就完成了!如果本地服务器仍在命令行中运行,你应该可以在浏览器中访问首页:

!

基于函数的视图:关于页面

render() 是一个 Django 快捷函数,用于处理模板。[render()](https://docs.djangoproject.com/en/5.0/topics/http/shortcuts/#render) 的第一个参数是request对象,第二个参数是模板名称。这意味着如果我们想创建一个使用名为about.html` 的模板的关于页面视图,我们可以这样编写:

# pages/views.py
from django.http import HttpResponse
from django.shortcuts import render

def home_page_view(request):
    return HttpResponse("Homepage")

def about_page_view(request):  # 新增
    return render(request, "pages/about.html")

新的视图命名为 about_page_view,其第一个参数是 HttpRequest 对象,我们将其命名为 request。它使用 render() 函数返回 request 对象和一个相关的模板 "pages/about.html"

模板

模板是 Django 中的表现层,提供了一种便捷的方式来生成 HTML 文件,这些文件还可以包含 CSS、JavaScript 以及图片等媒体资源。默认情况下,Django 的模板加载引擎会在每个应用中查找 templates 子目录。这意味着我们可以在 pages 应用中添加一个 templates 目录,然后像下面这样放置 about.html

└── pages
    ├── templates
    │   ├── about.html

这种方法虽然可行,但不是最佳实践。Django 会选择它找到的第一个名称匹配的模板。如果两个不同的应用中都有 about.html 文件,会发生什么?Django 无法轻易判断在我们的函数中应该使用哪一个。幸运的是,有一个简单的解决方法:将模板文件放在另一个包含应用名称的目录中,以实现命名空间化。换句话说:apptemplatesapptemplate file

在我们的 pages 应用中,结构如下所示:

└── pages
    ├── templates
    │   ├── pages
    │   │   ├── about.html

作为 Django 的最佳实践,在应用中存储模板文件时,应始终采用这种方法。你可以在文本编辑器中或直接在命令行中创建这些新目录:

(.venv) $ mkdir pages/templates
(.venv) $ mkdir pages/templates/pages

然后,在 pages/templates/pages 目录中添加一个名为 about.html 的新文件。在 Visual Studio Code 中,可以通过鼠标导航到屏幕左上角,点击”File”,然后点击”New File”来完成。确保在正确的位置命名并保存文件。

about.html 文件将包含一个 <h1> 标签作为标题和一个 <p> 标签作为段落文本。

<!-- pages/templates/pages/about.html -->
<h1>About page</h1>
<p>This is the new template-powered About page.</p>

我们的模板完成了!视图 about_page_view 在被调用时可以访问该模板,但还剩下一步:配置 URL 调度器。

URL 调度器

此时,添加新 URL 路由的模式应该开始变得熟悉了。我们导入视图 about_page_view,然后设置其路径 about/ 和视图名称。

# pages/urls.py
from django.urls import path
from .views import home_page_view, about_page_view  # 新增

urlpatterns = [
    path("about/", about_page_view),  # 新增
    path("", home_page_view),
]

就这样!使用 runserver 命令启动开发 Web 服务器。

(.venv) $ python manage.py runserver

如果你导航到 http://127.0.0.1:8000/about/,将看到关于页面。

! ## Django 模板语言

所有的 Web 框架(包括 Django)都需要一种动态生成 HTML 的方式,最常见的方法是使用包含静态内容和特殊语法(用于插入动态内容)的模板。Django 有自己的模板语言,它并不试图取代 Vue、React 或 Angular 等专用的 JavaScript 前端。相反,它是一种刻意有限制但依然强大的方式,用于使用注释、变量、过滤器、标签和模板继承。

让我们重新审视 about.html 模板,试用其中一些功能。我们先从注释标签开始,它允许我们添加开发者可见但不会在页面上显示的代码。{% comment %}{% endcomment %} 标签之间的任何内容都会被忽略。

<!-- pages/templates/pages/about.html -->
<h1>About page</h1>
<p>This is the new template-powered About page.</p>
{% comment %}This is just a comment. It won't appear on the web page!{% endcomment %}

如果你保存文件并刷新浏览器,注释将不可见。它不会被渲染到发送给 about_page_view 然后发送到浏览器的模板 HTML 中。如果你想要添加多行注释,comment 标签可以实现,但总的来说,Django 模板不应该过于复杂。虽然可以在模板中添加基本逻辑,但最好将繁重的逻辑处理移到视图或模型文件中(我们将在本书后面做这些)。

模板上下文

每当渲染 Django 模板时,同时会创建一个上下文。这个上下文是一个类似字典的对象,变量名作为键,变量值作为值。我们可以(而且将会)用信息更新这个上下文,这些信息通常来自数据库。当你使用给定的上下文渲染模板时,上下文字典中的每个键都会成为模板中的一个变量,你可以访问和使用它。

render() 快捷函数期望的参数顺序如下:

  1. HttpRequest 对象(通常命名为 request
  2. template_name(模板名称)
  3. context 字典(上下文)

换句话说:render(request, template_name, context)。默认情况下,context 设置为 None,但我们可以手动添加信息来查看效果。

# pages/views.py
from django.http import HttpResponse
from django.shortcuts import render

def home_page_view(request):
    return HttpResponse("Homepage")

def about_page_view(request):
    context = {"name": "Alice"}  # 新增
    return render(request, "pages/about.html", context)  # 新增

这里,我们添加了一个变量 context,包含一个键为 "name"、值为 "Alice" 的字典。要在模板中显示上下文,变量的语法是用双大括号 { } 包围变量名。由于键值是 "name",我们输入 { name } 来使其可见。

<!-- pages/templates/pages/about.html -->
<h1>About page</h1>
<p>My name is {{ name }}.</p>

如果你刷新浏览器,现在将显示名字”Alice”。

! 作为练习,在 pages/views.py 文件中将名字”Alice”替换为你自己的名字。然后刷新浏览器,看它是否显示在网页上。

我们还可以添加上下文中其他的键/值对。例如,让我们添加一个”age”。

# pages/views.py
from django.http import HttpResponse
from django.shortcuts import render

def home_page_view(request):
    return HttpResponse("Homepage")

def about_page_view(request):
    context = {
        "name": "Alice",
        "age": 33,  # 新增
    }
    return render(request, "pages/about.html", context)

要在网页上显示年龄,我们可以在模板中使用 { age } 语法作为变量来显示。注意,name 是字符串(因为它在引号内),而 33 是整数。在 Python 代码和 Django 应用中设置数据类型很重要,我们稍后会探讨这一点。

<!-- pages/templates/pages/about.html -->
<h1>About page</h1>
<p>My name is {{ name }}. I am {{ age }} years old.</p>

再次刷新关于页面以查看更新。

! 在 pages/views.py 文件中,将年龄变量更新为你自己的年龄并刷新网页查看更新结果。任何作为变量传递给模板上下文的内容都可以在我们的网页上渲染。

Django 模板语言还有超过八十个内置标签内置过滤器,提供额外的功能。我们将在本书后面更深入地探讨它们。

测试

为代码编写测试与编写代码本身同样重要。没有配套的测试,你就无法向开源项目或任何管理规范的公司提交新的代码变更。原因很简单:虽然你的代码变更可能很小,但无法预知它可能无意中破坏项目中的其他内容。编写良好的测试能让你对代码库充满信心,不会花费太多时间,并且可以自动化,在任何新代码变更时自动运行。用 Django 联合创始人 的话来说:“没有测试的代码,按照设计就是有缺陷的。”

测试可以分为两大类:单元测试和集成测试。单元测试检查单个独立功能,而集成测试检查多个关联部分。单元测试运行速度更快且更易于维护,因为它们只关注少量代码。集成测试运行速度较慢且更难以维护,因为失败不会直接指出具体的原因。大多数开发者专注于编写大量的单元测试和少量的集成测试。

下一个问题是:测试什么?每当你创建新功能时,都需要一个测试来确认它按预期工作。例如,在我们的项目中,我们有首页和关于页面,应该测试这两个页面在预期的 URL 上存在。现在看起来可能没有必要,但随着项目规模的增长,无法预知会发生什么变化。

Python 标准库包含一个名为 unittest 的内置测试框架,该框架提供了 TestCase 实例以及大量的 assert 方法用于检查并报告失败。Django 的测试框架在 Python 的 unittest.TestCase 基类之上提供了多个扩展。其中包括用于发出模拟浏览器请求的测试客户端、几个 Django 特定的额外断言方法,以及四个测试用例类:SimpleTestCaseTestCaseTransactionTestCaseLiveServerTestCase。 一般来说,当不需要数据库时使用 SimpleTestCase,而想要测试数据库时使用 TestCaseTransactionTestCase 有助于直接测试数据库事务,而 LiveServerTestCase 则启动一个实时服务器线程,用于测试基于浏览器的工具(如 Selenium)。

注意:你可能已经注意到 unittestdjango.test 中的方法使用驼峰命名法(camelCase),而不是更符合 Python 风格的下划线命名法(snake_case)。原因是 unittest 基于 Java 的 jUnit 测试框架,该框架使用驼峰命名法,所以当 Python 添加 unittest 时,它也沿用了驼峰命名法。

如果你查看 pages 应用,Django 已经提供了一个 tests.py 文件供我们使用。由于我们的项目不涉及数据库,我们将在文件顶部导入 SimpleTestCase。对于我们的第一个测试,我们将检查网站的两个 URL(首页和关于页面)是否返回 HTTP 200 状态码——这是成功 HTTP 请求的标准响应。

# 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 退出服务器,然后在命令行中输入 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

如果你看到类似 AssertionError: 301 != 200 的错误,很可能是忘记在上面的 "/about" 后面添加尾部斜杠。浏览器知道在没有提供斜杠时自动添加,但这会导致 301 重定向,而不是 200 成功响应!

Git 和 GitHub

是时候用 Git 跟踪我们的更改并将它们推送到 GitHub 了。我们首先初始化目录并检查更改的状态。

(.venv) $ git init
(.venv) $ git status

在上一章中我们了解到,通过创建 .gitignore 文件,我们可以指示 Git 忽略某些文件和目录。我们将包含包含虚拟环境的 .venv 目录。但我们也忽略 __pycache__ 目录(包含编译后的字节码)和数据库本身 db.sqlite3。除了体积相当大之外,将基于文件的数据库包含在源代码控制中还存在安全风险。在一个大型团队中,对项目设置不同级别的访问权限是很常见的。通常,每个开发者都可以查看源代码,因为任何错误都可以通过 Git 快速回滚和更改。回滚生产数据库的更改要困难得多,而且大多数开发者只要有本地或测试数据库可用,就不需要访问它。因此,最佳实践是将数据库访问与源代码分开,即使对于像 SQLite 这样基于文件的数据库也是如此,所以我们在这里也会这样做。创建一个新的 .gitignore 文件并添加以下三行:

.venv/
__pycache__/
db.sqlite3

再次运行 git status 确认这三个文件被忽略。我们想要记录虚拟环境中已安装的包,这可以通过创建 requirements.txt 文件来实现。

(.venv) $ pip freeze > requirements.txt

最后运行一次 git status 确认新创建的 requirements.txt 文件是可见的。然后,添加所有预期的文件和目录,并附上一个初始提交信息。

(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "initial commit"

GitHub 上创建一个名为 personal_website 的新仓库,确保选择”Private”单选按钮。然后点击”Create repository”按钮。

在下一页上,滚动到”…or push an existing repository from the command line”处。将那里的两条命令复制粘贴到你的终端中。

命令看起来应该像下面这样,只是用户名 wsvincent 会被替换为你的 GitHub 用户名。

(.venv) $ git remote add origin https://github.com/wsvincent/personal-website.git
(.venv) $ git branch -M main
(.venv) $ git push -u origin main

结论

恭喜你构建了第二个 Django 网站。本章的完整源代码可在 GitHub 上找到,供你参考。虽然这个网站相当基础,但我们已经探索了几个基本概念,包括基于函数的视图、模板、URL 调度器和测试。我们还再次使用了 Git,创建了 requirements.txt.gitignore 文件,并首次将代码推送到 GitHub。有了这个基础,我们可以更快地进入下一章,在那里我们将使用基于类的视图、模板继承和更多的测试来构建一个公司网站。