第 11 章:Bootstrap¶
Web 开发需要多种技能。你不仅要正确地编写网站程序,用户还期望它看起来美观。从头开始创建一个漂亮网站所需的所有 HTML/CSS 可能会让人感到不知所措。
虽然手动编写现代网站所需的所有 CSS 和 JavaScript 是可行的,但在实践中,大多数开发者会使用像 Bootstrap 或 TailwindCSS 这样的框架。我们将在项目中使用 Bootstrap,它可以根据需要进行扩展和自定义。
Pages 应用¶
在上一章中,我们通过直接在 urls.py 文件中编写视图逻辑来展示主页。虽然这种方法可行,但我觉得它有些 hack,而且随着网站的增长,这种方式显然无法扩展;对于 Django 新手来说也容易感到困惑。相反,我们可以也应该创建一个专门的 pages 应用来管理所有静态页面,例如主页、将来的关于页面等。这样可以让我们的代码保持整洁有序。
在命令行上,使用 startapp 命令创建新的 pages 应用。如果服务器仍在运行,你必须先按 Control+c 退出。
(.venv) $ python manage.py startapp pages
然后,立即更新 django_project/settings.py 文件。我经常忘记这一步,所以最好把创建新应用看作一个两步过程:运行 startapp 命令,然后更新 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",
"accounts",
"pages", # new
]
现在,我们可以更新 django_project 目录中的 urls.py 文件,添加 pages 应用并移除 TemplateView 的导入和之前主页的 URL 路径。
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("accounts.urls")),
path("accounts/", include("django.contrib.auth.urls")),
path("", include("pages.urls")), # new
]
是时候添加我们的主页了,这意味着 Django 标准的 URL/视图/模板三步操作。我们从 pages/urls.py 文件开始。首先,用文本编辑器创建它。然后导入我们尚未创建的视图,设置路由路径,并为每个 URL 命名。
# pages/urls.py
from django.urls import path
from .views import HomePageView
urlpatterns = [
path("", HomePageView.as_view(), name="home"),
]
此时 views.py 的代码应该看起来很熟悉。我们使用的是 Django 的 TemplateView 泛型类视图,这意味着我们只需指定 template_name 就能使用它。
# pages/views.py
from django.views.generic import TemplateView
class HomePageView(TemplateView):
template_name = "home.html"
我们已经有一个现成的 home.html 模板。让我们确认它在新的 URL 和视图下仍然正常工作。启动本地服务器 python manage.py runserver 并导航到主页 http://127.0.0.1:8000/ 确认它保持不变。它应该显示你已登录的超级用户账户的名称和年龄,这是我们上一章末尾使用过的。
测试¶
我们添加了新的代码和功能,所以是时候编写测试了。项目中的测试永远不嫌多。虽然编写测试需要一些前期时间,但从长远来看它们总是能为你节省时间,并在项目复杂度增长时为你提供信心。
让我们添加测试来确保新主页正常工作。pages/tests.py 文件中的代码应该如下所示:
# pages/tests.py
from django.test import SimpleTestCase
from django.urls import reverse
class HomePageTests(SimpleTestCase):
def test_url_exists_at_correct_location_homepageview(self):
response = self.client.get("/")
self.assertEqual(response.status_code, 200)
def test_homepage_view(self):
response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "home.html")
self.assertContains(response, "Home")
在最上面一行,我们导入 SimpleTestCase,因为我们的主页不依赖数据库。如果依赖数据库,我们就必须使用 TestCase。然后,我们导入 reverse 来测试 URL 和视图。我们的测试类 HomePageTests 包含两个测试,检查主页 URL 返回 200 状态码,以及它使用了我们预期的 URL 名称、模板,并且响应中包含"Home"。
按 Control+c 退出本地服务器,然后运行测试确认全部通过。
(.venv) $ python manage.py test
Found 7 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.185s
OK
Destroying test database for alias 'default'...
测试哲学¶
在应用程序中,你可以测试的内容没有限制。例如,我们现在还可以根据登录或注销状态以及模板是否显示正确内容来添加测试。但 80/20 法则——即 80% 的结果来自 20% 的原因——同样适用于测试和生活中的大多数事情。对于 Web 应用程序来说,编写尽可能多的单元测试来测试那些可能永远不会失败的东西是没有意义的。如果我们在开发核反应堆,尽可能多的测试是有意义的,但大多数网站的风险要低得多。
因此,虽然你总是希望围绕新功能添加测试,但从一开始只拥有部分测试覆盖是可以接受的。随着新的 Git 分支和功能不可避免地出现错误,为每个错误添加一个测试,这样它们就不会再次失败。这种方法被称为回归测试,即每次有新更改时重新运行测试,以确保之前开发和测试的软件按预期运行。
Django 的测试套件非常适合进行大量单元测试和自动回归测试,因此开发者可以对项目的一致性充满信心。
Bootstrap¶
现在是为我们的应用程序添加样式的时候了。如果你从未使用过 Bootstrap,那你可要大开眼界了。就像 Django 一样,它用很少的代码就能完成很多事情。
有两种方法可以将 Bootstrap 添加到项目中:下载并在本地提供所有文件,或者依赖内容分发网络(CDN)。第二种方法实现起来更简单,只要你有稳定的互联网连接即可,所以我们在这里使用它。
我们的模板将模仿 Bootstrap 入门页面上提供的"Starter template",需要添加以下内容:
- 在
<head>顶部的meta name="viewport"和 content 信息 - 在
<head>中的 Bootstrap CSS 链接 - 在
<body>底部的 Bootstrap JavaScript bundle
建议你自己手动输入所有代码,但添加 Bootstrap CDN 是个例外,因为它很长且容易打错。我建议从 Bootstrap 网站复制粘贴 Bootstrap CSS 和 JavaScript Bundle 链接到 base.html 文件中。
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Newspaper App{% endblock title %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/
bootstrap.min.css" rel="stylesheet" integrity="sha384..."
crossorigin="anonymous">
</head>
<body>
<main>
{% block content %}
{% endblock content %}
</main>
<!-- Bootstrap JavaScript Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/
bootstrap.bundle.min.js" integrity="sha384..." crossorigin="anonymous">
</script>
</body>
</html>
这段代码片段没有包含 Bootstrap CSS 和 JavaScript 的完整链接;它是缩写的。请从快速入门文档中复制粘贴 Bootstrap 5.3 的完整链接。
如果你用 python manage.py runserver 重新启动服务器并刷新主页 http://127.0.0.1:8000/,你会看到字体大小和链接颜色已经改变。
让我们在页面顶部添加一个导航栏,包含主页、登录、注销和注册页面的链接。值得注意的是,我们可以使用 Django 模板引擎中的 if/else 标签来添加一些基本逻辑。我们希望"Log in"和"Sign up"按钮在用户注销时显示,而"Log out"和"Change password"按钮在用户登录时显示。
同样,这里可以复制粘贴,因为本书的重点是学习 Django,而不是 HTML、CSS 和 Bootstrap。如果有任何格式问题,你可以参考官方 GitHub 仓库。
<!-- templates/base.html -->
...
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'home' %}">Newspaper</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
{% if user.is_authenticated %}
<li><a href="#" class="nav-link px-2 link-dark">+ New</a></li>
</ul>
<div class="mr-auto">
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
{{ user.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'password_change' %}">
Change password</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<form method="post" action="{% url 'logout' %}"
style="display:inline;">
{% csrf_token %}
<button type="submit" class="btn btn-link nav-link"
style="display:inline; cursor:pointer;">Logout</button>
</form>
</li>
</ul>
</li>
{% else %}
</ul>
</div>
<div class="mr-auto">
<form class="form d-flex">
<a href="{% url 'login' %}" class="btn btn-outline-secondary">Log in</a>
<a href="{% url 'signup' %}" class="btn btn-primary ms-2">Sign up</a>
</form>
</div>
{% endif %}
</div>
</div>
</nav>
<main>
<div class="container">
{% block content %}
{% endblock content %}
</div>
</main>
...
如果你刷新主页 http://127.0.0.1:8000/,我们的新导航栏神奇地出现了!注意,目前还没有"+ New"新文章的实际链接;代码中用 href="#" 表示一个占位符。我们稍后会添加它。另外,注意我们已登录的用户名现在出现在右上角,带有一个下拉箭头。如果你点击它,会看到"Change password"和"Log Out"的链接。

如果你在下拉菜单中点击"Log Out",导航栏会变成"Log In"或"Sign Up"的按钮链接,"+ New"链接消失了。让已注销的用户创建文章没有意义。

如果你点击顶部导航栏中的"Log In"按钮,你还可以看到我们在 http://127.0.0.1:8000/accounts/login 的登录页面看起来更好了。

唯一看起来不太协调的是灰色的"Log In"按钮。我们可以使用 Bootstrap 来添加一些漂亮的样式,比如把它变成绿色,更有吸引力。修改 templates/registration/login.html 文件中的"button"行。
<!-- templates/registration/login.html -->
{% extends "base.html" %}
{% block title %}Log In{% endblock title %}
{% block content %}
<h2>Log In</h2>
<form method="post">{% csrf_token %}
{{ form }}
<!-- new code here -->
<button class="btn btn-success ms-2" type="submit">Log In</button>
<!-- end new code -->
</form>
{% endblock content %}
现在刷新页面,看看我们新按钮的效果。

注册表单¶
如果你点击"Sign Up"链接,你会看到页面已经有了 Bootstrap 样式和令人分心的帮助文本。例如,在"Username"后面写着"Required. 150 characters or fewer. Letters, digits, and @/./+/-/_ only."那么,这些文本是从哪里来的呢?当你在 Django 中遇到看起来像"魔法"的东西时,请放心,它绝对不是魔法。如果你没有写过这段代码,那它一定存在于 Django 的某个地方。
弄清楚 Django 底层运行机制的最好方法是下载源代码并亲自查看。所有的代码开发都在 GitHub 上进行,你可以在 https://github.com/django/django/ 找到 Django 仓库。要将其复制到你本地的计算机上,在命令行中打开一个新标签页(Command+t)并导航到所需位置。让我们现在就导航过去,因为我们一直在使用桌面上的 code 文件夹。
# Windows
$ cd onedrive\desktop\code
# macOS
$ cd ~/desktop/code
在 GitHub 网站上,你会看到一个绿色的"<> Code"按钮。点击它并选择"GitHub CLI"来查看下载仓库的命令行指令。

在命令行上,输入 gh repo clone django/django 将 Django 源代码复制到你计算机上一个名为 django 的新目录中。
$ gh repo clone django/django
将 Django 源代码保存在你的计算机上需要不时手动更新以保持最新。毕竟,Django 会定期更新以修复错误、提高安全性和添加新功能。为什么不直接使用内置的 GitHub 搜索呢?在 GitHub 上搜索有时有效,但最近一直不太稳定,GitHub 也意识到了这一点并正在努力改进。下载源代码并自行搜索是非常有价值的工具,因此值得花时间学习如何操作,就像我们现在做的一样。
在你的文本编辑器中,打开 Django 源代码来执行搜索。例如,在 VS Code 文本编辑器中按 Command+Shift+F 键在所有文件中进行"查找"搜索。输入"150 characters or fewer"进行搜索,你会找到指向 django/contrib/auth/models.py 页面的顶部链接。具体代码在第 350 行,该文本是 auth 应用中 AbstractUser 的 username 字段的一部分。
注意: 我们现在有两个命令行标签页打开:一个用于项目代码,一个用于 Django 源代码。确保你清楚哪个是哪个。将来,我们还会对源代码进行额外搜索,但本书中的所有代码都需要切换回带有项目源代码的终端标签页。切换回来,因为我们即将向项目中添加更多代码。
我们现在有三个选择:
- 覆盖现有的
help_text - 隐藏
help_text - 重新设置
help_text的样式
我们选择第三个选项,因为这是介绍优秀的第三方包 django-crispy-forms 的好方式。
处理表单是具有挑战性的,而 django-crispy-forms 使编写 DRY(Don't Repeat Yourself,不要重复自己)代码变得更容易。首先,按 Control+c 停止本地服务器。然后使用 pip 在项目中安装该包。我们还将安装 Bootstrap5 模板包。
(.venv) $ python -m pip install django-crispy-forms==2.2
(.venv) $ python -m pip install crispy-bootstrap5==2024.2
在 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",
# 3rd Party
"crispy_forms", # new
"crispy_bootstrap5", # new
# Local
"accounts",
"pages",
]
然后,在 settings.py 文件的底部,也添加两行新代码。
# django_project/settings.py
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" # new
CRISPY_TEMPLATE_PACK = "bootstrap5" # new
现在在我们的 signup.html 模板中,我们可以快速使用 crispy forms。首先,在顶部加载 crispy_forms_tags,然后将 {{ form }} 替换为 {{ form|crispy }}。我们还将把"Sign Up"按钮更新为绿色的 btn-success 样式。
<!-- templates/registration/signup.html -->
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block title %}Sign Up{% endblock title%}
{% block content %}
<h2>Sign Up</h2>
<form method="post">{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit">Sign Up</button>
</form>
{% endblock content %}
如果你用 python manage.py runserver 重新启动服务器并刷新注册页面,我们可以看到新的变化。

我们还可以在登录页面中添加 crispy forms。过程是一样的。以下是更新后的代码:
<!-- templates/registration/login.html -->
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block title %}Log In{% endblock title %}
{% block content %}
<h2>Log In</h2>
<form method="post">{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success ms-2" type="submit">Log In</button>
</form>
{% endblock content %}
刷新登录页面,就能看到更新效果。

Git 和 requirements.txt¶
我们现在已经向 Django 项目添加了几个包,所以是时候创建一个 requirements.txt 文件了。
(.venv) $ pip freeze > requirements.txt
目前,我的文件内容如下:
# requirements.txt
asgiref==3.8.1
black==24.4.2
click==8.1.7
crispy-bootstrap5==2024.2
Django==5.0.6
django-crispy-forms==2.2
mypy-extensions==1.0.0
packaging==24.1
pathspec==0.12.1
platformdirs==4.2.2
sqlparse==0.5.0
然后我们可以做一个快速的 Git 提交来保存本章的工作并将其存储在 GitHub 上。
(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "add Bootstrap styling"
(.venv) $ git push origin main
结论¶
我们的 Newspaper 应用看起来不错了。为了改善表单的外观,我们向网站添加了 Bootstrap 和 Django Crispy Forms。用户身份验证流程的最后一步是配置密码更改和重置。同样,Django 已经处理了繁重的工作,只需要我们编写最少的代码。