跳转至

第 2 章:Hello, World 网站

在本章中,我们将回顾网站和 Web 框架的工作原理,了解 Django 的架构,并构建一个简单的 Django 网站来显示"Hello, World"。我们还将学习 Django 的两个基本组成部分——URL 映射器和视图,并首次使用 Git。本章及所有后续章节的完整源代码可在本书的官方 GitHub 仓库上获取。

互联网的工作原理

你可能每天都在使用互联网,但除非你是 Web 开发者,否则你不太可能完全了解当你在浏览器中输入 https://learndjango.com 这样的地址并按回车键时发生的完整细节。支撑这种用户体验的是一个由通信协议、Web 服务器和逻辑组成的复杂网络。

计算机科学专业的学生通常要学习一整门关于网络通信的课程,也有专注于这一领域的网络工程师。作为 Web 开发者,一般性的理解就足以入门,但如果你最终在高流量网站上工作,网络通信的细节和细微差别将越来越重要。

支撑互联网的是一个由相互连接的机器组成的网络,称为服务器。这些是特殊的计算机,没有屏幕、鼠标或键盘,通常放置在数据中心的服务器机架中。在过去,Web 开发者必须运行自己的物理服务器,但现在更常见的是从大型托管公司租用空间,俗称"云"。可视化这个连接机器网络的一种方式是通过海底电缆地图,它交互式地展示了连接各大洲和国家的海底电缆。

海底电缆地图

让我们追踪一下当你尝试访问 https://learndjango.com 时会发生什么。我们可以将其简化为六个步骤:

  1. 你在浏览器中输入一个域名
  2. 浏览器通过 DNS(域名系统)查找该域名对应的 IP 地址
  3. 浏览器与 Web 服务器建立网络连接
  4. 浏览器发送一个 HTTP 请求来获取所需的资源(例如主页)
  5. 网站处理请求(下文详述)并返回 HTTP 响应
  6. 浏览器开始渲染网页

HTTP(超文本传输协议)是计算机通过互联网通信以驱动网站的规则集。它由万维网的发明者 Tim Berners-Lee 创建,他还创建了 HTML 标记语言和 URL 系统。一旦收到 HTTP 请求,Web 浏览器就开始使用 HTML 渲染网页。网页所需的任何额外资源或额外的网页都会经历相同的 HTTP 请求和 HTTP 响应周期,直到用户离开网站并关闭网络连接。

当你请求像 learndjango.com 这样的网站时,你的 Web 浏览器首先请求 DNS(域名系统)将域名转换为 IP(互联网协议)地址——一个计算机用来在彼此之间进行查找的唯一数字标识符。一旦 Web 浏览器知道了 IP 地址,它就会与该 IP 地址上包含所需网站内容的服务器建立网络连接。浏览器为所需资源(例如主页)发送 HTTP 请求,服务器返回包含其内容的 HTTP 响应。

Web 框架的工作原理

网站分为两大类别:静态和动态。静态网站由独立的 HTML 文档组成,如果你的网站有十个页面,你就需要十个独立的 HTML 文档来提供服务。这种方法只适用于非常小的网站。大多数网站是动态的,由数据库、HTML 模板和应用服务器组成,可以在发送到浏览器之前生成文件。使用动态网站,相对少量的代码就可以生成数百甚至数千个网页。像 Django 这样的 Web 框架就是为动态网站设计的。

在万维网的早期,开发者必须自己手动编写动态网站的所有部分,这容易出错,通常不安全,且性能不佳。像 Django 这样的 Web 框架很快出现来标准化这一过程。Web 开发者意识到许多任务是常规性的,由社区来执行和监控比个人更好。

从本质上讲,像 Django 这样的 Web 框架有三个主要任务:

  1. 将 URL 映射到视图逻辑以渲染页面
  2. 提供一个与数据库交互的抽象层
  3. 通过模板系统显示类 HTML 代码

就是这样!虽然 Web 框架使用不同的编程语言编写,并且有稍微不同的设计理念,但它们的总体目标本质上是相同的。

Django 架构

既然我们已经回顾了网站和 Web 框架的工作原理,让我们来了解 Django 的架构。有四个主要组件需要考虑:URL、视图、模型和模板。

从视觉上看,Django 的请求和响应周期如下图所示,实线表示必需的交互,虚线表示可选的交互。

Django 架构

当一个 HTTP 请求从 Web 浏览器传入时,Django 首先接触到的部分是 URL 分发器(urls.py 文件),它搜索已配置的 URL 模式,并在第一个匹配的视图(views.py 文件)处停止。视图组装请求的数据和样式,然后生成 HTTP 响应返回给 Web 浏览器。从技术上讲,这就是我们需要的一切。仅使用 URL 分发器和视图就可以拥有一个 Django 网站,我们将在本章后面看到。

然而,更常见的情况是还涉及另外两个组件:模型和模板。对于数据库驱动的网站,视图将与模型(models.py 文件)交互,模型定义数据库表、行为并支持来自数据库的查询。然后这些数据被发送回视图,在大多数情况下,视图将其发送到模板进行渲染。模板主要是一个 HTML 文件,但也可以是任何基于文本的格式,包括 XML 和 JSON。一旦视图拥有了所有必要的信息,它就会返回一个 HTTP 响应给 Web 浏览器。

这个 Django 请求/响应循环对 Web 浏览器发出的每个新 HTTP 请求重复进行。

模型-视图-控制器 vs 模型-视图-模板

如果你以前构建过网站,你可能熟悉许多 Web 框架使用的模型-视图-控制器(MVC)模式,包括 Ruby on Rails、Spring(Java)、Laravel(PHP)和 ASP.NET(C#)。这是一种在应用内部将数据和逻辑分离并展示为更容易让开发者理解的不同组件的流行方式。

在传统的 MVC 模式中,有三个主要组件:

  • Model(模型):管理数据和核心业务逻辑
  • View(视图):以特定格式渲染模型中的数据
  • Controller(控制器):接受用户输入并执行应用特定的逻辑

Django 的方式有时被称为模型-视图-模板(MVT),但更准确地说是一种包含 URL 配置的四部分模式,即模型-视图-模板-URL(MVTU):

  • Model(模型):管理数据和核心业务逻辑
  • View(视图):描述哪些数据被发送给用户,但不涉及其展示方式
  • Template(模板):将数据展示为 HTML,可包含可选的 CSS、JavaScript 和静态资源
  • URL Configuration(URL 配置):配置到视图的正则表达式组件

MVC 中的"View"类似于 Django 中的"Template",而 MVC 中的"Controller"在 Django 中被分为"View"和"URL 分发器"。

如果你是 Web 开发的新手,MVC 和 MVT 之间的区别并不重要:本书展示的是 Django 的做法。然而,如果你是一个有 MVC 经验的 Web 开发者,可能需要一点时间来将思维转换到"Django 方式",它更加松耦合,比 MVC 方法更容易修改。

初始设置

对于我们的第一个 Django 网站,我们将尽可能简单地构建一个"Hello, World"网站。虽然大多数 Django 网站都有 URL 分发器、视图、模型和模板,但从技术上讲,我们只需要 URL 分发器和视图。这就是我们这里要用的,但后续章节会引入模板和模型。

首先,打开一个新的命令行 Shell 或使用 VS Code 内置的终端。对于后者,点击顶部的"Terminal",然后点击"New Terminal",它将在 VS Code 界面底部出现。

确保你不在已有的虚拟环境中,检查命令行提示符前的括号中没有内容。你甚至可以输入 deactivate 来确保万无一失。然后,导航到你桌面的 code 目录,用以下命令创建一个 helloworld 目录。

# Windows
$ cd onedrive\desktop\code
$ mkdir helloworld
$ cd helloworld

# macOS
$ cd ~/desktop/code
$ mkdir helloworld
$ cd helloworld

创建一个名为 .venv 的新虚拟环境,激活它,并用 Pip 安装 Django,就像我们在上一章做的那样。我们现在也可以安装 Black。

# Windows
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $ python -m pip install django~=5.0.0
(.venv) $ python -m pip install black

# macOS
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python3 -m pip install django~=5.0.0
(.venv) $ python3 -m pip install black

现在,我们将使用 django-admin startproject 命令创建一个名为 django_project 的新项目。记得在命令末尾加上句号(.),这样项目就会安装在我们的当前目录中。

(.venv) $ django-admin startproject django_project .

让我们暂停一下来查看 Django 提供的默认项目结构。你可以通过用鼠标在桌面上打开新目录来直观地探索。.venv 目录可能一开始不可见,因为它是一个"隐藏文件",但它确实存在并包含关于我们虚拟环境的信息。

├── django_project
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

Django 创建了一个 django_project 目录和一个 manage.py 文件。在 django_project 中,有五个新文件:

  • __init__.py 表示该文件夹中的文件是 Python 包的一部分。没有这个文件,我们就无法从另一个目录导入文件,而在 Django 中我们经常需要这样做!
  • asgi.py 配置一个可选的 ASGI(异步服务器网关接口)应用
  • settings.py 控制我们 Django 项目的整体设置
  • urls.py 告诉 Django 在响应浏览器或 URL 请求时构建哪些页面
  • wsgi.py 配置 WSGI(Web 服务器网关接口)应用,这是 Django 的默认设置

manage.py 文件不是 django_project 的一部分,但用于执行各种 Django 管理命令,如运行本地 Web 服务器或创建新应用。

让我们通过执行 python manage.py runserver 来启动 Django 的内置 Web 服务器,试试我们的新项目。此服务器适合开发使用,但不适合生产环境。我们将在本书后面部署网站时深入了解生产环境的设置。

(.venv) $ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
June 28, 2024 - 16:43:31
Django version 5.0.6, using settings 'django_project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

如果你访问 http://127.0.0.1:8000/,你应该会看到以下图片:

Django 欢迎页面

注意,由于我们首次尝试连接到 SQLite,项目目录中已经创建了一个 db.sqlite3 文件。目前,它是空的。

├── django_project
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3   # new
└── manage.py

数据库迁移

在命令行上,我们仍然看到关于 18 个未应用迁移的警告;让我们现在来探讨正在发生什么。迁移是 Django 自动创建的特殊脚本,用于跟踪数据库的变化。随着项目的进展,Django 数据库模型(定义数据库结构及其所有表)经常会有许多变化。Django 迁移框架允许开发者跟踪随时间的变化,并使数据库与特定迁移文件中的配置保持一致。

当你使用 startproject 命令开始一个新项目时,Django 包含了几个内置应用(后面会详细介绍什么是应用),它们会对数据库进行更改,包括 admin、auth、contenttypes 和 sessions。我们可以使用管理命令 migrate 将这些更改应用到本地数据库。首先输入 Control + c 来停止本地服务器。

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

migrate 命令应用所有可用的迁移并列出它们:Apply all migrations: admin, auth, contenttypes, sessions。输出包括每个应用及其迁移脚本的完整名称。例如,Applying contenttypes.0001_initial... OK 表示 contenttypes 应用中的迁移脚本 0001_initial 已成功运行。

重启开发服务器,将不再有任何警告。

(.venv) $ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
June 28, 2024 - 16:43:31
Django version 5.0.6, using settings 'django_project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

db.sqlite3 文件现在已填充了 Django 的内置表和数据。如果你安装了免费的 SQLite 查看器扩展 SQLite Viewer,你可以直观地检查它们。

我们将在本书后面更深入地介绍数据库。在这个阶段,重要的一点是 Django 使用迁移文件来控制数据库的变化,而 migrate 管理命令负责应用它们。

创建应用

一个 Django 项目可以包含许多"应用",这是一种保持代码整洁和可读的组织技巧。每个应用应该控制一个独立的功能模块。如果你查看 django_project/settings.py 文件,已经有六个 Django 为我们提供的内置应用。它们位于 django.contrib 目录中,控制 admin、auth、contenttypes、sessions、messages 和 staticfiles 的功能。此时你不需要知道每个应用的作用。

# django_project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

没有什么强制要求使用应用这种约定——如果你愿意,你可以将所有代码写在一个文件中——但这种分离逻辑的约定使得构建和理解 Django 项目更加容易。当我们想向 Django 项目添加功能时就使用应用。例如,一个电子商务网站可能有一个用于用户认证的应用,另一个用于支付,第三个用于商品列表详情。何时以及如何将功能拆分为应用是非常主观的,但一个好的经验法则是:当单个应用感觉做了太多事情时,就该将功能拆分为独立的应用,每个应用只负责一个功能。

要创建一个新应用,进入命令行,使用 Control+c 退出正在运行的服务器。然后使用 startapp 命令,后跟我们的应用名称。在这个例子中,我们将使用名称"pages"。Django 的最佳实践是应用名称使用复数形式——pages、payments 等——除非这样做没有意义,比如"blog"应用。

(.venv) $ python manage.py startapp pages

如果你直观地查看 django_project 目录,Django 在其中创建了一个新的 pages 目录,包含以下应用文件:

├── pages
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py

让我们来了解每个新的 pages 应用文件的作用:

  • admin.py 是内置 Django Admin 应用的配置文件
  • apps.py 是应用本身的配置文件
  • migrations/ 跟踪对 models.py 文件的任何更改,使其与数据库保持同步
  • models.py 是我们定义数据库模型的地方,Django 会自动将其转换为数据库表
  • tests.py 用于应用特定的测试
  • views.py 是我们处理 Web 应用请求/响应逻辑的地方

即使我们的新应用存在于 Django 项目中,Django 也不会"知道"它的存在,直到我们明确将其添加到 django_project/settings.py 文件中。在你的文本编辑器中打开该文件,滚动到 INSTALLED_APPS,你会看到六个内置的 Django 应用。在底部添加 pages

# 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
]

你的第一个视图

我们将为第一个网站创建一个静态页面,输出文本"Hello, World!"。这个页面不涉及数据库甚至模板文件。相反,它是一个很好的入门,让你了解视图和 URL 在 Django 中是如何工作的。

视图是一个 Python 函数,它接受 Web 请求并返回 Web 响应。响应可以是网页的 HTML 内容、重定向、404 错误、图像或几乎任何东西。当一个网页被请求时,Django 会自动创建一个包含请求元数据的 HttpRequest 对象。然后 Django 加载适当的视图,将 HttpRequest 作为第一个参数传递给视图函数。视图最终负责返回一个 HttpResponse 对象。

在我们的 pages 应用中,已经有一个名为 views.py 的文件,它带有以下默认文本:

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

# Create your views here.

我们将在下一章中详细介绍 render,但现在,用以下代码更新 pages/views.py 文件:

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

def home_page_view(request):
    return HttpResponse("Hello, World!")

让我们逐行分析:

  • django.http 模块导入 HttpResponse
  • 定义一个名为 home_page_view 的函数。在 Python 中,惯例是对函数和变量名使用 snake_case——全小写字母用下划线分隔
  • 传递给视图的第一个参数是 HttpRequest 对象。惯例是将其命名为 request 以提高可读性,但重要的是顺序而不是名称,所以从技术上讲你可以称它为 req 或其他任何名称,它仍然有效
  • 视图返回一个带有文本字符串"Hello, World!"的 HttpResponse 对象

所有视图都是这样工作的:首先,你为视图定义一个名称,命名 HttpRequest 对象(在本例中为 request),然后返回某些内容。可以向视图添加更多逻辑和参数,但一般模式是相同的。

URL 分发器

视图已经就位,现在是时候配置相关的 URL 了。在你的文本编辑器中,在 pages 应用内创建一个名为 urls.py 的新文件,并用以下代码更新它:

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

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

在第一行,我们从 Django 导入 path 来驱动我们的 URL 模式。通过引用 .views,我们告诉 Django 在当前目录中查找 views.py 文件并导入名为 home_page_view 的视图。注意 path 模式中没有前导斜杠 /。Django 会自动为我们添加前导斜杠。

我们这里的 URL 模式有两个部分:

  • 路由本身,这里由空字符串 "" 定义
  • 对视图 home_page_view 的引用

换句话说,如果用户请求由空字符串 "" 表示的主页,Django 应该使用名为 home_page_view 的视图。

我们差不多完成了。最后一步是更新 django_project/urls.py 文件,它是通往各应用中其他 URL 模式的网关。这种架构模式在本书后面构建越来越复杂的 Web 应用时会更有意义。

Django 自动导入并为内置管理后台设置了一个 path。要包含额外的 URL 路径,我们从 django.urls 模块导入 include 函数,然后设置其路径。在本例中,我们再次使用空字符串 "",并包含 pages 应用中所有的 URL。如果你在这个阶段想了解更多细节,Django 文档有更完整的描述。

以下是更新后的代码:

# 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
]

现在,每当用户访问由空字符串 "" 表示的主页时,Django 会在 pages 应用内查找匹配的 URL 路由。

我们有了所需的所有代码。要确认一切按预期工作,重启我们的 Django 服务器:

(.venv) $ python manage.py runserver

如果你刷新 http://127.0.0.1:8000/ 的浏览器,它将显示文本"Hello, World!"

Hello World 主页

就是这样!我们从零创建了一个 Django 项目,并第一次接触了 Django 的架构和典型模式:

  • 创建一个项目,然后在其中创建一个应用
  • 编写一个视图
  • 将视图连接到 URL 分发器

当然可以用更少的代码行甚至在单个文件中编写一个 Django"Hello, World"应用:如果你好奇可以查看 django-microframework 仓库。但在这个阶段,更重要的是开始内化典型的 Django 项目结构和模式。

Git

在上一章中,我们安装了版本控制系统 Git。让我们在这里使用它。第一步是初始化(或添加)Git 到我们的仓库。确保你已经用 Control+c 停止了本地服务器,然后运行 git init 命令。

(.venv) $ git init

如果你输入 git status,你会看到自上次 Git 提交以来的更改列表。由于这是我们的第一次提交,此列表包括整个目录的内容。

(.venv) $ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    .venv
    django_project/
    db.sqlite3
    manage.py
    pages/

nothing added to commit but untracked files present (use "git add" to track)

注意虚拟环境 .venv 被包含了。最佳实践是不要将虚拟环境包含在 Git 版本控制中,因为它可能包含 API 密钥等秘密信息。

解决方案是在项目级目录中创建一个名为 .gitignore 的新文件,它告诉 Git 忽略什么。开头的句号表示这是一个"隐藏"文件。文件仍然存在,但这是向开发者传达其内容可能是用于配置而非版本控制的一种方式。

以下是你的项目结构现在应该是这样的:

├── django_project
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── pages
│   ├── migrations
│   │   └── __init__.py
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── .gitignore   # new
├── db.sqlite3
└── manage.py

在这个新的 .gitignore 文件中,添加一行 .venv

# .gitignore
.venv/

如果你再次运行 git status,你会看到 .venv 不再出现了。它已被 Git"忽略"。在专业项目中,.gitignore 文件通常相当长。出于效率和安全原因,通常有很多目录和文件应该从版本控制中移除。然而,对于像这样的学习项目,优化并不重要。

同时,我们确实想要一份虚拟环境中安装的包的记录。当前的最佳实践是创建一个 requirements.txt 文件来记录这些信息。pip freeze 命令会输出当前虚拟环境的内容,通过使用 > 运算符,我们可以一步完成:将内容输出到名为 requirements.txt 的新文件中。如果你的服务器还在运行,先输入 Ctrl+c 和回车退出,然后再输入此命令。

(.venv) $ pip freeze > requirements.txt

一个新的 requirements.txt 文件将出现,包含所有已安装的包及其依赖项。如果你查看这个文件,即使我们只安装了两个包——Django 和 Black,你也会看到九个包。那是因为 Django 和 Black 也依赖其他包。通常当你安装一个 Python 包时,你也在安装多个依赖包。由于跟踪所有包很困难,requirements.txt 文件至关重要。

# requirements.txt
asgiref==3.8.1
black==24.4.2
click==8.1.7
Django==5.0.6
mypy-extensions==1.0.0
packaging==24.1
pathspec==0.12.1
platformdirs==4.2.2
sqlparse==0.5.0

接下来,我们要执行第一次 Git 提交来存储所有最近的更改。Git 有一个很长的选项/标志列表。例如,要添加所有最近的更改,我们可以使用 git add -A。然后,要提交更改,我们将使用 -m 标志("message")来描述发生了什么变化。始终为你的提交添加消息非常重要,因为大多数项目很容易就有数百甚至数千次提交。每次添加描述性消息有助于以后的调试工作,因为你可以搜索你的提交历史。

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

小结

恭喜!本章涵盖了很多内容,从互联网和 Web 框架的工作原理开始,然后深入到 Django 的架构。接着我们构建了第一个 Django 网站,同时学习了应用、视图、URL 和 Django 内部 Web 服务器。我们还使用 Git 来跟踪我们的更改、创建 .gitignore 文件并生成 requirements.txt 文件。

如果你遇到困难,将你的代码与官方仓库进行对比。

请继续进入下一章,我们将使用模板和更高级的基于函数的视图来构建一个更复杂的 Django 应用,同时引入测试。