第 11 章:Bootstrap

Web 开发需要多种技能。不仅要正确编写网站程序,用户还期望它看起来美观。如果一切从零开始创建,添加所有必要的 HTML/CSS 来构建一个漂亮的网站可能会让人不知所措。

虽然手工编写现代网站所需的所有 CSS 和 JavaScript 是可能的,但在实践中,大多数开发者都会使用像 BootstrapTailwindCSS 这样的框架。我们将在项目中使用 Bootstrap,它可以根据需要进行扩展和定制。

Pages 应用

在上一章中,我们通过将视图逻辑包含在 urls.py 文件中来显示首页。虽然这种方法可行,但在我看来感觉不够优雅,而且随着网站的发展,它肯定不具备可扩展性;对于 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",  # 新增
]

现在,我们可以更新 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")),  # 新增
]

是时候添加我们的首页了,这意味着 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 介绍页面上的”入门模板”,需要添加以下内容:

  • <head> 顶部添加 meta name="viewport" 和 content 信息
  • <head> 中添加 Bootstrap CSS 链接
  • <body> 部分底部添加 Bootstrap JavaScript 包

自己键入所有代码是推荐的,但添加 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>
                </ul>
            </div>
            {% 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”的链接。

已登录的 Bootstrap 导航首页

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

已注销的 Bootstrap 导航首页

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

Bootstrap 登录页面

唯一看起来不太对劲的是灰色的”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 }}
    <!-- 新代码开始 -->
    <button class="btn btn-success ms-2" type="submit">Log In</button>
    <!-- 新代码结束 -->
</form>
{% endblock content %}

现在刷新页面,看看我们的新按钮的效果。

带新按钮的 Bootstrap 登录页面

注册表单

如果你点击”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”查看下载仓库的命令行说明。

GitHub Django 仓库

在命令行中,输入 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 应用中 AbstractUserusername 字段的一部分。

注意:我们现在打开了两个命令行标签页:一个用于项目代码,一个用于 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",
    # 第三方
    "crispy_forms",  # 新增
    "crispy_bootstrap5",  # 新增
    # 本地
    "accounts",
    "pages",
]

然后在 settings.py 文件的底部添加两行新代码。

# django_project/settings.py
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"  # 新增
CRISPY_TEMPLATE_PACK = "bootstrap5"  # 新增

现在在我们的 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 的注册页面

我们还可以将 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 %}

刷新登录页面,更新就会显示出来。

使用 Crispy Forms 的登录页面

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

总结

我们的新闻应用看起来不错。为了改善表单的外观,我们向网站添加了 Bootstrap 和 Django Crispy Forms。用户认证流程的最后一步是配置密码更改和重置。同样,Django 已经处理了大部分繁重的工作,我们只需要编写最少的代码。