密西根大学每个人的-Django-笔记-全-

密西根大学每个人的 Django 笔记(全)

001:欢迎来到课程 👋

在本课程中,我们将开始学习“Django for Everybody”专项课程的第一部分。本课程旨在为后续学习打下坚实基础,确保所有学员都具备必要的预备知识。

课程概述

本课程的目标是让学员掌握使用Django开发Web应用所需的核心技能。我们假设学员已具备一定的Python编程基础,例如已完成“Python for Everybody”专项课程的学习。因此,本课程将不再额外设置HTML、CSS或SQL等前置要求。

相反,我们将在本课程中涵盖所有支撑整个专项课程的基础知识。我们将使用PythonAnywhere平台,安装Django框架,并完成一些涉及HTML、CSS和SQL的作业练习。

课程目标

上一段我们介绍了课程的整体定位,接下来我们来看看本课程的具体目标。其核心理念是:在课程结束时,确保所有学员都站在同一起跑线上,以便我们能顺利开始深入探索Python与Django如何协同工作。

以下是本课程将涵盖的主要内容:

  • 安装并配置Django开发环境。
  • 学习Web开发的基础知识,包括HTML与CSS。
  • 初步接触SQL,理解数据库的基本操作。
  • 为后续深入学习Python与Django的整合开发做好准备。

总结

本节课中,我们一起了解了“Django for Everybody”专项课程第一部分的欢迎信息与课程设置。我们明确了本课程无需额外的HTML、CSS或SQL基础,并将从安装Django开始,逐步学习Web开发的核心组件,最终目标是让所有学员为后续的Django深度开发学习做好充分准备。

002:为何选择Django 🎯

在本节课中,我们将探讨为何选择Django作为Web开发框架,以及它相较于其他选项(如Flask)在教学和实际应用中的优势。


概述

作为课程的开篇,我将解释创建这门课程的原因,并重点说明为何选择Django而非其他Python Web框架。我将分享个人教学经验、行业调研结果以及Django如何帮助学生高效构建功能完整的网站。


教学背景与框架选择历程

上一节我们介绍了课程的基本情况,本节中我们来看看选择Django的具体原因。三年前,我是一名教授,当时我们为初学者和数据挖掘课程教授Python。我认为学习第二门语言很重要,因此也为Web开发课程教授了PHP。我喜欢PHP,它能教你理解Web的实际工作原理。我还教授了SQL、JavaScript、jQuery和JSON。我花了五年时间建设那门课程,包括构建评分系统。

作为教师,我的一个衡量标准是学生在课程结束时能达到的水平。我观察15周的学习后,学生完成的最后一项作业是什么,并以此判断教学效果。多年来,我努力让学生走得更远,并调整课程前期内容以提高效率,以便在后期加入更多实用内容。因为入门课程中有大量基础机制需要学习,只有掌握了这些机制才能应用它们,而不能一开始就让学生复制粘贴数千行代码。

三年前,我有一门非常成熟的PHP Web开发课程。但后来我被告知,由于大家都想用Python,并且许多高级课程(如移动应用开发)都在使用Flask,因此不再希望我教授PHP进行Web开发。我被要求转向Python,具体来说是转向Flask。

我使用过Ruby on Rails、PHP(原生)、CakePHP(一个框架),并且进行过大量使用框架和不使用框架的编程。另一位老师曾用Flask授课,但我不喜欢那门课的效果。我观察了课程最后学生完成的内容,认为成果并不丰富。但我仍以开放的心态开始了探索。

实际上,我有一整年时间在教授PHP(他们允许我教最后一年),那也是我开发“Web Applications for Everybody”课程的时候,该课程后来也成为了慕课(MOOC),我很喜欢它,并且很多人都在使用,它确实是一门很棒的慕课。我仍然坚信学习多门语言是个好主意,所以你可以在学习“Django for Everybody”之后,再去学习“Web Applications for Everybody”。


行业调研:Django vs. Flask

为了做出决定,我进行了广泛调研。我会询问遇到的每一个人:“你使用Flask还是Django?”幸运的是,早在五年前,Python有大约25个选择,但到三年前,基本上缩小到了Flask和Django。当然还有其他很酷的专门框架,如Sanic,但Flask和Django是两大主流选择。PHP框架的问题在于选择太多(尽管Symfony在PHP领域逐渐胜出)。

人们的回答充满了激情。他们会说“Django是最棒的,Flask完全是垃圾”,或者遇到下一个人时,对方会说“Django是垃圾,Flask太棒了”。大约50%的人热爱一个而讨厌另一个。这令人惊讶,我完全没有得到任何趋势性数据。

调研进行到一半时,我意识到没有得到有效数据。一半人热爱一个,讨厌另一个。于是我改变了提问方式。

我不再问是喜欢Django还是Flask,因为他们在回答自己喜欢什么,而我真正想问的是教学顺序。所以我说:“我只能教一个,Django和Flask,你会先教哪个,后教哪个?顺序是什么?”

结果,所有人都不得不承认一个事实:Django作为第一门课程的教学效果要好得多,好非常多。

因此,我得到了非常强有力的数据支持,于是选择了Django。


选择Django的优势与成果

回顾过去,这个选择非常明智。我已经为这门Django课程工作了大约两年半。每个学期我都在改进它,优化幻灯片,并努力压缩前期基础内容,以便更高效地完成入门部分,从而在课程后期进行更深入、更好的教学。我对此感到非常满意。

你现在看到的“Django for Everybody”慕课,正是这两年精心设计课程的成果。我为此感到自豪。你可以访问 projects.dj4e.com 查看学生构建的一些项目。我对他们能够独立构建的内容之多感到惊讶。坦白说,用PHP或Flask都无法达到这样的效果。Django是唯一能让学生在15周后,构建出具有真实用户界面的、功能完整的网站的方法。这对我来说,意味着Django取得了巨大的成功。


Python社区与Web开发现状

最后,我想分享最近查看的Python社区开发者调查结果。我惊讶地发现,Python的首要用途是Web应用程序,第二用途是数据应用程序。这让我很惊讶,因为我知道Python在数据应用领域非常强大且占据主导地位。

出现这种结果的部分原因是,Web应用的使用量并非两倍于其他,而是Python用途非常广泛,列表上有很多项目,Web开发只是以微弱优势略微领先于数据分析。你可以批评Python的Web开发,但我的回答是:即使它不完美,它也将会变得完美,因为人们喜欢用Python编写代码,而不想用Rust、Elixir或Scala等更难学习的语言。坦白说,无论Python Web开发存在什么缺点,都将会被修复。


总结

本节课中,我们一起学习了选择Django作为本课程核心框架的决策过程。我们回顾了教学背景、行业调研结果,并明确了Django在帮助初学者高效构建完整Web应用方面的独特优势。欢迎来到“Django for Everybody”,我真的很享受这个过程,并一直期待这门课程足够成熟、发展得足够好,能够成为一门慕课。现在我们做到了,很高兴认识大家。

003:在PythonAnywhere上安装Django 🚀

在本教程中,我们将学习如何在PythonAnywhere托管环境中安装和配置Django。这是课程的第一个作业,也是让Django应用在PythonAnywhere上运行的第一步。我们将从创建一个全新的账户开始,逐步完成所有必要的设置。

概述 📋

PythonAnywhere是一个托管环境,我们将在其中完成所有工作。本教程将引导你完成从创建账户到运行一个基础Django应用的全过程。请注意,随着时间推移,Python和Django的版本会更新,因此请务必参考最新的安装文档。如果本教程与官方指令有冲突,请以官方指令为准。

启动Bash控制台

首先,我们需要启动一个Bash控制台。进入“Consoles”部分,启动一个Bash shell。这是一个Linux环境,我们使用Bash shell进行操作。

创建虚拟环境

接下来,我们需要选择要使用的Python版本,并通过虚拟环境来管理。我们将创建一个名为django4的虚拟环境,并指定初始的Python版本。

运行此命令可能需要一些时间,尤其是在使用免费账户时。请耐心等待,直到看到美元符号$提示符返回。这个过程大约需要一分钟。

安装Django库

现在,我们将在虚拟环境中使用pip命令下载并安装Django库。请确保你已激活django4虚拟环境。

这个安装过程也需要一些时间。完成后,我们可以检查Python和Django的版本,以确保安装成功。

配置自动激活虚拟环境

为了避免每次启动新控制台时都需要手动激活虚拟环境,我们可以配置.bashrc文件。这个文件在每次启动新shell时都会运行。

以下是配置步骤:

  1. 进入“Files”标签页。
  2. 打开.bashrc文件进行编辑。
  3. 添加自动切换到django4虚拟环境的命令。
  4. 保存文件。

配置完成后,每次启动新的Bash控制台时,都会自动激活django4虚拟环境。这可以避免因忘记激活虚拟环境而导致的常见错误。

下载课程示例代码

现在,我们将从GitHub下载课程中使用的示例代码。这些代码可供你参考和复制。

使用git clone命令将代码库克隆到本地。这个过程也需要一些时间。

克隆完成后,你可以在dj4e-samples文件夹中看到所有示例文件。

你可以随时使用pwd(打印工作目录)命令来确认当前所在位置。

安装课程依赖库

接下来,我们将安装整个学期可能需要的其他库依赖。运行以下命令:

这个过程会下载并写入大量数据到磁盘,请耐心等待。

验证安装

安装完成后,我们使用python manage.py check命令来验证Django应用是否可以正确加载。这个命令会预加载所有内容并检查语法错误。

如果看到语法错误,请检查你是否运行了正确版本的Python。确保你使用的是Python 3,而不是Python 2。切勿编辑manage.py文件

创建并测试数据库

现在,我们将测试创建数据库的能力。运行以下命令:

makemigrations命令会读取示例中的所有模型,而migrate命令会实际创建数据库。这主要用于验证你的安装和虚拟环境是否正常工作。

创建Django项目

验证完成后,我们回到主目录并创建一个新的Django项目。

创建一个名为django_projects的目录,然后进入该目录。

使用django-admin命令创建一个名为mysite的新Django应用。

创建完成后,进入django_projects/mysite目录,你会看到新创建的项目文件。

配置Django设置

我们需要编辑settings.py文件以允许外部连接。找到大约第28行,在ALLOWED_HOSTS列表中添加一个星号*

保存文件。这个设置非常重要,否则你的应用将无法接受来自外部的连接。

配置PythonAnywhere Web应用

现在,我们需要在PythonAnywhere上配置Web应用。进入“Web”标签页,添加一个新的Web应用。

选择“Manual configuration”,并选择Python 3.9版本,以匹配我们之前创建的虚拟环境。

在配置中,需要设置以下两个路径:

  • 代码目录/home/<你的用户名>/django_projects/mysite
  • 工作目录:与代码目录相同

接着,设置虚拟环境路径为:/home/<你的用户名>/.virtualenvs/django4

最后,编辑WSGI configuration file。删除其所有内容,并粘贴提供的特定配置代码,然后保存。

配置完成后,点击“Reload”按钮。PythonAnywhere将读取所有文件并启动Django应用。

如果一切配置正确,你的应用应该能成功运行。

错误调试

如果应用出现错误(例如500错误),可以进行调试。首先,在控制台中进入你的项目目录(包含manage.py的目录),运行python manage.py check命令。

这个命令会给出详细的错误追踪信息,帮助你定位问题,例如在settings.py文件的哪一行出现了语法错误。修复错误后,保存文件,回到Web标签页重新加载应用,然后刷新页面。

创建Django应用(App)

现在,我们可以在项目中创建一个具体的应用。进入项目目录,运行以下命令创建一个名为polls的应用:

这将在mysite目录下创建一个新的polls文件夹。

重要提示:在后续的Django教程中,当它指示你运行runserver命令时,在PythonAnywhere环境中,你应该:

  1. 在控制台中运行python manage.py check来确保语法正确。
  2. 然后回到Web标签页,点击“Reload”按钮。
    这等同于在本地运行runserver命令。

总结 🎉

在本教程中,我们一起完成了在PythonAnywhere上安装和配置Django的全过程。我们学习了如何:

  • 启动Bash控制台并创建Python虚拟环境。
  • 安装Django及课程所需的依赖库。
  • 配置环境以自动激活虚拟环境。
  • 下载课程示例代码。
  • 创建Django项目和应用。
  • 在PythonAnywhere上配置和部署Web应用。
  • 进行基本的错误调试。

关键是要确保文件路径和虚拟环境配置正确,并且在需要“运行服务器”时,使用checkreload的组合操作。现在,你的Django开发环境已经准备就绪,可以开始构建Web应用了。

004:理解DJ4E自动评分器 🧪

在本节课中,我们将学习密歇根大学《Django for Everybody》课程中一个重要的工具:DJ4E自动评分器。我们将了解它的工作原理、如何提交作业进行自动评分,以及当作业出现问题时如何利用评分器提供的反馈进行调试。

概述

自动评分器是课程中用于评估学生Django应用程序作业的工具。它通过模拟浏览器行为,向你的应用程序发送一系列HTTP请求,并根据返回的页面内容判断你的代码功能是否正确实现。理解其工作流程能帮助你更高效地完成和调试作业。

自动评分器的工作流程

上一节我们概述了自动评分器的用途,本节中我们来看看它的具体工作流程。整个过程涉及你的开发环境、你的应用程序以及评分器服务器之间的交互。

  1. 代码开发与本地测试:你首先在PythonAnywhere或其他开发环境中编写Django应用程序代码。
  2. 手动测试:在提交给评分器之前,强烈建议你按照作业要求,在浏览器中手动测试所有功能点,例如登录、增删改查操作。
  3. 提交至评分器:完成代码并经过基本测试后,你将作业提交到DJ4E自动评分器。
  4. 自动测试执行:评分器开始工作。它会模拟一个用户,按照预设的测试用例访问你的应用程序URL,执行一系列操作(如添加Make、添加Auto、更新、删除等)。
  5. 结果分析与评分:评分器检查每个请求的响应状态码和页面内容。如果所有检查都通过,你会获得满分。如果任何一步失败,评分会停止,并给出错误信息。
  6. 查看详细反馈:你可以通过“显示/隐藏检索页面”按钮,查看评分器在出错时具体访问了哪个URL以及获得了什么页面内容,这为调试提供了直接线索。

核心交互模型

为了更清晰地理解,我们可以用以下模型来描述自动评分器与你的应用之间的交互:

用户浏览器 <--手动测试--> 你的Django应用 (托管于 PythonAnywhere)
                                    ^
                                    |
                           自动评分器服务器 (dj4e.com)
                                    |
                                    v
                            发送HTTP请求并验证响应

关键代码概念:自动评分器本质上是一个执行自动化端到端测试的脚本。它使用类似requestsSelenium的库来发送HTTP请求,并用assert语句验证响应,例如:

# 伪代码示例:评分器检查“添加品牌”表单
response = requests.get(your_app_url + '/make/create')
assert response.status_code == 200
assert 'submit' in response.text  # 检查页面是否有提交按钮

调试与利用反馈

当自动评分器报告错误时,不要慌张。以下是有效的调试步骤:

  1. 阅读错误信息:评分器会明确指出哪一步失败了,例如“未找到包含提交按钮的表单”或“HTTP 404错误”。
  2. 查看详细页面:点击“显示/隐藏检索页面”,查看出错时评分器收到的实际HTML内容。你甚至可以复制那个URL,在自己的浏览器中打开,直接看到问题所在。
  3. 在本地复现并修复:根据错误信息,回到你的开发环境,修复代码(例如,确保所有视图和URL配置正确,表单元素完整)。
  4. 重新测试并提交:修复后,务必在本地重新进行手动测试。确认问题解决后,再次提交给自动评分器。

重要提示:自动评分器的运行需要时间(可能长达20秒),因为它要执行多个请求-响应周期。请耐心等待结果。

总结

本节课中我们一起学习了DJ4E自动评分器的工作原理和使用方法。记住几个关键点:始终先进行彻底的手动测试;评分器是你的助手和朋友,它提供的详细反馈是调试的宝贵资源;遇到问题时,利用“显示检索页面”功能查看具体细节。掌握与自动评分器协作的流程,将帮助你更顺利地完成《Django for Everybody》课程中的实践项目。

005:Python的诞生与早期岁月

在本节课中,我们将跟随Python创始人吉多·范罗苏姆的讲述,了解Python语言诞生的背景、早期开发历程以及其最初的开源发布过程。我们将看到一门新语言是如何从一个解决实际问题的想法,逐步成长为一个拥有社区的项目的。

诞生背景

上一节我们介绍了课程的整体框架,本节中我们来看看Python诞生的具体故事。吉多·范罗苏姆当时在荷兰国家数学与计算机科学研究所工作,参与一个名为Amoeba的分布式操作系统项目。

该操作系统内核在网络通信和进程管理方面表现优异,但缺乏足够的用户级应用软件。团队需要为这个新系统编写一整套工具,从编辑器、邮件程序到登录和备份工具。

选择与契机

在编写这些工具时,吉多意识到用C语言开发进度缓慢。C语言的优势(如高性能)在此类工具开发中并非必需,而选择C的唯一原因是当时只有C语言的编译器可用。

此时,吉多想到了他曾参与过的ABC语言。ABC语言在数据处理和代码结构方面非常优秀,但它过于高层抽象,不适合与操作系统底层的服务器、文件系统和进程进行交互。

基于对ABC语言设计和实现的理解,吉多产生了一个想法:他可以利用业余时间,从头设计和实现一门新语言。他相信,即使算上开发新语言的时间,用这门新语言来编写Amoeba所需的工具套件,整体效率也会高于直接使用C语言。

早期开发与演示

在接下来的三个月里,吉多白天完成本职工作,晚上和业余时间则投入Python的开发。三个月后,他做出了一个可以演示的版本。

这个早期版本已经包含了一个交互式解释器。最初的演示内容通常包括:

  • 将一个表达式赋值给变量并打印出来。
  • 定义一个小函数并调用它。
  • 将一些元素放入数组并对其进行迭代。

这些基础功能都已实现。很快,他的两位同事被这门语言吸引,开始提供帮助。研究所内的其他一些人也对此感到兴奋,并开始使用它。

尽管Python当时还不够成熟,无法直接用于编写Amoeba的工具,但它已经足够有用,可以在研究所内部的Unix系统上运行。人们开始用它编写小脚本,并贡献错误修复。

开源发布

到1990年底,也就是项目启动约一年后,团队计划将这门语言以开源形式发布(当时“开源”这个词尚未被创造出来)。他们参考了X Window System等项目的发布模式。

以下是发布过程中的关键步骤:

  1. 吉多向研究所的管理层和法律部门申请发布。
  2. 他说明这是利用业余时间、为研究项目开发的代码。
  3. 他准备了一份与MIT许可证类似的许可协议。
  4. 在获得批准后,团队于1991年2月进行了首次Python发布。

首次发布在当时是一个重要的里程碑。他们将代码发布到Usenet新闻组上,并很快开始收到积极的反馈。团队也形成了定期发布新版本的习惯。

社区的成长

在90年代上半叶,Python用户和开发者社区开始自发组织起来。一个重要的事件是吉多受美国国家标准与技术研究院邀请访美两个月。

在此期间,他们组织了第一届Python研讨会。通过这次会议,吉多结识了一些人,并最终获得了一份在美国的工作。从1995年到2000年,他在美国弗吉尼亚州工作,期间见证了Python社区和基础设施的显著成长。

他们创建了Python官网,结识了许多至今仍活跃在社区的核心人物(如巴里·华沙)。Python版本也从1.3逐步迭代到1.5.2,其中1.5.2在之后很长一段时间里都被视为一个稳定可靠的“黄金标准”。

总结

本节课中我们一起学习了Python语言的早期历史。我们了解到Python的诞生源于一个实际的项目需求——为Amoeba操作系统快速开发工具套件。吉多·范罗苏姆结合ABC语言的优点,创造性地开发了Python,并通过果断的开源发布,吸引了早期贡献者,从而奠定了其社区驱动发展的基础。从在研究所内部的悄然兴起,到通过Usenet走向世界,再到社区的自组织与国际化,Python在最初的十年里完成了从个人项目到拥有全球影响力语言的华丽转身。

006:伊利诺伊州乔利埃特 🏁

在本节课中,我们将跟随查克博士的赛车之旅,学习如何构建一个简单的Django应用。我们将从创建项目和应用开始,逐步实现一个显示赛车比赛信息的网页。


概述

我们将创建一个名为“Racing”的Django项目,并在其中构建一个应用来展示查克博士在伊利诺伊州乔利埃特的赛车之旅。这个过程将涉及创建项目、应用、定义模型、编写视图和配置URL。


创建Django项目和应用

首先,我们需要创建一个Django项目。Django项目是应用的容器,它包含整个网站的配置。

命令

django-admin startproject racing

接下来,进入项目目录并创建一个名为“races”的应用。应用是Django项目中实现特定功能的模块。

命令

cd racing
python manage.py startapp races

创建应用后,需要将其添加到项目的设置中,以便Django能够识别它。


定义数据模型

模型是Django中用于定义数据结构的方式。每个模型对应数据库中的一张表。

在我们的赛车应用中,我们需要一个模型来存储比赛信息。这个模型可以包含比赛名称、地点和日期等字段。

代码

from django.db import models

class Race(models.Model):
    name = models.CharField(max_length=200)
    location = models.CharField(max_length=200)
    date = models.DateField()

    def __str__(self):
        return self.name

定义模型后,需要生成并应用数据库迁移,以在数据库中创建相应的表。

命令

python manage.py makemigrations
python manage.py migrate

创建管理界面

Django提供了一个强大的内置管理界面,可以方便地管理模型数据。

要使用管理界面,首先需要创建一个超级用户。

命令

python manage.py createsuperuser

然后,在admin.py文件中注册我们的Race模型。

代码

from django.contrib import admin
from .models import Race

admin.site.register(Race)

现在,我们可以通过访问/admin路径来登录管理界面,并添加或编辑比赛数据。


编写视图和模板

视图负责处理用户请求并返回响应。模板则用于生成HTML页面。

首先,在views.py中创建一个视图函数,用于获取所有比赛并传递给模板。

代码

from django.shortcuts import render
from .models import Race

def race_list(request):
    races = Race.objects.all()
    return render(request, 'races/race_list.html', {'races': races})

接下来,创建一个模板文件race_list.html来显示比赛列表。

代码

<!DOCTYPE html>
<html>
<head>
    <title>查克博士赛车之旅</title>
</head>
<body>
    <h1>查克博士赛车之旅:伊利诺伊州乔利埃特</h1>
    <ul>
    {% for race in races %}
        <li>{{ race.name }} - {{ race.location }} ({{ race.date }})</li>
    {% endfor %}
    </ul>
</body>
</html>

配置URL

最后,我们需要配置URL,以便用户能够访问我们的视图。

首先,在应用目录下的urls.py文件中定义应用级别的URL。

代码

from django.urls import path
from . import views

urlpatterns = [
    path('', views.race_list, name='race_list'),
]

然后,在项目级别的urls.py中包含应用的URL配置。

代码

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('races/', include('races.urls')),
]

运行开发服务器

完成以上步骤后,我们可以运行开发服务器来查看我们的应用。

命令

python manage.py runserver

在浏览器中访问http://127.0.0.1:8000/races/,即可看到查克博士在乔利埃特的赛车比赛列表。


总结

本节课中,我们一起学习了如何构建一个简单的Django应用。我们从创建项目和应用开始,定义了数据模型,设置了管理界面,编写了视图和模板,并配置了URL。最终,我们成功创建了一个显示赛车比赛信息的网页。通过这个实践,你应该对Django的基本工作流程有了初步的了解。

007:东京

概述

在本节课中,我们将回顾一次在东京举行的面对面办公时间活动。查克老师与来自世界各地的学生见面,并听取他们学习Python和Django课程的体验与反馈。


活动开场

大家好,我是查克。我们现在在东京。

正如我在上一个视频中承诺的那样,我设法来到了东京。

自2012年MOOC课程开始以来我都没来过这里,这令人惊讶,但我们终于做到了。

和往常一样,我想让你们认识一下班上的其他一些同学。

我会告诉你们他们的名字,可能还有他们的一些留言。


学生介绍

以下是参与本次东京见面会的部分学生分享。

克里斯蒂安:
我的名字是克里斯蒂安。查克博士教我。我教我的学生。谢谢你,查克博士。我喜欢老师教授编程。我教初中和高中学生。

川口先生:
非常感谢您精彩、卓越的课程。非常感谢。感谢您的到来,与您交谈非常愉快。

本:
我是本。我来自澳大利亚。我非常喜欢上查克博士的瑜伽课程。谢谢你。

埃马纽埃尔:
我是来自乌干达的埃马纽埃尔,目前就读于东京农业大学。学习查克博士的课程是一种荣幸。我将把它用于水文建模,并教给我的实验室同事Python,以替代Matlab。非常感谢查克博士。欢迎来到课堂。

艾哈迈德:
嘿,我的名字是艾哈迈德,我来自突尼斯。我非常喜欢上Python课。现在能见到查克博士是莫大的荣幸。非常感谢。我希望继续和你一起学习。谢谢你。

简:
我的名字是简,我来自印度。我正在以正确的方式从专业人士那里学习Python。非常感谢你。谢谢你。

泽维尔:
嗨,我的名字是泽维尔。我每天使用你的课程进行数据科学学习,非常感谢你传授的所有这些技能。谢谢。

英雄:
嗨,我是英雄。这是第一次体验,部分现实变成了领导者。谢谢你。

莎亚:
嗨,我的名字是莎亚,我正在学习Python。我真的很享受学习一门新的编程语言,并且很感激这门课程。谢谢你。

美:
嗨,我是美。我正在为每个人运行Python。谢谢你。


活动总结与展望

以上就是本次见面会的情况。

接下来几天我们将在名古屋。

这是五年没来日本后的第二站。

下次网上见。干杯。


总结

本节课中,我们一起回顾了查克老师在东京与学生的面对面交流。我们听到了来自不同背景的学生分享他们学习Python和Django课程的动机、应用以及感激之情,这体现了该课程广泛的国际影响力与实践价值。

008:在PythonAnywhere上使用命令行Shell 🖥️

在本节课中,我们将学习如何在PythonAnywhere上使用命令行Shell。这是课程早期的作业之一,我们将逐步了解其基本操作。虽然现代用户界面通常是图形化的,但服务器环境大多使用命令行界面。理解命令行的工作原理对于Web开发至关重要。

为什么使用命令行?🤔

你可能会问,为什么我们要使用这种看似过时的命令行界面?答案在于服务器环境。绝大多数服务器,尤其是运行Linux系统的服务器,都没有图形用户界面。它们使用命令行界面,通过文本提示符接收指令。因此,了解命令行操作是处理服务器和排查问题的基本技能。

访问PythonAnywhere的Shell

首先,登录到PythonAnywhere,进入“Consoles”标签页。在这里,你可以启动不同类型的控制台,例如交互式Python环境或MySQL Shell。对于本课程,我们主要使用Bash Shell。Bash是Linux系统中一种常见的Shell变体。

启动Bash Shell后,你会看到一个美元符号 $ 提示符,这表示Shell正在等待输入。

基本Shell命令 📝

以下是几个常用的Shell命令,我们将逐一介绍。

1. 列出文件和目录

ls 命令用于列出当前工作目录中的文件和文件夹。

ls

使用 ls -l 可以显示详细信息,包括文件大小、修改日期和权限。

ls -l

2. 显示当前目录

pwd 命令用于打印当前工作目录的完整路径。

pwd

3. 切换目录

cd 命令用于切换目录。单独使用 cd 会返回你的主目录。

cd

要进入特定目录,可以输入路径。例如,进入名为“django_projects”的目录:

cd django_projects

使用 cd ~ 可以快速返回主目录。

cd ~

4. 删除文件

rm 命令用于删除文件。请谨慎使用此命令,因为它会直接删除文件,而不是将其移至回收站。

rm db.zap

5. 搜索文件内容

grep 命令用于在文件中搜索特定字符串。使用 -r 参数可以递归搜索当前目录及其子目录中的所有文件。

grep -r "autos_create" *

使用命令行技巧 💡

掌握一些技巧可以提高命令行使用效率。

1. 命令自动补全

在输入文件名或目录名时,可以按 Tab 键自动补全。如果存在多个匹配项,按两次 Tab 键会显示所有选项。

2. 历史命令导航

使用上下箭头键可以浏览之前输入的命令。按 Enter 键可以重新执行选中的命令,这能节省大量时间。

识别不同的交互环境 🔍

在使用命令行时,你可能会遇到不同的提示符,这表示你正在与不同的程序交互。识别这些提示符很重要,以免输入错误的命令。

1. Python交互环境

输入 python 命令会进入Python交互环境,提示符变为 >>>

python

在此环境中,你可以输入Python代码,但不能使用Shell命令。要退出Python环境,输入 quit()

quit()

2. SQLite交互环境

输入 sqlite3 命令并指定数据库文件会进入SQLite交互环境,提示符变为 sqlite>

sqlite3 db.sqlite3

在此环境中,你可以执行SQL命令。SQL命令必须以分号 ; 结尾。要退出SQLite环境,输入 .quit

.quit

3. Django Shell

在Django项目目录中,输入以下命令可以进入Django Shell:

python manage.py shell

Django Shell看起来像Python交互环境,但它预加载了Django应用程序的所有代码。你可以在此执行Django特定的操作。要退出Django Shell,同样使用 quit()

总结 📚

本节课我们一起学习了在PythonAnywhere上使用命令行Shell的基本操作。我们介绍了几个常用的Shell命令,如 lspwdcdrmgrep,并掌握了一些提高效率的技巧,如命令自动补全和历史命令导航。此外,我们还学习了如何识别和退出不同的交互环境,包括Python、SQLite和Django Shell。理解这些内容将帮助你在服务器环境中更有效地工作。

009:密歇根州安娜堡

概述

在本节课中,我们将回顾一次特别的面对面办公时间活动。这次活动汇集了来自Coursera、edX的在线学生以及密歇根大学安娜堡校区的在校学生。我们将聆听几位学生分享他们学习本系列课程的经历、收获以及这些课程如何影响了他们的学术与职业发展。

活动介绍

我们正在进行又一次办公时间活动。这次活动有些特别。这是一次在线课程(Coursera学生和edx学生)与目前身处密歇根大学安娜堡校区或校园内的学生之间的交流活动。

因此,我想让你们认识一些你们的同学。我们将从Jeremy开始。

学生分享

以下是几位参与学生的分享。

Jeremy
Jeremy是Ts的店主。他学习了“Python for everybody”课程。这是一次不可思议的经历。他实际上正在考虑接下来要学习Django还是PHP课程。他是活生生的例子,证明任何人都能从这门课程中获益良多。他强烈推荐这门课程,无论你是否是传统意义上的学生。这门课程非常棒,并且会在你的生活中发挥作用。他保证这一点。

Gel
Gel学习了Charles的课程。他学习了Django课程和物联网课程。他非常喜欢这些课程,并且它们帮助他在当时开发了现实生活中的应用程序。他将永远为此心怀感激。他当时在哥伦比亚。他当时是一名本科生,但对软件开发一直很感兴趣,并且懂一些Python。他发现这门在线的Django课程可以帮助他构建一个某人想要的、高度定制化的应用程序。所有的图像上传、整个CRUD系统、GET和POST请求,一切都完美地协同工作。这正是他喜欢这门课程的地方——它很真实。这门课程能真正让你准备好开发大多数公司都需要的那种应用程序。

Jacob
Jacob学习了“Python for everybody”和互联网历史课程。他是密歇根大学机器人专业的大二学生。他学习这门课程只是因为它非常有趣。Charles博士让这门课变得如此有趣,让人能记住所有内容,这真的很容易。课程中提到的所有有趣的事情也非常酷。他实际上在申请密歇根大学的入学文书中提到了这门课程,并且他被录取了。他确信这就是他被录取的原因。他也将其用于研究生院的申请。这确实有效。

Scott
Scott学习了一些PostgreSQL课程和一些Django课程。这对他所做的一个个人项目非常有帮助,他把这个项目写进了简历。他认为这对于他参加的许多面试都非常重要,因为他感觉很多面试官希望看到你在课外也主动学习过。拥有一个能展示技能的个人项目非常有帮助。他接下来想学习互联网历史课程。他认为这是在学校很多课程中不会深入探讨,但了解计算机领域某些选择背后的动机非常有趣。

Riva
Riva学习了互联网历史和“Python for everybody”系列课程。她之前并不了解Python,但这门课程或系列让她对Python编程整体产生了浓厚兴趣。她尤其被网络爬虫所吸引,因为她认为它具有很多能力,比如可以从各种网站提取数据,用于创建数据库或进行分析。Python是一门非常通用的语言,她认为每个人都应该学习它。她学习Coursera课程既是为了个人提升,也是为了在课外获得一些主动学习的经历。她强烈推荐Charles博士的课程。

另一位学生
这位学生学习了PostgreSQL课程和Django课程。

总结

本节课中,我们一起回顾了一次特别的线下学生分享会。我们了解到,学习在线课程并将其经历写入申请文书,可能有助于你被密歇根大学录取。我们从多位学生那里听到了他们学习Python、Django、数据库等课程的真实体验和积极反馈。这些课程不仅有趣、易于学习,更能帮助学习者构建真实可用的应用程序,提升个人技能,并在学术和职业发展中发挥实际作用。

010:HTML超文本标记语言第一部分

概述

在本节课中,我们将要学习超文本标记语言(HTML)的基础知识。我们将了解HTML在请求-响应周期中的位置,回顾其历史发展,并探讨编写规范HTML的重要性。

HTML在请求-响应周期中的角色

上一节我们介绍了Web应用的基本通信模型。本节中我们来看看HTML在其中扮演的具体角色。

当我们点击一个链接时,浏览器会向网络服务器发送请求。服务器运行代码,可能查询数据库,然后生成响应。这个响应通常就是HTML。浏览器接收HTML后,会解析它并构建文档对象模型(DOM),最终呈现出我们所看到的网页。

在本讲座中,我们将主要关注响应部分,即HTML本身,以及它如何被解析和呈现。

HTML是一种使用特殊字符(如 <>)来添加标签的技术,这些标签用于指示我们希望在网页上看到的内容。例如,<p> 是段落标签,<strong> 标签使内容加粗,<em> 标签用于强调。

我们通过这些标签来“标记”内容,从而传达语义信息。

HTML与Web的演进

Web技术,包括HTML和超文本传输协议(HTTP),是相对近期的发明,至今发展不足三十年,并且仍在持续演进。HTML和CSS正处于技术的前沿。

回顾早期,HTML的设计初衷与今天大不相同。在20世纪90年代初,它运行在NeXT计算机上。早期的浏览器,如来自CERN的NeXT浏览器,每个内容都在新窗口中打开,图片也不是内联显示的。随后出现的NCSA Mosaic是第一个在UNIX、Windows和Macintosh上普遍可用的浏览器,它具有灰色背景、蓝色链接,访问过的链接会变成紫色。

早期的网页与今天我们所见的截然不同。那时,人们仅仅因为能够点击链接并看到事情发生而感到惊奇和满足。而今天,商业利益驱动着网页必须设计得极其精美,关注像素数量、元素对齐、布局和导航外观。因此,我们现在需要创建美观的网页,而在过去,能拥有一个正常工作的网页就足以让人惊叹。

当然,计算机性能已大幅提升,能够处理视频和图像。在过去,图像对网络带宽和计算机显示时间来说成本都很高,这影响了当时网页的设计方式。使用“时光机”(即互联网档案馆)回顾这些旧网页是件有趣的事,你会发现其中很多仍然能够工作,这本身就很神奇。

早期HTML的“狂野西部”时代

在早期,HTML有点像“狂野西部”。浏览器不希望显示“破碎”的HTML,因此它们会主动补偿错误。

那时,标签可以是大写的,段落标签可能没有闭合,列表标签可能没有结束,属性甚至可能没有用双引号括起来。存在各种各样不规范的情况。实际上,你可以把这样糟糕的页面代码放入浏览器,它仍然会尝试显示。

所以,虽然从技术上讲HTML是一种非常精确的语言,并且你可能犯语法错误,但浏览器在解析HTML时极其灵活。然而,这并不意味着你会得到可预测的结果。浏览器可能会进入所谓的“怪异模式”。

标准化与W3C的建立

为了建立我们今天所拥有的标准环境,万维网的创始人之一蒂姆·伯纳斯-李帮助成立了一个名为万维网联盟(W3C)的组织。

W3C决定,不能仅仅让各个浏览器厂商自行定义HTML,而是应该编写一份规范来定义HTML是什么。然后,多个厂商可以基于此规范生产浏览器。虽然这花费了一些时间才取得成功,但由于每个厂商都需要构建浏览器,他们与万维网联盟合作,在90年代中后期(大约1994年至1999年)制定了许多优秀的Web标准。

编写规范的HTML

一旦我们有了规则,我们就倾向于遵循这些规则。

因此,标签需要小写,像 src="image.jpg" 这样的属性必须用双引号括起来,必须有开始标签和结束标签。现在,我们对HTML的要求更加精确,并尽可能规范地编写它。

这样做是为了从浏览器中获得最佳的性能和渲染效果。如果你不编写规范的HTML,浏览器将自行决定如何渲染内容;而如果你编写规范的HTML,你才能真正控制浏览器的布局方式。

总结

本节课中我们一起学习了HTML的基础知识。我们了解了HTML在Web请求-响应流程中的关键作用,回顾了其从早期简单、容错性强的“狂野西部”状态,发展到今天由W3C标准严格规范的过程。我们认识到,编写规范、精确的HTML对于确保网页在不同浏览器中具有一致的外观和性能至关重要。

下一节,我们将深入探讨HTML文档本身,查看一些HTML示例,并详细学习HTML的语法。

011:HTML超文本标记语言第二部分

概述

在本节课中,我们将继续学习HTML的基础语法,包括文档结构、特殊文件名、标签与属性、特殊字符处理,以及超链接、图像、列表和表格等核心元素的用法。我们将了解HTML如何通过标记来定义内容的结构和含义,而不是其外观。

文档结构与特殊文件名

上一节我们介绍了HTML的基本概念,本节中我们来看看一个标准HTML文档的布局和一些约定俗成的文件名。

一个HTML文档通常以<html>标签开始。文档顶部可能有一个<!DOCTYPE>声明。文档内部分为<head><body>两部分。<head>包含非打印信息,如样式声明、页面标题和JavaScript加载。页面的主要内容则放在<body>标签内。

以下是文档结构的基本代码示例:

<!DOCTYPE html>
<html>
<head>
    <title>页面标题</title>
</head>
<body>
    <!-- 页面内容在这里 -->
</body>
</html>

关于文件名,Web服务器在访问一个目录时,会按照配置寻找特定的默认文件。常见的默认文件名包括:

  • index.html
  • index.htm
  • index.php
  • default.htm(Windows服务器)

不同系统的配置可能不同,但index.html通常是最通用的选择(除非使用PHP,则常用index.php)。当你在本地磁盘查看文件时(例如下载了课程提供的html.zip压缩包),需要手动打开index.html文件。

相对链接与标签嵌套

我们可以在同一目录下存放多个文件,并使用相对链接在它们之间创建连接。查看课程提供的zip文件,你会看到许多文件通过锚点标签(<a>)相互指向。

正如之前提到的,HTML使用如段落标签<p>、加粗标签<strong>和强调标签<em>等标签。这些标签必须成对出现,有开始和结束标签。HTML的一个重要特性是,我们是在逻辑上标记文本。例如,我们指定某段文字是一个段落,但我们在编写HTML时输入的回车换行并不重要。

浏览器会根据自身的宽度重新排列文本。这是因为我们定义的是段落,而不是像在纸上那样固定布局。浏览器窗口可能很大,也可能很小(例如在手机上),因此我们通常不希望硬性规定像素宽度(除非进行复杂布局)。对于段落,你只需告诉浏览器“这是一个段落”,然后由浏览器负责换行。这也意味着页面的高度会随之改变。

HTML中的空白字符(空格、换行等)通常会被浏览器忽略。但有些标签如<pre>(预格式化文本)会保留这些空白和换行。大多数时候,我们让浏览器自动处理换行即可。

标签、属性与特殊字符

标签有开始标签和结束标签。结束标签以斜杠/表示。标签可以嵌套。例如,一个<p>标签内部可以包含<strong><em>标签。这表示整个内容是一个段落,其中包含一段加粗文本。

也存在自闭合标签,例如图像标签<img>。你可以在标签内使用斜杠自闭合(如<img src="..." />),这等同于单独的结束标签。

在开始标签上,我们可以添加属性,即键值对。例如,查阅<img>标签的文档,你会知道需要添加src属性来指定要显示的图片文件。

由于小于号<、大于号>等字符在HTML中有特殊含义,我们需要一种方法来显示它们本身。我们使用HTML实体来实现。例如:

  • 小于号:&lt;
  • 大于号:&gt;
  • 与符号:&amp;
  • 版权符号:&copy;

HTML有一整套特殊字符实体,包括数学符号等,你可以根据需要查阅。但最需要掌握的是<>&这三个。

HTML注释以<!--开始,以-->结束。注释可以跨越多行。

超链接:HTML的核心

超链接是HTML的关键元素,也是搜索引擎工作的基础,更是“超文本”中“超”字的体现。它使我们能够通过点击链接到新页面,从而构建知识网络。

锚点标签<a>用于创建超链接。可点击的文本位于开始标签<a>和结束标签</a>之间。href(超文本引用)属性用于指定链接的目标URL。这个URL会被放入浏览器地址栏,从而跳转到新页面,形成一个可点击的热点。

在早期,为了让用户知道哪里可以点击,链接通常被设计成醒目的蓝色并带有下划线。此外,访问过的链接会变成紫色,这有助于用户在互联网规模还不大的时候,追踪自己已经浏览过的内容。

链接分为绝对引用和相对引用。绝对引用以http://https://等协议开头。相对引用则不是,它假设目标文件与当前文件位于服务器的同一目录下。如果你想链接到不同文件夹或不同服务器上的文件,就必须使用绝对引用。

图像、列表与表格

图像为网页增添了许多乐趣。图像标签是<img>,其src属性指定图像文件。图像可以单独显示,也可以像字符一样嵌入在文本中。需要注意的是,图像可能很大,所以示例中使用了一个小图。

你甚至可以将图像本身作为锚点标签的可点击部分。例如,点击一个图片可以跳转到另一个页面(如list.html)。

列表是HTML的重要组成部分,分为有序列表<ol>和无序列表<ul>。无序列表以<ul>开始和结束,界定整个列表范围。每个列表项使用<li>标签。如果你希望列表项之间有默认的间距,可以将段落<p>放在<li>里,但这不是必须的,你也可以直接放链接或其他内容。列表会自动添加项目符号并进行缩进。

表格在早期曾有一段曲折的历史,当时人们滥用表格来进行页面布局(如在表格单元格中放置段落和图片),这种做法已被淘汰,现在我们用CSS进行布局。然而,表格始终是展示表格化数据(如汽车列表,包含品牌、型号、里程等列)的理想选择。

表格标记很直接:用<table>开始,用<tr>定义行。表头行使用<th>标签,数据行使用<td>标签。关键在于,我们只将表格用于真正的表格数据,而不是用于非表格内容的布局。

总结

本节课我们一起学习了HTML的核心语法和元素。我们了解了文档结构、相对与绝对链接、标签与属性的使用、特殊字符的处理方法。重点掌握了超链接的创建、图像的嵌入、列表的构建以及表格的正确用法。记住,HTML的核心在于定义内容的结构和语义,而非其视觉呈现。将样式控制交给CSS,这将是我们下一讲的主题。

我将在www.wa4e.com/code/html提供一些示例代码的讲解,鼓励你查看这些简单的页面,使用开发者模式观察它们,并查看源代码,以更好地理解HTML基础是如何运作的。HTML是一个持续演进、充满创造力的领域,非常有趣。

012:HTML代码详解 🧑‍💻

在本节课中,我们将通过分析示例代码来深入了解HTML。我们将探讨文档对象模型与源代码的区别,并逐一解析常见的HTML元素和结构。

概述

我们将通过一个示例HTML文件,学习如何查看源代码和文档对象模型,理解它们之间的差异,并探索段落、链接、列表、特殊字符、图像和表格等基本HTML元素的用法。


文档对象模型与源代码 🔍

上一节我们介绍了课程目标,本节中我们来看看HTML代码的两个重要视图:源代码和文档对象模型。

源代码是服务器发送给浏览器的原始文件内容。你可以通过浏览器的“查看页面源代码”功能看到它。

文档对象模型是浏览器解析源代码后生成的结构化表示。它更“整洁”,并且可以通过开发者工具动态修改。在开发者工具的“元素”面板中看到的就是DOM。

核心概念

  • 源代码:原始的、静态的HTML文件。
    <p>这是一个未闭合的段落</p>
    
  • 文档对象模型:浏览器解析后生成的、可交互的树状结构。
    // DOM可以通过JavaScript动态修改
    document.querySelector('p').textContent = '修改后的内容';
    

两者关键区别在于:源代码是固定不变的,而DOM可以被JavaScript动态改变


基础文档结构 📄

以下是HTML文档的基本骨架。

<!DOCTYPE html>
<html>
<head>
    <title>页面标题</title>
</head>
<body>
    <!-- 页面内容在这里 -->
</body>
</html>

在示例中,<head>标签包含元信息,<body>标签包含所有可见内容。浏览器会忽略HTML中多余的空格和换行,渲染时将它们压缩。


文本与段落标签

现在,我们来学习如何组织文本内容。

以下是常用的文本级标签:

  • <p>:定义段落。浏览器会自动在段落前后添加一些边距。
  • <strong>:加粗文本,表示重要性。
  • <em>:倾斜文本,表示强调。
  • <a>:定义超链接。href属性指定链接地址,标签之间的文本是可点击的。
  • <pre>:预格式化文本。它会保留源代码中的所有空格和换行,并使用等宽字体显示,常用于展示代码。

注意<a>标签的href属性可以使用相对路径(如list.htm)或绝对路径(如https://www.example.com)。浏览器会将相对路径转换为完整的绝对URL。


列表与导航 🧭

列表是组织一系列项目的有效方式。在示例中,一个无序列表<ul>被用作导航菜单。

以下是列表的结构解析:

  • 整个列表由<ul>开始,</ul>结束。
  • 列表中的每一项都由<li>标签包裹。
  • 可以在<li>标签内部使用<p>标签来增加项目之间的间距。

这个结构清晰地展示了如何用HTML创建层次化的导航链接。


特殊字符

在HTML中,某些字符具有特殊含义,必须使用实体引用来表示。

以下是必须转义的核心字符及其代码:

  • 小于号 <&lt;
  • 大于号 >&gt;
  • 和号 &&amp;

此外,还可以使用实体引用显示各种符号,例如:

  • 黑桃 ♠:&spades;
  • 红心 ♥:&hearts;
  • 方块 ♦:&diams;

浏览器内置了这些字符的显示支持。


链接与图像 🌐

链接和图像是网页最基本的媒体元素。

关于链接的更多细节:

  • <a>标签的target="_blank"属性可以让链接在新标签页中打开。
  • 链接的样式会随状态改变(例如,访问过的链接通常会变成紫色)。

图像通过<img>标签嵌入。

  • src属性指定图像文件的路径(可以是相对或绝对路径)。
  • 图像默认像一个大字符一样嵌入在文本流中。
  • 可以将<img>标签放在<a>标签内部,从而创建一个可点击的图片链接。


表格 📊

表格用于展示行列数据,不应再用于整个页面的布局。

一个基本的表格结构如下:

<table>
    <thead>
        <tr><th>标题1</th><th>标题2</th></tr>
    </thead>
    <tbody>
        <tr><td>数据A</td><td>数据B</td></tr>
    </tbody>
</table>

即使源代码中遗漏了<tbody>标签,浏览器在构建DOM时也会自动补全,以确保结构的正确性。这再次体现了DOM是经过浏览器“修复”和标准化后的版本。


容错性与错误HTML ⚠️

HTML规范具有很强的容错性。浏览器会尝试修复一些常见的代码错误。

浏览器会自动修正的错误包括:

  • 标签名大小写不统一(会统一转为小写)。
  • 某些标签未正确闭合(会尝试自动闭合)。
  • 属性值缺少引号(会尝试补全)。

重要提示:尽管浏览器会修复错误,但编写符合标准的HTML至关重要。依赖浏览器的纠错可能导致不可预测的渲染结果,尤其是在复杂的页面或不同的浏览器中。始终使用验证工具检查你的代码。


总结

本节课中我们一起学习了HTML的核心实践。
我们区分了源代码文档对象模型,理解了DOM是动态且可变的。
我们练习了使用段落、强调、链接、列表、特殊字符、图像和表格等基本元素来构建内容。
我们还了解到浏览器会尝试修复有瑕疵的HTML代码,但开发者仍应致力于编写清晰、标准的代码。

通过操作示例代码,你现在应该对HTML如何工作以及如何在浏览器中检查和调试它有了更直观的认识。

013:HTML与HTTP的发明者

在本节课中,我们将了解万维网发明者蒂姆·伯纳斯-李对网络未来发展的看法。他阐述了网络基础设施完善后可能引发的下一场革命。

上个月,我参加了在波士顿举办的第四届万维网年度会议。我有机会与万维网的发明者蒂姆·伯纳斯-李交谈。我向他请教了一些关于网络未来发展的问题。我们有一段蒂姆的简短录像,他在其中讲述了网络未来的方向。

以下是蒂姆·伯纳斯-李分享的核心观点:

  • 网络尚未展现全部潜力:蒂姆认为,目前我们所见的网络应用仅仅是开始。
  • 基础设施是革命的前提:他强调,只有当网络成为像电力一样可靠、可被“假定”存在的基础设施时,真正的变革才会发生。
  • 下一场革命将是文化性的:在技术基础设施完备之后,革命的重点将从技术本身转向社会与文化层面。

蒂姆指出,万维网革命之所以能够发生,是因为互联网已经作为一项基础技术在全球悄然部署。同样,当万维网本身成为人们可以“假定”存在的基础设施时,下一场变革——可能是一场“文化革命”——就会到来。这场革命可能带来我们目前难以想象的、更好的做事方式。

本节课中我们一起学习了蒂姆·伯纳斯-李对网络演进的深刻见解。他提醒我们,当前的技术发展是在为更深层次的社会与文化变革铺设道路。真正的创新往往发生在底层技术变得透明和普适之后。

014:路易斯安那州新奥尔良

在本节课中,我们将回顾一次在新奥尔良举行的面对面办公时间活动,并聆听几位参与课程学习的学生分享他们的经历与心得。

活动开场

大家好,我们又一次来到了面对面办公时间。这次活动地点在路易斯安那州的新奥尔良。尽管通知时间很短,不到24小时,我们仍有几位学生到场。接下来我将介绍他们,并请他们向各位同学简单说几句。

学生分享

以下是到场学生的自我介绍与学习经历。

  • 大卫·米勒:我学习了《互联网历史》这门课程,是和我身边的儿子一起学的。我住在路易斯安那州的斯莱德尔。
  • 亚伯兰·米勒:我学习了《Python for Everybody》和《互联网历史》这两门课程。课程非常有趣,谢谢。
  • 艾米丽·阿什利:我在2015年学习了《Python for Everybody》课程,现在是一名软件开发人员。

深入交流:职业转型故事

上一节我们认识了到场的学生,本节中我们来深入了解一下艾米丽的职业转型经历。

在参加《Python for Everybody》课程之前,艾米丽在非营利组织工作,拥有城市规划学位,当时并非软件开发人员。

那么,从学习Python到成为软件开发人员之间,她做了什么呢?她通过参加软件会议来了解人们正在构建什么以及使用什么技术,并学习了这些知识。此外,她还参加了一个编程训练营。

关于这个故事,我认为最值得称道的一点是,她以《Python for Everybody》课程为起点,参加了一些高强度的训练营。我特别欣赏的是,参加行业会议是一种结识同行、了解领域动态并最终找到进入公司途径的好方法。我认为这非常棒。

活动尾声与未来预告

以上就是我们在新奥尔良的全部内容。接下来几个月,我还会前往日本、南非和蒙特利尔,希望能在那里见到大家。

顺便一提,刚才背景中走过的是乔治华盛顿大学的首席信息官PB·加勒特,他也是我的一位朋友。那么,来自新奥尔良的问候就到这里。

总结

本节课中我们一起回顾了新奥尔良办公时间的活动,并聆听了三位学生的分享,特别是艾米丽从非营利领域成功转型为软件开发者的励志经历。她的故事强调了以基础课程为起点,通过主动学习、参与行业社群和训练营来实现职业转换的有效路径。

015: CSS层叠样式表第一部分 🎨

在本节课中,我们将要学习CSS(层叠样式表)的基础知识。CSS是用于控制网页外观和布局的语言,它能让我们的HTML内容变得美观。我们将了解CSS的基本语法、如何将其与HTML关联,以及它在现代Web开发中的重要性。


上一节我们介绍了HTML作为网页内容的结构。本节中我们来看看如何通过CSS为这些结构添加样式。

请求响应周期中的CSS

理解CSS的作用,最好将其置于请求响应周期的背景下。用户点击链接,浏览器发送HTTP请求,服务器可能查询数据库后返回HTML。浏览器解析HTML并构建文档对象模型(DOM),最终呈现在屏幕上。到目前为止,我们只讨论了HTML的格式。

今天我们将讨论,除了HTML,浏览器还会请求另一个文件——CSS文件。CSS将作用于文档对象模型,改变页面的视觉呈现。CSS可以内嵌在HTML中,也可以存放在独立的文件中,通过单独的请求获取。我们主要关注的是如何利用CSS让DOM变得美观。

安装Web开发者工具

为了更有效地学习和调试CSS,我推荐你安装一个名为“Web Developer”的浏览器插件,它由Chris Pederick开发。这个插件提供了一些浏览器内置开发者工具所没有的实用功能,特别是对于CSS样式的开启、关闭和排查非常有用。它是一个浏览器扩展,适用于Chrome、Safari、Firefox等主流浏览器。在后续的一些作业中,你可能会需要用到它。

CSS的历史与重要性

在HTML课程中我们提到,早期的网页只有灰色背景和蓝色链接列表,但这已足以让人惊叹。然而,随着Web进入商业领域,精确的布局和美观的页面变得至关重要。例如,像雅虎这样的公司会研究像素级的间距对用户体验和收入的影响。CSS正是为了满足这种对像素级完美布局的需求而发展起来的,它是一门非常精确的语言。

早期曾尝试用HTML表格等方式来控制外观,但这从来都不是一个好方法。CSS的出现实现了内容(HTML)与表现(样式)的分离。

无CSS的网页:结构与可访问性

使用Chris Pederick的Web Developer插件,你可以查看关闭CSS后的网页。一个设计良好的网页,即使没有CSS,其HTML结构本身也应该是清晰、逻辑分明且易于导航的。这就像回到了Web的早期时光。

这种简洁的结构化HTML对于可访问性至关重要。视障用户依赖屏幕阅读器来“听”网页,清晰的结构能让他们更好地理解内容。在Web发展过程中,曾有一段时间复杂的HTML标记破坏了这种可访问性。如今,我们回归到创建简洁的HTML和用CSS添加样式的模式,这确保了所有人,包括视障用户,都能访问网站。

CSS与HTML的协作关系

最终,浏览器中呈现的页面是由HTML和CSS共同构建的。HTML文档被请求并下载,其中可能包含一些内联CSS,但大部分样式通常来自外部的CSS文件,有时还需要JavaScript的配合。

关键在于,HTML本身不包含“美”,美来自于CSS。因此,即使你是一名后端开发者而非设计师,也需要了解足够的CSS知识,以便编写出结构良好的HTML,让专业的CSS开发者能够在此基础上创造出出色的视觉效果。

大型项目中的职责分离

在大型项目中,例如开源的Sakai学习管理系统,CSS实现了“关注点分离”。后端开发者负责业务逻辑并生成HTML,而前端设计师则专注于CSS样式。HTML成为后端开发者和前端设计师协作的交汇点。前端设计师可能需要微调HTML,并主要编写CSS。

这种分离意味着多人可以协作,并发展出高度专业化的技能。我的专长在于后端,CSS方面能力有限。如果我做出了好看的页面,很可能是我使用了像Bootstrap这样的CSS框架。我的职责是编写结构良好的HTML,以便专业的CSS开发者能为其添加样式,使其变得非常美观。

CSS基础语法

CSS的语法与HTML不同。HTML使用尖括号<>,而CSS使用花括号{}和分号;,看起来有点像C语言。它本质上是一系列规则,虽然规则的顺序很重要(后定义的规则优先级更高),但它不是按顺序执行的代码。

一个CSS规则的基本结构如下:

selector {
    property: value;
    property: value;
}

  • 选择器: 指定要应用样式的HTML元素。最简单的选择器是标签名,例如 bodypbody 选择器意味着将样式应用于文档中的所有 <body> 标签。
  • 声明块: 由花括号 {} 包裹,包含一个或多个用分号 ; 分隔的声明。
  • 声明: 每个声明由一个属性和对应的组成,例如 font-size: 120%;

你可以把CSS规则想象成:找到文档中的某些部分,然后用“画笔”为它们上色。选择器就是告诉你“画哪里”,声明块就是告诉你“用什么颜色和方式画”。

CSS属性速查表

我必须承认,我并不是一个熟练的CSS开发者,经常需要借助速查表。CSS属性非常多,例如 background-colorborder-bottompadding 等。它们的含义通常很直观,但具体的拼写和用法我常常记不住。拥有一份速查表对我非常有帮助。网上有大量关于每个CSS属性的优秀文档和示例。

在接下来的学习中,我们将逐步练习使用这些CSS属性。我也会通过示例代码,逐行讲解如何应用它们。


本节课中我们一起学习了CSS的基础概念、其重要性、与HTML的协作方式以及基本语法。下一节,我们将探讨如何将CSS与HTML文档关联起来。

016:CSS层叠样式表第二部分

概述

在本节课中,我们将学习如何在HTML中使用CSS,具体探讨将CSS规则应用到HTML不同部分的三种基本方法。我们还将了解<span><div>这两个没有默认样式的特殊标签,以及如何使用ID和类选择器来精确地选择和控制页面元素。

CSS与HTML的结合方式

上一节我们介绍了CSS的基本语法,本节中我们来看看如何将CSS规则应用到HTML文档中。主要有三种方法。

1. 内联样式

最简单但最少用的方法是使用内联样式。你可以在任何HTML标签上使用style属性,并在其中直接写入CSS规则。

示例代码:

<p style="border-style: solid; border-color: red; border-width: 5px; font-family: monospace;">
  这是一个带有红色边框和等宽字体的段落。
</p>
<body style="font-family: Arial, sans-serif;">
  ...
</body>

在这个例子中,<p>标签被设置为红色实线边框和等宽字体,而<body>标签则设置了Arial字体。由于<p>标签的样式声明更“接近”元素本身,所以它的font-family设置会覆盖<body>的全局设置。这体现了“层叠”的核心概念:距离目标元素越近的样式声明,优先级越高(除非使用了!important规则,我们稍后会讨论)。

2. 内部样式表

另一种方法是将CSS规则放在HTML文档的<head>区域内的<style>标签中。

示例代码:

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      font-family: Arial, sans-serif;
    }
    h1 {
      color: blue;
    }
    p {
      border-style: solid;
      border-color: red;
      border-width: 5px;
    }
    a {
      color: green;
      text-decoration: none;
      background-color: lightgray;
    }
  </style>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

这样,所有<body>内的元素都会继承Arial字体,所有<h1>标题会变成蓝色,所有<p>段落会有红色边框,所有<a>链接会变成绿色无下划线且背景为浅灰色。这种方法适用于单个页面,但当网站有多个页面时,维护起来会很麻烦。

3. 外部样式表

最常用、最推荐的方法是将CSS规则写入一个单独的文件(.css文件),然后在HTML中通过<link>标签引入。

HTML文件 (index.html):

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="rules.css">
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

CSS文件 (rules.css):

body {
  font-family: Arial, sans-serif;
}
h1 {
  color: blue;
}
p {
  border-style: solid;
  border-color: red;
  border-width: 5px;
}
a {
  color: green;
  text-decoration: none;
  background-color: lightgray;
}

浏览器会单独请求rules.css文件,并将其中的规则应用到HTML文档中。这种方法便于在多页面间共享和复用样式,也利于维护。

三种方式的优先级与协作

通常,最佳实践是:

  1. 主要样式放在外部样式表中。
  2. 对于某个页面特有的微调,使用内部样式表<style>标签)。
  3. 对于极个别元素的特殊样式,使用内联样式

它们的优先级遵循“层叠”原则:内联样式 > 内部样式表 > 外部样式表(同样,距离元素越近优先级越高)。你可以通过这种组合方式,灵活地控制整个网站的外观。

无默认样式的标签:<span><div>

随着CSS的普及,开发者需要一些“干净”的标签作为画布,以便完全通过CSS来控制样式,而不受HTML标签历史默认样式的影响。这就是<span><div>的用途。

以下是它们的关键区别:

  • <span>:是一个行内元素,本身没有任何默认样式(如边距、边框)。它用于对一小段行内文本进行样式化或分组。
  • <div>:是一个块级元素,本身同样没有任何默认样式。它用于定义文档中的分区或节,是页面布局的基石。

示例代码:

<p style="border: 1px solid green;">这是一个有默认边距的段落。</p>
<div style="border: 1px solid blue;">这是一个没有边距的div块。</div>
<span style="color: green;">这是一段被span包裹的绿色文字。</span>
<div style="border: 1px solid orange;">
  <div style="border: 1px solid red;">这是一个嵌套在橙色div内的红色div。</div>
</div>

<p>标签本身有默认的边距(margin)和内边距(padding),使其看起来更美观。而<div><span>则从零开始,你需要通过CSS为其添加所有样式,包括布局、边距、颜色等。这使得它们成为构建复杂页面布局的理想选择。

精确选择:ID 与 类选择器

我们经常需要更精确地选择特定元素来应用样式,而不是针对所有同类标签。这时就需要使用ID和类选择器。

ID 选择器

ID属性在单个HTML文档中应该是唯一的。它用于标识页面中一个特定的、唯一的元素(如页头、主内容区)。

CSS中使用 # 来指定ID选择器。

示例代码:

<style>
  #first {
    background-color: lightblue;
    padding: 10px;
  }
</style>
<div id="first">这个div拥有唯一的ID“first”。</div>

类选择器

类属性可以在单个HTML文档中用于多个元素。它用于标识一组具有相同样式的元素。

CSS中使用 . 来指定类选择器。

示例代码:

<style>
  .highlight {
    background-color: yellow;
  }
  .center {
    text-align: center;
  }
</style>
<p class="highlight">这个段落被高亮。</p>
<p class="highlight center">这个段落同时被高亮和居中。</p>

组合与层级选择

你可以组合使用这些选择器,实现更精细的控制。

以下是几种高级选择器用法:

  • 多个类:一个元素可以拥有多个类(如 class=“shout loud”),它将同时应用这两个类定义的所有样式。
  • 后代选择器:可以选择特定元素内部的元素。

示例代码:

<style>
  /* 选择类为“shout”的元素 */
  .shout {
    text-transform: uppercase; /* 文字转为大写 */
  }
  /* 选择类为“loud”的元素 */
  .loud {
    color: red;
  }
  /* 选择ID为“third”的元素内部的所有<p>标签 */
  #third p {
    background-color: lightyellow;
  }
</style>

<p class=“shout loud”>这段文字既是红色又是大写。</p>

<div id=“third”>
  <p>这个段落背景是浅黄色。</p>
  <p>这个段落背景也是浅黄色。</p>
</div>
<p>这个段落不受影响,背景不是浅黄色。</p>

使用后代选择器(如 #third p)可以避免给内部每个<p>标签都单独添加类名,使代码更简洁。

总结

本节课中我们一起学习了:

  1. 将CSS融入HTML的三种方式:内联样式内部样式表外部样式表,理解了它们之间的优先级(层叠)关系。
  2. 认识了两个没有默认样式的关键HTML标签:行内元素 <span> 和块级元素 <div>,它们是现代网页布局的基础。
  3. 掌握了如何使用 ID选择器 (#id)类选择器 (.class) 来精确选择页面元素,并了解了后代选择器等高级用法,从而能够更高效、更有针对性地控制页面样式。

下一节,我们将探讨如何使用CSS来设置图片、颜色和字体等具体样式属性。

017:图像、颜色与字体 🎨

在本节课中,我们将学习如何使用CSS来美化网页,具体包括如何控制图像、颜色和字体。这些是让网页从功能化走向美观化的核心技能。

上一节我们介绍了CSS选择器和盒模型,本节中我们来看看如何运用这些知识来装饰我们的内容。

图像与浮动布局

CSS可以让我们灵活地控制图像的位置和外观。一个常见的技巧是使用float属性让文本环绕图片。

以下是一个让图片浮动到右侧并添加边框的CSS代码示例:

img {
    float: right;
    margin: 1em;
    border: 2px solid black;
}
  • float: right;:将元素(如图片)从正常的文档流中“取出”,并使其浮动到其容器的右侧。后续的文本内容会环绕它。
  • margin: 1em;:在图片周围添加边距,1em大约等于当前字体中字母“M”的宽度,这能创建舒适的留白。
  • border:为图片添加一个2像素宽的实线黑色边框。

有时,在浮动元素之后,您可能希望后续内容不再环绕,而是回到左侧边界正常显示。这时可以使用clear属性。

<br clear="all">

这个<br>标签会清除所有浮动效果,确保其后的内容从新的一行开始,不受之前浮动元素的影响。

颜色的使用

颜色是视觉设计的基础。CSS提供了多种方式来指定颜色。

以下是定义颜色的几种主要方法:

  1. 颜色名称:使用预定义的颜色名,如 redgreenblue。这种方法简单直接,适合快速测试和原型设计。
  2. 十六进制代码:使用#开头的六位十六进制数,格式为 #RRGGBB。例如,#FF0000 是纯红色,#00FF00 是纯绿色,#FFFFFF 是白色。每两位数字代表红、绿、蓝的强度(00-FF,即0-255)。
  3. RGB/RGBA值:使用 rgb(red, green, blue) 函数,参数是0-255的数字。例如,rgb(255, 0, 0) 是红色。rgba() 则多了一个透明度(Alpha)参数,范围是0(完全透明)到1(完全不透明),例如 rgba(255, 0, 0, 0.5) 是半透明的红色。

选择配色方案可以借助在线调色板工具,它们能帮助您创建和谐美观的颜色组合。

字体的控制

字体极大地影响网页的可读性和风格。默认的衬线字体(如Times New Roman)在屏幕上可能不够清晰,因此通常推荐使用无衬线字体(如Arial, Helvetica)。

设置字体时,需要使用 font-family 属性,并提供一个字体栈(font stack)。这是因为不同操作系统安装的字体不同,字体栈可以指定优先使用的字体列表,如果第一个不可用,则尝试下一个,最后以一个通用字体族(如 sans-serif)作为兜底。

body {
    font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}

在这个例子中,浏览器会依次尝试使用 Segoe UI、Tahoma 等字体,如果都不可用,则使用系统默认的无衬线字体。

您还可以控制字体的其他样式:

  • font-weight: bold;:设置粗体。
  • font-style: italic;:设置斜体。
  • text-decoration: none;:常用于移除链接的下划线。
  • font-size:设置字体大小。可以使用 px(像素)、em(相对于父元素字体大小)、rem(相对于根元素字体大小)等单位。相对单位(如 em, rem)在响应式设计中更具灵活性。

链接样式的定制

链接(<a>标签)有多个特殊的状态,可以分别设置样式,以提供更好的交互反馈。

以下是链接的四种主要状态:

  • a:link:未访问过的链接的样式。
  • a:visited:已访问过的链接的样式。
  • a:hover:鼠标悬停在链接上时的样式。
  • a:active:链接被点击瞬间的样式。

一个常见的样式设置是移除默认的下划线,并在鼠标悬停时重新添加或改变颜色:

a {
    color: black;
    text-decoration: none; /* 移除下划线 */
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/d3ed335669f46a6aba39302c6248b43e_20.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/d3ed335669f46a6aba39302c6248b43e_21.png)

a:hover {
    text-decoration: underline; /* 悬停时显示下划线 */
    color: blue;
}

总结

本节课中我们一起学习了CSS在美化网页方面的几个关键应用:通过floatclear控制图像与文本的布局;使用颜色名称、十六进制码或RGB值来定义色彩;通过font-family字体栈来确保字体的跨平台一致性,并控制其大小和样式;最后,我们还了解了如何为链接的不同状态定制样式,以提升用户体验。

CSS是一门不断进化的艺术与科学,虽然基础概念相对稳定,但新的属性和技术(如Flexbox、Grid布局、CSS自定义属性等)一直在涌现。掌握这些基础知识,您就已经能够显著改善网页的外观。对于更复杂和精美的设计,可以借助像Bootstrap这样的前端框架,或者深入研究CSS的广阔世界。希望本教程对您的学习有所帮助!

018:CSS代码详解第一部分 🎨

在本节课中,我们将学习层叠样式表(CSS)的基础知识。我们将了解如何将样式应用于HTML元素,探索CSS的“层叠”特性,并学习如何使用选择器、类(class)和ID来精确控制网页的外观。


CSS简介与基本语法

层叠样式表(CSS)用于控制网页的视觉呈现。CSS可以通过多种方式应用到HTML元素上。

以下是应用CSS的几种基本方式:

  • 内联样式:直接在HTML标签的style属性中定义样式。例如:<p style="color: blue;">
  • 内部样式表:在HTML文档的<head>部分使用<style>标签定义样式规则。
  • 外部样式表:将样式规则写入独立的.css文件,然后在HTML中通过<link>标签引入。

一个CSS规则由选择器声明块组成。声明块包含一个或多个用分号分隔的属性-值对。其基本语法如下:

选择器 {
    属性1: 值1;
    属性2: 值2;
}

例如,color: blue; 中,color是属性,blue是值。


理解“层叠”与开发者工具

上一节我们介绍了CSS的基本语法,本节中我们来看看CSS的核心特性——“层叠”。CSS的“C”代表“Cascading”(层叠),这意味着样式可以来自多个来源,并且有明确的优先级规则。

我们可以使用浏览器的开发者工具来直观地查看和理解这些规则。在工具中选中一个元素,可以看到应用到它身上的所有CSS规则,并了解它们的来源(用户代理样式表、作者样式表、内联样式等)和优先级。

“层叠”的规则是:越具体、越靠近元素的样式,优先级越高。例如,内联样式(直接写在标签上的style)的优先级高于在<style>标签或外部样式表中定义的样式。


常用CSS属性示例

了解了层叠原理后,让我们通过一些具体的属性来实践如何改变元素的外观。

以下是一些常用的CSS属性及其效果:

  • color:设置文本颜色,如 color: blue;
  • font-family:设置字体。可以指定一个字体栈,浏览器会依次尝试。例如 font-family: Arial, sans-serif; 表示优先使用Arial字体,如果不可用则使用任何无衬线字体。
  • border:为元素添加边框。这是一个简写属性,可以同时设置边框的样式、颜色和宽度。例如 border: 5px solid red; 会创建一个5像素宽的红色实线边框。在调试布局时,临时给元素加边框是查看其占据空间的常用技巧。
  • marginpadding:控制元素的外边距和内边距。margin-top: 1em; 会在元素上方增加一个字符高度的空间。em是一个相对单位,表示当前字体尺寸的倍数,在响应式设计中非常有用。


使用选择器应用样式

上一节我们直接在元素上写样式,但给每个标签都添加内联样式是不现实的。本节中我们来看看如何使用选择器来批量应用样式规则。

我们可以在<style>标签或外部CSS文件中编写规则。选择器用于“选中”我们想要样式化的HTML元素。

以下是几种基本选择器的用法:

  • 元素选择器:直接使用HTML标签名。例如,p { ... } 会选中页面中所有的<p>段落标签。
  • ID选择器:使用#号加上元素的id属性值。ID在页面中应该是唯一的。例如,#first { ... } 会选中 id="first" 的那个元素。
  • 类选择器:使用.号加上元素的class属性值。类可以被多个元素共享。例如,.shout { ... } 会选中所有 class="shout" 的元素。

一个元素可以拥有多个类,用空格分隔,如 class="shout more-space",这样它会同时应用.shout.more-space两个类的样式。


组织代码:外部样式表

当样式规则越来越多,或者网站有多个页面时,将CSS代码写在每个HTML文件里会变得难以维护。本节中我们来看看如何将样式提取到外部文件中。

我们创建一个独立的文件,例如 style.css,将所有的CSS规则放入其中。然后在HTML文件的<head>部分,通过一行代码引入它:

<link rel="stylesheet" href="style.css">

这样做的好处是:

  1. 代码复用:多个HTML页面可以共享同一个CSS文件,保持网站风格一致。
  2. 结构清晰:实现了内容(HTML)与表现(CSS)的分离,便于管理和维护。
  3. 提高性能:浏览器可以缓存CSS文件,加速后续页面的加载。

结构标签:<div><span>

为了更灵活地应用样式,我们经常需要给HTML内容添加“钩子”。本节中我们来看看两个没有默认样式的通用容器标签:<div><span>

它们的主要区别和用途如下:

  • <div> 标签:是一个块级元素,用于组合其他元素,本身不带有任何默认样式(如段落那样的外边距)。它常用于布局和作为CSS样式化的容器。
  • <span> 标签:是一个行内元素,用于对文本的一部分进行组合或样式化,同样没有默认样式。

通过为<div><span>添加classid属性,我们可以精确地对文档中的特定部分应用复杂的CSS样式。


组合与层级选择器

仅仅使用基本的类选择器有时还不够精确。本节中我们来看看如何组合选择器,以选中更特定的元素。

我们可以通过空格来组合选择器,表示层级关系。例如:

  • #third p { background-color: yellow; }
    这条规则的意思是:选中idthird的元素内部的所有<p>段落标签,并将它们的背景色设为黄色。它不会影响#third外部的段落,也不会影响#third内部的其他非<p>元素。这种层级选择器让我们能进行非常精细的样式控制。


实战:构建一个简单的导航栏

最后,让我们运用所学知识来构建一个简单的导航栏。良好的实践是先用语义化的HTML描述结构,再用CSS添加样式。

首先,我们使用HTML5的<nav>标签来定义导航区域,并使用列表<ul><li>来组织链接。这为屏幕阅读器等辅助技术提供了清晰的语义。

<nav>
    <ul>
        <li><a class="back" href="#">上一页</a></li>
        <li><a class="forward" href="#">下一页</a></li>
    </ul>
</nav>

此时,它只是一个简单的垂直链接列表。在下一个教程中,我们将通过CSS将其美化成一个水平的导航栏,并添加颜色、悬停效果等。


本节课中我们一起学习了CSS的基础。我们理解了CSS的层叠原理,学会了使用元素、类和ID选择器来应用样式,掌握了如何通过外部样式表组织代码,并认识了<div><span>这两个重要的结构标签。最后,我们还开始了导航栏的HTML结构搭建。在接下来的课程中,我们将深入CSS布局和更复杂的选择器。

019:CSS代码详解第二部分

在本节课中,我们将继续深入学习CSS,探索字体、颜色、链接样式、图片布局、盒模型、定位以及层叠规则等核心概念。我们将通过具体的代码示例来理解这些样式如何影响网页的呈现。

字体设置

上一节我们介绍了CSS的基础选择器,本节中我们来看看如何控制网页的字体。字体是网页设计中的重要组成部分,CSS提供了多种方式来指定字体。

以下是设置字体的几种方法:

  • 使用通用字体族:浏览器内置了一些通用字体族,如 serif(衬线体)、sans-serif(无衬线体)、monospace(等宽字体)。这些字体在所有浏览器中都有,常被用作备用字体。
    font-family: sans-serif;
    
  • 使用系统字体:你可以指定用户系统上可能存在的字体,如 ArialHelvetica。为了兼容性,通常需要提供一个字体列表,浏览器会按顺序尝试加载。
    font-family: Arial, Helvetica, sans-serif;
    
  • 使用网络字体:现代网页设计常通过链接引入外部字体文件。例如,通过 <link> 标签从Google Fonts等服务加载字体。浏览器会下载并缓存这些字体以供使用。
    <link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">
    
    font-family: 'Lato', sans-serif; /* 指定Lato字体,并以sans-serif作为备用 */
    

颜色定义

颜色是美化网页的关键。CSS提供了多种定义颜色的方式。

以下是几种常见的颜色定义方法:

  • 颜色名称:CSS支持一些预定义的颜色名称,如 redgreenblue。这些名称在所有浏览器中保持一致。
    color: red;
    
  • 十六进制颜色码:这是最常用的方式,使用 # 开头,后跟六位十六进制数字。每两位分别代表红、绿、蓝(RGB)的强度。
    color: #8b4513; /* 一种棕褐色 */
    
  • 颜色选择器:在HTML5的表单中,<input type="color"> 元素会提供一个可视化的颜色选择器,方便用户选取颜色。选取的值会以十六进制格式返回。

链接样式

链接(<a> 标签)的默认样式是蓝色(未访问)和紫色(已访问)。CSS允许我们通过伪类选择器来定制链接在不同状态下的外观。

以下是链接的几种状态及其样式设置:

  • a:link:设置未访问链接的样式。
    a:link { color: red; }
    
  • a:visited:设置已访问链接的样式。
    a:visited { color: orange; }
    
  • a:hover:设置鼠标悬停在链接上时的样式。
    a:hover { color: white; background-color: navy; text-decoration: none; }
    
  • a:active:设置链接被点击瞬间(但页面尚未跳转)的样式。这个状态通常非常短暂。
    a:active { color: yellow; }
    

图片布局与浮动

CSS的 float 属性可以让文本环绕图片,这是实现图文混排的经典方法。

以下是关于图片浮动和清除浮动的要点:

  • float: left;float: right;:使图片向左或向右浮动,后续的文本内容会环绕在图片周围。
    img { float: left; margin-right: 15px; }
    
  • clear 属性:当一个元素被设置为 clear: both; 时,它会移动到所有浮动元素的下方,确保自己不再被浮动元素影响。这常用于在浮动内容之后开始新的段落或区块。
    <br style="clear:both">
    <!-- 或 -->
    <div style="clear:both"></div>
    
  • 图片作为链接:可以将 <img> 标签包裹在 <a> 标签内,使图片本身成为一个可点击的链接。

层叠与优先级

CSS中的“C”代表“层叠”(Cascading)。样式规则的优先级决定了当多个规则冲突时,哪一个会生效。

以下是CSS优先级的基本规则:

  • 就近原则:通常情况下,距离HTML元素更近的样式规则(如行内样式)会覆盖较远的规则(如外部样式表)。
  • !important 规则:在样式声明后添加 !important 会赋予该规则最高优先级,可以覆盖其他规则。
    color: red !important;
    
  • !important 的层叠:如果多个 !important 规则冲突,则同样遵循就近原则,距离元素更近的 !important 规则胜出。

注意:虽然 !important 功能强大,但过度使用会导致样式难以维护和管理,通常被视为一种“最后手段”。

盒模型

CSS将每个元素视为一个矩形的盒子,这个盒子由内到外包括内容、内边距、边框和外边距。

以下是盒模型的组成部分:

  • 内容:元素的实际内容,如文本或图片。
  • 内边距:内容与边框之间的透明区域。内边距会继承元素的背景色。
    padding: 10px;
    
  • 边框:围绕在内边距外部的边界线。可以设置其样式、宽度和颜色。
    border: 5px solid black;
    
  • 外边距:边框与其他元素之间的透明区域。外边距是透明的,显示的是父元素的背景。
    margin: 20px;
    

整个元素在页面中占据的总宽度是:内容宽度 + 左右内边距 + 左右边框宽度 + 左右外边距。高度计算方式类似。

尺寸控制与溢出处理

我们可以控制元素盒子的尺寸。当内容超出设定的尺寸时,就需要处理溢出问题。

以下是控制尺寸和处理溢出的方法:

  • 设置尺寸:使用 widthheight 属性。建议使用相对单位(如 em,基于字体大小),这样在用户缩放页面时布局会更灵活。
    width: 20em; /* 大约20个字符的宽度 */
    height: 3em; /* 大约3行文字的高度 */
    
  • overflow 属性:当内容溢出盒子时,此属性决定如何处理。
    • overflow: visible;:默认值,内容会溢出并显示在盒子外部。
    • overflow: hidden;:溢出的内容被直接裁剪掉,不可见。
    • overflow: scroll;:无论内容是否溢出,都会显示滚动条。
    • overflow: auto;:仅在内容溢出时显示滚动条。

定位

CSS的 position 属性允许你精确控制元素在页面上的位置。

以下是几种常见的定位方式:

  • position: static;:默认值。元素遵循正常的文档流排列。
  • position: relative;:元素先放置在正常文档流中的位置,然后可以通过 toprightbottomleft 属性相对于其原始位置进行偏移。它原本在文档流中占据的空间会被保留。
    position: relative; top: 20px; left: -10px;
    
  • position: absolute;:元素脱离正常文档流,相对于其最近的非 static 定位的祖先元素进行定位。如果找不到这样的祖先,则相对于整个文档(<body>)。它不再占据原来的空间。
    position: absolute; top: 40px; right: 30%;
    
  • position: fixed;:元素脱离正常文档流,相对于浏览器窗口进行定位。即使页面滚动,它也会固定在窗口的同一位置。常见的“返回顶部”按钮就用此实现。
    position: fixed; bottom: 20px; right: 20px;
    

Z-index 层叠顺序

当使用定位(特别是 absolutefixedrelative)导致元素重叠时,z-index 属性控制它们的堆叠顺序。

以下是关于 z-index 的要点:

  • 数值越大,越靠前:拥有较高 z-index 值的元素会覆盖较低值的元素。
    z-index: 100; /* 位于上层 */
    z-index: -1; /* 可能位于背景之后 */
    
  • 默认值:元素的默认 z-indexauto,可视为 0。
  • 注意事项z-index 只对定位元素(position 值不是 static)生效。在复杂的页面中,管理多个 z-index 值可能会变得棘手。

导航栏实例分析

最后,让我们简要分析一个简单导航栏的实现思路,它综合运用了以上多个概念。

以下是实现一个导航栏的关键CSS步骤:

  1. 设置容器:通常使用 <nav><div> 作为导航栏容器,设置其背景色和高度。
    nav { background-color: lightgray; height: 3em; }
    
  2. 样式化列表:导航项通常用列表 <ul><li> 实现。需要移除列表默认的圆点符号和内外边距。
    nav ul { list-style-type: none; padding: 0; margin: 0; }
    
  3. 水平排列:将 <li> 设置为 display: inline-block; 或使用 float,使它们水平排列。
    nav li { display: inline-block; }
    
  4. 样式化链接:设置 <a> 标签的样式,如颜色、内边距,并移除下划线。
    nav a { color: darkblue; text-decoration: none; padding: 0.5em 1em; display: block; }
    
  5. 定位特定项:可以使用 position: absolute; 将某些导航项(如“返回”按钮)定位到容器的特定角落。
    .back { position: absolute; left: 20px; }
    

本节课中我们一起学习了CSS的多个核心概念,包括字体与颜色的定义、链接状态的样式控制、图片的浮动布局、决定样式生效的层叠规则、构成页面布局基础的盒模型、对元素尺寸和溢出内容的控制、用于精确布局的定位技术、管理元素重叠顺序的Z-index,最后还分析了一个导航栏的简单实现。掌握这些基础知识是进行有效网页样式设计的关键。CSS功能强大且细节丰富,需要不断练习才能熟练运用。

Django入门与全栈开发:P20:Mozilla基金会的诞生

在本节中,我们将跟随米切尔·贝克的讲述,了解Mozilla项目如何从一个企业内部的志愿者项目,演变为独立的Mozilla基金会。这段历史展示了开源项目在面临企业战略变动时的生存与发展之道。


我被解雇了,具体描述取决于你的用词。事实证明,他们很难找到人替代我的位置。我以志愿者的身份继续参与项目。网景公司的工程团队非常明确他们更愿意跟随谁的领导。

因此,我们必须找到一种方式,让留下来的网景和美国在线管理层与我能够协同工作。我们达成了一种合作模式,使他们能够发布他们想要的产品,同时我继续领导这个项目。我认为这令人意外,但对我而言并非完全如此。

于是,我作为志愿者在Mozilla项目上工作了数年。这期间包括我们发布第一个产品的年份,我们将其命名为Mozilla。我们为其技术成就感到非常自豪,在当时,许多人惊讶于它的优秀。但它并未提供良好的用户体验。

后来,我一度与米奇·卡普尔合作参与另一个开源项目。很幸运,他启动了这个开源项目,并联系我和布伦丹进行交流,希望学习一些经验。他预约前来与我们交谈的那天,正好是我被解雇的日子。因此,时机非常巧合。

是的,那是Chandler项目,是在Chandler项目之前,但在那之后。我为此工作了一段时间。米奇一直是Mozilla的支持者,甚至在Firefox之前,尽管他并不喜欢当时的产品。但他认识到我们是生态系统中的重要组成部分。这种情况一直持续到2003年。

当时,美国在线决定几乎完全停止对客户端产品的投资。幸运的是,他们明白直接终止项目并不妥当。他们对Mozilla的名称和品牌有所了解,认为应该为此做些什么。

最终,我与他们合作。他们中的一些人也认识米奇,而我当时正与米奇合作,他们也认识他。因此,我们花费了大量时间试图找出可行的方案。我的伙伴布伦丹当时仍在网景公司,他非常渴望做出改变。

许多我们想要留住的关键人员也极度希望继续从事Mozilla的工作。最终,我们从美国在线获得了200万美元的种子资金。米奇在此过程中提供了帮助,并协助处理了一些其他事务。

我们获得了Mozilla商标名称和四台大型服务器。这些服务器在当时对我们至关重要,我们花了近18个月才走完美国在线的采购流程获得它们。我想这些机箱至今仍被保留着,因为它们在当时意义重大。

于是,在2003年,Mozilla基金会成立了。


在本节中,我们一起回顾了Mozilla基金会成立的关键历程。从企业内部的志愿者领导,到获得种子资金和关键资产,最终独立成为基金会,这个过程体现了社区力量、关键人物的坚持以及战略时机的重要性。这为理解大型开源项目的演进提供了生动的历史背景。

021:印度孟买IIT科技节

概述

在本节课中,我们将跟随课程讲师查克博士,一同回顾他在印度孟买IIT科技节期间与学生们进行的面对面交流活动。本节内容并非技术教学,而是记录了课程社区的一次真实互动,展现了全球学习者交流的热情。


大家好。

我们正在印度理工学院的科技节现场,这里氛围非常棒。我在这里进行了一场讲座,之后大约有45分钟到一小时的时间,我们就在讲堂外继续交谈和提问。我一直有点担心工作人员会请我们离开,因为我们在下一场讲座期间制造了不小的声响。但我只是想和大家打个招呼,并给每个人一个机会,向课程的其他同学挥手问好。

好的,我们开始吧。请大家说“嗨”。你可以说“嗨”。
嗨,你叫什么名字?好的,我们稍后再回来和你聊。
我是……哦,很高兴认识你。

嗨,我是兰,我叫查兹。也被称为……TensorFlow 达人。
嗨,我是乒。
嗨,我是丹尼什。
我是苏,我是梅耶。
……这位是。
你好,我是……系。拍一下。
H在夏天。加油,蓝色军团。加油,蓝色军团。
嗨,我是……
嗨,你们后面的朋友?

别跑开。
嗨,我是德文。
嗨,我很好。
怎么样,我是杰。
好的,让我看看……我是普林斯。
嗨,我是……
我……

好的,我是尼拉。
好的,我们都拍到了吗?
好的,这位是查克,但我们现在在印度。

所以我们必须……这有点算是美国特色。因为在印度,有牛。

所以,我们在印度理工学院孟买分校度过了一段愉快时光,并且有牛的照片。那么,我们下次再见。

干杯。


总结

本节课中,我们一起观看了查克博士在印度孟买IIT科技节上与来自世界各地的学生互动交流的片段。虽然这不是一次技术讲解,但它生动地体现了本课程“面向所有人”的社区精神,以及学习者在现实世界中分享知识与热情的场景。我们看到了简单的问候、自我介绍以及轻松愉快的氛围,这正是开放课程连接全球学习者的魅力所在。

022: 数据库工作原理 🗄️

在本节课中,我们将要学习数据库的基本工作原理,特别是SQL(结构化查询语言)的历史背景、核心概念及其重要性。我们将从数据库技术出现之前的时代讲起,逐步理解为什么SQL会成为现代数据库交互的标准。

从磁带时代到磁盘时代 💾

上一节我们介绍了SQL的重要性,本节中我们来看看数据库技术是如何从物理存储介质发展而来的。

在数据库出现之前的“旧时代”,计算机没有足够的内存或磁盘来存储所有数据。早期甚至没有磁盘,使用的是磁带。磁带是一种物理介质,数据按顺序存储。要访问磁带末端的数据,必须快速或倒带,这个过程非常耗时,并非“随机访问”。随机访问(如内存或磁盘)允许你在大致固定的时间内到达任何位置。

这种物理限制导致了一种特定的数据处理模式。例如,在银行场景中,白天的所有交易会被记录在穿孔卡片上。晚上,操作员会将这些交易卡片按账户排序,并与存储昨日余额的磁带进行合并处理。这个过程可能持续数小时,效率低下。

幸运的是,磁盘驱动器的发明改变了这一切。磁盘也是磁性介质,但数据存储在高速旋转的盘片的同心圆磁道上,并由一个磁头快速移动来读写。这使得访问数据的速度(例如,等待盘片旋转到特定位置)从几分钟缩短到了百分之一秒级别,实现了真正的随机访问。

数据库标准化与SQL的诞生 📜

上一节我们了解了存储介质的进步,本节中我们来看看这如何催生了数据库标准化和SQL语言。

拥有了磁盘这项强大的新技术后,问题变成了:如何最好地利用它来构建数据库?在20世纪60-70年代,各大计算机厂商(如IBM、DEC)开发了各自专有的数据库技术(如索引顺序存取法、网状数据库)。客户一旦选择了某个厂商,就会被其技术“锁定”,难以切换,且价格受制于人。

美国联邦政府(通过国家标准与技术研究院NIST)采购了大量计算机,意识到了供应商锁定的风险。因此,NIST要求数据库厂商必须制定一个统一的标准交互语言,否则将停止采购。这迫使厂商们坐下来共同制定标准。

恰逢其时,一种基于数学关系模型的“关系型数据库”理论出现了,它被认为是存储和检索数据的更好方式。SQL(最初可能代表“简单查询语言”)作为这个标准被推出。它的核心思想是:程序员无需关心数据在磁盘上的具体物理存储结构(如柱面、磁道),而是通过一种简单的语言来表达想要进行的“增、删、改、查”操作。

以下是SQL核心操作的概念:

  • CREATE: 创建数据库结构(如表)。
  • READ: 查询数据。
  • UPDATE: 更新现有数据。
  • DELETE: 删除数据。

SQL成为了一个美丽的抽象层。它允许不同的数据库系统(包括旧式的和新式的关系型数据库)使用同一种语言进行交互。当关系型数据库的性能最终赶上并超越旧式数据库时,应用程序无需重写,只需切换底层数据库即可。这推动了长达十多年的惊人创新。

关系型数据库与常见系统 🏗️

上一节我们看到了SQL作为抽象层的力量,本节中我们来看看它主要服务的“关系型数据库”以及一些常见系统。

关系型数据库最初是一个数学构想,旨在用行和列的表格(关系)以及它们之间的连接来最优地表示数据网络。尽管其数学基础优美,但早期的实现效率不高。随着时间推移,其理论优势才通过优化转化为实际的性能优势。

现在有许多常见的数据库系统:

  • PostgreSQLMySQL: 功能强大的开源数据库。
  • OracleSQL Server: 复杂的商业数据库。
  • SQLite: 我们将在本课程中使用的嵌入式数据库。它非常轻量、快速,将整个数据库存储在单个文件中,适用于移动应用或嵌入式系统,但不适合高并发的生产级网站。

关键点: 尽管这些系统在内部实现和规模上不同,但它们都使用SQL语言。因此,在本课程中学到的关于SQLite的知识,同样适用于其他数据库系统。

数据库模式:与数据库的契约 📝

上一节我们介绍了各种数据库系统,本节中我们来看看使用数据库的第一步:定义模式。

要与数据库协作,首先需要定义一个“模式”。模式是你与数据库软件之间的一份契约,它规定了数据的“形状”:

  • 有哪些列(字段)?
  • 每列存储什么类型的数据(如整数、文本、浮点数)?
  • 文本列的最大长度是多少?

定义模式后,数据库软件(如Postgres、MySQL)就能根据这份契约来优化其在磁盘上的数据存储结构。这些数据库软件包含了价值数十亿美元的研究成果,用于最优地存储和检索数据。你只需要声明“我将有这些列”,剩下的优化工作交给数据库完成。

一旦模式确立,你就可以基于它来执行所有SQL操作:插入数据、查询数据、更新数据和删除数据。

总结 🎯

本节课中我们一起学习了数据库技术的发展历程。我们从磁带顺序访问的局限性,讲到磁盘随机访问带来的革命。我们看到了厂商锁定问题如何促使SQL成为数据库交互的通用标准语言。SQL作为一个抽象层,隐藏了复杂的物理存储细节,允许开发者通过声明式的语言操作数据,并促进了关系型数据库等技术的演进。最后,我们了解了定义数据库模式的重要性,它是应用与数据库之间高效协作的基础。接下来,我们将开始动手编写SQL语句来创建和操作数据库。

023:结构化查询语言SQL简介 🗃️

在本节课中,我们将学习结构化查询语言(SQL)的基础知识。SQL是与数据库交互的核心语言,我们将通过实际操作来了解其基本语法和核心概念。

概述

我们将从如何连接到SQLite数据库开始,逐步学习创建表、插入数据、查询、更新和删除数据等基本操作。这些是使用任何数据库都必须掌握的核心技能。

连接到数据库并创建表

首先,我们需要进入一个命令行环境来操作SQLite。如果你使用的是类似PythonAnywhere的平台,可以打开命令行工具。SQLite是一个轻量级数据库,它将所有数据存储在一个文件中。

使用以下命令启动SQLite并指定数据库文件:

sqlite3 zip.sqlite3

执行后,你会看到 sqlite> 提示符,表示已进入SQLite命令行界面。

以下是几个有用的SQLite元命令:

  • .tables:列出当前数据库中的所有表。
  • .schema:查看指定表或所有表的创建语句(即结构定义)。

现在,让我们创建我们的第一个表。SQL语句的语法与其他编程语言有所不同。

CREATE TABLE Users (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(128),
    email VARCHAR(128)
);

这条 CREATE TABLE 语句定义了一个名为 Users 的表,包含三列:

  • id:整数类型,不能为空,是自动递增的主键。主键是表中每一行的唯一标识符。
  • name:可变长度字符串,最多128个字符。
  • email:可变长度字符串,最多128个字符。

核心概念:定义表结构就是与数据库建立一个“契约”。例如,我们将 email 列定义为 VARCHAR(128),这意味着你无法插入超过128个字符的数据。数据库会严格执行这个契约以优化存储和保证数据完整性。违反契约(如插入129个字符)会导致操作失败。

执行创建语句后,使用 .tables 命令可以看到 Users 表已存在。使用 .schema Users 可以再次查看它的结构定义。

操作数据:增删改查 (CRUD)

上一节我们创建了表结构,本节中我们来看看如何向表中添加和操作数据。SQL的核心操作可以概括为增(Create)、查(Read)、改(Update)、删(Delete),即CRUD。

插入数据 (INSERT)

使用 INSERT INTO 语句向表中添加新行。

INSERT INTO Users (name, email) VALUES ('Chuck', 'csev@umich.edu');
INSERT INTO Users (name, email) VALUES ('Ted', 'ted@umich.edu');

这条语句指定了要插入数据的表名 (Users)、列名 (name, email),以及对应的值。值的类型必须与列定义的类型匹配,否则操作会失败。

删除数据 (DELETE)

使用 DELETE FROM 语句从表中删除行。非常重要:务必使用 WHERE 子句来指定删除哪些行,否则将删除表中的所有数据。

DELETE FROM Users WHERE email = 'ted@umich.edu';

核心概念:你可以将 DELETE FROM Users 理解为“删除Users表中的所有行”。WHERE 子句的作用就像一个过滤器,在上述例子中,它将被删除的行限制为 email 等于 'ted@umich.edu' 的那些行。这相当于一个隐含的循环:遍历所有行,仅删除满足条件的行。

更新数据 (UPDATE)

使用 UPDATE 语句修改表中已有的数据。同样,必须使用 WHERE 子句来精确指定要更新哪些行,否则会更新所有行。

UPDATE Users SET name = 'Charles' WHERE email = 'csev@umich.edu';

这条语句将 email'csev@umich.edu' 的用户的 name 更新为 'Charles'SET 关键字用于指定要修改的列及其新值。

查询数据 (SELECT)

使用 SELECT 语句从表中读取数据,这是最常用的操作。

-- 查询所有列的所有数据
SELECT * FROM Users;

-- 查询特定列的数据
SELECT name, email FROM Users;

-- 带条件的查询
SELECT * FROM Users WHERE email = 'csev@umich.edu';
  • SELECT * 表示选择所有列。
  • FROM Users 指定从哪个表查询。
  • WHERE 子句用于过滤结果,只返回满足条件的行。你可以将其读作“选择Users表中所有满足...条件的行”。

数据库擅长搜索和排序。我们可以使用 ORDER BY 子句对结果进行排序。

-- 按name列升序排序
SELECT * FROM Users ORDER BY name;

-- 按email列降序排序
SELECT * FROM Users ORDER BY email DESC;

通过在关键列上创建索引,可以极大地提高搜索和排序的效率。

SQL的通用性与工具

以上介绍的 INSERTSELECTUPDATEDELETE 以及 WHEREORDER BY 等子句,构成了SQL最核心和通用的部分。现代主流关系型数据库(如MySQL、PostgreSQL、Oracle)都支持这些标准语法,这使得SQL知识具有很强的可移植性。

当然,不同数据库也有各自的扩展和特定语法(例如限制返回行数的子句可能不同),但核心思想是一致的。

为了更直观地操作SQLite数据库文件(后缀通常为 .sqlite3.db),除了命令行,你还可以使用图形化工具 DB Browser for SQLite。你可以下载并安装它,用它来打开数据库文件、浏览表格内容,甚至直接执行SQL命令,这对初学者非常友好。

总结

本节课我们一起学习了SQL的基础知识。我们掌握了如何连接SQLite数据库,使用 CREATE TABLE 定义表结构,并通过 INSERTSELECTUPDATEDELETE 语句进行数据的增删改查。我们还强调了 WHERE 子句在更新和删除操作中的关键作用,以及使用 ORDER BY 进行排序。

这些单表操作是SQL的基石,约占日常使用的60%。接下来,我们将探讨多表SQL和表之间的关系(连接查询),并学习如何通过Django的ORM(对象关系映射器)来连接数据库,从而在Web开发中更高效地使用数据。

024:基础SQL详解 🗃️

在本节课中,我们将学习SQL的基础知识,包括如何创建表、插入数据、查询、更新和删除数据。我们将使用SQLite数据库进行实际操作,帮助你理解SQL的核心概念和基本操作。

概述

SQL(结构化查询语言)是用于管理和操作关系型数据库的标准语言。本节将介绍SQL的基本操作,包括创建表、插入数据、查询数据、更新数据和删除数据。我们将通过实际操作演示这些命令的使用方法。

环境准备

首先,我们需要在Linux环境中使用SQLite数据库。SQLite是一个轻量级的数据库,它将所有数据存储在一个文件中,非常适合学习和开发。

sqlite3 test.sqlite3

运行上述命令后,你将进入SQLite的命令行界面。可以输入.help查看可用的命令。

创建表

在SQL中,我们使用CREATE TABLE语句来创建新表。表由列组成,每列都有特定的数据类型。

以下是创建users表的SQL语句:

CREATE TABLE users (
    name TEXT,
    email TEXT
);

执行上述命令后,可以使用.tables命令查看当前数据库中的所有表。使用.schema users命令可以查看users表的结构。

插入数据

创建表后,我们需要向表中插入数据。使用INSERT INTO语句可以向表中添加新记录。

以下是向users表插入数据的示例:

INSERT INTO users (name, email) VALUES ('Kristen', 'kf@u.edu');

插入数据后,可以使用SELECT * FROM users;查询表中的所有记录。

查询数据

查询数据是SQL中最常用的操作之一。使用SELECT语句可以从表中检索数据。

以下是查询users表中所有记录的示例:

SELECT * FROM users;

如果需要按特定条件查询数据,可以使用WHERE子句。例如,查询邮箱为csev@umich.edu的记录:

SELECT * FROM users WHERE email = 'csev@umich.edu';

更新数据

如果需要修改表中的数据,可以使用UPDATE语句。注意,UPDATE语句通常需要配合WHERE子句使用,以避免更新所有记录。

以下是更新users表中特定记录的示例:

UPDATE users SET name = 'Charles' WHERE email = 'csev@umich.edu';

如果不使用WHERE子句,UPDATE语句将更新表中的所有记录。

删除数据

删除数据使用DELETE语句。与UPDATE类似,DELETE语句通常需要配合WHERE子句使用,以避免删除所有记录。

以下是删除users表中特定记录的示例:

DELETE FROM users WHERE email = 'ted@umich.edu';

如果不使用WHERE子句,DELETE语句将删除表中的所有记录。

排序数据

使用ORDER BY子句可以对查询结果进行排序。默认按升序排序,如果需要降序排序,可以添加DESC关键字。

以下是按邮箱升序排序的示例:

SELECT * FROM users ORDER BY email;

以下是按姓名降序排序的示例:

SELECT * FROM users ORDER BY name DESC;

删除表

如果需要删除整个表,可以使用DROP TABLE语句。

以下是删除users表的示例:

DROP TABLE users;

执行该命令后,users表及其所有数据将被永久删除。

总结

本节课我们一起学习了SQL的基础操作,包括创建表、插入数据、查询数据、更新数据和删除数据。通过实际操作,我们掌握了SQL的核心命令及其使用方法。SQL是数据库操作的基础,熟练掌握这些操作对于后续学习Django和Web开发至关重要。希望本节课的内容对你有所帮助!

025:SQL标准的诞生与影响

在本节课中,我们将跟随利兹·方的讲述,了解SQL标准是如何在数据库技术发展的关键时期被创建出来的。我们将探讨其背后的驱动力、核心概念以及它对整个软件行业产生的深远影响。

概述

数据库技术的发展并非一蹴而就。在早期,市场上存在多种不同的数据库产品和技术路径,这给用户的选择和应用开发带来了困扰。本节内容将回顾那个百家争鸣的时代,解释行业为何以及如何走向标准化,并最终催生了SQL这一通用的数据库查询语言。

市场需求的驱动

最初,市场上涌现出许多数据库产品。随着技术发展,用户面临一个现实问题:如何在不同的产品之间做出选择?例如,是购买IBM、Oracle的产品,还是选择更便宜的方案?这种选择困境开始普遍出现。

应用多样性与标准化需求

数据库管理系统(DBMS)之上可以构建的应用种类极其繁多。为了确保应用程序能在不同的平台上运行,就需要建立某种统一的标准。早期的数据存储方式多样,包括层次结构的文件系统(如IBM的IMS)、网状结构或平面文件。业界一直在争论哪种数据模型更优,后来我们认识到数据需要自我描述标签(即元数据,现在常称为模式)。

数据库系统研究组的贡献

为了解决这些问题,数据库系统研究组提出了一套参考模型或规范,定义了数据库管理系统应具备的最小功能集。一个合格的DBMS必须能够存储、检索、修改、组织、删除和操作数据。这成为了一项技术规范。

标准化组织的成立

与此同时,一个名为X3H2的NC小组(现称Insight,隶属于美国国家标准协会)成立了。这个小组被称为数据管理语言组,旨在推动标准化工作。唐·多伊奇和兰·加拉格尔等人都参与其中。

标准化的核心:接口与语言

进行标准化时,人们意识到,需要统一的不是产品的全部能力,而是交互的接口。这就像灯泡可以有红色、白色等多种多样,但需要标准化的是灯泡与灯座之间的接口。对于软件系统而言,标准化的核心就是通信接口或共同语言

关系型数据库与SQL的兴起

当时,IBM的克里斯·戴特提出了关系型数据库理论,他谈论规范化,并引入了“表”这个易于理解的概念来描述平面文件。为了从表中检索数据,一种简单的查询语言应运而生,其基本形式如下:

SELECT column_name
FROM table_name;

例如,从员工表中查询信息:

SELECT name
FROM employee;

符合性测试的重要性

采纳标准后,符合性测试变得至关重要。用户需要确保所购买的产品符合特定版本的ISO标准(如JTC1)。否则,应用程序可能无法正常工作。无论底层是Oracle、Sybase还是Microsoft SQL Server,用户都希望自己的应用程序(例如学生课程记录系统)能够跨平台运行。这正是市场的需求。

认证实验室与产品清单

为此,设立了经过认证的实验室(如Nav Labs)。这些实验室对产品进行验证,并发布一份经过认证的、符合标准的产品清单。采购时,用户可以要求产品必须“符合SQL标准”,并依据这份清单进行购买。这是由支付费用的用户驱动的严格需求。

标准化时机的把握

时机决定一切。标准化进行得太早,可能会扼杀创新,因为新的想法无法进入已被标准固化的市场。标准化进行得太晚,则会出现过多互不兼容的技术变体,让用户面临艰难选择。SQL的成功,正是在恰当的时机把握住了平衡。

总结

本节课我们一起回顾了SQL标准诞生的历史背景。我们了解到,市场的多样化选择困境、应用程序对可移植性的需求,以及业界对统一交互语言的共识,共同推动了SQL标准的建立。标准化的核心在于定义通用的接口和语言,而非限制功能。同时,符合性测试确保了标准的实际效力,而对时机的精准把握则是SQL能够取得成功并持续促进创新的关键因素。

026:安大略省基奇纳 🏢

在本节课中,我们将跟随查克·塞弗伦斯博士,了解他在安大略省基奇纳进行的一次面对面办公时间。你将看到一些课程学员的分享,并了解关于未来Django课程的计划。

大家好,我们现在在安大略省的基奇纳。我在这里与一家名为Desire2Learn的学习管理系统供应商合作,帮助他们实施一个名为“学习工具互操作性”的协议。这个协议实际上是自动评分器将成绩发送回Coursera平台的方式。

接下来,我想让你们认识一些一起学习的同学。他们可以谈谈对课程的感受,并告诉我们他们的名字。

以下是几位学员的自我介绍:

  • “嗨,我是Tasie和Sala。我已经完成了‘Python for Everybody’的前四门课程,正在等待顶点课程。希望一切顺利,谢谢。”
  • “嘿,大家好,我是Louis。我刚刚完成了‘Python for Everybody’的前两门课程,非常兴奋能向查克博士和社区的其他成员学习Python,并期待继续通过Coursera的课程学习。”
  • “嗨,我的名字是Jugger。我正在学习‘Python for Everybody’课程,已经完成了前两门Python课程,并且非常兴奋地准备开始第三门以及顶点课程,感谢查克博士。”
  • “嘿,我是Hermann。我正在Coursera上学习‘Python for Everybody’,目前正在学习第三门课程。这真的很令人兴奋。”
  • “嗨,我是Dave。我刚刚完成了‘Python for Everybody’和‘Web Design for Everybody’课程,并且很期待当Django课程上线时去学习它。”

我不知道下次会在哪里见到你们。秋季学期我即将回到校园授课。我将教授一门Django课程,我希望将来能把它变成“Django for Everybody”课程。因此,接下来我会忙于为我的Django课程构建自动评分器之类的东西。我希望能在不久的将来,在接下来的某次办公时间中见到你们。

本节课中,我们一起了解了查克博士在基奇纳的线下办公情况,并聆听了多位“Python for Everybody”课程学员的学习经历与期待。我们还得知了未来可能推出“Django for Everybody”系列课程的计划。

027:课程欢迎与概述 🎬

在本节课中,我们将学习Django应用程序的基本结构,并理解其背后的设计模式。我们将探讨Django如何组织文件,以及它如何遵循模型-视图-控制器(MVC)模式。课程内容基于Django官方教程,旨在帮助初学者建立扎实的基础。


Django应用程序的结构 🏗️

本节中,我们将深入了解Django应用程序的文件结构。Django应用程序不仅仅是包含一堆文件夹和文件的集合,而是有明确的设计逻辑。我们将反复查看相同的结构图,包括模型(Models)、视图(Views)、URL配置(URLs.py)等文件,以帮助您理解为什么Django应用程序的文件布局是这样的。

通过多次重复讲解这些文件的作用、如何操作它们以及模板(Templates)的工作原理,您将逐渐熟悉Django应用程序的结构。这种重复是为了确保您能够深入理解每个部分的功能和它们之间的协作方式。


理解模型-视图-控制器(MVC)与Django 🧩

上一节我们介绍了Django应用程序的结构,本节中我们来看看Django与模型-视图-控制器(MVC)模式的关系。如果您询问Django开发者Django是否遵循MVC模式,可能会得到一个模糊的答案。但事实上,Django在很大程度上是一个MVC应用程序,大多数Web应用程序都是MVC模式的某种变体。

在Django中,我们将模型(Model)、视图(View)和控制器(Controller)分别对应为:

  • 模型(Model):负责数据处理和数据库交互。
  • 视图(View):负责用户界面和展示逻辑。
  • 控制器(Controller):在Django中通常由URL配置和视图函数共同实现,负责处理用户请求并协调模型和视图。

通过将Django置于通用的MVC应用程序背景中,我们可以更好地理解其设计哲学和工作原理。


课程作业与学习资源 📚

以下是本课程的作业安排和学习资源:

本课程的作业基于Django官方教程(Djangoproject.org),该教程涵盖了构成Django应用程序的基本核心主题。通过完成这些作业,您将逐步掌握Django的关键概念和实际应用技巧。


总结 🎯

本节课中,我们一起学习了Django应用程序的基本结构,并探讨了其与模型-视图-控制器(MVC)模式的关系。我们强调了理解文件布局的重要性,并介绍了基于Django官方教程的作业安排。通过本课程的学习,您将能够逐步构建对Django的深入理解,并为后续开发Web应用程序打下坚实基础。

028:理解模型-视图-控制器(MVC)🎯

在本节中,我们将学习一个在Web开发中广泛使用的架构概念:模型-视图-控制器(MVC)。理解MVC有助于我们清晰地组织代码,并理解Django框架各部分是如何协同工作的。

概述

MVC是一种将应用程序逻辑分为三个核心组件的设计模式:模型(Model)视图(View)控制器(Controller)。它不依赖于特定的编程语言或操作系统,是Web开发中一种通用的架构思想。虽然它不是理解Web服务器工作原理的绝对核心,但掌握这套术语对于开发者之间的沟通至关重要。

请求-响应循环回顾

在深入MVC之前,我们先回顾一下基础的请求-响应循环。用户通过浏览器点击链接,浏览器会拦截这个动作,向运行在80端口的Web服务器发起一个GET请求。Web服务器处理这个请求,并最终返回一个网页。这就是基本的请求-响应过程。

用户点击 -> 浏览器发送GET请求 -> Web服务器处理 -> 返回网页

我们本节课的重点,就是详细探讨Web服务器内部在处理请求时具体做了什么。

理解MVC的三个组件

模型-视图-控制器这三个术语,帮助我们清晰地划分Web服务器内部的工作。任何一部分工作都可以归类为模型、视图或控制器。它的价值在于,当讨论代码时,我们可以明确地说“这部分是控制器逻辑”。

尽管从逻辑顺序上,叫“控制器-视图-模型”或“模型-控制器-视图”可能更准确,但“模型-视图-控制器”(MVC)这个说法更顺口,因此被广泛使用。

以下是三个组件的定义:

  • 控制器(Controller):它决定了“接下来发生什么”。控制器是处理请求的起点,它定义了事件的执行序列,并在处理结束后决定下一步的去向。你可以把它看作是协调整个流程的“胶水”。
  • 视图(View):这是我们最终看到的东西。视图通常是请求-响应周期的终点,服务器生成视图(如一个HTML页面)并将其返回给浏览器。
  • 模型(Model):这代表了应用程序中的持久化数据存储,通常指数据库。请求-响应周期常常需要与模型交互,例如向数据库插入新数据、更新已有数据,或者从数据库中读取数据以便在视图中展示。

典型的Web请求处理步骤

当一个请求到达Web服务器时,通常会经历一系列典型的步骤,这些步骤清晰地体现了MVC的分工。

以下是处理一个包含数据的请求(如表单提交)的常见步骤:

  1. 接收与处理数据(模型/控制器):如果请求中包含需要处理的数据(如表单数据),控制器会接收这些数据,进行验证,然后通过模型将其存储到数据库中。
  2. 决定下一步(控制器):数据存储完成后,控制器需要决定接下来将用户引导至哪个页面。例如,跳转到一个“感谢”页面,或者返回应用首页。
  3. 获取展示数据(模型):确定了目标页面后,可能需要从数据库中检索一些数据来填充这个页面。
  4. 生成并返回响应(视图):最后,使用模板等技术,将数据和HTML结合,生成最终的视图(HTML响应),并将其发送回浏览器。
请求进入 -> 处理/存储数据 -> 决定路由 -> 检索数据 -> 渲染视图 -> 返回响应

MVC在Django中的映射

Django框架的设计深受MVC模式的影响。虽然命名上略有不同(Django有时被称为MTV框架:Model, Template, View),但其核心思想是相通的。

我们可以将Django的组件映射到MVC概念上:

  • URL配置 (urls.py) - 控制器urls.py 文件是控制器的核心部分。它根据接收到的URL请求,决定将请求路由到哪个处理函数(视图)。这正体现了控制器“决定接下来发生什么”的职责。
  • 视图函数 (views.py) - 控制器 & 视图views.py 中的视图函数承担了控制器和视图的双重角色。
    • 控制器角色:它可能包含处理输入数据、决定重定向到其他页面的逻辑(例如,使用 redirect() 函数)。
    • 视图角色:它负责组织数据,并调用模板来生成最终的HTML输出。
  • 模型 (models.py) - 模型models.py 完全对应MVC中的模型。它定义了数据结构,并提供了与数据库交互的所有方法,无论是存储数据还是检索数据,都是通过Django的模型层来完成的。
  • 模板 (templates/) - 视图:模板文件专门负责数据的呈现,是视图层中专注于展示的部分。它们与 views.py 协同工作,生成最终的用户界面。

总结

本节课我们一起学习了模型-视图-控制器(MVC)这一重要的Web架构模式。我们了解到MVC将应用逻辑分为模型(数据)、视图(展示)和控制器(流程控制)三部分。通过回顾典型的请求处理步骤,我们看到了这三个组件是如何协同工作的。最后,我们将MVC概念映射到了Django框架的具体组件上:urls.py 和部分 views.py 逻辑充当控制器models.py 对应模型,而 views.py 的数据处理与模板渲染则共同构成了视图。理解MVC有助于我们更好地设计和理解Django应用程序的结构。

029:在PythonAnywhere上修复Django应用错误

在本节课中,我们将学习如何在PythonAnywhere平台上诊断和修复Django应用中的两种常见错误:启动失败和运行时错误。我们将通过具体的步骤和示例,帮助你理解如何定位问题并快速解决。

概述

当你在PythonAnywhere上运行Django应用时,可能会遇到两种基本错误。第一种是启动失败,即应用无法启动,你会在访问应用时看到“出了点问题”的提示。第二种是运行时错误,即应用可以启动,但在处理请求时出现问题,你会看到一个带有错误追踪信息的黄色页面。本节将指导你如何系统地排查和修复这些问题。

启动失败:应用无法启动

上一节我们介绍了两种错误类型。本节中我们来看看第一种:启动失败。当你修改代码后点击“重新加载”按钮,然后访问应用时看到“出了点问题”的提示,这通常意味着应用启动失败。PythonAnywhere无法启动你的应用,因为它存在致命错误。

理解启动过程

当你在PythonAnywhere上点击“重新加载”按钮时,会发生一系列操作。系统会进入你的源代码目录,设置虚拟环境,并运行一个配置文件(类似于在本地运行 python manage.py runserver 命令)。这个过程会加载你的 settings.pyurls.py 以及 settings.py 中列出的所有应用。如果在加载这些文件的任何阶段出现错误,应用就会启动失败。

诊断步骤:使用 python manage.py check

要诊断启动失败,最有效的方法是使用Django的检查命令。以下是具体步骤:

  1. 在PythonAnywhere控制面板中,进入“Consoles”标签页。
  2. 创建一个新的“Bash Console”。
  3. 在Bash控制台中,首先确认你处于正确的虚拟环境中。你可以通过运行 python --version 来检查。
  4. 使用 pwd 命令确认当前目录,然后切换到你的Django项目目录。通常命令是:
    cd ~/django_projects/mysite
    
  5. 运行Django的检查命令:
    python manage.py check
    

这个命令会模拟应用启动过程,加载所有设置、URL配置和应用。如果存在导致启动失败的语法错误或导入错误,它会在这里显示出来。

示例:修复导入错误

假设 python manage.py check 命令输出了一个错误追踪信息,指出在 polls/views.py 的第1行有一个 ModuleNotFoundError: No module named 'jango' 错误。

  1. 根据错误信息,定位到有问题的文件:polls/views.py
  2. 打开该文件,发现第1行代码是:
    from jango.http import HttpResponse
    
    这里 jango 拼写错误,应该是 django
  3. 将其修正为:
    from django.http import HttpResponse
    
  4. 保存文件。
  5. 再次在Bash控制台中运行 python manage.py check。如果命令成功执行且没有输出错误,说明问题已解决。
  6. 最后,返回PythonAnywhere的“Web”标签页,点击“Reload”按钮重新加载你的Web应用。
  7. 刷新你的应用页面,它现在应该可以正常访问了。

关键提示:在修复后先运行 check 命令进行验证,是一个好习惯。这比直接重新加载应用更快,并且能确保没有遗留其他错误。

备用方案:查看错误日志

如果 python manage.py check 没有发现问题,但应用仍然无法启动,你可以查看PythonAnywhere的错误日志。

  1. 在“Web”应用配置页面,找到“Error log”的链接并点击。
  2. 日志文件会按时间顺序记录所有错误。你需要滚动到底部查看最近的错误。
  3. 日志中的错误信息通常与 python manage.py check 的输出类似,但可能更冗长。仔细阅读,定位根本原因。

运行时错误:黄色追踪页面

上一节我们解决了应用无法启动的问题。本节中我们来看看第二种情况:运行时错误。当应用成功启动,但在处理某个特定请求时发生错误,你就会看到一个黄色的调试页面,其中包含了详细的错误追踪信息。

理解黄色追踪页面

这个页面意味着你的应用已经通过了所有启动检查,settings.pyurls.py 等文件都已成功加载。错误发生在执行你的业务逻辑代码时(例如,在 views.py 中处理一个视图函数)。

黄色页面的顶部通常会有一个简短的错误摘要,下方则是详细的“Traceback”(追踪信息),它展示了错误发生时的完整函数调用栈。

诊断与修复步骤

以下是修复运行时错误的通用步骤:

  1. 阅读错误摘要:首先看黄色页面顶部的错误描述,它通常直接指出了问题所在,例如 NameError: name ‘HttpsResponse’ is not defined
  2. 查看追踪信息:向下滚动,在追踪信息中寻找属于你自己项目的文件(如 polls/views.py)。错误最后指向的那几行,通常就是问题的根源。
  3. 定位并编辑代码:根据错误信息指出的文件和行号(例如 polls/views.py, line 6),打开对应的文件进行修改。
  4. 利用编辑器提示:PythonAnywhere的在线编辑器会用黄色三角形标记出它检测到的代码问题(如未定义的变量、未使用的导入)。这是一个很好的辅助调试工具。
  5. 保存并重新加载:修复代码后保存文件。然后,返回“Web”标签页点击“Reload”按钮,或者直接刷新你的应用页面来测试是否修复成功。

示例:修复未定义变量错误

假设你在访问投票详情页时看到了黄色页面,错误信息显示 NameError at /polls/1/,并指出在 polls/views.py 的第6行,变量 HttpsResponse 未定义。

  1. 打开 polls/views.py 文件。
  2. 找到第6行,发现代码是:
    return HttpsResponse(“Hello, world.”)
    
    这里 HttpsResponse 拼写错误,应该是 HttpResponse(注意大小写)。
  3. 同时,编辑器可能在代码旁显示黄色三角形,提示“Undefined variable ‘HttpsResponse’”。
  4. 将其修正为:
    return HttpResponse(“Hello, world.”)
    
  5. 保存文件后,编辑器中的黄色警告会消失。
  6. 刷新你的应用页面,错误应该已经解决,页面可以正常显示。

总结

本节课中我们一起学习了在PythonAnywhere上调试Django应用的两种核心方法。

  • 对于启动失败(“出了点问题”),核心工具是使用Bash控制台运行 python manage.py check 命令。它能快速定位导致应用无法加载的语法或导入错误。
  • 对于运行时错误(黄色追踪页面),关键在于仔细阅读错误信息,在追踪栈中找到自己项目文件的错误行,并进行修正。编辑器的实时语法检查也能提供很大帮助。

记住调试的通用流程:重现错误 -> 阅读错误信息 -> 定位问题代码 -> 修复 -> 验证。通过不断练习,你会越来越熟练地处理这些开发中常见的问题。

030:Django 模型与数据库

概述

在本节课中,我们将学习Django框架中一个核心概念:模型。模型是Django用于与数据库交互的工具,它允许我们使用Python代码来定义数据结构,而无需直接编写复杂的SQL语句。我们将了解如何创建模型、进行数据库迁移以及使用Django的管理界面来操作数据。

上一节我们介绍了Django项目的基本结构和视图,本节中我们来看看如何定义和管理应用程序的数据。

什么是Django模型?

Django模型是一个Python类,它继承自django.db.models.Model。这个类中的每一个属性都代表数据库表中的一个字段。Django会根据这个模型类自动在数据库中创建对应的数据表。

核心概念公式
一个Django模型类 = 一张数据库表

示例代码

from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    published_date = models.DateField()

在上面的代码中,我们定义了一个Book模型,它对应数据库中的一张表。这张表将拥有titleauthorpublished_date三个字段。

创建模型并进行迁移

定义好模型后,我们需要告诉Django去创建对应的数据库表。这个过程分为两步:生成迁移文件和执行迁移。

以下是创建并应用模型迁移的步骤:

  1. 生成迁移文件:Django会检测你对模型所做的更改(例如新建或修改了一个模型类),并生成一个描述这些更改的Python脚本。

    python manage.py makemigrations
    
  2. 执行迁移:Django会读取迁移文件,并将其中的指令应用到数据库中,从而创建或修改表结构。

    python manage.py migrate
    

执行完这两条命令后,你的模型所对应的数据表就已在数据库中准备就绪了。

使用Django管理界面

Django提供了一个强大的内置管理界面(Admin Site),允许开发者无需编写额外代码就能对模型数据进行增、删、改、查操作。

要使用管理界面,你需要完成以下步骤:

  1. 创建超级用户:首先,你需要创建一个可以登录管理后台的账户。

    python manage.py createsuperuser
    

    按照提示输入用户名、邮箱和密码。

  2. 注册模型:在应用的admin.py文件中,注册你定义的模型,这样它才会在管理界面中显示。

    # 在 yourapp/admin.py 中
    from django.contrib import admin
    from .models import Book
    
    admin.site.register(Book)
    
  3. 访问管理界面:运行开发服务器后,在浏览器中访问 http://127.0.0.1:8000/admin/,使用你创建的超级用户账号登录。你就能看到Book模型,并可以对其进行管理操作。

总结

本节课中我们一起学习了Django模型的基础知识。我们了解到模型是连接Python代码与数据库的桥梁,通过定义模型类可以自动生成数据库表。我们学会了使用makemigrationsmigrate命令来同步数据库结构,并且探索了如何使用Django内置的管理界面来方便地管理数据。掌握模型是构建Django Web应用数据层的基石。

031:Django数据模型 🗄️

在本节课中,我们将要学习Django数据模型。数据模型是Django应用与数据库交互的核心,它使用一种称为“对象关系映射”的技术,让我们可以用Python对象来操作数据库,而无需编写复杂的SQL语句。

回顾SQL

在之前的课程中,我们介绍了SQL(结构化查询语言)。SQL是与数据库通信的标准方式,它允许我们在一定程度上跨不同数据库进行移植。

以下是一些基本的SQL命令示例:

CREATE TABLE Users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(128),
    email VARCHAR(128)
);

我们可以运行这些命令来创建表、查询数据等。然而,SQL也存在一些问题。不同数据库(如PostgreSQL、MySQL)在某些高级功能(如外键、分页查询)的实现上存在差异。学习SQL的基础部分相对容易,但要精通所有数据库的高级特性则非常困难。

对象关系映射器的出现

为了解决SQL的复杂性和数据库差异性问题,出现了对象关系映射器。ORM在物理数据库层和SQL抽象层之上,又构建了一层抽象。

在Django中,我们通过创建Python类来定义数据模型。这个类会映射到数据库中的一张表。ORM的优势在于:

  • 简化开发:约80%的常见数据库操作,使用ORM比直接写SQL更简单。
  • 数据库可移植性:ORM代码知道如何处理SQLite、PostgreSQL和MySQL等不同数据库的差异。你可以在开发时使用SQLite,部署时切换到MySQL,而无需修改核心代码。
  • 聚焦业务逻辑:开发者可以更专注于数据结构和业务逻辑,而非数据库语法。

定义Django模型

在Django中,我们在应用的 models.py 文件中定义模型。

我们通过继承 django.db.models.Model 类来创建模型。这个基类为我们提供了大量现成的功能。在类内部,我们定义属性,这些属性对应数据库表中的列。

以下是一个定义 User 模型的例子,它等同于之前用SQL创建的 Users 表:

from django.db import models

class User(models.Model):
    name = models.CharField(max_length=128)
    email = models.CharField(max_length=128)
  • User 类名对应数据库表名(Django通常会做一些命名转换,例如可能变成 appname_user)。
  • nameemail 是模型字段,分别对应表中的列。
  • models.CharField 表示这是一个字符字段,max_length 参数定义了最大长度。

迁移:同步模型与数据库

定义模型后,我们需要将其同步到数据库,这个过程称为“迁移”。

迁移分为两步:

  1. 生成迁移文件:在项目根目录下运行 python manage.py makemigrations。此命令会读取 models.py 中的变更,并生成一个包含具体SQL操作的Python脚本文件(如 0001_initial.py)。
  2. 应用迁移:运行 python manage.py migrate。此命令会执行迁移文件中定义的SQL操作,在数据库中实际创建或修改表结构。

迁移机制非常强大,它允许你在开发环境调试好数据库结构后,将同样的变更安全地应用到生产环境,即使生产环境使用的是不同的数据库。

在开发中,Django默认会创建一个 db.sqlite3 文件作为数据库。如果你把数据库弄乱了,可以删除这个文件,然后重新运行 migrate 命令来重建。

使用Django Shell操作数据

Django提供了一个增强的Python Shell,预加载了项目环境,方便我们测试模型操作。

要启动它,请在项目根目录运行:

python manage.py shell

在Shell中,我们可以进行数据的增删改查操作。以下是核心的CRUD操作示例:

创建数据

from myapp.models import User # 从你的应用导入模型

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/0d752d16c37a1be222fb33c5777caf09_28.png)

u = User(name='Kristen', email='k@example.com') # 在内存中创建对象
u.save() # 将对象保存到数据库,执行INSERT操作
print(u.id) # 保存后,数据库会自动分配一个id

查询数据
以下是查询数据的基本方法。

  • User.objects.all():获取所有用户对象(对应 SELECT *)。
  • User.objects.filter(email='k@example.com'):根据条件过滤(对应 WHERE 子句)。
  • User.objects.filter(email='k@example.com').values():执行查询并获取结果字典。

更新数据

User.objects.filter(email='k@example.com').update(name='NewName')

删除数据

User.objects.filter(email='k@example.com').delete()

模型字段类型

Django提供了丰富的字段类型,它们不仅定义了数据库中的列类型,还常常包含额外的验证逻辑。

例如,models.EmailField 是一个邮箱字段。数据库本身可能没有专门的“邮箱”类型,但Django的ORM层会确保输入的数据符合邮箱地址格式。其他常用字段还包括 IntegerFieldDateFieldTextField 等。

在后续课程中,我们还会学习用于表间关联的字段,如 ForeignKey(外键)、ManyToManyField(多对多关系)等。

模型在Django架构中的位置

理解模型在整体架构中的位置非常重要。

一个典型的Django请求流程如下:

  1. 用户通过浏览器发起请求。
  2. URL配置(urls.py)将请求路由到对应的视图函数(views.py)。
  3. 视图函数是处理业务逻辑的核心。它通过模型(models.py)与数据库进行交互
  4. 视图函数使用模板和表单来生成最终的HTML响应,返回给浏览器。

此外,Django强大的内置管理后台(admin site)也是通过读取 models.pyadmin.py 的配置,自动生成界面来管理模型数据的。

总结

本节课中我们一起学习了Django数据模型的核心概念。

我们了解到,Django模型是其对象关系映射器的实现。它允许我们使用Python类来定义数据库结构,并通过这些类的实例来操作数据,这比直接编写SQL更加直观和高效。模型带来了数据库可移植性,并提供了强大的迁移工具来管理数据库结构的演变。同时,模型还是Django自动生成管理后台和表单的基础。

虽然ORM能处理大部分场景,但了解底层的SQL知识仍然非常重要,因为在处理一些非常复杂的查询时,直接使用SQL可能更合适。然而,对于典型的Web应用开发,Django模型将是你最得力的工具之一。

032:Django迁移详解 🗄️

在本节课中,我们将详细学习Django中的数据模型、迁移和数据库之间的关系。初学者初次接触这些概念时容易感到困惑,因此我们将深入解析其工作原理和操作流程。

概述

我们将探讨makemigrationsmigrate命令的核心功能,理解迁移文件如何作为数据库结构的“蓝图”,以及Django如何利用这些文件在不同数据库间保持一致性。最后,我们会介绍如何重置迁移状态。

数据模型、迁移与数据库的关系

初次查看数据模型、迁移和数据库本身时,很容易感到困惑。因此,我们需要更详细地梳理这个过程。

makemigrations命令负责读取models.py文件,并创建所谓的“迁移”文件。关键在于,这个过程由settings.py中的INSTALLED_APPS列表引导。如果你创建了一个新应用并编辑了模型文件,但运行makemigrations后没有任何反应,那么你应该检查是否在settings.py中注册了这个新应用。

迁移的本质与跨数据库特性

迁移文件是跨数据库可移植的。这意味着迁移是对数据库表结构的逻辑描述,并且它们记录了数据库的演化过程

例如,首次运行迁移时,会根据最初的models.py生成0001_initial.py。如果你修改了models.py并再次运行迁移,Django会比较0001_initial.py与当前models.py的差异,然后生成0002_auto...py0002迁移并不会清空所有数据重新开始,而是执行类似“你只是添加了一个字段,那我就添加这个字段”的操作。但它的实现方式并非直接使用原生SQL。

迁移文件本身是类Python的代码。它们的跨数据库可移植性意味着,即使你在开发电脑上使用SQLite,在生产环境使用MySQL,两边的迁移文件也是相同的。因为它们只描述“这里有哪些列,是什么类型,应该是什么样子”。

迁移(migrate)命令的作用

这与migrate命令形成对比。migrate命令会读取所有迁移文件,识别当前连接的数据库类型,然后运行相应的SQL命令来创建或修改必要的表。

它会检查并执行:这是第一个迁移,应用它;这是第二个迁移,应用它;这是第三个迁移,应用它。最终,在应用完所有迁移后,数据库中的表结构就反映了所有迁移的累积效果,也就等同于当前最新的models.py文件。

这个过程有些复杂,但它关系到数据库的可移植性,以及我们希望将可移植部分(迁移逻辑)与不可移植部分(具体数据库的SQL实现)分离的设计目标。

迁移文件实例解析

如果你查看makemigrations的输出,以DJ4E-Samples这个示例项目为例,里面有很多模型。查看所有models.py文件,你会发现项目中的每个应用都有一个models.py文件。这是用Python代码定义的,例如class Book(...)

这种定义方式非常具有可移植性,也是我们在Django代码中看待数据库模型的方式。当你运行makemigrations时,会生成这些迁移文件,它们通常位于类似appname/migrations/0001_initial.py的路径下。

你可以看到我这里有一些迁移文件,实际上有两个。这意味着它先创建了第一个,然后我肯定修改了模型文件,再次运行makemigrations,它就记录了模型从第一个版本到第二个版本的变化。

你可以查看这些文件,但不要编辑它们。你可以将它们提交到GitHub,但不要手动修改。它们是一种非常可移植的表示形式,看起来有点像models.py文件。你应该始终通过makemigrations命令来生成它们。

同样,这些文件可以放入GitHub。

应用迁移与数据库演化

migrate命令会读取所有这些迁移文件。它不只是读取一个,如果存在一系列迁移文件,它会按需按顺序应用它们。

如果数据库已经应用了第一和第二个迁移,但还没有应用第三个,migrate能够识别这一点。它会说:“我知道该怎么做了,我只需要对这个已经存在的、正在运行的数据库进行微调,添加一列或修改某列的宽度,或者执行你在models.py中编辑所要求的任何操作。”

migrate完成后,你的数据库中就会有一系列表。这些表的命名基于应用名称和该应用内的模型名称。

所以,在这些表里,会有类似bookone_bookbookone_instancebookone_author等表。这些表我们之前查看过。migrate命令就是收集项目中所有应用的所有小迁移,然后将它们整合成一个包含所有这些内容的数据库。

迁移的演化记录机制

我一直在强调迁移是关于演化的。如果你在这里查看,会发现一个名为django_migrations的小表。Django在这个表中记录哪些迁移已经应用到了这个数据库

当它遍历迁移文件时,它会说:“哦,这个迁移在这个数据库里还没做,所以我需要对数据库做一点小小的调整。” 它会记住已经执行过的迁移。

在这些小型应用中,这个机制相当巧妙。

重置迁移状态

我们并不总是需要如此精巧地处理。有时,你可能只是想:“我搞糊涂了,我要重新运行makemigrations。” 因为你并不是在生产环境,你只是在反复练习。你可以重新运行makemigrations

以下是操作方法:你基本上只需要删除那些迁移文件。你可以删除所有以000开头的文件。如果你打算重新运行makemigrations,就删除应用migrations目录下所有以000开头的文件,然后重新运行makemigrations。它会读取你的models.py文件,并执行所有必要的操作来重新创建迁移文件。

请注意,这不是数据库本身,而是关于如何创建数据库的指令。这就是你重新运行makemigrations的方法。

从头开始运行迁移(更简单的方法)

实际上,从头开始运行migrate更简单。你可以清空一个应用的所有迁移(就像我刚刚对bookone做的那样),然后重新开始。

但是,你也可以通过直接删除数据库文件来清空所有迁移和整个数据库。因为我们使用的是SQLite,只需删除db.sqlite3文件即可。因为在执行migrate后,所有东西最终都存储在那个文件里。

所以,你删除那个文件,它就消失了。你可能创建的管理员账户或其他数据也会被一并清除,所有表都没了。这就是我喜欢SQLite的一点:删除这一个文件,就真的什么都没了。

然后,你重新运行python manage.py migrate,它会读取所有迁移文件,为你创建一个崭新、干净、完美的空数据库。

所以,如果你卡住了,并且不害怕丢失数据(对于大多数初学者来说,清空SQLite数据库再重新运行migrate是可以接受的),那就尽管尝试。就像我说的,如果你创建了一个管理员账户,嗯,你得重新创建它,但这可能是最坏的情况了。至少在这些入门应用中是这样。

总结

本节课中,我们一起学习了如何设计数据模型,如何在models.py中编码定义它,如何进行迁移操作,以及如何通过迁移来创建数据库。我们了解了makemigrations生成可移植的“蓝图”,而migrate则根据当前数据库类型执行具体的SQL操作。我们还掌握了在开发过程中如何重置迁移状态,以便重新开始。

接下来,我们将讨论可以运行哪些Python代码来更新数据库中的这些模型。

033:在Shell中使用Django模型演练 🐚

在本节课中,我们将学习如何在Django Shell中操作数据模型。我们将从设置环境开始,逐步演示如何创建、读取、更新和删除模型数据,让你直观地理解Django ORM的基本操作。


环境准备与项目设置

上一节我们介绍了Django模型的概念,本节中我们来看看如何在交互式Shell中实际操作它们。首先,我们需要准备好开发环境。

我使用PythonAnywhere的Bash控制台,这是一个Linux环境。我认为每个人都应该学习Linux。

首先,我进入主目录,使用cd ..命令返回上一级文件夹,然后使用ls -l命令查看文件夹列表。这里有一个名为dj4e-samples的文件夹,其中包含示例代码,你可以克隆它来编辑和查看文件,作为自己项目的参考。


我们进入该文件夹:

cd dj4e-samples

然后使用git pull命令从GitHub获取最新的代码。这样,如果我修复了示例代码中的任何错误,你总能获得最新版本。

我已经执行过这个命令了。接下来,我需要切换到我的虚拟环境。你可能会注意到我之前没有激活虚拟环境,现在激活了。这是因为下一个命令是pip命令,最好在虚拟环境中运行,以免影响系统全局的Python安装。

我切换到名为django3的虚拟环境:

source django3/bin/activate

现在运行pip命令来安装项目所需的所有依赖包:

pip install -r requirements.txt

这个命令会安装一系列工具代码。看起来有些包已经安装过了,但运行它可以确保所有Python依赖都已就位。

安装完成后,我使用clear命令清屏。接下来,运行一个在PythonAnywhere中常用的命令:

python3 manage.py check

这个命令会读取urls.py、应用程序配置等文件,检查是否存在问题。如果检查通过,就可以继续;否则需要先修复错误。我们不想在检查未通过的情况下继续执行makemigrations,因为它会开始创建更改。


创建数据库迁移与表

makemigrations命令是Django读取所有models.py文件并创建迁移文件的过程。迁移文件不是SQL,而是Python代码。由于项目是从GitHub克隆的,所有迁移文件已经存在。当然,你也可以删除它们并从头开始重新生成。

makemigrations会读取像users/models.py这样的文件,然后创建迁移。接下来,我们将删除现有的数据库(一个SQLite3文件)并重新创建。以下是具体步骤:

首先,运行迁移命令:

python3 manage.py makemigrations

然后,删除旧的数据库文件(如果存在),并应用迁移来创建新的数据库和表:

rm db.sqlite3
python3 manage.py migrate


你会看到,对于这个Django项目中的每个应用程序,它都在创建一系列表。例如,users应用的初始迁移会创建我们将要操作的表。


启动Django Shell并进行操作

完成上述设置后,是时候启动Django Shell了:

python manage.py shell

这个命令会启动Django,加载所有Django库并读取所有配置文件。此时可能会出现一些错误,如果出现,需要返回去修复。manage.py check命令已经做了一部分检查,但现在Shell会加载你的views.py等文件。

在Shell中,你可以输入Python命令,但这些命令是在Django环境中执行的,不仅仅是纯Python,而是预加载了所有Django应用程序的Python环境。所以启动需要一点时间。

现在我们可以开始操作了。首先,导入用户模型:

from users.models import User

然后,创建一个新的用户对象。以下两个命令非常重要:

u = User(name='Kristen', email='kristen@umich.edu')
u.save()

第一行命令User(name='Kristen', email='...')会在Django的Python内存中创建一个新的用户模型对象,并将其赋值给变量u。此时,它尚未存储到数据库中。

我们可以验证这一点:

print(u.id)  # 输出:None

因为如果它被保存到数据库并获得一行记录,就会得到一个序列号(ID)。u.save()命令的作用是:将内存中u变量包含的数据存储到数据库中。

保存后,我们再查看ID:

print(u.id)  # 输出:1

现在可以看到它被存储到数据库中,ID为1。这些数字很简单,从1开始递增。让我们再添加几个人:

u = User(name='Sally', email='sally@umich.edu')
u.save()
u = User(name='Ted', email='ted@umich.edu')
u.save()
u = User(name='Bob', email='bob@umich.edu')
u.save()
u = User(name='Chuck', email='csev@umich.edu')
u.save()

现在,最后一个创建的u是Sally,我们可以查看她的信息:

print(u.name)  # 输出:Sally
print(u.id)    # 输出:5

她是第五个。至此,我已经有五个模型对象被持久化存储到数据库中了。


查询、过滤与删除数据

现在,如果我们想检索这些用户对象,可以这样做:

users = User.objects.values()
print(users)

User是模型名,实际上是一个类。objects是它的一个属性,它有一个values()方法。执行后会返回一个QuerySet,它功能上类似于一个列表。列表里是一系列对象,看起来就像我们输入的数据,但这是从数据库中提取出来的。现在数据里包含了每个行的主键ID。

我们可以添加过滤器,这类似于SQL中的WHERE子句:

users = User.objects.filter(email='csev@umich.edu').values()
print(users)

这个模式是:User.objects.filter(...).values()filter是一个方法,它返回的对象也有values方法,所以可以链式调用。这基本上是说“只给我邮箱是csev@umich.edu的记录”。返回的QuerySet是一个列表,现在应该只有一条记录(如果没有邮箱唯一性约束,也可能有多条)。

我们也可以删除记录。首先获取特定记录,然后在其上运行delete方法:

user_to_delete = User.objects.filter(email='ted@umich.edu')
user_to_delete.delete()

这会删除Ted的记录。然后我们再次检索所有对象,可以看到Ted已经不见了:

all_users = User.objects.values()
print(all_users)



更新数据与排序

我们还可以更新数据。让我们检查一下之前那条记录,并更改名字。这是对数据库进行CRUD操作中的“更新”(Update)。

user_to_update = User.objects.filter(email='csev@umich.edu')
user_to_update.update(name='Charles')

现在,如果我们查询这条记录:

updated_user = User.objects.filter(email='csev@umich.edu').values()
print(updated_user)

会发现名字已经从“Chuck”变成了“Charles”。这不仅仅是内存中的更改,Django已经将这一切写入了数据库。



最后,我们还可以对结果进行排序。你可以按邮箱升序排列:

users_asc = User.objects.order_by('email').values()
print(users_asc)

或者降序排列:

users_desc = User.objects.order_by('-name').values()
print(users_desc)

以上就是在Python Shell中可以执行的一些操作。要退出Shell,请输入:

quit()


总结 🎯

本节课中我们一起学习了如何在Django Shell中操作模型。我们从设置虚拟环境和项目依赖开始,然后创建了数据库迁移和表。接着,我们在Shell中导入了模型,并演示了创建User().save())、读取User.objects.values())、更新.update())和删除.delete())数据的基本CRUD操作,最后还展示了如何使用过滤器(.filter())和排序(.order_by())。这些是使用Django ORM进行数据交互的核心技能,希望本教程对你有所帮助。

034:重置SQLite3数据库 🔄

在本节中,我们将学习当Django项目中的数据库(特别是SQLite3)因模型迁移错误或数据混乱而无法正常工作时,如何通过完全重置数据库来解决问题。这是一种在开发阶段快速恢复项目状态的有效方法。

上一节我们介绍了Django模型和迁移的基本概念。本节中我们来看看当迁移过程出错时,如何通过重置数据库来重新开始。

概述

当你在Django教程(例如教程2)中操作模型时,可能会遇到迁移错误或数据库状态混乱的情况。此时,与其花费大量时间调试复杂的迁移问题,不如直接重置数据库并重新开始。本教程将指导你完成以下步骤:

  1. 检查项目是否有语法错误。
  2. 删除旧的迁移文件和数据库文件。
  3. 重新生成迁移文件并应用迁移。
  4. 重新创建超级用户。

详细步骤

第一步:检查项目状态

在执行任何重置操作之前,务必先使用 python manage.py check 命令验证项目代码是否存在语法错误。这可以确保问题确实出在数据库层面,而非代码本身。

以下是操作命令:

python manage.py check

如果命令输出“No issues”,则表明代码没有语法问题,可以继续进行数据库重置。

第二步:定位并删除相关文件

要彻底重置数据库,需要删除两个关键部分:迁移文件和数据库文件。

迁移文件是数据库结构的便携式表示,通常位于每个应用目录下的 migrations 文件夹中,文件名带有数字编号(如 0001_initial.py)。数据库文件默认是项目根目录下的 db.sqlite3

以下是需要删除的文件:

  • 迁移文件:位于应用目录(如 polls2/migrations/)下的所有 .py 文件(__init__.py 除外)。
  • 数据库文件:项目根目录下的 db.sqlite3 文件。

删除这些文件后,你的项目将回到仅包含模型代码(models.py)的初始状态。请注意,此操作会永久删除数据库中的所有数据。

第三步:重新生成并应用迁移

删除旧文件后,需要重新创建数据库结构。

首先,运行 makemigrations 命令。该命令会读取所有 models.py 文件,并生成新的初始迁移文件(通常是 0001_initial.py)。

以下是操作命令:

python manage.py makemigrations

接着,运行 migrate 命令。该命令会读取新生成的迁移文件,并实际在数据库中创建对应的数据表。

以下是操作命令:

python manage.py migrate

执行成功后,你会看到一个新的 db.sqlite3 文件被创建,并且其文件大小不再为零。

第四步:重新创建超级用户

由于数据库已被清空,之前创建的管理员账户也会消失。如果需要使用Django管理后台,必须重新创建超级用户。

使用 createsuperuser 命令,并按照提示输入用户名、邮箱和密码。

以下是操作命令:

python manage.py createsuperuser

至此,你的Django项目数据库已完全重置。你可以重新运行开发服务器,项目将基于最新的模型定义重新开始。请注意,之前添加的任何数据(如问题、选项等)都已丢失。

总结

本节课中我们一起学习了在Django开发过程中重置SQLite3数据库的完整流程。当遇到棘手的迁移错误或数据库状态不一致时,按照“检查 -> 删除 -> 重建”的步骤操作,可以快速让项目回到一个干净、可工作的状态。这是一种非常实用的开发调试技巧。记住,此方法会清除所有数据,因此仅推荐在开发环境中使用。

035:面对面办公时间实录

在本节课中,我们将通过一段真实的课堂互动记录,来了解《Django for Everybody》课程社区的氛围和学员的反馈。这段内容记录了在加利福尼亚州旧金山举行的一次面对面办公时间。

概述

本次办公时间是《Django for Everybody》系列课程社区活动的一部分。课程讲师Dr. Chuck与学员们齐聚一堂,新老学员依次进行自我介绍,并分享学习这门课程的体验与收获。

学员自我介绍与课程反馈

以下是学员们依次进行的自我介绍。他们来自不同背景,但都通过这门课程获得了积极的改变。

  • Valerie:向大家问好。
  • Emma:认为这是最好的通关课程。
  • S Lee:很高兴认识大家。
  • Anthony:认为Python是最棒的,这门课程也是最好的。
  • Robert:认为Dr. Chuck拯救了他的职业生涯。
  • Rosalie:欢迎大家,并希望大家享受课程。
  • Matt:这门课程帮助他重新开始编程,并实现了从密尔沃基到加利福尼亚的搬迁。
  • Lana:希望大家像她一样享受这门课程。
  • Dick:希望大家和他一样享受这门课程。
  • Ben:向Dr. Chuck表示感谢。
  • Boick:这门课程将他引入了计算机科学领域。
  • Bobby:这门课程以及整个系列课程将帮助他实现职业转型。
  • Alex:对来到这里感到非常兴奋。
  • Ryan:非常喜欢这门课。
  • Di:非常喜欢Dr. Chuck的课程,也很高兴见到Dr. Chuck本人。
  • Wendyinki:向大家问好。
  • Saayya:强烈推荐《for Everybody》系列课程。

课程传统与社区文化

在学员们介绍完毕后,Dr. Chuck提到了课程社区的两个传统。

首先,他幽默地承认自己经常犯错,并以此拉近与学员的距离。随后,他引导大家进行第二个传统:为所有提供帮助的教学助理们送上热烈的掌声,以感谢他们对课程社区的支持和贡献。

总结

本节课中,我们一起回顾了一次生动的《Django for Everybody》课程线下办公时间。通过学员们的自我介绍,我们看到了这门课程如何帮助不同背景的人学习编程、实现职业转型或重拾对计算机科学的兴趣。同时,我们也感受到了该课程社区互助、友好的氛围。本次办公时间在掌声中圆满结束。

036:Django中的URL路由 🧭

在本节课中,我们将开始学习如何生成应用程序的实际输出,并深入了解视图(Views)和模板(Templates)的使用。这部分内容非常丰富,是构建Django应用的核心。

到目前为止,我们已经开始讨论URL。URL配置很简单:当收到特定格式的URL请求时,选择一个视图并将请求发送给该视图。接下来,我们将重点讨论视图(views.py)、模板,以及后续会讲到的表单。我们还会花大量时间讨论模型(models.py),包括模型如何与Python代码交互、如何通过Shell操作、以及如何指导我们读写数据库。

随着我们将视图组合起来,我们开始填充应用的各个部分,真正开始构建整个应用程序。我知道,要到达能看到完整应用的阶段需要一些时间。

保持代码更新 🔄

无论你是在笔记本电脑上还是在PythonAnywhere上使用DJ4示例代码,都应该定期执行 git pull 命令。因为我一直在完善这些示例,我希望你能确保拥有最新的代码副本。有时我会添加一些小的内容或文档,虽然不改变核心示例,但会进行补充。

视图:应用的核心 🎯

视图是应用程序的核心。URL最终会路由到视图,而模型(Models)则服务于视图的需求,作为允许视图读写数据库的中间层。

views.py 文件包含模型相关的逻辑,并处理传入的数据。例如,当我们收到表单和POST数据时,视图会将这些数据复制到数据库中。views.py 还决定是重定向用户到另一个页面,还是生成实际的HTML页面。在生成HTML时,它通常使用模板,然后将结果发送回客户端。

因此,真正的工作是在视图中完成的。你会发现,你会在视图中编写大量代码,在URL配置中写一行,在模型文件中写几行,而视图(包括模板)是代码最集中的地方。

Django的URL处理流程 🔗

当Django收到一个传入的文档请求时,它首先会解析URL。域名之后的第一部分通常是应用程序名。请记住,Django有一个项目(Project),其下有一个或多个应用程序(App)。在DJ3示例中,你会看到很多应用程序,每个都展示某个主题的示例代码。

URL的第二部分是应用程序名,实际上它也是Django项目内的文件夹名。

在应用程序内部,URL的下一部分通常对应一个视图。应用程序内的视图在 urls.py 中定义。在此之后,URL可能包含两种参数:

  1. 一种是跟在问号 ? 后面的键值对参数,使用 & 符号连接。
  2. 另一种是直接放在斜杠 / 后面的参数,这更像REST风格的漂亮URL,它将参数直接放在URL路径中,而不是使用问号(后者是较旧的做法)。

URL分发器(路由器)🛣️

Django中有一个称为URL分发器(我有时在图中称它为路由器)的组件。它的基本功能是让你能够定义URL,指定如何解析和处理这些URL,以及如何将这些URL路由到不同的视图代码。

我们在 urls.py 文件中进行这些配置。主要有三种基本模式:

以下是URL路由的三种基本模式:

  1. 路由到预定义的类:将特定的URL模式路由到一个预定义的视图类。
  2. 路由到函数(旧式):路由到一个函数。这个函数接收一个 request 对象作为参数。这个 request 对象封装了所有数据:参数、URL、请求是否安全、来自哪个主机、IP地址等。视图函数查看这个请求对象,决定做什么(可能查询数据库),然后返回一个响应(可能是重定向或HTML)。函数是较低层级的做法。
  3. 路由到类(推荐):也可以定义一个类。定义类的方式非常优雅,类中可以有像 getpost 这样的方法,具体取决于我们正在处理的HTTP请求类型。在这些方法中,请求对象和任何其他URL参数也会被传入。

解析示例 urls.py 📝

让我们看一个来自 views 应用的示例 urls.py 文件,我们会看到所有这三种路由的例子。

urlpatterns 是一个全局变量,它是一个列表,但对Django有特殊意义。

你会看到这些 path() 命令(还有其他描述方式)。path('', ...) 中的空字符串路径意味着紧接在应用程序名之后的只是一个斜杠 /。然后我们指定要发送到的视图。

例如,TemplateView.as_view() 基本上是为了节省你编写代码的工作。如果你只想从 templates 文件夹中取出一个模板并返回它,你就不必在 views.py 中编写自己的代码。这就是为什么我们要从 django.views.generic 导入 TemplateView。这就像是说:“我已经写好了那个模板,我不想写代码去读取并发送它。” 这是Django为我们提供的一个预定义功能。

更旧式的做法是这里的语法:from . import views 导入 views.py,然后 views.function_name 指向那里的函数。

而这些是来自本应用程序 views.py 的类。语法有点奇怪:ClassName.as_view(),其中 as_view() 是一个静态方法,它返回一个可以响应传入请求的函数。

总结 📚

本节课我们一起学习了Django中URL路由的核心概念。我们了解到视图是应用逻辑处理的核心,URL分发器负责将请求路由到正确的视图。我们介绍了三种路由模式:预定义类、函数视图和类视图,并通过示例 urls.py 文件观察了它们的实际应用。

下一节,我们将实际查看 views.py 文件,深入了解视图的具体实现。

037:Django视图

在本节课中,我们将学习Django视图(View)的核心概念。视图是处理Web请求并返回响应的关键组件。我们将探讨如何编写视图代码,从最简单的静态页面渲染到处理带有参数的动态请求。

概述

视图是Django MTV(Model-Template-View)架构中的“V”。它负责处理业务逻辑:接收一个Web请求(HttpRequest对象),并返回一个Web响应(HttpResponse对象)。本节课我们将学习几种编写视图的方法。

使用TemplateView渲染静态页面

上一节我们介绍了URL配置,本节中我们来看看如何创建最简单的视图——渲染一个静态HTML文件,而无需编写任何Python逻辑。

Django提供了一个便捷的类视图 TemplateView。你只需要在urls.py中指定要使用的模板文件,Django就会自动处理请求并返回该HTML文件。

代码示例:

# 在 urls.py 中
from django.views.generic import TemplateView

urlpatterns = [
    path('', TemplateView.as_view(template_name='views/main.html')),
]

在这个例子中,当用户访问网站根路径时,Django会查找并渲染 views/main.html 这个模板文件。需要注意的是,模板文件的路径结构通常是 应用名/templates/应用名/。这种看似重复的命名(例如 views/templates/views/)是为了避免不同应用间的模板文件命名冲突,因为Django的模板名称在所有应用中都是全局的。

这种视图适用于简单的、不需要从数据库读取数据的静态页面。

编写函数视图

当我们需要处理更复杂的逻辑时,就需要编写自己的视图函数。每个视图函数至少接收一个参数:request对象。

request对象(HttpRequest类的实例)封装了所有来自浏览器的请求数据,例如URL、请求方法(GET/POST)、查询参数、请求体等。视图函数的工作就是处理这个request,并返回一个response对象(通常是HttpResponse或其子类)。

以下是编写一个简单函数视图的步骤:

  1. 定义函数:在views.py中定义一个函数,第一个参数必须是request
  2. 编写逻辑:在函数体内编写处理请求的业务逻辑。
  3. 返回响应:函数必须返回一个HttpResponse对象。

代码示例:

# 在 views.py 中
from django.http import HttpResponse

def funky(request):
    html = """
    <html>
    <body>
        <h1>Funky Page</h1>
        <p>This is a funky page.</p>
    </body>
    </html>
    """
    return HttpResponse(html)

# 在 urls.py 中关联URL
from django.urls import path
from . import views

urlpatterns = [
    path('funky/', views.funky),
]

当用户访问 /funky/ 路径时,Django会调用funky函数,并将包含请求信息的request对象传递给它。函数返回一个包含HTML字符串的HttpResponse,Django会将其发送回用户的浏览器。

从请求中获取参数

在实际应用中,我们经常需要处理带有参数的请求,例如通过URL查询字符串(?key=value)传递数据。这些参数可以通过request对象轻松获取。

request.GET属性是一个类似字典的对象,它包含了所有通过URL查询字符串传递的参数。

以下是处理带参数请求的方法:

  1. 访问 request.GET 字典。
  2. 使用键名来获取对应的值。

代码示例:

# 在 views.py 中
from django.http import HttpResponse

def danger(request):
    # 从查询字符串中获取名为 ‘guess’ 的参数值,例如 ?guess=42
    guess_value = request.GET.get('guess', '')  # 第二个参数是默认值
    html = f"""
    <html>
    <body>
        <h1>Danger Page</h1>
        <p>Your guess was {guess_value}.</p>
    </body>
    </html>
    """
    return HttpResponse(html)
# 在 urls.py 中
path('danger/', views.danger),

现在,如果用户访问 /danger/?guess=42,视图函数会从request.GET[‘guess’]中取出值’42’,并将其插入到返回的HTML页面中。这就是视图与用户输入进行交互的基本方式。

总结

本节课中我们一起学习了Django视图的基础知识。我们了解了视图作为请求处理器的角色,并掌握了三种创建视图的方法:

  1. 使用内置的TemplateView来快速渲染静态模板。
  2. 编写自定义的函数视图来处理请求并返回简单的HttpResponse
  3. 在函数视图中通过request.GET字典来获取URL中的查询参数,实现动态内容生成。

理解request(输入)和response(输出)对象是掌握Django视图开发的关键。在接下来的课程中,我们将学习如何将视图与模板和模型结合,创建更加强大和动态的Web应用。

038: Django视图内部与HTML转义 🔐

在本节课中,我们将要学习Django视图的核心概念,并重点探讨一个至关重要的安全议题:HTML转义。我们将了解如何安全地处理用户输入,防止跨站脚本攻击,并学习函数视图、类视图以及重定向等不同视图的编写方式。


概述:视图与安全责任

作为Web应用开发者,我们有责任创建安全的应用程序。因此,我们需要关注那些表面上看似无害,实则可能引发安全问题的代码。关键在于,我们需要理解用户数据如何被处理并最终呈现在浏览器中。

潜在的安全风险:跨站脚本攻击

上一节我们介绍了视图的基本概念,本节中我们来看看一个具体的安全隐患。问题通常出现在我们从用户那里获取数据时,例如通过表单或URL参数。

例如,我们从请求对象中获取一个名为 guess 的参数:

guess = request.GET.get('guess', '')

假设用户输入了 42,这看起来无害。然后,我们可能直接将这个值拼接进HTML并返回给浏览器:

response = f"<p>Your guess was {guess}</p>"

问题在于:如果恶意用户提交的数据本身包含HTML或JavaScript代码,会发生什么?因为HTML和JavaScript是编程语言,浏览器会执行它们。这意味着,用户可以通过向你的系统输入数据,来“编程”控制浏览器。

更严重的是,这可能导致跨站脚本攻击。攻击者可以将一段恶意代码存入你的系统,当其他用户查看这些数据时,他们的浏览器就会执行这段代码。这段代码可以窃取用户的Cookie、调用恶意Web服务,甚至执行如修改成绩等破坏性操作。

以下是一个危险的视图代码示例,它直接拼接了用户输入:

# 危险示例:未转义用户输入
def bad_view(request):
    guess = request.GET.get('guess', '')
    response = f"<p>Your guess was {guess}</p>"
    return HttpResponse(response)

如果用户提交 guess 的值为 <b>test</b>,这段HTML会被浏览器解析,从而显示为加粗的文本。如果提交的是 <script>alert('owned')</script>,那么JavaScript代码将会被执行。

解决方案:HTML转义

为了防止这种攻击,我们必须对用户提供的、将要放入HTML响应的任何数据进行转义。转义会将危险的字符(如 <, >, &, ")转换为对应的HTML实体(如 &lt;, &gt;, &amp;, &quot;)。这样,浏览器会将它们显示为普通文本,而不会解析为代码。

Django提供了一个便捷的函数 django.utils.html.escape() 来完成这项工作。

以下是安全的视图代码示例:

from django.utils.html import escape

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/64119f0f9e9e405ab16ed9f27fcd8ff6_10.png)

def safe_view(request):
    guess = request.GET.get('guess', '')
    # 关键步骤:转义用户输入
    safe_guess = escape(guess)
    response = f"<p>Your guess was {safe_guess}</p>"
    return HttpResponse(response)

经过转义后,即使用户输入 <script>alert('owned')</script>,它也会被转换为 &lt;script&gt;alert('owned')&lt;/script&gt;,从而在页面上安全地显示为文本,而不会执行。

核心原则:任何来自用户的数据,在放入HTML响应之前,都必须进行转义。

从URL路径中获取参数

除了通过 request.GET 获取查询参数,Django还允许我们从URL路径本身提取更美观的参数。

urls.py 中,我们可以这样定义路径:

from django.urls import path
from . import views

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/64119f0f9e9e405ab16ed9f27fcd8ff6_12.png)

urlpatterns = [
    path('rest/<int:guess>', views.game, name='game'),
]

这里的 <int:guess> 是一个路径转换器。它告诉Django:匹配 rest/ 后面的一个整数,并将其作为名为 guess 的参数传递给视图函数 views.game

对应的视图函数可以这样写:

def game(request, guess):  # guess 参数由Django自动从URL中提取并传入
    safe_guess = escape(str(guess))  # 同样需要转义
    response = f"<p>Your guess was {safe_guess}</p>"
    return HttpResponse(response)

这种方式让URL更清晰,并且将解析URL参数的工作交给了Django框架。

类视图简介

之前我们看到的都是函数视图,它们是相对底层的写法。Django还提供了基于类的视图,这能更好地利用面向对象的特性,如继承,从而减少重复代码并提高可维护性。

以下是一个简单的类视图示例,用于处理GET请求:

from django.views import View
from django.http import HttpResponse
from django.utils.html import escape

class MainView(View):
    def get(self, request):
        return HttpResponse("This is a GET request.")

urls.py 中,我们需要使用 as_view() 方法来将类转换为视图函数:

urlpatterns = [
    path('main/', views.MainView.as_view(), name='main'),
]

类视图的强大之处在于可以清晰地分离不同HTTP方法(如GET和POST)的处理逻辑。我们将在后续课程中深入利用类视图的继承等特性。

类视图同样可以接收URL路径参数:

class RemainView(View):
    def get(self, request, guess):
        safe_guess = escape(guess)
        return HttpResponse(f"Your guess was {safe_guess}")

对应的URL配置:

urlpatterns = [
    path('remain/<slug:guess>', views.RemainView.as_view(), name='remain'),
]

使用重定向响应

到目前为止,我们返回的都是包含内容的HTTP响应(状态码200)。但有时,我们不想直接返回页面,而是想告诉浏览器:“你找的地方不对,请去另一个地址”。这时就需要使用重定向

重定向使用HTTP状态码302(临时重定向)或301(永久重定向),并在响应头中设置 Location 字段来指定目标URL。浏览器收到这种响应后,会自动跳转到新的地址。

在Django中,可以使用 HttpResponseRedirect 来发送重定向。

以下是重定向的视图函数示例:

from django.http import HttpResponseRedirect

def bounce(request):
    # 将用户重定向到另一个URL
    return HttpResponseRedirect('https://www.example.com/simple.html')

在控制器(Controller)的语境下,重定向是一种强大的工具,用于在完成某些操作(如表单提交)后,将用户的浏览器引导到新的页面。

如果你在浏览器的开发者工具“网络”选项卡中观察访问 /bounce/ 的过程,你会看到:

  1. 第一个请求收到状态码 302Location 头。
  2. 浏览器立即自动发起第二个请求,去获取 Location 头指定的新URL,并返回状态码 200(成功)。


总结

本节课中我们一起学习了Django视图的几个关键方面:

  1. 安全第一:我们深入探讨了跨站脚本攻击的原理,并学习了通过 escape() 函数对用户输入进行HTML转义的重要性。这是构建安全Web应用的基石。
  2. 视图类型:我们了解了函数视图和类视图两种编写方式。类视图通过面向对象特性为后续构建复杂功能提供了更好的结构。
  3. 参数获取:我们学习了两种从用户获取数据的方式:通过 request.GET 获取查询字符串参数,以及通过URL路径转换器(如 <int:guess>)从美观的URL中提取参数。
  4. 响应类型:除了返回HTML内容的 HttpResponse,我们还学习了使用 HttpResponseRedirect 进行重定向,以控制用户的浏览流程。

记住,始终对来自用户并即将嵌入HTML的数据进行转义,是保护你的应用和用户免受攻击的最简单有效的方法之一。在接下来的课程中,我们将学习使用模板来更优雅、更安全地生成HTML,进一步简化视图的工作。

039:在Django中使用模板

概述

在本节课中,我们将学习Django框架中的模板系统。模板是用于动态生成HTML(或其他文本格式)的强大工具,它允许我们将Python代码与网页设计清晰地分离开来。我们将了解模板的基本概念、工作原理以及如何在视图中使用它们来创建用户界面。


视图的另一半:模板

上一节我们介绍了视图(Views),它是处理请求并返回响应的Python代码。本节中我们来看看视图的另一半——模板(Templates)。模板是一种创建HTML的方式,它构成了Django应用用户界面的最后一块拼图。从此刻起,我们将开始构建用户界面,而不仅仅是介绍Django应用的各个部分。

根据Django官网的描述,模板提供了一种动态生成HTML的便捷方式。Django内置了一个默认的模板引擎。其基本流程是:我们将数据传递给模板,这个过程称为“渲染”(rendering),然后引擎会生成最终的HTML,我们将其封装在HTTP响应中返回给浏览器。请求-响应循环本身没有改变,改变的是我们不再使用Python代码拼接字符串,而是使用模板引擎。

以下是我们之前一直在做的事情:在视图类中,我们接收请求对象,可能还有一些其他参数,然后通过字符串拼接和转义来生成HTML响应。

# 示例:使用字符串拼接的视图
def my_view(request):
    html = "<html><body>Your guess was: " + str(guess) + "</body></html>"
    return HttpResponse(html)

这种方法存在一些问题:需要确保语法正确、处理缩进问题、混合使用单引号和双引号等,代码会变得冗长且难以维护。


模板的基本原理

模板的基本原理是使用一段软件(即Django提供的模板引擎)。一个模板本质上是一个包含了一些“热点”(hotspots)的HTML文件,这些热点指示“在此处插入数据”。其余部分则是普通的静态HTML。

我们通过一个对象(通常是字典)将数据传递给模板。然后,一段代码(渲染引擎)会将所有内容组合起来,生成嵌入了数据的最终HTML。我们传递的渲染数据通常来自模型(Model),例如从数据库加载的记录。

在最简单的形式中,我们有一个称为“上下文”(context)的字典,它包含键值对。模板则是包含特殊标记的HTML。

# 上下文数据示例
context = {'guess': 200}

<!-- 模板示例 (guess.html) -->
<p>Your guess was {{ guess }}</p>

双花括号 {{ }} 是模板语言的特殊标记。它表示:“将此标记替换为上下文字典中对应键的值”。因此,在最终输出中,{{ guess }} 会被替换为数字 200。你可以想象编写一个简单的函数来实现这种替换,但Django已经为我们实现了功能更强大的引擎。


模板实战:一个猜数字示例

让我们看一个实际的代码示例。我们有一个名为 Tmpl 的应用,其中包含一个视图函数 game,它接收一个参数(猜测的数字)。

# views.py 中的视图函数
from django.shortcuts import render

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/43f5809944415a97856564e3c2f35e39_15.png)

def game(request, guess):
    # 将URL中的参数转换为整数
    guess_int = int(guess)
    # 创建上下文字典
    context = {'guess': guess_int}
    # 渲染模板并返回响应
    return render(request, 'tmpl/game.html', context)

视图函数接收 request 对象和从URL捕获的 guess 参数。我们创建了一个上下文字典,其中键 'guess' 对应转换后的整数值。然后,我们调用 render() 函数,传入请求对象、模板名称和上下文数据。render() 函数会处理所有替换工作,并返回一个可以直接返回给浏览器的 HttpResponse 对象。

现在,让我们看看对应的模板文件 game.html

<!-- templates/tmpl/game.html -->
<p>Your guess was {{ guess }}.</p>
{% if guess < 42 %}
    <p>Too low!</p>
{% elif guess > 42 %}
    <p>Too high!</p>
{% else %}
    <p>Just right!</p>
{% endif %}

模板中有两种主要标记:

  1. 双花括号 {{ }}:用于输出变量的值。例如 {{ guess }} 会输出传递进来的猜测数字。
  2. 花括号百分号 {% %}:用于包含模板标签,实现逻辑控制,如 iffor 循环等。在这个例子中,我们根据 guess 的值判断其与目标数字42的关系,并输出相应的提示信息。

如果传入的 guess 是200,那么渲染后的HTML将是:“Your guess was 200. Too high!”。

这种方法使视图代码变得非常简洁,避免了在Python代码中混合HTML和逻辑判断。


模板的存放与命名约定

一个Django项目由一个或多个应用(app)组成。当Django启动时,它会根据 settings.py 中的配置加载所有应用的文件。

这里有一个重要概念:模板名称在整个Django项目中是全局的。这意味着,如果你在多个不同的应用中都有一个名为 detail.html 的模板,Django将无法区分应该使用哪一个。

为了解决这个命名冲突问题,我们采用了一个通用约定:

  1. 在每个应用的 templates 目录下,再创建一个与该应用同名的子文件夹。
  2. 将模板文件放在这个子文件夹中。
  3. 在视图里引用模板时,使用 app_name/template_name.html 的格式。

例如,对于我们的 Tmpl 应用:

  • 模板文件路径应为:tmpl/templates/tmpl/game.html
  • render() 函数中,模板名称应写为:'tmpl/game.html'

虽然这看起来有些重复(应用名出现了两次),但这是必要的,它能确保在大型项目中模板名称的唯一性。这是Django社区的通用做法。


模板语言功能简介

我们之前已经看到了模板语言的两个核心功能:变量替换和逻辑控制。实际上,Django模板语言的功能远不止这些。

以下是模板语言的一些关键特性:

  • 变量:使用 {{ variable_name }} 访问并显示上下文中的变量。
  • 标签:使用 {% tag_name %} 执行逻辑操作。例如:
    • {% for item in list %} ... {% endfor %} 用于循环。
    • {% if condition %} ... {% endif %} 用于条件判断。
    • {% url 'view_name' %} 用于生成URL。
    • {% csrf_token %} 用于添加CSRF令牌(后续课程会涉及)。
  • 过滤器:可以在变量输出前对其进行修改,语法是 {{ variable|filter }}。例如:
    • {{ name|lower }} 将变量转换为小写。
    • {{ value|length }} 获取列表或字符串的长度。
    • {{ date|date:"Y-m-d" }} 格式化日期。

这些功能使得我们能够在模板中处理大部分展示逻辑,保持视图代码专注于业务逻辑和数据准备。


总结

本节课中我们一起学习了Django模板系统。我们了解到模板是用于分离业务逻辑和展示层的关键组件,它通过特殊的语法({{ }}{% %})将动态数据嵌入到静态的HTML骨架中。我们掌握了如何在视图中使用 render() 函数来结合上下文数据与模板,并理解了由于模板名称的全局性,需要遵循 app_name/template_name.html 的命名约定来组织模板文件。模板语言提供的变量、标签和过滤器等功能,为我们构建动态、灵活的用户界面奠定了坚实的基础。从下一节开始,我们将利用这些知识创建更复杂的网页应用。

040:Django模板语言(DTL)详解

概述

在本节课中,我们将深入学习Django模板语言(DTL)。我们将探讨其核心语法、功能以及如何安全地使用它来动态生成HTML内容。DTL是Django默认的模板引擎,其设计兼顾了功能性与安全性,是构建动态Web页面的重要工具。

模板语言基础

上一节我们介绍了模板的基本概念,本节中我们来看看DTL的具体语法和功能。

DTL使用双花括号 {{ }} 进行变量替换。这种语法也被许多其他模板引擎采用,有时甚至被称为“Mustache”语法,因为花括号形似胡须。在Django中,所有通过双花括号输出的内容都会自动进行HTML转义,这是防止跨站脚本(XSS)攻击的关键安全特性。

如果你确定内容是安全的,并希望直接输出HTML,可以使用 safe 过滤器来禁用自动转义。其语法为 {{ variable|safe }}|safe 部分被称为过滤器,它修改了变量的输出方式。

核心语法元素

DTL不仅支持变量替换,还支持逻辑控制、循环和模板继承。以下是其主要功能:

  • 变量替换与转义:使用 {{ variable }}。这是最常用的功能,用于将上下文中的数据插入到HTML中。
  • 逻辑控制:使用 {% if condition %} ... {% endif %}{% else %}。其语法类似Python,便于理解。
  • 循环:使用 {% for item in list %} ... {% endfor %} 来遍历列表或查询集。
  • 代码执行与URL解析:使用 {% %} 标签执行代码,例如 {% url ‘view_name’ arg %} 用于根据视图名称和参数生成URL。
  • 模板继承:使用 {% block block_name %} ... {% endblock %} 定义可替换的块,允许子模板扩展和覆盖父模板的特定部分。

实践示例:从简单到复杂

让我们通过一系列代码示例,具体了解这些功能如何应用。

静态模板渲染

最简单的用例是渲染一个不需要任何动态数据的静态HTML模板。

def simple_view(request):
    return render(request, ‘myapp/simple.html‘)

对应的模板 simple.html 就是普通的HTML文件。Django提供了一个内置的 TemplateView 类视图,专门用于处理这种简单场景。

变量替换

现在,我们向模板传递一些动态数据。以下视图向模板传递了一个键为 ‘zap’、值为 42 的上下文。

def guess_view(request):
    context = {‘zap‘: 42}
    return render(request, ‘myapp/guess.html‘, context)

在模板 guess.html 中,我们使用双花括号来引用这个变量:

<p>Your guess is {{ zap }}</p>

最终,浏览器将显示“Your guess is 42”。{{ zap }} 会被自动转义后替换为上下文字典中 ‘zap’ 键对应的值。

理解 safe 过滤器

为了展示自动转义和 safe 过滤器的作用,我们传递一个包含HTML标签的字符串。

def safe_view(request):
    context = {
        ‘zap‘: 42,
        ‘txt‘: ‘<b>Bold Text</b>‘
    }
    return render(request, ‘myapp/safe_demo.html‘, context)

在模板 safe_demo.html 中,我们以三种方式输出 txt 变量:

<p>Escaped: {{ txt }}</p>          <!— 显示:<b>Bold Text</b> —>
<p>Safe: {{ txt|safe }}</p>        <!— 显示:Bold Text (加粗) —>

第一个输出被转义,用户看到的是原始的 <> 字符。第二个输出使用了 safe 过滤器,浏览器会将其中的 <b></b> 解释为HTML标签,从而使文本加粗显示。重要提示:只有在你完全信任该变量的内容时(例如,它来自你的系统,而非用户输入),才应使用 safe 过滤器。

循环与条件判断

DTL可以处理列表等数据结构,并进行逻辑判断。

def loop_view(request):
    f = [‘apple‘, ‘orange‘, ‘banana‘, ‘lychee‘]
    n = [‘peanut‘, ‘cashew‘]
    context = {
        ‘fruits‘: f,
        ‘nuts‘: n,
        ‘zap‘: 42
    }
    return render(request, ‘myapp/loop.html‘, context)

在模板 loop.html 中,我们可以遍历列表并使用条件判断:

<ul>
{% for x in fruits %}
    <li>{{ x }}</li>
{% endfor %}
</ul>

<p>
{% if nuts %}
    Number of nuts: {{ nuts|length }}
{% else %}
    No nuts available.
{% endif %}
</p>

{% for %} 标签会遍历 fruits 列表,为每个元素生成一个列表项。{% if %} 标签检查 nuts 变量是否存在(非空),如果存在,则使用 length 过滤器输出其长度。

访问嵌套对象

DTL支持通过点号 . 访问对象的属性或字典的键,从而遍历复杂的数据结构。

def nested_view(request):
    context = {
        ‘outer‘: {‘inner‘: 42}
    }
    return render(request, ‘myapp/nested.html‘, context)

在模板 nested.html 中,可以这样访问嵌套的值:

<p>The value is {{ outer.inner }}</p>

这将输出“The value is 42”。DTL能够处理对象、字典、列表等多种嵌套结构。

处理URL参数

最后,我们看一个处理来自URL的参数的例子。假设URL模式捕获了一个名为 guess 的参数。

# urls.py 中的路径可能类似:path(‘game/<int:guess>/‘, views.GameView.as_view())
class GameView(View):
    def get(self, request, guess):
        # guess 参数已被路径转换器转为整数
        context = {‘guess‘: guess}
        return render(request, ‘myapp/game.html‘, context)

在模板 game.html 中,我们可以根据这个猜测值进行逻辑判断:

<p>Your guess is {{ guess }}.</p>
{% if guess > 42 %}
    <p>Too high!</p>
{% elif guess < 42 %}
    <p>Too low!</p>
{% else %}
    <p>Exactly right!</p>
{% endif %}

模板继承 🧩

上一节我们介绍了DTL的基本语法,本节中我们来看看如何通过模板继承来高效地组织代码。

模板继承允许你创建一个基础“骨架”模板,其中包含网站的通用结构(如页头、页脚、导航栏),并定义一些可以由子模板填充的“块”(block)。

以下是基础模板 base.html 的示例:

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My Site{% endblock %}</title>
</head>
<body>
    <div id=“sidebar”>
        {% block sidebar %}
        <ul><li><a href=“/”>Home</a></li></ul>
        {% endblock %}
    </div>
    <div id=“content”>
        {% block content %}{% endblock %}
    </div>
</body>
</html>

子模板 child.html 使用 {% extends “base.html” %} 来继承基础模板,并可以覆盖或填充其中定义的块:

{% extends “base.html” %}

{% block title %}My Amazing Page{% endblock %}

{% block content %}
    <h1>Welcome!</h1>
    <p>This is the content of the child template.</p>
{% endblock %}

这样,child.html 生成的页面将具有 base.html 的整体结构,但标题和主要内容区域被替换为子模板中定义的内容。这种方式极大地减少了代码重复,提高了可维护性。

总结

本节课中我们一起学习了Django模板语言(DTL)的核心知识。我们了解了如何使用 {{ }} 进行安全的变量替换,以及如何通过 safe 过滤器在必要时输出原始HTML。我们探索了 {% %} 标签的强大功能,包括实现条件逻辑(if/else)、遍历数据(for)、生成URL以及最重要的——通过模板继承来构建可复用的页面布局。

DTL语法直观,与Python相似,并且将安全置于首位(默认自动转义),是开发Django应用程序时生成动态内容的可靠工具。掌握DTL是成为高效Django开发者的关键一步。

041:Django模板继承 🧩

在本节课中,我们将要学习Django中一项强大的技术——模板继承。这项技术能帮助我们避免重复编写HTML代码,让Web应用的开发更加高效和整洁。

概述

模板继承是Django模板系统的核心功能之一。它的核心思想与面向对象编程中的“继承”概念相似:创建一个基础模板(父模板),其中包含网站中多个页面共有的部分(如导航栏、页脚),然后让其他页面模板(子模板)继承这个基础模板,并只定义各自独特的内容部分。这样,我们就能实现“一次编写,多处使用”,极大地提升了代码的可维护性。

模板继承的工作原理

上一节我们介绍了如何使用简单的模板来渲染动态数据。本节中我们来看看如何通过继承来组织这些模板。

模板渲染的基本流程是:提供一些渲染数据和一份模板,Django将它们结合生成最终的HTML输出。模板继承则在这个流程中引入了一个“基础模板”的概念。

最终,子模板和基础模板会组合成一个“终极模板”,再与渲染数据结合,产生渲染后的输出。

为何需要模板继承?

设想一下,我们有许多页面,每个页面顶部都需要一个相同的导航菜单。这个菜单可能包含40到50行HTML代码。我们肯定不希望在每个页面的模板中都重复编写这几十行代码。

以下是使用模板继承的主要优势:

  • 避免重复:将公共部分(如<head>、导航栏、页脚)放在基础模板中。
  • 易于维护:修改公共部分时,只需改动基础模板一处,所有继承它的页面都会自动更新。
  • 结构清晰:页面模板只关注自身独特的内容,逻辑更清晰。

实践:创建基础模板与子模板

让我们通过一个简单的例子来实践。假设我们有一个简单的页面模板 condo.html,它包含完整的HTML结构。

现在,我们希望创建多个拥有相同框架(如<html><head><body>标签)但内容不同的页面。做法是:

  1. 创建基础模板:将重复的部分(框架)提取出来,保存为一个独立的模板文件,例如 base.html
  2. 创建子模板:为每个具体页面创建模板,例如 cond2.html。在这个子模板中,我们使用 {% extends “base.html” %} 标签来声明继承关系。
  3. 定义可替换块:在基础模板中,使用 {% block content %}{% endblock %} 这样的标签来标记出允许子模板填充内容的位置。在子模板中,用同样的 block 名称来包裹自己独特的内容。

其核心思想是:我们将一个完整的大模板拆分为可重用的部分(放在基础模板)和不可重用的部分(放在子模板的块中)。然后,我们就能拥有许多从一个“基础”衍生出来的不同页面。

在面向对象编程中,我们常称其为“基类”。在这里,base.html 就是我们的基础,我们将在其之上构建其他内容。

代码示例:视图与模板的配合

以下是实践中如何工作的。我们有一个视图函数(或基于类的视图)来处理请求。


views.py 中,我们像之前一样准备上下文数据并渲染模板:

# views.py 示例片段
def game2(request, guess):
    context = {‘guess’: guess}
    return render(request, ‘cond2.html’, context)

关键在于,Python视图代码完全不知道基础模板 base.html 的存在。它只负责渲染 cond2.html

继承关系只存在于模板层面。在 cond2.html 模板文件中,通过 {% extends “base.html” %} 这行代码声明:“嘿,先去抓取 base.html,然后扩展它。”

理解渲染顺序

它的工作方式初看可能有点反直觉。实际上,渲染过程是这样的:

  1. Django 首先读取子模板 cond2.html
  2. 发现 {% extends %} 标签后,去加载基础模板 base.html
  3. 将子模板中 {% block content %}{% endblock %} 之间的所有内容,填充到基础模板中同名的 block 区域。
  4. 用子模板的内容替换掉基础模板中的块之后,就形成了最终的完整模板。
  5. 最后,Django 将这个渲染好的结果作为 HTTP 响应返回。


当我们开始为网站添加导航时,会大量使用这项技术。

总结

本节课中我们一起学习了Django的模板继承。我们了解了它如何借鉴面向对象编程的思想,通过创建基础模板来存放公共内容,再让子模板继承并覆盖特定块,从而实现代码的复用和逻辑的清晰分离。这是构建具有一致外观的网站的强大工具。

接下来,我们将讨论URL映射与反向解析,学习如何在一个页面中生成指向其他页面的链接。

042:反转Django视图与URL 🔄

在本节课中,我们将学习Django中的URL映射与URL反转。这是关于路由的核心概念,它允许我们在页面之间动态生成链接,而不是硬编码URL字符串。

概述

URL反转是Django提供的一组实用功能,用于根据视图的名称和参数动态生成对应的URL。这有助于提高代码的灵活性和可维护性,特别是在组合多个应用程序或重构项目时。


URL反转的基本概念

上一节我们介绍了Django的视图和URL配置。本节中,我们来看看如何避免在代码中硬编码URL路径。

URL反转的核心思想是:当我们在代码中(例如在视图或模板里)需要生成一个指向另一个页面的链接时,我们不直接写入像 /sample/route/second 这样的字符串,而是通过视图的名称来“查找”或“反转”出正确的URL。

这样做的好处是,即使URL模式(定义在 urls.py 中)发生变化,只要视图名称不变,所有引用该视图的链接都会自动更新。

以下是使用URL反转的主要原因:

  • 避免硬编码:提高代码的灵活性。
  • 便于维护:当应用程序被重命名或URL结构改变时,无需手动修改所有链接。
  • 促进代码复用:使应用程序更容易在不同的Django项目之间移植和组合。
  • 便于查找引用:在代码中更容易找到对特定视图的引用。

在模板中使用URL反转

现在,我们来看看如何在HTML模板中使用URL反转功能。

在Django模板中,我们使用 {% url %} 标签来生成URL。其基本语法是传递一个字符串参数,格式通常为 应用名称:视图名称

假设我们有一个名为 route 的应用,其 urls.py 文件如下:

# urls.py (在 route 应用中)
from django.urls import path
from . import views

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/abddbb243fb7959f5f313c296acf81f4_10.png)

urlpatterns = [
    path('first/', views.first_view, name='first_view'),
    path('second/', views.second_view, name='second_view'),
]

在模板中,硬编码的链接看起来像这样:

<!-- 硬编码URL -->
<a href="/sample/route/second">硬编码链接到第二视图</a>

而使用URL反转的链接则更加灵活:

<!-- 使用URL反转 -->
<a href="{% url 'route:second_view' %}">通过反转链接到第二视图</a>

{% url 'route:second_view' %} 会被Django模板引擎替换为正确的URL路径,例如 /sample/route/second。这样,如果我们将来修改了 urls.py 中的路径,模板中的链接会自动更新。


跨应用链接与参数传递

上一节我们看到了在同一应用内进行URL反转。本节中,我们来看看如何链接到其他应用,以及如何处理需要参数的视图。

Django的URL反转机制可以跨应用工作。例如,我们有一个名为 gview 的应用,它包含两个视图:一个用于列出所有猫咪(cats),另一个用于显示特定猫咪的详情(cat),后者需要一个 id 参数。

gview 应用的 urls.py 可能如下所示:

# urls.py (在 gview 应用中)
urlpatterns = [
    path('cats/', views.CatListView.as_view(), name='cats'),
    path('cat/<int:pk>', views.CatDetailView.as_view(), name='cat'),
]

现在,从 route 应用的模板中,我们可以这样生成指向 gview 应用的链接:

<!-- 链接到gview应用的列表视图(无参数) -->
<a href="{% url 'gview:cats' %}">查看所有猫咪</a>

<!-- 链接到gview应用的详情视图(需要参数) -->
<a href="{% url 'gview:cat' 42 %}">查看ID为42的猫咪详情</a>

注意第二个链接,我们向 {% url %} 标签传递了第二个参数 42。Django会将其匹配到URL模式 <int:pk> 的位置,生成类似 /gview/cat/42 的URL。

一个重要的优点是,Django会在反转时进行一些基本的错误检查。例如,如果忘记给需要参数的视图提供参数,或者提供了错误类型的参数,Django通常会给出明确的错误提示,这有助于在开发早期发现问题。


命名空间

为了进一步避免不同应用间的视图名称冲突,Django引入了URL命名空间的概念。

命名空间通常在项目的根 urls.py 中定义,当引入(include)某个应用的URL配置时,可以为其指定一个命名空间。

# 项目的根 urls.py
from django.urls import path, include

urlpatterns = [
    # 为‘route’应用指定命名空间‘ns_route’
    path('sample/route/', include(('route.urls', 'route'), namespace='ns_route')),
]

定义命名空间后,在模板中引用该应用下的视图时,就需要使用命名空间前缀:

<!-- 使用命名空间引用视图 -->
<a href="{% url 'ns_route:second_view' %}">通过命名空间链接</a>

这提供了另一层抽象,使得即使两个不同的应用有同名的视图,或者你想为同一个应用提供不同的引用入口,也能清晰地区分开。


在Python代码中使用URL反转

除了在模板中,我们也经常需要在Python代码(如视图、模型或表单)中动态生成URL。这时我们可以使用 django.urls 模块中的 reverse()reverse_lazy() 函数。

reverse() 用于在URLconf已加载的上下文中(如视图函数内)立即反转URL。
reverse_lazy() 是一个惰性求值版本,常用于类属性或模型方法等URLconf可能尚未加载的场景。

以下是在视图函数中使用 reverse_lazy() 的示例:

# views.py
from django.urls import reverse_lazy
from django.views.generic import View
from django.shortcuts import render

class MyView(View):
    def get(self, request):
        # 反转URL并将结果字符串存入上下文
        url_to_cats = reverse_lazy('gview:cats')
        url_to_dog_detail = reverse_lazy('gview:dog', args=[42]) # 传递参数

        context = {
            'link1': url_to_cats,
            'link2': url_to_dog_detail,
        }
        return render(request, 'my_template.html', context)

在以上代码中,reverse_lazy('gview:cats') 会生成指向猫咪列表页的URL字符串,而 reverse_lazy('gview:dog', args=[42]) 会生成指向ID为42的狗狗详情页的URL字符串。这些字符串可以被传递到模板中或用于其他逻辑。


总结

本节课中我们一起学习了Django的URL反转机制。

我们了解到,URL反转是一种通过视图名称和参数动态生成URL的强大工具。它在模板中通过 {% url %} 标签使用,在Python代码中通过 reverse()reverse_lazy() 函数调用。这种方法避免了在代码中硬编码URL路径,从而大大提升了项目的可维护性、灵活性和代码复用能力。

我们还探讨了如何跨应用生成链接、如何向URL传递参数,以及如何使用命名空间来组织URL,防止名称冲突。

接下来,我们将运用这些知识,开始学习Django的通用视图,它们能让我们用极少的代码快速构建列表页、详情页、创建页、更新页和删除页,从而更高效地开发Web应用。

043:佛罗里达州奥兰多站

在本节课中,我们将回顾密歇根大学《给所有人的Django课程》在佛罗里达州奥兰多举行的一次面对面办公时间。本节内容并非技术教学,而是通过分享往期学生的真实经历与感悟,为初学者提供学习动力和社区支持。

概述

本次办公时间在佛罗里达州奥兰多举行,课程讲师查克博士(Dr. Chuck)邀请了几位已完成或正在学习该系列课程的学生进行分享。他们介绍了自己的学习背景、学习体验以及课程带来的积极影响。

学生分享

以下是几位参与学生的自我介绍与学习心得。

  • 埃莉诺·麦考伊·卡特:她正在学习该系列的第一门课程,并表示非常喜欢Python,认为这是一个很棒的项目。
  • 乔·哈:他刚刚完成第二门课程,并鼓励大家坚持下去,承诺“坚持下去,终有回报”
  • 莱克西·卡森:她在约一年前完成了全部课程,并指出这门课程对她的职业生涯产生了巨大的积极影响。她目前是一名高级数据科学家,并向所有希望进入该领域的人强烈推荐此课程。
  • 查·穆卡:他正处于职业转型期,在了解到查克博士的Python课程后,决定开始学习。
  • 达伦·波伊克:他已经在Coursera上学习了Python课程,并计划使用查克博士的《Python3》教材为当地高中生开发一门STEM(科学、技术、工程、数学)课程。
  • 亨特:他简短有力地推荐道:“查克博士很棒,选这门课吧。”

社区互动与未来展望

在分享环节之后,查克博士与达伦就其为高中生开发STEM课程的计划进行了交流。查克博士对此表示高度期待,并幽默地表示可能需要为此再次造访奥兰多。这体现了该学习社区不仅关注个人技能提升,也致力于知识的传播与教育的拓展。

最后,查克博士预告了未来的活动安排,可能会前往布莱切利园,并期待与大家在网络上的其他地方再见。

总结

本节课中,我们一起聆听了来自佛罗里达州奥兰多办公时间的学生分享。这些真实的经历表明,系统性地学习Python和Django能够有效助力个人职业发展,无论是转型、晋升还是从事教育传播。他们的故事为所有初学者提供了坚持学习的动力和信心。记住核心建议:“坚持下去,终有回报”。学习之旅中,你并不孤单,有一个活跃的社区在共同前进。

044:荷兰布雷达面对面办公时间回顾

在本节课中,我们将回顾密歇根大学《Django for Everybody》课程在荷兰布雷达举行的一次特殊面对面办公时间。本次会议汇集了来自世界各地的学生,他们分享了学习Python和Django的经历与感受。


查克刚刚在荷兰布雷达结束了又一次精彩的面对面办公时间。他的儿子正在这里参加国际地板球锦标赛。多位学生出席了本次活动,查克邀请他们进行自我介绍并谈谈对课程的体会。

以下是部分学生的分享:

  • 学生一:我名叫霍尔。我很喜欢学习你的课程。
  • 学生二:我是杰里,来自比利时。我热爱Python。我甚至在肯塔基州纹了一个Python语言的纹身,因为我非常喜欢Python,也非常享受你的课程。
  • 学生三:我是罗杰。我的故事没那么精彩,我没有Python纹身。我在荷兰。感谢你的课程帮助我入门Python,它确实起到了这个作用。
  • 学生四:我是贝基。我很感激几年前学习了这门课程,它让我开始了计算机科学的学习。我从一名法律专业学生转行到了技术领域。
  • 学生五:我是亚历克斯。你的课程帮助我开始了Python学习,我非常喜欢,感谢你做的一切。
  • 学生六:我是瑞维卡。我和我的老板以及另一位同事一起学习了你的课程。课程非常棒。
  • 学生七:我是丹妮拉。我非常感谢查克的启发,两年前我开始学习Django,并且打算尽快开始教授Django。
  • 学生八:我是詹娜。我和贝基一起开始了Python课程,现在我已经转向了PHP,并且本周早些时候刚刚毕业。我也有一个很酷的纹身,是吃豆人图案。

查克感谢大家的参与,并提到七月他将在美国各地旅行,届时可能会再次与大家见面。


上一节我们听到了普通课程学生的分享,本节中我们来看看一个特别的场景。查克身处荷兰布雷达的残奥会比赛现场,他的儿子正在美国轮椅地板球队效力。出乎意料的是,比利时国家队中有一位他的课程学生。

查克邀请了托马斯和他的比利时轮椅地板球队队友们进行介绍。

以下是比利时队部分成员的介绍:

  • 托马斯:我名叫托马斯,34岁。我和我的比利时地板球队队友们在一起。我一直学习你的Python课程,它非常棒,讲解清晰,练习很好,我从中受益匪浅,非常感谢。
  • 迈克尔:我是迈克尔,来自比利时,住在布雷达,36岁。
  • 迪特尔:我是迪特尔,我接下来可能也会学习你的Python课程,很期待。
  • 队员四:我27岁,是比利时国家队的一名守门员。

由于即将进行比利时对阵美国的比赛,队员们需要参加赛前会议,介绍匆匆结束。查克表示会在视频中加入一些比美比赛的片段。


本节课中我们一起回顾了一次特别的线下交流活动。我们看到了来自不同背景的学生如何通过《Django for Everybody》课程开启编程之旅,并将学习热情延伸到生活的其他领域,例如体育团队中。这体现了编程学习的广泛影响力和社区交流的价值。

045:Python对象术语入门 🐍

在本节课中,我们将学习面向对象编程(OOP)的核心术语。我们的目标不是立即学会如何编写复杂的对象,而是理解诸如“类”、“对象”、“方法”、“属性”等关键概念。这对于阅读Python文档和使用各种库(如数据库操作库或BeautifulSoup)至关重要。


程序的传统视角

上一节我们提到了学习术语的重要性,本节我们先回顾一下对程序的传统理解。

一个经典的程序模型是输入-处理-输出。例如,一个将欧洲电梯楼层转换为美国电梯楼层的程序。在这个模型中,我们使用变量、逻辑、算法和循环等一系列步骤来实现目标。

代码示例:

# 传统的过程式编程示例:楼层转换
euro_floor = input('请输入欧洲楼层: ')
us_floor = int(euro_floor) + 1
print('对应的美国楼层是:', us_floor)

面向对象视角

实际上,从学习Python的第一天起,我们就在使用对象。字符串、列表、字典都是对象。

在面向对象视角下,程序由许多对象组成。数据在这些对象之间流动。每个对象都是一个自包含的小单元,内部包含代码(方法)和数据(属性)。通过将复杂问题分解为多个可以独立开发和交互的对象,能使问题变得更简单。

例如,一个字符串对象内部既有字符数据,也有像 .upper().strip() 这样的方法代码。


对象的边界与封装

面向对象模式的一个优点是它建立了清晰的边界

  • 对象的创建者负责构建内部功能,并提供一个接口供外部使用。创建者无需关心对象外部如何使用它。
  • 对象的使用者通过接口与对象交互,获取数据或调用功能,而无需了解对象内部的复杂实现细节。

这种“隔离墙”对编写对象和使用对象的程序员都非常有益,是一种强大的设计模式。


核心术语定义

现在,我们来正式定义一些核心术语。理解这些词汇是阅读文档和进行交流的基础。

以下是关键术语及其解释:

    • 定义:类是创建对象的模板或蓝图。它定义了未来对象将拥有哪些数据(属性)和代码(方法)。
    • 类比:就像饼干模具。模具本身不是饼干,但它定义了饼干的形状。类本身不是一个具体的对象。
    • 公式/概念类 = 模板
  • 对象 / 实例

    • 定义:对象是类的一个具体实例。它是根据类的模板创建出来的、实际存在于内存中的实体。
    • 类比:用同一个饼干模具压出来的每一块饼干都是一个独立的实例。根据“狗”这个类,可以创建出名为“Lassie”的具体狗对象。
    • 公式/概念对象 = 类的实例
  • 方法

    • 定义:方法是定义在类内部的函数。它是对象能够执行的操作行为
    • 说明:它本质上是一个函数,但其作用域被限定在所属的类或对象内部。在某些语境下,调用方法也被称为向对象“发送消息”。
    • 代码示例:字符串的 .upper() 就是一个方法。

  • 属性 / 字段
    • 定义:属性是存储在对象内部的数据。每个对象都有自己独立的属性值。
    • 说明:“属性”和“字段”这两个术语经常互换使用,都指对象内部的数据变量。
    • 代码示例:一个“狗”对象可能拥有 nameagebreed 等属性。


总结

本节课中,我们一起学习了面向对象编程的基础术语。我们了解到是创建对象的模板,对象是类的具体实例。对象封装了数据(属性)和操作(方法),并通过清晰的边界与程序的其他部分交互。虽然作为初学者可能还不需要编写复杂的类,但理解这些概念对于有效使用Python强大的内置对象和第三方库至关重要。在接下来的课程中,我们将看到这些概念在实际代码中的应用。

046:Python对象与类

在本节课中,我们将学习Python中面向对象编程的核心概念:类和对象。我们将通过一个简单的例子来理解类如何作为模板,以及如何创建和使用对象实例。


概述:类与对象的关系

上一节我们介绍了面向对象编程的基本定义。本节中,我们来看看如何用Python代码实现这些概念。

类(Class)是对象的蓝图或模板,它定义了对象将拥有的数据(属性)和行为(方法)。对象(Object)是类的具体实例。你可以把类想象成饼干模具,而对象就是用这个模具压出来的每一块饼干。

定义第一个类

以下是创建一个简单类的Python代码:

class PartyAnimal:
    x = 0

    def party(self):
        self.x = self.x + 1
        print("So far", self.x)

我们来分解这段代码:

  • class 是一个关键字,用于定义一个新的类,类似于定义函数时使用的 def
  • PartyAnimal 是我们为这个类选择的名称。
  • 冒号 : 表示一个缩进代码块的开始。
  • 在类内部,我们通常定义两样东西:
    1. 数据(属性):例如 x = 0,这看起来像一个赋值语句。
    2. 行为(方法):例如 def party(self):,这看起来像一个函数定义。定义在类内部的函数称为“方法”。

方法中的 self 参数是一个关键概念。它是在类内部代码中,用来引用对象自身(即实例)的方式。当方法被调用时,self 会自动成为调用该方法的对象的别名。

创建和使用对象

定义了类之后,我们还需要用它来创建对象。这个过程称为“实例化”或“构造”。

an = PartyAnimal()

这行代码的意思是:使用 PartyAnimal 这个模板(类)来创建一个新的对象,并将这个对象赋值给变量 an。现在,an 就是一个 PartyAnimal 类型的对象。

创建对象后,我们可以调用它的方法:

an.party()
an.party()
an.party()

当我们调用 an.party() 时,Python 内部实际上执行的是 PartyAnimal.party(an)self 参数就成为了 an 的别名。因此,在 party 方法内部,self.x 指的就是 an 这个对象自己的 x 属性。

让我们跟踪一下代码的执行过程:

  1. 构造 PartyAnimal 对象,其内部属性 x 被初始化为 0
  2. 第一次调用 an.party()self.x(即 an.x)从 0 变为 1,打印 “So far 1”。
  3. 第二次调用:x1 变为 2,打印 “So far 2”。
  4. 第三次调用:x2 变为 3,打印 “So far 3”。

我们一直在使用对象

实际上,从学习Python开始,我们就一直在使用对象。点运算符 . 就是用来访问对象内部属性或方法的操作符。

以下是几个我们熟悉的例子:

x = list() # 构造一个空的列表对象
print(type(x)) # 输出:<class 'list'>
print(dir(x)) # 查看列表对象的所有方法和属性

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/7ac3ba1234133d5e81fca1b0a2c35d56_13.png)

y = ‘hello there‘ # 构造一个字符串对象
print(y.upper()) # 调用字符串对象的 upper 方法
  • list()‘hello there‘ 都是在构造特定类型的对象(列表和字符串)。
  • x.sort()y.upper() 都是在调用对象的方法。
  • type() 函数告诉我们一个对象的类型(即它是由哪个类创建的)。
  • dir() 函数可以列出一个对象的所有可用属性和方法。

检查自定义类的对象

对于我们自己创建的类,同样可以使用这些工具进行检查:

an = PartyAnimal()
print(type(an)) # 输出:<class ‘__main__.PartyAnimal‘>
print(dir(an)) # 你会看到 ‘x‘ 和 ‘party‘ 在列表中

type(an) 的输出表明 anPartyAnimal 类的一个实例。dir(an) 的结果会显示我们定义的两个成员:属性 x 和方法 party,以及其他一些Python自动添加的内部方法(通常以双下划线 __ 开头)。

我们可以使用点运算符来访问它们:

  • an.x:访问对象的 x 属性。
  • an.party():调用对象的 party 方法。

总结

本节课中我们一起学习了Python中类和对象的核心机制。我们了解到:

  1. 是创建对象的模板,使用 class 关键字定义。
  2. 对象是类的实例,通过像调用函数一样使用类名来创建(例如 PartyAnimal())。
  3. 类内部包含属性(数据)和方法(函数)。
  4. 类方法中的第一个参数 self 代表对象实例本身。
  5. 点运算符 . 用于访问对象的属性和方法。
  6. 我们一直在使用的数据类型(如列表、字符串、字典)本质上都是对象,我们一直在调用它们的方法。

下一节,我们将探讨对象的生命周期,即它们是如何被创建和销毁的。

047:对象生命周期与继承

在本节课中,我们将要学习Python对象的生命周期,包括对象的创建与销毁过程,并初步了解面向对象编程中一个重要的概念——继承。

对象生命周期 🧬

上一节我们介绍了对象的基本概念和方法。本节中我们来看看对象的生命周期。对象生命周期指的是对象从创建到被销毁的整个过程。我们作为对象的创建者,可以在对象创建和销毁的时刻插入特定的代码来执行初始化或清理工作。

为此,我们使用两个特殊的函数:构造函数析构函数。我们通常不直接调用它们,而是由Python在特定时刻自动调用。

  • 构造函数:在对象被创建时自动调用,常用于设置变量的初始值。它更为常用。
  • 析构函数:在对象被销毁时自动调用,用于执行清理工作,但实际使用频率较低。

以下是构造函数和析构函数的一个代码示例:

class PartyAnimal:
    x = 0

    def __init__(self):
        print('I am constructed')

    def party(self):
        self.x = self.x + 1
        print('So far', self.x)

    def __del__(self):
        print('I am destructed', self.x)

an = PartyAnimal()
an.party()
an.party()
an = 42
print('an contains', an)

运行这段代码,输出如下:

I am constructed
So far 1
So far 2
I am destructed 2
an contains 42

代码执行过程如下:

  1. an = PartyAnimal() 这行代码创建了一个PartyAnimal对象,并自动调用了构造函数 __init__,打印出“I am constructed”。
  2. 随后两次调用 an.party() 方法,增加x的值并打印。
  3. 当执行 an = 42 时,变量an不再指向PartyAnimal对象,而是指向整数42。此时,原来的PartyAnimal对象不再被引用,Python在销毁它之前会自动调用其析构函数 __del__,打印出“I am destructed 2”。
  4. 最后打印an的值,确认它现在是整数42

这个例子展示了我们如何介入对象的创建和销毁时刻。需要注意的是,如果你像上面那样覆盖了一个变量,就相当于丢弃了原来的对象。

多个对象实例 🧩

到目前为止,我们创建了一个类,然后只创建它的一个实例(对象)。更有趣的情况是,当我们创建同一个类的多个实例,并将它们存储在不同的变量中时,每个实例都拥有自己独立的实例变量副本。

让我们看看下面的代码,这里我们移除了析构函数,并为构造函数添加了参数:

class PartyAnimal:
    x = 0
    name = ''

    def __init__(self, z):
        self.name = z
        print(self.name, 'constructed')

    def party(self):
        self.x = self.x + 1
        print(self.name, 'party count', self.x)

s = PartyAnimal('Sally')
s.party()

j = PartyAnimal('Jim')
j.party()
s.party()

代码执行过程如下:

  1. s = PartyAnimal('Sally') 创建第一个实例,构造函数参数 z 接收字符串 'Sally',并将其赋值给实例变量 self.name。这个实例存储在变量 s 中。
  2. 调用 s.party()s 实例的 x 值变为 1。
  3. j = PartyAnimal('Jim') 创建第二个独立的实例,self.name 被设置为 'Jim',存储在变量 j 中。
  4. 调用 j.party()j 实例的 x 值变为 1。
  5. 再次调用 s.party()s 实例的 x 值从1增加到2。

关键点在于,每次使用 new 关键字(在Python中是直接调用类名)进行构造时,都会创建一套全新的实例变量。因此,s.xj.x 是两个完全独立的变量。我们拥有了两个对象:一个在变量 s 中,一个在变量 j 中,它们拥有各自独立的实例变量副本。

继承简介 🧬➡️🧬

接下来我们将讨论继承。继承是指以一个已有的类为基础,扩展其功能以创建新类的思想。这允许新类(子类)获取现有类(父类)的属性和方法,并可以添加或覆盖特定的功能。我们将在后续课程中详细探讨这个概念。


本节课中我们一起学习了Python对象的生命周期,理解了构造函数和析构函数的作用,实践了如何创建同一个类的多个独立实例,并初步认识了面向对象编程的核心概念——继承。掌握这些知识是构建更复杂、模块化程序的基础。

048:继承与核心概念回顾 🧬

在本节课中,我们将要学习面向对象编程的最后一个核心概念:继承。这是一种代码复用的高级形式。我们还会系统回顾之前介绍过的所有核心术语,帮助你建立完整的知识框架。


概述

本节将介绍继承的概念,展示如何通过扩展现有类来创建新类,从而实现代码复用。随后,我们将回顾类、对象、方法、构造函数等核心术语的定义,并解释你其实从一开始就在使用对象和方法。


继承:代码复用的高级形式

上一节我们介绍了类和对象的基本构建。本节中,我们来看看如何通过继承来复用和扩展现有代码。

继承的核心思想是:不从零开始创建新类,而是以一个现有类为基础进行扩展。这被称为子类化扩展。原有类称为父类,新类称为子类。这种关系就像“父与子”,子类继承了父类的所有特性,并可以添加自己的新特性。

当你设计对象和对象层次结构时,经常会用到继承。它是一种非常巧妙的代码复用方式。不过,目前你只需要理解这个概念和术语,不必深究何时或为何使用它。


继承代码示例

让我们通过代码来理解继承。以下是之前用过的 PartyAnimal 类:

class PartyAnimal:
    x = 0
    name = ""
    def __init__(self, nam):
        self.name = nam
        print(self.name, "constructed")
    def party(self):
        self.x = self.x + 1
        print(self.name, "party count", self.x)

现在,我们通过继承来创建一个新的 FootballFan 类:

class FootballFan(PartyAnimal):
    points = 0
    def touchdown(self):
        self.points = self.points + 7
        self.party()

以下是关键点解析:

  • class FootballFan(PartyAnimal): 这行代码表示 FootballFan继承PartyAnimal 类。括号内的 PartyAnimal 指明了父类。
  • 这意味着 FootballFan 自动拥有 PartyAnimal 的所有属性和方法,包括 xname__init__party
  • 然后,我们在子类中添加了新的属性 points 和新的方法 touchdown

运行继承后的代码

让我们看看继承如何在实际运行中生效。

首先,创建一个普通的 PartyAnimal 对象:

s = PartyAnimal("Sally")
s.party()

输出会是:

Sally constructed
Sally party count 1

现在,创建一个 FootballFan 对象:

j = FootballFan("Jim")
j.party()
j.touchdown()

以下是执行过程:

  1. j = FootballFan("Jim"):由于 FootballFan 自身没有 __init__ 方法,它使用了从 PartyAnimal 继承来的构造函数。因此,它会设置 name="Jim"x=0,并打印 “Jim constructed”。同时,FootballFan 独有的 points 属性也被初始化为 0
  2. j.party():调用从父类继承来的 party 方法,将 x0 增加到 1,并打印 “Jim party count 1”。
  3. j.touchdown():调用子类新增的方法。它将 points0 增加到 7,然后通过 self.party() 再次调用继承来的 party 方法,将 x1 增加到 2,并打印 “Jim party count 2”。

通过继承,我们复用了 PartyAnimal 的所有功能,并轻松地为 FootballFan 添加了足球迷特有的行为。


核心概念回顾 📚

现在我们已经介绍了继承,让我们系统地回顾一下面向对象编程的所有核心术语。理解这些术语对后续学习至关重要。

以下是所有关键概念的定义:

  • :一个模板或蓝图。它本身不是一个具体的东西,而是定义了某种东西的形态。我们通过类来声明:当创建这个类的实例时,它将包含哪些变量和方法。
    • 类比:类就像是制作雪人饼干的模具

  • 属性:类内部定义的变量。

  • 方法:定义在类内部的函数。

  • 对象:当我们根据类这个模板实际“构造”出一个具体实例时,得到的就是对象。一个对象包含了类中定义的数据(属性)和行为(方法)。

    • 类比:对象就是用那个模具压出来的一个个雪人饼干
  • 构造函数:一个特殊的方法(在Python中是 __init__),用于在对象首次被创建时,对其进行初始化设置。

  • 继承:创建新类的能力,但这个新类可以导入并获取一个现有类的所有功能(属性和方法)。这实现了高效的代码复用。


你早已在使用对象和方法

面向对象编程非常强大。在本课程剩下的部分,我们虽然不会亲自去编写新的类,但会一直使用对象

事实上,你从这门课的一开始就一直在使用对象和方法了:

  • 当你写 x = “hi” 时,x 就是一个字符串对象
  • 当你写 x.upper() 时,你就是在调用字符串对象的 upper 方法
  • 当你写 fh = open(‘file.txt’) 时,fh 就是一个文件对象
  • 当你写 fh.read() 时,你就是在调用文件对象的 read 方法

那个小小的 . 运算符,就是用来访问对象内部属性和调用其方法的。所以,我现在终于向你解释了,当我说“调用read方法”或“调用upper方法”时,到底是什么意思,以及那个点号为什么在那里。


总结

本节课中我们一起学习了:

  1. 继承:通过扩展现有类(父类)来创建新类(子类)的机制,是一种高级的代码复用方式。
  2. 核心术语回顾:我们明确了对象属性方法构造函数继承的准确定义。
  3. 实践认知:认识到你从一开始就在使用对象和方法(例如字符串操作、文件处理),. 运算符是访问它们的关键。

掌握这些概念需要时间。你可能要很久之后才会遇到一个足够复杂的问题,使得创建新类成为解决方案的一部分。但当你需要时,面向对象编程会是一个非常强大的工具。现在,你只需要理解这些术语和基本概念,为未来的深入学习打下基础。


感谢学习,我们下次再见!

049:Python赋值语句的真实故事

在本节课中,我们将要学习Python中变量和赋值语句的真实工作原理。我们将从一个常见的简化模型开始,然后深入探讨Python内部实际使用的“指针”或“引用”模型,并通过代码示例来理解其行为。

概述

许多入门教程,包括我自己的《Python for Everybody》课程,都使用了一个简化的“抽屉”模型来解释变量。这个模型虽然易于理解,但并未准确反映Python在内存中处理变量的真实方式。本节课程将揭示这个真相,解释Python变量实际上是对象的引用,并展示这如何影响我们的代码。

从简化的“抽屉”模型说起

上一节我们提到了一个常见的入门解释。以下是该模型的核心观点:

  • 变量是内存中一个被命名的位置,程序员可以在此存储数据,之后通过变量名来检索。
  • 程序员可以选择变量名。它们就像是内存位置的标签。
  • 可以在后续语句中更改变量的内容。

例如,x = 12 就像在一个标签为 x 的抽屉里放入数字12。x = 14 则用数字14替换了抽屉里的内容。

揭示真相:Python变量是对象的引用

然而,上述“抽屉”模型并不完全正确。本节中我们来看看Python内部实际发生的情况。

一个Python变量是一个符号名称,它是一个指向某个对象的指针引用

我们可以使用内置的 id() 函数来查看变量指向的内存地址。以下代码演示了这一点:

x = 42
y = x
print(id(x))  # 输出 x 指向的内存地址
print(id(y))  # 输出 y 指向的内存地址,你会发现它与 id(x) 相同

运行这段代码,你会发现 xyid 是相同的。这说明我们并没有两个独立的“42”,而是有两个指针 xy 指向了内存中同一个 42 对象。

当我们执行 x = 2000 时,并不是改变了原来那个“42”抽屉里的内容,而是让 x 这个指针指向了内存中另一个新创建的 2000 对象。原来的 42 对象可能还在内存中(例如在“空闲列表”上),但 x 已经不再指向它了。

同样,y = x 这个语句并不是将 x 指向的对象复制一份给 y,而是让 y 也指向 x 当前所指的同一个对象地址。

复杂对象(如列表)的行为

对于列表这样的可变对象,引用模型带来的影响会更加明显。

x = []          # 创建一个空列表对象,x 指向它
print(id(x))    # 输出列表对象的地址
x.append('hello') # 修改列表对象的内容
print(id(x))    # 地址没有改变,x 仍然指向同一个列表对象

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/abbafe424d25d07754c8bba4f1a6d19d_23.png)

y = x           # y 指向与 x 相同的列表对象
x.append('world')
print(y)        # 输出:['hello', 'world'],因为 y 和 x 指向同一个列表

这里,y = x 并没有创建列表的副本,它只是增加了一个指向同一列表的新引用 y。因此,通过 x 修改列表,通过 y 也能看到变化。

如何创建真正的副本

有时我们需要一个对象的独立副本。以下是创建列表副本的方法:

x = ['hello']
print(id(x))

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/abbafe424d25d07754c8bba4f1a6d19d_29.png)

# 使用 list() 构造函数创建副本
y = list(x)     # 遍历 x 中的元素,创建一个包含相同元素的新列表对象
print(id(y))    # y 的地址与 x 不同,它是一个独立的对象

x.append('world')
print(x)        # 输出:['hello', 'world']
print(y)        # 输出:['hello'],y 不受影响

y = list(x) 这种方式被称为复制构造函数。它会创建一个全新的列表对象,并将原列表中的元素复制过来,从而使 y 成为一个独立的副本。

为何存在简化模型

像C、Fortran这类语言,其变量模型更接近“抽屉”模型,变量名直接对应一块固定的内存空间。Python底层是用C语言实现的,在C代码中,明确区分指针和指针所指的数据是非常自然的。Python将“指针”的概念对初学者隐藏了起来,以降低入门门槛。但理解其引用本质,对于掌握Python的高级特性(如可变对象传递、浅拷贝与深拷贝等)至关重要。

总结

本节课中我们一起学习了Python赋值语句的真实故事。

  • Python变量不是存储数据的“抽屉”,而是指向内存中对象的引用(指针)。
  • y = x 这样的赋值语句,是让 y 指向 x 所指向的同一个对象,而非创建副本。
  • 对于可变对象(如列表),多个引用指向同一对象会导致修改相互影响。
  • 可以使用 id() 函数查看对象的内存地址,辅助理解引用关系。
  • 需要独立副本时,应使用复制构造函数(如 list())或其他拷贝方法。

理解这个“引用模型”是深入掌握Python编程的关键一步。虽然入门时的简化模型有助于快速上手,但了解真相能让你写出更准确、更高效的代码。

050:C++的设计哲学与应用领域

在本节课中,我们将学习C++语言的设计初衷、核心优势以及其最适合的应用领域。我们将探讨为何C++在特定类型的软件开发中至关重要,以及开发者应如何根据项目需求选择合适的工具和技术。


🎯 C++的核心应用领域

C++的发明者Bjarne Stroustrup试图界定C++特别适用的领域。他思考了最初构建C++的目的,以及经过近30年发展后,其优势所在。同时,他也探讨了C++的局限性。

他认为C++有一个核心或主要的应用领域,这通常被称为传统的“系统编程”,但这个术语并不完全准确。因为这更多是一种编程语言风格和编程范式。

🏗️ 基础设施软件的定义

为了更精确地描述,他提出了“基础设施”软件的概念。他大致将其定义为:如果它崩溃,就会有人受伤或遭受重大损失。这类软件是我们系统的基础,必须可靠运行,社会才能正常运转。

他试图阐明在这些领域中什么是最重要的,并总结出几个关键概念:

  • 紧凑的数据结构
  • 为了可维护性和最小化错误而设计的强类型接口
  • 相较于随机代码,更强调算法,因为我们需要可靠性和可分析性,并确保其正确性。

他撰写论文的出发点正是基于这种思考:对于必须可靠的基础设施软件,什么才是正确的编程风格,以及我们需要什么样的语言特性来支持这种风格。

⚠️ 可靠性的现实案例

我们可以找到真实的例子。例如,一些现代AI大脑系统的核心、我们手机系统的基础、汽车中的刹车系统。问题在于:我们如何使这些系统可靠?

如何确保太空探测器在前往火星的半路上不会出现逻辑上等同于“蓝屏死机”的故障?毕竟我们无法派维修人员去修理。如何确保它们能进入正确的轨道?

美国喷气推进实验室(JPL)曾丢失过一个火星探测器,原因是两个团队沟通出现了问题。他们以为沟通顺畅,但实际上,一个团队使用英制单位,另一个使用国际单位制(SI)。结果导致了导航错误,使价值数亿美元的设备进入了错误的轨道。这不是一个好结果。这是200名优秀工程师毕生工作的心血,付诸东流。

只需在程序各部分之间的接口上做极其微小的改进,就可以避免此类问题。因此,他对这类事情非常感兴趣。

🗺️ C++的适用光谱

所以,存在一个核心领域,C++提供的设施(尽管不完美)以及他正在研究的那类东西是必不可少的。

然后存在一个巨大的灰色地带,在那里你有选择。在他看来,C++可能有帮助,但并非必不可少。

最后,还存在一些领域,可能不适合应用那种严格的技术。例如,如果只是为自己编写一个小型应用程序,或者某人试图快速推出一个小的Web应用,他们不需要那种级别的可靠性。也许他所说的那种编程方式并不适合他们。

🔧 技术与工具的多样性

但这里真正重要的是要认识到:存在适用于不同领域的不同技术和不同语言,我们必须认识到这一点。我们不能为所有人使用同一种语言,也不能为所有人使用同一种应用该语言的技术。我们不能只有单一的工具链或单一类型的系统。

由此我们可以更进一步:我们可能也不需要为所有工程师、软件开发人员或任何构建基础设施软件的人提供相同类型的培训或教育。

🎓 不同的角色,不同的思维

例如,开发手机自动软件更新机制的人,与开发一个小游戏的人,需要不同的教育、知识和培训。因为前者一个小小的失误,就可能毁掉数百万人的一整天甚至一整周。如果911呼叫无法接通,甚至可能有人受伤。

这不仅仅是运行在其上的软件,还包括更新软件,以及任何涉及安全关键问题的链条中的东西。但从事这项工作的人,必须与编写小型应用程序(比如快速制作几本书)的人有不同的思维方式。后者本身没有错,但他们确实需要不同的思考方式。

事实上,如果你将这种非常严格的、工程导向的思维应用到小型商业应用上,你可能会晚上市一年,从而失去市场。另一方面,如果你将“唯快不破”的态度应用到我汽车的转向系统上,那可不是个好主意。这些人必须有不同的思维方式。

🛠️ 通过教育塑造思维

而让人们以不同方式思考的途径,就是给予他们不同的教育。我们没有一个“标准程序员”的必要标准,也不应该有。如果有,我们也应该有多个标准。

他认为,在某种意义上,这个领域必须整顿好自己的行为,否则就会有其他人来替我们整顿。

💡 用户自定义字面量:一个改进的例子

有人问到关于“用户自定义字面量”的功能,例如为常量添加后缀(如 s 表示秒),这是他在某个中间阶段添加的功能,还是简单的运算符重载?

他解释道,他们观察到人们正在为各种基本类型发明各种后缀。例如,用 u 表示无符号,用 l 表示长整型(他记不清其他的了)。然后他想,在C++中你几乎可以做任何事情,但你不能创建自己的字面量

同时,他观察到存在一些有效的技术,但人们因为记法太丑陋而不愿使用。他之前展示的“单位”例子就是其中之一。实际上,在过去10年里,库已经可以实现他展示的所有功能(虽然不那么优雅)。例如,费米实验室就有一个不错的库,但它没有得到应有的广泛应用,因为用户不喜欢那种记法。

因此,他们开始研究:如何从根本上清理人们的源代码?如何让代码看起来像理想语言中的样子?如何让代码看起来更像物理教科书中的方程?

📚 让代码像教科书一样清晰

“单位”后缀只是一种让代码看起来像物理教科书方程的方式。每个人在高中物理中都学过:首先确保单位匹配,然后再进行详细计算。那么为什么人们不这样做呢?因为这样做太繁琐,或者当他们这样做时,代码看起来太丑,或者如果使用运行时技术,成本太高。

因此,他和朋友们认为这个问题值得解决。他们尝试找出已有的解决方案,并经历了多次演进。他认为最后的收尾工作是由David完成的。现在这已成为标准功能,虽然目前还没有被广泛实现,但预计一年后这个例子很可能也会在你的计算机上运行。

🚀 编译时计算与常量表达式

有人问,除了后缀,是否还有其他例子让人们能在类内部创建自己的字面量?

他回顾道,当他开始设计C++时,他提供了构造函数,允许你从参数构造特定类型的对象。这非常有效,人们使用构造函数就像使用字面量一样,但它们并不是字面量,存在运行时成本。

因此,在C++11中,他们做的第一件事就是引入常量表达式作为一个更基础的概念。这是他与同事Gabriel Dos Reis合作的工作。

他们引入了常量表达式函数(可在编译时求值)和常量表达式类型(可在编译时进行类型编程)。这在高性能计算和嵌入式系统领域非常重要。常量表达式求值比早期版本更通用、更易用,当然也更优雅。这强化了他们之前的发展方向。

✨ 统一的初始化语法

所以,在1984年的C++中,你可以写 complex(1, 2) 来创建一个复数。今天,你可以写同样的东西,并且让这个复数在编译时创建。因此,你可以把它放在ROM中(虽然不知道为什么要把复数放在ROM里,但你可能需要一个点,道理相同)。

现在,你仍然可以写 pointcomplex。另一条思路是C++对初始化的泛化和安全化。因此,如果你知道所需的类型(例如,有一个返回复数的函数),你可以简单地写 {1, 2}。编译器会说:“哦,{1, 2} 应该生成一个复数”,然后它就会生成并返回一个复数。如果这发生在编译时,它就会在编译时完成。

⚡ 性能与安全性的双重提升

因此,这些特性协同工作:你获得了更好的记法、更好的性能。任何能在编译时完成的事情,在并发系统中效果更好,因为你无法在程序启动前就已计算好的常量上发生竞态条件,也就不会有线程问题。

{ } 意味着:为我将要放入的东西找到一个构造函数。它会查看目标位置,然后判断是否存在双参数、三参数或其他类型的构造函数。或者如果只是一个结构体,它会将第一个元素放入第一个成员,对结构体也同样有效。

🔍 改进的错误检查

这就是统一初始化。它会尽可能进行初始化。当然,如果存在任何歧义,它会发现歧义。如果你处于一个目标不明确的上下文中(例如调用一个函数,目标可能是pointcomplex),编译器会告诉你存在歧义。因此,错误检查实际上得到了改进。

C++11在发现错误方面比C++98略好一些。他来自这样一个哲学流派:编译器是你生成代码、构建程序时最好的朋友。而要让编译器成为你最好的朋友,你实际上需要有更多的类型。

🧱 类型系统的重要性

如果所有东西都是整数,类型系统能帮你什么?不能。如果所有东西都是浮点数,我无法告诉你它是英制单位还是公制单位,你就会遇到错误。因此,你需要类型丰富的接口。为此,你需要能够构建廉价、灵活、方便、易用且简单的类型。

所以他们从 complex(1, 2) 这样的形式,发展到 {1, 2},并且类型是可选的,只在需要时才需要指定。最后,现在如果你愿意,甚至可以写 1 + 2i(定义 i 为虚部单位),你就能进行复数运算,而无需显式写出 complex


本节课中我们一起学习了C++语言的设计哲学,明确了其核心应用领域是构建高可靠性的“基础设施”软件。我们探讨了通过强类型接口、编译时计算和统一初始化等特性来提升代码的可靠性、性能和可读性。最重要的是,我们认识到软件开发中需要根据不同的目标和风险,采用不同的技术、工具和思维方式,没有一种“一刀切”的解决方案。

051:Django通用视图 🚀

在本节课中,我们将要学习Django通用视图。通用视图是一种强大的工具,可以让我们用极少的代码创建常见的网页功能,例如显示列表或详细内容。我们将探讨如何利用通用视图来避免代码重复,并保持应用的一致性。


上一节我们介绍了模板继承,本节中我们来看看视图的继承。我们一直在使用视图,但现在是时候深入了解如何通过继承来创建更简洁、更强大的视图了。

我们位于views.py文件中。这里定义的视图很大程度上受到models.py内容的影响。这意味着视图可以根据模型中的数据自动构建。

只需查看models.py中的数据即可。这再次体现了“不要重复自己”的原则。如果你在models.py中定义了字段、帮助文本或字段描述,这些信息不仅用于数据库,还可以用于视图。你会看到这一切是如何完美结合的。但有时,当我们使用这些高级技术时,最终得到的代码量非常少。深刻理解这少量代码至关重要,因为它可能巧妙地重用了大量其他代码,并从其他地方引入了功能。

回顾我们讨论过的SQL,四个基本操作是:向数据库插入数据、从数据库读取数据、更新数据和删除数据。这通常被称为CRUD:Create(创建)、Read(读取)、Update(更新)和Delete(删除)。

我们将构建许多功能,无论是广告、汽车还是其他内容。我们需要插入一辆汽车、更新一辆汽车、删除一辆汽车、列出所有汽车、插入一只猫、删除一只猫、列出所有猫、更新一只猫等等。我们将反复构建这些用户界面。在你自己的项目中,可能会有四五个表,每个表都有类似的CRUD用户界面。有时你会对它们进行一些定制,使其看起来不同,但很多时候它们看起来都一样,导致我们重复编写代码,而这是软件开发中应该避免的。


这里有一个简单的CRUD应用示例。它目前只包含读取和详情功能。我们有一个猫的列表,点击列表中的猫,可以看到特定猫的详细信息,然后有一个返回列表的链接。这就像一个详情页面。但我们将要做的不止这些。这个例子只是为了展示通用视图如何通过保持简单来工作。

我们将讨论列表视图和详情视图,这是众多通用视图中的两种。这是我们的GView应用。以下是urls.py文件。在这个GView应用中,有猫列表、单个猫、狗列表和单个狗的URL。你可以看到这种重复。我在这里放这么多是为了展示使用不同方法(使用或多或少的细节和继承)来实现它们。它们基本上都一样,我给它们命名,有些使用整数主键。



首先从模板开始。在猫列表模板中,我们期望有一个名为cat_list的上下文变量。

这是一个猫对象的列表。然后我们将遍历cat_list,无论有多少只猫,在一个列表内生成<li>标签,并创建一个链接URL。接着提取猫的名字。猫有名字和主键。我们将生成一个指向“Sophie”的链接,链接地址是/cat/12(无论那个数字是什么)。如果猫列表为空,我们将显示“数据库中没有猫”。实际上,我们有一个脚本用于预加载所有这些数据。示例代码中有关于如何预加载数据的说明,请查看其README文件。



在详情页面中,我们将传入一个猫对象,它有一个名字。然后我们想要一个返回猫列表的链接。这里我们再次使用了上一讲的内容,使用URL宏,传入应用内视图的名称。这是GView应用中的cat视图。这将生成一个有效的链接,字符串“返回列表”作为链接文本。

我们将构建所有这些内容。这是基本思路,模板和HTML就是这样工作的。但我们将看看视图是如何工作的。再次提醒“不要重复自己”的原则。有时这个原则会给我们带来麻烦,因为其理念是将某些逻辑放在一个地方,并在尽可能多的地方重用。这可以很好地描述为:重用、重用、重用。做一次,然后尽可能多地重用,这样如果你需要修复其中的错误,就不必找到所有49个放置此代码的地方。

因为我们一遍又一遍地做同样的事情:猫、狗、马、汽车。列表视图、详情视图、编辑视图等都是常见功能。如果你要做这些事情,为什么不使其看起来一致、功能一致呢?如果一个开发者构建了猫的编辑界面,另一个人构建了狗的编辑界面,他们各自为政,设计可能很酷但不同,这会导致不一致。你仍然想要一个漂亮的设计,但你希望猫的数据和狗的数据在这个一致的设计中都有意义。

所以两者都可以有一个好的设计。

我们有模板。模板期望像cat_listdog_list这样的变量。猫详情页面只有一个cat变量。如果我们暂时忘记将要学习的所有内容,只写一个视图,它会像第一个例子这样。这个CatListView继承自View。我们将查询数据库。我们已经导入了Cat模型。Cat.objects.all()是查询所有猫对象并获取猫对象列表的方法,将其放入变量stuff中。我们之前在shell中做过这个,但现在是在视图的实际Python代码中。正如所说,视图是所有事情发生的地方。这意味着进行数据库调用,加载所有猫记录。

将它们转换为对象,并给我这些对象的列表。然后我将上下文传递到模板GView/cat_list.html中。我将传递整个列表。如果你回头看,模板中有一个小循环,打印出Sophie、Frankie等,并在它们下面生成指向详情页面的链接。

如果不打算重复自己,我们就会这样做。接下来,对于狗,我们将用另一种方式来做。

看起来工作量更大,但可重用性更高。我们在这里做的是:现在有一个DogListView类。我们将在这个对象中创建一个名为model的属性变量,并说明这个特定视图的模型是Dog

现在的想法是,在get方法中,我们取这里的代码,每次我们说Cat,都使其通用化,并由这一个变量驱动。使这有点棘手的是,有时我们只使用模型,而其他时候我们实际上会将其转换为文本字符串。

看看这个。我们有一个变量model。在get请求中,self.model是指向我们所在特定实例中model变量的方式。self.model._meta.verbose_name.title().lower()。你可以通过最终查看Stack Overflow自己弄清楚。我就是这样弄清楚的。我想获取模型名称的小写版本。模型名称是一个字符串“Dog”,这与Dog模型不同。所以这只是一种方式,model_name将变成小写的“dog”。但如果模型是Cat,那么model_name将变成小写的“cat”。我正在尝试编写可重用的代码。是的,可重用的代码看起来有点复杂,因为它是通用的。

但你会注意到,我正在创建一个名为model_name_list的变量,因为我只是将它们连接在一起。所以它将是dog_list。接下来会发生的是,我将与模型对话并检索所有对象。self.model.objects.all()。然后我将获取所有对象的列表并放入stuff中。然后我将把它放入上下文。接着,我将从GView文件夹中获取一个模板,模板名是model_name_list。所以以前是cat_list,现在是dog_list。我将它们连接在一起,并传入上下文。然后当这一切发生时,你会得到一个狗列表和其中的狗。我所做的是将这段代码通用化并放在这里,然后以某种方式传递给由DogListView类创建的对象我想要操作的实际模型。

我这样做是为了让你了解这是如何工作的。你可以对马做完全相同的事情。

通过这样做:导入django.views.generic,然后说我要创建一个名为HorseListView的类。我将扩展不仅仅是View,而是generic.ListView。这是一个通用类,你可以查看Django文档。这是一个执行通用列表视图的类。这个类的规则是,你必须在这里添加一行来说明它作用于哪个模型。仅此而已。这是因为get代码已经在这个模型中,或者我们正在扩展这个模型,或者继承了所有这些代码,不是从DogListView,而是从generic.ListView

所以我们把5行的东西,稍微通用化后变成8行,现在变成了1行。这就是扩展的工作原理。但你必须信任,并且必须去阅读ListView的文档,以了解如何告知它应该使用哪个模型。

我想通过自己创建一个列表视图来展示一点这是如何发生的,它不如真正的Django ListView酷,只是为了让你了解Django ListView幕后发生了什么。

所以,如果我创建一个新类DJ4EListView,然后扩展View。然后我将有一个get方法,它将包含所有这些代码。它寻找一个名为model的变量。你会注意到它没有在这里定义model,因为这是一个打算被重用的类。我会说,哦,无论在这里设置的模型是什么(通过我们正在扩展的类创建时),去获取它,将其转换为文本,获取其标题,然后转换为小写,使其成为model_name。无论模型是什么,检索其所有对象并放入stuff,然后放入model_name_list。例如,car_list。然后将该列表传递到模板,并渲染GView/car_list.html和上下文。

DJ4EListView本身并没有做任何具体的事情,它正在创建一个我稍后要扩展的类。我将用两行代码扩展它:创建一个扩展DJ4EListViewCarListView。为了获得这里的所有代码,我唯一需要做的就是扩展它,然后我需要添加model = Car。你会注意到CarListView的语法与HorseListView完全相同,对吧?

对于HorseListView,你基本上告诉generic.ListView要操作哪个模型,然后generic.ListView完成所有工作。如果你看CarListView,它说使用这个我刚刚发明的通用列表视图来向你展示它的源代码。这是幕后的源代码。正如我所说,它比我做的要复杂和精密得多。这是可扩展通用列表视图最粗糙、最简单的版本。


但当一切都说完了,你不需要编写任何那些版本。你实际上只需要创建一个列表视图并告诉它是什么模型,创建一个猫视图并告诉它是什么模型,然后一遍又一遍地重用这个列表视图。所以我给你展示了四种不同的方法来做这件事。也许四五种不同的方法。真正的方法只有一种。但当你看到像HorseListView扩展generic.ListViewmodel = Horse这样的代码时,你可能会觉得需要信任很多。答案是,是的,你会习惯的。现在你必须编写的代码只有两行。你仍然需要编写模板,因为它会调用你的模板,但它通过查看那个“horse”知道模板的名称。哦,是的,horse_detailhorse_list。这是实际的详情页面,显示马的信息并返回列表等。使用这些通用视图,你可以使代码非常简短、精炼和简单。

通用视图非常出色。它们允许我们生成许多相似的页面而无需重复自己。它们保持代码的一致性。“不要重复自己”原则意味着即使一遍又一遍地做同样的事情看起来很快(复制、粘贴、修改),但如果你真的想构建可靠的、成千上万行代码、具有数百个功能的应用程序,你也不希望一遍又一遍地粘贴相同的东西。



更少的代码意味着更少的错误。这基本上是我们关于视图讨论的结束。这实际上是我们将要开始做大部分工作的地方。URL和模型通常不会有太多活动,尽管我们接下来将讨论表单,它涉及视图、模型和模板。表单是提升我们用户体验的事物之一。我们很快会再见面。


本节课中我们一起学习了Django通用视图。我们了解了如何利用ListViewDetailView等通用类视图来大幅减少重复代码,遵循DRY(不要重复自己)原则,并保持应用不同部分之间的一致性。通过继承这些内置的通用视图,我们可以用极少的代码实现常见的Web模式,使开发更高效,代码更易于维护。

052:南非开普敦面对面办公时间 📍

在本节课中,我们将回顾密歇根大学《Django for Everybody》课程第52集的面对面办公时间内容。本次办公地点位于南非开普敦,查克博士与当地的学生们进行了交流。

概述

本节内容记录了查克博士在南非开普敦大学附近与课程学生举行的一次线下办公时间。学生们依次进行了自我介绍,并分享了学习本课程的经历与收获。

学生自我介绍

以下是参与本次办公时间的各位学生的自我介绍。

  • Christy:我正在攻读生物信息学博士学位,这门课程对我的健康领域研究很有帮助。
  • Cabango:我去年完成了这门课程,现在正在学习机器学习和相关技术。
  • Ryanan:我去年学习了这门课程,这是我第一次接触编程和网络技术。
  • Jeff:我去年学习了大部分Python课程,非常享受所有的MOOC内容,我会坚持编程和实践。
  • Stephen Markco:我来自MusicT,我的邮箱地址出现在Python课程中。查克博士补充道,Stephen的邮箱在解析电子邮件章节中被用作示例。
  • Joseph:我学习了互联网历史课程,觉得非常棒,同时也在学习Python for Everyone课程。
  • Garyrry:我从互联网历史课程开始学习,虽然最初对计算机编程感到害怕,但现在已成为一名Web开发者。
  • Joey:我终于见到了查克博士,感谢这门课程,我专程来到南非参加这次见面。
  • John:我是一名工程师,每天在工作中都会用到从这门课程学到的知识。
  • Ka:我也是一名工程师,非常喜欢查克博士的所有课程,并期待未来有更多新课程。

总结

本节课中我们一起回顾了在南非开普敦举行的线下办公时间。查克博士与来自不同背景的学生们相聚,学生们分享了学习本课程如何助力他们的学术研究、职业转型与日常工作。本次交流在轻松的氛围中结束,并预告下一次办公时间可能在丹佛举行。

053:表单-GET、POST与HTTP 🧩

在本节课中,我们将学习HTML表单如何工作,以及它们如何通过GET和POST两种HTTP方法与服务器通信。表单是网页中让用户填写信息并提交给服务器的核心组件。我们将探讨这两种方法的基本原理、区别以及在实际开发中的使用场景。

概述

表单是HTML中用于创建用户输入界面的元素。它们允许用户在浏览器中填写数据,并将这些数据发送到服务器。数据发送主要有两种HTTP方法:GET和POST。理解这两种方法的区别对于构建安全、高效的Web应用至关重要。

GET与POST方法

上一节我们介绍了表单的基本概念。本节中,我们来看看向服务器发送表单数据的两种基本方式。

在HTML中,我们一直使用的锚点标签(<a>)发起的是GET请求。这意味着你请求获取一个文档并显示它。你可以在URL末尾附加参数,例如 ?guess=42。然而,表单不仅可以发起GET请求,还可以发起POST请求。POST允许我们以另一种方式向服务器发送数据。

代码示例:数据转储工具

为了清晰地展示GET和POST数据,我们先看一段工具代码。这段代码用于将请求中的数据(如GET参数、POST参数)以字典形式转储并安全地显示在网页上。

from django.utils.html import escape

def dump_data(label, data_dict):
    """
    安全地转储并显示字典数据。
    label: 数据标签
    data_dict: 要显示的字典(如request.GET或request.POST)
    """
    output = f‘<p><strong>{label}:</strong></p>’
    for key, value in data_dict.items():
        output += f‘<p>{escape(key)} = {escape(value)}</p>’
    return output

这段代码遍历字典中的所有键值对,使用escape函数进行HTML转义以防止跨站脚本攻击,然后将它们格式化成段落输出。

GET请求示例

以下是一个使用GET方法的简单表单。它包含一个文本输入框和一个提交按钮。

<form>
    <p>请输入你的猜测:</p>
    <input type=“text” name=“guess” size=“40”>
    <input type=“submit” value=“提交”>
</form>

当用户输入“42”并点击提交按钮后,浏览器会向当前URL发起一个新的GET请求,并在URL末尾附加参数,变成 ?guess=42。服务器端的Django框架会自动解析这个URL,并将参数 guess=42 放入 request.GET 字典中,供我们使用dump_data函数查看。

POST请求示例

现在,我们来看一个使用POST方法的相同表单。关键区别在于<form>标签的method属性。

<form method=“post”>
    {% csrf_token %}
    <p>请输入你的猜测:</p>
    <input type=“text” name=“guess” size=“40”>
    <input type=“submit” value=“提交”>
</form>

注意:为了简化示例,我们暂时使用了@csrf_exempt装饰器。在实际应用中,必须包含{% csrf_token %}标签以防止跨站请求伪造攻击。

当用户提交这个表单时,数据(guess=42)不会显示在URL中。相反,它作为HTTP请求体的一部分被发送。在服务器端,这些数据可以通过 request.POST 字典来访问。

GET与POST的工作原理对比

以下是GET和POST方法在数据传输方式上的核心区别:

  • GET请求:数据作为URL的一部分(查询字符串)发送。格式为 URL?key1=value1&key2=value2。数据在浏览器地址栏可见,有长度限制(通常约2000字符),并且可以被书签保存。
  • POST请求:数据放在HTTP请求体中发送。数据在地址栏不可见,没有严格的长度限制,适合传输大量数据(如文件上传),且不能被书签直接保存。

何时使用GET与POST

你可能会问,选择哪种方法是否重要?答案是肯定的。遵循以下简单规则至关重要:

  • 使用GET的情况:适用于读取、搜索等不改变服务器状态的操作。GET请求应该是幂等的,即多次执行相同的GET请求应产生相同的结果(语义上)。例如,查看日历、访问收件箱列表。
  • 使用POST的情况:必须用于创建、修改或删除数据等会改变服务器状态的操作。这不仅是良好实践,也关乎安全。网络爬虫通常只跟随GET链接来“阅读”数据,而会避免POST请求,以防止意外修改网站数据。

此外,POST请求能保持URL简洁美观,而GET参数会使URL变得冗长。

HTML表单输入类型简介

接下来,我们将快速回顾HTML表单中可用的各种输入标签类型,它们可以创建丰富的用户界面。

以下是常见的HTML输入类型:

  • 文本输入 (<input type=“text”>): 单行文本字段。
  • 密码输入 (<input type=“password”>): 用于输入密码,内容被隐藏。
  • 提交按钮 (<input type=“submit”>): 用于提交表单。
  • 单选按钮 (<input type=“radio”>): 允许用户从一组选项中选择一个。
  • 复选框 (<input type=“checkbox”>): 允许用户选择多个选项。
  • 下拉选择 (<select>): 创建下拉列表供用户选择。
  • 文本区域 (<textarea>): 多行文本输入框。
  • 隐藏域 (`): 存储不需要用户看到但需要提交的数据。

总结

本节课中,我们一起学习了HTML表单的基础知识以及GET与POST两种HTTP方法。我们了解了GET请求将数据附加在URL中,适用于安全的数据读取操作;而POST请求将数据放在请求体内,适用于会改变服务器状态的数据提交操作。我们还通过代码示例演示了如何在Django中处理这两种请求的数据,并强调了根据操作类型正确选择方法的重要性。理解这些概念是构建交互式Web应用的关键第一步。

054:构建HTML表单 📝

在本节课程中,我们将学习HTML表单的基础知识。表单是Web应用与用户交互的核心组件,用于收集和提交数据。我们将介绍几种常见的表单输入类型,并解释它们的工作原理。

概述

HTML表单允许用户输入数据,这些数据随后可被发送到服务器进行处理。表单由多种输入元素组成,每种元素适用于不同的数据收集场景。本节将逐一介绍这些元素及其用法。

文本输入框

文本输入框是最基本的表单元素,用于收集单行文本信息。其HTML代码示例如下:

<input type="text" name="account" id="account">
  • type="text":定义输入类型为文本。
  • name="account":这是发送到服务器时的键名。例如,用户输入“Beth”后提交,服务器将收到 account=Beth
  • id="account":主要用于关联 <label> 标签,便于浏览器识别和辅助功能使用。

密码输入框 (type="password") 在功能上与文本输入框类似,但用户输入时会显示为星号(*)以隐藏内容。需要注意的是,密码在提交时仍以明文形式传输。

隐藏域 (type="hidden") 在用户界面中不可见,常用于在表单中传递不需要用户填写但服务器需要的数据。

单选按钮

单选按钮用于在多个选项中仅选择一项的情况。其特点是所有选项共享同一个 name 属性。

<input type="radio" name="when" value="AM"> AM
<input type="radio" name="when" value="PM"> PM

当用户选择“PM”并提交时,服务器将收到 when=PM。同一 name 下的单选按钮,无论它们在页面上的位置如何,都只能有一个被选中。

复选框

复选框允许用户从多个选项中选择零个、一个或多个。每个复选框通常拥有独立的 name

<input type="checkbox" name="class1" value="si106"> SI 106
<input type="checkbox" name="class2" value="si539"> SI 539
<input type="checkbox" name="class3"> SI 579
  • 如果复选框被选中,其 value 值(若未设置则默认为“on”)会随表单提交。
  • 如果未被选中,则其 name 不会出现在提交的数据中。

下拉选择框

下拉选择框(<select>)为用户提供了一个选项列表,用户只能从中选择一项。

<select name="soda">
    <option value="0">Coke</option>
    <option value="1">Pepsi</option>
    <option value="2">Mountain Dew</option>
</select>
  • <select> 标签的 name 属性定义了提交时的键名。
  • <option> 标签的 value 属性定义了每个选项对应的值。
  • 可以使用 selected 属性预设默认选项,如 <option value="peanuts" selected>Peanuts</option>

文本区域

文本区域 (<textarea>) 用于输入多行文本,如评论或博客内容。

<textarea name="comments" rows="10" cols="40">默认文本...</textarea>
  • name:提交时的键名。
  • rowscols:定义文本区域显示的行数和列数。
  • 用户输入的所有文本(包括换行和空格)都会作为值提交。

提交与按钮

表单需要一个机制来触发数据提交,这通常通过提交按钮完成。

<input type="submit" value="Submit Form">

  • 点击提交按钮会收集表单中所有数据并发送到服务器。
  • value 属性决定了按钮上显示的文本。

此外,还可以使用普通按钮 (type="button") 并配合JavaScript来执行其他操作,例如取消或返回:

<input type="button" value="Escape" onclick="location.href='/'; return false;">

这段代码会在点击按钮时将浏览器导航至根目录 (/),return false 阻止了表单的默认提交行为。

HTML5输入类型

HTML5引入了一系列新的输入类型,它们在现代浏览器中能提供更好的用户体验和内置验证,并在旧浏览器中优雅地降级为普通文本输入框。

  • 颜色选择器 (type="color"): 弹出颜色选择器,提交值为十六进制颜色代码(如 #ff0000)。
  • 日期选择器 (type="date"): 弹出日期选择控件。
  • 邮箱 (type="email"): 浏览器会验证输入内容是否符合邮箱格式(包含@符号等)。
  • 数字 (type="number"): 通常配合 minmax 属性使用,限制输入范围,并验证是否为有效数字。
  • URL (type="url"): 验证输入是否为有效的URL格式。

这些HTML5输入类型的关键优势在于客户端验证。如果用户输入不符合要求(例如,在邮箱字段中未输入@符号),浏览器会在提交前阻止表单发送,并给出提示。这提供了即时反馈,但请注意,服务器端仍然必须对数据进行验证,因为客户端验证可以被绕过或在不支持的浏览器中失效。

总结

本节课我们一起学习了构建HTML表单的核心元素。我们从基础的文本输入框、密码框讲起,探讨了用于单项选择的单选按钮和多项选择的复选框,接着介绍了下拉选择框和多行文本区域,最后讲解了提交按钮的作用以及HTML5带来的增强型输入类型和客户端验证功能。理解这些表单元素是开发交互式Web应用的基础。在实际使用中,你可以查阅更多在线资源来深入了解表单的高级样式和功能。

055:表单与跨站请求伪造 (CSRF)

在本节课中,我们将要学习Web表单中的一个重要安全概念——跨站请求伪造(CSRF),了解其攻击原理,并掌握Django如何内置防护机制来抵御此类攻击。

概述

上一节我们介绍了表单的基本概念。本节中,我们来看看与表单相关的安全议题。具体来说,我们将深入探讨一种名为“跨站请求伪造”(CSRF)的安全漏洞,理解其工作原理,并学习Django框架如何默认提供防护。

CSRF攻击原理

CSRF攻击的核心思想与Cookie的发送机制有关。设想你已登录某个合法网站(例如一个成绩管理系统)。此时,一个恶意网站可以生成一个包含表单的页面,该表单的提交目标(action)指向那个合法网站的URL。

关键在于,浏览器在向某个站点发送POST请求时,会自动附加该站点的Cookie(包括会话ID)。因此,如果攻击者能诱使你(已登录的用户)在恶意网站上点击提交按钮,浏览器就会带着你的合法会话Cookie,向目标服务器发送一个伪造的请求。服务器收到请求后,会通过Cookie识别出已登录的合法用户身份,从而可能执行危险操作(例如修改成绩)。

攻击者无需知道你的Cookie具体内容,只需要诱使你点击一个指向目标服务器的恶意表单。

CSRF防护机制

CSRF的防御策略是使用一个CSRF令牌(Token)。请注意,它与会话(Session)相关,但不同于会话ID本身。

防护流程如下:

  1. 服务器生成一个大型随机数,作为CSRF令牌,并将其存储在用户的会话(Session)中。
  2. 每当服务器生成一个表单页面时,会在表单内添加一个隐藏的输入标签(<input type="hidden">),其值就是这个CSRF令牌。
  3. 当用户提交表单时,这个CSRF令牌会随着表单数据一同POST回服务器。
  4. 服务器接收到请求后,会从POST数据中提取CSRF令牌,并与会话中存储的令牌进行比对。
  5. 如果两者匹配,请求被视为合法,继续处理。如果不匹配,则拒绝该请求,并可能记录安全错误或注销用户。

以下是合法请求的流程示意图:

攻击与防护对比

让我们通过一个修改成绩的例子,对比攻击发生和被防护的情况。

CSRF攻击场景:

  1. 用户(讲师)已登录合法成绩管理系统(会话ID:C7)。
  2. 用户访问了恶意网站,该网站包含一个伪造的表单,其action指向成绩管理系统的修改成绩URL。
  3. 用户被诱骗点击了提交按钮。
  4. 浏览器自动附带上合法站点的会话Cookie(C7),向合法服务器发送POST请求。
  5. 服务器通过Cookie识别出用户是已登录的讲师C7,因此执行了修改成绩的操作,攻击成功。

启用CSRF防护的场景:

  1. 合法服务器在生成表单时,除了会话Cookie,还会在表单中嵌入一个隐藏的CSRF令牌(例如:csrf_token=‘a9b3f…’)。
  2. 用户提交合法表单时,CSRF令牌随数据一起发送回服务器。
  3. 服务器验证会话中的令牌与POST数据中的令牌是否一致,一致则允许操作。

  1. 对于恶意网站生成的表单,由于攻击者无法得知或伪造正确的CSRF令牌(该令牌存储在用户与合法服务器的会话中),因此当伪造的请求发出时,服务器验证令牌会失败,从而阻断攻击。

Django中的CSRF防护

幸运的是,Django框架默认提供了强大的CSRF防护支持,开发者通常无需手动实现上述复杂逻辑。

  • Django内置了CSRF中间件(CsrfViewMiddleware)。
  • 一旦启用了会话(Session)功能,CSRF防护便会自动生效。
  • 在模板中使用表单时,只需在<form>标签内添加 {% csrf_token %} 模板标签,Django便会自动处理令牌的生成和验证。

总结

本节课中我们一起学习了跨站请求伪造(CSRF)的安全概念。我们了解了CSRF攻击如何利用浏览器的Cookie自动发送机制来冒充用户执行非意愿操作,并深入探讨了通过使用CSRF令牌进行验证的核心防护原理。最后,我们了解到Django框架已经内置了完整的CSRF防护机制,极大地简化了开发者的安全工作。下一节,我们将具体学习如何在Django表单中应用CSRF防护。

056: Django中的CSRF支持 🛡️

在本节课中,我们将学习如何在Django中实现CSRF(跨站请求伪造)保护。我们将了解为什么需要它,如果不处理它会怎样,以及如何通过手动和自动两种方式在表单中正确添加CSRF令牌。


概述

上一节我们介绍了CSRF的概念。本节中,我们来看看如何在Django中实际实现它。Django默认启用了CSRF保护,这意味着对于POST请求,我们必须采取一些明确的措施,否则请求会被拒绝。


未启用CSRF保护的表单

首先,我们回顾一个之前见过的简单表单视图。当时,我们使用了 @csrf_exempt 装饰器来告诉Django不要因为CSRF处理而“炸掉”这个请求。

from django.views.decorators.csrf import csrf_exempt

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a8a0f7d99052164338cfe898a6e75d86_11.png)

@csrf_exempt
def my_first_form(request):
    # ... 处理表单的代码

这个装饰器的作用是关闭该视图的CSRF验证。现在,我们将创建一个没有此装饰器的视图,名为 fail_form,来演示如果CSRF保护开启而我们没有处理会发生什么。

当我们访问这个表单,输入一些内容并点击提交时,会收到一个 403 Forbidden 错误。在HTTP状态码中,403表示“禁止访问”——服务器理解请求,但拒绝授权。这是因为Django的CSRF中间件检查失败,在请求到达我们的视图代码之前就中止了它。


手动添加CSRF令牌

为了让表单正常工作,我们需要在发出的POST请求中包含CSRF令牌。以下是一种手动添加令牌的低级方法:

  1. 在HTML表单中,添加一个隐藏的输入字段。
  2. 从Django的 csrf 模块获取当前请求的令牌。
  3. 将这个令牌值填入隐藏字段。

以下是实现代码:

from django.middleware.csrf import get_token

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a8a0f7d99052164338cfe898a6e75d86_18.png)

def manual_csrf_form(request):
    # 获取CSRF令牌
    csrf_token = get_token(request)
    
    # 构建包含令牌的HTML表单字符串
    form_html = """
    <form method="post">
        <input type="hidden" name="csrfmiddlewaretoken" value="__TOKEN__">
        <input type="text" name="guess">
        <input type="submit" value="提交">
    </form>
    """
    # 替换占位符为真实的令牌
    form_html = form_html.replace("__TOKEN__", csrf_token)
    return HttpResponse(form_html)

当这个页面被渲染时,隐藏字段 csrfmiddlewaretoken 会包含一个长而复杂的随机字符串(令牌)。提交表单时,这个令牌会和表单数据(如 guess=42)一起发送到服务器。

Django的中间件会在请求到达视图之前验证这个令牌。只有验证通过,我们的视图函数才会被执行。因此,在视图内部打印出的POST数据中看到 csrfmiddlewaretoken,本身就意味着CSRF检查已经成功。


使用模板标签自动添加CSRF令牌

手动方法虽然有效,但很繁琐。Django提供了更简单的方式——使用模板标签。这是推荐的做法

首先,确保在你的视图渲染模板时使用了 RequestContext(使用 render() 函数会自动处理这一点)。然后,在模板的表单标签内,使用 {% csrf_token %} 标签。

以下是一个示例:

views.py

from django.shortcuts import render

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a8a0f7d99052164338cfe898a6e75d86_23.png)

def easy_csrf_form(request):
    return render(request, 'myapp/form_template.html')

form_template.html

<form method="post">
    {% csrf_token %}
    <input type="text" name="guess">
    <input type="submit" value="提交">
</form>

这个 {% csrf_token %} 标签会自动生成完整的隐藏输入字段,包括正确的名称和值。作为开发者,你无需关心令牌的具体生成和放置细节。


构建一个带CSRF保护的猜数游戏

现在,我们将应用所学知识,构建一个功能完整的猜数游戏视图。这次我们将使用基于类的视图,它可以将GET和POST请求的处理逻辑清晰地分离到不同方法中。

views.py

from django.views import View
from django.shortcuts import render

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a8a0f7d99052164338cfe898a6e75d86_28.png)

class GuessingGameView(View):
    
    def get(self, request):
        """处理GET请求,显示空表单"""
        # 渲染模板,初始没有消息
        return render(request, 'game.html', {'message': None})
    
    def post(self, request):
        """处理POST请求,检查猜测结果"""
        # 从POST数据中获取用户的猜测
        user_guess = request.POST.get('guess')
        # 调用函数检查猜测值
        message = self._check_guess(user_guess)
        # 将消息传入上下文并重新渲染页面
        context = {'message': message}
        return render(request, 'game.html', context)
    
    def _check_guess(self, guess):
        """辅助函数:检查猜测值并返回提示信息"""
        secret_number = 42
        try:
            guess_int = int(guess)
            if guess_int < secret_number:
                return "猜低了!"
            elif guess_int > secret_number:
                return "猜高了!"
            else:
                return f"恭喜你!答案是 {secret_number}。"
        except (ValueError, TypeError):
            # 如果输入无法转换为整数
            return f"格式错误:'{guess}' 不是一个有效的数字。"

game.html

<h1>猜数字游戏</h1>
<form method="post">
    {% csrf_token %}
    <label for="guess">输入你的猜测:</label>
    <input type="text" id="guess" name="guess">
    <input type="submit" value="提交猜测">
</form>

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a8a0f7d99052164338cfe898a6e75d86_30.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a8a0f7d99052164338cfe898a6e75d86_31.png)

{% if message %}
    <p><strong>{{ message }}</strong></p>
{% endif %}

工作流程解析

  1. GET请求:用户首次访问页面时,触发 get() 方法,渲染一个带有空表单和CSRF令牌的页面。
  2. 用户交互:用户在表单中输入数字(例如42)并点击提交。
  3. POST请求:浏览器将表单数据和CSRF令牌一起发送。Django中间件先验证CSRF令牌。
  4. 令牌有效:验证通过,请求被路由到 post() 方法。
  5. 处理逻辑post() 方法从 request.POST 中获取猜测值,调用 _check_guess 函数生成提示信息。
  6. 返回响应:将提示信息放入上下文,重新渲染 game.html 模板。此时,因为 message 变量有值,页面上会显示出“恭喜你!答案是 42。”

关键点:只要我们在模板中正确使用了 {% csrf_token %},CSRF保护就会自动生效。如果令牌缺失或无效,用户会收到403错误,并且我们的 post() 方法根本不会被执行。


总结

本节课中,我们一起学习了Django的CSRF支持:

  • CSRF保护是默认开启的,对于POST请求必须处理,否则会导致403错误。
  • 手动添加令牌涉及使用 get_token(request) 获取令牌并嵌入HTML,但这种方法较复杂。
  • 自动添加令牌是标准做法,只需在表单模板中使用 {% csrf_token %} 标签即可。
  • 基于类的视图(如 View)能优雅地分离GET和POST处理逻辑。
  • 验证时机:CSRF验证发生在请求到达视图函数之前。如果视图代码得以执行,就意味着CSRF检查已经通过。

通过构建一个猜数游戏示例,我们实践了如何在一个功能完整的视图中集成CSRF保护、处理用户输入并给出反馈。


接下来,我们将讨论一个常见的细节问题:“提交后刷新页面”会带来什么影响,以及如何修复它。

057:POST刷新模式 🚀

在本章中,我们将探讨Web表单处理中的一个常见问题:当用户提交表单后刷新页面时,浏览器会重新发送POST请求。我们将学习如何通过“POST重定向刷新”模式来优雅地解决这个问题,从而避免重复提交和浏览器警告,提升用户体验。

表单的复杂性

上一节我们介绍了如何在Django中处理表单并实现CSRF保护。表单看似简单,实则包含许多细节。一旦我们掌握了这些细节,就能高效地创建和处理一个又一个表单。

成功背后的隐患

不久前,我们成功实现了CSRF保护,表单工作正常。然而,从成功的顶峰往往潜藏着失败的深渊。这个问题并不严重,修复起来也不难,但我们必须解决它。

问题的核心在于:当浏览器发送一个POST请求并收到一个状态码为200的HTML页面响应(这正是我们之前所做的)后,如果用户点击浏览器的刷新按钮,浏览器会重新发送之前的POST请求。

POST请求通常用于改变服务器状态的操作,例如转账。假设你点击转账按钮(这是一个POST请求),成功转出100美元,页面显示“转账成功”。此时如果你点击刷新按钮,浏览器会再次发送相同的POST请求,导致你再次转出100美元。刷新操作会重新执行整个之前的交易,而不仅仅是获取页面。

浏览器的保护机制

由于浏览器知道POST请求可能对服务器造成潜在危险,当用户试图刷新一个由POST请求生成的页面时,浏览器会弹出一个警告框。这个警告并非来自你的应用程序,而是浏览器自身发出的。

警告信息大致是:“要显示此页面,Firefox必须重新发送之前执行的操作信息(如搜索、订单确认或转账)。”这对用户来说是个糟糕的体验。用户可能并不清楚什么是POST请求,也不完全理解刷新的后果,这个警告信息显得混乱且不友好。

作为程序员,我们的目标是避免这种情况。我们希望所有的信息和用户体验都来自我们自己的应用程序,而不是让浏览器接管这些交互。因此,在处理POST请求后直接返回一个状态码为200的HTML页面,是一种不良实践。这会导致浏览器发出不受你控制的、丑陋的警告,损害了应用的可用性。

解决方案:POST重定向刷新

我们可以通过一些额外的工作来彻底避免这个问题。这就是我们将要介绍的“POST重定向刷新”模式。

接下来,我们将展示如何使用“POST重定向刷新”模式来修复上述所有问题。这个模式的标题虽然越来越长,但它能有效地解决问题。

核心流程如下:

  1. 用户提交表单(POST请求)。
  2. 服务器处理表单数据。
  3. 服务器不直接返回HTML,而是返回一个HTTP重定向响应(状态码302),指示浏览器去请求另一个URL(通常是结果页面)。
  4. 浏览器自动向这个新URL发送一个GET请求
  5. 服务器对这个GET请求返回结果页面(状态码200)。

这样,用户最后看到的结果页面是由一个GET请求生成的。此时再刷新页面,浏览器只会重新发送无害的GET请求,而不会重复提交POST数据。

本章总结

本节课我们一起学习了Web开发中一个重要的模式——POST重定向刷新。我们了解到,在处理表单等POST请求后,直接返回页面会导致用户刷新时重复提交数据,并触发浏览器的警告。通过实施“处理POST -> 返回重定向 -> 浏览器GET新页面”的流程,我们可以避免重复提交,将最终状态页面的控制权完全掌握在自己手中,从而提供更流畅、更专业的用户体验。

058:在Django中实现POST重定向 🔄

在本节课中,我们将学习如何解决一个常见的Web开发问题:用户提交表单(POST请求)后刷新页面导致数据被重复提交。我们将通过实现“POST重定向”模式来解决这个问题,并了解如何使用会话(Session)在两次请求间传递临时消息。

概述

当用户通过POST请求提交表单数据后,如果直接返回一个HTML页面(例如“订单成功”页面),此时用户刷新浏览器,浏览器会重新发送上一次的POST请求,可能导致数据被重复处理(如重复下单)。为了解决这个问题,最佳实践是:在处理完POST请求后,不直接返回HTML,而是返回一个重定向响应(HTTP 302),引导浏览器发起一个新的GET请求来显示结果页面。这样,即使用户刷新,也只是重复GET请求,不会重复提交数据。

上一节我们介绍了表单提交的基本流程,本节中我们来看看如何通过重定向来优化这个流程,避免重复提交。

POST/刷新问题与解决方案

问题根源

当服务器对POST请求响应200 OK并返回HTML内容后,该响应会与浏览器的历史记录和当前URL关联。如果用户此时刷新页面,浏览器会弹窗询问“确认重新提交表单吗?”,若用户确认,则会再次发送相同的POST请求,导致服务器重复处理数据。

核心规则

处理POST请求的一个基本规则是:永远不要在POST请求的末尾直接返回HTML作为响应。正确的做法是,在处理完POST数据后,返回一个重定向(Redirect)。

解决方案:POST重定向模式

这个模式遵循“处理POST -> 发送重定向 -> 显示结果”的流程。我们通常将用户重定向回同一个页面(或一个确认页面),让浏览器自动发起一个GET请求来获取最终显示的内容。这符合MVC模式中“控制器”的角色:根据操作结果,将用户的浏览器导航到另一个地址。

以下是HTTP状态码的简要回顾,其中我们将用到302 Found(重定向):

  • 200 OK:请求成功。
  • 404 Not Found:资源未找到。
  • 403 Forbidden:无权限访问。
  • 302 Found:请求的资源已被暂时移动到新位置(用于重定向)。

工作流程详解

下图展示了POST重定向模式如何工作:

  1. 首次访问(GET请求):用户访问页面,服务器返回带有表单的HTML。此时没有消息需要显示。
  2. 提交表单(POST请求):用户填写表单并提交,浏览器发送POST请求。
  3. 服务器处理并重定向:服务器处理数据(如检查猜测值、更新数据库),不渲染页面,而是将需要显示的消息(如“猜对了”、“太高了”)存储到用户的会话(Session)中,然后返回一个302重定向响应,指示浏览器跳转回原页面(或指定页面)。
  4. 浏览器自动发起GET请求:浏览器接收到302响应后,立即自动向重定向地址发起一个新的GET请求。
  5. 显示结果(GET请求):服务器在处理这个GET请求时,从会话中取出之前存储的消息,将其从会话中删除(防止重复显示),然后将消息与页面一起渲染返回给用户。
  6. 安全刷新:此时用户看到的页面是GET请求的结果。如果用户刷新页面,浏览器只会重复最后的GET请求,不会重复提交POST数据,从而避免了重复操作。

这种在两次请求间通过会话传递一次性消息的技术,常被称为“闪现消息”(Flash Message)模式。

代码实现步骤

让我们通过一个“猜数字”的例子,看看代码如何实现上述流程。

1. 处理GET请求的视图部分

当用户首次访问或重定向后访问页面时,处理GET请求:

def guess_view(request):
    # 尝试从会话(session)中获取可能存在的消息
    message = request.session.get('message', False)
    
    # 如果消息存在,则获取后立即从会话中删除它(闪现一次)
    if message:
        del request.session['message']
    else:
        message = None
    
    # 渲染模板,将消息(可能为None)传递给模板
    context = {'message': message}
    return render(request, 'guess_template.html', context)

2. 处理POST请求的视图部分

当用户提交猜测时:

def guess_view(request):
    if request.method == 'POST':
        # 1. 从POST数据中获取用户的猜测
        user_guess = int(request.POST.get('guess'))
        
        # 2. 检查猜测值(业务逻辑)
        if user_guess > secret_number:
            msg = "太高了!"
        elif user_guess < secret_number:
            msg = "太低了!"
        else:
            msg = "恭喜你,猜对了!"
        
        # 3. 将结果消息存储到会话中
        request.session['message'] = msg
        
        # 4. 关键步骤:返回重定向响应,而不是render
        return redirect(request.path)  # 重定向回当前URL,触发GET请求
    
    # ... 以下是处理GET请求的代码(见上一部分)

3. 模板(HTML)示例

模板负责渲染表单和显示消息:

<!-- guess_template.html -->
{% if message %}
    <p><strong>{{ message }}</strong></p>
{% endif %}

<form method="post">
    {% csrf_token %}
    <label for="guess">输入你的猜测:</label>
    <input type="number" id="guess" name="guess" required>
    <button type="submit">提交</button>
</form>

流程回顾与总结

让我们再清晰地梳理一遍整个交互过程:

  1. 初始GET:用户访问页面,视图未在会话中找到消息,渲染一个空表单。
  2. 提交POST:用户输入数字并提交。视图处理POST数据,生成提示信息msg,将其存入request.session[‘message’],然后执行return redirect(request.path)
  3. 浏览器重定向:服务器对POST请求返回302状态码和重定向地址。浏览器自动向该地址发起新的GET请求。
  4. 最终GET:视图再次被调用(处理GET请求)。它从会话中取出message,删除它,并将消息渲染到页面中呈现给用户。
  5. 安全状态:此时页面地址是GET请求的结果。刷新只会重复步骤4,不会重复步骤2,从而彻底避免了重复提交问题。

本节课中我们一起学习了POST重定向模式。其核心要点是:在视图函数中处理完POST请求后,务必使用redirect()进行重定向,而不是render()。同时,我们学会了使用会话(Session)在不同请求间安全地传递一次性信息(闪现消息)。这是一个提升Web应用健壮性和用户体验的重要技术。

接下来,我们将转向Django的内置表单功能,看看Django如何让我们更轻松地创建和验证表单,而无需手动编写大量HTML输入标签。

059:伦敦学员见面会 🎥

在本节课中,我们将回顾一次在伦敦举行的面对面办公时间活动,并认识几位参与课程的学员。


我们身处伦敦肖尔迪奇区。在最后一刻,我们组织了一次线下办公时间活动。几位学员来到了布鲁斯厨房餐厅。我想向大家介绍他们,让他们打个招呼,并向课程的其他同学说些话。

以下是参与活动的学员介绍。

  • 约翰:大家好,我是约翰。我在附近不远的地方工作。很高兴能见到查克博士。这门课程让我受益匪浅,非常感谢。
  • 法拉:大家好,我叫法拉,住在东伦敦。我在2015年就学习过这门课程,并且很快将开始学习新的Python专题课程。
  • 弗格斯:大家好,我是弗格斯。我去年十一月开始学习查克博士的几门课程,感觉非常棒,非常感谢。

本节课中,我们一起了解了课程线下办公时间的实况,并认识了来自伦敦的几位学员。查克博士也提到,几周后将在南非与大家再次见面。

060:日本京都面对面办公时间

在本节课中,我们将回顾密歇根大学《Django for Everybody》课程在日本京都举行的一次特殊面对面办公时间。本次会议在卡拉OK包厢中举行,参与者来自世界各地,分享了他们的学习体验和背景。


大家好,我是Chuck。我们现在在日本京都。这是第一次在卡拉OK包厢举行的办公时间。我们没有唱歌,但吃了很多食物。这主要是一个安静、私密的房间,我可以使用。我认为未来可能会开始使用卡拉OK包厢作为会议地点,因为效果很好。

和往常一样,我想请在座的各位介绍一下自己,让大家认识一下你的同学。请挥挥手,打个招呼,并简单谈谈你对这门课程的感想。

以下是参与者的自我介绍:

  • Barrett:我来自美国,但住在日本。我目前教英语,但希望借助Chuck博士的课程,转型进入教育编程领域。
  • Gary:我来自中国。我在Coursera上学习Chuck教授的Web开发课程。我认为他是一位非常有趣的人。上他的课永远不会感到无聊,鼓励大家选课,不要睡着。
  • Se:我今天早上刚从韩国飞过来。非常高兴能来到这里参加这次活动。
  • Heri:我来自印度尼西亚。我的背景是林业,我学习编程是为了帮助我的研究。谢谢Chuck。
  • Uki Shaakai:我来自日本。我非常喜欢学习Python和编程,很高兴见到你。
  • You:我来自日本,很高兴见到你。
  • Shoji:我是这次活动的观察员,也是京都大学的教员,参与京都大学的MOOC项目。我和Chuck认识很久了,通常当他来日本或我们一起去开会时,我们会一起唱卡拉OK。

上一节我们介绍了本次办公时间的参与者和环境,本节中我们来看看Chuck分享的个人计划。

我不知道接下来具体会去哪里。但我对2020年有一个小决心。我打算取消很多常规旅行,尝试去一些从未去过的地方。因为世界各地有很多Python会议,所以我打算开始参加更多非传统会议地点的全球Python会议。也许不久后我就会来到你附近的国家。期待在线相见。


本节课中我们一起回顾了一次特别的课程办公时间,了解了来自不同背景的学习者如何通过Django课程探索编程世界,并知晓了讲师未来的旅行教学计划。

061:欢迎来到课程3 🎉

在本课程中,我们将从Django应用的基本形态,转向构建应用时所需掌握的更多技能与技巧。

上一节我们介绍了Django的基础,本节中我们来看看如何提升应用的功能与效率。我们将学习诸如会话管理、用户系统等特性。我们会深入了解Django的做事方式,你会发现很多Django代码都非常简洁精炼。

但在这简洁的背后,其实蕴含着一整套复杂的机制。Django方法论的一个重要部分是“不要重复你自己”。这意味着,不要将一行代码稍作修改就重复写上一千遍。

相反,我们采用面向对象编程的方式,只对一大段代码中的小部分进行修改。这是一种“不要重复你自己”的形式,也是一种类似函数和对象的重用形式。我希望你已经接触过一些面向对象编程,但它的难点在于初次接触时很难完全理解。

因此,作为本课程的一部分,我们将回顾面向对象编程,并立即应用它。作为一名教师,我喜欢先展示概念,然后立即使用它。在课程中,你会看到我展示大量代码,然后指出:虽然这段长代码能工作,但并非最佳做法。最佳做法是扩展一个对象,并只插入少量代码。

我们将反复实践这种方法,让我们的应用先变得更大、更复杂,然后再让它们变得更小、更精炼。而更小的版本才是正确的做法。但我不希望在你理解背后原理之前,只教你某个功能的两行简化版代码。

所以,我们将先探索Django的对象,然后学习如何使用这些对象。在本课程中,我们将从构建小型应用开始,逐渐增加应用的规模和复杂度。

然后,我们会减少脚手架代码,让你最终能够独立构建应用。


本节课中我们一起学习了本课程的目标与学习方法:我们将深入Django的高级特性,如会话和用户管理,并重点通过面向对象编程实践“不要重复你自己”的原则,从构建复杂应用开始,最终学会编写精炼、高效的代码。

062:Django中的URL路由

在本节课中,我们将开始学习Django应用的实际输出部分,包括视图和模板的使用。这里有很多内容需要覆盖。

概述

到目前为止,我们已经开始讨论URL。URL很简单,它规定了当收到特定格式的请求时,选择一个视图并将请求发送给该视图。接下来我们将详细讨论视图、模板,以及后续会讲到的表单。我们还会花大量时间讨论模型,以及模型如何与Python代码交互、如何通过模型读写数据库。

随着我们将视图组合起来,我们开始真正构建整个应用。我知道需要一些时间才能看到完整的成果。

保持代码更新

无论你在哪里使用DJ4示例代码(无论是在你的笔记本电脑上还是在PythonAnywhere上),都应该时不时地执行 git pull 命令。因为我一直在更新这些示例,我希望你能确保拥有最新的代码副本。有时我会添加一些小内容,虽然不改变核心示例,但会补充一些文档。

视图:应用的核心

视图是应用的核心。URL最终会找到视图,模型则服务于视图的需求,作为允许视图读写数据库的中间层。

视图文件(views.py)具有模型方面的功能,它处理传入的数据(例如当我们收到表单和POST数据时,会将其复制到数据库中)。视图有时决定是否将用户重定向到另一个页面,或者是否要生成实际的HTML页面。它通常使用模板来生成HTML,然后将其发送回客户端。

因此,视图是真正完成工作的地方。你会发现,你在视图中编写大量代码,在URL文件中写一行,在模型文件中写几行,然后在视图中写很多内容。我认为模板也属于视图的一部分。

Django如何处理请求

当Django收到一个传入的文档请求时,它首先解析URL。域名之后的第一部分通常是应用名称。请记住,Django有一个项目,其下有一个或多个应用。在每个DJ3示例中,你会看到很多应用,每个应用都展示某个主题的示例代码。

因此,URL的第二部分是应用名称,实际上它也是Django项目内的文件夹名称。

在应用内部,URL的下一部分通常是视图。应用内的视图在 urls.py 文件中定义。之后,URL可能包含两种参数:

  1. 一种是跟在问号后面的键值对参数,使用 & 符号分隔。
  2. 另一种是直接放在斜杠后面的参数,这更像REST风格的漂亮URL,将参数直接放在URL路径中,而不是使用问号(那是比较老派的做法)。

URL分发器(路由器)

Django中有一个称为URL分发器(我在图中称之为路由器)的组件。它基本上让你能够定义URL,以及如何解析、处理和将这些URL路由到各个视图代码。

我们在 urls.py 文件中完成这项工作。有三种基本模式:

  1. 路由到预定义类:将特定的URL模式路由到一个预定义的类。
  2. 路由到函数:这是比较老派的方式,使用一个函数。该函数接收一个 request 对象,该对象封装了所有数据(参数、URL、是否是安全请求、来自哪个主机、IP地址等)。视图函数查看这个请求对象,决定做什么(可能查询数据库),然后返回一个响应(可能是重定向或HTML)。
  3. 路由到类视图:定义一个类来处理请求。我们将看到,定义类视图非常方便。类中可以有像 getpost 这样的方法,具体取决于我们处理的HTTP请求类型。在这些方法中,请求对象和任何其他URL参数也会被传入。

示例解析

让我们看一个来自 views 应用的示例 urls.py 文件,我们会看到所有这三种路由类型的例子。

urlpatterns 是一个全局变量,它是一个列表,但对Django有特殊意义。

你会看到这些 path 命令(还有其他方式来描述这些路由):

  • path('', ...) 中的空白路径意味着应用名称之后直接就是斜杠 /
  • 然后指定要将请求发送到哪个视图。

例如,TemplateView.as_view(...) 基本上是为了节省你编写代码的工作。如果你只想从模板文件夹中取出一个模板并返回它,你就不必在 views.py 中编写自己的代码。这就是为什么我们从 django.views.generic 导入 TemplateView。我们可以说:“我写了这个模板,就是不想写代码去读取并发送它。” 这是Django中一个预定义的组件,为我们完成这个任务。

更老派的方式是这里的语法:from . import views 会导入 views.py 文件,然后 views.function_name 指向其中的函数。

还有类视图,同样来自这个应用的 views.py。语法有点奇怪:ClassName.as_view() 是一个静态方法,它返回一个可以响应传入请求的函数。

下一步

接下来,我们将实际查看 views.py 文件,并仔细研究视图。

总结

本节课中,我们一起学习了Django中URL路由的核心概念。我们了解到视图是应用处理请求和生成响应的核心,URL分发器负责将不同的URL路径映射到对应的视图函数或类视图。我们还初步了解了三种基本的路由模式,并通过示例 urls.py 文件看到了它们的具体应用。理解URL路由是构建Django Web应用的基础,下一节我们将深入视图的内部工作机制。

063:Django视图

在本节课中,我们将学习Django视图(Views)的核心概念。视图是处理Web请求并返回响应的关键组件。我们将探讨如何编写视图代码,包括使用无需编写Python代码的预定义视图,以及如何编写自定义视图函数来处理请求和响应。


静态HTML视图

上一节我们介绍了视图的基本概念,本节中我们来看看如何创建一个简单的静态HTML视图,而无需编写任何Python逻辑。

Django提供了一个名为TemplateView的预定义类视图。我们只需在urls.py中配置它,并指定一个模板文件,Django便会自动读取该HTML文件并将其发送回浏览器。这是一种常见的模式,适用于仅需返回静态内容的页面。

以下是配置步骤:

  • django.views.generic导入TemplateView
  • urlpatterns列表中,使用path函数将URL路径映射到TemplateView.as_view(),并通过template_name参数指定模板文件路径。
  • 模板文件的路径结构通常为{应用名}/templates/{应用名}/{模板文件名}.html。这种重复应用名的做法是为了避免不同应用间的模板文件命名冲突。

例如,在urls.py中的配置代码如下:

from django.views.generic import TemplateView

urlpatterns = [
    path('', TemplateView.as_view(template_name='myapp/main.html')),
]

对应的模板文件应位于:myapp/templates/myapp/main.html


请求与响应对象

了解了静态视图后,我们来看看在需要编写Python代码的自定义视图中,如何处理数据。这涉及到两个核心对象:请求对象响应对象

当Django收到一个HTTP请求时,它会将请求数据封装成一个HttpRequest对象,并作为第一个参数传递给视图函数。这个对象包含了所有传入的数据,例如请求方法(GET/POST)、URL参数、请求头等信息。

视图函数的主要职责是处理这个请求,并生成一个HttpResponse对象作为返回。响应可以是HTML文本,也可以是一个重定向指令。

一个最简单的自定义视图函数示例如下:

from django.http import HttpResponse

def simple_view(request):
    html_content = "<html><body><h1>Hello, World!</h1></body></html>"
    return HttpResponse(html_content)

在这个例子中,request参数包含了所有请求信息,函数最终返回一个包含HTML字符串的HttpResponse对象。


处理URL查询参数

上一节我们看到了基本的请求处理,本节中我们来看看如何从URL中提取具体的参数。这在处理用户输入时非常有用。

当用户访问一个带有查询字符串的URL(例如/somepath?guess=42)时,这些参数会被存储在request.GET对象中。request.GET是一个类似字典的结构,我们可以通过键名来获取对应的值。

以下是一个处理查询参数的视图函数示例:

from django.http import HttpResponse

def show_guess(request):
    # 从 request.GET 字典中获取名为 'guess' 的参数值
    guess_value = request.GET.get('guess', 'No guess provided')
    response_html = f"<html><body><p>Your guess was: {guess_value}</p></body></html>"
    return HttpResponse(response_html)

如果用户访问/show_guess?guess=42,页面将显示“Your guess was: 42”。如果未提供guess参数,则会显示默认值“No guess provided”。


课程总结

本节课中我们一起学习了Django视图的基础知识。我们首先介绍了如何使用TemplateView来直接渲染静态HTML模板而无需编写视图函数。接着,我们深入探讨了视图函数的核心,即处理HttpRequest对象并返回HttpResponse对象。最后,我们学习了如何通过request.GET字典来获取URL中的查询参数,从而实现基本的动态内容渲染。掌握这些概念是构建Django Web应用交互功能的第一步。

064:Django模板使用指南 🧩

在本节课中,我们将学习Django模板系统。模板是生成动态HTML内容的关键部分,它允许我们将Python代码与HTML结构分离,从而创建更清晰、更易维护的用户界面。


视图的另一半:模板

上一节我们介绍了视图(Views)作为处理请求和响应的Python代码。本节中,我们来看看如何通过模板(Templates)来生成最终的HTML页面。模板本质上是一种包含动态“热点”的HTML文件,这些热点会被我们传入的数据替换。

Django官网将其描述为“动态生成HTML的便捷方式”。我们使用Django默认的模板引擎。其基本流程是:我们将数据(通常来自模型)传递给模板,模板引擎进行“渲染”(Rendering),最终生成填充了数据的HTML,并通过HTTP响应返回给浏览器。这与我们之前直接拼接字符串返回响应的流程一致,但使用模板引擎能让代码更简洁、更易读。

为何使用模板?

以下是我们在视图中直接拼接HTML字符串的示例:

def my_view(request):
    html = "<html><body>Your guess was " + str(guess) + "</body></html>"
    return HttpResponse(html)

这种方法存在几个问题:

  • 需要处理字符串拼接(+)和转义。
  • Python代码的缩进与HTML结构的缩进容易冲突,导致代码难以阅读和维护。
  • 当HTML结构复杂时,代码会变得冗长且容易出错。

模板系统解决了这些问题,让我们能够专注于HTML结构本身。

模板基础:渲染过程

模板的核心是一个包含特殊标记的HTML文件。我们通过一个称为“上下文”(Context)的字典对象向模板传递数据。模板引擎会读取模板文件,找到这些特殊标记,并用上下文字典中对应的值替换它们,最终生成纯HTML。

一个最简单的例子:

  • 上下文数据(Context){'guess': 'fun stuff'}
  • 模板文件(Template)<p>Your guess was {{ guess }}</p>
  • 渲染结果(Rendered HTML)<p>Your guess was fun stuff</p>

这里的 {{ guess }} 就是双花括号标记,它告诉模板引擎:“用上下文字典中 'guess' 键对应的值来替换这个位置”。


在视图中使用模板

让我们看一个在视图中实际使用模板的完整例子。假设我们有一个处理猜数字的视图。

以下是视图代码(views.py):

from django.shortcuts import render

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/5ee9f9d6f7c269e1788997336852b116_13.png)

def game_view(request, guess):
    # 1. 准备上下文数据
    context = {
        'guess': int(guess)  # 将URL参数转换为整数并存入上下文
    }
    # 2. 渲染模板并返回HTTP响应
    return render(request, 'tmpl/guess.html', context)

代码解析:

  1. 我们创建了一个 context 字典,其中键 'guess' 的值来自URL参数 guess
  2. 我们使用 render() 函数。它接收三个参数:
    • request: 原始的HTTP请求对象。
    • 'tmpl/guess.html': 模板的路径(稍后解释路径规则)。
    • context: 包含要替换到模板中数据的字典。
  3. render() 函数会处理模板,并直接返回一个 HttpResponse 对象,因此我们可以直接返回它。

模板语言基础语法

Django模板语言提供了两种主要标记来在HTML中嵌入动态内容和逻辑。

1. 变量替换:{{ variable }}

双花括号用于输出变量的值。这是最常用的标记。

示例
在模板文件 guess.html 中:

<p>Your guess was {{ guess }}.</p>

如果传入的上下文是 {'guess': 200},渲染结果为:

<p>Your guess was 200.</p>

2. 标签逻辑:{% tag %}

花括号加百分号用于执行更复杂的操作,如循环、条件判断等。它们不会直接输出内容,而是控制模板的逻辑流。

示例:在模板中添加条件判断。

<p>Your guess was {{ guess }}.</p>
{% if guess < 42 %}
    <p>Too low!</p>
{% elif guess > 42 %}
    <p>Too high!</p>
{% else %}
    <p>Just right!</p>
{% endif %}

在这个例子中:

  • {% if guess < 42 %} 开始一个条件判断块。
  • {% elif guess > 42 %}{% else %} 提供其他分支。
  • {% endif %} 结束整个 if 块。
    如果 guess 为200,渲染结果将是:
<p>Your guess was 200.</p>
<p>Too high!</p>

模板文件的组织与命名约定

Django项目可以包含多个应用(Apps),所有应用的模板在加载后都位于一个全局命名空间中。这可能导致不同应用中的同名模板文件发生冲突。

为了解决这个问题,我们采用一个通用约定:

templates 目录下,为每个应用创建一个同名的子文件夹,并将该应用的模板文件放入其中。

目录结构示例:

my_project/
├── my_app/
│   ├── views.py
│   └── templates/       # 应用自己的模板目录
│       └── my_app/      # 以应用名命名的子文件夹
│           └── index.html
└── another_app/
    ├── views.py
    └── templates/
        └── another_app/
            └── index.html

在视图中引用模板时,需要包含这个子文件夹路径:

return render(request, 'my_app/index.html', context)
# 而不是 return render(request, 'index.html', context)

虽然这看起来有些冗余(路径中包含了两次应用名),但这是确保模板名称全局唯一的最佳实践,避免了潜在的冲突。


总结

本节课中我们一起学习了Django模板系统的核心概念和使用方法。

我们了解到:

  1. 模板的作用:将业务逻辑(Python视图)与表现层(HTML结构)分离,使代码更清晰、更易维护。
  2. 渲染流程:视图准备上下文数据,调用 render() 函数,指定模板路径,由模板引擎完成变量替换和逻辑处理,生成最终HTML。
  3. 基本语法
    • 使用 {{ variable }} 来插入变量值。
    • 使用 {% tag %}(如 {% if %}{% for %})来添加逻辑控制。
  4. 组织规范:通过在 templates 目录下创建与应用同名的子文件夹来管理模板,这是防止命名冲突的重要约定。

从下一节开始,我们将利用视图、模型和模板这三个核心组件,正式构建具有完整用户界面的Django Web应用。

065: 修复PythonAnywhere上的Django应用错误 🐛

在本节课中,我们将学习如何在PythonAnywhere平台上调试和修复Django应用中的两种常见错误:启动失败运行时错误(Traceback)。我们将通过具体的步骤和命令,帮助你快速定位并解决问题。


概述

当你在PythonAnywhere上开发Django应用时,可能会遇到两种主要错误。第一种是启动失败,此时应用完全无法启动,页面显示“Something went wrong”。第二种是运行时错误,应用可以启动,但在处理请求时会抛出异常,显示黄色的错误追踪(Traceback)页面。本节将分别介绍这两种情况的诊断和修复方法。


启动失败:诊断与修复

上一节我们概述了两种错误类型,本节中我们来看看如何解决第一种——启动失败。当你的应用完全无法启动时,需要按以下步骤进行诊断。

诊断步骤

以下是诊断启动失败问题的标准流程:

  1. 打开Bash控制台:在PythonAnywhere控制面板中,创建一个新的Bash控制台。
  2. 检查虚拟环境:确保你位于正确的虚拟环境中。虚拟环境决定了Python版本、Django版本和所有依赖包。你可以使用以下命令检查:
    python --version
    
    同时,确认Web应用配置中的虚拟环境名称与此处一致。
  3. 进入项目目录:使用cd命令切换到你的Django项目目录。通常路径类似/home/你的用户名/django_projects/mysite。你可以使用pwd命令确认当前路径。
  4. 运行Django检查命令:这是最关键的一步。在项目根目录(即manage.py文件所在目录)运行:
    python manage.py check
    
    这个命令会模拟Django的启动过程,加载settings.pyurls.py以及所有已安装的应用,并检查其中的Python代码错误。

解读错误信息

运行python manage.py check后,如果存在错误,你会看到一个追踪信息(Traceback)。解读它的关键是找到错误链条的末端,那里通常指向你的代码。

例如,错误信息可能显示:

File “/home/.../polls/views.py”, line 1, in <module>
    from django.https import HttpResponse
ModuleNotFoundError: No module named ‘django.https’

这表明在polls/views.py文件的第1行,你错误地导入了django.https,而正确的应该是django.http

修复与验证

  1. 根据错误提示,打开对应的文件(如polls/views.py)并修正错误。
  2. 保存文件后,不要立即点击Web界面的“重载”按钮。建议再次运行python manage.py check命令。如果命令执行成功且没有输出错误,说明代码层面的启动问题已解决。
  3. 此时,再返回PythonAnywhere的Web应用配置页面,点击“重载”按钮来重启你的应用。
  4. 刷新你的应用网页,检查是否恢复正常。


运行时错误:解读Traceback

解决了启动问题后,应用可能还会在运行中出错。本节我们来看看如何处理运行时错误,即黄色的Traceback页面。

当应用能够启动但处理请求时出错,你会看到一个包含详细错误信息的黄色页面。这实际上是一个好迹象,说明Django框架本身和你的应用基础配置是正常的,问题出在具体的业务逻辑代码上。

分析Traceback页面

Traceback页面包含了错误的完整调用堆栈。分析时,请关注以下几点:

  1. 错误摘要:页面顶部通常会有一个简短的错误描述,如 NameError at /polls/
  2. 错误详情:描述下方会明确指出错误类型和相关信息,例如 name ‘HttpsResponse’ is not defined
  3. 本地变量:页面还会显示错误发生时相关函数的局部变量,这对理解上下文非常有帮助。
  4. 追踪路径:你需要从下往上阅读“Traceback”部分。最下面的行通常是最初引发错误的你的代码文件。向上追溯,可以看到Django内部是如何调用到你的代码的。

例如,一个典型的错误可能指向:

File “/home/.../polls/views.py”, line 6, in index
    return HttpsResponse(“Hello, world”)
NameError: name ‘HttpsResponse’ is not defined

这清楚地指出,在polls/views.py的第6行,你使用了一个未定义的变量名HttpsResponse(正确应为HttpResponse)。

利用代码编辑器提示

在PythonAnywhere的在线编辑器中修改代码时,可以利用其自带的错误提示功能。编辑器会用黄色三角形标记出它检测到的问题,例如未使用的导入或未定义的变量名。修复这些提示后,黄色标记会消失,这能帮助你提前避免一些错误。

修复代码后,保存文件并点击Web应用配置页面的“重载”按钮,然后刷新你的应用页面即可。


查看错误日志

如果问题依然存在,或者你想查看历史错误记录,可以查看PythonAnywhere提供的错误日志。

  1. 在Web应用配置页面,找到“Error log”的链接并点击。
  2. 日志文件按时间顺序记录,最新的错误在文件底部。
  3. 日志中的错误信息与你在Bash中运行python manage.py check或Traceback页面看到的信息是一致的,只是查看方式不同。

总结

本节课中我们一起学习了在PythonAnywhere上调试Django应用的两种核心方法:

  1. 对于应用无法启动(“Something went wrong”):使用Bash控制台,进入项目目录,运行 python manage.py check 命令来定位启动过程中的代码错误。
  2. 对于运行时错误(黄色Traceback页面):仔细阅读Traceback信息,从底部开始找到你的代码文件及出错行,根据错误描述进行修复。善用编辑器的实时错误提示功能。

记住,python manage.py check 是诊断启动问题的利器,而耐心阅读Traceback是解决运行时错误的关键。通过反复实践,你会越来越熟练地处理这些调试场景。

066:在PythonAnywhere上运行的Django应用概览 🚀

在本节课中,我们将要学习一个Django Web应用在PythonAnywhere服务器上的整体运行流程。我们将从宏观角度理解请求如何从浏览器发出,经过Django应用处理,并最终返回响应的完整周期。这对于理解Django项目的结构和各部分如何协同工作至关重要。

宏观视角:Django应用的本质

Django应用本质上是一个运行在服务器上的Python程序。你创建对象、加载数据,并通过这些对象来实现你的Web应用功能。其核心是请求-响应循环:用户在浏览器中请求一个页面,该请求通过网络路由到你的Django应用;应用处理请求(可能涉及读取数据库),生成输出,然后将其发送回浏览器;浏览器解析响应并展示给用户。这个循环是浏览器和互联网运作的基本引擎。

理解这个流程,你可以将其想象为一张数据流向图,也可以简单地将其视为一组文件夹和文件,因为在开发应用时,你主要就是在操作文件夹和文件。

项目结构:多应用视角

为了更好地理解整体结构,我们可以从一个包含多个应用的项目视角来看。一个典型的Django项目可能包含以下部分:

  • mysite/:这是整个Django项目的全局配置文件夹。
  • polls/autos/hello/:这些是具体的应用,每个应用负责特定的功能模块。

每个应用内部通常包含类似的文件结构,例如 urls.pyviews.pymodels.py 等。而 mysite/ 文件夹下的 urls.pysettings.py 则负责项目的全局路由和配置。

启动与配置:一切的起点

当你在PythonAnywhere点击“重新加载”按钮时,你的Django应用就启动了。启动过程始于读取 settings.py 文件。这个文件是你的Django实例的核心配置文件

以下是 settings.py 中的几个关键部分:

  • ALLOWED_HOSTS:这是一个安全过滤器,用于指定哪些主机/域名可以向你的应用发送请求。在开发初期,我们通常将其设置为 ['*'] 以接受所有请求。
    ALLOWED_HOSTS = ['*']
    
  • INSTALLED_APPS:这里列出了所有需要加载的应用,包括Django内置的应用和你自己创建的应用。每添加一个新应用,都需要在此处添加一行。
  • ROOT_URLCONF:这指定了项目的根URL配置模块。通常指向 mysite.urls。它定义了在域名之后(例如 django4.pythonanywhere.com/ 之后)的URL路径如何被路由。
  • 数据库配置:用于设置应用使用的数据库。

settings.py 中的 ROOT_URLCONF = 'mysite.urls' 这一行,实际上是在导入并执行 mysite/urls.py 这个Python模块。

路由解析:从全局到局部

上一节我们介绍了项目的全局配置,本节中我们来看看请求是如何被路由到具体处理函数的。

当请求到达时(例如访问 django4.pythonanywhere.com/polls/),Django首先会查阅根URL配置文件(mysite/urls.py)。

以下是 mysite/urls.py 的一个示例:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('polls/', include('polls.urls')),  # 将 /polls/ 开头的请求转交给 polls 应用
    path('autos/', include('autos.urls')),
    # ... 其他应用路由
]

这个文件定义了一个URL模式列表。它检查请求路径中紧跟在域名后面的部分。例如,如果路径以 polls/ 开头,Django就知道将这个请求(以及 polls/ 之后的部分)转交给 polls 应用内部的 urls.py 去进一步处理。

应用内部路由与视图

当一个请求被路由到特定应用(如 polls)后,该应用内部的 urls.py 文件将接管后续的路由工作。它负责解析应用内部的URL路径。

以下是 polls/urls.py 的一个示例:

from django.urls import path
from . import views

urlpatterns = [
    # 例如:/polls/
    path('', views.IndexView.as_view(), name='index'),
    # 例如:/polls/5/
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    # 例如:/polls/5/results/
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    # 例如:/polls/owner
    path('owner', views.owner, name='owner'),
    # 例如:/polls/5/vote
    path('<int:pk>/vote/', views.vote, name='vote'),
]

这个文件定义了应用层级的URL模式。例如,路径 <int:pk>/ 会匹配像 /polls/5/ 这样的URL,并将数字 5 作为参数 pk(主键)传递给对应的视图函数 views.DetailView

视图:处理请求的核心

视图(View)是处理业务逻辑的核心。它接收Web请求,并返回Web响应。视图可以是一个函数(函数视图),也可以是一个类(类视图)。

polls/urls.py 中,views.IndexViewviews.owner 就分别指向类视图和函数视图。让我们看一下 polls/views.py 中的 IndexView

from django.views import generic
from .models import Question

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        return Question.objects.order_by('-pub_date')[:5]

这是一个类视图,它继承自Django提供的通用视图 generic.ListView。我们只需指定使用哪个模板(template_name)和上下文变量名(context_object_name),并定义获取数据的方法(get_queryset)。Django的通用视图帮我们处理了大量重复性工作。

模板:渲染数据的助手

视图负责准备数据,而模板(Template)则负责将这些数据渲染成最终的HTML页面。模板使用Django模板语言,可以插入变量、使用逻辑标签。

视图会将数据(上下文)传递给模板。例如,IndexView 将查询到的问题列表以 latest_question_list 为变量名传递给 polls/index.html 模板。

以下是模板 polls/templates/polls/index.html 可能的样子:

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

模板接收 latest_question_list 变量,并使用 {% for %} 标签循环遍历,为每个问题生成一个列表项链接。

完成循环:返回响应

当模板渲染完成后,视图会将其封装成一个 HttpResponse 对象。这个响应对象沿着来的路径返回:从应用视图,到Django核心,再通过网络发送回用户的浏览器。浏览器接收到响应后,将其解析并显示为网页,从而完成整个请求-响应循环。

总结

本节课中我们一起学习了Django应用在PythonAnywhere上的运行概览。我们梳理了从请求发出到响应返回的完整流程:settings.py 是应用的启动配置中心;urls.py 进行全局路由分发;应用内的 urls.py 进行细粒度路由;视图 处理核心业务逻辑并准备数据;模板 负责将数据渲染成用户看到的HTML界面。理解这个“大图景”对于构建和调试Django Web应用至关重要。

067:动态Web内容简介

在本节课中,我们将学习动态Web内容的基本概念,理解浏览器与服务器之间如何通过请求-响应周期进行通信,并初步了解构成现代Web应用的各种技术。

概述

当我们谈论构建Web应用时,理解底层的网络协议至关重要。虽然使用框架时无需了解所有细节,但在调试应用时,这些知识必不可少。本节课将介绍动态Web内容的基础,包括请求-响应模型、网络套接字以及构成Web页面的前端与后端技术。

请求-响应周期

上一节我们介绍了动态Web内容的重要性,本节中我们来看看其核心机制:请求-响应周期。

浏览器是一个运行在您电脑或手机上的应用程序。它监视着来自屏幕、键盘或鼠标的事件。当您点击网页上的某个链接时,浏览器会拦截这个点击事件。

随后,浏览器会打开一个网络套接字,连接到远程的Web服务器,并发送一个请求。这个请求是特定格式的,包含一个GET命令和它想要获取的URL。

代码示例:一个简单的HTTP GET请求

GET /page1.html HTTP/1.1
Host: www.example.com

服务器收到请求后,会在内部进行一系列工作,例如读取磁盘文件或运行程序来生成您所需的内容。完成这些工作后,服务器会通过同一个套接字连接将响应发送回浏览器。

浏览器接收到响应(通常是HTML文档)后,会解析其中的HTML、CSS和JavaScript代码,最终将这些代码渲染成您所看到的可视化页面。

以下是请求-响应周期的关键步骤:

  1. 用户交互:用户在浏览器中触发一个事件(如点击链接)。
  2. 建立连接:浏览器打开一个网络套接字连接到Web服务器。
  3. 发送请求:浏览器通过该连接向服务器发送格式化的HTTP请求。
  4. 服务器处理:服务器处理请求,生成相应的内容。
  5. 返回响应:服务器将生成的HTML等内容通过连接发回浏览器。
  6. 渲染显示:浏览器解析接收到的内容,并将其渲染显示给用户。

Web应用的技术栈

理解了基本的通信模型后,我们来看看构建一个完整Web页面所涉及的技术。

Web应用的技术主要分为两大类:前端技术后端技术。前端技术负责网页的外观、感觉和交互性,运行在用户的浏览器中。后端技术则负责在远程服务器上处理数据、生成并发送HTML、CSS和JavaScript

公式:Web页面 = 前端技术 + 后端技术

以下是构成现代Web应用的主要技术组件:

  • 前端技术:包括HTML(结构)、CSS(样式)、JavaScript(交互逻辑)以及jQuery、Vue.js等库和框架。
  • 后端技术:包括Django、Flask等Web框架,以及MySQL、PostgreSQL等数据库。
  • 网络协议:HTTP/HTTPS协议是浏览器与服务器通信的基石。尽管HTTP/2等新协议允许在单个连接中传输多个文档,但其核心的请求-响应模型保持不变。

本课程的重点将更多地放在后端技术上,即如何使用Django等工具处理数据逻辑,而如何让页面看起来更美观则属于前端设计的范畴。

总结

本节课中我们一起学习了动态Web内容的基础。我们了解了浏览器与服务器之间通过请求-响应周期进行通信的完整流程,认识了网络套接字在这一过程中扮演的角色。同时,我们也概览了构建Web应用所需的前端与后端技术栈。理解这些基本原理,是成为一名优秀的Web应用开发者的重要一步。在接下来的课程中,我们将深入探索这些技术,特别是如何使用Django框架来构建功能强大的后端。

068:网络套接字与连接 🔌

在本节课中,我们将要学习网络通信的基础概念——网络套接字。我们将了解套接字如何像电话系统一样工作,以及计算机如何通过IP地址和端口号进行通信。

网络套接字的概念 📞

网络套接字是计算机进行通信的方式。这个概念在20世纪60年代设计网络软件时被提出。其核心思想是,计算机之间的数据交换并非通过永久连接进行,而是像打电话一样:建立连接、交换数据、然后断开连接。


当时的设计者认为,由于未来会有大量计算机和数据源,让每台计算机都永久连接到所有数据是不现实的。他们设想了一个协议:当需要数据时,计算机“拨号”连接到数据源,获取数据后便释放连接。这种“连接-交谈-断开”的模式,使得互联网能够扩展到数十亿设备,就像电话系统能够服务全球所有人一样。

套接字的工作原理

在软件中,套接字是通过一个名为“Sockets”的抽象库来实现的。套接字本质上就是计算机之间的电话呼叫。

以下是套接字通信的基本步骤:

  1. 发起呼叫:一方知道另一方的地址并发起连接。
  2. 等待应答:被呼叫方接受连接。
  3. 双向通信:连接建立后,双方可以进行读写操作,就像操作一个文件,但可以同时读写。
  4. 遵循协议:通信双方需要遵循约定好的协议,以确定谁先发送、谁先接收,就像打电话时说“你好”一样。

重要的是,即使你只是在浏览器中请求一个文件,你的程序也是在和服务器上的另一个应用程序对话。这个服务器端的应用程序负责读取文件并将其发送出来。例如,当我们上传图片时,后续查看图片也需要通过服务器端的应用程序来决定是否有权限访问,而不仅仅是直接获取文件。

IP地址与端口号 🌐

上一节我们介绍了套接字的基本工作模式,本节中我们来看看计算机如何精确地找到彼此并进行对话。这依赖于IP地址和端口号系统。

每台联网的计算机都有一个IP地址,它是一个数字标识。目前主要有两种IP地址格式:IPv4和IPv6。IPv4地址由四组用点分隔的数字组成,例如 142.16.42.114

由于互联网通信本质上是应用程序之间的对话,我们需要一种方法来区分同一台计算机上运行的不同应用程序。这就是TCP端口号的作用。你可以把端口号想象成电话分机号:IP地址是总机号码,端口号是内部分机。

IP地址 : 端口号

因此,我们不需要为每个应用程序分配一个独立的IP地址,而是通过“IP地址+端口号”的组合来定位特定服务。

常见的端口号与应用

以下是互联网上一些常见服务及其对应的标准端口号:

  • 25:用于服务器之间的电子邮件通信(SMTP协议)。
  • 23:用于旧的远程登录服务(Telnet协议)。
  • 80:用于不加密的Web通信(HTTP协议)。
  • 443:用于加密的Web通信(HTTPS协议),这是当前的首选方式。
  • 109/110:用于邮件客户端(如Thunderbird)从服务器获取邮件的协议(POP3)。

在上图中,客户端(可能是一台个人电脑或另一台服务器)通过连接到IP地址 74.208.28.17725 号端口来发送电子邮件。箭头代表不同的协议,这意味着与电子邮件服务器通信需要使用电子邮件协议,与Web服务器通信则需要使用HTTP或HTTPS协议。

在开发Web应用时,我们最常打交道的是 80 端口(HTTP)和 443 端口(HTTPS)。

过渡到HTTP协议

了解了网络套接字和端口的基础后,接下来我们将简要看一下超文本传输协议(HTTP)。

HTTP正是我们用来与 80443 端口上的Web服务器进行对话的协议。在接下来的课程中,我们将深入探索这个构成万维网基础的协议。

总结

本节课中我们一起学习了网络通信的核心机制。我们了解到网络套接字是计算机间临时性、协议化的“电话呼叫”。通信通过“IP地址”定位计算机,再通过“端口号”定位计算机上的具体应用程序来实现。这种设计使得全球数十亿设备能够高效、有序地进行数据交换,构成了当今互联网的基石。

069:超文本传输协议 (HTTP) 🕸️

在本节课中,我们将要学习超文本传输协议 (HTTP)。这是浏览器与服务器进行通信所使用的核心协议。我们将了解它的历史、工作原理以及它如何成为互联网上最重要的协议之一。

协议简介与历史背景

超文本传输协议是浏览器用来与服务器对话的协议。我们也将它用于许多其他用途,因为它是一个非常简洁和优雅的协议。这个协议发明于1990年,距今已有三十多年。

当时存在许多不同的协议。你需要一个特定的客户端软件来匹配特定的服务器软件,并连接到正确的端口进行通信。蒂姆·伯纳斯-李和罗伯特·卡里奥在CERN的一项发明是:与其使用众多不同的软件,不如创建一个称为“网络浏览器”的多协议软件。

统一资源定位符 (URL) 的创新

于是我们有了URL,即统一资源定位符。我们每天都在使用它,但在1990年,这本身就是一个惊人的创新。因为在过去,你需要使用特定的软件并连接到特定的地址。

URL包含了三个非常重要且独立的部分:

  1. 协议:允许使用多种协议。最常见的是 http://https://,其他你可能见过的还有 ftp://mailto:
  2. 主机:一个域名,这是获取服务器地址的一种很好的符号化方式。它最终会解析为一个IP地址,例如 141.206.142.2
  3. 文档:服务器内的一个文档。例如 /page1.htm 就是服务器上的一个文档。

在1990年之前,存在许多协议。但在1990年之后,HTTP这一协议因其简洁性,找到了进入众多优秀解决方案的途径。如今,除了电子邮件,它已成为我们在互联网上使用的最主要的协议。

HTTP的简洁性与成功

正如之前提到的,HTTP由蒂姆·伯纳斯-李和罗伯特·卡里奥在CERN发明,用于检索文档和图像。他们构建它时,并没有打算创造最伟大的东西,只是想做一个简单的东西。但结果证明,它成为了最伟大的东西,因为它非常基础且简单。

其他工程师会发现:“哇,我可以用一种不同的方式使用它。”它的基础概念非常简单:你连接到一台服务器,确定它的位置,发送一个带有少量额外数据的命令,然后你会得到一些东西作为回应——可能是一个文档、一张图片、数据或HTML。这确实非常了不起。

底层的套接字是建立“通话”连接的部分,而HTTP则是在这个“通话”建立后我们所做的事情。

互联网的开放标准:RFC

互联网从20世纪60年代开始如此成功的原因之一是其彻底的开放性。这是一种非常友好、相互尊重的协作氛围,允许批评。没有一个超级天才或一家公司单独设计了它,这是一场围绕一系列开放标准的协作。

这些标准通过会议制定,被称为“请求评论”(RFC)。制定这些标准的组织叫做互联网工程任务组(IETF)。RFC是一个有趣的概念,它体现了工程师精神:即使一个设计被所有人认为是最棒的,我们也应该始终质疑。没有设计是完美的,没有设计是无可置疑的。

这些RFC文档是100%公开的。正是这些开放的规范和标准,使得像Sun Microsystems这样的新公司,以及像Linux甚至Windows这样的新操作系统,能够作为完全参与者加入进来。它们只需阅读这些规范,就能立即与所有其他系统互操作。这是互联网运作的基础和根本。

深入HTTP协议规范

现在,让我们具体看看其中一个RFC。例如RFC 2616(关于HTTP/1.1)。如果你想构建一个网络浏览器或网络服务器,你可以阅读这份文档。它会详细告诉你如何发起请求、请求的格式(包括头部和消息体),以及客户端和服务器应遵循的协议规则。

不过,让我用更简单的方式告诉你它是如何工作的。

HTTP请求/响应周期详解

以下是HTTP请求/响应的基本工作流程:

  1. 建立连接:客户端通过套接字连接到服务器,通常是端口80(HTTP)或端口443(HTTPS)。
  2. 客户端发送请求:客户端首先发送一行请求,以回车换行符结尾。基本格式如下:
    GET /path/to/document HTTP/1.0
    
    其中,GET是方法,/path/to/document是URL路径,HTTP/1.0是协议版本。
  3. 发送可选的请求头:在请求行之后,客户端可以发送一系列头部信息,格式为 名称: 值。这些头部可以包含浏览器偏好的语言、浏览器能力、Cookie(用于身份验证)等信息。头部结束后,需要一个空行表示头部结束。
  4. 服务器发送响应:服务器收到请求后,会发回响应。响应首先包含一个状态行,例如:
    HTTP/1.1 200 OK
    
    其中,200是状态码,表示成功。其他常见状态码有404(未找到)。
  5. 发送响应头:状态行之后是响应头部,格式与请求头类似。这些头部包含关于响应的元数据,例如:
    Content-Type: text/html
    Last-Modified: Wed, 22 Oct 2023 12:00:00 GMT
    
    其中,Content-Type告诉客户端接下来数据的格式(如 text/htmlimage/pngapplication/json)。
  6. 发送响应体:在响应头部之后,有一个空行,然后是实际的响应数据(例如HTML文档、图片数据等)。客户端根据Content-Type来解析和呈现这些数据。

动手实践:使用Telnet模拟HTTP请求

你可以使用telnet命令在命令行中手动模拟这个过程,直接与Web服务器对话。这能让你直观地看到原始的HTTP协议交换。

以下是操作步骤:

  1. 打开终端(Windows命令提示符、Mac终端或Linux终端)。
  2. 输入命令连接到服务器的80端口(例如 data.pr4e.org):
    telnet data.pr4e.org 80
    
  3. 连接成功后,由于HTTP协议要求客户端先说话,你需要手动输入HTTP请求。为了快速准确,建议提前准备好并粘贴以下内容:
    GET /page1.htm HTTP/1.0
    
    
    注意:在第二行(即HTTP/1.0之后)必须按两次回车(即一个空行),以表示请求头结束。如果你要发送Cookie等头部信息,需要在空行之前添加。
  4. 输入并发送后,服务器将返回原始的HTTP响应,包括状态行、头部和HTML文档内容。

通过这个练习,你会看到你是在与一个软件(Web服务器应用)对话,而不是直接访问文件。你必须遵循正确的协议格式,否则服务器会返回错误。

命令行工具与Linux技能的重要性

我是一个使用控制台、终端和基于文本界面的忠实粉丝。我认为它们实际上更容易理解,并且非常棒。在某种程度上,所有花哨的图形用户界面都分散了我们对计算机内部简单性的注意力。

了解命令行,特别是Linux命令行,是一项非常重要的技能。在本课程中,我们将开始使用Web服务器,你最好开始学习一些Linux知识。知道如何启动服务器、调试应用程序、查找日志文件等,在真实的生产系统中至关重要。我们会在简化的环境中练习,但一旦进入真实的工作环境,这些简化环境就会消失。

课程预告与总结

接下来,我们将用Python向你展示,用尽可能少的代码,浏览器是如何工作的:浏览器如何发送HTTP协议请求,服务器如何响应HTTP协议并返回文档。

本节课中我们一起学习了:

  • HTTP协议的基本概念和历史。
  • URL的结构及其重要性。
  • 互联网开放标准(RFC)和IETF的作用。
  • HTTP请求/响应周期的详细步骤,包括状态码和头部信息。
  • 如何使用telnet工具手动模拟HTTP请求,深入理解协议细节。
  • 掌握命令行和Linux技能对于Web开发的重要性。

理解HTTP是理解Web如何工作的基石,它将帮助我们更好地学习后续的Django Web开发。

070:用Python构建简单Web浏览器 🖥️

在本节中,我们将学习如何使用Python的socket库构建一个非常简单的Web浏览器和服务器。我们将通过编写代码来模拟浏览器向服务器发送HTTP请求并接收响应的过程。


概述

我们将创建一个Python脚本,该脚本使用socket库连接到远程服务器,发送一个HTTP GET请求,并接收服务器的响应。通过这个过程,你将理解网络通信的基本原理,以及如何在Python中处理网络数据。


导入socket库

首先,我们需要导入Python的socket库。这个库内置在Python中,提供了网络通信的基本功能。

import socket

socket库的功能类似于文件操作,但它是用于发送和接收数据的网络端点。


创建和连接socket

创建和连接socket是一个两步过程:首先创建socket对象,然后连接到远程服务器。

以下是创建和连接socket的代码:

mysock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
mysock.connect(('data.pr4e.org', 80))
  • socket.socket():创建一个socket对象,类似于“制作一部电话”。
  • mysock.connect():连接到指定的域名和端口,类似于“拨打电话”。

如果连接失败(例如服务器不存在),connect方法会抛出异常。在实际应用中,我们通常会使用tryexcept来处理这类错误,但为了简化代码,这里省略了错误检查。


发送HTTP请求

连接成功后,我们需要向服务器发送一个HTTP GET请求。这个请求的格式与之前通过Telnet发送的请求完全相同。

以下是发送HTTP请求的代码:

cmd = 'GET http://data.pr4e.org/romeo.txt HTTP/1.0\r\n\r\n'.encode()
mysock.send(cmd)

  • cmd:这是一个字符串,包含HTTP GET请求。\r\n\r\n表示请求头结束。
  • encode():将字符串转换为UTF-8格式,因为网络传输通常使用UTF-8编码。
  • mysock.send():将编码后的请求发送到服务器。

接收服务器响应

发送请求后,服务器会返回响应。我们需要循环接收数据,直到服务器关闭连接。

以下是接收服务器响应的代码:

while True:
    data = mysock.recv(512)
    if len(data) < 1:
        break
    print(data.decode(), end='')
  • mysock.recv(512):每次接收最多512字节的数据。如果数据未到达,该方法会等待。
  • if len(data) < 1::如果接收到的数据长度为0,表示服务器已关闭连接,退出循环。
  • data.decode():将接收到的UTF-8数据解码为Unicode字符串,以便在Python中打印。

关闭socket

接收完所有数据后,我们需要关闭socket以释放资源。

mysock.close()

运行代码

如果你正确运行上述代码,它将连接到服务器并打印出HTTP响应头和内容。响应头之后是一个空行,然后是实际的页面内容。


调试工具

过去,我们通常使用Telnet或类似的工具来调试网络应用。但现在,浏览器的开发者工具提供了更强大的功能,可以查看请求头、响应头、发送的数据和接收的数据。这对于调试复杂的Web应用非常有用。


总结

在本节中,我们学习了如何使用Python的socket库构建一个简单的Web浏览器。我们通过创建socket、连接到服务器、发送HTTP请求、接收响应并关闭连接,模拟了浏览器与服务器之间的通信过程。虽然在实际开发中,我们通常使用更高级的库(如requests)来处理HTTP请求,但理解底层原理对于深入学习Web开发非常重要。

071:用Python构建简单HTTP服务器 🖥️

在本节课中,我们将学习如何使用Python的socket库构建一个最简单的HTTP服务器。我们将从零开始,理解服务器如何监听连接、接收请求并发送响应。


上一节我们介绍了如何使用Python构建一个简单的HTTP客户端(浏览器)。本节中,我们来看看如何构建一个HTTP服务器。

服务器核心概念

一个HTTP服务器的核心任务是:

  1. 监听一个特定的网络端口,等待客户端连接。
  2. 接受客户端的连接请求。
  3. 接收客户端发送的HTTP请求数据。
  4. 处理请求并构造一个HTTP响应。
  5. 发送响应数据回客户端。
  6. 关闭本次连接,然后回到第1步,等待下一个连接。

代码解析:一个简单的HTTP服务器

以下是构建一个简单HTTP服务器的核心代码结构。我们将逐部分解析其工作原理。

import socket

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/ff6928b67427fff626520a26f18f8625_20.png)

def create_server():
    # 1. 创建socket(“电话”)
    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        # 2. 绑定到本地地址和端口(“电话号码”)
        serversocket.bind(('localhost', 9000))
        # 3. 开始监听,允许最多5个连接排队
        serversocket.listen(5)
        while True:
            # 4. 等待并接受客户端连接(“接电话”)
            (clientsocket, address) = serversocket.accept()
            # 5. 接收客户端发送的请求数据
            rd = clientsocket.recv(5000).decode()
            # 6. 处理请求(这里只是打印请求行)
            lines = rd.split('\n')
            if len(lines) > 0:
                print(lines[0])
            # 7. 构造HTTP响应
            data = "HTTP/1.1 200 OK\r\n"
            data += "Content-Type: text/html; charset=utf-8\r\n"
            data += "\r\n"
            data += "<html><body><h1>Hello World</h1></body></html>\r\n\r\n"
            # 8. 发送响应并关闭连接
            clientsocket.sendall(data.encode())
            clientsocket.shutdown(socket.SHUT_WR)
    except KeyboardInterrupt:
        print("\nShutting down...\n")
    except Exception as exc:
        print("Error:\n")
        print(exc)
    finally:
        serversocket.close()

if __name__ == '__main__':
    print('Access http://localhost:9000')
    create_server()

关键步骤详解

以下是服务器工作流程中几个关键步骤的说明:

  1. 创建与绑定Socketsocket.socket()创建了一个通信端点。bind(('localhost', 9000))将其绑定到本机的9000端口。这就像安装了一部电话并分配了号码。
  2. 监听连接listen(5)告诉操作系统开始监听端口。参数5表示如果服务器正忙,操作系统可以为其排队最多5个等待的连接。
  3. 接受连接accept()是一个阻塞调用。程序会停在这里,直到有客户端发起连接。一旦连接建立,它会返回一个新的socket对象(clientsocket)用于与这个特定的客户端通信。
  4. 接收请求:服务器调用clientsocket.recv(5000)来接收客户端发送的数据。根据HTTP协议,客户端必须先发送请求
  5. 构造与发送响应:服务器按照HTTP响应格式(状态行、头部、空行、正文)组装数据,并通过clientsocket.sendall()发送回去。
  6. 关闭连接:发送完毕后,服务器调用shutdown()close()来关闭连接。客户端在接收完数据后也会关闭其连接,完成一次完整的“通话”。


上一节我们详细解析了服务器代码的每个部分。本节中,我们将看看如何运行并与这个服务器交互。

运行与测试服务器

要运行这个服务器,你需要在一个终端窗口执行Python脚本。

启动服务器

在命令行中运行:

python server.py

输出会显示:

Access http://localhost:9000

此时,服务器已在后台运行并等待连接。

使用浏览器测试

打开你的网页浏览器(如Chrome, Firefox),在地址栏输入:

http://localhost:9000

然后按回车。

你将看到

  • 浏览器窗口显示“Hello World”。
  • 服务器的终端窗口会打印出类似这样的日志:
    GET / HTTP/1.1
    GET /favicon.ico HTTP/1.1
    
    第一行是你访问根路径的请求。第二行是浏览器自动请求网站图标(favicon.ico)的请求,这是浏览器的默认行为。

使用Python客户端测试

除了浏览器,我们也可以用Python写一个简单的客户端来测试服务器。以下是两种方式:

方式一:使用底层socket(模拟原始请求)

import socket

mysock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
mysock.connect(('127.0.0.1', 9000))
cmd = 'GET / HTTP/1.1\r\n\r\n'.encode()
mysock.send(cmd)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/ff6928b67427fff626520a26f18f8625_46.png)

while True:
    data = mysock.recv(512)
    if len(data) < 1:
        break
    print(data.decode(), end='')
mysock.close()

方式二:使用高级的urllib库

import urllib.request
fhand = urllib.request.urlopen('http://localhost:9000')
for line in fhand:
    print(line.decode().strip())

运行任何一个客户端脚本,你都会在客户端窗口收到服务器返回的“Hello World”响应,同时在服务器窗口看到相应的GET请求日志。


总结与展望

本节课中我们一起学习了如何使用Python的socket库构建一个最简单的HTTP服务器。我们理解了服务器从监听端口、接受连接、接收HTTP请求到发送固定响应的完整生命周期。

这个服务器功能非常有限,无论收到什么请求,它都只会回复“Hello World”。然而,它清晰地演示了Web服务器最核心的工作原理。在实际开发中,我们会使用像Django、Flask这样的成熟框架,它们封装了这些底层细节,让我们能专注于处理不同的URL请求和生成动态内容。

当你未来运行Django开发服务器时,在终端里看到的那些GET /admin/POST /submit/日志,其底层原理与我们今天构建的这个简单服务器是一脉相承的。理解这个基础,能帮助你在遇到更复杂的问题时,知道如何深入探究。

072:浏览器开发者模式详解 🔍

在本节课程中,我们将学习如何使用浏览器的开发者工具,特别是调试控制台。我们将了解如何查看网络请求、检查文档对象模型(DOM)、以及调试JavaScript代码。这些工具对于Web开发至关重要,能帮助我们理解浏览器与服务器之间的交互过程。

概述

浏览器开发者工具是现代Web开发的核心工具之一。它允许开发者检查网页的HTML结构、调试JavaScript代码、监控网络请求以及修改CSS样式。本节将介绍其基本功能和使用方法。

浏览器开发者工具的打开方式

在Firefox浏览器中,可以通过菜单栏的“工具”选项找到开发者工具。在Chrome浏览器中,则通过“视图”菜单下的“开发者”选项进入“JavaScript控制台”。两种浏览器的工具界面相似,都提供了多个功能选项卡。

网络请求监控

网络监控功能可以显示每个请求-响应周期的详细信息。例如,当我们访问一个网页时,浏览器会向服务器发送HTTP请求,并接收响应。开发者工具可以展示请求的URL、HTTP方法、状态码以及请求和响应头信息。

以下是查看网络请求的步骤:

  1. 打开开发者工具,切换到“网络”选项卡。
  2. 刷新页面,工具将记录所有网络请求。
  3. 点击任意一个请求,可以查看其详细信息。

例如,一个成功的请求会返回状态码 200 OK,而一个不存在的页面会返回状态码 404 Not Found。服务器错误则通常返回 500 系列状态码。

文档对象模型(DOM)检查

“检查元素”功能允许我们查看和修改当前页面的文档对象模型(DOM)。DOM是浏览器解析服务器返回的HTML代码后生成的内存中的树状结构。

需要注意的是,在“元素”面板中看到的DOM结构可能与服务器返回的原始HTML源代码不同。因为JavaScript可以在页面加载后动态修改DOM。

例如,我们可以直接在“元素”面板中修改一个标签的样式:

<p style="color: red;">这段文字会变成红色</p>

这种修改仅发生在浏览器端,服务器上的原始文件并未改变。

开发者工具的高级功能

除了基本检查,开发者工具还提供其他实用功能:

  • 持久化日志:勾选此选项可以保留多个页面请求之间的控制台日志,方便追踪跨页面的问题。
  • 禁用缓存:在调试时启用,可以确保浏览器每次都从服务器获取最新资源,而不是使用本地缓存。
  • 性能分析:对于复杂页面,网络选项卡会显示所有资源(如CSS、JavaScript、字体、图片)的加载情况。一个页面可能由数十个甚至上百个请求构成,这些工具能帮助分析页面加载性能。

浏览器选择建议

对于Web开发和学习,建议使用 FirefoxChrome 浏览器。它们的开发者工具功能全面且对开发者友好。Safari浏览器的开发者工具在功能和信息展示上相对局限,可能会给调试工作带来不便。因此,在本课程中,我们将主要使用Firefox或Chrome进行开发和测试。

总结

本节课我们一起学习了浏览器开发者模式的核心功能。我们了解了如何打开开发者工具、监控网络请求与分析状态码、检查与实时编辑DOM,并认识了日志持久化、缓存禁用等高级调试功能。掌握这些工具是进行高效Web开发的基础,它们能帮助我们深入理解网页的运行机制并快速定位问题。

073:吉多·范罗苏姆的欢迎致辞

在本节课中,我们将学习Python语言创始人吉多·范罗苏姆的欢迎致辞。他将分享关于Python、编程学习以及社区支持的重要观点。

我是吉多·范罗苏姆,我创造了Python语言。

我在Python上工作了25年,当然,也与Python社区中一个庞大的群体共同协作。

看到你们所有人都参加这门课程,我感到非常兴奋。

我为你们使用我的语言来学习感到非常自豪。

Python只是你们踏上编程之路的第一步。

编程中有许多令人兴奋的东西可以学习,其中很多都可以使用Python来学习,也有很多可以使用其他工具来学习。

你们不会孤单。

将有数以百万计的人走在你们前面,或者与你们同时学习Python。

你们可以互相帮助。

你们可以一起学习。


本节课中我们一起学习了Python创始人吉多·范罗苏姆的欢迎致辞。他鼓励初学者以Python为起点开启编程之旅,并强调了学习过程中的社区支持与协作精神。

074:布莱切利园面对面办公时间

在本节课中,我们将跟随查克教授,回顾2019年在英国布莱切利园与来自世界各地的在线课程学生举行的一次特殊线下见面会。你将看到学生们如何将课程中学到的知识应用于不同领域。


查克在这里。现在是2019年,我们来到了布莱切利园。

似乎每逢奇数年份的春天,我们都会来到这里,尽管现在景色非常美丽,树木正在开花,但我们又一次来到了布莱切利园。因此,我想让你认识一些你的同学。

以下是部分同学的介绍。

卡勒曼
你好。我叫卡勒曼,来自罗马尼亚。我学习了历史课程,并使用“Python for Everybody”课程来教导其他同事。

查克
那么你在罗马尼亚教了多少学生Python?我会在这个视频里放一些你教Python的照片。

凯娜
大家好,我叫凯娜·塞维安诺,原籍秘鲁,现居伦敦。我是一名供应链专业人士,希望使用Python和其他工具来解决供应链问题。我对课程非常感兴趣,谢谢你。我正在学习“Python for Everybody”专项课程,刚刚完成第一门课,非常期待继续学习第二门。

玛丽亚
谢谢。我叫玛丽亚,原籍俄罗斯,现居伦敦。我在2000年完成了“Python for Informatics”课程。从那时起,我就想见查克博士,但一直没能实现。我必须说,这个专项课程间接帮助我在伦敦的谷歌找到了工作。

查克
哇,恭喜你,谢谢。


你好,我是来自丹麦哥本哈根的乔。互联网在呼唤,我使用这些课程中的一些材料来撰写关于技术、人类和创新如何互动的论文。

查克
你真的是从哥本哈根飞过来的吗?


我昨天飞过来的,就为了这次见面。

查克
这令人印象深刻,谢谢你。

奥利维亚
你好,我是奥利维亚,我住得离这里很近。

查克
你不是飞来的吧?

奥利维亚
不,不是,只是开车来的。我正在学习“Python for Everybody”课程,快学完了。我是一名海洋生物学家,使用Python来研究海绵的基因组。

查克
所以,最终你会用这门课程来攻读博士学位吗?

奥利维亚
是的,真的很酷。Python可以用于任何事情。

阿玛尼
你好,我叫阿玛尼,来自赖顿。我学习了“Python for Everybody”专项课程中的三门课。我是苏塞克斯大学的博士候选人。

查克
你的研究课题是什么?

阿玛尼
云计算和车载网络。

查克
你知道吗,我有一个理论。我的理论是,如果我们给所有汽车装上Wi-Fi,让它们互相广播自己的位置,这正是我们正在研究的。

阿玛尼
是的,互联汽车。是的,如果汽车能知道其他汽车的位置,会很有帮助,对吧?

查克
是的。

贾尼斯
你好,我是贾尼斯,原籍希腊。我从事计算机科学工作,但我非常喜欢这门课程,并且经常向非IT领域的朋友推荐它。这就是我来到这里的原因,非常感谢你邀请我们。

埃尔
你好,我是埃尔,原籍希腊,现居伦敦。编程是我的业余爱好,我非常喜欢去年的这门课程。

马丁
你好,我是来自伦敦的马丁。我是一位悠闲的绅士,但我决定学习“Python for Everybody”课程,只是为了不让我的大脑生锈。

查克
我很高兴你成功来到了这里。但我们会继续制作更多专项课程,这样你的大脑就永远不会变成浆糊了。

史蒂夫
你好,我是史蒂夫,我是你Python顶点课程和“Web Applications for Everybody”课程的助教之一。

查克
是的,这是我和史蒂夫第三次在布莱切利园见面了。但就在一周前,在密歇根,我们在安娜堡校区举行了五周年庆祝活动,我把史蒂夫请了过去。所以,让我们大家为史蒂夫和他所做的所有工作热烈鼓掌。

好的,我相信我的下一次行程……有两件事。首先,我将直接从伦敦去参加赛车比赛,我想邀请大家到伊利诺伊州的赛车场来看我,但你们可能去不了伊利诺伊州的赛车场。南非可能是我下一个会见学生的地方,所以希望能在南非见到你们。干杯。


本节课中,我们一起学习了查克教授在布莱切利园与全球学生线下交流的片段。我们看到,来自不同国家、拥有不同背景(如历史、供应链、生物学、计算机科学等)的学生们,都通过“Python for Everybody”系列课程掌握了编程技能,并将其应用于学术研究、职业发展和个人兴趣中。这次见面会生动地展示了在线教育如何连接世界,以及Python作为一种工具的强大通用性。

075:前两个数据包与互联网的诞生

在本节课中,我们将回顾互联网发展史上一个标志性时刻:1969年10月29日,从加州大学洛杉矶分校(UCLA)到斯坦福研究院(SRI)发送的前两个数据包。我们将了解这一事件发生的背景、过程及其深远意义。

背景:资源共享的需求

上一节我们介绍了互联网诞生的时代背景,本节中我们来看看其最初的设计目标。互联网的前身阿帕网(ARPANET)并非为军事目的而设计,其核心驱动力是资源共享

美国国防部高级研究计划局(ARPA)希望建立一个网络,以便全国各地的研究人员能够共享昂贵的大型计算资源。例如,犹他大学拥有出色的图形操作系统,斯坦福研究院拥有数据库,加州大学洛杉矶分校拥有仿真系统,伊利诺伊大学拥有高性能计算设备。每当有新研究员加入时,他们都希望获得与其他机构相同的能力。ARPA的解决方案是:通过网络登录到其他机构的计算机来使用这些资源。

因此,网络设计的核心理念是:资源共享。这与许多人认为的“防止核打击”的军事起源不同。

设计与建设:从理论到现实

认识到资源共享的需求后,ARPA主任鲍勃·泰勒邀请拉里·罗伯茨管理该项目。罗伯茨找到了伦·克莱因罗克,因为他拥有相关的排队论等理论基础,能够证明分组交换网络是可行的。

以下是网络设计阶段的关键参与者及其贡献列表:

  • 赫布·巴斯金:提出了时间共享需求,要求网络必须在半秒内传递短消息。最终网络实现了200毫秒的延迟。
  • 韦斯·克拉克:提出了关键理念,将通信功能从主机中分离出来,交由专门的“接口消息处理机”(IMP)负责。
  • 伦·克莱因罗克:坚持在设计中加入软件,以便进行实验、生成流量、设置测量钩子并评估结果。
  • 霍伊·弗兰克:强调了网络可靠性,要求网络不能有单点故障。这催生了两连通拓扑的设计,即每对节点之间必须有两条独立路径。

这些设计规范交给了BBN公司来建造IMP。BBN在合同签订8个月后,按时、按预算将第一台IMP交付给了UCLA。

历史性时刻:发送“LO”

1969年9月2日,UCLA的IMP与本校的主机成功连接。同年10月,斯坦福研究院(SRI)也安装了一台IMP,并通过一组4条、每条8 kbps的线路与UCLA相连。一个两节点的网络就此形成。

1969年10月29日晚10点30分,历史性的一刻到来。程序员查理·克莱恩坐在UCLA的终端前,试图通过阿帕网远程登录到40英里外的SRI主机。他们通过电话进行协调。

以下是当时尝试发送登录命令“LOGIN”的过程记录:

  1. 查理输入字母“L”。电话那头SRI的比尔·杜瓦尔确认:“收到L。”
  2. 查理输入字母“O”。比尔确认:“收到O。”
  3. 然而,当查理尝试输入字母“G”时,SRI的系统崩溃了。

因此,在互联网上成功发送的第一个单词是 “LO”。克莱因罗克教授幽默地指出,这恰好是“Lo and behold”(你瞧)的开头,仿佛预言了互联网时代的到来。如今,在UCLA博尔特大厅的一处入口地面上,有一组用瓷砖拼成的ASCII码图案,正是“LO AND BEHOLD”,以纪念这一事件。

文物与记录:IMP与日志

在UCLA复原的实验室里,存放着互联网上的第一件设备:IMP Number 1。这是一台由BBN改造的Honeywell小型机,作为网络交换机使用。

比硬件更重要的是记录。在程序员乔恩·波斯特尔的坚持下,团队从1969年10月开始维护一份“IMP日志”。在这本工程师的日志中,查理·克莱恩亲手记录了那个历史性夜晚:“与SRI通话,主机到主机。”这是互联网第一次通信存在的唯一书面记录。

核心理念:分布式控制与开放精神

在设计与早期运维中,两个核心理念得以确立:

  1. 分布式控制:克莱因罗克从香农的信息论中得到启发,认为大型网络不能有单一控制点,必须将控制权委托给所有对等节点。这成为了互联网的基础架构原则。
  2. 开放与协作的研究文化:ARPA向首席研究员提供资金时,采取的是“你是专家,去做你最擅长的事”的信任态度。克莱因罗克将这种哲学传递给他的研究生,鼓励他们自主研究,例如开发主机到主机协议。这种非产品化、以研发创造为核心的文化,是互联网成功的关键。

本节课中我们一起学习了互联网起源的关键故事。我们了解到,互联网诞生于资源共享的实用需求,其首次通信发送了“LO”两个字母。通过早期设计,可靠性可实验性分布式控制等核心原则被确立。第一台IMP设备和原始的工程师日志,成为了这段历史的实物见证。最重要的是,一种开放、协作、鼓励创新的研究文化,为互联网未来的蓬勃发展奠定了基石。

076:Cookie与会话

在本节课中,我们将学习Web开发中的两个核心概念:Cookie和会话。我们将首先了解Cookie的工作原理,它是浏览器端的概念,然后探讨会话,这是服务器端的概念。理解这两者的区别和联系对于构建需要用户状态管理的Web应用至关重要。

Cookie:浏览器端的标记

上一节我们介绍了课程概述,本节中我们来看看Cookie。Cookie是服务器存储在用户浏览器中的一小段数据。它的主要目的是让服务器能够识别和区分来自不同浏览器的请求。当Web服务器处理来自互联网上成千上万个浏览器的请求时,它需要一种方法来“标记”每个浏览器,以便知道正在与谁通信。

Cookie与登录没有直接关系。它的作用是在每个浏览器上留下一个标记,并在每次请求时将这个标记返回给服务器。通常,当服务器首次收到一个没有标记的浏览器请求时,它会生成一个唯一的标识符(通常是一个随机数)并通过响应头发送给浏览器,这个过程称为“设置Cookie”。

以下是关于Cookie的几个关键点:

  • 作用域与安全性:Cookie与特定的网站(域名)关联。一个网站只能读写自己设置的Cookie,不能访问其他网站的Cookie,这确保了基本的安全性。
  • 有效期:Cookie可以设置有效期。有两种主要类型:
    • 持久性Cookie:设置一个固定的过期时间(例如1000秒后或某个具体日期)。浏览器会将其保存到磁盘,即使关闭浏览器,在有效期内再次访问网站时也会发送该Cookie。
    • 会话Cookie:不设置过期时间,其生命周期与浏览器会话一致。当用户关闭所有浏览器标签页和浏览器窗口时,此类Cookie会被清除。

实际操作:查看和设置Cookie

理解了Cookie的基本概念后,让我们通过实际操作来加深理解。我们可以在浏览器的开发者工具中查看当前网站的Cookie。

以下是在Django视图中处理Cookie的示例代码:

def cookie_demo(request):
    # 打印请求中携带的所有Cookie(字典形式)
    print(request.COOKIES)

    # 创建一个HTTP响应
    response = HttpResponse("C is for cookie and it's good enough for me")

    # 设置一个会话Cookie(关闭浏览器后失效)
    response.set_cookie('zap', 42)

    # 设置一个持久性Cookie,1000秒后过期
    response.set_cookie('sakaicar', 42, max_age=1000)

    return response

代码执行流程如下:

  1. 当浏览器首次访问该视图且未携带Cookie时,request.COOKIES 是一个空字典。
  2. 视图函数创建一个响应对象,并通过 response.set_cookie() 方法设置两个Cookie。
  3. 服务器在响应头中发送 Set-Cookie 指令。
  4. 浏览器接收到响应后,会将Cookie存储起来。
  5. 此后,浏览器向同一网站发起的每一次请求,都会在请求头中自动携带这些Cookie。此时在视图函数中,request.COOKIES 字典将包含键值对 {'zap': '42', 'sakaicar': '42'}

会话:服务器端的状态管理

我们已经了解了Cookie如何在浏览器端存储标记。本节中我们来看看会话,它利用这个标记在服务器端存储更多用户相关的数据。

会话是存储在服务器上的数据,用于在同一个用户的不同请求之间保持状态。Cookie最常见的用途之一就是为会话提供支持:服务器将唯一的会话ID(通常是一个长的随机字符串)存储在浏览器的Cookie中。当浏览器发送请求时,会附带这个会话ID Cookie,服务器通过这个ID找到对应的会话数据。

在Django中,会话的使用非常简便。request.session 对象就像一个字典,你可以用它来存储和读取当前会话的数据。

# 在会话中存储数据
request.session['favorite_color'] = 'blue'

# 从会话中读取数据
favorite_color = request.session.get('favorite_color', 'unknown')

Django会自动处理会话ID Cookie的创建、发送和验证。开发者只需关心 request.session 中的数据即可,无需直接操作底层的Cookie。

总结

本节课中我们一起学习了Cookie和会话。

  • Cookie 是服务器设置在浏览器的小型数据片段,主要用于标识浏览器。它可以是临时的(会话Cookie),也可以是长期的(持久Cookie)。
  • 会话 是服务器端的概念,用于存储用户在一次访问过程中的状态信息。它依赖于Cookie(通常是存储会话ID的Cookie)来关联浏览器与服务器上对应的数据。

理解Cookie作为浏览器标记的机制,以及会话如何利用这个标记在服务器端管理用户状态,是构建交互式Web应用的基础。在接下来的学习中,你将看到如何运用会话来实现用户登录、购物车等功能。

077:Django会话管理 🍪

在本节课中,我们将要学习Django中的会话管理。我们将了解会话是什么,它们如何与浏览器Cookie协同工作,以及如何在Django视图中使用会话来存储和读取跨多个请求的数据。


上一节我们介绍了Cookie以及服务器如何在浏览器中设置Cookie。本节中我们来看看如何利用Cookie做一些有用的事情,即构建会话。会话是服务器上的一块存储区域,用于记录诸如“某某用户已登录”之类的信息。

在服务器内部,我们已在Django中启用了一段名为“会话中间件”的代码。它的基本作用是在每个传入请求到达urls.py之前,将自己“注入”到处理流程中。它利用特定的Cookie值(例如3e)来查找对应的会话,并将其与请求关联起来。因此,当请求进入urls.py并路由到你的视图函数时,Cookie已被设置,会话也已建立(如果需要则创建新会话,如果需要则查找旧会话)。服务器上存储着许多会话,它们保存在数据库、文件或其他地方。基本上,每个不同的浏览器都有一个对应的会话对象,我们通过之前设置的Cookie值来查找它们。会话中间件的一个功能是,如果它没有看到传入的Cookie,它会生成一个大的随机数,发送回浏览器作为Cookie,同时创建一个新的会话。这样,在未来的所有请求中,它都能通过这个Cookie查找到该会话。

通常,当我们遇到一个新浏览器时,就会立即开始会话。这更简单。虽然有时为了节省一点资源可以延迟创建,但对于大多数应用来说,通常是一开始就使用会话。之后,当用户登录时,我们会用某个唯一的随机数标记每个浏览器。当该Cookie被发送回服务器时,我们就可以将传入的请求与对应的会话重新关联起来。

Django中间件负责处理会话的创建、删除、数据插入、删除和更新。会话标识符是一个我们首次遇到浏览器时放入浏览器Cookie的大随机数。然后,我们使用相同的数字在服务器上存储一些数据。之后,当该Cookie在后续请求中返回时,我们基本上会在请求经过urls.py进入我们的视图之前,查找会话数据并将其与请求重新关联。

我们需要启用此功能。如果你使用django-admin创建项目,这已经默认配置好了。你可以查看你的settings.py文件,会发现它已存在。你可能已经执行过多次makemigrationsmigrate命令,并看到它创建了一个会话表。这些都是默认设置,因此你的大多数Django应用都已配置为支持会话,并且将会话存储在数据库中,这对于中小型站点来说是一个很好的方式。正如所说,会话将存储在数据库中,你执行迁移时会看到它为你创建了一个sessions表,位于你的SQLite数据库(如db.sqlite3)中。

如上所述,我们遇到了这些浏览器。我们在数据库中创建了一个会话对象,并在每个浏览器中放置了一个Cookie。这些Cookie名称相同(例如sessionid),但值是我们选取的随机数。当一个浏览器发送请求时,它会带上它的Cookie。Django中间件在所有会话中查找,然后将其连接起来,再处理请求。因此,有许多浏览器,每个都有不同的数字;也有许多会话,每个也有不同的数字。连接的过程可能包括创建新会话。现在,让我们从单个浏览器和单个会话对象的角度来看。

在一个会话存储区域中,某个时刻,请求进入,会话被重新关联。我们进行一些操作,将一些数据写入会话,然后返回响应。在另一个请求中,我们可以读取之前写入的数据。我们写入会话,可以从另一个请求读取。在这里,我们可以读取相同的信息。然后,我们收到一个POST请求,读取一堆信息,但也写入更多信息。因此,你可以在某种程度上放松对细节的纠结,认识到这个会话是一个跨越多个请求-响应周期的变量。Cookie变量也跨越多个周期。而GET和POST数据则会消失。每个请求的GET数据,你使用它、消耗它或存入数据库,但一个请求的GET数据在下一个请求中是不可用的。因此,我们有时需要使用会话这类东西来存储少量信息,以便在后续请求中能取回。

请求对象有一个名为request.session的小属性,它基本上是一个字典。我们可以向其中放入键、删除键、更新键,可以调用.get()方法来检索一个键以查看它是否存在,等等。因此,在某种意义上,我们只需取出request.session并将其视为一个神奇的字典。但我们必须记住,每个浏览器都有自己的字典,而当我们在一个视图函数中时,我们只处理一个浏览器,因此该会话就是与该浏览器关联的那个。所以,它是跨越所有这些请求的持久化数据接口

以下是一个非常简单的视图函数示例:

def view_count(request):
    num_visits = request.session.get('num_visits', 0) + 1
    request.session['num_visits'] = num_visits
    if num_visits > 4:
        del request.session['num_visits']
    return HttpResponse(f'view count={num_visits}')

在这个视图函数中,我们通过request.session.get('num_visits', 0)获取键为'num_visits'的值,如果不存在则默认为0。然后将其加1,再存回会话。这类似于计数的方式:如果之前是4,就加1变成5;如果不存在,就设为0然后加1变成1。因此,num_visits要么是1,要么是之前的值加1。然后,我们实际存储回会话的代码就是request.session['num_visits'] = num_visits。就这样。它是一个字典,一个神奇的字典。它在请求进来之前就已存在,我们只是修改它,神奇的后台机制会负责更新所有内容。此外,如果计数超过4,我们会通过del request.session['num_visits']将其删除。这样,计数会循环:1, 2, 3, 4, 1, 2, 3, 4...。del是标准的Python语法,用于从字典中删除一个条目。然后,我们只是在响应字符串中打印出访问次数。

如果我们开始访问,最初没有Cookie,也就没有会话。然后我们进入会话代码,你会看到它设置了一个会话Cookie。我们看到一个名为sessionid的会话Cookie,它有一个过期日期(不是浏览器关闭就过期的那种会话),还有一个大的随机数。这基本上就是会话ID,直到我们获得新会话。作为副作用,我们还在服务器的会话中存储了值1并打印出来。我们进来,建立了会话,但我们的视图代码并没有主动建立会话,我们只是从中获取了东西。这是因为会话的建立发生在中间件中,这在我们代码之外。中间件在我们的代码开始之前和结束之后运行,因此会话相关的事情在我们操作时就已经处理好了。


视图计数为1。然后,当我们点击刷新时,它会发送该会话ID的Cookie进来。它会加载会话GJ2233,然后我们的代码将该数字加1并存储回会话,于是变成2。如果我们继续刷新,会显示3、4,然后重置为1。在这段代码中,我们只是使用request.session来存放和读取数据。而Cookie是用于查找会话的东西。


如果我们想查看会话表中有什么,它确实只是一个表。你可以通过迁移看到它被创建。我们可以直接进入数据库,查询表,看到django_session表,然后执行SELECT * FROM django_session;,这将显示其中的数据。我们会看到(即session_key),以及一个时间戳(用于使会话过期,因为会话不会永远存在,Django中间件会定期清理它们,否则会话将永远存在)。实际数据是这个长字符串。这是一个编码后的字符串,如果你真想查看,它是使用一种叫做base64的技术编码的。Base64是一种将任意数据编码为基本上由字母和数字(大小写)组成的方法。它有点不那么紧凑,会扩展数据,但能确保没有特殊字符,避免在存储或传输时出现问题。如果我们查看我们的数据,并获取那个base64编码的变量,解码后,你会发现它基本上就像一个键值对,看起来很像一个字典。它是一个被序列化成JSON格式的字典。我们可以用JSON库解析它,看看里面有什么。不过你不需要太担心这个,它只是这个长字符串在每个请求-响应周期中被读取和写入(当你修改会话数据时写入)。


本节课中我们一起学习了Django会话管理。我们讨论了Cookie,这是一种用大随机数标记每个浏览器的方法;以及会话,这是在数据库中存储的、由该大随机数索引的一些数据。每个请求进来时,我们取回Cookie,然后查找会话。会话数据(基本上是键值对,即一个字典)都存储在那里。我们还简单介绍了如何在Django中使用request.session来访问和实现会话功能。

078:韩国首尔面对面办公时间

概述

在本节课中,我们将跟随课程讲师查克前往韩国首尔,体验一次特别的线下办公时间。我们将看到来自世界各地的学习者如何聚集一堂,分享他们学习Python和Django的经历,并感受开源社区与知识分享的温暖氛围。


前往办公时间

讲师查克正在前往韩国首尔办公时间的路上。和往常一样,他并不知道会有多少人来参加。可能是一个人,也可能是二十个人,这种不确定性正是其乐趣的一部分。这就像一场与成千上万人的“盲约”。

首尔线下聚会

我们现在来到了首尔又一次的办公时间现场。这是一个规模庞大的聚会,地点距离著名的“交叉双臂”雕像不远,这个雕像似乎与某个重要的Gangnam Style视频有关。

和以往一样,查克希望介绍在场的同学们,让他们打个招呼。由于人数众多,这会花上一点时间。

以下是到场同学的自我介绍:

  • 第一位同学:我的名字是(此处为韩语名)。我正在学习Python。
  • Shelby:我主修创意写作,这与编程无关,但我对语言学感兴趣。因此,我从今年夏天开始学习Python课程,已经完成了三门。我有很多时间。查克回应道:看来我们让你上瘾了。你大概也同意编程就像写作,但又有不同。Shelby表示:我喜欢各种写作和语言。查克说:对我来说,这正是课程开始时使用的例子。
  • Shiino:我实际上是跟着Shelby来的。我正在认真考虑学习Python这门很棒的语言,你的课程看起来也非常棒。
  • Victor Lee:我是Victor Lee,我在韩国翻译了Python教材。查克此时展示了Victor翻译的韩语版教材,并请大家为Victor鼓掌。查克说:我非常不擅长自拍,但我非常感谢你将这本书翻译成韩语。我付了你一杯啤酒的报酬让你翻译Python2的版本,或许我可以再付你几杯啤酒来翻译Python3的版本。我们非常感谢你,这一切的核心就是每个人都在贡献,Victor为我们贡献了很多。
  • Suin:Python是一门非常有趣的语言。我每天使用Scrapy和Tkinter。很高兴认识你,非常感谢。
  • John Gongsong:很高兴在这里见到查克,感觉像见到了名人。我之前已经在笔记本电脑上见过你了,所以现在能亲眼见到真是太好了。
  • Chong Min:我在你的课程中学到了很多乐趣,谢谢。
  • Honalo(来自巴西):我热爱Python,热爱学习。我和我的未婚妻Priscilla正在首尔度假。Priscilla也打了招呼。查克提到:这真是个愉快的巧合。就像九月份在拉斯维加斯的办公时间一样,来参加的人都不是本地人,全是在拉斯维加斯度假的。
  • Amber:我刚刚开始学习《Python for Everyone》课程,很高兴加入大家。
  • Joine:很高兴在韩国见到你。我想感谢你确保每个人都拿到了啤酒,并完成了所有翻译工作以确保我们收到了需要的订单。
  • 另一位同学:我非常喜欢查克博士的课程,很高兴来到这里。
  • Xang:我是通过这门课程开始Python编程的。现在我在一个实验室工作,编写生物信息学算法相关的程序。如果没有这门课程,我无法想象自己能做这些。我之前学生命科学。查克说:哦,是的,然后你通过我的课程学习了Python编程,现在成为了一名初级程序员。恭喜你,非常感谢你。你并不孤单,我真的很想和你一起喝一杯,欢迎加入课程。
  • Hang:能在课堂上见到视频里的真人真是太棒了。查克博士是我的第一位编程老师,Python是我的第一门编程语言,我真的很感激。查克回应:欢迎成为一名程序员。
  • Michelle(来自芝加哥):我开始在韩国从事金融工作。我以前学过像SAS这样的工具,但实际上金融领域也使用Python,它是一门非常棒的语言。昨天我收到邮件通知时非常惊喜。查克说:是的,事情总是这样随机,无法预测我下次会在哪里。
  • Huen:我已经注册了接下来的课程。来到这里让我动力十足,我等不及要在Coursera上看到自己的脸了。
  • Cizz Lee:大约两年前,我开始了这门Python编程课程。那时我对编程一无所知,是零基础。但现在我成为了一名开发者。我热爱向女孩们教授Python编程,我也是韩国Django Girls的组织者之一。这是我的同事Maco。我们刚刚举办了一场活动,帮助了大约50人,我们希望在编程世界激励更多女性。查克说:所以你从一个完全不懂编程的人,变成了通过Django Girls激励其他女性成为程序员的人。现在我想把我的贴纸要回来,我要把它贴在我的笔记本电脑上。这样在未来的录制视频中,你们就会看到我电脑上来自首尔Django Girls的贴纸了。
  • Hassan:我在韩国生活了很长时间。实际上,我说Python,这很简单。为了办公室工作,我使用其他语言,但在空闲时间我只用Python。每当有人想和我讨论学习编程语言时,我就建议他们安装Python并从你的课程开始。我非常高兴见到你,因为我在2014年完成了这门课程,等了两年来见你,非常感谢。

聚会结束

查克总结道:多么有趣的一段时光!我们在这里待了大约一小时,喝了些饮料,差点被请出去,但我们重新整理了房间所以没有被赶走。我不知道下一次办公时间会在哪里,可能在凤凰城或芝加哥。我已经在芝加哥举办过两次办公时间了。一旦确定了下次地点,我会立刻通知大家。干杯!


总结

本节课中,我们一起体验了在韩国首尔举行的Django课程线下办公时间。我们看到了来自不同背景、不同国家的学习者如何因为对Python和编程的热爱而相聚。从创意写作者到生命科学学生,从金融从业者到社区组织者,每个人都分享了技术如何改变了他们的学习或职业路径。这次聚会不仅是一次答疑,更体现了开源、分享与社区支持的精神。我们期待下一次在未知地点的相遇。

079:一对多模型概述 🗂️

在本节课中,我们将学习数据建模的基础知识,特别是“一对多”关系模型。这是理解数据库设计中最简单也最核心的概念。

数据建模简介

上一节我们介绍了Django应用的基本结构。本节中,我们来看看数据建模的核心部分。我们目前正逐步构建一个Django应用,虽然尚未完成整个应用,但重点聚焦在 models.py 文件上,并会涉及一些 admin.py 的内容。models.py 是我们定义数据库结构的地方。我们通过数据模型对象来查看这个数据库,并且可以在 admin.py 中注册我们的数据模型。本节课我们不会过多讨论路由、视图、表单或模板,而是专注于Django应用的数据建模部分。后续课程我们会涵盖其他内容。

为什么数据建模很重要

我接触数据库的时间比较晚。很久以前在研究生阶段见过一点,当时不太喜欢关系型数据库。后来为了工作学习了关系型数据库,那时我已经是相当资深的程序员了,他们教我如何进行数据模型设计,我学得很快,并且彻底爱上了数据库。

模型设计是一个有趣的待解决问题,其基本理念归根结底是数据存储和检索的效率。这就像如果你构建一个像Facebook这样的在线应用,如果不巧妙地设计数据存储方式,假设每次登录Facebook都需要读取其所有数据来验证你的身份,那么登录一次可能需要花费2000到3000小时。显然,Facebook每秒有数百万人登录,其速度远快于此。因此,数据库的价值就在于将读取数据的时间从数天缩短到每秒数百万次。有时在数据建模时,你可能会觉得这很困难,会问为什么我们要如此费力。答案是,如果不这样做,应用就无法扩展。对于简单的数据挖掘,你可以遍历数据,但在构建交互式在线应用时,速度至关重要。

事实证明,像数据库中的大多数事情一样,你可以很快学会基础知识,而其余部分则是一种需要不断实践才能掌握的艺术。

数据建模的目标与方法

我们的目标是确定应用中想要存储的数据,然后绘制一张图,将这些数据分布到多个表中,并在这些表之间创建链接。通常,我们会从一个样本数据集开始。下图是我们将要构建的本地图书馆应用的数据模型,也是我们后续会反复参考的最终目标。不过,我们不会直接到达这个终点,而是从数据本身开始。

以下是该数据模型的图示:

在不同的应用或组织中工作时,你会发现数据模型非常重要。例如,下图是我的自动评分器(基于我构建的名为“Suie”的框架)的数据模型。如果你早期学习这个,我会展示数据模型、存储方式以及各种关联。这个数据模型花费了大量时间来完善,是应用的重要组成部分。另一个我参与的开源学习管理系统“Schi”的数据模型则复杂得多,下图只是其一小部分。如果你想改进这个应用,就必须理解其数据模型,因为数据最终会以扁平形式呈现在用户界面,但实际存储在非常特定的位置并通过链接关联,这就是为什么你会看到许多小方框和连接线。

以下是“Schi”系统部分数据模型的图示:

数据库规范化理论

关于数据库规范化有一整套理论,它非常重要。如果你有时间,建议阅读相关资料,如第一范式、第二范式、第三范式等。我不打算深入讲解其中的数学原理。

数据库规范化的关键要点可以归结为:不要重复存储字符串数据。这里的字符串数据指的是姓名、URL等内容。在一个系统中,任何字符串数据原则上只应存储一次。例如,我的电子邮件地址在系统中应以文本形式存储,且只存储一次,不应该出现多个副本。你需要做的是将电子邮件地址存储在一个地方,并赋予它一个编号(ID),然后在所有需要引用该电子邮件地址的地方使用这个编号。

这些整数编号就是。我们创建键作为查找的句柄,然后指向这些键,并使用整数在表之间建立链接。因为整数处理速度快,计算机更擅长理解和移动整数,它们占用数据量少,比较速度也快。因此,整数在数据库链接中非常重要。

核心概念总结

让我们用公式和代码来总结核心概念:

  • 核心目标:通过规范化设计,避免数据冗余,提升存储和查询效率。
  • 关键实现:使用主键外键 在表之间建立关联。
    • 主键公式Primary Key = Unique Identifier for each row in a table
    • 外键公式Foreign Key in Table A = Primary Key from Table B,用于建立从A到B的引用。
  • 代码示例(Django模型概念)
    # 假设Author和Book是一对多关系(一个作者写多本书)
    class Author(models.Model):
        name = models.CharField(max_length=100)
        email = models.EmailField(unique=True)  # 确保电子邮件唯一
    
    class Book(models.Model):
        title = models.CharField(max_length=200)
        # 外键字段,指向Author模型的主键,建立“一对多”关系
        author = models.ForeignKey(Author, on_delete=models.CASCADE)
    
    在上面的代码中,Book 模型中的 author 字段是一个外键,它存储的是 Author 表中某条记录的主键(一个整数ID),而不是直接存储作者的姓名或电子邮件字符串。这避免了重复存储作者信息。

后续内容预告

接下来,我们将查看一些示例数据,然后为这些数据设计一个数据模型。

本节课总结

本节课中,我们一起学习了数据建模的基础,重点理解了一对多关系模型的重要性。我们探讨了数据建模的目标是提高效率,其核心方法是避免数据冗余,并通过整数主键和外键在表之间建立高效的链接。我们还简要了解了数据库规范化的概念,并预览了后续的实践步骤。掌握这些基础是设计出高效、可扩展Django应用的关键。

080:一对多模型中的去重优化

在本节中,我们将学习如何设计一个避免数据重复的数据库模型。我们将从一个简单的电子表格视图开始,分析其中存在的问题,并最终将其转化为一个高效的关系型数据库结构。

概述

设计数据模型通常从用户界面开始。但为了理解核心概念,我们将从一个类似电子表格的视图入手。如果你曾尝试用电子表格管理音乐或视频收藏,你会发现它非常方便,因为你可以滚动查看和操作数据。学过SQL后,你甚至知道可以用SQL查询和更新各个单元格。

然而,一段时间后,你会发现电子表格中开始出现大量重复数据。这是因为在输入数据时,复制粘贴非常容易,导致相同的信息出现在多个地方。但当你需要修改这些信息时,问题就出现了。这种在多个地方垂直复制数据的行为非常糟糕。

从电子表格到数据模型

以下是我们应用程序需要处理的数据的一个展开的表格视图。这些数据包括一系列书籍。

  • 书名:例如《群体的智慧》、《计算机网络导论》。
  • ISBN:书籍的国际标准书号。
  • 书籍实例:作为图书馆,我们可能拥有多本同一本书的物理副本。每个物理副本称为一个“实例”,每个实例可能有自己的状态(如借出状态)和到期日。
  • 其他信息:如书籍的语言、类型和作者。

现在,观察这个表格,你首先会发现:任何存在垂直复制的地方都是问题。因此,你需要仔细寻找垂直复制。

在这个包含所有数据的表格视图中,有问题的字段包括状态、语言、类型和作者。例如,“英语”、“思考类”这些字符串重复出现了多次。《群体的智慧》这本书有三个实例,但因为所有数据都在一个包含多列的大表中,我们不得不将书名“群体的智慧”复制三次,以表示这三个实例。语言、类型和作者名也存在同样的问题。

这种垂直复制是你需要设计一个比这更智能的数据模型的第一个线索。

核心原则:消除重复

核心思想是:不允许像“EN”(英语)这样的字符串在整个数据库中出现超过一次。因此,你必须将数据拆分。

你必须通过创建语言表、书籍表和实例表来消除重复。在书籍表中,“群体的智慧”只出现一次。在语言表中,“英语”也只出现一次,“西班牙语”也只出现一次。

但随之而来的问题是:我们如何将数据从一个包含多列的大表,拆分到三个具有不同列集合的表中,同时仍然能表示原始的所有数据?我们必须能够表示所有这些信息。

关系数据库的精髓:建立连接

这就是我们需要建立连接的地方,这也是关系数据库的精髓所在:它们不复制数据,而是在不同数据项之间建立连接

具体到我们的例子:

  • 对于《群体的智慧》这本书,我们将有三个书籍实例。但它们都指向书籍表中的同一条书籍记录。
  • 同样,有两本书指向“英语”语言,一本书指向“西班牙语”语言。

这看起来似乎没什么大不了的。但当处理数百万条记录时,它就至关重要了。字符串数据会带来麻烦,需要更多的扫描和存储空间。

遵循“只保留一份副本”的规则还有另一个好处:如果我们想把“EN”改为大写,或者修正《群体的智慧》书名的拼写错误,我们只需要修改一个地方。这带来了性能和存储上的优势,也提高了可修改性。

绘制数据库关系图

那么,我们如何开始绘制数据库的关系图呢?表之间的关系类型通常是“多对一”。

当我们说“多对一”时,意思是:存在一个“英语”语言条目,然后有多本书籍指向它。这可以是多本书,也可能只有一本,但本质上是“多”对“一”。

观察Django示例中的数据模型,我们可以看到书籍表和语言表之间的连接有两个端点,并且有方向性。一端标记为“0..*”,另一端标记为“1”。

  • 0..* 表示数量在0到无穷大之间(包括0)。这意味着一本书可能没有指定语言(在数据库概念中称为NULL)。或者,多本书可以对应一种语言。
  • 1 表示一种语言。

因此,在书籍和语言的关系中,书籍端允许0到无穷大的数量,语言端固定为1。我们可能有一种语言,但图书馆里没有该语言的书籍,这也是允许的。

接下来,看看书籍和书籍实例之间的关系。书籍实例的目的是记录我们拥有的同一本书的多个物理副本。因此,我们可能有多个副本。

观察这个关系,它与上一个非常相似:书籍实例的数量可以从0到无穷大。你可以在书籍表中有一本书的记录,但实际上并没有它的物理副本,这是可以的。我们知道它的作者、类型等信息。然后,当我们购买了这本书的副本时,我们会在实例表中添加一条记录,并将其链接到那本书。所以,同样,可以有0到无穷多个书籍实例映射到一条书籍记录。

理解关系表示法

这个小图例“0..* 对 1”就是我们解读这种表关系的方式。你可能会看到使用不同语法表示的数据库图表,但它们都在描述相同的事情:即箭头每一端事物的数量(基数)。

总结

在本节中,我们一起学习了数据库设计中的一个关键优化:消除数据重复。我们从电子表格的局限性出发,认识到垂直复制数据会带来存储、性能和维护上的问题。通过引入“一对多”的关系模型,我们将数据拆分到不同的表中,并使用连接(而非复制)来关联它们。这不仅能节省空间、提升效率,还使得数据修改变得更加简单和安全。下一节,我们将具体学习如何在数据库中物理地存储这种关系信息。

081:数据库中主键与外键的存储 🔑

在本节课中,我们将要学习如何在数据库中表示表与表之间的链接关系。我们将重点介绍主键和外键这两个核心概念,它们是构建关系型数据库模型的基础。


数据库中的链接表示

到目前为止,我们一直在用箭头图示表之间的关系,并假设你已经理解了其原理。例如,一些“书籍实例”行可以神奇地连接到一些“书籍”行,而“书籍”行又可以连接到“语言”行。

实际上,我们是通过一种特定的方式来实现这种连接的,即在每个表中添加一个称为“键”的列。


主键:行的唯一标识符

我们称这种键列为主键。通常,我们将这个列命名为 id。数据库中有一种方法可以将其设置为自动递增主键,并为其建立索引,这使得查询速度非常快。这个主键就像是行的“句柄”。

主键是箭头指向的终点。例如,我们可以设定“英语”是语言表中的第1行(id=1),“Wi of crowds”是书籍表中的第1行(id=1),“Introduction to networking”是书籍表中的第2行(id=2)。这个 id 可以在整个系统的其他地方被引用,整数重复在这里不是问题。

核心概念:主键是表中唯一标识每一行的列。

CREATE TABLE book (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255),
    ...
);

外键:指向其他表的链接

接下来,我们在表中添加另一种列,称为外键。这些外键是箭头的起点。按照惯例,我们以目标表的名称加上下划线和 id 来命名它们,例如 book_id

外键允许我们从一个表指向另一个表。通过在每个需要作为箭头起点的位置设置外键,并在每个需要作为箭头终点的位置设置主键,我们就建立了表之间的关系。实际上,我们几乎为每个表都设置了主键,以确保每一行都有一个唯一标识。

在这个例子中,一个表可能只有一个外键,但一个表完全可以拥有多个外键,指向多个不同的表。

核心概念:外键是一个表中的列,它引用另一个表的主键,用于建立表间关系。

# 在Django模型中的示例
class BookInstance(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)  # book_id 就是外键

主键与外键的关系

正如之前所说,主键是我们添加的、作为行句柄的列。而外键是我们添加到表中、用于指向其他表某行的列。

一旦我们创建了这些外键列和主键列,就可以开始利用这些链接来构建数据模型。这些链接共同作用,最终形成了我们之前图示的完整数据关系网络。


总结与预告

本节课中,我们一起学习了数据库关系模型的核心:主键外键。主键唯一标识表中的每一行,是关系的目标;外键则存储了指向其他表主键的值,是关系的起点。通过组合使用它们,我们可以在数据库中清晰地表示和管理复杂的数据关联。

在下一节中,我们将探讨如何在Django框架中表示和操作这些链接关系。

082:Django中一对多模型的表示 🗂️

在本节课中,我们将学习如何在Django数据模型中表示一对多关系。我们将探讨如何使用外键字段链接不同的模型,理解模型迁移的工作原理,并学习如何配置删除行为以维护数据的一致性。

数据模型与关系表示

上一节我们讨论了如何通过将数据拆分到多个表并使用主键和外键链接来减少数据冗余。但在Django课程中,我们不会直接操作数据库层面,而是通过Django的数据模型来实现。

在Django中,我们使用models.py文件中的数据模型来完成这项工作。模型字段类型包括字符、数字、日期或长文本字段等。但其中一些字段是特殊的,用于处理关系,例如一对多外键字段多对多字段。这些字段用于在我们的Django数据模型中建立链接。

以下是这些关系在模型中的表示方式:

如果我们的BookInstance模型链接到Book表,而Book表又链接到Language表,最终在我们的模型中会形成特殊的关联线。

定义模型与外键

我们来看一个具体的模型定义。大部分内容都很熟悉。我们有一个Language模型,它有一个name字段。关于Django的一个特点是它会自动添加id字段,这很方便。因此,你可以假设数据模型中的每个表都会自动拥有id字段。

我们甚至不需要讨论外键的名称,尽管Django会按照我展示的方式(例如lang_id)来命名它们。我们只需要定义一个常规的字段。Language模型适合作为链接的目标,因为它有一个由Django自动添加的id列。Book表也有一个id列,因此也适合作为链接的目标。

但关键在于Book模型中的这个字段:

language = models.ForeignKey('Language', on_delete=models.SET_NULL, null=True)

这是我们实际建模Book表与Language表之间链接需求的地方。我们从箭头的起点(即Book模型)开始,声明我们需要一个language字段。但这个language不是一个字符串或数字,它是一个外键,指向名为Language的表。这个名为Language的表与模型匹配。

这里有几个参数需要说明:

  • on_delete:我们稍后会详细讨论。
  • null=True:表示该字段是否必需。当null=True时,我们允许该字段为空。这意味着Book表中的一条记录可以没有关联的语言。

类似地,Instance表(即BookInstance模型)指向一本书:

book = models.ForeignKey('Book', on_delete=models.CASCADE)

它是一个指向Book模型的外键,并且有一个due_back字段。这里的状态字段被我简化了。我们再次定义了箭头的起点,因为箭头的终点是隐含的(即目标模型的id)。

Django会自动为这些模型中的每一行提供主键,我们无需显式声明。但如果你想覆盖主键的名称,也可以在Django模型中指定。不过,通常大家只需要一个名为id的列,所以不声明它,Django会自动为你添加。


再次从数据模型的角度来看,BookLanguage之间的箭头代表多对一关系,BookInstanceBook之间也是一个多对一关系。这些外键链接代表了数据模型中的箭头。


模型迁移流程

现在来回顾一下,你可能已经做过一些迁移操作。一旦你编辑了这些数据模型并确认正确,你需要执行一个两步流程,使你的数据库反映出这些模型的变化。

第一步是创建迁移makemigrations命令的工作方式是:检查你settings.py中注册的所有应用程序,对于每一个应用,它都会检查数据模型自上次运行makemigrations以来是否发生了变化。

makemigrations实际上会根据models.py在你的应用程序中创建文件。这些是一系列文件,例如这里的0001是第一个。如果我们更改了模型并再次运行makemigrations,将会生成00020002是在0001基础上的演进。

迁移文件是可移植的,它们不特定于MySQL数据库或SQLite数据库等。

第二步是应用迁移migrate命令读取这些迁移文件,然后更新数据库中的实际表。因此,migrate是将数据库中应有的内容的可移植表示,映射到数据库中的实际物理表示。

你会注意到,如果你成功运行这两条命令后再运行,它们会显示“未检测到更改”。因为对于makemigrations,它会比较你的模型和已有的迁移文件,如果完全匹配,就认为没有变化。对于migrate,它会比较迁移文件中的内容和数据库表,如果一切都已就绪,也会认为没有变化。

这有时会让人困惑。在我的文档中,我可能会说“运行makemigrations”,而你看到“未检测到更改”。另一个可能导致“未检测到更改”的原因是,你必须在settings.py文件中添加该应用,它才能被检测到。所以你可能编辑了models.py文件并进行了更改,但如果该应用不在settings.py中,那么更改就不会被检测到。makemigrations会遍历settings.py中列出的所有应用程序,检查models.py和迁移文件之间的差异。通过实际操作展示可能更容易理解,我们稍后会看到。

迁移的底层实现

如果我们看看migrate做了什么,它会创建SQLite文件。你可以在运行migrate后查看这个文件(虽然不是必须的)。

你可以看到,我使用了一个通配符%来查询所有以book1开头的表。结果显示有三个表:book1_bookbook1_instancebook1_languagebook1是应用程序的名称,因为Django有项目和项目内的应用程序,book1就是我们正在操作的应用程序。而instancebooklanguage是我们在models.py中指定的三个模型。

如果我们使用schema命令查看book1_book表,可以看到migrate语句是如何生成这个表的。

我们创建了book1_book表。它有自动递增的id字段,这是SQL的标准内容。它有titleISBN字段,然后有这个lang_id字段,这就是外键。所以它现在正以正确的方式构建外键。

在SQLite中构建外键有一种方式,在MySQL中有另一种方式。migrate操作理解如何构建所有这些差异。

如果你查看language表,你只会看到自动生成的id列和一个name列。查看instance表,你会看到iddue_back(日期类型),以及book_id,这是指向book表的外键。这样,你就能大致看出这些表是如何实现图中描述的链接的。

配置删除行为

现在,我来谈谈之前提到的on_delete参数。问题是,当你有一个表中的一行(例如book表中的一行)指向language表中的一行,并且有记录指向它时,会发生什么。假设有两本书都指向“英语”。然后你删除了“英语”这一行。那些指向它的行会怎样?

有几种不同的处理方式,但两种常见的方式是CASCADE(级联)和SET_NULL(置空)。

  • CASCADE表示:如果你删除那些行(指目标行),你将删除所有具有匹配language_id的行。
  • SET_NULL基本上表示:没关系,我们只是清除这些链接并将它们设为NULL

在这个例子中,如果我们从language表中删除一种语言,我们可能只想将其设为NULL


因此,如果你查看BookLanguage之间的关系(就在这里),如果我们删除其中一个,我们将其设置为NULL。我们将相应的语言设置为NULL。从某种意义上说,我们只是丢弃了这些链接,这是可以的,因为书籍被允许没有语言。所以语言字段允许为NULLnull=True)。

另一方面,如果我们从books表中删除一本书,那么我们可能希望清除与该书相关的所有实例。这就是为什么我们在BookInstance模型的book外键条目中,设置on_delete=models.CASCADE,并且没有设置null=True

SET_NULL要求你能够接受该字段为NULL(即null=True)。所以你必须说:我愿意允许此列中出现空值。哦,顺便说一下,如果此列指向的内容被删除,则将其设置为NULL,这意味着不要删除该行。

CASCADE意味着:如果我们指向的内容被删除,则删除此行,因为我们不想要不一致的数据。我们不希望那些小东西指向无处,变得毫无意义。


总结

本节课中,我们一起学习了在Django中如何通过外键定义一对多关系。我们了解了模型字段ForeignKey的用法,包括关键的on_delete参数(如CASCADESET_NULL)如何控制关联数据的删除行为。我们还回顾了Django的迁移机制:通过makemigrations生成可移植的迁移文件,再通过migrate命令将这些变更应用到数据库,从而在底层数据库中正确地创建表和外键约束。理解这些概念是构建复杂、数据关系清晰的Django应用的基础。

接下来,我们将更深入地探讨模型、迁移和数据库在较低层次上是如何工作的。

083:使用Django Shell探索一对多模型 🐚

在本节课中,我们将学习如何使用Django Shell来操作数据模型。我们将通过编写Python代码,创建、保存和查询具有“一对多”关系的模型对象,从而理解Django ORM如何简化数据库操作。


上一节我们介绍了Django的数据模型设计。本节中,我们来看看如何通过代码来操作这些模型。

为了操作数据模型,我们需要使用Django Shell。Django Shell看起来很像Python交互式Shell,但它会预先加载许多Django环境配置。你不能直接运行普通的Python交互模式。在Django Shell中,系统会读取settings.py文件,找到所有已注册的应用并预先加载它们,然后才提供一个Shell环境。这意味着在你进入Shell之前,已经完成了一系列初始化工作。

例如,执行from book1.models import Book, Language, Instance这样的导入语句,在普通的Python Shell中是无法工作的。这是因为book1应用是通过python3 manage.py shell命令加载的。之后,所有相关的类就被预加载好了,你可以运行任何你想操作的Django类和对象的Python代码。

让我们看看具体能做什么。

如果你还记得,我们的数据模型中有一个名为Language的模型。我们可以在内存中创建一个新的Language对象,代码如下:

z = Language(name='E')

这行代码的意思是:给我一个新的Language对象,将其name属性设置为'E',并将这个对象赋值给变量z。这是一个面向对象的构造函数,我们在构造一个对象。

此时,这个对象并没有被存入数据库,它只是在内存中被创建并返回。它是一个拥有name属性的Python对象,并且Django知道它是一个Language模型。

save()方法z.save()的作用是,将这个内存中的对象持久化到数据库中,即写入并传输到数据库。

这个操作的一个很酷的地方在于,id字段会在保存的那一刻被自动设置。所以我们可以查询z.id来获取它。请记住,我们放入模型的每一行数据都会获得一个id值,我们将使用这个id值作为指向该行的句柄。这就是主键,是我们指向表中某一行的方法。

接下来,我们可以创建一本书。我们已经创建了一个语言z并知道了它的id。现在我们来创建一本书,它有titleisbn等属性。我们这样写:

x = Book(title='PY4E', isbn='42', lang=z)

这里的lang=zz就是那个变量。我们不需要直接使用id。Django完全了解id是如何工作的。所以当你写lang=z时,它会自动创建一个lang_id字段,并将语言行的id值放进去。所有这些操作都是自动完成的。你只需要有一个包含id值的变量,并将其传入即可。

这段代码的作用是创建一本书,并将其与一个语言关联起来。同样,这本书在调用x.save()之前也不会被保存。保存后,这本书被传输到数据库。它内部有一个外键,并且获得了自己的主键。这是我们插入的第一本书,所以它的主键x.id是1。

然后,我们可以创建一个借阅实例Instance。实例需要一个due_back日期,以及它属于哪本书。我们使用变量x来指明我们谈论的是哪本书。

a = Instance(due_back='2024-01-01', book=x)
a.save()

这行代码创建了一个内存中的Instance对象,并将其赋值给变量a。这个实例指向变量x所代表的书籍(即PY4E ISBN 42)。保存后,它也会有一个id

至此,我们创建了一个语言记录,然后创建了一本链接到该语言记录的书籍记录,最后创建了一个链接到该书籍记录的借阅实例记录。这就是整个链接过程。这些就是指针,而Django在这种情况下试图向我们隐藏外键和主键的整个概念,除了我们主动询问id的时候。实际上你并不需要经常操作id。你只需要说:看,我有一本书,它的语言是这个东西。我不知道它的id是什么,请你帮我创建一个列并完成所有这些神奇的操作。因此,这些“一对多”关系或外键关系的大部分簿记工作,在Django中都是自动完成的。

现在,我们可以检索这些对象,并沿着这些关系链进行遍历。我们再次进入Shell,导入我们的模型。

我们可以这样写:

x = Book.objects.get(pk=1)

Book是我们从models导入的类(即book1.models.Book)。我们与这些类内部进行交互的许多方法都在.objects属性下。你可以将其理解为该模型中所有的行或所有对象,即存在于该表中的所有行。Book.objects.get(pk=1)的意思是:你想要哪一个?获取主键等于1的那一个。pk代表主键。我认为叫id可能更直观,但id是主键列的常用命名约定,大约98%的情况下都是如此。这不是强制要求,但太常见了,几乎可以认为是事实。所以pk实际上指的是主键,而十有八九它就是id列。

因此,我们从books表中加载数据,获取books表中行号为1的那一行。然后我们可以查询它的标题x.title,结果是'PY4E'

有标题,还有langlang是指向语言的外键。所以我们可以说x.lang,这代表整个语言行。而x.lang.name则是该行中的name列。我们就像沿着小箭头向下走,然后从对应的行中取出一个元素。这就像从一本书走到对应的语言行,然后从那个对应的语言行中取出名称。我认为这是一个非常漂亮的语法,一段时间后就会开始理解。你沿着这些小箭头遍历。这就是为什么数据模型的图示如此重要。

接下来,我们去获取第一个借阅实例,即book_instance表中的第一行。

y = Instance.objects.get(pk=1)

这行代码表示:给我第一行,将其返回到变量y中。这个实例的due_back日期是什么?然后你可以继续遍历。你可以说:我处在一个book instance,我想沿着外键走到book表,获取那本书,然后给我那本书的标题。

y.book.title

所以,从一个借阅实例,你可以走到书籍表,然后获取书籍的标题,我们得到'PY4E'。你可以看到,这是从一行开始,走到由这个关系指向的对应行,这就是这种关系的本质。

我们将大量进行这样的操作,当我们再次回顾时,这一切都会显得非常直观。


本节课中,我们一起学习了如何使用Django Shell来探索和操作“一对多”数据模型。我们掌握了如何创建、保存模型对象,以及如何通过简洁的语法(如x.lang.namey.book.title)沿着外键关系进行遍历查询。Django ORM自动处理了大部分外键关系的簿记工作,使我们能够更专注于业务逻辑。

084:波特兰面对面办公时间记录

在本节课中,我们将回顾密歇根大学《Django for Everybody》课程在俄勒冈州波特兰市举行的一次线下办公时间。本次记录旨在展示学习社区的互动,并分享部分参与者的学习经历与感受。


开场与地点

大家好,我们现位于俄勒冈州的波特兰市,正在进行又一次的课程办公时间活动。

我差点说错地点,但至少我确认了是在正确的州——俄勒冈州,波特兰市。我们所在的那条街道非常繁忙,并且在这里成功举办了一次办公时间。和往常一样,我希望向课程的其他学员介绍在场的各位。


学员自我介绍

以下是部分参与课程的学员进行的简短自我介绍。

  • Arvin:我报名过很多在线课程,而这门Python课程是我第一个完整完成全部四个部分的学习。这要感谢优秀的讲师。
  • Scott:大家好,我是Scott,我正在学习《Python for Informatics》课程。
  • Alireza:大家好,我是Alireza。在过去的四个月里,我见到Chuck博士的次数比见到我孩子的次数还多。我刚刚完成了Python数据结构部分,课程非常棒。
  • Paul:大家好,我是Paul。我17岁的儿子也叫Paul。我们正在一起通过Coursera学习,这是一个父子合作项目。他是一名高中生,学得很好。对我来说这是爱好,对他则是为了掌握未来可用的技能。
  • Maddie:大家好,我是Maddie,我是Pitzer学院的有机生物学专业学生,但我对学习Python感到非常兴奋。
  • Margaret:大家好,我是Margaret,实际上我是从田纳西州来这里度假的。Python课程是我一直想开始的编程入门课。
  • Diane:大家好,我是Diane。我正在学习这个系列课程的第四门,并将在今年夏天完成毕业项目。这非常有趣。
  • Alejandro:大家好,我是Alejandro。很多年前,当我住在古巴哈瓦那并工作时,就学习了精彩的互联网历史课程。感谢Chuck博士的精彩课程。
  • Andrew:大家好,我是Andrew。我和我的同事Nathan一起加入了Coursera,他今天因为工作没能到场。他算是以虚拟方式“抢镜”了。多亏了Chuck博士的所有出色工作。
  • Herb:大家好,我是Herb。很久以前我学习了互联网历史、技术与安全课程,后来成为了课程的社区助教,现在是一名导师。我参与这个课程已经很长时间了,非常享受帮助学生的过程。

上一节我们听到了学员们的自我介绍,其中Herb作为课程导师的角色尤为突出。接下来,我们将特别关注他的贡献。


导师的重要性

Herb是课程中不到十位的导师之一。他孜孜不倦地帮助所有学生,并且完全是义务劳动。因此,我喜欢做的一件事就是去导师们所在的城市,请他们吃顿饭。对于多年辛勤工作来说,一顿免费的晚餐只是微小的补偿。

但如果没有像Herb这样的导师,这些课程将无法保持活力。即使有最好的讲座视频、作业和测验,如果没有人性的互动,也毫无意义。因为学习是关乎人的事情,而不是单纯的信息传递。正是像Herb这样的人,让这些课程在我离开、所有讲座都结束后,依然能够持续运行。


更多学员分享

让我们继续聆听其他学员的分享。

  • Maureen:我是一名化学家。我学习这门课程是为了能够以自动化的方式提取数据并每周生成图表,而不是在Excel中手动完成。

活动总结与未来预告

好的,以上就是本次办公时间的全部内容。

下一次办公时间将在不到24小时后于西雅图举行。在那之后,大家还可以参加再之后在英国布莱切利园举行的办公时间。所以,如果你计划去英国度假,欢迎来参加办公时间。


在本节课中,我们一起回顾了波特兰线下办公时间的记录,听到了来自不同背景学员的学习故事,并了解了课程导师对学习社区不可或缺的支持作用。这体现了在线课程背后真实的人际连接与学习热情。

085:Django中的用户创建与管理 👤

在本节课中,我们将学习Django框架内置的用户认证系统,包括如何创建超级用户、管理用户以及理解登录与注销的基本流程。Django提供了强大的开箱即用功能,使我们无需从头构建这些基础模块。


创建与更新代码库

在开始之前,建议定期更新你的示例代码库。使用以下命令从GitHub拉取最新的代码和修复:

git pull

这能确保你拥有最新的示例和错误修复。


Django内置的认证系统

上一节我们提到了保持代码库更新。本节中我们来看看Django的核心功能之一——内置的用户认证系统。

Django内置了一套完整的登录、注销和用户管理系统。它提供了多种实现方式,并拥有一个方便的管理员界面。Django的优势在于,开发者无需自行构建这些基础维护功能。系统包含了用户、权限、用户组和密码管理等,这些功能在“本地图书馆”应用中被使用,但在我们当前的应用中不会深入使用权限和组。Django还支持以多种技术存储这些信息。


创建超级用户

首先,你需要在系统中创建一个超级用户。以下是创建步骤:

  1. 登录到你的Django系统。
  2. 运行创建超级用户的命令。
  3. 根据提示输入用户名、电子邮件地址和密码。

电子邮件地址用于密码恢复。创建命令如下:

python manage.py createsuperuser

“超级用户”在系统中拥有所有权限,无需任何特殊授权即可执行任何操作。这个概念类似于Linux系统中的 sudo 命令,它允许普通用户以超级用户权限执行命令。


数据库重置与重建

创建超级用户时,Django会在数据库的相应表中插入一行记录。了解如何重置开发环境非常重要。

如果你使用SQLite进行开发,可以随时通过删除数据库文件来清空所有数据。这将移除所有由迁移创建的表、所有用户数据以及超级用户。之后,你需要使用以下命令重新初始化数据库:

python manage.py migrate

此命令会重新创建所有空表。完成后,你必须再次创建你的超级用户。


使用管理员界面管理用户

一旦创建了超级用户,其余用户可以通过Django内置的管理员界面进行管理。

在管理员界面中,除了你自己定义的应用模型,你还会看到“用户”和“组”这两个模型。它们是Django内置的模型,你无需负责构建它们。虽然权限和用户组功能非常有用,但在当前阶段,我们主要关注用户的创建。未来我们将学习使用社交登录(例如通过GitHub登录),那样就无需手动创建大量用户。

目前,我们使用的是“本地用户”,即信息存储在数据库中的用户。这种方式易于创建和维护,但通常需要一定的手动管理。


应用中的登录与注销

现在我们已经了解了如何创建和管理用户,接下来的问题是:在我们的应用代码中,如何实现用户的登录和注销功能?

我们将在后续课程中深入探讨如何在视图和模板中集成Django的认证视图,以处理用户的登录和注销请求。


总结

本节课中我们一起学习了Django用户认证系统的基础。我们了解了如何创建超级用户、通过管理员界面管理用户、以及重置开发数据库的流程。我们还明确了“本地用户”的概念,并为学习下一部分——在应用中实现登录和注销功能——做好了准备。

086: Django的登录与登出URL 🔐

在本节课中,我们将学习如何在Django应用中处理用户的登录与登出功能。我们将了解会话、登录和登出之间的关系,以及如何配置和使用Django内置的认证URL。


概述

登录与登出是Web应用的核心功能。在Django中,这些功能建立在会话(Session)机制之上。本节将解释它们之间的关系,并展示如何配置项目以启用Django内置的登录/登出视图,以及如何在模板中生成正确的登录/登出链接。


会话、Cookie与登录的关系

上一节我们介绍了会话(Session)。需要明确的是,创建会话并不等同于用户登录

登录行为实际上会改变会话中的数据。最基本的登录方式依赖于会话的存在:登录是将当前用户的信息(如用户名、邮箱)存入会话的过程;而登出则是将这些信息从会话中移除。

你可以在同一个浏览器会话中,让多个用户依次登录和登出。最终,你得到的是同一个会话对象,只是它关联的用户数据不同。

请记住:

  • Cookie和会话不是一回事。
  • 会话和登录也不是一回事。
  • 但它们彼此构建:会话建立在Cookie之上,而登录又建立在会话之上

配置Django项目以支持登录/登出

为了让登录和登出功能正常工作,你需要在项目的 settings.py 文件中,将Django的认证支持添加到 INSTALLED_APPS 列表中。

# settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',       # 核心认证框架
    'django.contrib.contenttypes',
    'django.contrib.sessions',   # 会话框架
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # ... 你的其他应用
]

如果你使用 django-admin startproject 命令创建项目,这些应用通常已被自动添加。了解这一点,有助于你知道这些“魔法”功能来自何处。

接下来,你需要在项目根目录的 urls.py 文件中添加认证相关的URL路径。

# 项目根目录下的 urls.py (例如: dj4e_samples/urls.py)
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),  # 添加这行
    # ... 你的其他URL配置
]

这行代码将Django内置的一系列认证视图(如登录、登出、密码重置等)的URL注册到 /accounts/ 路径下。完成这一步,Django的登录/登出功能就被激活了。


在代码中获取登录/登出URL

我们通过 reverse() 函数来获取登录和登出视图对应的具体URL。我们一直在使用 reverse 来根据视图名称查找URL。

django.contrib.auth.urls 中,Django已经为登录和登出视图定义了名称(name='login'name='logout')。因此,我们可以像查找自己编写的视图一样,通过名称来获取它们的URL。

# 在视图或模板中获取URL
from django.urls import reverse

login_url = reverse('login')    # 例如: /accounts/login/
logout_url = reverse('logout')  # 例如: /accounts/logout/

使用 reverse() 而不是硬编码URL的好处是灵活。如果你的URL配置发生了变化(例如,路径不是 /accounts/),代码无需修改。


登录后的重定向:next 参数

关于登录的一个常见需求是:用户访问一个需要登录才能查看的页面时,如果未登录,应被引导至登录页,并在登录成功后自动跳转回他原本想访问的页面

例如,你收藏了一个需要登录的页面,登出后点击收藏夹,系统会要求你先登录,然后自动带你回到那个页面。这通过使用 next 查询参数来实现。

以下是 next 参数的应用方式:

<!-- 在模板中生成带有next参数的链接 -->
<!-- 方法一:指定一个固定的返回页面 -->
<a href="{% url 'login' %}?next=/az/open/">登录并返回AZ Open页面</a>
<a href="{% url 'logout' %}?next=/az/open/">登出并返回AZ Open页面</a>

<!-- 方法二:动态返回当前页面 (更常用) -->
<a href="{% url 'login' %}?next={{ request.path }}">登录并返回本页</a>
<a href="{% url 'logout' %}?next={{ request.path }}">登出并返回本页</a>

在方法二中,request.path 是Django自动注入到模板上下文中的变量,它表示当前页面的路径。这是一种非常实用的模式,意味着“登录/登出完成后,请回到我现在所在的这个页面”。


在模板中使用用户对象

Django会自动将一个 user 对象注入到每个模板的上下文中。你可以利用这个对象来判断用户状态并显示相应信息。

以下是模板中可用的常见用户属性:

{% if user.is_authenticated %}
    <p>欢迎,{{ user.get_full_name }} ({{ user.email }})!</p>
    <p>您的用户ID是: {{ user.id }}</p>
    <!-- user.id 是用户表的主键,在创建外键关联时非常有用 -->
    <a href="{% url 'logout' %}?next={{ request.path }}">登出</a>
{% else %}
    <p>您尚未登录。</p>
    <a href="{% url 'login' %}?next={{ request.path }}">登录</a>
{% endif %}
  • user.is_authenticated: 布尔值,判断用户是否已认证(登录)。
  • user.get_full_name(): 获取用户全名。
  • user.email: 获取用户邮箱。
  • user.id: 获取用户的主键(ID),这在需要将其他数据表通过外键关联到用户时至关重要。

示例与实践

在我的示例代码的 az 应用中,我创建了一些视图来演示这些概念。以下是相关链接在模板 (main.html) 中的呈现方式:

  • 公开页面:无需登录即可访问。
  • 受保护页面:使用 @login_required 装饰器保护,未登录用户访问时会自动重定向到登录页,并附带 next 参数。

例如,一个受保护页面的登录链接最终可能生成如下HTML:

<a href="/accounts/login/?next=/az/open/">登录</a>

当用户点击此链接并成功登录后,Django会将其重定向回 /az/open/ 页面。

登出链接同理:

<a href="/accounts/logout/?next=/az/open/">登出</a>

总结

本节课我们一起学习了Django中处理登录与登出的核心机制:

  1. 理清了关系:登录/登出是操作会话数据的行为,它们建立在Django的会话机制之上。
  2. 完成了配置:通过在 INSTALLED_APPS 中添加 django.contrib.auth 和在项目URL中包含 django.contrib.auth.urls 来启用认证系统。
  3. 学会了生成URL:使用 reverse('login')reverse('logout') 动态获取认证URL。
  4. 掌握了重定向技巧:利用 next 参数实现登录/登出后返回指定页面或当前页面,提升用户体验。
  5. 运用了模板对象:在模板中使用自动注入的 user 对象来显示用户信息和控制界面元素。

下一节,我们将探讨如何不仅引导用户到登录页面,更进一步地如何配置和自定义登录页面本身

087:在视图中使用Django登录功能

在本节课中,我们将学习如何在Django视图中集成和使用内置的登录功能。我们将了解如何创建自定义登录页面模板,如何在视图中访问用户信息,以及如何保护特定视图,使其仅对已登录用户开放。

理解登录页面与模板

上一节我们介绍了用户对象以及在模板和URL中的基本用法。本节中,我们来看看登录页面本身。

Django提供了登录功能,但需要我们自己提供一个模板来控制其外观。这样,登录页面就能与应用的整体风格保持一致,而不是看起来像Django的默认页面。

我们需要创建一个名为 registration/login.html 的模板。请注意,在Django项目中,模板名称是全局的。无论这个模板位于哪个应用(例如home应用)中,只要路径是 registration/login.html,Django的登录视图就能找到并使用它。

在这个模板中,我们需要遵循一些规则来处理Django视图传递过来的上下文数据。

以下是模板中需要处理的关键部分:

  • 错误信息:如果表单提交有误(如密码错误),Django会传递错误信息,模板需要显示它们。
  • next 参数:这是一个重要的参数。当用户尝试访问一个受保护的页面但未登录时,会被重定向到登录页。此时,原页面的URL会作为 next 参数传递过来。登录成功后,用户将被带回这个原始页面。
  • 登录表单:Django的视图会提供一个 form 上下文变量,它包含了用户名和密码输入框等所有必要的HTML元素。我们只需在模板中输出 {{ form }} 即可生成表单。

在表单中,我们必须将 next 参数的值作为一个隐藏字段传回。这样,在用户提交登录表单后,Django才知道登录成功后应该跳转到哪里。

<!-- 示例:在表单中包含next参数 -->
<form method="post">
  {% csrf_token %}
  {{ form }}
  <input type="hidden" name="next" value="{{ next }}">
  <button type="submit">登录</button>
</form>

在视图和模板中访问用户数据

我们可以在模板和Python视图代码中访问当前登录用户的信息。

在模板中,我们可以使用 user 对象。但需要注意,其属性仅在用户已认证(user.is_authenticated 为真)时才有意义。

以下是用户对象的一些常用属性:

  • user.username:用户名
  • user.email:用户邮箱
  • user.id:用户的主键(PK)

在Python视图代码中,用户信息存储在 request 对象中。

# 在视图函数中访问用户信息
def my_view(request):
    if request.user.is_authenticated:
        # 用户已登录,可以访问其信息
        username = request.user.username
        user_id = request.user.id
        # ... 其他逻辑
    else:
        # 用户未登录
        # ... 处理未登录情况
    return HttpResponse(...)

保护视图:仅允许登录用户访问

接下来,我们探讨如何保护视图,使其只对已登录用户开放。一个典型的场景是:用户收藏了“收件箱”页面,登出后直接点击收藏夹链接。此时,应用应检测到用户未登录,将其重定向到登录页面,并在登录成功后自动跳转回“收件箱”。

有两种方法可以实现视图保护。

第一种是手动检查。我们在视图逻辑中判断 request.user.is_authenticated

from django.shortcuts import render, redirect
from django.urls import reverse

def manual_protect_view(request):
    """手动保护视图的方法(不推荐)"""
    if not request.user.is_authenticated:
        # 用户未登录,构造登录URL并重定向
        login_url = reverse('login') + '?next=' + request.path
        return redirect(login_url)
    # 用户已登录,正常显示页面
    return render(request, 'my_template.html', context)

这种方法可行,但代码略显冗余,且需要手动处理重定向逻辑。

第二种,也是推荐的方法,是使用Django提供的 LoginRequiredMixin。这是一个“混入类”(Mixin),可以轻松地为基于类的视图添加登录要求。

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import View

class ProtectedView(LoginRequiredMixin, View):
    """使用LoginRequiredMixin保护视图(推荐)"""
    login_url = '/login/'  # 指定登录页面URL
    redirect_field_name = 'next'  # 指定重定向参数名,默认为'next'

    def get(self, request):
        # 只有已登录用户才能执行到这里的代码
        return render(request, 'my_template.html', context)

使用 LoginRequiredMixin 后,Django会自动处理所有检测和重定向逻辑。如果未登录用户尝试访问该视图,他们会被自动重定向到登录页面(并附带 next 参数),登录成功后再返回原视图。这极大地简化了代码,是Django中的最佳实践。

课程总结

本节课中我们一起学习了Django登录功能在视图中的核心应用。

我们了解到,Django的登录功能配置简单:添加必要的应用和URL模式,并创建自定义的 registration/login.html 模板即可。我们学习了模板中如何处理 next 参数和表单,以实现登录后的智能跳转。

我们掌握了在模板(通过 user 对象)和视图(通过 request.user)中访问用户认证信息的方法。

最重要的是,我们学会了如何保护视图。虽然可以手动检查 request.user.is_authenticated,但最优雅和高效的方式是使用 LoginRequiredMixin。这个混入类能自动确保只有已登录用户才能访问特定视图,否则将其重定向至登录页,整个过程对开发者完全透明。

理解这些底层机制非常重要,尽管Django用很少的代码就实现了复杂功能,但知其所以然能帮助我们更好地使用和调试它。

088:休斯顿面对面办公时间记录

在本节课中,我们将整理并翻译一段来自课程面对面办公时间的视频记录。这段记录展示了学习社区的交流场景。


大家好,欢迎来到德克萨斯州休斯顿。这是Coursera面对面办公时间的又一次活动。

现场观众很棒,天气非常好。我们恰好错过了一周前的特大洪水,所以大家能看到这里没有被淹。我们正在一家名为Bohemos的酒吧露台上愉快地交流,这是一个波西米亚风格的地方。现在,让大家认识一下身边的同学。请向大家问好,并做自我介绍。

以下是同学们的自我介绍:

大家好,我是Snehalanki,我是一名化学工程师,是Python课程的初学者。我很高兴来到这里,听Chuck博士的课。谢谢。

我是Randy,我年纪较大,热爱Python。这是你参加的第二次办公时间活动。这是我的第二次办公时间活动。第一次是在他的公路旅行中,他当时去了爵士乐场所。没错,那是和我儿子一起去的爵士乐场所。是的,我儿子当时也在。

大家好,我是Brad。好的,你来拿相机吧。这是什么意思。

大家好,我是Chuck。我们现在在伊利诺伊州芝加哥市帕尔默之家希尔顿酒店的地下室,这是“互联网历史、技术与安全”课程的又一次办公时间。让我为大家介绍几位朋友。

请打个招呼吧。大家好,同学们好,我是Allison。

同学们好,我是来自芝加哥的James。

大家好,我是Randy。

这次我和我儿子一起来观看轮椅橄榄球赛,所以这是又一次和儿子一起的旅行。

我叫Roy Hanen,我已退休。我来这里是想看看发生了什么,因为我想学点东西。很好。

我也叫Chuck博士,但我是一名退休的牙医。不,那不是我的视频,那是别人。但我确实是Chuck博士,只是拼写不同。好的,是的。梁叔,好的。

大家好,我是Many。还有Andy,你好像是计算机专家之类的?选修Chuck博士的所有课程吧,你会喜欢的。好的,你能学到很多。玩得开心,谢谢。

我是Ragu,我是一名管理员,正尝试学习Python,这有点难。好的,谢谢。

嘿,我叫Lue,我开始学习Python了。我热爱这门课,所以我们会继续学下去的。好的。

大家好,我是Cheryl,Randy是我的丈夫。我在这里除了学习拆电脑,还想试着学点别的。

大家好,我是一名电气工程师,我已经修完了Python和互联网存储课程,两门都非常好。好的。

大家好,我是Amy。我已经修完了互联网历史和Python课程,现在受到鼓舞要去完成Python的毕业项目。我想我能做到。哦,你一定能做到的,我相信你。谢谢。

我是Lee,我来这里是为了欢迎Chuck博士来到德克萨斯,并且非常喜欢他的课程,强烈推荐。他一直在请大家喝啤酒。

大家好,我叫Rahiem。我正在学习Python,以便理解我工作中的程序员同事。很酷。

我叫Carrie Lee,我和我儿子Dalton一起来的。我修了Python课程,还和我的两个孩子一起修了互联网历史课程。很酷,Dalton,你想打个招呼吗?很酷,我。是的。

大家好,我叫Barak。我正在学习“Python for Everybody”专项课程,以及Andrew博士的机器学习课程。感谢Chuck博士和Coursera团队让远程教育变得如此便捷。很酷。

好的,我叫Leon El Porera,我来自安哥拉。我邀请大家来和我们一起学习,这真的很有趣。我很乐意去,如果有个好理由的话。

好的,以上就是全部内容。我想,下一次办公时间将在几周后于纽约市举行,届时有一个Perl会议。但在我们离开、在我结束之前,我想特别感谢一下课程的导师们。这些人非常专注,付出了大量辛勤的工作。哇,我差点被一只鸟撞到。

“当鸽子哭泣时”,对吧?Prince(歌手)刚在昨天(或两天前)去世了。但我想为我们课程中出色的导师们鼓掌。

好的,非常感谢每一位导师,不仅是我课程中的,还包括所有在线课程的导师。你们做了这么多工作,让这一切对每个人都意义非凡。干杯。


本节课中,我们一起回顾了课程办公时间中同学们相互介绍的环节,感受到了在线课程线下社区的活力与互助精神。

089:使用Django表单功能

在本节课中,我们将要学习Django框架如何帮助我们高效地处理Web表单。我们将了解Django表单如何作为连接模型、视图和模板的“粘合剂”,并简化创建、验证和渲染表单的复杂过程。

到目前为止,我们主要讨论了表单的工作原理。我们讨论了表单在不同编程语言或框架中的通用工作方式,以及浏览器如何处理表单、如何生成HTML来创建表单、POST请求如何工作、刷新和重定向机制,以及CSRF保护。除了细微差别,这些主题无论你使用何种技术栈都是通用的。

上一节我们介绍了表单的通用概念,本节中我们来看看Django如何具体帮助我们完成刚才讨论的所有事情。

Django表单:核心的“粘合剂”

如果你观察这张图,会发现表单处于中心位置,这是我们正在填充的最后一块拼图。表单以多种方式连接:它们可以与模型关联,在视图中使用,也在模板中使用。因此,它们是Django中一个非常强大的部分。

我们一直在做的抽象工作——编写少量代码,然后找到多种方式重用这些代码,或者编写一些扩展对象的代码并添加少量功能,最终得到一个非常强大的对象——这正是我们在表单中要做的事情。表单可以在视图中使用,可以在模板中使用,甚至可以与模型交互。我们将详细讨论表单。

为什么需要Django表单?

手动构建所有HTML表单(就像我在之前的一些示例中所做的那样)非常繁琐,并且存在其他问题。主要问题是会产生大量重复代码。我们使用面向对象或其他技术来避免重复自己。

编写一个简陋表单的HTML很容易,但谁不想要一个美观的表单呢?如果你编写了HTML和一些CSS,做了一个简陋的版本,那么你必须修复它,或者交给设计师,然后他们必须去修复100个页面。

Django中有一些工具,比如我们稍后会讨论的crispy forms,它基本上是说:“嘿,我正在使用Django表单,等一下,我希望它们变得漂亮。”然后你就可以让它们变漂亮。如果你想改变它们的美观方式,你可以改变其工作方式,然后一次性改变所有表单。这就是不重复自己、能够依赖工具代码的意义。在复杂的应用程序中,你会爱上这种不必重复自己的想法。

尽管我已经向你们展示了一些东西,比如POST重定向、刷新、CSRF等,但实际上它比那更复杂。特别是当你开始讨论从数据库拉取数据放入编辑表单,然后检查数据是否有效时:如果数据无效,则返回一个错误表单,并给用户重新提交的机会;如果成功,则执行POST重定向,然后在会话中发送一条消息。我们在简单的示例中做过这些,但如果你仔细看看所有需要考虑的事项,那是相当复杂的。

表单处理的复杂性:创建、编辑、删除

我们不会在第一讲中完成所有这些,但这让你了解创建表单需要做的事情:比如新建一只猫,然后填写一些关于猫的表单字段,然后点击保存。

以下是向猫数据库插入一只猫需要执行的步骤:

  1. 浏览器最初发送GET请求。
  2. 你用一个空表单(包含一堆填空字段)进行响应。
  3. 用户几乎可以取消并退出。
  4. 用户可能输入一些数据并点击POST。
  5. 问题在于,如果你对数据有一些规则(例如,此项必填,必须至少5个字符等),你必须将包含该数据的表单发回,让他们修复并重新提交。
  6. 如果数据良好,则继续将数据存储到数据库中。
  7. 但随后我们必须在会话或类似的东西中放置某种成功消息,然后发送重定向。
  8. 浏览器立即返回并获取重定向URL,该URL提取消息、发送页面并显示成功。

我们已经完成了其中的部分内容。我们尚未完成的主要部分是中间部分,即验证数据并让用户进入错误重试循环,给他们另一次机会。

编辑过程(你拉取一只现有的猫并编辑其字段)甚至更复杂。你需要加载一只猫,但可能会因为请求的猫在数据库中不存在而返回错误页面。你发送包含旧数据的表单,用户可能离开、可能编辑、可能返回。你可能需要验证数据并遇到错误,然后他们必须修复数据,直到获得有效数据。然后你将数据存储到数据库,在会话中放入消息,发送重定向到成功URL,并在该成功URL中输出消息。

删除操作也不像看起来那么简单。删除的问题在于你不能使用GET请求删除,因此通常你需要导航:比如你看到一个小删除按钮,点击它,然后进入一个删除页面。该页面将加载你想要删除的数据,以便你构建一个确认页面。确认页面结果变得非常重要。

因为也存在将GET转换为POST的棘手方式。但如果他们请求了错误的数据,我们必须发送错误消息。否则,我们发送一个确认表单,询问“你确定要删除这个吗?”这对用户来说既是一种友好的确认,同时也将其转换为一个表单。当然,他们可以离开或取消,但如果他们点击“是”,则会转换为POST请求并包含键值。它要做的第一件事是再次从模型加载数据,如果数据不存在(他们得到了错误的键),则发送错误页面。如果你得到了正确的键并且用户有权限删除数据,那么你与模型对话并删除该条目。然后你在会话中放入“删除成功”的祝贺信息,并进行重定向(你不能用200 GET响应,而要用302响应)。然后浏览器立即执行GET请求到成功URL,从会话中提取消息,生成一个包含消息的成功页面。

Django表单对象的优势

现在我们将多次介绍这些内容,所以你不需要第一次就完全理解。我只是想告诉你表单很复杂,但很酷的是,我们将要创建的Django表单对象就像“粘合剂”。它们可以生成HTML,允许一致的外观和感觉,并且允许你改变外观和感觉。

当POST数据从浏览器返回时,表单会将其全部放在正确的位置。它根据你设定的一组规则验证数据(例如,这必须是某种类型的东西、日期、字符、整数或浮点数等)。所有这些都在Django中为你完成。如果出现问题,它会交给你一个错误表单,你可以直接发回给用户。当然,一旦验证通过,它会将数据移入模型,甚至可以自动进行数据库存储。

因此,表单就像一个“粘合剂”,它与模板对话,与浏览器对话,从浏览器读取数据。在某种程度上,表单就像控制器,就像视图,它只是让你的视图非常简洁。一旦你理解了它,你就会爱上它。

创建一个简单的Django表单

表单存在于一个名为forms.py的文件中,这并不奇怪。基本上,它看起来很像模型文件。

以下是表单的一个简单示例:

# forms.py
from django import forms

class BasicForm(forms.Form):
    title = forms.CharField(max_length=100)
    mileage = forms.IntegerField()
    purchase_date = forms.DateField()

表单与模型的一个不同之处在于,它们可以有称为“验证器”的东西。模型有关于最大长度等的规则,但还有更复杂的验证器。你甚至可以编写自己的函数代码,当表单传入时,你会查看某个字段并说:“哦,我想要两个大写字符后跟三个小写字符。抱歉,你没有按照我说的做。”这就是所谓的验证器,当错误页面返回时会显示:“嘿,你缺少这些东西。”我们将展示验证器的工作原理。

但基本上,你只需像为模型那样放入几行代码,就有效地定义了一个用户界面及其背后的一堆逻辑。

在本节中,我还没有做的一件事是将其连接到模型。你可能会说:“等等,我想创建一个表单,我实际上主要想从模型继承。”你可以这样做,但这不是本节的内容,这将在接下来的整个部分中介绍,不会太久。

在视图中使用表单对象

我们刚刚创建了这个表单,它有三个字段。这是一个非常简单的视图,它没有实际用处,只是展示你可以做的事情之一:创建一个基本表单。

# views.py
from django.shortcuts import render
from .forms import BasicForm

def form_create(request):
    form = BasicForm()
    return render(request, 'myapp/form_template.html', {'form': form})

当基本表单被定义为具有这三个字段时,你调用form.as_table()方法。这只是为了转储内容,这个UI看起来并不漂亮,但它为你生成了所有这些HTML输入标签。我们告诉它以表格形式输出,这不一定是最好看的方式,但所有HTML都是由表单工具生成的。它可以以列表形式发送,也可以以段落形式发送等。

这只是为了向你展示,如果你访问这个URL并查看源代码,你会看到里面的内容。所以你定义了一个表单对象,你可以做的事情之一就是“给我生成HTML”。这不是一个好例子,我们稍后会举一个更好的例子。更好的例子当然是在模板中使用它。

在模板中渲染表单

以下是一个包含简单表单的模板:

<!-- form_template.html -->
<form method="post">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <input type="submit" value="Submit">
    <button type="button" onclick="window.location.href='/cancel/'">Cancel</button>
</form>

我们有一个表单,方法是POST。我们有一些提交内容,我们为自己放了一个取消按钮,我们有一个CSRF令牌。我们将使用form.as_table,并用<table>标签包围它,使其看起来更漂亮。

这是一种更典型的方式。第一个例子只是为了转储内容。当你这样做时,你基本上是在GET请求中创建表单,创建它并将其传递到上下文中。form.as_table来自上下文,来自传入的变量。

我们创建了包含所有字段的表单对象,然后将其传递到我们的视图和模板中,模板会渲染它。这就是它看起来更漂亮的样子。如果你查看该表单的源代码,你会看到它和我们之前拥有的所有相同的表格内容,只是现在被包裹在一些东西中以使其看起来更漂亮。我们添加了一个提交按钮,它有一个表单标签,然后你实际上可以点击提交按钮并发生一些事情。这是在模板中使用表单的更典型方式。

使用表单进行数据编辑

我谈到了创建表单、更新表单和删除表单。这是一个表单如何使用的例子。现在我还没有讨论模型,我们稍后会做。所以我只是假装我们有一些旧数据。通常,这些旧数据(标题、里程、购买日期)会来自模型,但现在我们只是编造它。

然后你基本上初始化一个表单,并将这些旧数据作为构造函数的参数传入。这是BasicForm类的一个构造函数,传入旧数据并返回一个BasicForm类型的对象。我将其存储在变量form中,然后传递到上下文中。你会注意到我使用了完全相同的模板(我之前展示的那个),因为form.as_table理解有时这些字段中有数据,它会正确地放入数据,正确地转义数据。它理解这是一个数字,理解这是一个日期。它已经把所有这些东西都搞清楚了。

这就是它的工作方式:你只需以某种方式获取旧数据的键值对,将其传入构造函数,现在你渲染表单,表单库会负责放入所有旧数据。相信我,你可能很高兴我没有向你展示如何编写HTML和代码来处理所有这些事情。当你进行编码时,这会有点不同,因为它在属性内部。你不需要知道这些,因为你只会使用form.as_table,它已经搞定了所有规则。这是数百行你不需要编写的代码。

下一步:数据验证

接下来,我们将讨论验证部分。这实际上是当我们从浏览器接收回数据时发生的事情。

本节课中我们一起学习了Django表单的核心概念及其作为“粘合剂”的作用。我们探讨了手动处理表单的复杂性,以及Django表单如何通过生成HTML、提供一致且可定制的外观、处理数据绑定和验证来简化这一过程。我们还初步了解了如何在forms.py中定义表单、在视图中实例化表单,并在模板中使用{{ form.as_table }}等方法来渲染表单。虽然我们尚未深入连接模型和验证提交的数据,但已经为理解Django表单的强大功能奠定了基础。在接下来的课程中,我们将重点学习如何利用Django表单进行有效的数据验证。

090:Django表单数据验证 📝

在本节课中,我们将要学习Django表单的数据验证机制。数据验证是用户点击提交按钮后,服务器对接收到的数据进行规则检查的过程。我们将详细探讨验证流程、错误处理以及Django处理表单数据的标准模式。

数据验证概述

上一节我们介绍了如何通过Django表单生成HTML并收集用户输入。本节中我们来看看当用户提交数据后,服务器如何进行验证。

数据验证发生在用户点击提交按钮,数据被发送到服务器之后。服务器检查接收到的POST数据,判断其是否符合预定义的规则。

验证流程与错误处理

以下是数据验证的基本流程:

  1. 用户填写表单并提交。
  2. 服务器接收POST数据。
  3. 服务器根据表单规则验证数据。
  4. 如果数据有效,则存储数据并重定向到成功页面。
  5. 如果数据无效,则返回带有错误信息的表单,让用户修正。

在创建表单时,我们讨论的部分正是服务器接收、验证数据并采取相应行动的环节。

数据错误示例

数据错误的典型场景是:用户填写表单时忘记填写必填项(如邮政编码),点击提交后,一个红色提示框弹出,显示“邮政编码为必填项”。

虽然可以在浏览器端通过设置required属性来提示必填项,但并非所有浏览器都支持此功能。因此,如果确实需要某项数据,必须在服务器端进行检查以确保其存在。

为了演示错误,我们在表单中设置了一个验证器:要求标题至少包含两个字符。如果用户只输入一个字符并提交,服务器验证会失败。

此时,我们希望页面能显示错误信息,并给予修正的机会,同时回显用户之前输入的数据,以便重新提交。

验证器定义

在Django表单中定义验证器非常简单。以下是一个要求字段至少包含两个字符的验证器示例:

from django import forms
from django.core.validators import MinLengthValidator

class MyForm(forms.Form):
    title = forms.CharField(validators=[MinLengthValidator(2)])

这并非一个复杂的验证器。在实际应用中,例如博客系统,你可能会要求标题至少包含5个字符。

代码流程解析

接下来,我们通过代码逐步分析GET和POST请求的处理流程。

GET请求流程

当用户通过GET请求访问表单页面(例如编辑一个已有条目)时,视图函数会执行以下操作:

  1. 接收GET请求,其中可能包含需要编辑的旧数据。
  2. 使用初始数据创建表单实例。
  3. 构建上下文(context)并将其传递给模板进行渲染。
  4. 返回状态码200及该页面的HTML。

这个过程是编辑流程的第一步:获取旧数据,展示给用户,并提供修改的机会。

POST请求成功流程

假设用户修改了数据且没有错误,流程如下:

  1. 用户点击提交,数据通过POST请求发送到服务器。
  2. 视图的post方法接收请求,从request.POST中读取所有表单数据。
  3. 使用POST数据构造一个新的表单实例。表单知道每个字段的名称,因此能正确匹配键值对。
  4. 调用表单的is_valid()方法进行验证。如果数据有效(form.is_valid()True),则通常会在此处将数据存储到模型。
  5. 最后,使用状态码302重定向到成功页面(例如/form/success)。
  6. 浏览器接收到重定向指令后,立即发起一个GET请求到成功页面。
  7. 成功页面视图返回感谢信息。

在实际应用中,你可能会在存储数据后,通过会话(session)传递一条成功消息到重定向后的页面,本示例为保持简洁省略了此步骤。

这就是成功的处理流程。

POST请求失败流程

现在,让我们看看当数据验证失败时会发生什么。例如,用户输入的标题太短。

  1. 用户提交包含无效数据的表单。
  2. post方法接收请求并构造表单对象。
  3. 此时,form.is_valid()返回False,因为数据违反了验证规则。
  4. 关键点在于,这个表单对象不仅包含了用户提交的旧数据,还自动生成了所有相关的错误信息。
  5. 视图函数会重新构建上下文,将包含错误信息的表单对象传递给模板。
  6. 模板渲染表单时,会显示出这些错误信息。
  7. 页面以状态码200返回,用户可以看到错误并修正数据,然后再次提交。

这里有一个值得注意的模式:Django的标准做法是在POST请求验证失败后,直接返回状态码200和包含错误的页面。这与我们之前“POST成功后应重定向”的原则似乎相悖。

Django社区普遍认为,如果POST请求没有改变任何服务器数据(仅仅是验证失败并返回错误页面),那么返回200是可以接受的。问题在于,如果用户此时刷新页面,浏览器可能会弹出“确认重新提交表单”的警告。这是Django框架的工作方式。业界一致同意的是:如果在POST请求中保存了数据,则绝对不能返回200,而必须使用重定向(如302),否则可能导致数据被重复提交(如重复下单、重复转账等)。在本例的验证失败场景中,我们只是检查了数据并返回了错误信息,没有存储任何数据。

课程总结

本节课中我们一起学习了Django表单数据验证的核心机制。

我们回顾了整个课程系列的内容:从HTML和HTTP协议基础,到GET与POST方法的区别;从如何防止刷新导致重复提交,到跨站请求伪造(CSRF)的保护及Django的应对策略;最后,我们深入探讨了如何利用Django表单来简化HTML生成,并实现强大的服务器端数据验证。

表单对象的功能远不止于此。随着我们构建更复杂的应用程序,将继续学习更多关于表单的高级特性和用法。

091: Autos示例项目详解 🚗

在本节课中,我们将详细解析一个名为“Autos”的Django示例项目。这是一个完整的CRUD(创建、读取、更新、删除)应用,我们将通过分析其代码结构来学习Django的核心概念,包括模型、表单、视图、URL配置以及如何使用Django的通用视图来简化开发。

项目概述与运行

首先,让我们了解一下这个Autos项目。这是一个管理汽车品牌(Make)和具体汽车(Auto)信息的应用。它演示了如何建立一对多的关系(一个品牌对应多辆汽车),并实现了完整的增删改查功能,所有操作都受到登录保护。

运行项目后,访问/autos路径,系统会要求你先登录。登录成功后,你可以看到主界面,可以添加品牌、查看品牌列表、添加汽车等。汽车添加表单中有一个下拉菜单,其选项来自品牌表,这正是一对多关系的体现。在列表中,你可以对每条记录进行更新或删除操作。

核心文件结构

为了理解这个应用,我们需要分析四个核心文件:models.pyforms.pyurls.pyviews.py。接下来,我们将逐一进行解析。

1. 数据模型 (models.py)

模型文件定义了应用的数据结构。我们有两个模型:Make(品牌)和Auto(汽车)。

Make模型非常简单,只有一个name字段,并使用了验证器确保名称至少有两个字符。__str__方法定义了当打印模型对象时显示的内容。

from django.core.validators import MinLengthValidator
from django.db import models

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_24.png)

class Make(models.Model):
    name = models.CharField(
            max_length=200,
            help_text='Enter a make (e.g. Dodge)',
            validators=[MinLengthValidator(2, "Make must be greater than 1 character")]
    )

    def __str__(self):
        return self.name

Auto模型包含更多字段:昵称、里程、评论,以及一个指向Make模型的外键ForeignKey)。on_delete=models.CASCADE参数意味着当删除一个品牌时,与之关联的所有汽车记录也会被自动删除。

class Auto(models.Model):
    nickname = models.CharField(
            max_length=200,
            validators=[MinLengthValidator(2, "Nickname must be greater than 1 character")]
    )
    mileage = models.PositiveIntegerField()
    comments = models.CharField(max_length=300)
    make = models.ForeignKey('Make', on_delete=models.CASCADE, null=False)

    def __str__(self):
        return self.nickname

2. 表单 (forms.py)

表单用于在网页上生成输入界面并处理用户提交的数据。这里我们使用了ModelForm,它能根据模型自动生成表单字段,极大地简化了代码。

以下是为Make模型创建的ModelFormMeta类中指定了数据来源的模型和需要包含的字段(这里使用__all__表示包含所有字段)。

from django.forms import ModelForm
from .models import Make

class MakeForm(ModelForm):
    class Meta:
        model = Make
        fields = '__all__'

3. URL路由 (urls.py)

URL配置文件将不同的网址路径映射到对应的视图函数。Autos应用的URL模式清晰地组织了CRUD操作的路由。

以下是主要的URL模式:

  • autos/:列出所有汽车(主视图)。
  • make/:列出所有品牌。
  • make/create/:创建新品牌。
  • make/<int:pk>/update/:更新指定主键(pk)的品牌。
  • make/<int:pk>/delete/:删除指定主键的品牌。
  • 类似的,还有auto/create/auto/<int:pk>/update/auto/<int:pk>/delete/用于汽车的CRUD操作。

每个路径都通过name参数被赋予了易于引用的名称。

from django.urls import path
from . import views

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_31.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_32.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_34.png)

urlpatterns = [
    path('', views.MainView.as_view(), name='all'),
    path('main/create/', views.AutoCreate.as_view(), name='auto_create'),
    path('main/<int:pk>/update/', views.AutoUpdate.as_view(), name='auto_update'),
    path('main/<int:pk>/delete/', views.AutoDelete.as_view(), name='auto_delete'),
    path('lookup/', views.MakeView.as_view(), name='make_list'),
    path('lookup/create/', views.MakeCreate.as_view(), name='make_create'),
    path('lookup/<int:pk>/update/', views.MakeUpdate.as_view(), name='make_update'),
    path('lookup/<int:pk>/delete/', views.MakeDelete.as_view(), name='make_delete'),
]

4. 视图逻辑 (views.py) - 基础方式

视图是处理业务逻辑的核心。我们将先看看如何“手动”编写视图来实现CRUD,然后再介绍Django提供的更简洁的“通用视图”。

上一节我们介绍了模型和URL,本节中我们来看看视图如何运作。所有需要登录才能访问的视图都通过LoginRequiredMixin进行保护。

列表视图:以MainView(汽车列表)和MakeView(品牌列表)为例。它们从数据库中获取所有对象,传递给模板进行渲染。get_context_data方法用于向模板传递额外的上下文数据,比如品牌数量。

from django.views import View
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Auto, Make
from .forms import MakeForm

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_57.png)

class MainView(LoginRequiredMixin, View):
    def get(self, request):
        make_count = Make.objects.all().count()
        auto_list = Auto.objects.all()
        ctx = {'make_count': make_count, 'auto_list': auto_list}
        return render(request, 'autos/auto_list.html', ctx)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_59.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_61.png)

class MakeView(LoginRequiredMixin, View):
    def get(self, request):
        make_list = Make.objects.all()
        ctx = {'make_list': make_list}
        return render(request, 'autos/make_list.html', ctx)

创建视图:以MakeCreate为例。GET请求时,它提供一个空表单;POST请求时,它验证表单数据,如果有效则保存到数据库,并重定向到成功页面。

class MakeCreate(LoginRequiredMixin, View):
    template = 'autos/make_form.html'
    success_url = reverse_lazy('autos:all')

    def get(self, request):
        form = MakeForm()
        ctx = {'form': form}
        return render(request, self.template, ctx)

    def post(self, request):
        form = MakeForm(request.POST)
        if not form.is_valid():
            ctx = {'form': form}
            return render(request, self.template, ctx)
        form.save()
        return redirect(self.success_url)

更新视图:与创建视图类似,但需要先通过主键获取要更新的现有对象。get_object_or_404是一个便捷函数:如果对象存在则返回,不存在则直接返回404错误页面。

class MakeUpdate(LoginRequiredMixin, View):
    model = Make
    success_url = reverse_lazy('autos:all')
    template = 'autos/make_form.html'

    def get(self, request, pk):
        make = get_object_or_404(self.model, pk=pk)
        form = MakeForm(instance=make)
        ctx = {'form': form}
        return render(request, self.template, ctx)

    def post(self, request, pk):
        make = get_object_or_404(self.model, pk=pk)
        form = MakeForm(request.POST, instance=make)
        if not form.is_valid():
            ctx = {'form': form}
            return render(request, self.template, ctx)
        form.save()
        return redirect(self.success_url)

删除视图GET请求显示确认删除页面,POST请求执行删除操作。

class MakeDelete(LoginRequiredMixin, View):
    model = Make
    success_url = reverse_lazy('autos:all')
    template = 'autos/make_confirm_delete.html'

    def get(self, request, pk):
        make = get_object_or_404(self.model, pk=pk)
        ctx = {'make': make}
        return render(request, self.template, ctx)

    def post(self, request, pk):
        make = get_object_or_404(self.model, pk=pk)
        make.delete()
        return redirect(self.success_url)

使用Django通用视图进行简化 ✨

上面展示的视图虽然功能完整,但代码中存在大量重复模式。Django提供了基于类的通用视图(Generic Class-Based Views)来封装这些通用模式,让代码变得极其简洁。

以下是使用通用视图重写的Auto模型的CRUD操作:

from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Auto

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_101.png)

class AutoCreate(LoginRequiredMixin, CreateView):
    model = Auto
    fields = '__all__'
    success_url = reverse_lazy('autos:all')

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_103.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_105.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_107.png)

class AutoUpdate(LoginRequiredMixin, UpdateView):
    model = Auto
    fields = '__all__'
    success_url = reverse_lazy('autos:all')

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/607c0b39ee9e4dd20c9c2c60affe5347_109.png)

class AutoDelete(LoginRequiredMixin, DeleteView):
    model = Auto
    fields = '__all__'
    success_url = reverse_lazy('autos:all')

代码解析

  • CreateViewUpdateViewDeleteView分别对应创建、更新和删除的通用视图。
  • 只需指定model(操作哪个模型)、fields(使用哪些字段,__all__表示全部)和success_url(操作成功后跳转的地址)。
  • 视图会自动处理GETPOST请求、表单生成、数据验证、对象获取(get_object_or_404)、保存和删除等所有逻辑。
  • 它们还会根据模型名称自动寻找对应的模板(如auto_form.htmlauto_confirm_delete.html),无需手动指定template_name

通过对比可以发现,通用视图将数十行代码浓缩为寥寥几行,且更易于维护。对于Make模型的视图,也应该采用同样的方式进行简化,而不是复制冗长的手动代码。

模板与登录配置

视图需要模板来呈现页面。通用视图有默认的模板命名约定,例如<modelname>_form.html。这些模板接收formobject等上下文变量。

关于登录,项目配置使用了Django内置的认证系统。关键步骤包括:

  1. settings.py中启用django.contrib.auth应用。
  2. 在项目根urls.py中包含认证相关的URL(如path('accounts/', include('django.contrib.auth.urls')))。
  3. 在模板目录(如templates/registration/)下创建名为login.html的登录模板。
  4. 在需要登录的视图类中添加LoginRequiredMixin

总结与作业建议 🎯

本节课我们一起深入学习了Django的Autos示例项目。我们分析了如何构建一个完整的CRUD应用,涵盖了从模型定义、表单创建、URL配置到视图逻辑的完整流程。

核心收获包括:

  1. 模型(Model):使用ForeignKey建立表间关系,利用validators进行数据验证。
  2. 表单(Form):使用ModelForm快速生成与模型对应的表单。
  3. 视图(View)
    • 掌握了手动实现CRUD视图的方法,理解了get_object_or_404、表单验证和重定向等关键操作。
    • 更重要的是,学会了使用Django的通用视图CreateViewUpdateViewDeleteView)来极大地简化代码,这是Django高效开发的精髓。
  4. 认证:使用LoginRequiredMixin轻松实现视图的登录保护。

对于你的作业,强烈建议不要直接复制示例中冗长的“手动”视图代码(如MakeCreateMakeUpdate)。相反,应该像AutoCreate等视图一样,采用基于通用视图的简洁写法。理解并运用这种模式,将使你后续的Django开发事半功倍。请花时间消化视图中的逻辑,特别是通用视图的工作原理,这将是未来所有Django项目的基础。

092:海牙面对面办公时间 👨‍🏫

在本节课中,我们将回顾一次在荷兰海牙举行的线下办公时间活动。活动中,来自世界各地的学生分享了他们学习Python和参与课程的体验与收获。


活动开场与介绍

大家好,欢迎来到我们“面向所有人的互联网历史、技术、安全与Python”系列课程的又一次办公时间。我们现在位于荷兰海牙。我想向大家介绍一些你们的同学。

以下是参与活动的同学自我介绍:

  • S: 我叫S,我对Python很感兴趣,也是Charles博士的忠实粉丝。
  • Well: 嗨,我叫Well,我刚开始学习Python,对于能学习更多Python知识感到非常兴奋。
  • Bine: 你好,我是Bine,我也对Python感到兴奋。谢谢,Severs先生。
  • Ruth: 我叫Ruth,Charles博士是最棒的,现在你也是最棒的。
  • Eva: 你好,我是Eva,很高兴见到Charles博士。
  • Hera: 嗨,我是Hera,超级开心能和Charles博士在这里。
  • Jane: 嗨,我是Jane,Charles博士帮助我轻松掌握了Bton,这太棒了。
  • Martin: 嗨,我是Martin,我正在跟Charles博士和Ilavitu学习Python。
  • Victor: 你好,我叫Victor,能和Chuck教授在这里我非常激动。我真心想感谢他和Courtra,因为他们,我现在开始编程了,并且我非常喜欢编程这件事。
  • Catalina: 我是Catalina,我真的、真的、真的很喜欢这门课。

活动组织与特别感谢

让我们为组织这次活动的Catalina送上热烈的掌声。

谢谢你。不,谢谢你。这是第一次有人在Twitter上联系我并主动提出要为我安排这样的活动,我非常感激。

更多学员分享

活动继续,更多同学分享了他们的故事:

  • Georgia: 你好,我是Georgia,我学习了Charles博士的一些课程,你们也应该这样做。
  • Tim: 我是Tim,我学习了Charles博士的所有课程,终于有机会见到他了。
  • Stefan: 嗨,我是Stefan,我正在学习Charles博士的Python课程,我非常喜欢Do mediumium,非常感谢。
  • Rob: 你好,我叫Rob,我正在学习Python课程,很高兴再次见到Charles博士。你有一个关于Coursera如何影响你就业的有趣故事。
    • Rob的补充: 是的,我在Coursera和edX上学习了很多课程,现在,我终于找到了工作。
    • 回应: 恭喜你,他们听到这个消息会很高兴的。
  • Arriif: 你好,我叫Arriif。感谢你促成这次活动,让我能看到其他有相同兴趣的人,非常感谢。
  • Marina: 嗨,我是Marina,这门课给了我很多自信,谢谢你。

活动花絮与未来展望

再次感谢大家,这是一个非常庞大的团队。这里的天气有点冷,今天是荷兰的春日第一天,我甚至看到一些郁金香开始发芽,但我们都戴着手套,因为我们人太多,室内坐不下。然后我们在这里坐了一个小时才意识到,这里有暖气,但我们没开。所以我们正在研究如何打开暖气。

我想,下一个我可能见到你们的地方是爱沙尼亚,因为下一次Coursera办公时间将在爱沙尼亚举行。那么,我们到时候见!


本节课中,我们一起回顾了在海牙举行的线下办公时间。我们听到了许多学员学习Python的积极体验、课程带给他们的信心,甚至是如何助力职业发展。这次聚会不仅是一次学习交流,也体现了学习社区的温暖与支持。我们期待在未来的活动中再次相聚。

093: Django中POST重定向的实现 🔄

在本节课中,我们将学习如何解决一个常见的Web开发问题:用户提交表单(POST请求)后刷新页面导致数据重复提交。我们将使用“POST重定向”模式来解决这个问题,并了解如何通过会话(Session)传递临时消息。


问题背景与解决方案

上一节我们讨论了表单提交的基本流程。本节中我们来看看一个具体问题:当用户提交表单(POST请求)后,如果直接刷新浏览器页面,可能会导致表单被重复提交(例如,订单被创建两次)。

这个问题的核心在于,浏览器刷新时会重复执行上一次的请求。如果上一次是POST请求,浏览器会询问用户是否确认重新提交表单。如果用户确认,就会导致重复操作。

解决方案是采用 POST/重定向/GET 模式。其基本规则是:永远不要在POST请求的响应中直接返回HTML页面。正确的做法是,在处理完POST请求的数据后,返回一个重定向响应(HTTP 302),将用户的浏览器引导至另一个URL,该URL通常通过GET请求来加载最终页面。

这种模式在MVC(模型-视图-控制器)架构中属于控制器的职责。控制器负责根据用户操作,决定将其浏览器引导至何处。

以下是该模式的核心步骤:

  1. 用户通过GET请求访问表单页面。
  2. 用户填写并提交表单,发送POST请求。
  3. 服务器处理POST数据(如保存到数据库)。
  4. 服务器不返回HTML,而是返回一个302重定向响应,指向一个成功页面(通常是同一个表单页面或一个确认页面)。
  5. 浏览器自动向重定向的地址发送一个新的GET请求。
  6. 服务器响应这个GET请求,返回最终的HTML页面(例如“操作成功”)。

这样,用户最后看到的页面是GET请求的结果。此时再刷新页面,只会重复GET请求,而不会重复提交POST数据,从而避免了重复操作。


HTTP状态码回顾

在深入实现之前,我们先回顾几个关键的HTTP状态码:

  • 200 OK: 请求成功,通常伴随请求的页面或数据。
  • 302 Found: 请求的资源已被暂时移动到另一个URL,浏览器应自动访问该URL。这正是我们实现重定向所需的状态码。
  • 403 Forbidden: 服务器理解请求但拒绝授权。
  • 404 Not Found: 服务器找不到请求的资源。

我们将利用 302 状态码来实现重定向功能。


POST/重定向/GET模式详解

下图清晰地展示了问题所在及解决方案的流程:

问题流程(左侧):

  1. 用户提交表单(POST)。
  2. 服务器处理数据并返回“成功”页面(200 OK)。
  3. 用户刷新页面,浏览器询问是否重新提交表单。
  4. 用户确认,导致POST请求被再次发送,数据被重复处理。

解决方案流程(右侧):

  1. 用户提交表单(POST)。
  2. 服务器处理数据,但不返回页面,而是返回 302 重定向 响应。
  3. 浏览器自动向重定向地址发送 GET 请求。
  4. 服务器对GET请求返回“成功”页面(200 OK)。
  5. 此时用户刷新页面,只会重复GET请求,安全地再次显示成功信息,而不会重复提交数据。


使用会话传递消息

在POST请求中,我们生成了“猜对了”、“猜高了”等消息,但随后我们进行了重定向。那么,如何将这个短暂的消息传递到重定向后的GET请求中并显示给用户呢?

答案是使用 会话(Session)。我们可以将消息临时存储在用户的会话中,在重定向后的GET请求里再从会话中取出并显示,然后立即将其从会话中删除。这种技术被称为 闪现消息(Flash Message)

下面我们将通过一个“猜数字”的例子,展示一个简单的实现方式。


代码实现:猜数字游戏

以下是实现POST重定向和闪现消息的关键代码逻辑。

处理GET请求的视图

当用户首次访问页面或重定向后访问时,处理的是GET请求。

def get_guess(request):
    # 尝试从会话中获取可能存在的消息(来自之前的POST处理)
    message = request.session.get('message', False)
    
    # 如果消息存在,则获取后立即从会话中删除它(闪现一次)
    if message:
        del request.session['message']
    
    # 渲染页面,将消息(如果有)传递给模板
    return render(request, 'guess.html', {'message': message})

流程说明:

  1. 检查会话中是否存在 'message' 键。
  2. 如果存在,将其值赋给变量 message,并立即从会话中删除该键值对,确保消息只显示一次。
  3. 使用 render 函数返回HTML页面,并将 message 变量传入模板。首次访问时,messageFalse,页面不显示任何消息。

处理POST请求的视图

当用户提交猜测的数字时,处理的是POST请求。

def post_guess(request):
    # 1. 从POST数据中获取用户猜测的数字
    user_guess = int(request.POST['guess'])
    
    # 2. 检查猜测结果,生成相应的提示消息
    if user_guess < secret_number:
        message = "太低了!"
    elif user_guess > secret_number:
        message = "太高了!"
    else:
        message = "恭喜你,猜对了!"
    
    # 3. 将消息存储到会话中,而不是直接渲染页面
    request.session['message'] = message
    
    # 4. 关键步骤:返回一个重定向响应,指向处理GET请求的同一个URL
    return redirect(request.path)  # request.path 是当前视图的URL路径

流程说明:

  1. request.POST 字典中获取用户提交的猜测值。
  2. 根据业务逻辑(与秘密数字比较)生成提示消息。
  3. 不再使用 render,而是将消息字符串存入 request.session 字典。
  4. 调用 redirect() 函数,并传入 request.path(当前URL路径),这会返回一个302重定向响应,告诉浏览器“请重新访问这个地址(使用GET方法)”。

请求-响应循环演示

让我们通过浏览器的开发者工具(网络选项卡)来观察这个流程:

  1. 初始GET请求:用户访问页面,服务器返回表单(200 OK)。

  2. 提交表单(POST请求):用户输入数字并点击提交。

    • 服务器处理POST后,返回302重定向,响应体为空。
  3. 自动重定向(GET请求):浏览器收到302后,立即自动向指定URL发送GET请求。

    • 这个GET请求到达 get_guess 视图,从会话中取出消息并渲染页面,返回200 OK和包含消息的HTML。

  1. 安全刷新:此时页面显示“太高了!”。如果用户刷新页面,浏览器只是重复步骤3的GET请求,再次渲染页面(消息已在第一次GET时被删除,所以可能不显示或显示新内容),而不会重复提交POST数据,从而彻底避免了重复提交的问题。


核心要点与最佳实践总结

本节课中我们一起学习了Django中处理表单提交的重要模式。

核心规则:在处理POST请求的视图函数末尾,如果需要向用户展示一个页面,应该使用 redirect,而不是 render。直接在POST后渲染HTML是错误做法,会导致刷新时重复提交。

实现POST/重定向/GET模式的步骤

  1. GET阶段:显示空表单或初始页面。
  2. POST阶段
    • 验证并处理表单数据。
    • 将需要反馈给用户的消息(如成功/错误提示)存入 request.session
    • 使用 return redirect('some_url') 结束视图。
  3. 重定向后的GET阶段
    • request.session 中取出消息。
    • (重要) 显示消息后,立即将其从会话中删除(实现“闪现”效果,只显示一次)。
    • 使用 render 渲染最终页面并显示消息。

闪现消息(Flash Message)模式:利用会话在两次请求(POST -> 重定向 -> GET)间传递一次性消息,并在显示后立即清除,是Web开发中的常用技巧。

通过这种方式,我们构建了更健壮、用户友好的Web应用,有效防止了因浏览器刷新导致的数据重复提交问题。在接下来的课程中,我们将看到Django如何通过其内置的 forms.py 和表单类,让创建和处理表单变得更加简单和高效。

094:构建HTML表单 📝

在本节课中,我们将学习如何在HTML中构建表单。表单是Web应用与用户交互的核心组件,用于收集和提交数据。我们将介绍几种常见的HTML表单元素及其工作原理,包括文本输入、密码框、单选按钮、复选框、下拉选择框、文本域以及提交按钮。此外,我们还会简要了解HTML5引入的一些新输入类型。


基本表单元素

上一节我们介绍了表单的基本概念,本节中我们来看看几种最常用的HTML表单输入类型。

文本输入框

文本输入框是最基础的表单元素,用于收集单行文本信息。其HTML代码结构如下:

<input type="text" name="account" id="account">
  • type="text":定义输入类型为文本。
  • name="account":这是关键属性。当表单提交时,服务器会收到一个键值对,键就是这里的name值。例如,如果用户输入“Beth”并提交,服务器收到的数据将是 account=Beth
  • id="account":主要用于将<label>标签与此输入框关联起来,以提升可访问性。对于表单数据的提交,id属性不是必需的。

密码输入框

密码输入框在视觉上会将输入内容隐藏(通常显示为星号或圆点),但其数据提交方式与文本输入框完全相同。

<input type="password" name="pass" id="passwd">
  • `type="password":定义输入类型为密码。
  • 重要提示:密码框仅在用户界面中隐藏输入内容。当表单数据提交到服务器时,密码是以明文形式传输的,除非应用了额外的加密措施(如HTTPS)。

隐藏输入框

隐藏输入框在页面上不可见,但它的值会随表单一起提交。常用于传递一些不需要用户填写但服务器需要知道的数据。

<input type="hidden" name="user_id" value="12345">

选择类表单元素

在了解了基本的文本输入后,我们来看看用于提供选项的表单元素。

单选按钮

单选按钮用于在多个选项中只能选择一项的场景。所有属于同一组的单选按钮必须拥有相同的name属性值。

<input type="radio" name="when" value="AM"> AM
<input type="radio" name="when" value="PM"> PM

以下是关于单选按钮的关键点:

  • 当用户点击其中一个选项时,同组(name相同)的其他选项会自动取消选中。
  • 提交表单时,服务器会收到 when=选中的value值(例如 when=PM)。
  • 这些按钮可以分散在页面的任何位置,只要name相同,它们就属于同一组。

复选框

复选框允许用户从多个选项中进行多项选择。每个复选框都是一个独立的实体,通常拥有不同的name

<input type="checkbox" name="class1" value="E"> English
<input type="checkbox" name="class2" value="S539"> S539
<input type="checkbox" name="class3"> Math

以下是关于复选框的关键点:

  • 每个复选框是否被选中是独立的。
  • 提交表单时,只有被选中的复选框namevalue会作为数据发送给服务器。
  • 如果未指定value属性(如上面的“Math”),默认提交的值为 on
  • 用户可以选中任意多个、一个或不选中任何复选框。

下拉选择框

下拉选择框(<select>)为用户提供了一个选项列表,通常用于节省空间,用户只能从中选择一项。

<select name="soda">
    <option value="0">Coke</option>
    <option value="1">Pepsi</option>
    <option value="2">Mountain Dew</option>
</select>

以下是关于下拉选择框的关键点:

  • name属性定义在<select>标签上。
  • 每个<option>标签的value属性是提交给服务器的值,而标签内的文本(如“Coke”)是显示给用户看的。
  • 可以使用selected属性来预设默认选项:<option value="peanuts" selected>Peanuts</option>。如果不设置,默认选中第一项。

文本域与按钮

除了上述元素,表单中还需要处理大段文本和用户提交动作。

文本域

当需要用户输入多行、大段文本(如评论、博客文章)时,应使用<textarea>标签。

<textarea name="feedback" rows="4" cols="50">默认文本...</textarea>
  • name:定义提交时的键名。
  • rowscols:定义文本域的初始高度(行数)和宽度(列数)。
  • 用户可以在此区域内自由输入,包括换行符和空格,所有内容都会作为字符串提交。

提交按钮

提交按钮用于触发表单数据的发送。

<input type="submit" name="submit_btn" value="提交表单">
  • value属性的值会显示在按钮的文本上。
  • 当按钮被点击时,表单中所有具有name属性的元素数据会被收集并发送到<form>标签action属性指定的地址。
  • 在多语言环境中,按钮文本可能需要翻译,因此服务器端逻辑通常通过检查name(如submit_btn)是否存在来判断是否提交,而不是依赖value的文本内容。

其他按钮与简单交互

有时表单中需要“取消”或“返回”按钮。这可以通过一个普通按钮结合简单的JavaScript来实现。

<input type="button" value="取消" onclick="location.href='/home'; return false;">
  • `type="button":定义一个普通按钮,点击它不会自动提交表单。
  • onclick:定义了点击按钮时执行的JavaScript代码。
  • location.href='/home':将浏览器导航到 /home 这个URL。
  • return false;:阻止按钮可能触发的任何默认表单提交行为。

HTML5新增输入类型

传统的输入类型已经服务了二十多年。HTML5引入了一系列新的输入类型,它们能在支持它们的浏览器中提供更好的用户体验(如内置验证和专用输入界面),并在旧浏览器中优雅地降级为普通文本输入框。

以下是几个HTML5输入类型的例子:

  • 颜色选择器<input type="color" name="favcolor">
    • 会弹出系统或浏览器提供的颜色选择器,提交的值是十六进制颜色码(如 #ff0000)。
  • 日期选择器<input type="date" name="birthday">
    • 会弹出日期选择控件,方便用户选择日期。
  • 邮箱验证<input type="email" name="usermail">
    • 浏览器会进行简单的格式验证(检查是否包含“@”符号等),如果格式无效,在提交时会阻止表单并提示用户。
  • 数字与范围<input type="number" name="quantity" min="1" max="5">
    • 用于输入数字,并可配合minmax属性限制范围。浏览器会验证输入值是否在指定范围内。
  • URL验证<input type="url" name="homepage">
    • 浏览器会验证输入内容是否符合URL格式。

核心优势与注意事项

  1. 内置验证:对于emailnumber(配合min/max)、url等类型,浏览器会在用户提交表单前进行初步验证。如果验证失败,表单不会被提交,用户会立即得到反馈。
  2. 优雅降级:在不支持HTML5新特性的旧浏览器中,这些输入框会显示为普通的文本输入框(type="text"),功能依然可用。
  3. 服务器端验证仍需进行切记,浏览器端的验证是为了提升用户体验,绝不能替代服务器端的数据验证。 恶意用户或旧浏览器仍然可能提交无效数据,因此必须在服务器端对所有接收到的数据再次进行严格的检查和清理。

总结

本节课中我们一起学习了构建HTML表单的基础知识。我们介绍了多种核心的表单元素:用于输入文本的textpasswordtextarea;用于单项选择的radio按钮和select下拉框;用于多项选择的checkbox;以及触发提交的submit按钮。我们还了解了如何使用button类型结合JavaScript实现其他交互。最后,我们预览了HTML5带来的一些更智能的输入类型(如dateemailnumber),它们能提供更好的输入体验和初步的客户端验证。

记住,这只是HTML表单世界的开端。在实际开发中,你还需要深入学习如何设置表单的actionmethod属性,如何处理提交的数据,以及如何结合CSS和JavaScript来创建更美观、交互性更强的表单。

095:表单的GET、POST与HTTP方法 🚀

概述

在本节课中,我们将学习HTML表单如何与服务器通信。我们将重点介绍两种基本的HTTP方法:GET和POST。理解这两种方法的区别对于构建安全、有效的Web应用程序至关重要。

表单是我们在浏览器中创建填空区域的方式。其核心思想是绘制一些字段和位置供用户输入,然后通过某种方式将数据发送到服务器。我们将探讨多种收集数据并将其发送到服务器的方法。

GET与POST方法

上一节我们提到了表单的基本概念。本节中,我们来看看与服务器通信表单数据的两种基本方式。

我们之前使用锚点标签(<a>)所做的一切都是GET请求。这意味着你将获取一个文档并显示它。你可以在URL末尾添加参数,例如 ?guess=42。然而,我们真正要介绍并详细讨论的是POST数据的概念。在表单中,你实际上可以进行GET或POST操作。

以下是两种方法的简要对比:

  • GET请求:数据作为URL的一部分发送(例如 ?key1=value1&key2=value2)。
  • POST请求:数据在HTTP请求的消息体内发送,不会显示在URL中。

代码示例:数据转储工具

为了清晰地展示数据,我们将使用一个简单的工具函数。GET数据、POST数据,甚至Cookie数据都以类似字典的键值对形式传入。以下代码定义了一个 dump_data 函数,用于安全地格式化并输出这些数据。

from django.utils.html import escape

def dump_data(label, data_dict):
    """
    安全地转储并格式化字典数据,用于显示。
    """
    output = f"<p><b>{label}:</b></p>"
    if data_dict:
        for key, value in data_dict.items():
            output += f"<p>{escape(str(key))} = {escape(str(value))}</p>"
    else:
        output += "<p>(no data)</p>"
    return output

这个函数遍历字典的所有项,调用 HTML escape 以防止跨站脚本注入攻击,然后将键和值打印出来,并包裹在段落标签中。这样我们就可以在多个示例中复用这段代码来展示数据。

使用GET方法的表单

现在,让我们看一个使用GET方法的简单表单示例。这个视图函数会打印一个表单,并在提交后检索表单数据。

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt  # 暂时禁用CSRF保护以简化示例
def getform(request):
    html = """
    <html>
    <body>
        <form>
            <label>请输入你的猜测:</label>
            <input type="text" name="guess" size="40" />
            <input type="submit" value="提交" />
        </form>
    </body>
    </html>
    """
    response = html
    if request.GET:
        response += dump_data("GET 数据", request.GET)
    return HttpResponse(response)

这个表单包含一个文本输入框(name="guess")和一个提交按钮。当用户在框中输入“42”并点击提交按钮时,会发生以下情况:

  1. 浏览器会自动在原始URL的末尾追加 ?guess=42
  2. 它发起一个新的GET请求,URL包含这个参数。
  3. Django框架会解析这个请求,并将 guess=42 这个键值对放入 request.GET 字典中。
  4. 我们的 dump_data 函数将其显示出来。

使用POST方法的表单

接下来,我们看看使用POST方法的相同表单。关键区别在于 <form> 标签的 method 属性。

@csrf_exempt  # 暂时禁用CSRF保护以简化示例
def postform(request):
    html = """
    <html>
    <body>
        <form method="post">
            <label>请输入你的猜测:</label>
            <input type="text" name="guess" size="40" />
            <input type="submit" value="提交" />
        </form>
    </body>
    </html>
    """
    response = html
    if request.method == 'POST':
        response += dump_data("POST 数据", request.POST)
    return HttpResponse(response)

这个表单与GET表单几乎完全相同,只是我们将 method="post" 添加到了 <form> 标签中。这告诉浏览器使用POST技术(而非GET技术)将数据发送到服务器。

提交表单后,你会注意到:

  • URL中没有出现 ?guess=42 这样的参数。
  • 数据通过HTTP连接以不同的方式传输,位于所有请求头信息之后。
  • 在服务器端,我们通过检查 request.method 来判断是否为POST请求,并通过 request.POST 字典来访问提交的数据,我们仍然能获取到 guess 关键字及其值“42”。

GET与POST的工作原理对比

为了更清晰地理解,以下是两种方法工作原理的对比:

GET请求过程:

  1. 浏览器将表单数据(如 guess=42追加到目标URL的末尾
  2. 发起一个到该新URL的GET请求。
  3. Django接收请求,解析URL中的参数,并将其存入 request.GET

POST请求过程:

  1. 浏览器将表单数据放入HTTP请求的消息体(body)中。
  2. 发起一个到表单指定URL的POST请求(URL本身不变)。
  3. 请求头中包含 Content-Type: application/x-www-form-urlencoded 等信息,告诉服务器如何解析消息体。
  4. Django接收请求,从消息体中解析参数,并将其存入 request.POST

POST数据中如果包含特殊字符也需要编码,但我们现在暂时忽略这个细节。重要的是理解,在POST请求中看不到URL参数,是因为数据是作为HTTP连接的一部分传输的。

如何选择GET还是POST?

你可能会问,这有区别吗?是的,区别很重要。

以下是选择GET或POST的一些基本规则:

  • POST应用于创建或修改数据。任何会改变系统状态的操作(插入、更新、删除)都应使用POST。
  • GET适用于读取或搜索。获取信息、查询数据时应使用GET。它绝不应该用于插入、修改或删除数据。

这样规定有几个原因:

  1. 网络爬虫(Web Spiders):爬虫会跟踪网页上的GET请求链接来收集信息。如果你的应用错误地将删除操作设计为GET请求,爬虫可能会意外地“点击”所有删除链接,导致数据被清空。而爬虫通常不会自动提交POST请求,因为它们假设POST可能会改变服务器状态。
  2. 幂等性(Idempotent):GET请求应该是幂等的,即多次执行相同的GET请求应该产生相同的效果(或返回相同语义的结果)。例如,访问 ?page=1 应该总是返回第一页的内容。这使得GET请求可以被收藏、缓存和重复访问。
  3. 数据长度限制:GET请求通过URL传递数据,而URL长度存在限制(通常约2000字符,因浏览器和服务器而异)。POST请求通过消息体传输数据,可以处理更大量的数据,特别是上传文件(如图片)时必须使用POST。
  4. URL美观与安全:POST不会将参数暴露在URL中,这使得URL更简洁,并且不会在浏览器历史记录或服务器日志中明文留下敏感数据(如密码)。

总结

本节课中,我们一起学习了HTML表单中GET与POST两种HTTP方法的核心知识。

我们了解到,GET方法将数据附加在URL中进行发送,适用于安全的、幂等的读取操作;而POST方法将数据放在请求体内发送,适用于会改变服务器状态的创建、更新或删除操作,并能处理更大量的数据。正确选择GET或POST是构建安全、可靠且符合Web标准的应用程序的基础。

下一节,我们将快速回顾HTML表单中可以使用哪些不同类型的输入标签(<input>)。

001:课程欢迎与介绍 🎬

在本节课中,我们将概述《给所有人的Django》系列课程的最终部分。我们将回顾已掌握的基础知识,并预览本课程将如何通过构建一个包含多种常见功能的完整应用,来深化你的Django技能。


通过之前的课程学习,我们已经掌握了Django的所有基础技能。我们理解了Django的模式,理解了面向对象编程以及Django如何运用它。我们也理解了如何构建一个应用,以及应用文件结构的意义。例如,当提到 urls.py 时,你应该知道它是什么,而不是感到陌生。同样,对于 settings.py 以及Django应用的整体布局,你现在都应该了然于心。

上一节我们回顾了基础,本节中我们来看看本课程的全新内容。现在,我们将从头开始,持续构建一个单一的应用。我选择的主题,映射了你在构建自己应用时可能会遇到的五、六种常见功能需求。例如,你可能会想:“哦,我该如何添加图片功能?” 因此,我们将逐一探讨这些功能的实现。

以下是本课程将涵盖的核心功能主题:

  • 如何添加用户登录功能。
  • 如何添加图片上传与展示功能。
  • 如何添加评论系统。
  • 如何实现收藏(或“喜欢”)功能。
  • 如何实现搜索功能。
  • 如何让网站图标(favicon)显示在浏览器标签页上。

你会发现,在未来的开发中,你会反复回顾本课程的内容。我自己在编写Django应用时,也常常回顾这些资料并参考示例代码。我将提供大量的示例代码,并且教学方式会逐渐从幻灯片讲解,过渡到更多地带领大家一步步分析示例代码。

在构建这个不断完善的“分类广告”Django应用(这将是本课程的核心项目)的过程中,我们还将沿途学习 JavaScriptJQueryJSONAjax 等技术,并将它们应用到我们的项目中。


本节课中我们一起学习了本课程的总体介绍与目标。我们明确了本课程将基于已有知识,通过一个包含登录、图片、评论、收藏、搜索和图标等常见功能的实战项目,来提升你的Django全栈开发能力,并在此过程中引入前端技术以增强应用交互性。

002:市场平台初始设置与安装 🛠️

在本节课中,我们将学习如何为“市场平台”作业进行初始设置与安装。我们将从GitHub获取一个预配置的Django项目,在PythonAnywhere上创建新项目,配置MySQL数据库,并完成应用的初始部署。

概述

我们将按照GitHub仓库中的说明,一步步完成市场平台应用的初始设置。这个过程包括克隆代码库、检查虚拟环境、安装依赖、配置数据库以及部署应用。

环境检查与准备

首先,我们需要确保已经准备好正确的开发环境。这包括检查PythonAnywhere上的虚拟环境。

以下是检查虚拟环境的步骤:

  1. 打开PythonAnywhere的“Consoles”标签页。
  2. 启动一个新的Bash控制台。
  3. 运行命令检查虚拟环境目录:ls -la ~/.virtualenvs/
  4. 如果看到类似 ve-django5-2 的目录,说明虚拟环境已存在。

上一节我们确认了开发环境,本节中我们来看看如何获取项目代码。

获取项目代码

我们将从GitHub克隆预配置的市场平台项目代码。

在Bash控制台中,执行以下命令:

  1. 进入Django项目目录:cd ~/django_projects
  2. 克隆GitHub仓库:git clone https://github.com/csev/dj4e-market
  3. 克隆完成后,进入项目目录:cd market

现在,文件列表中应该会出现一个名为 market 的新目录,其中包含了项目的基础结构。

安装项目依赖

项目代码包含一个 requirements.txt 文件,列出了所有必需的Python包。

在项目目录下,执行以下命令来安装依赖:

  1. 激活虚拟环境(如果尚未激活):source ~/.virtualenvs/ve-django5-2/bin/activate
  2. 升级pip工具:pip install --upgrade pip
  3. 安装项目依赖:pip install -r requirements.txt
  4. 验证Django版本:python -m django --version

如果一切顺利,你将看到Django 5.2已安装成功。

配置数据库

市场平台应用将使用MySQL数据库,而不是默认的SQLite,以获得更好的性能。

以下是配置MySQL数据库的步骤:

  1. 在PythonAnywhere控制面板,进入“Databases”标签页。
  2. 初始化MySQL(如果尚未初始化)。
  3. 创建一个新的数据库,命名为 market
  4. 返回项目,编辑配置文件 market/config/settings.py
  5. 找到 DATABASES 设置部分。
  6. 注释掉默认的SQLite配置,并确保MySQL配置正确。配置示例如下:
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'market',
        'USER': '你的PythonAnywhere用户名',
        'PASSWORD': '你的数据库密码',
        'HOST': '你的用户名.mysql.pythonanywhere-services.com',
    }
}
  1. 保存文件后,运行检查命令:python manage.py check,确保没有错误。

应用数据库迁移

配置好数据库连接后,需要将Django的数据模型同步到新的MySQL数据库中。

在项目目录下,执行以下命令:

  1. 生成迁移文件:python manage.py makemigrations
  2. 应用迁移,创建数据库表:python manage.py migrate

你可以通过MySQL控制台执行 SHOW TABLES; 来验证所有表是否已成功创建。

创建管理员账户

新的数据库需要创建一个超级用户来访问Django管理后台。

运行以下命令创建管理员:
python manage.py createsuperuser
按照提示输入用户名(例如 admin)、邮箱(可选)和密码。

配置Web应用

最后一步是在PythonAnywhere上配置Web应用,使其指向我们的新项目。

  1. 进入“Web”标签页。
  2. 找到你的Django Web应用配置。
  3. 将“代码”和“工作目录”路径修改为指向 ~/django_projects/market
  4. 在WSGI配置文件中,找到引用项目设置的部分,将其从 mysite.settings 修改为 config.settings
  5. 保存所有更改,并点击绿色的“重载”按钮使配置生效。

配置完成后,访问你的网站URL(例如 yourusername.pythonanywhere.com),应该能看到市场平台的主页。访问 /admin 并使用刚才创建的超级用户账号登录,可以进入管理后台。

总结

本节课中我们一起学习了市场平台Django应用的完整初始设置流程。我们完成了从GitHub克隆项目、安装依赖、配置MySQL数据库、执行数据迁移、创建管理员账户到最终部署上线的所有关键步骤。现在,你的应用已经搭建在一个更强大的MySQL数据库之上,并做好了进一步开发的功能基础。在接下来的课程中,我们将基于此基础构建市场平台的具体功能。

003:面对面办公时间-西班牙巴塞罗那

在本节课中,我们将回顾密歇根大学《Django for Everybody》课程在西班牙巴塞罗那举行的第二次面对面办公时间。本次会议旨在让课程的学生和讲师进行线下交流,分享学习经验。

会议开场与介绍

讲师首先向大家问好,并解释了本次是巴塞罗那的第二次办公时间。由于第一次会议的录像丢失,因此安排了这次额外的交流机会。

以下是参与本次办公时间的部分学生自我介绍:

  • Ia:计划开始学习互联网历史课程,并获得了相关的贴纸。她认为Python比PHP更酷。
  • el:正在学习Python,并且已经完成了相关的专题课程。
  • Christina:是一名语言学家,正在鼓励语言学领域的人学习Python。
  • co:正在学习Python。
  • Marta:正在学习Python专题课程,认为课程非常有趣,并强烈推荐给大家。
  • Sean:他有幸见到了Charles Severance教授(课程讲师),并认为其他同学会羡慕他们能与老师进行有趣的交流。
  • Teresa:她非常享受Python课程,并认为通过课程可以学到很多。

会议总结与结束

在大家自我介绍之后,讲师对本次在巴塞罗那与优秀学生们进行的成功办公时间表示满意。

他向大家致意,并期待下一次的会面。


本节课中我们一起学习了巴塞罗那线下办公时间的概况,通过学生们的自我介绍,我们看到了来自不同背景的学习者如何通过《Django for Everybody》课程探索Python和Web开发。这种线下交流为在线学习提供了宝贵的补充。

004:Django中的所属行概述 🏷️

在本节课中,我们将要学习Django中的“所属行”概念。我们将探讨如何限制用户只能编辑或删除他们自己创建的数据行,同时允许所有用户查看这些数据。这是构建真实应用程序(如分类广告系统)时的常见需求。


什么是所属行? 🤔

到目前为止,您编写的每个CRUD应用程序都允许登录用户编辑或删除所有数据行。在列表视图中,每个数据行旁边都会显示更新和删除按钮。

真实应用程序并非如此运作。例如,在构建分类广告系统时,某些广告属于您,某些广告属于其他人。您可以查看所有广告,但不能编辑所有广告。因此,我们需要决定何时显示编辑和删除按钮,并且只对数据行的所有者显示这些按钮。同时,我们必须防止用户通过其他方式编辑或删除他人的数据。

这就是“所属行”的概念:如果您拥有某一行数据,您将看到编辑或删除按钮;如果您不拥有该行数据,您只能查看该行数据。


回顾通用视图的使用 🔄

在之前的CRUD应用程序中,我们大量使用了Django提供的通用视图,特别是generic.ListView。通过一系列操作,我们最终得到了一个非常简单的视图。

例如,我们创建了一个HorseListView,它继承自generic.ListView。我们只需通过以下代码告知该视图使用哪个模型:

class HorseListView(generic.ListView):
    model = Horse

generic.ListView包含数千行代码,我们通过继承并指定模型来利用其功能。之后,一系列约定开始生效:变量将被命名为horse_list,它是一个从数据库中检索出的模型对象列表。这使得代码非常简洁,减少了重复。每个视图看起来都很相似,如果您熟练掌握,可以快速进行此类更改。


所属行视图的实现 🛠️

在所属行视图中,我们将采用类似的方法。我们将创建一个Article模型,并扩展OwnerListView。稍后我们将查看OwnerListView的内部实现,会发现它本身继承自generic.ListView,并通过model = Article来告知OwnerListView

不同之处在于,OwnerListView是我们自己编写的代码。我们将为列表视图添加一个功能,然后扩展它,创建一个名为OwnerListView的新视图(或称为“所属行列表视图”)。这样,我们就拥有了处理所属行所需的一切。

例如,每个Article都将有一个owner字段。如果该字段的值等于当前登录用户,我们将显示编辑和删除按钮;如果不等于,则不显示。但请注意,不显示链接并不意味着防止了编辑操作。稍后我们将展示如何实现编辑保护。

这是一个常见问题,我们希望避免在每个需要所属行功能的视图中编写数百行代码。因此,我们使用面向对象的技术来实现这一点。


利用继承实现代码复用 📚

我们将使用继承来实现所属行功能。继承是面向对象编程中的一个概念:您有一个类,可以创建另一个类来继承父类的所有属性和方法,并扩展它,添加一些新功能。这是“不要重复自己”原则的另一种形式。

您可以在父类中实现一次通用功能,然后创建子类来扩展父类。子类(或称为派生类)是更专门化的类,它们继承父类的属性和方法,并可以引入自己的新功能。之后,我们使用子类来完成特定任务。


下一步:深入通用视图 🔍

接下来,我们将探讨如何深入这些通用视图,并为它们添加我们自己的特殊功能。这将帮助我们更好地理解Django视图的工作机制,并能够根据需求进行定制。


总结 📝

在本节课中,我们一起学习了Django中的“所属行”概念。我们了解了为什么在真实应用程序中需要限制用户只能编辑或删除他们自己的数据行。我们回顾了如何使用Django的通用视图,并探讨了如何通过继承generic.ListView来创建OwnerListView,以实现所属行功能。最后,我们讨论了面向对象编程中的继承概念,以及如何利用它来实现代码复用和功能扩展。

005:通用视图内部机制与所属行回顾 🧠

在本节课中,我们将深入探讨Django通用编辑视图的内部工作原理。我们将学习如何通过重写和扩展其内部方法,将一个通用的编辑视图改造为支持“所属行”功能的视图。理解这些机制是自定义视图行为的关键。

通用编辑视图流程回顾 🔄

上一节我们介绍了CRUD操作的基本概念,本节中我们来看看通用编辑视图的内部处理流程。这是一个复习内容,我们将快速回顾编辑表单的完整生命周期。

当用户点击编辑按钮时,会发起一个GET请求(例如 /1/15)。视图会执行以下步骤:

  1. 根据主键(1、2或15)从数据库加载数据。
  2. 如果未找到对应数据,则返回404错误。
  3. 用加载的旧数据填充表单,并呈现给用户。
  4. 用户编辑数据并提交POST请求。
  5. 根据表单规则验证提交的数据(注意:模型处理后端逻辑,表单处理前端交互,验证是表单的职责)。
  6. 如果验证出错,则返回表单让用户修正。
  7. 如果数据有效,则重新加载对应的模型实例。
  8. 检查该模型实例是否可访问(这对我们即将实现的“所属行”功能至关重要)。如果实例不存在或用户无权访问,则返回404错误(这可能是因为用户篡改了表单或提交到了错误的URL)。
  9. 如果可访问,则更新数据并保存。
  10. 最后,重定向到成功URL。

这就是基本的表单处理流程,我们在之前实现每个CRUD功能时都遵循了类似的模式。

通用视图的方法流程图 📊

Django文档揭示了通用视图类内部的运作机制。通过方法流程图,我们可以看到类是如何一步步执行其“魔法”的。

以下是通用列表视图(部分编辑视图也类似)的典型方法调用顺序:

  1. setup()
  2. dispatch():检查请求方法是否被允许。
  3. get_template_names()
  4. get_queryset():从数据库加载数据。
  5. get_context_object_name()
  6. get_context_data():获取模板上下文数据。
  7. render_to_response()

你可能会问,为什么文档要展示这些?答案是,这为我们提供了介入和修改这些内置类行为的接入点。通过重写这些方法,我们可以在不深入了解整个类的情况下,扩展其功能。

编辑视图中的关键方法 🎯

现在,让我们从方法调用的角度,来看待通用编辑视图的处理过程。

以下是编辑视图中关键方法被调用的时刻:

  • GET请求阶段:请求携带主键进入。视图通过 get_queryset() 方法读取数据。随后,在渲染模板前,会调用 get_context_data() 方法来准备上下文数据。这是我们之后可以介入并添加自定义逻辑的地方。
  • POST请求阶段:用户提交表单后,视图首先在 form_valid() 方法中进行验证。我们也可以在 form_valid() 中加入自己的代码。如果验证失败,视图会再次使用 get_context_data() 渲染带错误信息的表单。如果验证通过,视图会再次调用 get_queryset() 获取数据对象,然后修改并保存数据。

核心要点form_valid()get_queryset()get_context_data() 这些关键时刻,正是我们扩展这个内置类行为的切入点。

方法重写技术:完全覆盖 🛠️

之前课程中,我展示了一个重写 get_queryset() 方法的示例。以下是其代码结构:

class WackyEquineView(generic.ListView):
    model = Car
    template_name = 'myapp/car_list.html'

    def get_queryset(self):
        # 完全忽略父类的 model (Car),返回 Horse 对象的列表
        return Horse.objects.all()

在这个 get_queryset 方法中,我完全覆盖并忽略了父类 generic.ListView 中的 get_queryset 逻辑。原本视图会根据 model = Car 查询汽车,但由于我的重写,它现在调用的是 WackyEquineView 中的方法,返回一个马匹对象的列表。因此,模板最终渲染的是马匹列表,而非汽车列表。

代码解析

  • 我创建了一个继承自 generic.ListView 的派生类 WackyEquineView
  • 我重写了 get_queryset 方法,使其返回 Horse.objects.all()
  • 这样做的结果是,我彻底改变了父类的行为,而我的代码完全无视了类属性中定义的 model = Car

这个示例的目的纯粹是为了演示:我可以在创建派生类时,通过完全重写方法来改变父类的行为。你需要了解被重写方法的规则(例如返回值类型)。编写这样的代码可能需要参考Django源码或Stack Overflow,并非轻而易举。

方法重写技术:增强扩展 ⚡

另一种重写方法是“增强”或“扩展”父类方法,而不是完全替换。以 get_context_data 方法为例。

get_context_data 的作用是准备传递给模板的上下文数据。在通用列表视图中,get_queryset 返回的列表(例如马匹列表)会被自动放入上下文(例如变量 horse_list),供模板循环渲染。

我不想完全替换 get_context_data,而是希望在父类提供的上下文基础上,额外添加一些数据。

以下是实现方式:

def get_context_data(self, **kwargs):
    # 1. 首先调用父类(generic.ListView)的 get_context_data 方法
    context = super().get_context_data(**kwargs)
    # 2. 然后添加自定义数据到上下文中
    context['crazy_thing'] = 'Crazy Thing'
    # 3. 返回合并后的上下文
    return context

代码解析

  • super().get_context_data(**kwargs):这行代码调用了父类(generic.ListView)的 get_context_data 方法。super() 关键字用于调用父类的方法。**kwargs 确保所有参数都被传递进去。
  • 父类方法执行后,会返回一个包含 horse_list 等数据的上下文字典 context
  • context['crazy_thing'] = 'Crazy Thing':我向这个字典中添加了一个新的键值对。
  • 最后,返回这个增强后的上下文字典。

这样,模板中既能访问到由父类逻辑生成的 horse_list,也能访问到我额外添加的 crazy_thing 变量。

两种技术的对比

  • 完全覆盖:就像 get_queryset 示例,彻底替换父类逻辑,适用于需要完全不同行为的情况。
  • 增强扩展:就像 get_context_data 示例,先调用父类逻辑获取结果,然后在此基础上修改或添加内容,最后返回组合后的结果。

总结 📝

本节课中我们一起学习了Django通用视图的内部机制与自定义方法。

  1. 我们回顾了通用编辑视图处理GET和POST请求的完整流程。
  2. 我们了解了Django通过方法流程图暴露的内部接入点,如 get_querysetget_context_dataform_valid
  3. 我们掌握了两种关键的方法重写技术:
    • 完全覆盖:彻底替换父类方法的行为(如 get_queryset 示例)。
    • 增强扩展:调用父类方法后,在其结果上添加额外逻辑(如 get_context_data 示例)。

在接下来的课程中,我们将运用这两种技术,通过创建 owner.py 代码并“介入”到这些通用视图中,来实现“所属行”字段的功能,确保用户只能访问和修改自己拥有的数据。

006:Django中的所属行-owner.py文件 🔐

在本节课中,我们将学习如何在Django应用中实现“所属行”功能,即确保用户只能查看、编辑或删除他们自己拥有的数据。我们将通过创建一个自定义的owner.py文件,并利用Django的类视图和面向对象编程来实现这一功能。


概述

我们将创建一个名为owner.py的文件,其中包含一系列自定义的视图类(如OwnerListViewOwnerUpdateView等)。这些类将扩展Django的通用视图,并添加额外的逻辑来根据当前登录的用户过滤数据。核心思想是:在模型中添加一个owner字段(外键关联到Django的用户模型),然后在视图中重写某些方法,以确保数据操作的安全性。


模型层的修改

上一节我们介绍了视图的基本结构,本节中我们来看看如何修改模型以支持所属行功能。

首先,我们需要在Article模型中添加一个owner字段。这个字段是一个外键,指向Django内置的User模型。以下是修改后的模型代码:

from django.db import models
from django.conf import settings

class Article(models.Model):
    title = models.CharField(max_length=200)
    text = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )

代码解释

  • owner = models.ForeignKey(...):这行代码是关键。它创建了一个指向用户模型的外键关系。
  • settings.AUTH_USER_MODEL:这是Django推荐的方式,用于引用用户模型。它允许项目灵活地使用自定义用户模型。
  • on_delete=models.CASCADE:这是一个级联删除选项。它意味着如果关联的用户被删除,那么属于该用户的所有文章也将被自动删除,以保持数据的一致性。

这个owner字段将用于在数据库层面建立文章与用户之间的关联。


视图层的实现

现在,我们来看看如何创建自定义的视图类。我们将不再直接使用Django的通用视图(如generic.ListView),而是创建自己的视图类(如OwnerListView),并在其中添加所属逻辑。

以下是owner.py文件的基本结构:

from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin

class OwnerListView(ListView):
    """所有者的列表视图基类。"""
    pass

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/89c28e67cdf3f58cc5fa0f53318ad234_15.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/89c28e67cdf3f58cc5fa0f53318ad234_16.png)

class OwnerDetailView(DetailView):
    """所有者的详情视图基类。"""
    pass

class OwnerCreateView(LoginRequiredMixin, CreateView):
    """所有者的创建视图基类。"""
    pass

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/89c28e67cdf3f58cc5fa0f53318ad234_18.png)

class OwnerUpdateView(LoginRequiredMixin, UpdateView):
    """所有者的更新视图基类。"""
    pass

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/89c28e67cdf3f58cc5fa0f53318ad234_20.png)

class OwnerDeleteView(LoginRequiredMixin, DeleteView):
    """所有者的删除视图基类。"""
    pass

注意:对于CreateViewUpdateViewDeleteView,我们混入了LoginRequiredMixin。这确保了只有登录的用户才能访问这些视图。如果未登录的用户尝试访问,他们将被重定向到登录页面。


重写 get_queryset 方法

对于OwnerUpdateViewOwnerDeleteView,我们需要确保用户只能修改或删除他们自己拥有的数据。这可以通过重写get_queryset方法来实现。

以下是OwnerUpdateView中重写get_queryset的示例:

class OwnerUpdateView(LoginRequiredMixin, UpdateView):
    def get_queryset(self):
        """
        重写此方法,只返回当前用户拥有的对象。
        """
        qs = super().get_queryset()
        return qs.filter(owner=self.request.user)

方法解释

  1. qs = super().get_queryset():首先调用父类(UpdateView)的get_queryset方法,获取默认的查询集。
  2. return qs.filter(owner=self.request.user):然后,我们在这个查询集上添加一个额外的过滤器,只保留owner字段等于当前登录用户(self.request.user)的记录。

这样,当用户尝试编辑一个不属于他们的文章时,get_queryset将返回一个空的查询集,Django会自动返回一个404(未找到)错误,从而保护了数据。


重写 form_valid 方法

对于OwnerCreateView,我们需要在保存新文章时自动将当前用户设置为owner。这可以通过重写form_valid方法来实现。

以下是OwnerCreateView中重写form_valid的示例:

class OwnerCreateView(LoginRequiredMixin, CreateView):
    def form_valid(self, form):
        """
        在表单验证通过后,自动设置owner为当前用户。
        """
        # 暂时不保存到数据库,先获取对象实例
        object = form.save(commit=False)
        # 将当前用户设置为owner
        object.owner = self.request.user
        # 现在保存到数据库
        object.save()
        # 调用父类的form_valid方法完成后续操作(如重定向)
        return super().form_valid(form)

方法解释

  1. object = form.save(commit=False)form.save(commit=False)会创建一个模型对象实例,但不会立即保存到数据库。这允许我们在保存之前修改对象的属性。
  2. object.owner = self.request.user:我们将当前登录的用户赋值给对象的owner字段。
  3. object.save():现在,我们将对象(包含新设置的owner)保存到数据库。
  4. return super().form_valid(form):最后,我们调用父类的form_valid方法来执行标准的后续操作,例如重定向到成功页面。

这样,即使用户在表单中看不到owner字段,新创建的文章也会自动关联到他们。


在应用中使用自定义视图

创建了owner.py中的基类后,我们在应用的views.py中使用它们就非常简单了。以下是Article相关的视图示例:

from .models import Article
from .owner import OwnerCreateView, OwnerUpdateView, OwnerDeleteView, OwnerListView, OwnerDetailView

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/89c28e67cdf3f58cc5fa0f53318ad234_44.png)

class ArticleListView(OwnerListView):
    model = Article
    # template_name 默认为 'article_list.html'
    # context_object_name 默认为 'article_list'

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/89c28e67cdf3f58cc5fa0f53318ad234_46.png)

class ArticleDetailView(OwnerDetailView):
    model = Article

class ArticleCreateView(OwnerCreateView):
    model = Article
    fields = ['title', 'text'] # 只允许用户编辑标题和内容

class ArticleUpdateView(OwnerUpdateView):
    model = Article
    fields = ['title', 'text']

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/89c28e67cdf3f58cc5fa0f53318ad234_48.png)

class ArticleDeleteView(OwnerDeleteView):
    model = Article

代码解释

  • 我们的ArticleListView现在继承自OwnerListView,而不是generic.ListView。对于列表和详情视图,Owner版本目前没有额外逻辑,但为未来扩展提供了统一的基础。
  • ArticleCreateViewArticleUpdateViewfields属性指定了哪些字段应该出现在表单中。注意,我们没有包含ownercreated_atupdated_at字段,因为这些字段是自动处理的。
  • 通过继承OwnerDeleteView,删除操作也自动受到了保护,用户只能删除自己的文章。


总结

本节课中我们一起学习了如何在Django中实现“所属行”功能。我们主要完成了以下工作:

  1. 修改模型:在Article模型中添加了owner字段,作为指向用户模型的外键。
  2. 创建自定义视图基类:在owner.py文件中创建了OwnerListViewOwnerCreateViewOwnerUpdateViewOwnerDeleteView等类。
  3. 实现访问控制
    • 通过LoginRequiredMixin确保只有登录用户才能进行创建、更新和删除操作。
    • 通过重写OwnerUpdateViewOwnerDeleteViewget_queryset方法,确保用户只能操作自己拥有的数据。
    • 通过重写OwnerCreateViewform_valid方法,在创建新对象时自动关联当前用户。
  4. 应用自定义视图:在应用的views.py中,让我们自己的视图(如ArticleCreateView)继承自owner.py中的基类,从而轻松获得所有安全功能。

这种方法充分体现了Django“不要重复自己”(DRY)的原则和面向对象编程的强大之处。我们通过创建可重用的基类,将所属逻辑封装在一处,从而简化了具体视图的代码,并确保了整个应用数据访问的一致性。

007:Crispy表单实战演练 🎨

在本节课程中,我们将学习如何使用Crispy Forms库来美化Django应用中的表单。我们将看到,无需修改视图逻辑或表单定义,仅通过调整模板和配置,就能将默认的简单表单转换为具有现代样式的精美表单。

概述

Django内置的表单渲染功能可以自动生成HTML,但样式通常比较基础。Crispy Forms是一个第三方库,它允许我们使用流行的前端框架(如Bootstrap)的样式来渲染表单,从而显著提升表单的外观和用户体验,而无需手动编写大量HTML和CSS。

上一节我们介绍了Django内置的表单渲染方式,本节中我们来看看如何使用Crispy Forms库来替代它。

内置表单渲染与Crispy表单对比

在Django中,我们创建一个表单对象,并将其传递给模板。在模板中,我们可以使用类似 {{ form.as_table }} 的语法来渲染表单。这会生成一个包含所有表单字段的HTML表格。

代码示例(内置渲染):

<table>
    {{ form.as_table }}
</table>

这种方法生成的HTML结构简单,但缺乏现代、美观的样式。

Crispy Forms库则提供了另一种渲染方式。它接收相同的表单对象,但输出的是应用了Bootstrap样式的、更美观的HTML代码。表单会拥有圆角边框、高亮效果、清晰的标签和必填字段标识(星号)。

代码示例(Crispy渲染):

{% load crispy_forms_tags %}
{% crispy form %}

关键点在于,我们没有改变视图(views.py)中创建和传递表单对象的逻辑,也没有改变表单类(forms.py)本身的定义。我们仅仅改变了模板中渲染这个对象的方式。

配置Crispy Forms

要使Crispy Forms正常工作,需要进行一些安装和配置。以下是必要的步骤:

首先,需要在Python虚拟环境中安装Crispy Forms及其Bootstrap适配器。

安装命令:

pip install django-crispy-forms
pip install crispy-bootstrap5

接下来,需要在Django项目的设置文件 settings.py 中进行配置。

配置 settings.py:

  1. crispy_formscrispy_bootstrap5 添加到 INSTALLED_APPS 列表中。
  2. 设置 CRISPY_ALLOWED_TEMPLATE_PACKSCRISPY_TEMPLATE_PACK 变量,指定使用Bootstrap 5。

代码示例:

# settings.py
INSTALLED_APPS = [
    ...
    'crispy_forms',
    'crispy_bootstrap5',
    ...
]

CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"

在模板中使用Crispy Forms

配置完成后,在模板中使用Crispy Forms非常简单。主要涉及两个模板标签。

以下是使用Crispy Forms渲染表单的模板示例:

代码示例:

<!-- awesome_template.html -->
{% load crispy_forms_tags %}
<!DOCTYPE html>
<html>
<head>
    <title>Awesome Form</title>
    <!-- 确保引入了Bootstrap的CSS -->
    <link href="path/to/bootstrap.css" rel="stylesheet">
</head>
<body>
    <form method="post">
        {% csrf_token %}
        <!-- 核心变化:使用crispy过滤器渲染表单 -->
        {% crispy form %}
        <!-- 提交按钮通常也会被crispy样式化 -->
        <input type="submit" value="Submit">
    </form>
</body>
</html>

代码解释:

  1. {% load crispy_forms_tags %}:这行代码必须放在模板顶部,它加载了Crispy Forms的模板标签库,使其提供的 crispy 过滤器可用。
  2. {% crispy form %}:这是核心步骤。form 是视图传递过来的表单对象。| 是过滤器管道符,crispy 是过滤器名称。这行代码将表单对象交给Crispy Forms库进行渲染,生成带有Bootstrap样式的HTML。

核心优势:面向对象设计的灵活性

这个练习展示了Django面向对象设计的一个强大优势。表单被定义为一个明确的对象(继承自 forms.Form)。视图负责处理这个对象的逻辑,而模板负责决定如何“显示”这个对象。

Django内置了 as_tableas_p 等方法来完成显示工作。Crispy Forms库则提供了一个替代的“显示器”。由于表单对象的结构是标准的,我们可以轻松地切换不同的渲染方式,而无需重写业务逻辑。这体现了“关注点分离”的原则,也是使用框架和库来提高开发效率的典范。

总结

本节课中我们一起学习了如何使用Crispy Forms库来美化Django表单。我们了解到,只需安装库、更新配置,并在模板中使用 {% crispy form %} 过滤器,就能将普通的表单转换为具有专业Bootstrap样式的精美表单。整个过程无需改动视图和表单模型的核心代码,充分体现了Django框架的灵活性和可扩展性。通过利用像Crispy Forms这样的优秀第三方库,我们可以更专注于应用的功能开发,同时轻松获得良好的用户界面。

008:我的文章(myarts)示例代码实战

在本教程中,我们将通过“我的文章”示例,深入探讨Django中“数据行所有权”的核心概念。我们将学习如何构建一个应用,让用户只能查看和编辑自己创建的文章。

概述

“我的文章”示例的核心是“数据行所有权”。这意味着数据库中的每一行数据(例如一篇文章)都归属于一个特定的用户。我们将学习如何:

  1. 在模型中建立用户与数据行之间的关联。
  2. 在视图和模板中,根据当前登录用户来过滤和显示数据。
  3. 保护数据,确保用户只能操作属于自己的内容。

模型与数据所有权

上一节我们介绍了核心概念,本节中我们来看看如何在实际的模型中实现数据所有权。

在Django中,我们使用外键来建立模型之间的关联。对于“我的文章”应用,我们需要在Article模型中添加一个指向Django内置User模型的外键字段,名为owner

# models.py
from django.db import models
from django.conf import settings

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_31.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_33.png)

class Article(models.Model):
    title = models.CharField(max_length=200)
    text = models.TextField()
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

关键点解析

  • settings.AUTH_USER_MODEL:这是一个字符串,指向项目中使用的用户模型。这比直接导入User模型更灵活。
  • on_delete=models.CASCADE:这表示如果关联的用户被删除,那么属于该用户的所有文章也会被自动删除。
  • owner字段的值是一个整数,对应auth_user表中用户的主键

在数据库层面,myarts_article表中会有一个owner_id列,其值就是auth_user表中用户的ID。这样,我们就建立了文章与用户之间的归属关系。

视图与URL配置

理解了模型结构后,我们来看看如何通过视图来控制数据的访问。

视图的配置非常简洁,因为我们使用了可重用的“所有者视图”。以下是urls.pyviews.py的关键部分:

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.ArticleListView.as_view(), name='all'),
    path('article/<int:pk>', views.ArticleDetailView.as_view(), name='article_detail'),
    path('article/create', views.ArticleCreateView.as_view(success_url=reverse_lazy('myarts:all')), name='article_create'),
    path('article/<int:pk>/update', views.ArticleUpdateView.as_view(), name='article_update'),
    path('article/<int:pk>/delete', views.ArticleDeleteView.as_view(success_url=reverse_lazy('myarts:all')), name='article_delete'),
]
# views.py
from django.views import generic
from .models import Article
from .owner import OwnerListView, OwnerDetailView, OwnerCreateView, OwnerUpdateView, OwnerDeleteView

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_51.png)

class ArticleListView(OwnerListView):
    model = Article

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_53.png)

class ArticleDetailView(OwnerDetailView):
    model = Article

class ArticleCreateView(OwnerCreateView):
    model = Article
    fields = ['title', 'text'] # 注意:这里不包含‘owner’字段

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_55.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_57.png)

class ArticleUpdateView(OwnerUpdateView):
    model = Article
    fields = ['title', 'text'] # 注意:这里不包含‘owner’字段

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_59.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_61.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_63.png)

class ArticleDeleteView(OwnerDeleteView):
    model = Article

视图解析

  • 我们的视图类(如ArticleListView)继承自自定义的OwnerListView等类,而不是Django原生的generic.ListView
  • ArticleCreateViewArticleUpdateView中,fields列表明确排除了owner字段。这是因为我们不想让用户在表单中手动选择文章的所有者,这个值将由后台逻辑自动设置。
  • success_url参数可以在URL配置中指定,这使得视图代码更加简洁。

这种设计的美妙之处在于,所有关于“所有权”的复杂逻辑都被封装在了owner.py的基类视图中,我们的业务视图变得非常清晰和简单。

模板中的权限控制

在用户界面中,我们需要根据所有权来显示或隐藏编辑和删除按钮。这主要在列表模板中实现。

以下是模板中控制按钮显示的逻辑:

<!-- article_list.html -->
{% for article in article_list %}
  <li>
    {{ article.title }}
    {% if article.owner == user %}
      (<a href="{% url 'myarts:article_update' article.id %}">Edit</a> |
      <a href="{% url 'myarts:article_delete' article.id %}">Delete</a>)
    {% endif %}
  </li>
{% endfor %}

模板逻辑解析

  • article.owner:这是当前文章对象owner字段的值,即创建该文章的用户ID。
  • user:这是Django模板上下文自动提供的变量,代表当前登录的用户对象。
  • {% if article.owner == user %}:这个条件判断检查当前文章的所有者是否就是当前登录的用户。如果成立,则显示编辑和删除链接。

重要提示:这仅仅是用户体验优化,而非安全措施。恶意用户仍然可以通过猜测或构造URL(如/article/4/update/)来尝试访问不属于自己的内容。真正的安全保护必须在视图层实现,我们接下来就会看到。

核心:所有者视图(owner.py)

所有权的魔法都发生在自定义的owner.py文件中。这里定义了所有视图基类,它们处理了自动设置所有者和权限验证的核心逻辑。

创建视图(OwnerCreateView)

当用户创建新文章时,我们需要自动将当前登录用户设置为文章的所有者。

# owner.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_83.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_85.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_87.png)

class OwnerCreateView(LoginRequiredMixin, CreateView):
    def form_valid(self, form):
        # 在保存到数据库之前,临时将表单数据存入对象
        object = form.save(commit=False)
        # 将当前登录用户设置为对象的所有者
        object.owner = self.request.user
        # 现在将对象(包含owner信息)保存到数据库
        object.save()
        # 调用父类的form_valid方法完成后续操作(如重定向)
        return super().form_valid(form)

代码解析

  1. LoginRequiredMixin:确保只有登录用户才能访问创建页面。
  2. form_valid(self, form):这是一个在表单验证通过后、数据保存前被调用的方法。我们覆盖它来插入自定义逻辑。
  3. form.save(commit=False):这行代码告诉Django:“根据表单数据创建一个模型对象,但先不要保存到数据库”。这样我们就获得了这个对象的一个临时实例。
  4. object.owner = self.request.user:将当前请求的用户(self.request.user)赋值给对象的owner属性。
  5. object.save():现在,将包含了正确owner信息的对象保存到数据库。
  6. super().form_valid(form):最后,调用父类(CreateView)的form_valid方法,它通常会处理重定向到成功页面等操作。

更新与删除视图(OwnerUpdateView, OwnerDeleteView)

对于更新和删除操作,核心保护机制是重写get_queryset方法,确保用户只能操作属于自己的数据。

# owner.py
from django.views.generic import UpdateView, DeleteView

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_111.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/be22e7f381c283e91cf00876c37f4d57_112.png)

class OwnerUpdateView(LoginRequiredMixin, UpdateView):
    def get_queryset(self):
        # 首先获取默认的查询集(根据URL中的pk查找对象)
        qs = super().get_queryset()
        # 在此基础上,增加一个过滤器:只返回owner是当前用户的对象
        return qs.filter(owner=self.request.user)

class OwnerDeleteView(LoginRequiredMixin, DeleteView):
    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(owner=self.request.user)

代码解析

  1. get_queryset(self):这个方法返回一个查询集(QuerySet),视图用它来查找要操作的对象。
  2. super().get_queryset():调用父类方法,获得基础的查询集(例如,查找id为特定值的文章)。
  3. .filter(owner=self.request.user):这是关键步骤。我们在基础查询上增加一个过滤条件,要求owner字段必须等于当前登录的用户。
  4. 安全效果:如果用户尝试更新或删除一个不属于自己的文章(例如ID为4的文章属于用户2,而当前用户是用户1),那么经过过滤的查询集将找不到任何记录。Django会因此抛出一个Http 404(页面未找到)错误,从而阻止了未授权操作。这个检查在加载数据提交更新时都会执行,提供了双重保护。

总结

本节课中我们一起学习了如何在Django中实现“数据行所有权”。我们通过以下步骤构建了一个安全的、用户专属的内容管理系统:

  1. 模型设计:在Article模型中添加owner外键字段,关联到用户模型。
  2. 视图封装:创建了可重用的OwnerCreateViewOwnerUpdateViewOwnerDeleteView等基类视图,将所有权逻辑(自动设置所有者、权限过滤)封装其中。
  3. 业务视图:我们的业务视图(如ArticleCreateView)只需继承这些所有者视图并配置modelfields,代码非常简洁。
  4. 模板优化:在模板中使用{% if article.owner == user %}来根据所有权显示或隐藏操作按钮,改善用户体验。
  5. 安全核心:认识到模板中的隐藏仅是前端优化,真正的安全依赖于视图层get_queryset方法中的权限过滤,它确保了用户只能访问和修改属于自己的数据。

这种基于面向对象编程的封装模式非常强大且优雅。一旦编写好owner.py,你就可以在项目的任何需要所有权功能的模型中复用这些视图,只需让对应的视图类继承它们即可,极大地提高了代码的复用性和可维护性。

009:Bootstrap导航菜单实战教程 🧭

概述

在本节课中,我们将学习如何在Django应用中集成一个功能完整的Bootstrap导航菜单。这个菜单将包含页面链接、下拉菜单、用户登录状态显示以及Gravatar头像等功能。我们将重点讲解如何通过模板继承复用菜单,以及如何根据当前页面动态高亮菜单项。


菜单结构与模板继承

上一节我们介绍了Bootstrap的基础布局。本节中我们来看看如何构建一个可复用的导航菜单。

我们采用多层模板继承的方式来组织代码:

  1. base_bootstrap.html: 提供基础的Bootstrap页面框架。
  2. base_menu.html: 继承自 base_bootstrap.html,并专门定义导航栏区域(block nav_bar)。
  3. 具体的页面模板(如 main_menu.html): 继承自 base_menu.html,只需关注页面主体内容(block content)。

这种结构类似于面向对象编程中的继承,让我们可以逐层定制和扩展功能。base_menu.html 的关键作用是重写了 base_bootstrap.html 中的 nav_bar 块。

构建导航菜单

以下是构建Bootstrap导航菜单的核心步骤,主要代码位于 base_menu.html 中。

1. 菜单基础框架

我们首先按照Bootstrap官方文档的指导,构建导航栏的基本HTML结构,包括品牌标识、响应式折叠按钮等。

<nav class="navbar navbar-expand-md navbar-dark bg-dark">
    <a class="navbar-brand" href="/">Brand</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarCollapse">
        <!-- 左侧菜单和右侧菜单将放在这里 -->
    </div>
</nav>

2. 动态高亮当前页面

一个常见的需求是让用户知道当前位于哪个页面。我们通过对比当前请求的路径与菜单项链接来实现高亮。

在Django模板中,我们可以使用 {% url %} 标签生成链接,并用 request.path 获取当前路径。

<ul class="navbar-nav mr-auto">
    {% url ‘menu:main‘ as xyz %}
    <li class="nav-item {% if request.path == xyz %}active{% endif %}">
        <a class="nav-link" href="{% url ‘menu:main‘ %}">主页</a>
    </li>
    {% url ‘menu:page1‘ as xyz %}
    <li class="nav-item {% if request.path == xyz %}active{% endif %}">
        <a class="nav-link" href="{% url ‘menu:page1‘ %}">页面一</a>
    </li>
</ul>

代码解释

  • {% url ‘menu:main‘ as xyz %}: 将名为 ‘menu:main‘ 的URL路径计算结果存入变量 xyz
  • {% if request.path == xyz %}active{% endif %}: 如果当前请求路径 request.path 等于菜单项链接,则为该 <li> 标签添加 active 类。Bootstrap 的 active 类会改变菜单项的背景色,实现高亮效果。

3. 处理用户登录状态

导航栏右侧通常显示用户相关的信息。我们需要根据用户是否登录来显示不同的内容。

<ul class="navbar-nav ml-auto">
    {% if user.is_authenticated %}
        <!-- 用户已登录:显示下拉菜单,包含Gravatar和登出链接 -->
        <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" id="userMenu" data-toggle="dropdown">
                <img src="{{ user|gravatar:60 }}" class="rounded-circle" style="width: 30px; height: 30px;">
            </a>
            <div class="dropdown-menu">
                <a class="dropdown-item" href="{% url ‘logout‘ %}?next={{ request.path }}">登出</a>
            </div>
        </li>
    {% else %}
        <!-- 用户未登录:显示登录链接 -->
        <li class="nav-item">
            <a class="nav-link" href="{% url ‘login‘ %}?next={{ request.path }}">登录</a>
        </li>
    {% endif %}
</ul>

代码解释

  • {% if user.is_authenticated %}: 判断当前用户是否已通过认证(登录)。
  • 已登录状态:显示一个下拉菜单,其中包含用户头像(通过Gravatar显示)和“登出”链接。?next={{ request.path }} 参数确保用户登出或登录后能返回当前页面。
  • 未登录状态:显示一个“登录”链接。

集成Gravatar头像

Gravatar是一项全球公认的头像服务。我们可以通过用户的邮箱地址获取其Gravatar头像。

1. 在模板中使用Gravatar

在模板中,我们使用自定义的模板过滤器 gravatar 来生成头像URL。

<img src="{{ user|gravatar:60 }}" class="rounded-circle">

这行代码会将 user 对象(通常是 request.user)的邮箱地址处理后,生成一个边长为60像素的Gravatar头像URL。

2. 创建自定义模板过滤器

我们需要在Django应用中创建一个自定义的模板标签来生成Gravatar URL。

yourapp/templatetags/app_tags.py 文件中:

import hashlib
from django import template

register = template.Library()

@register.filter
def gravatar(user, size=60):
    # 获取用户邮箱,处理并生成MD5哈希
    email = user.email.strip().lower().encode(‘utf-8‘)
    email_hash = hashlib.md5(email).hexdigest()
    # 构建Gravatar URL
    gravatar_url = f"https://www.gravatar.com/avatar/{email_hash}?s={size}&d=identicon"
    return gravatar_url

原理

  1. 获取用户邮箱,去除空格并转为小写。
  2. 使用MD5算法对邮箱进行单向哈希。这能保护用户邮箱隐私,因为无法从哈希值反推出原始邮箱。
  3. 将哈希值插入到Gravatar的标准URL格式中,并指定尺寸参数 s

3. 在模板中加载标签

在使用自定义过滤器的模板顶部,需要先加载它。

{% load app_tags %}

总结

本节课中我们一起学习了如何为Django应用创建一个专业、动态的Bootstrap导航菜单。

我们掌握了以下核心技能:

  • 利用Django的模板继承机制extendsblock)来复用菜单代码,保持项目结构清晰。
  • 通过比较 request.path 与菜单URL,实现当前页面高亮active 类)。
  • 根据 user.is_authenticated 条件判断,在菜单中动态切换登录/登出状态的显示。
  • 创建并使用自定义模板过滤器gravatar),集成Gravatar全球头像服务,并理解了其通过MD5哈希保护用户隐私的原理。

这个菜单组件是Web应用的通用基础模块。你可以直接使用课程提供的示例代码,并根据需要修改菜单项链接和品牌信息,快速应用到自己的项目中。

010:部署Django应用

概述

在本节课中,我们将学习如何部署一个Django应用到生产环境。部署是将开发完成的应用程序发布到服务器,使其能够被互联网上的用户访问的过程。我们将涵盖从准备应用到配置服务器的关键步骤。


准备部署环境

上一节我们介绍了Django应用开发的基础。本节中我们来看看如何为部署做准备。部署前,需要确保你的应用代码是稳定且经过测试的。

以下是部署前需要完成的准备工作列表:

  • 冻结依赖项:使用 pip freeze > requirements.txt 命令生成项目依赖列表。
  • 设置环境变量:将敏感信息(如SECRET_KEY、数据库密码)从代码中移出,改为从环境变量读取。
  • 配置静态文件:确保 STATIC_ROOTSTATIC_URL 设置正确,以便收集静态文件。
  • 选择数据库:将开发用的SQLite数据库迁移到更适用于生产的数据库,如PostgreSQL或MySQL。
  • 关闭调试模式:在部署环境中,务必将 DEBUG 设置为 False

完成这些步骤后,你的应用就基本具备了部署的条件。


选择部署平台

准备好了应用,接下来我们需要选择一个地方来托管它。不同的平台有不同的优势和配置方式。

以下是几种常见的Django部署平台选项:

  • PaaS(平台即服务):例如Heroku、PythonAnywhere。它们管理服务器基础设施,简化了部署流程。
  • VPS(虚拟专用服务器):例如DigitalOcean、Linode、AWS EC2。提供完整的服务器控制权,需要自行配置所有软件。
  • 容器化部署:使用Docker将应用及其环境打包成容器,然后部署到Kubernetes或AWS ECS等服务上。

对于初学者,从PaaS开始通常是最简单快捷的方式。


部署流程示例(以Heroku为例)

我们以Heroku这个流行的PaaS为例,来看一个简化的部署流程。这个过程涉及将代码推送到云端并配置运行环境。

以下是使用Heroku部署的核心步骤:

  1. 创建Heroku账户并安装CLI工具
  2. 在项目根目录创建 Procfile 文件,定义启动命令:web: gunicorn your_project_name.wsgi
  3. 使用Git初始化仓库,并将代码提交。
  4. 通过CLI登录Heroku并创建应用heroku create your-app-name
  5. 将代码推送到Herokugit push heroku main
  6. 运行数据库迁移:heroku run python manage.py migrate
  7. 收集静态文件:heroku run python manage.py collectstatic
  8. 打开应用heroku open

通过以上步骤,你的Django应用就应该成功上线了。


总结

本节课中我们一起学习了部署Django应用的核心知识。我们从部署前的准备工作开始,讨论了如何整理依赖和配置。接着,我们比较了不同的部署平台,并最终以Heroku为例,一步步完成了一个实际的部署流程。记住,部署是一个需要耐心和细致操作的过程,多实践几次就会变得更加熟练。

011:部署Django应用

概述

在本节课中,我们将跟随课程视频片段,了解一次在澳大利亚珀斯举行的面对面办公时间活动。我们将看到来自世界各地的学习者如何利用在线课程充实自己,并感受学习社群的氛围。

珀斯面对面办公时间

大家好,我是查克·塞弗伦斯。我们现在正在进行又一次面对面办公时间活动。这次我们在澳大利亚西端的珀斯,这是世界上最偏远的首都城市。

我想向你们介绍一些来自珀斯的同学。如果你愿意,可以告诉我们你的名字并打个招呼。

以下是参与本次办公时间的几位学习者:

  • 罗纳德:你好,我的名字是罗纳德。
  • 马文:这是马文。
  • 戴安娜:嗨,我是戴安娜,来自新西兰。
  • 埃洛伊丝:嗨,我的名字是埃洛伊丝。
  • 特里:嗨,我是特里,我很喜欢查克的课程。

学习者的背景与动机

上一节我们认识了参与活动的学习者,本节中我们来听听他们分享的学习经历和参与课程的原因。

罗纳德提到他正在完成第七门Coursera课程,几乎像在用这些课程攻读一个硕士学位。他拥有学士学位,正在构建自己的硕士课程体系。

马文分享说,他在90年代学习过计算机科学,曾是一名Solaris和Linux系统管理员。他现在学习这些课程纯粹是为了乐趣和怀旧,因为他从事计算机相关行业。

埃洛伊丝表示,她利用Coursera来填补自己教育中的一些空白,并且在这个过程中获得了许多乐趣。

特里则简单地表达了对课程的喜爱,认为这些课程与众不同。

总结

本节课中我们一起了解了密歇根大学《Django for Everybody》课程一次在澳大利亚珀斯举行的线下办公时间。我们看到了来自不同背景的学习者如何通过在线课程平台持续学习、充实自我,并享受学习带来的乐趣。这体现了在线教育的广泛影响力和社群学习的价值。

012:JavaScript概述与发展历史 🚀

在本节课中,我们将要学习JavaScript的基础知识及其发展历史。JavaScript是一种广泛使用的编程语言,尤其在Web开发中扮演着核心角色。我们将探讨它的起源、特点以及与其他编程语言的关系,帮助初学者理解JavaScript的基本概念和应用场景。


JavaScript的起源与定位 🌟

JavaScript是另一种编程语言。如果你只了解Python,你会发现JavaScript与Python有很大不同。

JavaScript最初是作为HTML和CSS的补充而设计的。它始于一种在浏览器内部运行的编程语言,直接在浏览器中执行。尽管它的名字类似Java,但其工作原理更接近Python。不过,JavaScript使用类似C语言的语法,是一种非常出色的语言,对许多人来说,它是首选和最喜爱的语言。因此,即使你热爱Python,也值得学习JavaScript。


JavaScript的发展历史 📜

JavaScript是一门有趣的语言,它由Brendan Eich在1995年用10天时间快速构建而成。Brendan拥有物理学博士学位,精通编程语言、形式语言以及如何快速构建编译器。

最初的想法是创建一种类似Java的轻量级版本,并将其嵌入浏览器中。如今,JavaScript已经通过标准化过程,被称为ECMAScript。


编程语言的历史背景 🕰️

为了理解JavaScript的定位,我们需要回顾一下编程语言的发展历史。你可能已经熟悉Python,而JavaScript是我们将要学习的另一门语言。以下内容将帮助你理解它们之间的影响和关系。

在20世纪50年代初期,我们使用汇编语言进行编程。当时大多数计算机只能很好地处理数值计算,因此出现了像Fortran这样的语言。回顾50年代和60年代,人们不断发明各种语言。但对我而言,真正改变一切的语言是C语言。

C语言于1972年发明,它非常类似于计算机的实际工作方式,这与我们在许多方面编程计算机的方式有所不同。C语言的另一个重要特点是,它认识到计算机越来越不局限于数值计算,而更多地涉及字符串、文字和消息处理。如今,大约95%的计算涉及数据移动而非数值运算,C语言在这方面表现出色。

在我开始使用C语言之前,使用的所有语言都是一种数值系统,勉强处理字符。而C语言是一个处理字符的系统,因此它成为了所有语言的母语言。Linux是用C语言编写的,Python是用C语言编写的,PHP是用C语言编写的,JavaScript很可能也是用C语言编写的。因此,C语言是我们目前使用的最低层次语言。当你编写一门语言并希望它快速运行时,该语言的实现通常用C语言编写。

C++是从C语言衍生出来的面向对象变体,我们在Python中讨论了很多关于面向对象的内容,接下来我们将在JavaScript中讨论面向对象。因此,C++影响了许多系统,也在一定程度上影响了Python。C++增加了面向对象层,但它可能是这些语言中最不面向对象的语言,不过它包含了面向对象的概念。

C#是微软的版本。Java是开源的,它成为了JavaScript的始祖。但再次强调,它们之间的关系可能不如你想象的那么密切。JavaScript实际上更多地源自C语言,而非Java。它只是模仿了Java的一些特性。

所有位于这条线上方的语言都使用分号和花括号来结束行和开始/结束代码块。C语言是第一个使用分号和花括号的语言。

而另一组编程语言不使用分号和花括号。C语言是第一个使用分号和花括号的语言。位于这条线下方的语言,如Bash、Perl、Python和PHP,实际上PHP也使用分号和花括号。Python是我们一直在学习的语言,它确实是一门独特的语言。因此,我们将学习一门属于这一类的语言,这将使你更容易学习C、C++、Java或Objective-C。所以,即使你已经了解Python,也值得学习JavaScript。

还有另一种趋势,C语言是一种高性能语言,主要面向计算机科学家构建复杂系统,如Linux。但也有一些面向最终用户的语言,它们旨在完成工作,但不像超级性能的专业语言那样。例如,Bash是Linux shell,你可以在其中输入Cd和Ls命令(如果你熟悉的话)。Perl是一种报告生成语言,引入了关联数组的概念。许多人开玩笑说他们不喜欢Perl,因为他们写完后无法阅读自己的代码。但它引入了许多优秀的概念,如关联数组,在Python中我们称之为字典。

Perl确实影响了Python,因为它易于编程。Perl真正擅长的是让几乎没有数据结构正式培训的人能够使用关联数组和嵌套关联数组来创建相当复杂的数据结构。Python和PHP也属于这一类。它们是一种解释型、易于使用的语言,字符串处理非常出色,使生活变得更加轻松。而在像C这样的语言中,字符串处理非常困难。

因此,我们已经学习了Python,接下来将学习JavaScript。但再次强调,你可以将JavaScript视为开启许多其他语言的钥匙。因此,即使在本课程中我们不会深入探讨,也值得学习一些基础知识,因为大多数情况下,你将通过复制、粘贴、调整、阅读和调试JavaScript代码来使用它。我们不会重点编写太多新的JavaScript代码。


JavaScript的应用场景 🛠️

在几种情况下,你会发现自己需要编写一些JavaScript代码。

首先是增强HTML,这将是我们首先要做的事情。浏览器中有一个运行JavaScript的机制,它通过文档对象模型与浏览器的用户界面交互。

你可以添加像jQuery这样的库,使这种交互更加容易,因为文档对象模型在不同浏览器之间的可移植性有时不如我们所愿。

如果你继续深入,你会发现越来越多的应用程序逻辑在浏览器中构建。因此,你可以使用模板语言在浏览器中使用JavaScript编写模型和视图部分(或者控制器和视图部分),例如使用Vue或React来实现这一点。

如果你继续深入,你可以将JavaScript仅仅视为一种语言,并在服务器端使用它作为基于服务器的语言。虽然我们使用Django和Python作为基于服务器的语言,但你实际上可以使用JavaScript从前到后编写整个应用程序,并使用像Node这样的工具来运行与数据库交互的服务器端。

JavaScript就像打开了一扇门,让你进入许多真正令人惊叹的功能世界。


JavaScript的语法特点 📝

如前所述,JavaScript属于Java的语法家族,这意味着它使用分号来结束行。空格不重要,使用开括号和闭括号来开始和结束代码块。

而在Python中,你只需结束一行,并通过缩进来表示一个代码块,通过取消缩进来表示该代码块的结束。

我必须承认,Python不是我最喜欢的语法。它是我最喜欢的语言,因为我喜欢它的库、它的思维方式、它的面向对象模式以及许多其他方面。但我确实喜欢花括号,因为对我来说,我喜欢看到开始和结束的代码块。而在Python中,你只能看到开始,但这没关系。我只是想说,现在你将看到一些这种语法,如果你只了解Python,它会看起来非常不同。如果你了解Java,你会觉得这很熟悉。


总结 📚

本节课中我们一起学习了JavaScript的概述与发展历史。我们探讨了JavaScript的起源、它在编程语言历史中的定位、其语法特点以及应用场景。JavaScript作为一门在浏览器中运行的语言,为Web开发提供了强大的动态功能。即使你已经熟悉Python,学习JavaScript也能帮助你更好地理解其他类C语言,并为你的开发技能增添重要的一环。在接下来的课程中,我们将进一步探讨如何在浏览器中使用JavaScript。

013:浏览器中的JavaScript

概述

在本节中,我们将学习如何在浏览器中使用JavaScript。这是JavaScript最原始和核心的用途之一,即增强HTML页面的交互性。我们将探讨JavaScript与HTML结合的基本方式、调试技巧以及开发工具的使用。

在HTML中嵌入JavaScript

上一节我们介绍了JavaScript的基本概念,本节中我们来看看如何将其与HTML结合使用。JavaScript最初的设计目的就是在浏览器中运行,以增强HTML页面的功能。

以下是一个简单的HTML示例,其中包含了JavaScript代码:

<!DOCTYPE html>
<html>
<body>
    <p>第一段来自HTML。</p>
    <script>
        document.write("第二段来自JavaScript。");
    </script>
    <noscript>您的浏览器不支持JavaScript。</noscript>
    <p>第三段来自HTML。</p>
</body>
</html>

在这段代码中,<script>标签标志着我们离开了HTML语法,开始编写JavaScript代码。document对象代表了浏览器中你看到的页面部分(即文档对象模型DOM)。通过document.write()方法,JavaScript可以直接向屏幕输出内容。

当浏览器解析此页面时,会依次输出:第一段HTML文本、JavaScript写入的文本、以及最后的第三段HTML文本。这演示了JavaScript在网页加载过程中能够访问并修改屏幕内容的能力,这种能力非常强大。

使用alert()进行调试

学习新编程语言时,第一件事通常是打印调试信息。在浏览器JavaScript中,我们可以使用alert()函数。

alert("Hello World");

alert()函数会弹出一个对话框显示传入的字符串,并暂停JavaScript的执行。用户必须点击“确定”按钮后,程序才会继续运行。这对于快速检查某段代码是否被执行非常有用。

请看以下示例:

<p>页面加载中...</p>
<script>
    alert("我在这里!");
    document.write("Hello World");
</script>
<p>页面加载完成。</p>

当浏览器解析到<script>标签时,会切换到JavaScript执行模式。alert()弹窗会阻塞浏览器,整个页面渲染过程会暂停,直到用户点击确认。之后,JavaScript会继续执行document.write(“Hello World”),并最终渲染最后一个段落。

需要注意的是,alert()会完全中断用户体验,因此不宜过度使用,尤其在循环中。

引入JavaScript的三种方式

有三种主要方式可以将JavaScript代码引入网页。

1. 内联脚本

即直接在HTML文件中使用<script>标签包裹代码,如上文所示。

2. 事件处理程序

我们可以将JavaScript代码作为HTML标签的事件属性值。这是一种事件驱动编程。

<a href="http://www.dr-chuck.com" onclick="alert('已点击'); return false;">点击我</a>

在这个例子中,onclick属性内的alert(‘已点击’); return false;就是JavaScript代码。当用户点击这个链接时,会触发onclick事件,执行这段代码。return false;的作用是告诉浏览器不要跟随链接的href跳转,即由JavaScript完全处理这次点击事件。

3. 外部脚本文件

最常用的方式是将JavaScript代码保存在独立的.js文件中,然后在HTML中引用。

假设我们有一个script.js文件:

// script.js
document.write("Hello World");

在HTML中这样引入它:

<script src="script.js"></script>

浏览器会加载并解析script.js文件中的代码并执行。通常,外部脚本文件用于定义一系列函数,构成一个函数库,供页面中的其他脚本后续调用。

处理JavaScript语法错误

在编程中难免会出现语法错误。但在网页环境中,如果JavaScript代码存在语法错误,浏览器默认不会向普通用户显示错误信息,而是会停止执行出错点之后的所有JavaScript代码。

请看以下示例:

<script>
    alert("第一个警告"); // 假设此行有语法错误
    alert("你不会看到这个警告");
</script>
<p>第一段文本</p>
<script>
    alert("第二个警告");
</script>
<p>第二段文本</p>

如果第一个<script>块的第一行存在语法错误,那么该块内其后的所有代码都会被忽略。浏览器会继续解析HTML,渲染段落,直到遇到下一个<script>标签时,才会重新开始解析和执行JavaScript。因此,用户会看到“第一段文本”,但看不到第一个警告和“你不会看到这个警告”这个警告,然后会看到“第二个警告”和“第二段文本”。

这意味着,一个脚本文件中的语法错误可能会使该文件中定义的大量函数失效。因此,在开发过程中,及时发现错误至关重要。

使用开发者控制台

由于错误对用户不可见,开发者必须借助浏览器提供的“开发者工具”来调试。

所有现代浏览器(Chrome, Firefox, Safari等)都内置了开发者工具。你可以通过右键点击页面选择“检查”或按F12键打开它。在开发者工具中,“控制台”(Console)标签页专门用于显示JavaScript的错误信息和调试输出。

在开发JavaScript时,务必保持控制台处于打开状态。否则,代码修改后看似没有效果,可能仅仅是因为一个上游的语法错误导致后续代码被忽略,而你却无从知晓。

更好的调试方法:console.log()

虽然alert()可以用于调试,但它会中断程序。更优的选择是使用console.log()函数。

console.log("这是一个调试信息");

console.log()会将信息输出到浏览器的开发者控制台,而不会暂停脚本执行。你可以在循环中或任何地方使用它,不会干扰用户体验。

例如:

<button onclick="console.log('按钮被点击'); alert('弹出框!'); console.log('弹出框已确认');">点击测试</button>

点击按钮后,你可以在控制台看到两条日志,同时页面上会弹出警告框。这是一种更高效、更专业的调试方式。在复杂的系统中,有时甚至会保留一些console.log()语句,以便在生产环境中为其他开发者提供线索。

使用调试器设置断点

浏览器开发者工具还提供了功能完整的JavaScript调试器。在“源代码”(Sources)标签页中,你可以查看页面加载的所有JavaScript文件。

你可以点击行号来设置断点。设置断点后,需要刷新页面。当JavaScript执行到该断点时,程序会暂停,此时你可以检查变量的值、单步执行代码,从而深入理解程序的运行状态。

需要注意的是,你不能在代码执行过后再“回溯”设置断点。必须提前设置好断点,然后刷新页面,让浏览器在重新执行时捕获它。

总结

本节课中我们一起学习了在浏览器中使用JavaScript的核心知识。我们了解了三种引入JavaScript代码的方式:内联脚本、事件处理属性和外部文件。我们掌握了使用alert()进行快速调试,以及更优的console.log()方法和开发者控制台的使用。我们还学习了如何处理语法错误以及如何利用浏览器内置的调试器设置断点。这些技能是进行前端Web开发的基础,无论后续学习jQuery、Vue等库或框架,还是Node.js服务器端开发,理解这些原生JavaScript在浏览器中的行为都至关重要。

014:JavaScript语言特性 🧩

在本节课中,我们将学习JavaScript作为一种编程语言的基础语法。我们将从注释开始,逐步介绍变量、数据类型、运算符等核心概念,帮助你理解JavaScript的基本构成。

注释

JavaScript支持两种风格的注释,它们分别受到不同编程语言的影响。

以下是两种注释的写法:

  • /* 这是一个多行注释,可以跨越多行。 */:这种多行注释风格来源于C语言。
  • // 这是一个单行注释,到行尾结束。:这种单行注释风格来源于C++语言。

我倾向于使用多行注释来编写文档区块,而使用单行注释进行简短说明。

语句与空格

在JavaScript中,空格和换行符通常不影响代码执行。虽然技术上并非所有语句都必须以分号结尾,但养成在每个语句末尾添加分号的习惯是有益的。

例如,以下代码虽然格式混乱,但依然可以运行:

x = 3 + 5
    * 4;
console.log(x);

对于JavaScript这类语言,解释器主要识别字符和分号来界定语句的结束,换行符被视为另一种空白字符。

变量与常量

现在,我们来看看如何命名变量以及定义字符串和数字常量。

变量命名

变量名可以包含字母、数字、下划线和美元符号,但不能以数字开头。变量名是大小写敏感的。

关于命名风格:

  • 我们有时使用大小写来传递信息,例如驼峰式命名或全大写命名。
  • 虽然JavaScript允许变量名以美元符号开头(受PHP和Perl影响),但为了代码风格更接近Java,通常不建议这样做。

字符串常量

单引号(')和双引号(")在定义字符串时功能完全相同。我倾向于在JavaScript代码中尽可能使用单引号,因为双引号是HTML的属性值界定符。这样在混合编写JavaScript和HTML代码时,可以更容易地区分两者。

例如,在编写包含HTML的JavaScript字符串时:

let htmlSnippet = '<p class="highlight">Text</p>';

这里,外层使用单引号表示JavaScript字符串,内层的双引号则是HTML属性所需。

数字常量

JavaScript只有一种数字类型:Number。所有数字在内部都是浮点数,没有独立的整数类型。

例如:

let result = 5 / 3; // 结果是浮点数 1.666...
let intPart = Math.trunc(result); // 使用 Math.trunc 截取整数部分,得到 1

即使intPart显示为1,它本质上仍然是一个浮点数。

运算符

上一节我们介绍了变量和常量,本节中我们来看看JavaScript中用于操作这些值的各种运算符。

算术运算符

基本的算术运算符与其他语言类似。

以下是常见的算术运算符:

  • +:加法
  • -:减法
  • *:乘法
  • /:除法
  • %:取模(求余数)

取模运算符(%)非常实用,例如,可以用它来在循环中每隔一定次数执行某个操作:

if (iterationCount % 100 === 0) {
    // 每循环100次执行一次
}

副作用运算符

这些运算符会在计算值的同时改变变量本身。

以下是常见的副作用运算符:

  • k++:先使用k的值,然后将k加1。
  • ++k:先将k加1,然后使用k的新值。
  • j += 5:等同于 j = j + 5。类似的还有 -=*=/=

虽然这些运算符源自C语言,在JavaScript和Java中也很常见,但为了代码清晰,我通常更倾向于使用完整的赋值语句(如 j = j + 5)。

比较与逻辑运算符

比较运算符用于比较值,并返回布尔值(truefalse)。

以下是常见的比较运算符:

  • =:赋值(例如 j = 10)。
  • ==:相等比较(允许类型转换)。
  • ===:严格相等比较(要求值和类型都相同)。
  • !=:不相等比较。
  • !==:严格不相等比较。
  • <, >, <=, >=:小于、大于、小于等于、大于等于。

严格相等(===)和严格不相等(!==)非常重要,它们能避免因JavaScript自动类型转换而导致的意外结果。

逻辑运算符用于组合多个布尔条件。

以下是常见的逻辑运算符:

  • &&:逻辑与(AND),两边都为true时结果为true
  • ||:逻辑或(OR),至少一边为true时结果为true
  • !:逻辑非(NOT),对布尔值取反。

例如:

if (k > 1 && j < 10) { /* 两者都成立时执行 */ }
if (!(k > 10)) { /* k 不大于 10 时执行 */ }

类型转换与特殊值

我们已经了解了运算符,现在来看看JavaScript在处理不同类型数据交互时的一些特性,特别是字符串拼接和特殊的数字值。

字符串拼接

JavaScript使用加号(+)进行字符串拼接,并且会自动将非字符串值转换为字符串。

例如:

let x = 12;
let message = "Hello " + x + " people"; // 结果为 "Hello 12 people"

这里,数字12被自动转换成了字符串。这种便利性有时可能导致意料之外的结果,尤其是在与加法运算混淆时。

特殊数字值:NaN 和 Infinity

当数学运算无法返回一个有效的数字时,会产生一些特殊值。

以下是两个主要的特殊数字值:

  • NaN:表示“非数字”。例如,将字符串“hello”乘以1会得到NaNNaN具有粘性,任何涉及NaN的后续运算通常仍会得到NaN。可以使用isNaN()函数来检测一个值是否为NaN
  • Infinity:表示无穷大。例如,1 / 0的结果是Infinity。可以使用isFinite()函数来检测一个值是否为有限数(即不是InfinityNaN)。

JavaScript遵循IEEE 754浮点数算术标准,对这些特殊值的处理方式在科学计算上是严谨和一致的。

类型检测

在动态类型语言中,有时需要检查变量的类型。typeof运算符可以返回一个表示变量类型的字符串。

例如:

typeof 42;        // 返回 "number"
typeof 'Hello';   // 返回 "string"
typeof undefinedVariable; // 如果变量未定义,返回 "undefined"

这在处理来自外部或用户输入的参数时非常有用,可以确保代码按预期处理不同类型的数据。

本节课中我们一起学习了JavaScript的基础语法,包括注释、变量、常量、各种运算符(算术、比较、逻辑)、类型自动转换以及NaNInfinity等特殊值的概念。理解这些基础是编写任何JavaScript代码的前提。下一节,我们将探讨函数和数组。

015:JavaScript函数与数组

在本节课中,我们将要学习JavaScript中的两个核心概念:函数与数组。理解这些概念对于编写动态的网页交互逻辑至关重要。

函数

上一节我们介绍了JavaScript的基础语法,本节中我们来看看如何定义和使用函数。JavaScript函数使用 function 关键字定义,这与Python使用 def 关键字不同。

函数的基本结构如下:

function functionName(parameters) {
    // 函数体
    return value; // 返回值
}

当代码执行到一个表达式并遇到函数调用时,它会运行该函数,并用 return 语句返回的值替换该调用位置。例如,add(4, 5) 的返回值 9 会用于后续计算。

变量作用域

函数内部定义的变量通常是该函数的局部变量。然而,JavaScript有一个特殊之处:如果不明确声明,函数内赋值的变量可能会成为全局变量。

请看以下示例:

var GL = 123; // 这是一个全局变量

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/cb3cf5088c1e40e258396cd03f3f8e34_11.png)

function check() {
    GL = 456; // 如果没有使用 `var`,这里会修改全局的 GL
}
check();
console.log(GL); // 输出:456

在这个例子中,函数 check 内部没有使用 var 声明 GL,因此它修改了外部的全局变量。

为了避免这种混淆,并让函数的行为更符合常规编程语言的预期,我们应始终使用 var 关键字在函数内部声明局部变量:

var GL = 123;

function check() {
    var GL = 456; // 使用 `var` 声明,这是一个局部变量
}
check();
console.log(GL); // 输出:123,全局变量未被修改

使用 var 可以确保函数变量被限制在自己的作用域内,除非你明确需要操作全局变量。养成在函数内使用 var 的习惯非常重要。

数组与对象

理解了函数的作用域后,我们接下来探讨JavaScript中用于组织数据的两种主要结构:数组和对象。

JavaScript的数组与Python的列表非常相似。以下是一个包含三个字符串的数组:

var a = [‘x‘, ‘y‘, ‘z‘];

你可以通过索引访问数组元素,例如 a[0] 会返回 ‘x‘

JavaScript的关联结构不是“字典”,而是“对象”。对象可以包含多种内容,我们之后会有专门课程讲解。目前,你可以将其视为键值对的集合:

var b = {‘name‘: ‘Chuck‘, ‘class‘: ‘DJ3‘};

访问对象属性有两种等效的语法:

  1. 使用方括号:b[‘name‘]
  2. 使用点号:b.name

这两种语法都表示查找对象 b 中名为 ‘name‘ 的属性。对于初学者来说,理解它们是同一回事很重要。

数组操作

以下是创建和填充数组的几种方式:

你可以先创建一个空数组,然后使用 push 方法添加元素(类似于Python列表的 append):

var arr = [];
arr.push(‘zero‘);
arr.push(‘one‘);

你也可以在创建时直接初始化数组:

var arr = [‘zero‘, ‘one‘];

或者使用 Array 构造函数:

var arr = new Array(‘zero‘, ‘one‘);

最简洁和常用的方式还是使用方括号 [] 字面量语法,这与Python的列表语法一致。

总结一下,JavaScript用数组存储有序列表,用对象存储键值对关联数据,而Python则分别使用列表字典

总结

本节课中我们一起学习了JavaScript的函数与数组。

  • 我们学习了如何使用 function 关键字定义函数,以及 return 语句的作用。
  • 我们深入探讨了变量作用域,强调了在函数内部使用 var 关键字声明局部变量的重要性,以避免意外修改全局变量。
  • 我们介绍了数组的创建和访问方式,以及对象作为关联结构的两种属性访问语法。
  • 我们比较了JavaScript(数组与对象)和Python(列表与字典)在数据结构上的异同。

掌握这些基础概念是编写有效JavaScript代码的关键。下一节,我们将探讨JavaScript中的控制结构。

016:JavaScript控制结构

在本节课中,我们将要学习JavaScript中的控制结构,包括条件判断和循环。理解这些概念是编写任何程序逻辑的基础。

条件判断:if语句

上一节我们介绍了变量和运算符,本节中我们来看看如何使用它们进行条件判断。每种编程语言都有if语句,JavaScript也不例外。

我们使用花括号 {} 来标记受if语句影响的代码块的开始和结束。

以下是if语句的基本结构:

if (answer === 42) {
    console.log("Hello world");
} else {
    console.log("Goodbye");
}

在这段代码中,如果变量answer的值严格等于42,则执行第一个代码块,打印“Hello world”;否则,执行else块中的代码,打印“Goodbye”。代码块内可以包含多行语句,这里为简洁只展示了一行。

多路分支:else if

你可以通过else if将多个条件串联起来,就像在Python中一样。程序会按顺序检查这些条件。

以下是多路if语句的示例:

if (score >= 90) {
    grade = 'A';
} else if (score >= 80) {
    grade = 'B';
} else if (score >= 70) {
    grade = 'C';
} else {
    grade = 'F';
}

程序会从上到下依次检查条件。一旦某个条件为真,就会执行对应的代码块,然后直接跳到整个if语句的末尾,不会同时检查所有条件。最后的else子句是可选的,如果所有ifelse if条件都不满足,则会执行else块中的代码。

不定循环:while循环

接下来,我们看看循环结构。while循环是一种不定循环,只要条件为真就会一直执行。它是一个“零次或多次”循环,意味着循环体可能一次都不执行。

以下是while循环的示例:

let fuel = 10;
while (fuel > 1) {
    console.log("vroom");
    fuel = fuel - 1;
}

在这个例子中,只要fuel大于1,就会不断打印“vroom”并将fuel减1,直到fuel不大于1时循环结束。如果忘记在循环体内更新条件变量(例如fuel),就可能导致无限循环。因此,构造while循环时需要确保循环最终能够终止。

计数循环:for循环

for循环是一种计数循环,结构上包含三个部分,用分号分隔。

以下是for循环的结构:

for (let count = 1; count <= 6; count++) {
    console.log(count);
}
  • 第一部分 (let count = 1):在循环开始前执行,用于初始化迭代变量。
  • 第二部分 (count <= 6):这是循环条件,在每次迭代前检查。只要条件为真,就执行循环体。
  • 第三部分 (count++):在每次循环体执行完毕后执行,通常用于更新迭代变量。

这个循环会依次打印数字1到6。当count变为7时,条件count <= 6为假,循环结束。在Python中,我们常用range()函数来实现类似功能。

循环控制:break与continue

在循环中,有时我们需要更精细地控制流程。JavaScript提供了breakcontinue语句。

break语句用于立即跳出当前所在的循环(如果是嵌套循环,则跳出最内层循环),继续执行循环之后的代码。

continue语句用于跳过当前循环迭代中剩余的语句,直接开始下一次迭代。在for循环中,执行continue后,仍会执行第三部分的增量表达式,然后再进行条件判断。

它们的用法与在Python中基本相同。

遍历对象与数组

最后,我们来看看如何遍历数据结构。对于对象(类似于Python的字典或关联数组),可以使用for...in循环来遍历其属性键。

以下是遍历对象的示例:

let balls = {golf: "Titleist", tennis: "Wilson", ping: "Ping"};
for (let ball in balls) {
    console.log(ball + ": " + balls[ball]);
}

在这个循环中,迭代变量ball会依次代表对象的每个键(如“golf”、“tennis”、“ping”),然后我们可以通过balls[ball]来获取对应的值。

而对于数组(线性列表),我们通常使用前面介绍的计数for循环来遍历,而不是for...in循环。

本节课中我们一起学习了JavaScript的核心控制结构:使用ifelse ifelse进行条件分支;使用whilefor进行循环迭代;以及使用breakcontinue控制循环流程。我们还了解了如何遍历对象和数组。掌握这些知识能帮助你阅读和理解大部分JavaScript代码的逻辑。

017:Brendan Eich讲述JavaScript的诞生故事

概述

在本节课中,我们将跟随JavaScript的创造者Brendan Eich的讲述,了解这门语言在1995年诞生时的背景、设计理念、面临的挑战以及它如何演变成今天Web开发的核心技术。我们将重点关注其设计中的关键决策和核心特性。

正文

我于1995年4月加入网景公司(Netscape)。那时,网景已经发布了其“Mosaic杀手”浏览器。如果你还记得,NCSA Mosaic曾是主导的浏览器,直到网景取而代之。当我加入时,公司已经运营了大约一年。我本有机会在最初就加入,但我错过了。不过,我加入的时间正好让我可以做我一直想做的事:为网页设计师和程序员创建一种能直接嵌入网页的编程语言。

当时有一种正在兴起的语言叫Java,它更像一种专业语言,需要类型声明,并且需要编译才能运行。而我想创造的是JavaScript,一种连编译器是什么都不知道的人也能使用的语言。它就像BASIC语言一样,可以直接加载运行。我们当时的定位很明确:这是两种不同的语言。就像微软有Visual Basic来搭配C++一样,JavaScript是用来搭配Java的。Sun公司的Bill Joy实际上很喜欢这个想法并同意了。正是他签署了商标许可协议,使我创造的东西被命名为“JavaScript”。

这个名字本身是一个“谎言”,它和Java的关系并不大,更多是语法上共同继承了C语言。我们的目标是让它易于使用,用户可以复制粘贴代码,从小脚本开始,逐渐发展成完整的程序。JavaScript取得了巨大的成功,但我的工作也相当仓促,因此其中存在一些错误。

我认为很重要的一点是,我当时就知道语言中会有错误和不足。因此,我把它设计得非常具有可塑性。这使得开发者能够将它塑造成他们想要的样子,不仅能投射自己的API风格,甚至能投射自己的语言模式。用Eric von Hippel的话说,他们可以在其上创建自己的“创新工具箱”。所以,它不是一种试图将你限制在单一范式的语言,它是一种多范式语言。很多语言一出现就可能会跌倒,需要第二次机会才能站稳。但JavaScript并没有真正“跌倒”,可以说它从来没有一个“版本2”。我曾尝试推动一个可能成为大版本2的“第四版”,但失败了。JavaScript的演进是持续的,网络世界本身就是关于演进的。

90年代的网页如今并不能全部正确渲染或工作,很多已经丢失,只能通过网络档案馆找到。JavaScript在最初就拥有足够的“优良部分”(借用Douglas Crockford的说法),拥有来自其他语言的足够“遗传物质”:来自Self语言的原型继承,以及来自Scheme语言的一等函数

Scheme的影响更多是精神上的而非实际的,因为我当时有严格的指令要让JavaScript看起来像Java,而且我只有10天时间做出原型。但一等函数非常强大,它们很适合事件驱动的编程模型。我受到了Atkinson的HyperCard的启发,所以你在JavaScript中会看到onclick这样的东西,HyperCard就有这种以“on”开头的事件处理程序模式(如on page down)。

因此,JavaScript在最初就拥有足够好的特质得以生存。但回想90年代中期,JavaScript曾被诟病,因为它主要被用于制造一些恼人的效果,比如状态栏的滚动消息、闪烁的图片或弹出大量窗口。我们本可以加入控制这些功能的设置,后来浏览器(如Firefox)带头自动抑制这些恼人行为,让情况好了很多。

随着摩尔定律的持续作用,以及JavaScript在标准制定过程中获得的一些渐进式改进,它在2004-2005年变得足够快和足够好,从而催生了Web 2.0革命。我认为这与Firefox从IE手中夺回市场份额,以及开发者意识到客户端编程栈可以富有表现力、强大且足够快速是分不开的,这主要得益于更快的计算机。

我本人接受过相关训练,有过很多实践经验,这让我能够从Scheme等语言中汲取灵感。当我最初进入计算机科学领域时,我是一个语言爱好者。我原本是数学和物理专业,最终本科毕业时是数学和计算机科学专业。我编程并研究形式语言理论,应用于词法分析、语法解析器等领域。我喜欢这些东西,因为它们在理论上非常优美和清晰。

这使我能够快速构建出一个语言解释器。我可以编写解析器和扫描器,我可以生成字节码,因为网景希望做服务器端嵌入,而Java即使可以通过解释解析树来运行,我还是为它做了一种内部的字节码,不是后来成为Java负担的Java字节码。我能快速完成所有这些,因为我以前做过。

当时速度是个问题,部分原因是我们都感觉微软会来追赶网景,因为他们曾在94年底试图收购网景。同时,我们在与Java的关系上也处于一种奇怪的博弈论状态。即使在网景内部,也有人认为如果我们有了Java,是否真的还需要第二种语言。他们没有看到为更多程序员、业余爱好者、设计师和初学者提供一个像Visual Basic那样的伴侣语言的好处。

今天,为微软平台编写Java或C++需要大量培训,成本更高。而JavaScript,就像当年的BASIC和Visual Basic一样,让人们能够粘合组件、设计页面、填补空白。微软的Windows更便宜、更普及,这也使得“用户创新工具箱”的方法成为可能。

因为JavaScript具有可塑性,而且有如此多的网页设计师,你会看到关于如何使用它的不同思想流派涌现出来。这在过去10年随着各种JavaScript库的出现变得非常明显。我认为这实际上是一个优势,正如我早前所说,JavaScript不会告诉你“这是唯一正确的写法,这是唯一真正的面向对象范式,这是你创建可重用抽象的唯一方式”。

当然,这并非没有缺点。对初学者来说可能比较困难,人们可能会重复发明某些轮子并犯错,或者不喜欢必须学习某个库。但你看jQuery库,因为它给了人们非常便捷的查询和操作范式。同样,这在JavaScript中不是强制性的,但很多人学习了jQuery后以为那就是JavaScript,甚至认为jQuery本身就是一种语言。jQuery很棒,John Resig(jQuery作者)曾与我们在Mozilla共事。但现在有太多优秀的库了,而且它们实际上在变得更精简、更具组合性,这是一个好趋势。

我认为,JavaScript通过其可塑性和对用户创新的培育,扮演了独特的角色。如果我当时做了更 rigid 的东西,它很可能已经失败了。我无法想象如何能逃脱C++那种面向对象的模式。部分原因是我不得不这样做,因为如果我在1995年5月那10天里在JavaScript中加入了“类”,我想我会被告知“这太像Java了,你在和Java竞争”。Sun公司可能有人会比当时更严厉地指责Bill Joy,这可能会毁掉整个合作。所以,我不仅受到时间限制,还受到市场指令的限制:让它看起来像Java,但不要做得太大,它只是Java身边那个傻乎乎的小兄弟语言。

但随后你加入了一些原始特性,比如闭包之类的东西,这样你就可以构建你想要的东西。是的,这一点在当时被很多人忽视了,甚至在第一个版本中,这些功能也并非都处于良好的工作状态。但在接下来的几年里,它不仅变得更加标准化和广为人知,在接下来的10年里,它被大力推广,比如Crockford就大力倡导闭包模式及其优秀用法。

人们发现JavaScript的可塑性、表达力和强大功能足够有吸引力,以至于有些人实际上抵制任何“ES版本2”。他们说:“我不希望你添加那些语法糖或特殊形式,来代表某些模式,我更愿意自己编写或通过库来获取。”

你创造了一种抽象,让实现者可以做疯狂的事情,人们可以重新思考解释器真正应该做什么。他们可以说:“好吧,V8引擎来了,带来了以前从未尝试过的不同优化。”我了解这些优化,因为我研究过Smalltalk,但之前没有人投入时间和金钱去做。谷歌可能是第一个,其他公司如苹果和Mozilla也在尽力跟进。V8在推动这方面发展上功不可没,尽管它并不完全是第一个,但它在2008年左右的出现汇聚了各方努力,非常有帮助,并向人们展示了可以做到什么程度。

令我感兴趣的是,当你对语言施加更密集的工作负载时,你会发现应该出现一个新的“V8”。它可能不会来自谷歌,因为他们可能已经厌倦了优化JavaScript。事实上,我相信Dart语言(由V8负责人发起)就是对这种情况的回应,他们想做一种不需要担心所有这些疯狂兼容性问题的语言。Dart可能不会成功,它也可能无法让JavaScript达到下一个性能水平。但我相信那个水平是存在的,而且性能仍在以比Java等语言更显著的速度提升。

现在,很多所谓的“HTML5”开发(包括JavaScript、CSS、Web API等)正在兴起。你会看到像Zynga这样的公司只做HTML5游戏,这比一些人想象的要快。我和纽约Union Square的风险投资家Fred Wilson聊过,他说:“是的,它已经来了。”他原以为还需要很多年。我们已经越过了那个拐点。你称之为HTML5,但它真正的含义是“Web技术栈”。你用来编写网页和托管Web应用的同一种技术栈,现在可以用来编写在设备上运行的应用,这些应用可能是托管的,可能是离线的,界限已经模糊。你可以将它们与一个URL关联,但也可以带着它们上飞机,而不用担心断开互联网连接会丢失任何东西。

总结

本节课中,我们一起学习了JavaScript的诞生故事。我们了解到,Brendan Eich在1995年用短短10天时间,在“看起来像Java”的指令和紧迫的时间限制下,创造出了这门语言。其核心设计理念是可塑性易于使用,它借鉴了多种语言的特性(如Self的原型继承和Scheme的一等函数),但并未被单一范式束缚。尽管早期被用于制造恼人效果,但随着性能提升和标准演进,JavaScript凭借其灵活性和强大的社区库生态系统,最终成为驱动Web 2.0革命和现代Web应用(包括移动和离线应用)的核心技术。它的成功很大程度上源于其允许开发者塑造语言本身,从而孕育了丰富的创新。

018:JavaScript面向对象概念

在本节课中,我们将要学习JavaScript中的面向对象编程概念。面向对象编程是一种将代码和数据组织成可重用“对象”的编程范式。我们将探讨类、对象、方法和属性等核心概念,并了解JavaScript实现这些概念的独特方式。

概述

面向对象编程是一种在许多编程语言中通用的核心范式。无论你学习Python、PHP还是JavaScript,其基本概念都是相通的。因此,如果你之前学习过其他面向对象编程课程,可能会发现一些熟悉的内容。本节将重点介绍这些概念在JavaScript中的具体实现。

核心概念:类与对象

上一节我们介绍了面向对象编程的通用性,本节中我们来看看其核心概念:类与对象。

类就像一个模板或蓝图。它定义了如何创建某种东西,但它本身并不是那个东西。你可以将类想象成一个饼干模具。

公式: 类 = 模板 / 蓝图

对象是类的一个实例。使用同一个饼干模具可以制作出成千上万个饼干,每个饼干都是该模具的一个实例。对象可以拥有自己独特的属性,例如不同的糖霜。

公式: 对象 = 类的实例

类中的代码与数据

在类中,我们封装了代码和数据。以下是构成类的两个主要部分:

  • 方法: 这是类中的代码部分,定义了对象可以执行的操作。你可以将其视为算法。
  • 属性: 这是类中的数据部分,定义了对象的状态或特征。它也被称为字段。

有些编程思想使用“消息”而非“方法”来描述对象间的通信,即向一个对象发送消息来触发其行为。在本教程中,我们将统一使用“方法”这一术语。

JavaScript对象的独特性

如果你接触过Python、PHP、Java或C++中的对象,你会发现JavaScript的对象有些不同。关键在于JavaScript的函数。

function关键字实际上是一个可执行语句。你可以调用和使用function关键字,而函数本身可以创建并返回代码,然后你可以将其赋值给一个变量。这被称为“一等函数”。

代码示例:

// 将数字赋值给变量
let x = 42;
// 将字符串赋值给变量
let y = "Hello";
// 将函数(代码)赋值给变量
let z = function(a, b) { return a + b; };

这种特性体现了代码与数据的对称性:在JavaScript中,代码本身也被视为一种可以操作和传递的数据。这种设计理念深受Lisp语言传统的影响,使得JavaScript在构建和复用代码方面非常灵活和优雅。

总结

本节课中我们一起学习了JavaScript面向对象编程的基础。我们了解了类作为模板、对象作为实例的核心关系,探讨了类中方法(代码)与属性(数据)的组成。最重要的是,我们认识了JavaScript通过“一等函数”实现面向对象模式的独特之处,这使得代码和数据可以同等对待,为构建灵活可复用的程序提供了强大支持。

接下来,我们将通过一个具体的例子,来看看如何在JavaScript中组合出一个类。

019:JavaScript面向对象-类 🧩

在本节课中,我们将学习如何在JavaScript中构建和使用类。我们将了解类的内部工作原理,包括如何定义类、创建实例、理解构造函数以及管理多个独立的对象实例。


概述

JavaScript中的类是一种创建对象的模板。虽然ES6引入了更简洁的类语法,但理解其底层基于函数的实现方式,能帮助我们更深刻地掌握JavaScript面向对象编程的核心机制。本节将介绍如何使用function关键字定义类,如何使用new关键字创建实例,以及this关键字在其中的作用。


类的定义与构造函数

上一节我们介绍了对象的基本概念,本节中我们来看看如何在JavaScript中定义一个类。在JavaScript中,类本质上是一个特殊的函数。

定义类的基本语法如下:

function PartyAnimal() {
    this.x = 0;
    this.party = function() {
        this.x = this.x + 1;
        console.log("So far " + this.x);
    };
}
  • function PartyAnimal() 是类的定义,其名称PartyAnimal就是类名。
  • 在函数体内部,this关键字代表即将创建的新对象实例。
  • this.x = 0; 为实例创建了一个名为x的属性(成员变量),并初始化为0。
  • this.party = function() { ... } 为实例创建了一个名为party的方法。这里使用了一个匿名函数。


创建类的实例

定义好类之后,我们需要使用new关键字来创建具体的对象实例。

创建实例的语法如下:

let an = new PartyAnimal();
  • new PartyAnimal() 是关键步骤。new操作符会执行以下操作:
    1. 创建一个新的空对象。
    2. PartyAnimal函数中的代码在这个新对象的上下文中执行(此时函数内的this就指向这个新对象)。
    3. 执行构造函数中的代码(如this.x = 0),为对象设置初始状态。
    4. 返回这个新创建并初始化好的对象。
  • let an = 将这个新创建的对象赋值给变量an,这样我们就可以通过an来操作这个实例了。

调用对象的方法

创建实例后,我们可以通过点符号(.)来访问其属性和调用其方法。

以下是调用方法的示例:

an.party(); // 输出: So far 1
an.party(); // 输出: So far 2
an.party(); // 输出: So far 3

每次调用an.party()方法时,都会执行方法内的代码:将实例自身的x属性加1,然后打印出当前值。由于anPartyAnimal的一个实例,方法内的this.x指的就是an.x


构造函数详解

构造函数是类中一段特殊的代码,它在使用new关键字创建新实例时自动执行,用于初始化对象的状态。

一个带有构造函数的类示例:

function PartyAnimal(name) {
    this.x = 0;
    this.name = name;
    console.log("Built " + this.name);
    this.party = function() {
        this.x = this.x + 1;
        console.log(this.name + "=" + this.x);
    };
}
  • 构造函数可以接收参数。这里的name参数在创建实例时传入。
  • this.name = name; 将传入的参数赋值给实例的name属性。
  • console.log("Built " + this.name); 这行代码让我们能直观地看到构造函数何时被执行。


创建多个独立实例

类的强大之处在于可以根据同一个模板创建多个相互独立的实例,每个实例都拥有自己独立的属性值。

以下是创建多个实例的示例:

let s = new PartyAnimal("Sally");
let j = new PartyAnimal("Jim");

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/16fad4d178a71749f2d47b9e5a440cb0_42.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/16fad4d178a71749f2d47b9e5a440cb0_43.png)

s.party(); // 输出: Sally=1
j.party(); // 输出: Jim=1
s.party(); // 输出: Sally=2
  • let s = new PartyAnimal("Sally"); 创建了第一个实例s,其name属性为"Sally",x初始为0。
  • let j = new PartyAnimal("Jim"); 创建了第二个实例j,其name属性为"Jim",x初始为0。它与实例s完全独立。
  • 当调用s.party()时,方法内的this指向s,因此修改的是s.x
  • 当调用j.party()时,方法内的this指向j,因此修改的是j.x
  • 最后再次调用s.party()时,s.x从1增加到2,而j.x仍然保持为1,这证明了两个实例状态的独立性。

核心概念总结

本节课中我们一起学习了JavaScript面向对象编程中关于类的核心概念:

  1. 类 (Class):创建对象的模板或蓝图。在JavaScript中,类通过function定义。
  2. 实例/对象 (Instance/Object):根据类创建的具体实体。使用new关键字创建。
  3. 属性 (Attribute/Member Variable):对象内部存储的数据,如this.xthis.name
  4. 方法 (Method):对象内部定义的函数,代表对象的行为,如this.party
  5. 构造函数 (Constructor):在创建新实例时自动运行的代码,用于初始化对象的状态。它是类定义函数本身的主体部分。
  6. this 关键字:在方法或构造函数内部,this指向当前所属的对象实例。

理解这些概念是后续学习DOM操作、jQuery等高级JavaScript技术的基础。JavaScript(和Python一样)提供了一套成熟而优雅的面向对象编程模型。

020:图片处理示例代码实战

在本节课中,我们将学习如何在Django应用中处理图片上传与展示。我们将通过一个CRUD(增删改查)应用示例,详细解析从模型定义、表单处理、视图逻辑到前端展示的完整流程。

概述

我们将构建一个允许用户上传、查看、编辑和删除图片的应用。核心在于理解如何将图片的二进制数据存入数据库,并在需要时正确地将其作为HTTP响应流返回给浏览器。这涉及到表单的multipart/form-data编码、文件大小验证、模型中的二进制字段处理以及通过视图动态提供图片内容。


模型定义:存储图片数据

上一节我们介绍了课程目标,本节中我们来看看数据是如何存储的。一切从模型开始,它定义了数据的结构。

models.py中,我们定义了一个Pic模型,它包含几个关键字段来存储图片信息:

from django.db import models
from owner.models import Owner

class Pic(models.Model):
    # 图片标题和描述
    title = models.CharField(max_length=200)
    text = models.TextField()
    # 存储图片二进制数据
    picture = models.BinaryField(null=True, blank=True, editable=False)
    # 存储图片的MIME类型,如‘image/png’
    content_type = models.CharField(max_length=256, null=True, blank=True, editable=False)
    # 记录所有者、创建和更新时间
    owner = models.ForeignKey(Owner, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

核心概念解释

  • BinaryField:用于在数据库中存储原始二进制数据(BLOB),这里用来存放图片文件的字节。
  • content_type:这是一个字符串字段,用于记录图片的MIME类型(例如 image/jpeg, image/png)。当浏览器请求图片时,我们必须通过HTTP响应头正确设置这个类型,浏览器才能正确渲染图片。

注意picturecontent_type字段被设置为editable=False,这意味着它们不会在Django默认的Admin后台表单中显示。我们通过自定义Admin类来排除它们,因为通过Admin界面直接更新二进制数据比较复杂。

# admin.py
from django.contrib import admin
from .models import Pic

@admin.register(Pic)
class PicAdmin(admin.ModelAdmin):
    exclude = (‘picture‘, ‘content_type‘)

视图逻辑:处理请求与响应

模型定义了数据结构,接下来我们看看视图如何协调表单与模型,处理用户的请求。

我们主要使用基于类的视图,并利用之前创建的Owner混合类来自动处理对象的所有者权限。

列表与详情视图

以下是用于显示图片列表和详情的视图,它们相对标准:

# views.py
from django.views.generic import ListView, DetailView
from owner.views import OwnerListView, OwnerDetailView
from .models import Pic

class PicListView(OwnerListView):
    model = Pic
    template_name = ‘pics/list.html‘

class PicDetailView(OwnerDetailView):
    model = Pic
    template_name = ‘pics/detail.html‘

OwnerListViewOwnerDetailView为我们处理了根据登录用户过滤列表和检查详情页权限的逻辑。

创建图片视图

创建视图(PicCreateView)是核心,它需要处理包含文件数据的POST请求。

# views.py
from django.views.generic import CreateView
from django.shortcuts import render, redirect
from .forms import CreateForm

class PicCreateView(CreateView):
    template_name = ‘pics/form.html‘
    form_class = CreateForm

    def get(self, request, *args, **kwargs):
        # 处理GET请求,渲染空表单
        form = self.form_class()
        return render(request, self.template_name, {‘form‘: form})

    def post(self, request, *args, **kwargs):
        # 处理POST请求,表单数据包含在request.POST和request.FILES中
        form = self.form_class(request.POST, request.FILES or None)
        if not form.is_valid():
            # 如果表单验证失败,重新渲染表单并显示错误
            return render(request, self.template_name, {‘form‘: form})
        # 表单有效,进行保存
        pic = form.save(commit=False) # 先不提交到数据库
        pic.owner = self.request.user # 设置所有者
        pic.save() # 保存到数据库
        return redirect(‘pics:all‘) # 重定向到列表页

关键点

  • 当表单包含文件时,必须使用enctype="multipart/form-data"
  • 上传的文件数据不在request.POST中,而是在request.FILES字典里。
  • form.save(commit=False)返回一个尚未保存到数据库的模型实例,允许我们在最终保存前对其进行修改(如设置owner字段)。

提供图片流的视图

图片本身不是静态文件,而是从数据库读取并通过一个特定的视图动态提供。

# views.py
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from .models import Pic

def stream_file(request, pk):
    # 根据主键获取图片对象
    pic = get_object_or_404(Pic, id=pk)
    # 构建HTTP响应,设置正确的Content-Type和Content-Length
    response = HttpResponse(pic.picture)
    response[‘Content-Type‘] = pic.content_type
    response[‘Content-Length‘] = len(pic.picture)
    return response

这个视图被映射到一个URL(例如/pics/picture/<int:pk>),在HTML中,图片标签的src属性就指向这个URL。

<img src="{% url ‘pics:picture‘ pic.id %}" alt="{{ pic.title }}">

表单处理:验证与数据清洗

视图是控制器,而表单是连接用户输入(浏览器)和模型(数据库)的桥梁。本节我们深入看看表单如何工作。

表单类CreateForm继承自ModelForm,但我们需要自定义文件字段和验证逻辑。

# forms.py
from django import forms
from django.core.exceptions import ValidationError
from .models import Pic

class CreateForm(forms.ModelForm):
    # 自定义文件上传字段
    picture = forms.FileField(
        label=‘Select an image to upload‘,
        help_text=‘Max file size: 2 MB‘
    )
    # 文件大小限制(字节)
    max_upload_limit = 2 * 1024 * 1024
    max_upload_limit_text = ‘2 MB‘

    class Meta:
        model = Pic
        fields = [‘title‘, ‘text‘, ‘picture‘] # 包含自定义的picture字段

    def clean(self):
        # 清洗数据,验证文件大小
        cleaned_data = super().clean()
        pic = cleaned_data.get(‘picture‘)
        if pic is None:
            return
        if len(pic) > self.max_upload_limit:
            # 如果文件太大,抛出一个验证错误
            raise ValidationError(f‘File must be < {self.max_upload_limit_text}‘)
        # 将文件数据和MIME类型暂存,供save方法使用
        self.cleaned_data[‘uploaded_file‘] = pic
        self.cleaned_data[‘content_type‘] = pic.content_type

    def save(self, commit=True):
        # 重写save方法,将文件二进制数据存入模型
        instance = super().save(commit=False) # 获取模型实例但不保存

        # 获取清洗阶段暂存的数据
        uploaded_file = self.cleaned_data.get(‘uploaded_file‘, None)
        content_type = self.cleaned_data.get(‘content_type‘, None)

        if uploaded_file is not None:
            # 将二进制数据和MIME类型赋值给模型字段
            instance.picture = uploaded_file.read()
            instance.content_type = content_type

        if commit:
            instance.save() # 如果commit为True,则保存到数据库
        return instance

表单工作流程

  1. 初始化:表单根据Meta.fields生成字段,包括自定义的FileField
  2. 验证 (clean):用户提交后,首先调用父类的clean方法进行基础验证(如字段是否必填、长度限制)。然后我们自定义的clean方法检查文件大小,并将文件对象和MIME类型暂存到cleaned_data中。
  3. 保存 (save):当视图调用form.save(commit=False)时,我们重写的save方法会执行。它从cleaned_data中取出暂存的文件数据,将其读取为字节并赋值给模型实例的picturecontent_type字段。

前端交互:表单与图片预览

数据在后端处理完毕,最后我们来看看用户直接交互的前端部分。

表单模板与客户端验证

以下是form.html模板的关键部分:

<!-- form.html -->
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form|crispy }}
    <button type="submit" class="btn btn-primary">Submit</button>
    <a href="{% url ‘pics:all‘ %}" class="btn btn-secondary">Cancel</a>
</form>

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a1805a9eacddcae358ce378cd48505b8_10.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a1805a9eacddcae358ce378cd48505b8_12.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a1805a9eacddcae358ce378cd48505b8_14.png)

<script>
$(document).ready(function(){
    $(‘#upload-form‘).submit(function(event){
        // 客户端验证文件大小
        var fileInput = $(‘#id_picture‘)[0];
        if (fileInput.files.length > 0) {
            var fileSize = fileInput.files[0].size; // 文件大小,单位字节
            var maxSize = {{ form.max_upload_limit }}; // 从后端传递限制值
            if (fileSize > maxSize) {
                alert(`File must be smaller than {{ form.max_upload_limit_text }}`);
                event.preventDefault(); // 阻止表单提交
                return false;
            }
        }
    });
});
</script>

关键点

  • enctype="multipart/form-data":表单必须设置此属性才能正确上传文件。
  • 客户端验证:使用JavaScript在文件上传前检查大小,提供即时反馈,提升用户体验。但服务器端验证(clean方法中)是必须的,因为客户端验证可以被绕过。

图片详情页与点击放大

detail.html中,我们展示图片并实现一个简单的点击放大效果。

<!-- detail.html -->
{% extends ‘base_bootstrap.html‘ %}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a1805a9eacddcae358ce378cd48505b8_25.png)

{% block content %}
<h1>{{ pic.title }}</h1>
<p>{{ pic.text }}</p>
<!-- 小图,可点击 -->
<img src="{% url ‘pics:picture‘ pic.id %}"
     style="max-width: 200px; float: right;"
     onclick="document.getElementById(‘overlay‘).style.display = ‘block‘"
     alt="{{ pic.title }}">

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a1805a9eacddcae358ce378cd48505b8_27.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/a1805a9eacddcae358ce378cd48505b8_29.png)

<!-- 全屏遮罩层,初始隐藏 -->
<div id="overlay" style="position: fixed; display: none; ..." onclick="this.style.display=‘none‘">
    <!-- 大图 -->
    <img src="{% url ‘pics:picture‘ pic.id %}" style="width: 90%; margin-top: 50px; border: 3px solid black;">
</div>
{% endblock %}

实现原理

  1. 页面上有一个缩略图,其onclick事件将overlay这个DIV的显示属性改为block,使其可见。
  2. overlay是一个覆盖全屏的固定定位层,背景是半透明灰色,里面包含一张大图。
  3. overlay层本身也设置onclick事件,点击任何位置(包括图片和背景)就将其隐藏(display: ‘none‘)。

这是一种利用CSS和少量JavaScript实现模态框效果的简单方法。


总结

本节课中我们一起学习了在Django应用中集成图片处理功能的完整流程。我们回顾一下关键步骤:

  1. 模型层:使用BinaryField存储图片二进制数据,并用CharField记录对应的content_type
  2. 表单层:创建自定义ModelForm,重写clean()方法进行服务器端文件验证,并重写save()方法将文件数据正确存入模型实例。
  3. 视图层
    • 使用基于类的视图处理CRUD操作,注意处理request.FILES
    • 编写一个专门的视图(stream_file)从数据库读取二进制数据并设置为HTTP响应,必须正确设置Content-Type响应头
  4. 前端层
    • 表单必须设置enctype="multipart/form-data"
    • 可以添加JavaScript进行客户端文件大小验证以提升体验。
    • 利用CSS和JavaScript实现简单的图片点击放大预览功能。

通过这个示例,你将掌握在Django中处理文件上传的核心模式,并可将其应用于其他类型的文件处理场景。

021:论坛(Forums)示例代码实战 🧑‍💻

在本节课中,我们将一起学习并分析一个Django论坛应用的示例代码。这个示例的核心是演示如何使用“多对多”关系,并再次实践“所有者”模式。我们将从模型设计开始,逐步深入到视图、模板和表单的处理逻辑,理解如何构建一个允许用户创建论坛帖子并发表评论的系统。

概述

我们将要分析的代码实现了一个简单的论坛功能。主要特性包括:

  • 用户可以创建、编辑、删除自己的论坛帖子。
  • 所有用户(包括未登录用户)可以查看帖子及其评论。
  • 登录用户可以对任何帖子发表评论。
  • 用户只能删除自己发表的评论。
  • 通过“所有者”模式控制编辑和删除权限。

这个应用涉及三个核心模型:User(Django内置)、Forum(论坛帖子)和Comment(评论)。ForumComment都与User模型存在“一对多”关系,而ForumUser之间通过Comment模型构成了一个“多对多”关系。

模型设计:理解数据关系

上一节我们概述了应用的功能,本节中我们来看看支撑这些功能的数据模型是如何设计的。理解模型之间的关系是理解整个应用逻辑的基础。

核心模型定义在 models.py 文件中,主要有两个:ForumComment

1. Forum 模型
Forum 模型代表一个论坛帖子。除了“多对多”字段,它的结构与我们之前构建的CRUD应用非常相似。

class Forum(models.Model):
    title = models.CharField(max_length=200)
    text = models.TextField()
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    comments = models.ManyToManyField(settings.AUTH_USER_MODEL,
        through='Comment', related_name='forum_comments')
  • owner 字段是一个指向 User 模型的外键,建立了“一个用户拥有多个论坛帖子”的“一对多”关系。
  • comments 字段是一个 ManyToManyField,它通过 Comment 模型与 User 模型关联。related_name='forum_comments' 非常重要,它会在 User 模型实例上创建一个名为 forum_comments 的反向查询管理器,用于获取该用户的所有评论。即使我们不立即使用它,明确命名也可以避免未来可能出现的名称冲突。

2. Comment 模型
Comment 模型作为连接表(或称为联结表、关联表),它存储了具体的评论内容,并包含两个外键。

class Comment(models.Model):
    text = models.TextField()
    forum = models.ForeignKey(Forum, on_delete=models.CASCADE)
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    updated_at = models.DateTimeField(auto_now=True)
  • forumowner 是两个外键,分别指向 ForumUser 模型。
  • text 字段存储评论内容。我们将评论数据直接建模在这个连接模型中,这是一种常见且便捷的做法。

数据关系图解
这三个模型的关系可以可视化如下:

User 表 (1) --<拥有>-- (多) Comment 表 (多) --<属于>-- (1) Forum 表
  • 一个 User 可以拥有多个 Forum(通过 Forum.owner)。
  • 一个 User 可以拥有多个 Comment(通过 Comment.owner)。
  • 一个 Forum 可以拥有多个 Comment(通过 Comment.forum)。
  • 因此,ForumUser 通过 Comment 表形成了“多对多”的关系:一个论坛可以有多个用户(通过他们的评论),一个用户也可以参与多个论坛(通过发表评论)。

视图逻辑:处理请求与响应

理解了数据模型后,我们来看看视图(views.py)如何操作这些模型来处理用户请求。大部分视图基于我们之前熟悉的CRUD模式构建,但论坛详情页和评论创建视图有一些值得关注的细节。

以下是几个关键视图的解析:

1. ForumDetailView(论坛详情视图)
这个视图负责显示一个特定的论坛帖子及其所有评论。它继承自 OwnerDetailView

class ForumDetailView(OwnerDetailView):
    model = Forum
    template_name = "forums/forum_detail.html"

    def get(self, request, pk):
        forum = get_object_or_404(Forum, id=pk)
        comments = Comment.objects.filter(forum=forum).order_by('-updated_at')
        comment_form = CommentForm()
        context = { 'forum': forum, 'comments': comments, 'comment_form': comment_form }
        return render(request, self.template_name, context)
  • 它通过 pk(主键)获取对应的 Forum 对象。
  • 然后,它过滤出所有 forum 字段等于当前论坛对象的 Comment 对象,并按更新时间倒序排列。
  • 同时,它创建了一个空的 CommentForm 实例,用于在页面上渲染评论输入框。
  • 最后,它将论坛对象、评论列表和评论表单一起传递给模板进行渲染。

2. comment_create_view(评论创建视图)
当用户在详情页提交评论表单时,会触发这个视图。

@login_required
def comment_create_view(request, pk):
    forum = get_object_or_404(Forum, id=pk)
    if request.method == 'POST':
        comment = Comment(text=request.POST['comment'], owner=request.user, forum=forum)
        comment.save()
        return redirect(reverse('forums:forum_detail', args=[pk]))
  • 它首先通过 pk 获取要评论的 Forum 对象。
  • POST 请求中,它创建一个新的 Comment 对象。关键步骤在于同时为这个评论对象设置两个外键owner=request.user(当前登录用户)和 forum=forum(当前论坛帖子)。
  • 调用 comment.save() 将这个包含关联关系的评论对象存入数据库。
  • 最后,它重定向回论坛详情页(forums:forum_detail),并使用 args=[pk] 传递论坛的主键,以显示刚添加的评论。这是一个典型的“Post-Redirect-Get”模式,避免了重复提交。

3. CommentDeleteView(评论删除视图)
这个视图处理评论的删除,它继承自 OwnerDeleteView

class CommentDeleteView(OwnerDeleteView):
    model = Comment
    template_name = "forums/comment_delete.html"

    def get_success_url(self):
        forum_id = self.object.forum.id
        return reverse('forums:forum_detail', kwargs={'pk': forum_id})
  • 大部分删除逻辑(如权限检查、确认页面渲染)都由父类 OwnerDeleteView 处理。
  • 我们需要重写 get_success_url 方法。因为删除评论后,我们不应该停留在评论页面,而应该返回到该评论所属的论坛详情页。
  • self.object 指向当前正在被删除的 Comment 对象(即使在数据库删除后,内存中仍有其副本)。通过 self.object.forum.id 可以获取到其所属论坛的ID,然后用它来生成正确的重定向URL。

模板与表单:构建用户界面

视图处理了业务逻辑,接下来我们看看模板(templates/)和表单(forms.py)如何共同构建用户界面。论坛详情页(forum_detail.html)是交互最复杂的部分。

1. 评论表单
我们使用一个简单的Django表单来生成评论输入框,这让我们能方便地使用Crispy Forms等库来美化样式。

# forms.py
class CommentForm(forms.Form):
    comment = forms.CharField(widget=forms.Textarea(attrs={'rows': 3}), label='')

在详情页模板中,我们这样渲染它:

{% if user.is_authenticated %}
    <form action="{% url 'forums:comment_create' forum.id %}" method="post">
        {% csrf_token %}
        {{ comment_form|crispy }}
        <input type="submit" value="提交评论">
        <a href="{% url 'forums:all' %}">所有论坛</a>
    </form>
{% endif %}
  • 表单的 action 指向 forums:comment_create 视图,并传递当前论坛的 id
  • 使用 {% if user.is_authenticated %} 确保只有登录用户能看到评论表单。

2. 显示评论列表
在表单下方,我们遍历从视图传递来的 comments 列表,显示所有评论。

{% for comment in comments %}
    <p>{{ comment.text }}</p>
    <p>由 {{ comment.owner }} 于 {{ comment.updated_at|naturaltime }} 发布</p>
    {% if user == comment.owner %}
        <a href="{% url 'forums:comment_delete' comment.id %}">
            <i class="fas fa-trash"></i> <!-- 删除图标 -->
        </a>
    {% endif %}
{% endfor %}
  • 循环输出每条评论的文本、所有者和发布时间(使用 naturaltime 过滤器显示为“刚刚”、“2分钟前”等友好格式)。
  • 使用条件判断 {% if user == comment.owner %},只有当当前登录用户是这条评论的所有者时,才显示删除图标。删除图标的链接指向 forums:comment_delete 视图,并传递评论的 id

3. 删除确认页面
当用户点击删除图标时,会跳转到 comment_delete.html 这个确认页面。这个模板通常继承自一个基础模板,并包含一个简单的确认表单。

<form method="post">
    {% csrf_token %}
    <p>你确定要删除这条评论吗?</p>
    <input type="submit" value="确认删除">
    <a href="{% url 'forums:forum_detail' object.forum.id %}">取消</a>
</form>
  • 表单提交后,会调用 CommentDeleteViewpost 方法执行删除。
  • “取消”链接使用 object.forum.id 返回到评论所属的论坛详情页。

URL配置与路由

最后,我们快速浏览一下 urls.py 文件,看看请求是如何被路由到对应视图的。配置方式与我们之前构建的CRUD应用类似。

app_name = 'forums'
urlpatterns = [
    path('', ForumListView.as_view(), name='all'),
    path('forum/<int:pk>', ForumDetailView.as_view(), name='forum_detail'),
    path('forum/create', ForumCreateView.as_view(), name='forum_create'),
    path('forum/<int:pk>/update', ForumUpdateView.as_view(), name='forum_update'),
    path('forum/<int:pk>/delete', ForumDeleteView.as_view(), name='forum_delete'),
    path('forum/<int:pk>/comment', comment_create_view, name='comment_create'),
    path('comment/<int:pk>/delete', CommentDeleteView.as_view(), name='comment_delete'),
]
  • ForumComment 模型定义了完整的CRUD路由。
  • 注意评论创建路由 forum/<int:pk>/comment,它接收论坛的 pk,并将其传递给 comment_create_view 函数视图。
  • 评论删除路由 comment/<int:pk>/delete 接收评论自身的 pk,并将其传递给 CommentDeleteView 类视图。

总结

本节课中我们一起学习并拆解了一个Django论坛应用的示例代码。我们重点探讨了:

  1. 模型设计:如何使用 ManyToManyField 并通过一个中间模型(Comment)来建立“多对多”关系,以及如何在中间模型中存储额外数据(如评论内容)。
  2. 视图逻辑:如何编写视图来创建关联对象(特别是同时设置多个外键),以及如何重写类视图的方法(如 get_success_url)来实现自定义的重定向行为。
  3. 模板交互:如何在模板中根据用户身份和对象所有权动态显示内容(如编辑/删除按钮),以及如何实现“Post-Redirect-Get”模式来提升用户体验。
  4. 权限控制:再次实践了“所有者”模式,确保用户只能修改或删除自己创建的内容。

这个示例将我们之前学到的许多Django概念(模型关系、CRUD操作、基于所有者的权限、表单处理、模板标签等)组合成了一个更复杂的实际应用。理解这个例子有助于你开始构建涉及复杂数据关系的Django项目。

022:面对面办公时间-佐治亚州亚特兰大

概述

在本节课中,我们将回顾一次在佐治亚州亚特兰大举行的面对面办公时间活动。通过参与学生的简短介绍,我们可以了解他们学习本课程的背景、动机与感受。


查克在这里,我们位于亚特兰大北部的巴克海特区。我们举办了一次很棒的办公时间活动。按照传统,我想让你认识一些与你一同上课的同学。

以下是参与本次活动的部分学生介绍。

  • 我是摩根,来自亚特兰大。我和我爸爸一起上了这门课,因为他让我上,但这门课也很有趣。
  • 我是杰克,我和我女儿一起上了这门课。这是一种对称,我们度过了愉快的时光,你的教学非常棒,谢谢。
  • 我是汤姆,来自亚特兰大。我认为Python会很有趣(巨蟒剧团)。在计算机领域工作了很长时间后,我觉得是时候学点汇编以外的东西了。但实际上,用汇编语言你也能完成所有需要做的事情。
  • 我叫乔·康特拉斯,我是佐治亚理工学院的电气工程师,想探索计算机科学领域,这门课太棒了。
  • 我电气工程不及格,所以成了计算机科学家,因此你应该没问题。我父亲是电气工程师,在我家那样才算酷。我做不了电气工程,所以不及格,然后成了计算机科学家。所以,如果你电气工程学得好,我希望你计算机科学也学得好。
  • 我是大卫,住在亚特兰大。我上了Python课,这是我的第一门Coursera课程,非常愉快,教得很好。谢谢。
  • 嗨,我叫露西,来自巴西。我上了Python课,我认为这是一个开始,谢谢。
  • 我是索尼娅,是佐治亚州立大学的认知科学研究生。我上了查克博士的课,现在我知道我想学更多了。认知科学实际上才是核心,如果你仔细想想的话。
  • 我是亚历克斯,来自亚特兰大和贝鲁特。索尼娅让我来上这门课。我也对认知科学感兴趣。说实话,我还没上过任何课,我的编程经历相当坎坷,但现在我有更多理由深入学习了。
  • 嗨,我是埃德,我刚上完互联网历史课,我很喜欢它,所以我期待下次开课的Python课程。
  • 嗨,我是塔克,来自亚特兰大。我刚完成我的Python课程,我也有这个证书。我想感谢查克博士。我是一名IT专业人士。
  • 嗨,我是莫琳,我已经上过互联网历史课了,我非常期待Python编程课。
  • 嗨,我是迪伊,来自亚特兰大。我是一名业务分析师。我还没上过这门课,但每当我的丈夫播放课程视频时,我已经在电视上旁听过很多次了。我计划报名参加下一期课程,非常感谢你们所有人的努力。

总结

本节课中,我们一起回顾了亚特兰大办公时间活动中学生们的分享。我们看到学生们来自不同的背景——有父女同学、职业转型者、在校学生以及IT专业人士。他们的学习动机多样,或出于兴趣,或为了职业发展,或受家人影响。尽管起点和经历不同,但他们都通过本课程开启了编程学习之旅,并表达了继续深入学习的愿望。这次活动也展示了学习社区的活力与支持。

023:部署Django应用

概述

在本节课中,我们将学习如何将开发完成的Django应用部署到生产环境,使其能够被互联网上的用户访问。部署是项目开发的最后一步,也是至关重要的一步。

上一节我们介绍了Django应用的开发与调试,本节中我们来看看如何让应用上线运行。


核心概念:服务器与WSGI

为了让Django应用在互联网上运行,我们需要一个Web服务器和一个应用服务器接口。在Python世界中,这通常通过WSGI(Web Server Gateway Interface)协议来实现。

一个简单的WSGI应用示例如下:

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [b'Hello, World!']

对于Django,我们使用其自带的wsgi.py文件作为WSGI入口点。


部署准备步骤

在开始部署前,需要完成一些准备工作。以下是部署前必须检查的清单:

  1. 设置DEBUG模式:在生产环境中,必须将DEBUG设置为False

    # settings.py
    DEBUG = False
    
  2. 配置ALLOWED_HOSTS:指定允许访问该应用的域名或IP地址列表。

    # settings.py
    ALLOWED_HOSTS = [‘yourdomain.com‘, ‘www.yourdomain.com‘]
    
  3. 收集静态文件:使用Django的collectstatic命令将各个应用中的静态文件集中到一个目录,便于Web服务器处理。

    python manage.py collectstatic
    
  4. 配置数据库:确保生产环境数据库(如PostgreSQL、MySQL)已正确设置,并更新settings.py中的DATABASES配置。


选择部署方式

Django应用有多种部署方式,每种方式适用于不同的场景和需求。

以下是几种常见的部署方案:

  • 传统服务器部署:在Linux服务器(如Ubuntu)上手动配置Nginx、Gunicorn/uWSGI和数据库。这种方式可控性强,适合学习和高定制化需求。
  • 平台即服务:使用Heroku、PythonAnywhere、Google App Engine等PaaS平台。它们简化了服务器管理,适合快速部署和小型项目。
  • 容器化部署:使用Docker将应用及其依赖打包成容器,然后在任何支持Docker的环境(如AWS、阿里云)中运行。这种方式保证了环境一致性,易于扩展。

示例:使用Gunicorn和Nginx部署

让我们以最常见的Linux服务器手动部署为例,了解其基本流程。

上一节我们列出了部署前的准备工作,本节中我们来看看具体的服务器软件配置。

  1. 安装Gunicorn:Gunicorn是一个Python WSGI HTTP服务器,用于运行Django应用。

    pip install gunicorn
    
  2. 测试运行Gunicorn:在项目根目录下,使用以下命令测试应用是否能通过Gunicorn运行。

    gunicorn your_project_name.wsgi:application
    
  3. 配置Nginx:Nginx作为反向代理服务器,处理静态文件请求并将动态请求转发给Gunicorn。需要编辑Nginx的站点配置文件。

    # /etc/nginx/sites-available/your_project
    server {
        listen 80;
        server_name yourdomain.com;
    
        location /static/ {
            alias /path/to/your/staticfiles/;
        }
    
        location / {
            proxy_pass http://127.0.0.1:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
    
  4. 使用进程管理器:为了让Gunicorn在后台持续运行,可以使用systemd或Supervisor来管理进程。


总结

本节课中我们一起学习了Django应用部署的核心知识和基本流程。我们了解了WSGI接口的作用,掌握了部署前的关键配置步骤,并浏览了从传统服务器到PaaS平台的多种部署方式。最后,我们以Gunicorn结合Nginx的方案为例,演示了部署的核心操作步骤。成功部署后,你的Django应用就将正式在互联网上提供服务了。

024:浏览器中的JavaScript执行模型 🚀

在本节课中,我们将要学习JavaScript在浏览器环境中的独特执行模型。我们将探讨它与传统编程语言(如Python)的区别,并理解事件驱动、非阻塞执行等核心概念。

概述

JavaScript不仅是一种编程语言,在浏览器中运行时,它还承担了部分类似操作系统的职责。与在操作系统上运行、可以执行“等待”操作的Python程序不同,浏览器中的JavaScript采用协作式多任务模型。这意味着开发者需要帮助浏览器协调诸如更新文档对象模型、处理滚动、管理多个标签页事件以及处理计时器等多种任务。

浏览器中的执行时机

JavaScript代码可以在多个不同的时机被触发执行。以下是主要的几种情况:

  • 文档解析时:当浏览器解析HTML遇到<script>标签时,会暂停解析并立即执行其中的JavaScript代码。
  • 用户界面事件触发时:例如用户点击(click)、调整窗口大小(resize)等。
  • 计时器到期时:通过setTimeoutsetInterval设置的延时任务。
  • 异步活动完成时:例如通过网络请求(如fetch)获取数据完成。

这种模型的核心在于,我们需要告诉浏览器:“当某件事发生时,请运行这段代码”。这直接引出了JavaScript中一等函数概念的重要性,因为函数可以作为数据被传递和存储,以便在将来某个时刻被调用。

代码执行方式解析

上一节我们介绍了JavaScript的执行时机,本节中我们来看看几种具体的代码执行方式及其原理。

1. 无JavaScript的页面

首先,看一个最简单的HTML页面(01-noscript.htm),它不包含任何JavaScript。

<h1>Hello World</h1>
<p>This is a paragraph.</p>

浏览器收到服务器响应后,会解析这些HTML标签并构建文档对象模型。你在开发者工具“检查元素”中看到的,并非原始的服务器响应文本,而是浏览器根据解析结果创建的DOM树。在此过程中,JavaScript引擎处于闲置状态。

2. 完全由JavaScript生成的页面

接下来是一个极端的例子(02-document-write.htm),整个页面内容完全由JavaScript生成。

<script>
    document.write("<h1>Hello World</h1>");
    document.write("<p>This is a paragraph.</p>");
</script>

这个页面没有直接的HTML标签。当浏览器解析到<script>标签时,会立即执行其中的document.write()方法。该方法会直接将传入的HTML字符串写入DOM。因此,尽管源代码不同,但最终生成的DOM与第一个例子完全相同。这说明了JavaScript在解析阶段就能完全控制DOM的构建

3. 通过事件属性执行代码

更常见的方式是将JavaScript代码与用户事件绑定。例如,在HTML标签的onclick属性中内联编写代码(03-onclick.htm)。

<a href="#" onclick="console.log('I was clicked');">Click Me</a>
<script>
    function myFunc() {
        console.log("I was clicked (from function)");
    }
</script>

页面解析时,<script>标签内的myFunc函数被定义,但不会立即执行。onclick属性中的字符串"console.log('I was clicked');"也被解析。只有当用户点击链接时,浏览器才会执行这段字符串形式的代码。onclick属性是指定事件处理程序的一种直接方式。

4. 通过计时器执行代码

JavaScript还可以通过计时器在未来某个时刻执行代码,这是异步编程的基础(04-timer.htm)。

<h1>Timer Test</h1>
<p>Check the console.</p>
<script>
    function myFunc() {
        console.log("I was called later");
    }
    setTimeout(myFunc, 5000); // 5秒后执行myFunc
    console.log("Timer started");
</script>

解析到<script>标签时,代码立即执行。关键点在于setTimeout(myFunc, 5000)这行代码:它立即返回,并不会等待5秒。它只是向浏览器注册了一个任务:“5秒后,请调用myFunc函数”。因此,控制台会先立刻输出“Timer started”,大约5秒后再输出“I was called later”。计时器到期就是一个触发代码执行的事件。

5. 一等函数的关键作用

理解上述事件模型的关键是一等函数。我们通过修改计时器例子来演示这一点(05-function-ref.htm)。

function myFunc() {
    console.log("I was called later");
}
console.log(myFunc); // 输出:ƒ myFunc() { ... }
setTimeout(myFunc, 5000); // 注意:myFunc后没有括号()

这里有一个重要区别:

  • myFunc():带括号表示调用函数,会立即执行函数体并返回其返回值。
  • myFunc:不带括号表示函数引用,它是一个代表函数本身的值,可以像变量一样被传递。

setTimeout需要接收一个函数引用作为参数,以便在将来调用它。如果错误地写成setTimeout(myFunc(), 5000),则会立即执行myFunc,并将其返回值(可能是undefined)传给setTimeout,导致计时器设置失败。console.log(myFunc)的输出直接展示了这个函数对象。

总结

本节课中我们一起学习了JavaScript在浏览器中的执行模型。我们了解到它与传统阻塞式编程的区别,核心在于事件驱动非阻塞执行。JavaScript代码可以通过文档解析、UI事件、计时器或异步回调等多种方式被触发。一等函数是这个模型得以实现的基石,它允许我们将函数作为参数传递,从而告诉浏览器“在事件发生时执行这个函数”。掌握这些概念是理解现代前端异步编程(如Promise、async/await)的基础。接下来,我们将更深入地探讨如何使用JavaScript来操作文档对象模型

025:使用JavaScript操作文档对象模型(DOM) 🧩

在本节课中,我们将要学习JavaScript如何控制浏览器中的文档对象模型(DOM)。DOM是网页内容的结构化表示,JavaScript可以读取、修改和更新它,从而动态改变用户看到的界面。


上一节我们介绍了JavaScript在浏览器中的角色,本节中我们来看看它如何具体操作DOM。

JavaScript拥有对文档对象模型的控制权。文档对象模型是我们查看浏览器时所看到的内容。浏览器解析HTML,有时我们的JavaScript会写入内容,然后创建这个我们称之为文档对象模型的对象,最终通过一个窗口展示给用户。窗口是文档对象模型的一个子集。

在我们编写的任何JavaScript代码中,都可以读取、操作和更新DOM。一旦DOM被更新,用户就能通过窗口立即看到这些变化。

我们也可以直接进入调试器并操作文档对象模型。例如,回到一个没有JavaScript的页面(01 No script),它只显示一个标题和一个段落,这就是一个文档对象模型。但如果你进入“检查元素”模式,然后定位到那个段落标签,你可以修改它,比如添加单词“Co”。

从某种意义上说,调试器本身就是JavaScript,如果你操作得当,实际上可以在调试器中输入JavaScript命令。调试器同样“拥有”文档对象模型。因此,你可以简单地改变文档对象模型,并且这确实会改变用户界面。你甚至可以改变标签的类型,比如将一个段落标签改为锚点标签,或者添加一些属性。你改变了DOM,你就能看到变化。


之前提到了“窗口”这个概念。在浏览器的JavaScript环境中,有两个基础对象:DOM和窗口。

DOM是一个抽象概念,它是标签(或元素)的结构化层次。窗口是我们看到它的方式。你可以想象我展示的这张图:解析响应、运行一些JavaScript、调整文档对象模型,然后展示给用户。

如果你改变了文档对象模型,它会立即更新。左边的图是一种简化,因为我们实际上是通过窗口来查看文档对象模型的。窗口就是你屏幕上的内容。

最容易演示这一点的方法是理解:文档对象模型可能非常大,而窗口可能非常小。文档对象模型本身不变,但你可以来回调整窗口大小。所以有时你会看到滚动条。滚动条不是文档对象模型中的概念,而是窗口的概念。窗口有特定的高度,文档对象模型可能无法完全放入窗口,这就与窗口高度有关,从而产生了滚动条。


例如,如果你查看 06height.html 这个文件,它只是输出一个标题、一个段落,然后打印出窗口的高度和宽度:window.innerHeightwindow.innerWidth。这些是窗口的高度和宽度,而不是文档对象模型的尺寸,是我们查看它的窗口的尺寸。因此,如果你在一个非常小的窗口中打开这个页面并开启调试器,它会显示高度是116。如果你把窗口调大并重新加载,高度会变成256。所以,在JavaScript中,我们可以询问窗口的信息,也可以对窗口进行操作。我们可以做的一件事是上下滚动窗口,这在JavaScript中很常见。如果你有一个很大的文档对象模型和一个带滚动条的窗口,并且你想向用户展示特定内容(比如跳到底部),JavaScript可以将窗口滚动到底部或顶部。我们可以控制窗口。在这个例子中,我们只是询问它有多高多宽,但我们也可以控制它。


现在,如果我们查看 07scroll.html,可以看到它有一个标题和九个段落,这就是实际内容。

如果我们有一个很高的窗口,可以看到整个文档。但如果我们把窗口调小,就只能看到文档对象模型的一部分,这时窗口高度就变了。

现在你有了一个滚动条。滚动条的作用是什么?它只是上下移动文档对象模型。更准确地说,它移动的是我们对文档对象模型的视图部分。如果你在某些浏览器中进行“检查元素”操作,你会看到它通过一些小的标注告诉我们,当前正文正在滚动,段落4以上的内容溢出到屏幕上方,段落7、8、9则溢出到屏幕下方。如果你调整窗口大小并刷新页面,你会看到这些变化。在窗口足够大时,整个内容都显示出来,正文就不会被标记为可滚动元素,标题也没有溢出,等等。你可以用不同尺寸的窗口尝试,刷新页面,然后检查元素,观察滚动、溢出和滚动条的行为。在这个例子中,我没有打印高度,但你会看到高度实际上在变化。


接下来我们要做的一件事,就是在JavaScript中修改文档对象模型。


本节课中我们一起学习了JavaScript如何通过操作文档对象模型(DOM)来动态控制网页内容。我们了解了DOM与窗口的区别,以及如何通过JavaScript读取和修改DOM,从而实时更新用户界面。我们还看到了如何通过调试器直接操作DOM,并理解了窗口尺寸和滚动行为对视图的影响。

026:修改文档对象模型(DOM)的JavaScript技术

在本节课中,我们将学习如何使用JavaScript动态地修改网页的文档对象模型(DOM)。我们将从简单的元素内容修改开始,逐步深入到创建新元素、将其添加到DOM中,以及通过JavaScript控制CSS样式来实现元素的显示与隐藏。

从控制台日志到DOM修改

上一节我们介绍了如何在浏览器控制台中查看JavaScript的运行情况。本节中,我们来看看如何实际修改文档对象模型(DOM)。

回忆一下之前使用调试器修改DOM的例子。我们修改了DOM,如果修改的内容在窗口中可见,那么变化会立即呈现。本质上,我们在一端运行JavaScript,用户界面(UI)就会随之改变,这个过程几乎是瞬间完成的。当然,如果代码运行时间过长,变化可能会延迟,因为浏览器需要先完成JavaScript代码的执行,然后才能重新显示页面。

我们可以手动进行这些修改,但更好的方式是通过事件来触发修改。为了触发修改,我们需要能够获取DOM的特定部分,或者说查询文档对象模型。

通过事件触发DOM修改

让我们回顾一个使用事件处理函数的例子。以下代码展示了我们可以在JavaScript中定义一个函数,并通过onclick方法在点击事件发生后调用该函数。请注意,引号的使用很重要。

<button onclick="myFunction()">点击我</button>

onclick属性是一个字符串,它只在点击的那一刻被解析,然后myFunction函数在那一刻被调用。

现在,让我们在onclick方法中做一些更有趣的事情。

修改现有元素的内容

以下是一个更具体的例子。页面的顶部是一个带有ID的H1标签,我们设置其id="fun"。ID在整个文档中必须是唯一的,这与CSS规则一致。DOM元素拥有属性和方法,其中之一就是innerHTML

<h1 id="fun">原始标题</h1>
<button onclick="changeHeader()">改变标题</button>

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/5b287a3cedf2ed8f1d3ae0536f312b14_9.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/5b287a3cedf2ed8f1d3ae0536f312b14_10.png)

<script>
function changeHeader() {
    document.getElementById('fun').innerHTML = '一个很酷的标题';
    console.log('标题已被点击并更改');
}
</script>

在这段代码中,我们简单地将innerHTML属性赋值为“一个很酷的标题”。这就是你看到变化的基本方式——我们正在改变元素。

这个过程的一个重要部分是函数会结束。函数执行完毕后,控制权交还给浏览器。浏览器检测到DOM发生了变化,然后重新显示页面。如果我们函数的最后一行做了某些疯狂的操作,没有将线程交还给浏览器,页面可能会出错。但在这里,我们只是修改DOM然后返回。

这段代码运行得非常快,眨眼之间就完成了。其中没有任何缓慢的操作,一切发生得都很快。浏览器得以迅速重新显示内容。因此,当你点击按钮时,你会看到标题立即变成了“一个很酷的标题”。

我也可以读取innerHTML然后追加内容,但在这个例子中,我们直接替换了它。一旦函数执行完毕,浏览器就会接管并重新显示更新后的内容。

向DOM中添加新元素

到目前为止,我们所做的都是找到标签、修改它,然后放回去。现在,让我们尝试向DOM中添加全新的元素。这是一个稍微复杂一点的操作。

以下是实现此功能的源代码。我们有一个按钮(锚点标签),它调用add函数。页面上还有一个<ul>标签,其id="zap",里面最初有一个列表项<li>

<ul id="zap">
    <li>第一个项目</li>
</ul>
<button onclick="add()">添加项目</button>

<script>
let counter = 1; // 全局变量

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/5b287a3cedf2ed8f1d3ae0536f312b14_16.png)

function add() {
    // 创建一个新的LI元素
    const newItem = document.createElement('li');
    // 设置其类名和内容
    newItem.className = 'new';
    newItem.innerHTML = '项目编号 ' + counter;

    // 找到UL元素,并将新的LI添加为其子元素
    document.getElementById('zap').appendChild(newItem);

    counter++; // 计数器加一
}
</script>

首先,我们创建一个全局变量counter并初始化为1。在add函数中,我们使用document.createElement('li')创建一个新的<li>元素。然后,我们可以设置它的属性,比如classNameinnerHTML。这里,我们将innerHTML设置为“项目编号”加上当前的计数器值。

接着,我们通过document.getElementById('zap')找到<ul>标签,并使用appendChild方法将新创建的<li>元素添加为其子元素。每次点击按钮,counter都会增加1,并且会添加一个新的列表项。

初始状态是“第一个项目”。点击按钮几次后,它会动态地添加“项目编号 1”、“项目编号 2”等项目。这样,我们就可以从JavaScript中操作DOM的许多方面,包括改变CSS,从而改变元素的外观。

通过JavaScript控制CSS样式

在这个例子中,我们有一个标题标签,其id="fun",这同样是我们在JavaScript中获取它的方式。我们还有两个按钮,id分别为poof(隐藏)和show(显示),按钮文字也是“隐藏”和“显示”。

我们将使用addEventListener来绑定事件。虽然也可以使用onclick,但这里我想让大家习惯一种做法:通常在文档加载的最后阶段,集中添加所有的事件监听器。这样,HTML先被显示,然后再附加交互行为。

<h1 id="fun">可隐藏的标题</h1>
<button id="show">显示</button>
<button id="poof">隐藏</button>

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/5b287a3cedf2ed8f1d3ae0536f312b14_23.png)

<script>
// 为“显示”按钮添加点击事件监听器
document.getElementById('show').addEventListener('click', function() {
    document.getElementById('fun').style.display = 'block';
});

// 为“隐藏”按钮添加点击事件监听器(使用箭头函数)
document.getElementById('poof').addEventListener('click', (event) => {
    document.getElementById('fun').style.display = 'none';
});
</script>

在脚本中,我们首先获取show按钮元素,然后为其添加一个事件监听器。我们监听click事件,第二个参数是一个函数(这里是一个匿名函数)。这个函数只有一行代码:获取id="fun"的元素,并通过.style.display将其CSS的display属性设置为'block'(显示)。

对于poof(隐藏)按钮,我们也添加一个点击事件监听器。这里使用了稍有不同的语法——箭头函数(=>)。这是现代JavaScript中更简洁的语法。箭头函数(event) => { ... }定义了一个匿名函数,它接收一个event参数(浏览器在调用时会传入事件对象),但在本例中我们并未使用它。函数体将标题的display样式设置为'none',使其消失。

运行这段代码,点击“隐藏”,标题会消失;点击“显示”,标题会重新出现。这只是一个如何通过JavaScript修改CSS的简单示例。

总结

本节课中,我们一起学习了使用JavaScript动态操作DOM的核心技术。我们从修改现有元素的innerHTML开始,然后学习了如何创建全新的DOM元素(createElement)并将其添加到文档中(appendChild)。最后,我们探索了如何通过JavaScript直接操作元素的style属性来控制CSS,从而实现如显示/隐藏这样的动态视觉效果。这些技术是构建交互式网页应用的基础。

027:浏览器中的JavaScript事件处理

在本节课中,我们将学习浏览器中的JavaScript事件处理机制。我们将了解什么是事件、如何注册事件监听器,以及一些常见的事件类型,如点击事件、窗口调整大小事件和页面内容加载完成事件。

事件处理概述

上一节我们介绍了JavaScript的基本概念。本节中,我们来看看浏览器中的事件处理。事件是浏览器中发生的特定动作,例如用户点击按钮、调整窗口大小或页面加载完成。JavaScript允许我们“监听”这些事件,并在事件发生时执行特定的代码。

事件注册的两种方式

在JavaScript中,有两种主要方式可以为HTML元素注册事件处理程序。

使用 onclick 属性

第一种方式是使用HTML元素的 onclick 属性。这是一种快捷方式,直接在HTML标签内指定当点击事件发生时要调用的函数。

<a href="#" onclick="myFunc()">点击我</a>

当您使用浏览器开发者工具检查此元素时,会看到它有一个关联的点击事件。

使用 addEventListener 方法

第二种方式是使用更通用的 addEventListener 方法。这种方法允许我们通过JavaScript代码动态地注册事件监听器。

以下是实现相同功能的步骤:

  1. 在HTML中为元素设置一个ID,以便在JavaScript中能够找到它。
    <a href="#" id="zap">点击我</a>
    
  2. 在JavaScript中,首先定义一个函数。
    function myFunc() {
        // 要执行的代码
    }
    
  3. 获取该HTML元素。
    let element = document.getElementById('zap');
    
  4. 为该元素添加事件监听器。
    element.addEventListener('click', myFunc);
    

这两种方法在功能上是等效的。onclick 属性可以看作是 addEventListener 的一个快捷方式。使用 addEventListener 的优势在于它更灵活,可以处理多种类型的事件,并且允许为同一个事件添加多个处理函数。

其他类型的事件

除了点击事件,浏览器还支持许多其他类型的事件。让我们看看两个重要的例子。

窗口调整大小事件

当用户调整浏览器窗口大小时,会触发 resize 事件。这对于实现响应式网页设计非常重要,因为您可以根据窗口尺寸动态调整页面布局。

以下是注册窗口调整大小事件监听器的代码:

function myFunc() {
    console.log('窗口高度:' + window.innerHeight);
    console.log('窗口宽度:' + window.innerWidth);
}

window.addEventListener('resize', myFunc);

当您运行此代码并调整浏览器窗口时,控制台会不断输出当前窗口的高度和宽度。像Bootstrap这样的框架内部就使用了这个事件来适配不同尺寸的屏幕。

页面内容加载完成事件

现代网页通常包含HTML、CSS、JavaScript和图片等多种资源。浏览器需要时间加载和解析所有这些资源。DOMContentLoaded 事件会在初始HTML文档被完全加载和解析后触发,而无需等待样式表、图像和子框架的完全加载。

如果您需要在页面所有元素都就绪后再执行JavaScript操作(例如,操作某个特定的图片或DOM元素),就应该监听这个事件。

以下是注册该事件监听器的代码:

function myFunc() {
    console.log('DOM内容已加载完成!');
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/38df08beafe7dd347a4e0950e92c58da_26.png)

document.addEventListener('DOMContentLoaded', myFunc);

通常,包含此监听器的 <script> 标签会放在HTML文档的 <body> 末尾,以确保它不会阻塞页面的渲染。

总结

本节课中我们一起学习了浏览器中的JavaScript事件处理。我们了解了事件的基本概念,比较了使用 onclick 属性和 addEventListener 方法注册事件监听器的区别。我们还探讨了 clickresizeDOMContentLoaded 等常见事件类型的应用场景。掌握事件处理是创建交互式网页应用的基础。

028:使用Fetch进行JavaScript网络请求 🚀

在本节课中,我们将要学习JavaScript中的网络事件处理,特别是如何使用fetch函数进行异步网络请求。我们将了解为什么网络操作必须是异步的,以及如何使用Promise链式调用来处理请求和响应。

概述

上一节我们介绍了浏览器中的各种事件处理。本节中我们来看看JavaScript如何处理网络请求。由于网络操作耗时,浏览器不能因此“冻结”,所以我们必须使用异步编程模式。

网络请求的异步性

在JavaScript中执行网络操作时,整个过程必须是异步的。代码不能暂停等待服务器响应,否则浏览器界面会失去响应。

因此,我们需要使用事件驱动的方式,将请求过程分解为多个步骤并串联起来。我们发起请求,然后等待“请求完成”事件,接着在数据接收过程中和全部接收完毕后处理响应。这是一个多步骤过程,每一步都触发下一步,但代码本身从不“等待”,它总是响应一个事件,然后触发下一个事件。

使用Fetch函数

以下是一个使用fetch函数的示例代码(文件:14_fetch.htm):

fetch('secret.txt')

fetch是一个内置函数,其参数是一个URL(本例中为secret.txt``)。fetch`函数返回一个Promise对象。

理解Promise

一个Promise代表一个尚未完成但将来会完成的操作。你可以使用.then()方法来指定当Promise完成(即“兑现”)时要运行的代码。

fetch('secret.txt')
  .then(function(response) {
    console.log(response);
    return response.text();
  })

在这段代码中,.then()方法内的函数接收一个参数(这里命名为response)。我们首先在控制台记录响应对象,然后调用response.text()

response.text()方法本身也返回一个Promise,因为它需要时间去从服务器获取文本内容。因此,第一个.then()的返回值是另一个Promise。

链式调用Promise

这就是为什么你会看到第二个.then()

fetch('secret.txt')
  .then(function(response) {
    console.log(response);
    return response.text();
  })
  .then(function(textString) {
    console.log(textString);
  });

第二个.then()在文本内容被成功获取后执行。此时,服务器返回的文本字符串(例如“42”)会作为参数(textString)传递给这个函数,我们将其记录到控制台。

整个过程包含三个快速步骤:

  1. 启动fetch请求(返回Promise A)。
  2. Promise A兑现,运行第一个.then()中的代码,并返回response.text()产生的Promise B。
  3. Promise B兑现,运行第二个.then()中的代码,处理最终的文本数据。

每一步都是瞬时或尽可能快地执行,代码中没有“等待”,我们只是注册函数来响应每个阶段的完成事件。

解析执行过程

你可以观察这个过程:fetch启动,第一个Promise兑现。控制台输出的response对象显示状态码200(表示成功),并且bodyUsed: false,这表示请求已发出且看起来顺利,但响应体(数据)尚未被读取。

当我们调用response.text()时,它返回一个新的Promise。此时,在第一个.then()中执行console.log(response)时,文本数据其实还未从网络到达。

这个新Promise将在文本数据实际接收完成后(可能需要几秒钟)兑现。届时,JavaScript会“回调”我们,兑现这个Promise并将文本字符串传递进来,这才是我们能够打印出服务器返回内容(例如“42”)的时刻。

更简洁的写法与DOM操作

我们可以让代码更简洁,并利用获取的数据做些实际工作,而不仅仅是打印。以下是更典型的写法(文件:15_fetch.htm):

fetch('secret.txt')
  .then(response => response.text())
  .then(textString => {
    document.getElementById('zap').innerText = textString;
  });

这段代码做了以下事情:

  1. 使用箭头函数response => response.text()简化了第一个.then()的处理。它获取响应并立即调用.text()方法返回下一个Promise。
  2. 第二个.then()接收最终解析出来的文本字符串(参数textString),然后通过document.getElementById('zap')找到ID为“zap”的HTML元素(例如一个<p>标签),并将其内部文本设置为从服务器获取的字符串。

这样,我们就实现了从服务器获取数据,然后动态更新网页文档对象模型(DOM)的内容。

总结

本节课中我们一起学习了JavaScript中的异步网络请求。我们了解到由于浏览器不能阻塞,网络操作必须通过事件和Promise进行异步处理。我们深入探讨了fetchAPI的使用方法,包括如何发起请求,如何通过.then()链式处理返回的Promise,以及如何将获取的数据用于更新网页内容。我们还看到了如何用更简洁的箭头函数语法来书写这些操作。这为后续学习更复杂的数据交换格式(如JSON)奠定了基础。

至此,我们已经快速浏览了浏览器中JavaScript的核心工作机制:我们讨论了文档对象模型(DOM)、浏览器窗口、UI事件处理程序(无论是通过onclick属性还是addEventListener方法添加)、DOM事件(如DOMContentLoaded)、窗口事件(如resize)。我们学会了如何修改标签、添加新标签、更改CSS样式,甚至如何从JavaScript发起网络请求。

029:使用Web Components创建自定义HTML标签 🏷️

在本节课中,我们将学习如何创建和使用自定义的HTML标签,即Web Components。这能帮助我们避免代码重复,并构建更模块化、响应式的用户界面。

概述

程序员不喜欢重复自己。从学习编写软件的第三周起,核心原则就是“不要重复自己”(DRY)。重复代码,尤其是包含微小差异的代码,容易导致错误。如果代码中存在一个错误,并且你复制粘贴了40次,那么就需要在40个地方进行相同的修改,这非常令人沮丧。因此,软件设计的一个重要部分就是如何避免重复。

我们之前一直在服务器端进行模板渲染。例如,在Django的视图函数中,我们使用render函数,它接收一个模板和一个上下文字典,生成HTML后发送给浏览器。这是一种经典的请求-响应循环,属于模型-视图-控制器模式。

现在,我们将把一小部分模板处理功能移到浏览器中。我们仍然会有服务器端的模板,但也会在浏览器中拥有一个微小的模板。

创建自定义标签

我们通过创建自定义标签来实现这个目标。例如,我们可以定义一个名为<dj4e-greeting>的标签。这是一个我们自己发明的标签。

我们可以向这个标签传递参数(属性),并定义这些属性的行为。我们还可以为标签设置ID以便操作。下面是一个示例,其中包含一个按钮,点击后会动态改变标签的属性。

<dj4e-greeting id="greet" name="First"></dj4e-greeting>
<button onclick="change()">Click Me</button>
<script type="module" src="./16_dj4e_greet.js"></script>

<script>标签引入的JavaScript代码会将<dj4e-greeting>这个自定义标签添加到浏览器中。一旦导入,我们就可以使用这个标签了。另一个小脚本则用于动态更改标签的name属性。

理解Web Components的实现

自定义元素的概念在浏览器中已经存在一段时间了。虽然你可以直接编写自定义元素,但过程会比较冗长。因此,我们使用一个名为lit-element的小型库,它基于面向对象编程,提供了许多基础功能,让我们无需重复编写样板代码。

我们通过扩展lit-element来创建一个名为SimpleGreeting的类。在构造函数中,我们调用父类的构造函数并设置默认值。这里涉及一个“影子DOM”的概念,它允许你选择是否从外部文档继承CSS样式,或者在自定义元素内部拥有自己的CSS。在本例中,我们选择不隔离标记,让元素继承外部文档的样式。

以下是SimpleGreeting类的核心代码:

import { LitElement, html } from './lit-element.js';

class SimpleGreeting extends LitElement {
  static get properties() {
    return {
      name: { type: String },
    };
  }
  constructor() {
    super();
    this.name = 'Somebody';
  }
  createRenderRoot() {
    return this; // 不使用影子DOM,直接渲染到元素本身
  }
  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}
customElements.define('dj4e-greeting', SimpleGreeting);

代码的核心是render方法。它使用模板字符串(反引号 ` 定义)来生成HTML。${this.name}部分是一个JavaScript表达式,它会将this.name属性的值插入到模板中。这个模板是响应式的:如果<dj4e-greeting>标签的name属性发生变化,浏览器中的文本会自动重新渲染。

与其他框架的关系

这种模式可能让你联想到React、Vue、Svelte等框架,它们都提供了自定义组件的生态系统。lit-element更像是一种基础能力,而非一个庞大的框架生态系统。它直接利用了浏览器的原生功能,使用起来相对简单直接。

从趋势上看,模板处理,乃至整个视图和控制器逻辑,都在向浏览器端迁移。在最极端的情况下,控制器和视图完全在浏览器中运行,模型则部分在浏览器、部分在服务器中。持久化数据仍然存储在服务器端,通过JSON和Web服务(如REST API)与浏览器前端进行数据交换。这使得应用能够非常响应迅速。我们将在接下来的课程中学习如何通过JSON在服务器和这些自定义标签之间传递数据。

本节总结

在本节中,我们探讨了JavaScript与浏览器文档对象模型,包括窗口对象、添加事件处理器、操作DOM数据、扩展DOM、控制CSS显示隐藏、使用fetch进行网络请求,最后学习了如何创建Web Components。当我们开始在前端JavaScript和服务器之间来回传递模型数据时,这一切会变得非常有趣和强大。


本节课中我们一起学习了:如何利用Web Components创建自定义HTML标签来避免代码重复,初步了解了lit-element库的使用,以及浏览器端模板渲染如何使应用更加模块化和响应式。

030:面对面办公时间-马萨诸塞州波士顿

概述

在本节课中,我们将跟随课程讲师查尔斯·塞弗伦斯博士,一同走进位于马萨诸塞州波士顿的“Atlantic Beer Garden”线下办公现场。您将看到课程的学生们分享他们学习Python和Django的经历与感受。


我正在拍摄视频,这个画面可能不太理想。所以,你现在看到的是我正在拍摄视频的场景。

大家好,我们现在在波士顿的Atlantic Beer Garden。我们刚刚进行了一场关于互联网历史、技术、安全以及“全民编程”Python课程的精彩讨论。现在,我想向您介绍班上的几位学生,让他们向大家打个招呼。大家不必说全名,只用名字即可。

以下是参与本次线下活动的学生自我介绍:

  • 约翰:大家好,我是约翰。
  • 凯莉:嗨,我是凯莉。
  • 肖恩:嗨,我是肖恩。
  • 莫沙:嗨,我是莫沙,你可以对镜头说点什么。……这是我目前上过的最好的课程之一。
  • 陈健:大家好,我是陈健,很高兴认识大家。
  • 怀亚特·杰克逊:我叫怀亚特·杰克逊,来自Bigbe Data。这是最棒的课程。
  • 萨米特:课程还在继续,这是我第一次上这门课,感觉很有趣。
  • 亚历克斯:嗨,我是亚历克斯,我只有13岁,我真的很喜欢这门课。
  • 穆罕默德:我是穆罕默德,我来自波斯顿,正在参加这门课程。
  • 乔尔:我是乔尔,我学习Python是为了给我日常使用的开源工具做贡献。
  • 瓦内利:嗨,我叫瓦内利,我在软件行业工作,我非常享受这门课,它非常高效,我也很喜欢查博士。
  • 格蕾丝:嗨,我是格蕾丝,我在去年冬天学习了Python,非常喜欢。
  • 约翰:我是约翰,我和她(格蕾丝)一起。
  • 费伊:嗨,我是费伊,我想学习更多关于Python的知识,我是开源软件的忠实粉丝。

好了,大家都打过招呼了,可以鼓掌或做点别的。向全班同学问好,可以吗?

就这样吧。我不知道我们下一次线下办公时间会在哪里举行,可能要几周之后了。那么,干杯!


总结

本节课中,我们一起走进了查尔斯·塞弗伦斯博士在波士顿举行的线下办公时间。通过学生们的自我介绍,我们看到了这门课程吸引了来自不同背景、不同年龄的学习者,他们都对学习Python和Django抱有极大的热情,并从中获得了乐趣与收获。这体现了“给所有人的编程”这一理念的广泛吸引力与实践成果。

031:多对多关系概述

在本节课中,我们将要学习数据建模中的多对多关系。我们将探讨其概念、为何需要它,以及它与之前学习的一对多关系有何不同。

概述

上一节我们介绍了一对多关系,本节中我们来看看多对多关系。请记住,我们目前正深入探讨Django应用的数据模型部分,主要关注models.py文件。我们也会涉及如何使用Shell查看模型、如何通过迁移更新数据库,以及如何通过模型读写数据。最终,我们会将模型与Django应用的其他部分(如视图、表单、模板)结合起来理解。

多对多关系的识别

在数据建模中,多对多关系是一种不同的结构。回顾上一个建模练习,我只讨论了LanguageBookInstance,而暂时搁置了Genre(体裁)和Author(作者)。这是因为GenreAuthor的关系更为复杂。

考虑以下情况:

  • 一本书可以属于多个体裁(例如,既是科技类,也是儿童类)。
  • 一本书可以有多个作者。
  • 一个作者可以撰写多本书。
  • 一个体裁可以包含多本书。

当你看到用户界面或平面数据表中出现用逗号分隔的列表(例如“科技,儿童”)时,这通常是一个信号,表明此处可能存在多对多关系。这种“逗号模式”本质上是数据重复的一种形式。

为何不能使用一对多

如果这是一对多关系,我们可能会在Book模型中设置一个指向Genre表的外键genre_id。但这意味着每本书只能选择一个体裁,不符合“一本书属于多个体裁”的现实需求。

一种初级的错误想法是:为书籍添加多个外键字段,例如genre_id1genre_id2genre_id3。这种方法存在严重问题:

  • 不灵活:如果一本书有四个作者,这个模型就无法表示。
  • 难以查询:查找某个作者的所有书籍会变得非常复杂。
  • 违反关系型数据库设计原则:它没有正确地建立实体间的“关系”。

对于作者这类数量可能很多(有些书甚至有10位作者)的情况,这种方法的缺陷尤为明显。

多对多关系的解决方案

上述问题将我们引向多对多关系的解决方案。多对多关系通过一个额外的、被称为“连接表”或“关联表”的中间表来实现。这个表存储了两个相关表的主键对,从而优雅地解决了“多个对多个”的关联问题。

在Django中,我们可以使用ManyToManyField字段来简洁地定义这种关系,框架会自动在后台为我们创建和管理这个中间表。

总结

本节课我们一起学习了多对多关系。我们了解了如何通过“逗号分隔列表”的UI模式来识别潜在的多对多关系,分析了一对多关系在处理此类问题时的局限性,并引出了多对多关系的基本概念及其必要性。在接下来的章节中,我们将具体学习如何在Django模型中实现多对多关系。

032:Django中的简单多对多示例

在本节课中,我们将要学习Django中“多对多”关系的基本概念和实现方式。我们将通过一个具体的例子,理解如何通过一个中间表来建立两个模型之间的多对多关联。

概述

多对多关系是指一个模型(例如“作者”)的实例可以关联到另一个模型(例如“图书”)的多个实例,反之亦然。在关系型数据库中,我们无法直接建立多对多关系,因此需要将其拆解为两个“一对多”关系,并通过一个“中间表”(也称为连接表或关联表)来实现。

多对多关系的本质

上一节我们介绍了数据库关系的基本类型,本节中我们来看看多对多关系的具体实现。

在关系型数据库中,我们无法直接建模多对多关系。我们的做法是将其分解为两个“一对多”关系,并创建一个连接表。

你可以称这个表为“作者-图书”表。稍后,我会称它为“著作”表。这个表非常简短,它只记录“这位作者著作了这本书”、“那位作者著作了那本书”这样的信息。通过这个表,我们可以查询某本书的所有作者,或者某位作者的所有著作。

我们称其为连接表、关联表或中间表。在Django术语中,我们称之为“through”表。它的目的是建立两个向外的“一对多”关系,这本质上就是我们捕获多对多关系的方式。

这就是为什么我说:一对多 + 一对多 = 多对多

本地图书馆应用中的多对多关系

在我们的本地图书馆应用中,存在两个多对多关系。

  • 一本书可以有多位作者,一位作者可以有多本书。我们用 1..* 来表示这种关系。
  • 同样,一个流派可以与多本书关联。有趣的是,从书到流派的关系是 0..*,这意味着一本书可以没有流派。

但一本书必须至少有一位作者。这就是 0..*1..* 之间的微妙区别。这些数据模型可以巧妙地捕捉到这类应用功能,我们稍后会看到它如何转化为Django模型,并成为数据的业务规则。

多对多关系的实现机制

那么,这是如何运作的呢?我们将创建两个表:Book 表和 Author 表。每个表的每一行都会有一个自增的ID主键列。

然后,我们将从“著作”表建立向外的外键。注意,“著作”表是箭头的起点,这意味着它内部包含外键列,而箭头的目标则是另一个表中的主键。

Django允许我们创建一种虚拟属性。我们将巧妙地命名它们。名称并不重要,但本质上,在 Author 模型内部,我们将有一个名为 books 的属性,它会自动填充该作者所写的所有书籍。在 Book 模型中,会有一个名为 authors 的虚拟属性,它实际上是通过读取“著作”表计算得出的,并非直接存储。这是一个包含所有作者的列表。

这是一种便捷的方式,因为Django倾向于隐藏这个中间表。事实上,你可以定义一个模型,甚至不用给这个中间表命名,Django会为你自动生成一个。它仍然存在,并且与这里展示的完全一样。但为了让你更好地理解,我宁愿在我的数据模型中展示这个表。如果你看到其他数据模型没有明确提到这个中间表,而只是有一个多对多字段,并且Django为你自动创建了这个表,请不要感到困惑。我喜欢明确地展示它。

Django模型定义

以下是我们的模型定义:

class Book(models.Model):
    title = models.CharField(max_length=200)
    authors = models.ManyToManyField('Author', through='Authored')

class Author(models.Model):
    name = models.CharField(max_length=100)
    books = models.ManyToManyField('Book', through='Authored')

Book 模型中,title 是书名,authors 是一个指向 Author 表的多对多字段,其关联的中间表是 Authored

Author 模型中,name 是作者名,books 是一个指向 Book 表的多对多字段,其关联的中间表同样是 Authored

中间表 Authored 在两个模型中都被引用。这些 booksauthors 属性是虚拟的,它们并不直接存储在 BookAuthor 表的行中。当你请求时,例如检索一本书并询问“这本书的作者是谁?”,这些数据会为你动态计算出来。这在某种程度上是一种便利。

同时,这也告诉Django关于外键的信息、外键的存储位置以及这是一个多对多字段的事实,所有这些设置都是为了建立正确的关联。

中间表模型

就像我说的,这里是我称为 Authored 的中间表。我认为这个名字很巧妙。中间表被建模为两个方向的外键。

class Authored(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

这不是一个多对多字段,而是两个外键。我已经命名了它们,并将它们匹配起来。on_delete=models.CASCADE 的含义与之前相同。现在,这只是两个“一对多”关系:中间表有一个指向 Book 的“一对多”关系,和一个指向 Author 的“一对多”关系。

on_delete=models.CASCADE 意味着,如果删除一个作者,我们希望自动删除 Authored 表中所有与该作者对应的条目;同样,如果删除一本书,我们也希望自动删除所有与该书对应的条目。这就是 on_delete=models.CASCADEAuthored 表中的含义。它会清理这个表,如果外部两个表中的条目被删除,这种自动清理会为我们完成,这非常方便。我们只需通过这个 ondelete 语句来请求它。

这真是太棒了。如此纯粹和简单。我不喜欢不把 Authored 表放在模型里的做法,因为虽然你可以那样做,然后让Django自动创建 Authored 表,但某些功能可能无法很好地工作,我不喜欢那样。

数据库迁移

假设我们创建了这个 models.py 文件。当然,如果你已经检出代码并运行了 makemigrations,它已经运行过了。但假设这是你第一次运行 makemigrations,你会看到它正在创建迁移文件。然后你运行 migrate,正如我们在之前的讲座中讨论过的,migrate 命令会根据迁移文件更新数据库。

makemigrationsmigrate 都会检查哪些迁移已经执行过。makemigrations 寻找 models.py 文件中的更改或差异,而 migrate 则寻找迁移文件中的差异。

再次记住,这两个命令所查看的内容列表都基于 settings.py 文件中的 INSTALLED_APPS 变量。我之所以加入这个说明,是因为学生们总是说“我的 makemigrations 什么都没做”。这通常有两个原因:要么它不在 settings.py 文件中,要么它已经执行过了。如果你运行 makemigrations 时它没有执行任何操作,可以从这两个方面检查。

在Shell中操作多对多关系

以下是一些我们可以在Django shell中进行的操作。同样,使用 python3 manage.py shell 命令,你需要在包含 manage.py 的文件夹中运行它,也就是项目文件夹。项目可以包含多个应用。

因为 manage.py shell 启动时会预加载所有在 settings.py 中注册的应用,读取它们的模型和其他一些东西,所以第一行代码 book_many.models 就已经存在了。这是因为Django在某种意义上已经“醒来”并预加载了所有存在的类和东西。所以,我们是在一个Django应用中,而不仅仅是在一个Python shell里。

我们导入 book_many 作为应用名,其模型是 BookAuthorAuthored

以下操作开始变得熟悉起来:

# 导入模型
from book_many.models import Book, Author, Authored

# 创建书籍对象
b1 = Book(title='Networking')
b1.save()
b2 = Book(title='Raspberry Pi')
b2.save()

# 创建作者对象
a1 = Author(name='Kristen')
a1.save()
a2 = Author(name='Severance')
a2.save()

此时,我们创建了两位作者和两本书,但现在我们需要将它们关联起来。本质上,我们将创建一个 Authored 记录,即 Authored 表中的一条记录,将书 b1 与作者 a2 连接起来并保存。然后,将书 b2 与作者 a1 连接起来并保存。再将书 b2 与作者 a2 连接起来并保存。这样,我们就向 Authored 表中插入了三条记录,建立了这些连接。

这就像读取 a1a2b1b2 的ID字段,然后将记录插入到 Authored 表的 book_idauthor_id 列中。

记住,在 Book 模型中,我们有一个名为 authors 的虚拟集合;在 Author 模型中,有一个名为 books 的虚拟集合。这些并不是实际存储在 Book 表或 Author 表中的数据,而是通过读取 Authored 表中的相应条目生成的。

所以,如果我们查询 b1.authors,这就像访问那个虚拟属性。它会从 Authored 表中读取所有对应的作者,然后 .values() 方法表示实际去检索它们。

# 查询书籍的作者
print(b1.authors.values()) # 输出: [{'id': 2, 'name': 'Severance'}]
print(b2.authors.values()) # 输出: [{'id': 1, 'name': 'Kristen'}, {'id': 2, 'name': 'Severance'}]

# 查询作者的著作
print(a1.books.values()) # 输出: [{'id': 2, 'title': 'Raspberry Pi'}]
print(a2.books.values()) # 输出: [{'id': 1, 'title': 'Networking'}, {'id': 2, 'title': 'Raspberry Pi'}]

现在我们可以看到,书1的作者列表只有一位作者(Severance)。书2的作者列表包含Kristen和我。作者1(Kristen)写了哪本书?她写了《Raspberry Pi》这本书。我们合著了那本书。作者2(我)写了哪本书?他独自写了《Networking》这本书,并且与Kristen合著了另一本书。

你还可以做很多其他事情,但你已经基本理解了:你不需要过多地直接操作 Authored 表。一旦一切设置好,你甚至可以在不明确知道 Authored 表的情况下遍历这些关系,因为当我们在创建数据模型并指定 through 表时,就已经预设了这些功能。

因此,through 表在某种程度上是一个有用且神奇的东西,但我们并不需要过多地显式处理它。

总结

本节课中我们一起学习了Django中多对多关系的核心概念。我们了解到,多对多关系是通过一个中间表(through 表)来实现的,该表将两个“一对多”关系连接起来。我们看到了如何在Django模型中定义 ManyToManyField 并指定 through 参数,以及如何在数据库层面通过外键建立这种关联。最后,我们通过Django shell演示了如何创建和查询多对多关系的数据,理解了虚拟属性(如 book.authorsauthor.books)是如何通过中间表动态计算得出的。掌握多对多关系是构建复杂数据模型的关键一步。

033:课程与会员的多对多数据模型 🧑‍🏫

在本节课中,我们将学习一个更复杂的多对多关系实例,它来自一个在线评分系统。我们将探讨如何在“人员”和“课程”之间建立多对多关系,并通过一个“会员”中间表来存储额外的关联信息,例如用户在课程中的角色。


上一节我们介绍了简单的多对多关系模型。本节中,我们来看看一个更贴近实际应用的例子:一个教育系统中的课程与会员关系。

这个数据模型用于追踪学生和教师在课程中的参与情况。一个人可以参加多门课程,一门课程也可以有多个人参与,因此这是一个典型的多对多关系。

为了建立这种关系,我们创建一个名为 Membership 的中间表(或称为“通过表”)。与之前类似,这个表包含两个外键:一个指向 Person 表,另一个指向 Course 表。这样,我们就通过两个“一对多”关系实现了“多对多”的映射。

现在,我们将在这个中间表中添加一些额外的数据,这是本例的关键不同之处。在教学中,一个人在一门课程中可能扮演不同的角色,例如学生、助教或讲师。这个“角色”信息是属于“人员-课程”这个连接本身的属性,而不是单独属于人员或课程。

因此,我们需要在 Membership 表中添加一个 role 字段来记录这个信息。这样,同一个人可以在A课程中是讲师,同时在B课程中是学生。

以下是实现这个模型的代码示例。我们首先定义 PersonCourse 模型,然后定义包含额外字段的 Membership 模型。

from django.db import models

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/16efea47d2ec2815fcf84bcc32918293_9.png)

class Person(models.Model):
    name = models.CharField(max_length=128)
    def __str__(self):
        return self.name

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/16efea47d2ec2815fcf84bcc32918293_11.png)

class Course(models.Model):
    title = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')
    def __str__(self):
        return self.title

class Membership(models.Model):
    # 定义角色选项
    LEARNER = 1
    TEACHING_ASSISTANT = 1000
    INSTRUCTOR = 5000
    ADMIN = 10000
    ROLE_CHOICES = [
        (LEARNER, 'Learner'),
        (TEACHING_ASSISTANT, 'Teaching Assistant'),
        (INSTRUCTOR, 'Instructor'),
        (ADMIN, 'Admin'),
    ]
    
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    # 记录连接创建和更新的时间
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    # 记录人员在此课程中的角色
    role = models.IntegerField(choices=ROLE_CHOICES, default=LEARNER)
    
    def __str__(self):
        return f"{self.person.name} in {self.course.title} as {self.get_role_display()}"

代码说明:

  1. PersonCourse 是基础模型。
  2. Course 模型中,我们使用 through='Membership' 参数指定了自定义的中间表。
  3. Membership 模型包含:
    • 指向 PersonCourse 的外键。
    • created_atupdated_at 字段,由Django自动管理。
    • role 字段,使用整数和 choices 参数来定义可选的用户角色。这种方式比直接存储字符串更高效,并且便于在管理界面中显示友好的名称。


了解了模型定义后,我们来看看如何在Django Shell中操作这些数据。以下是创建对象并建立关联的步骤:

  1. 首先,我们进入Django Shell并导入模型。
  2. 然后,创建一个人(Person)对象和一门课程(Course)对象。
  3. 最后,通过创建 Membership 对象来将这个人以特定角色(如讲师)添加到课程中。
# 在 Django Shell 中操作
from many.models import Person, Course, Membership

# 创建一个人和一门课程
p = Person(name="Dr. Chuck")
p.save()
c = Course(title="Python for Everybody")
c.save()

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/16efea47d2ec2815fcf84bcc32918293_17.png)

# 初始时,课程没有会员
print(c.members.all())  # 输出:<QuerySet []>

# 创建一个会员关系,指定角色为讲师(INSTRUCTOR,对应值5000)
m = Membership(person=p, course=c, role=Membership.INSTRUCTOR)
m.save()

# 现在,课程有了会员
print(c.members.all())  # 输出:<QuerySet [<Person: Dr. Chuck>]>
# 也可以查询这个人参加了哪些课程
print(p.course_set.all()) # 输出:<QuerySet [<Course: Python for Everybody>]>
# 查看会员详情,包括角色
print(m.get_role_display()) # 输出:Instructor

本节课中我们一起学习了如何构建一个带有额外信息的复杂多对多关系模型。我们通过创建 Membership 这个中间表,不仅连接了“人员”和“课程”,还成功地存储了“角色”这一关联属性。这种方法在需要记录关系本身元数据的场景中非常实用,例如社交网络的好友关系、订单与商品的关系等。掌握这种模式将帮助你设计出更强大、更灵活的Django数据模型。

034:面对面办公时间-英国布莱切利园

概述

在本节课中,我们将跟随课程团队,走进位于英国布莱切利园的特别办公时间现场。这里不仅是计算机科学、互联网历史和计算技术的发源地之一,也是一次特别的“同学会”。我们将通过这次活动,了解来自世界各地的学习者,并感受这个历史地点与编程学习的独特联系。

课程内容

大家好,我们来到了布莱切利园。计算机科学、互联网历史、技术和所有计算的起点就在这里,布莱切利园。我们举办了一次非常特别的、创纪录的办公时间活动,我想让大家认识一下你们的同学们。那么,我们开始吧。

以下是参与本次活动的同学和导师的自我介绍。

大家好,我是史蒂夫。我也是,但你应该说你是个特别的人,你不只是史蒂夫。我是Python课程所有课程的导师之一。欢迎你,史蒂夫。多年来能与你共事是我的荣幸。史蒂夫是那个做所有艰苦工作的人,而我得到了所有的赞誉。像史蒂夫和其他导师这样的人,他们实际上做了艰苦的工作。所以谢谢你,史蒂夫。非常感谢你。谢谢邀请我来,在你的帮助下,这太棒了。

大家好,我是邦瓦尔,来自法国,现在住在伦敦。很高兴来到这里。查克博士,很高兴见到你。我上过互联网历史课程,我非常喜欢它。我真的很想上关于树莓派的下一个课程。别担心,这是广角镜头,所以我并不是真的在拍你的鼻子。

我是克雷格,我正在学习Python网络数据课程,网上见。当那个人过来跟你说话时,你有遇到麻烦吗?真的吗?不过你没事,他们让你回来了。

我是史蒂夫,我上完了第一个Python课程,很快就要开始学习其他一些课程了。我发现它对于我入门这门语言真的非常非常有帮助,非常感谢。

大家好,我是珍妮,我是一名软件开发人员。我使用“Python for Everybody”课程在泰国清迈的一所国际学校教授编程课。很高兴来到英国布莱切利园。你知道那本书被翻译成中文了吗?这在泰国好用吗?不太好用。好吧,所以我想我还没那么酷。

你好,我是来自法国的阿尔诺,我在我的大学担任Arduino讲师。因为上了Python课,我忘记了Arduino的语法,这在我的工作中很尴尬。我的意思是Processing。我的意思是。你不再用Processing了,用Python代替Processing。不,我用Arduino,我应该在大学里做调试工作。到处都是游泳马拉松,然后我就在Python里把它忘了。欢迎你,你是从巴黎来的吗?是的。你从巴黎来这里。是的。我想是坐欧洲之星来的吧,不像从澳大利亚来那么远,对吧?我想没人能超过我。不,不,我不是说“不,不”,我是说你说得对,欧洲之星巴黎比我们想象的近。

我是凯文,我只从伦敦来。但能听到科·萨拉是如何组织课程以及你如何制作课程的,真的很好,谢谢你。谢谢你能来。

大家好,我是J,我是来自中国的苏。你们是学生吗?是的,我们目前在英国工作。很高兴在布莱切利园见到你们,Python课程很棒。

你好,我是扬尼斯。我使用Python很长时间了,但这是我第一次参加课程,只是为了正确地看看这门语言。谢谢你。

你好,我是伊莎贝尔。我用你的课程来复习我的计算机科学知识。是的,我正在跟你学习Python。我来布莱切利园至少五次了。就像你一样,我也是个粉丝,就像我一样。我本希望能在二战期间在这里工作。那会多么令人兴奋。是的,你知道,这里有三门课程的人都是女性。是的,非常令人印象深刻。你没有他们中最有趣的工作。但正是如此。

大家好,我是帕特里克,我上过互联网历史课程。今天对我来说是非常激动人心的一天,因为当我上那门课时,我说我想去布莱切利园,我今天就在这里了。我还说我想参加一次查克博士的办公时间,所以我今天一次就做到了两件事,这真是太棒了。我的一个想法是,这是我们的同学聚会,我有点在想,嘿,我们开个同学会吧。

大家好,我是大卫,我上过你的Python课程和互联网历史课程,非常喜欢,欢迎你,很高兴你在这里。

大家好,我是保罗,我上过Python课程,它们很棒,谢谢你。

大家好,我是来自剑桥的休,我上过你的Python课程,目前正在学习网络课程。非常兴奋能和你一起在这里,并且尝试教孩子们,所以我实际上是在学习它,因为我儿子为了考试正在学Python,所以我希望能帮助他。你离树莓派总部只有几个街区远,对吧?我们在剑桥,他们也在那里做Python相关的事情,规模很大。是的,我想以某种方式把所有这些联系起来,我想到了一个绝妙的计划。

大家好,我是罗杰。我上完了“Python for Everybody”课程,刚刚开始网络课程,我真的很期待能顺利完成它。

大家好,我是达里乌斯,我最初来自波兰,现在住在英国。我刚刚开始我的Python之旅,我应该说非常感谢你为这一切所做的一切。真的,真的很好。那么,问题是:你知道布莱切利园的历史和三位波兰密码学家吗?哦,非常厉害。但是,这里的历史学家们做得非常好,把这一点说得很清楚。这引出了我的下一个问题:你打算来参加办公时间吗?我有很多计划,我会去很多地方,所以也许我们会想办法解决。还有一件事。好吧,向我的妻子和两个女儿问好。

大家好,我是麦迪,我住在伦敦北部。我上了第一门Python课程,这样我就能跟上我小儿子在学校学习的进度。很高兴见到你。

你好,我叫安德鲁,我是个退休人员,只是为了兴趣而学习Python课程。我希望能把它和我用树莓派做的一些小项目结合起来。但我认为Python课程是一门引人入胜的课程。对于一个退休人员来说,你确实很年轻。离70岁还远着呢。

你好,我是马达姆。我和我的家人一起来的,我刚刚开始学习Python。我原本是学生物学的。对,工作,后来我想教我女儿编程。是的,在某个时候,那会变得很正常。是的,就在下一代密码破译者中。确切地说,重要的是把他们带到这个起点。在未来,情况会好得多。我们打破了这片土地,它会一直保持被打破的状态。当然。

你好,我是爱丽丝。我住在布里斯托尔。布里斯托尔。那很远。在那个方向。有几个小时车程。我几个月前上了Python课程,我打算上你的新课程,你推出了很棒的课程3。它们很好,我试过其他一些课程……我不得不把那部分剪掉。很高兴在现实中见到你。我是一名分析师,我希望能在工作中运用我学到的Python知识,我会跟着你学完顶点课程。希望到我们开始顶点课程时,我能想出办法。

我是大卫,来自利物浦。我有兴趣学习Python来提高我的编程技能,我自己正在学习成为一名技术专家。

我是辛西娅,我正在上你的Python课程,这门课非常出色。我强烈推荐给任何想学习编程的人。我现在正处于职业间歇期,在抚养孩子。这对我保持理智非常有帮助。这实际上是一个非常重要的应用场景。但是,你的职业与核反应堆或隐形飞机有关吗?不,是做一点质谱分析。哦,质谱分析。不管怎样,我知道没有计算部分它就很复杂。完全没用。质谱分析是一个大海捞针的问题,如果没有编程,我们根本无法理解数据,我以前从来不知道怎么做,我希望当我回去工作时,我实际上能知道自己在做什么。太好了。这是度过职业间歇期的一个好方法。

大家好,我叫奥芬,来自伦敦。我上过互联网历史技术课程,我期待着学习Python。很好,很好。你想成为……你在躲吗?只是我的妻子。好吧,好吧,你是摄影师。那么,我们会给你们所有人拍一张广角照片。为你们的出色、主动性和……给自己一轮热烈的掌声。

总结

本节课中,我们一起在布莱切利园这个具有历史意义的地点,进行了一次特别的面对面办公时间活动。我们见到了来自世界各地的课程学习者和贡献者,听到了他们学习Python、计算机历史和技术的个人故事与动机。这次活动不仅是一次知识的交流,更是一次跨越地域的“同学会”,体现了在线学习社区的活力与连接。

035:部署Django应用

在本节课中,我们将学习如何将开发完成的Django应用部署到生产环境,使其能够通过互联网被用户访问。部署是Web应用开发的最后关键一步,涉及服务器配置、静态文件处理、数据库设置和安全考量等多个方面。

准备工作

上一节我们介绍了Django应用开发的核心流程。本节中我们来看看部署前需要完成的准备工作。

部署一个Django应用到生产环境前,你需要确保以下几点:

  • 代码就绪:你的应用在本地开发环境中运行正常,所有功能均已测试。
  • 选择托管服务:你需要一个服务器来运行你的应用。这可以是一台虚拟私有服务器(VPS),如DigitalOcean、Linode或AWS EC2实例,也可以是平台即服务(PaaS),如Heroku、PythonAnywhere或Railway。
  • 生产环境配置:Django的生产环境设置与开发环境不同,需要调整设置以确保安全性和性能。

核心部署步骤

以下是部署Django应用的主要步骤概述。

  1. 配置生产设置:在 settings.py 中,你需要将 DEBUG 设置为 False,并正确配置 ALLOWED_HOSTS 列表,包含你的域名或服务器IP地址。
    DEBUG = False
    ALLOWED_HOSTS = [‘yourdomain.com‘, ‘www.yourdomain.com‘, ‘服务器IP地址‘]
    
  2. 设置静态文件:使用 python manage.py collectstatic 命令收集所有静态文件(CSS, JavaScript, 图片)到一个目录,并通过Web服务器(如Nginx)或云存储服务(如AWS S3)来提供这些文件。
  3. 配置数据库:生产环境通常使用更健壮的数据库,如PostgreSQL或MySQL,而不是默认的SQLite。你需要在 settings.py 中更新 DATABASES 配置。
  4. 选择WSGI服务器:Django本身不直接处理HTTP请求。你需要一个WSGI服务器,如Gunicorn或uWSGI,来运行你的Django应用。
  5. 设置Web服务器:使用一个Web服务器,如Nginx或Apache,作为反向代理。它接收用户的HTTP请求,将其转发给Gunicorn/uWSGI处理,并负责提供静态文件和处理SSL加密(HTTPS)。
  6. 配置域名与SSL:将你的域名指向服务器IP地址,并为域名安装SSL证书(例如使用Let‘s Encrypt的免费证书),以启用HTTPS,保证数据传输安全。

一个简单的部署示例(以Gunicorn + Nginx为例)

了解了基本步骤后,我们来看一个使用Gunicorn和Nginx的常见部署方案的具体操作流程。

  1. 在服务器上安装依赖:通过SSH连接到你的服务器,安装Python、pip、你的项目依赖(通常通过 requirements.txt)、PostgreSQL、Nginx和Gunicorn。
  2. 上传项目代码:使用Git或FTP等工具将你的Django项目代码上传到服务器。
  3. 配置Gunicorn:创建一个系统服务文件(如 gunicorn.service)来管理Gunicorn进程,确保应用在服务器启动时自动运行。
  4. 配置Nginx:编辑Nginx的站点配置文件,设置反向代理规则,将动态请求转发给Gunicorn,并直接提供 collectstatic 收集的静态文件。
    # Nginx配置示例片段
    location /static/ {
        alias /path/to/your/staticfiles/;
    }
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
  5. 应用配置并重启服务:运行 sudo systemctl restart gunicornsudo systemctl restart nginx 使所有配置生效。
  6. 测试:在浏览器中输入你的服务器IP地址或域名,检查应用是否正常运行。

部署后的维护

成功部署应用后,维护工作同样重要。

  • 日志监控:定期检查Nginx和Gunicorn的日志文件,以便及时发现和解决问题。
  • 备份:定期备份你的数据库和项目代码。
  • 更新:及时更新服务器操作系统、Python包以及Django本身,以修复安全漏洞。

本节课中我们一起学习了部署Django应用到生产环境的核心概念和基本流程。从准备工作、关键配置到具体的Gunicorn与Nginx部署示例,我们了解到部署是将代码转化为可公开访问服务的关键桥梁。记住,部署是一个需要细心和反复测试的过程,对于初学者,从简单的PaaS平台开始尝试可能更容易上手。

036:JSON - JavaScript对象表示法

概述

在本节课中,我们将学习JSON(JavaScript Object Notation),这是一种轻量级的数据交换格式。我们将了解JSON在Web开发中的作用,特别是它如何作为服务器与浏览器之间传输数据的“线格式”。通过本课,你将理解JSON的基本语法、它与JavaScript对象的关系,以及如何在Django视图中生成和返回JSON数据。


请求-响应循环回顾

上一节我们介绍了传统的Web请求-响应循环:浏览器向服务器发送请求,服务器通过URL路由到视图,视图处理并返回HTML,浏览器解析HTML并更新文档对象模型(DOM)。我们主要使用JavaScript在客户端操作DOM。

本节中,我们将向前迈出重要一步。我们将探讨如何让JavaScript直接向服务器代码发起请求,通过视图和URL获取数据,而不仅仅是获取用于渲染的HTML标记。这里的核心区别在于,我们传输的不再是HTML标记,而是数据。

数据交换格式的演变

Web发展已久,传统的请求-响应循环非常有效。但随着应用复杂化,一个问题出现了:我们能否获取HTML之外的东西?随之而来的争论是:应该使用什么语法来传输数据?

最初,XML(可扩展标记语言)是首选方案。事实上,术语“Ajax”中的“X”就代表XML。然而,在实践中,我们更多时候使用的是JSON,而非XML。因此,我们需要一种各方都同意的“线格式”——一种能在网络传输中使用的通用数据表示法。

什么是序列化与反序列化?

不同编程语言有不同的数据结构:PHP有数组,Python有字典,JavaScript有对象,Java有哈希映射和数组列表等。为了在不同语言间交换数据,我们必须约定一种通用的线格式。

这个过程涉及两个关键步骤:

  1. 序列化:将服务器内部的结构(如Python字典)转换为字符串,以便通过网络传输字符。
  2. 反序列化:在接收端(如浏览器),将接收到的字符串转换回本地的数据结构(如JavaScript对象)。

JSON就是这样一种被广泛接受的线格式。无论使用何种服务器端语言,它们都需要能够将数据序列化为JSON字符串发送,并能够将接收到的JSON字符串反序列化为本地对象。

JSON的起源与语法

JSON的流行很大程度上归功于Douglas Crockford。他并非发明者,但他认识到了这种格式的简洁与实用,并对其进行了规范。JSON语法本质上是JavaScript对象字面量语法的一个子集,它非常简单,以至于许多开发者会觉得它非常自然。

以下是一个JavaScript对象字面量的例子:

const who = {
    "name": "Chuck",
    "age": 29,
    "college": true
};

JSON语法与此非常相似,它规定:

  • 数据以键值对形式存在。
  • 键名必须用双引号包裹。
  • 值可以是字符串、数字、布尔值、数组、对象或null
  • 数据由逗号分隔,花括号{}保存对象,方括号[]保存数组。

JSON与Python字典的语法也高度相似,这使得在Python和JavaScript之间交换数据变得非常直观。

在Django中返回JSON

现在,我们来看看如何在Django视图中生成并返回JSON数据。

假设我们有一个简单的视图函数:

import time
from django.http import JsonResponse

def json_fun(request):
    time.sleep(1)  # 仅为演示,通常不需要暂停
    data = {
        "name": "Chuck",
        "age": 29,
        "college": True
    }
    return JsonResponse(data)

在这个例子中:

  1. 我们创建了一个Python字典 data
  2. 使用Django的 JsonResponse 类来返回这个字典。
  3. JsonResponse 会自动将字典序列化为JSON字符串,并正确设置HTTP响应头(例如 Content-Type: application/json),然后将其发送给浏览器。

当浏览器访问这个端点时,它会收到一个JSON字符串。浏览器开发者工具可以显示原始的JSON字符串,也可以将其解析后以格式化的对象形式展示出来。

总结

本节课我们一起学习了JSON。我们了解到JSON是一种简洁、优雅的数据交换格式,它作为通用的“线格式”,使得不同编程语言(尤其是服务器端的Python和客户端的JavaScript)能够轻松地交换数据。我们回顾了序列化与反序列化的概念,探索了JSON与JavaScript对象字面量的紧密关系,并实践了如何在Django视图中使用 JsonResponse 来返回JSON数据。

JSON的简单性使其成为现代Web开发中异步数据交换(Ajax)的基石。在接下来的课程中,我们将构建一个聊天应用,它将综合运用我们学到的JavaScript、DOM操作以及JSON数据交换技术。

037:JSON与Fetch API详解 🚀

在本节课中,我们将学习如何在Django应用中处理JSON数据,并使用JavaScript的Fetch API从客户端异步获取这些数据。我们将探讨JSON的基本概念、如何在Django视图中返回JSON响应,以及使用Promise和Async/Await两种模式编写健壮的Fetch请求。


概述 📋

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,其语法与JavaScript对象和Python字典高度相似。这使得它在Web开发中,尤其是在前后端分离的架构中,成为理想的数据传输格式。Fetch API是现代浏览器提供的用于发起网络请求的强大接口。本节课我们将结合两者,学习如何从Django后端获取JSON数据并在前端进行处理。


JSON基础:语法与本质 🔍

JSON的本质是一种数据格式语法。它并非只能从服务器动态获取,其基础语法可以直接在JavaScript中定义。

上一节我们概述了课程目标,本节中我们来看看JSON的基本语法。

以下是一个在JavaScript中直接定义对象的例子:

const who = {
    "first": "Chuck",
    "last": "Severance",
    "email": "csev@umich.edu"
};
console.log(who);

这段代码创建了一个名为 who 的JavaScript对象常量并打印它。其语法是:花括号 {} 包裹,内部是一系列 "键": 值 对。值可以是字符串、数字、布尔值、数组,甚至是嵌套的对象。

JSON的巧妙之处在于,我们可以将这种完全相同的语法通过网络进行发送和接收。这使得数据序列化和反序列化变得非常直观。


Django视图:返回JSON响应 🐍

理解了JSON的语法后,我们来看看如何在Django后端生成并返回JSON数据。

Django提供了便捷的工具来创建JSON响应。与返回HTML的普通视图不同,我们需要返回一个明确标明内容类型为JSON的响应。

以下是Django视图返回JSON的示例代码:

from django.http import JsonResponse
import time

def json_fun(request):
    time.sleep(2)  # 模拟延迟,便于观察
    data = {
        'first': 'Chuck',
        'last': 'Severance',
        'email': 'csev@umich.edu'
    }
    return JsonResponse(data)

在这段代码中:

  1. 我们使用 JsonResponse 类。
  2. 向其传递一个Python字典 data
  3. JsonResponse 会自动将字典序列化为JSON字符串,并正确设置HTTP响应头(如 Content-Type: application/json)。

当我们访问这个视图的URL时,浏览器开发者工具的“网络”(Network)选项卡会显示,响应头是 application/json,响应体就是序列化后的JSON文本。浏览器通常会友好地解析并格式化显示这些数据。

一个关键优势是:Python字典的语法与JavaScript对象(JSON)的语法几乎完全相同,这使得数据转换非常自然。


使用Fetch API:Promise模式 ⚡

现在我们已经能从服务器获取JSON了,接下来学习如何在浏览器中使用Fetch API来异步获取这些数据。

Fetch API支持两种主要的编码风格:Promise链式调用和Async/Await。首先,我们来看Promise模式。

Promise模式通过 .then().catch() 方法链式处理异步操作。以下是使用Promise模式发起Fetch请求的示例结构:

fetch(url)
  .then(response => response.json()) // 将响应解析为JSON
  .then(data => {
    console.log('成功获取数据:', data);
    // 在此处处理数据,例如更新UI
  })
  .catch(error => {
    console.error('请求过程中发生错误:', error);
    // 在此处处理错误,例如显示错误信息
  });

这段代码的工作流程是:

  1. fetch(url) 启动一个异步请求,并返回一个Promise。
  2. 第一个 .then() 在请求完成、收到响应后执行,它尝试将响应体解析为JSON(这又是一个返回Promise的操作)。
  3. 第二个 .then() 在JSON解析成功后执行,接收解析出的 data 对象,供我们使用。
  4. .catch() 会捕获链中任何阶段发生的错误。

在实际开发中,我们需要处理各种潜在错误,例如:

  • URL语法错误fetch 调用本身会立即失败。
  • 404 Not Found:服务器返回了响应,但状态码不是成功的2xx系列。
  • 200 OK但内容非JSON:服务器返回了HTML或其他内容,导致 response.json() 解析失败。

Promise模式代码紧凑,适合处理大多数简单的异步场景。


使用Fetch API:Async/Await模式 🧵

除了Promise链,Async/Await提供了另一种更接近同步代码风格的异步处理方式。

Async/Await基于Promise,但允许我们以更线性的方式编写代码。以下是使用Async/Await重写的Fetch示例:

async function loadData(url) {
  try {
    const response = await fetch(url); // 等待fetch完成
    if (!response.ok) { // 检查HTTP状态码是否成功
      throw new Error(`HTTP错误! 状态码: ${response.status}`);
    }
    const data = await response.json(); // 等待JSON解析完成
    console.log('成功获取数据:', data);
    // 在此处处理数据
    return data;
  } catch (error) {
    console.error('无法加载数据:', error);
    // 在此处处理错误
  }
}

这段代码的关键点:

  1. 函数使用 async 关键字声明,表示其内部包含异步操作。
  2. await 关键字可以“暂停”异步函数的执行,直到后面的Promise完成。这并非真正的线程阻塞,而是语法糖,它让引擎将代码分割,在Promise解决后继续执行
  3. try...catch 块可以集中处理所有异步和同步错误。
  4. 我们可以更精细地控制流程,例如在获取响应后、解析JSON前,检查 response.okresponse.status,从而区分“404错误”和“返回了非JSON内容”等不同情况。

Async/Await模式代码结构更清晰,错误处理更集中,尤其在处理复杂异步逻辑时能提供更好的控制力。


总结 🎯

本节课中我们一起学习了:

  1. JSON的本质:一种与JavaScript对象和Python字典语法相似的数据交换格式。
  2. Django返回JSON:使用 JsonResponse 可以轻松将Python字典序列化为JSON并返回给前端。
  3. Fetch API的两种模式
    • Promise模式:通过 .then().catch() 链式调用,代码简洁,适用于大多数场景。
    • Async/Await模式:使用 async/await 关键字,让异步代码看起来像同步代码,流程控制更清晰,错误处理更直接。

两种Fetch编码风格各有优势。Promise链更为简洁和函数式;而Async/Await则更易于阅读和调试,尤其适合复杂的异步逻辑。你可以根据项目需求和个人偏好进行选择。掌握这两种方法将使你能够高效地构建与现代Django后端交互的动态Web前端。

038:JSON、AJAX与聊天应用 🗨️

在本节课中,我们将综合运用JSON与AJAX技术,构建一个简单的多用户聊天应用。该应用将通过服务器传输数据,并使用轮询机制实现实时消息更新。

概述

我们将创建一个基础聊天界面,并使用两个浏览器标签页模拟两个用户。在一个标签页中输入的消息,将通过异步方式自动出现在另一个标签页中。为实现此功能,我们将采用每4秒轮询一次服务器的方式获取最新消息。虽然这种方式效率不高,但它能清晰地展示AJAX与JSON的工作流程。

项目结构解析

上一节我们介绍了JSON与AJAX的基本概念,本节中我们来看看如何将它们整合到一个实际应用中。

URL配置

首先,查看项目的urls.py文件。我们定义了两个主要路径:

# urls.py 示例
urlpatterns = [
    path('talk/', views.talk_main, name='talk'),
    path('messages/', views.messages_json, name='messages'),
]

  • talk/:对应聊天应用的主页面视图。
  • messages/:这是一个JSON端点(API),用于提供最新的聊天消息数据。

数据模型

聊天功能的核心是一个简单的数据模型,用于存储每条消息。

# models.py 示例
from django.db import models

class Message(models.Model):
    text = models.TextField()          # 消息内容
    user = models.CharField(max_length=100) # 发送者
    created_at = models.DateTimeField(auto_now_add=True) # 创建时间

模型包含三个字段:消息文本、发送者以及自动记录的消息创建时间。创建时间将用于计算并显示“X分钟前”这样的友好时间格式。

视图逻辑

接下来,我们分析处理请求的视图(views.py)。

主页面视图 (talk_main)
这个视图很简单,仅负责渲染聊天界面的HTML模板。

消息提交视图 (post_message)
当用户在表单中点击发送按钮时,数据会提交到这个视图。该视图将新消息保存到数据库,然后重定向回主聊天页面。

JSON数据视图 (messages_json)
这是关键视图,它处理来自前端的AJAX请求。以下是它的工作原理:

  1. 接收GET请求。
  2. 从数据库中查询最新的10条消息,按创建时间倒序排列。
  3. 对每条消息数据进行处理,提取我们需要发送给前端的信息(如消息文本和友好时间格式)。
  4. 将处理后的数据列表封装,并使用JsonResponse返回。
# views.py 片段示例 (messages_json视图)
from django.http import JsonResponse
from django.utils.timesince import timesince
from .models import Message

def messages_json(request):
    messages = Message.objects.all().order_by('-created_at')[:10]
    results = []
    for msg in messages:
        # 处理数据,生成前端需要的格式
        result = [msg.text, f"{timesince(msg.created_at)} ago"]
        results.append(result)
    return JsonResponse({'results': results})

前端界面与交互

现在,我们看看用户看到的界面以及背后的JavaScript如何工作。

HTML模板 (talk.html)
模板中包含一个简单的表单用于输入消息,以及一个占位符<div>,其idchat-content。初始时,这个<div>内包含一个加载动画( spinner )。

<!-- talk.html 片段 -->
<form method="post" action="post">
    <input type="text" name="message" placeholder="输入消息">
    <button type="submit">发送</button>
</form>
<div id="chat-content">
    <!-- 初始为加载动画,随后会被JavaScript替换为消息列表 -->
    <img src="spinner.gif" alt="加载中...">
</div>

JavaScript 动态更新
页面加载完成后,JavaScript开始接管,实现动态更新。

以下是实现此功能的核心JavaScript代码逻辑:

  1. 定义更新函数 (updateMessages):这个函数使用fetch() API向/messages/端点发起请求,获取最新的JSON格式消息数据。
  2. 处理响应:通过Promise链处理响应。首先将响应解析为JSON,然后得到消息数据数组。
  3. 更新DOM:清空chat-content div的内容(移除旧的列表或加载动画)。接着,遍历消息数组,为每条消息生成一个包含文本和时间的段落(<p>标签),并将其追加到chat-content div中。
  4. 设置轮询:使用setTimeout函数,让updateMessages函数每4秒(4000毫秒)自动执行一次,从而实现定期检查新消息。
  5. 错误处理:使用.catch()捕获可能的网络或服务器错误,并进行提示。
  6. 初始化:通过监听DOMContentLoaded事件,在页面完全加载后,等待2秒首次执行updateMessages函数(为了让用户能看到初始的加载动画)。

// JavaScript 代码逻辑示例
function updateMessages() {
    fetch('/messages/', {cache: "no-store"})
        .then(response => response.json())
        .then(data => {
            let container = document.getElementById('chat-content');
            container.innerHTML = ''; // 清空当前内容
            // 假设返回的数据格式为 {results: [[消息1, 时间1], [消息2, 时间2], ...]}
            for (let row of data.results) {
                container.innerHTML += `<p>${row[0]} <br>  ${row[1]}</p>`;
            }
            // 设置4秒后再次更新
            setTimeout(updateMessages, 4000);
        })
        .catch(error => {
            alert('获取消息时出错: ' + error);
        });
}

// 页面加载完成后,启动更新流程
document.addEventListener('DOMContentLoaded', function() {
    setTimeout(updateMessages, 2000);
});

工作流程总结

让我们回顾一下整个应用的工作流程:

  1. 用户A在浏览器标签1中打开聊天页面,看到加载动画。
  2. 2秒后,JavaScript启动,向/messages/请求数据,并用返回的消息列表替换加载动画。
  3. 用户A输入消息并提交。表单提交到服务器,消息被保存,页面刷新(重定向),短暂显示加载动画后再次被消息列表替换。
  4. 与此同时,用户B在标签2中打开页面。JavaScript同样每4秒轮询一次/messages/
  5. 当服务器收到用户A的新消息后,用户B的浏览器在下一次轮询(最多4秒后)就会获取到这条新消息,并自动更新其页面上的消息列表,无需手动刷新。

由于DOM操作(清空和重填chat-content)在JavaScript函数中瞬间完成,并且浏览器通常会在函数执行完毕后统一重绘页面,因此用户会感觉更新是瞬间发生的,非常流畅。

本节课总结

本节课中我们一起学习了如何利用JSON和AJAX构建一个动态的聊天应用。我们掌握了以下关键点:

  • JSON作为数据桥梁:服务器使用JsonResponse将数据库中的数据序列化为JSON格式,前端JavaScript再将其解析为可操作的对象。
  • AJAX实现异步通信:使用fetch() API,前端可以在不刷新整个页面的情况下,向服务器请求数据并更新页面的局部内容。
  • 轮询机制:通过setTimeout定期执行AJAX请求,实现了简单的“实时”更新效果。
  • 前后端协作:明确了后端视图(提供数据API)和前端的JavaScript(请求并渲染数据)各自的分工与协作方式。

JSON是一种强大且通用的数据交换格式,得到了几乎所有现代编程语言和平台的优秀支持。在Django和JavaScript中,对JSON的操作都非常简便,这使得它成为构建现代动态Web应用的理想选择。虽然本例中的轮询方式并非最高效的实时通信方案(更高效的方案如WebSocket),但它清晰地展示了异步数据交互的核心原理。

039:JSON聊天示例代码详解 🗨️

在本节课程中,我们将详细解析一个使用Fetch、Ajax和setTimeout的简单聊天应用示例代码。这个示例将多个浏览器端的JavaScript概念整合在一起,演示了如何实现异步通信。

概述

我们将分析一个模拟两个用户在同一系统中聊天的应用。其核心机制是:一个用户发送消息后,另一个用户的屏幕无需手动刷新,就能通过异步请求自动看到新消息。我们将从URL配置、视图函数、前端JavaScript代码以及定时轮询机制等方面,逐步拆解其工作原理。


URL配置与视图

首先,我们来看项目的URL配置和主要的视图函数。

URL配置 (urls.py)

urls.py文件中,我们定义了三个主要端点:

urlpatterns = [
    path('talk/', views.talk, name='talk'),          # 主聊天页面
    path('messages/', views.messages_json, name='messages_json'), # JSON消息端点
    path('slow/', views.slow, name='slow'),          # 用于显示加载动画的端点
]
  • talk/:渲染主聊天页面。
  • messages/:一个返回JSON格式消息列表的API端点。
  • slow/:一个故意延迟的端点,用于演示加载动画。

主视图函数 (views.py)

主视图函数talk处理GET请求,简单地渲染聊天页面模板。

def talk(request):
    return render(request, 'talk/talk.html')

前端页面结构

上一节我们介绍了后端的基本配置,本节中我们来看看前端页面的结构。

聊天页面模板 (talk.html)

talk.html模板包含了聊天应用的基本结构:

  1. 一个用于提交新消息的表单。
  2. 一个用于显示消息列表的<div>容器,其初始内容是一个加载动画(旋转图标)。
  3. 引入关键的JavaScript代码。
<form method="post">
    {% csrf_token %}
    <input type="text" name="message" placeholder="Enter message">
    <button type="submit">Send</button>
</form>

<div id="chat-content">
    <!-- 初始加载动画 -->
    <div class="spinner">Loading...</div>
</div>

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/249a4ce6f6138e185a6c5f82f7a17af5_10.png)

<script src="{% static 'talk/chat.js' %}"></script>

当页面首次加载时,用户会看到这个加载动画。随后,JavaScript代码会通过Ajax请求获取真实数据并替换这个动画。


数据模型与JSON API

为了动态获取消息,我们需要一个提供数据的后端接口。

数据模型 (models.py)

消息数据由一个简单的模型Message管理,包含发送者、消息内容、创建时间等字段。

class Message(models.Model):
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    text = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

JSON视图函数 (messages_json)

messages_json视图负责处理对/messages/的请求,并返回最新的10条消息。

其工作流程如下:

  1. 从数据库查询最新的10条消息。
  2. 循环处理每条消息:对消息文本进行HTML转义(出于安全考虑,在后端完成),并使用naturaltime过滤器格式化时间(如“1分钟前”)。
  3. 将处理好的数据构造成一个数组,并以JSON格式返回。
from django.utils.html import escape
from django.contrib.humanize.templatetags.humanize import naturaltime
import json

def messages_json(request):
    # 获取最新的10条消息
    messages = Message.objects.order_by('-created_at')[:10]
    results = []
    for msg in messages:
        # 后端转义文本,格式化时间
        result = {
            'text': escape(msg.text),
            'time': naturaltime(msg.created_at)
        }
        results.append(result)
    # 返回JSON响应。注意:直接返回数组不是最佳实践,此处为简化示例。
    return JsonResponse(results, safe=False)

关键点:JSON API的作用不仅仅是返回原始模型数据。它经常需要从模型中获取数据,进行加工(如转义、格式化),使其更适合前端显示,然后再以JSON形式返回。


核心JavaScript逻辑

现在,我们进入最核心的部分:前端JavaScript如何与后端交互并动态更新页面。

消息更新函数 (updateMessages)

updateMessages函数是应用的核心,它使用Fetch API异步获取消息数据并更新DOM。

以下是该函数的主要步骤:

function updateMessages() {
    console.log('Requesting JSON...');
    // 1. 发起Fetch请求
    fetch('/messages/')
        .then(response => {
            // 2. 将响应解析为JSON对象
            return response.json();
        })
        .then(rows => {
            // 3. 成功获取数据后,更新DOM
            console.log(rows); // rows现在是一个JavaScript数组
            let contentDiv = document.getElementById('chat-content');
            // 3.1 清空容器(移除加载动画)
            contentDiv.innerHTML = '';
            // 3.2 遍历消息数组,构建HTML并插入
            for (let i = 0; i < rows.length; i++) {
                let msg = rows[i];
                contentDiv.innerHTML +=
                    `<p>${msg.text}<br> ${msg.time}</p>`;
            }
            // 3.3 成功更新后,计划下一次调用(实现轮询)
            setTimeout(updateMessages, 4000);
        })
        .catch(error => {
            // 4. 错误处理:捕获网络或服务器错误
            console.error('Fetch error:', error);
            alert('Failed to retrieve messages. Check console for details.');
        });
}

代码解析

  • fetch('/messages/'):向JSON端点发起GET请求。
  • .then(response => response.json()):第一个Promise解析后,将响应体转换为JavaScript对象(数组)。
  • .then(rows => { ... }):第二个Promise在获得数据后执行。它清空消息容器,然后遍历数据数组,为每条消息生成HTML段落并追加到容器中。
  • setTimeout(updateMessages, 4000);:在成功更新后,设置一个4秒后再次调用updateMessages的定时器,从而实现定期轮询。
  • .catch(...):捕获请求或处理过程中发生的任何错误,并给出提示。

重要概念response.json()方法将接收到的JSON字符串解析为浏览器可以操作的JavaScript对象或数组。


定时轮询与初始化

我们已经看到了单个更新周期如何工作,现在来看看整个应用是如何启动并持续运行的。

页面加载与初始延迟

当页面加载完成后,我们并不立即获取消息,而是等待一个短暂的初始延迟。

// 等待整个DOM加载完毕
document.addEventListener('DOMContentLoaded', function() {
    // 页面加载2秒后,执行第一次消息更新
    setTimeout(updateMessages, 2000);
});
  • DOMContentLoaded事件确保HTML文档完全加载和解析后再执行脚本。
  • 第一个setTimeout(updateMessages, 2000)让加载动画显示2秒,然后才进行第一次数据获取,这提升了用户体验。

轮询机制

应用通过递归调用setTimeout来实现轮询:

  1. 第一次updateMessages调用成功后,会设置一个4秒后的定时器再次调用自己。
  2. 这个过程不断重复,从而每隔大约4秒就自动检查一次新消息。
  3. 注意:错误处理catch块中没有设置新的setTimeout。这意味着如果某次请求失败(如服务器错误、网络断开),轮询会自动停止,防止在错误状态下持续发送无效请求。

关于轮询的说明:虽然本示例使用了简单的定时轮询(每4秒请求一次),但这并不是高效的方式,因为它会消耗不必要的网络和服务器资源。在实际生产环境中,应考虑使用更高级的技术,如WebSockets或Server-Sent Events (SSE),来实现真正的实时通信。本例采用轮询仅是为了以最简单的方式演示异步更新概念。


关键要点与总结

本节课中我们一起学习了如何构建一个简单的异步聊天应用前端。

核心机制总结

  1. 异步数据获取:使用fetch() API从/messages/ JSON端点获取数据,避免整页刷新。
  2. DOM操作:JavaScript在获取数据后,动态清空并重写#chat-content容器内的HTML,实现内容的局部更新。
  3. 定时轮询:通过setTimeout递归调用,定期检查并更新消息。
  4. 错误处理:使用Promise的.catch()方法捕获请求异常,为用户提供反馈并停止错误状态下的轮询。

需要注意的实践

  • 返回JSON数组:本示例中,视图直接返回了一个JSON数组(JsonResponse(results, safe=False))。虽然可行,但更佳实践是返回一个包含状态码和数据等信息的对象,例如:{'status': 'ok', 'messages': results}
  • 轮询的效率:如前所述,定时轮询并非实时应用的最佳选择,仅适用于教学演示。

这个示例虽然简单,但优雅地将异步请求、定时器、DOM操作和错误处理等多个前端概念结合在一起,为理解更复杂的实时Web应用打下了坚实的基础。

040:网站图标(Favicon)实战教程 🎯

在本节课程中,我们将学习如何在Django应用中添加和自定义网站图标(Favicon)。网站图标是显示在浏览器标签页、书签栏和地址栏旁的一个小图标,它是网站品牌形象的一部分。

上一节我们介绍了静态文件的配置,本节中我们来看看如何为网站添加一个专属的图标。

理解Favicon的工作原理

网站图标通常是一个名为 favicon.ico 的文件,放置在网站的根目录或静态文件目录中。浏览器会自动向网站根目录下的 /favicon.ico 路径请求这个图标。

在Django中,我们通过配置静态文件服务来提供这个图标。以下是一个典型的URL配置示例,它使用 static 函数来服务位于 static 目录下的 favicon.ico 文件:

# urls.py 示例片段
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.conf import settings
from django.conf.urls.static import static

urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# 这行配置允许Django在开发服务器上提供 static/favicon.ico 文件

创建并替换自定义Favicon

以下是创建和使用自定义图标的步骤:

  1. 准备图标文件:首先,你需要一个图标文件。你可以使用在线工具(如Favicon Converter)将普通的PNG或JPG图片转换为ICO格式。ICO是一种专用于图标的图像格式。

  2. 替换默认文件:将生成的新 favicon.ico 文件上传到你的Django项目的静态文件目录中,通常路径是 your_app/static/favicon.ico,并覆盖旧文件。

  1. 处理浏览器缓存:浏览器会强力缓存Favicon。即使你更新了服务器上的文件,浏览器可能仍然显示旧的图标。为了解决这个问题,一个有效的方法是在访问图标URL时添加一个查询参数。例如,访问 /favicon.ico?version=3。因为查询参数不是路径的一部分,浏览器会将其视为一个新的资源请求,从而绕过缓存获取新图标。

总结

本节课中我们一起学习了Django网站图标的添加与自定义。核心要点包括:Favicon是网站品牌标识的一部分;通过Django的静态文件系统提供图标;由于浏览器缓存机制,更新图标后可能需要通过添加查询参数(如 ?v=2)来强制刷新。现在,你可以为自己的Django应用创建一个独特的图标了。

041:社交登录代码实战 🚀

在本教程中,我们将学习如何为Django应用集成社交登录功能,具体以GitHub登录为例。我们将使用名为social-auth-app-django的第三方库来实现此功能。

概述

上一节我们介绍了Django应用部署的基础知识。本节中,我们将深入探讨如何实现社交登录功能,让用户能够使用他们的GitHub账户登录我们的应用。这可以简化注册流程,提升用户体验。

配置社交登录框架

首先,我们需要配置Django设置以启用社交登录。在项目设置中,有一段代码会检查社交登录的配置状态。

# 在 settings.py 中,大约第176行附近
try:
    from .github_settings import *
    SOCIAL = True
except:
    SOCIAL = False

这段代码尝试导入一个名为github_settings.py的配置文件。如果该文件不存在,它会将SOCIAL变量设置为False,并在运行管理命令时提示你配置社交登录。

创建配置文件

我们的项目模板中包含一个示例文件github_settings_dist.py。我们需要基于它创建真实的配置文件。

以下是具体步骤:

  1. 在项目设置目录中,创建一个新的空文件,命名为github_settings.py
  2. 打开github_settings_dist.py文件,将其中的所有内容复制。
  3. 将复制的内容粘贴到新建的github_settings.py文件中。

完成此操作后,再次运行manage.py命令时,将不再显示配置提示,因为系统现在可以找到配置文件。

理解登录流程切换

配置完成后,登录流程会发生变化。应用会从使用标准的login.html模板,切换到使用专为社交登录设计的login_social.html模板。

新的模板会检查github_settings.py中是否已正确设置GitHub的API密钥。如果已设置,页面上将显示“使用GitHub登录”的链接。这个链接指向由social-auth-app-django库提供的认证端点。

配置URL路由

为了让社交登录链接生效,我们需要在项目的urls.py文件中添加相应的路由。

# 在 urls.py 中添加
if SOCIAL:
    # 导入社交登录相关的URL配置
    from django.urls import include, path
    urlpatterns += [
        path('social/', include('social_django.urls', namespace='social')),
    ]

这段代码会在SOCIALTrue时,将社交认证的URL包含到我们的项目中。

获取并配置GitHub OAuth密钥

目前我们的github_settings.py文件还没有有效的密钥。要使用GitHub登录,必须在GitHub上注册一个OAuth应用。

以下是操作步骤:

  1. 访问GitHub的开发者设置页面。
  2. 创建一个新的OAuth应用程序。
  3. 在创建应用时,需要填写以下关键信息:
    • 应用名称: 你的应用名称。
    • 主页URL: 你的应用主页,例如 https://samples.dj4e.com
    • 授权回调URL: 这是GitHub在认证后重定向用户回的地址,格式通常为 https://你的域名/oauth/complete/github/
  4. 注册成功后,GitHub会提供一个客户端ID和一个客户端密钥
  5. 将这两个值分别填入github_settings.py文件中的 SOCIAL_AUTH_GITHUB_KEYSOCIAL_AUTH_GITHUB_SECRET 变量。

重要提示:务必保护好你的客户端密钥,不要将其提交到公开的代码仓库。

测试社交登录

完成所有配置后,就可以进行测试了。

  1. 运行 python manage.py check 来验证配置是否正确。
  2. 访问网站的登录页面。现在你应该能看到“使用GitHub登录”的按钮或链接。
  3. 点击该链接,你将被重定向到GitHub的授权页面。
  4. 授权后,GitHub会将你重定向回我们的应用,并自动完成登录。

登录成功后,系统会获取你在GitHub上的公开信息(如用户名、邮箱)。例如,个人资料图片(Gravatar)就会根据关联的邮箱地址显示。

关于其他社交平台

选择GitHub作为示例,是因为其开发者应用注册流程相对简单直接。你也可以用同样的方式集成Twitter、Google或Facebook登录。

然而,这些平台对应用的审核通常更严格,可能需要提供更多信息(如隐私政策、应用图标等)才能通过审核。GitHub是初学者实践社交登录的理想起点。

总结

本节课中我们一起学习了为Django应用集成GitHub社交登录的全过程。我们首先通过创建github_settings.py配置文件来启用社交登录功能,然后配置了URL路由。接着,我们在GitHub开发者平台注册了OAuth应用以获取必要的密钥和密钥。最后,我们完成了测试,验证了用户可以通过GitHub账户成功登录。掌握此方法后,你可以举一反三,为应用添加更多社交登录选项。

042:收藏功能(Favs)示例代码实战 🚀

在本节课中,我们将学习如何实现一个“收藏”功能。这个功能演示了Django中的“多对多”关系,并引入了Ajax技术,以实现无需刷新整个页面的动态交互。我们将从模型设计开始,逐步讲解视图、模板和JavaScript代码,最终构建一个允许用户点击星标来收藏项目的完整应用。

模型设计 🗂️

上一节我们介绍了项目的基本结构,本节中我们来看看核心的数据模型是如何设计的。

模型定义了数据的结构。在这个收藏功能中,我们有两个核心模型:Thing(物品)和Fav(收藏记录)。

  • Thing 模型代表一个可以被收藏的物品。它包含标题、描述等字段,并通过一个owner外键关联到用户,以确定谁拥有(可以编辑/删除)这个物品。
  • Fav 模型是连接用户和物品的中间表,它建立了“多对多”关系。一个用户可以收藏多个物品,一个物品也可以被多个用户收藏。

以下是核心的模型代码:

# favs/models.py
from django.db import models
from django.contrib.auth.models import User

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/93f7b6f482b038f42d521918ac98ff02_17.png)

class Thing(models.Model):
    title = models.CharField(max_length=200)
    text = models.TextField()
    # 物品所有者,用于权限控制
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

class Fav(models.Model):
    # 关联被收藏的物品
    thing = models.ForeignKey(Thing, on_delete=models.CASCADE)
    # 关联执行收藏的用户
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        # 确保同一个用户不能重复收藏同一个物品
        unique_together = ('thing', 'user')

    def __str__(self):
        return f'{self.user.username} likes {self.thing.title}'

关键点解析:

  • Fav.thingFav.user 是两个外键,分别指向 ThingUser 模型。
  • on_delete=models.CASCADE 表示当关联的 ThingUser 被删除时,对应的 Fav 记录也会被自动删除。
  • unique_together = (‘thing‘, ’user‘)Meta 类中的一个约束,它确保数据库中不会出现 (用户A, 物品X) 这样的重复组合,即一个用户只能收藏某个物品一次。

URL配置与视图概览 🔗

定义好数据如何存储后,我们需要定义用户如何访问这些功能。这通过URL配置和视图来完成。

URL配置将特定的网页地址映射到处理该请求的视图函数。以下是本项目的主要URL模式:

# favs/urls.py
from django.urls import path
from . import views

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/93f7b6f482b038f42d521918ac98ff02_31.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/93f7b6f482b038f42d521918ac98ff02_32.png)

urlpatterns = [
    path(‘’, views.ThingListView.as_view(), name=‘thing_list’),
    path(‘thing/<int:pk>’, views.ThingDetailView.as_view(), name=‘thing_detail’),
    path(‘thing/create/’, views.ThingCreateView.as_view(), name=‘thing_create’),
    path(‘thing/<int:pk>/update/’, views.ThingUpdateView.as_view(), name=‘thing_update’),
    path(‘thing/<int:pk>/delete/’, views.ThingDeleteView.as_view(), name=‘thing_delete’),
    # 收藏功能相关的URL
    path(‘thing/<int:pk>/favorite/’, views.AddFavoriteView.as_view(), name=‘thing_favorite’),
    path(‘thing/<int:pk>/unfavorite/’, views.DeleteFavoriteView.as_view(), name=‘thing_unfavorite’),
]

视图分类:

  • 标准CRUD视图ThingListView, ThingDetailView, ThingCreateView, ThingUpdateView, ThingDeleteView。这些视图处理物品的列表展示、详情查看、创建、更新和删除,与之前课程中的示例类似,并通过Owner混合类(Mixin)来确保用户只能操作自己拥有的物品。
  • 收藏功能视图AddFavoriteViewDeleteFavoriteView。这是本节课的新内容,它们专门处理通过Ajax发送的收藏与取消收藏请求。

接下来,我们将深入最复杂的部分——列表视图,它需要为前端提供初始的收藏状态数据。

列表视图:传递收藏状态 📋

列表视图(ThingListView)不仅要列出所有物品,还需要告诉前端模板当前登录用户已经收藏了哪些物品,以便正确显示“已收藏”或“未收藏”的星标。

以下是ThingListView中处理此逻辑的关键部分:

# favs/views.py
class ThingListView(OwnerListView):
    model = Thing
    template_name = ‘favs/thing_list.html’

    def get(self, request):
        # 调用父类方法获取物品列表
        thing_list = Thing.objects.all()
        favorites = [] # 初始化收藏ID列表
        if request.user.is_authenticated:
            # 获取当前用户所有收藏记录的物品ID
            # 使用列表推导式将QuerySet转换为纯ID列表,如 [4, 7]
            favorites = list(request.user.fav_set.values_list(‘thing_id’, flat=True))
            print(favorites) # 调试用,可在控制台查看
        ctx = {‘thing_list’: thing_list, ‘favorites’: favorites}
        return render(request, self.template_name, ctx)

代码逻辑:

  1. request.user.fav_set:利用Django的反向查询关系,获取当前用户的所有Fav记录。
  2. .values_list(‘thing_id‘, flat=True):从这些记录中只提取出thing_id字段,形成一个ID值列表。
  3. 将这个ID列表(favorites)传递给模板上下文(ctx)。

这样,在模板中,我们就可以通过检查当前遍历的物品ID是否在favorites列表中,来决定显示哪一颗星标。

前端模板:动态显示星标 ⭐

视图准备好了数据,现在需要在网页上显示出来。模板负责生成HTML,并利用传递过来的favorites列表控制星标的显示状态。

以下是模板中渲染每个物品及其收藏星标的核心代码:

<!-- favs/templates/favs/thing_list.html -->
{% for thing in thing_list %}
  <div>
    <span>{{ thing.title }}</span>
    {% if user.is_authenticated %}
      <!-- 两颗重叠的星标,通过样式控制只显示一颗 -->
      <!-- 空心星:点击后执行收藏 -->
      <a href=“#”
         onclick=“favPost(‘{% url ‘thing_unfavorite’ thing.id %}‘, {{ thing.id }}); return false;”
         id=“favorite_star_{{thing.id}}”
         {% if thing.id in favorites %}style=“display: none;”{% endif %}>
        ☆ <!-- 空心星符号 -->
      </a>
      <!-- 实心星:点击后取消收藏 -->
      <a href=“#”
         onclick=“favPost(‘{% url ‘thing_favorite’ thing.id %}‘, {{ thing.id }}); return false;”
         id=“unfavorite_star_{{thing.id}}”
         {% if not thing.id in favorites %}style=“display: none;”{% endif %}>
        ★ <!-- 实心星符号 -->
      </a>
    {% endif %}
  </div>
{% endfor %}

模板逻辑:

  • 循环遍历所有物品(thing_list)。
  • 如果用户已登录(user.is_authenticated),则显示星标区域。
  • 两颗星标:实际上渲染了两个链接(<a>标签),一个代表“未收藏”(☆),一个代表“已收藏”(★)。它们通过CSS的display: none;样式来控制只显示其中一个。
  • 条件判断{% if thing.id in favorites %} 用来判断当前物品是否已被收藏。如果已收藏,就隐藏空心星(☆),显示实心星(★);反之则隐藏实心星,显示空心星。
  • onclick事件:点击星标时,会调用JavaScript函数favPost,并传入对应的URL(用于收藏或取消收藏)和物品ID。

这种“两颗星切换显示”的方式是实现UI状态变化的一种简洁方法。接下来,我们看看点击事件背后的JavaScript如何工作。

JavaScript:处理Ajax请求与UI更新 🛠️

当用户点击星标时,我们不希望整个页面刷新,而是只向服务器发送一个轻量级的请求,并在收到响应后局部更新星标显示。这通过JavaScript(使用Fetch API)实现,即Ajax技术。

以下是处理收藏/取消收藏的JavaScript函数:

// 静态文件中的JavaScript或模板内嵌的<script>
function favPost(url, thing_id) {
    console.log(‘Requesting to favorite/unfavorite‘);
    // 使用Fetch API向服务器发送POST请求
    fetch(url, {
        method: ‘POST‘,
        headers: {
            ‘Content-Type’: ‘application/x-www-form-urlencoded; charset=UTF-8‘,
        },
        // 请求体为空,因为操作由URL本身定义
    })
    .then(response => {
        console.log(‘Favorite/Unfavorite operation finished‘);
        // 请求完成后,切换两颗星标的显示状态
        const star = document.getElementById(`favorite_star_${thing_id}`);
        const unstar = document.getElementById(`unfavorite_star_${thing_id}`);
        // 切换显示/隐藏
        if (star.style.display === ‘none‘) {
            star.style.display = ‘inline‘;
            unstar.style.display = ‘none‘;
        } else {
            star.style.display = ‘none‘;
            unstar.style.display = ‘inline‘;
        }
    })
    .catch(error => console.error(‘Error:‘, error));
}

JavaScript逻辑:

  1. fetch(url, {method: ‘POST‘, …}):向传入的URL(由模板生成,如/thing/4/favorite/)发送一个HTTP POST请求。
  2. .then(response => {…}):这是一个Promise回调。当服务器响应返回后,执行其中的代码。这实现了异步处理,不会阻塞页面。
  3. UI更新:在回调函数中,通过document.getElementById获取到当前物品对应的两颗星标元素,然后翻转它们的显示状态(将显示的隐藏,隐藏的显示)。这样就实现了点击后星标外观的即时变化。

至此,前端的交互流程已经完成。最后,我们需要看看服务器端如何响应这个Ajax POST请求。

后端视图:处理收藏与取消收藏 ✅

前端发送的Ajax请求最终由Django视图处理。AddFavoriteViewDeleteFavoriteView负责在数据库中创建或删除Fav记录。

由于这是一个简单的API端点,并且为了避免跨站请求伪造(CSRF)保护的复杂性(在Ajax中需要额外处理),示例中使用了@csrf_exempt装饰器。在实际生产环境中,需要根据安全要求谨慎处理。

以下是处理“添加收藏”的视图:

# favs/views.py
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse
from django.shortcuts import get_object_or_404

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/93f7b6f482b038f42d521918ac98ff02_64.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-dj-evrn/img/93f7b6f482b038f42d521918ac98ff02_65.png)

@method_decorator(csrf_exempt, name=‘dispatch‘)
class AddFavoriteView(LoginRequiredMixin, View):
    def post(self, request, pk):
        # 1. 获取要收藏的物品
        thing = get_object_or_404(Thing, id=pk)
        # 2. 创建Fav对象(建立用户与物品的关联)
        fav = Fav(thing=thing, user=request.user)
        try:
            fav.save() # 尝试保存
        except IntegrityError:
            # 如果已经收藏过(违反unique_together约束),则忽略
            pass
        # 3. 返回一个空的成功响应(前端不处理具体内容,只根据状态码判断)
        return HttpResponse()

以下是处理“取消收藏”的视图:

# favs/views.py
@method_decorator(csrf_exempt, name=‘dispatch‘)
class DeleteFavoriteView(LoginRequiredMixin, View):
    def post(self, request, pk):
        # 1. 获取要取消收藏的物品
        thing = get_object_or_404(Thing, id=pk)
        # 2. 查找并删除对应的收藏记录
        try:
            fav = Fav.objects.get(user=request.user, thing=thing)
            fav.delete()
        except Fav.DoesNotExist:
            # 如果记录不存在(可能已被删除),则忽略
            pass
        # 3. 返回一个空的成功响应
        return HttpResponse()

视图逻辑:

  • LoginRequiredMixin:确保只有登录用户才能访问。
  • get_object_or_404(Thing, id=pk):根据URL中的主键pk获取物品对象,如果不存在则返回404错误。
  • 添加收藏:尝试创建新的Fav对象并保存。使用try...except IntegrityError来捕获可能因重复收藏(违反unique_together)而引发的异常,并优雅地忽略它。
  • 取消收藏:尝试查找当前用户和指定物品对应的Fav记录,如果找到则删除。
  • return HttpResponse():两个视图都返回一个空的HTTP响应,状态码默认为200(成功)。前端JavaScript的Fetch API接收到成功的响应后,就会触发更新UI的回调函数。

总结 📝

本节课中我们一起学习了如何在Django应用中实现一个完整的收藏功能。我们回顾一下关键步骤:

  1. 模型设计:使用Fav作为中间模型,通过ForeignKey建立UserThing之间的多对多关系,并用unique_together确保数据唯一性。
  2. URL与视图:配置了处理收藏(favorite/)和取消收藏(unfavorite/)的URL,并创建了对应的视图类来处理POST请求。
  3. 列表视图增强:在ThingListView中查询当前用户的收藏列表,并将物品ID列表传递给模板。
  4. 模板逻辑:在模板中使用条件判断,根据物品是否在收藏列表中,来显示不同的星标(☆或★),并绑定调用JavaScript的点击事件。
  5. Ajax交互:编写JavaScript函数favPost,使用Fetch API向服务器发送异步POST请求,并在请求成功后切换星标的显示状态,实现无刷新交互。
  6. 后端处理:视图接收Ajax请求,在数据库中创建或删除Fav记录,并返回简单的成功响应。

这个示例综合运用了Django的模型关系、基于类的视图、模板系统以及前端的Ajax技术,是一个典型的现代Web交互功能实现案例。

043:道格拉斯·克罗克福德发现JSON的故事

在本节课中,我们将了解JSON(JavaScript Object Notation)这一当今最流行的数据交换格式是如何被道格拉斯·克罗克福德发现并推广的。我们将追溯其起源、设计理念,以及它为何能取代XML成为Web应用数据交换的首选。


JSON的发现与本质

上一节我们介绍了数据交换格式的背景,本节中我们来看看JSON是如何被发现的。

JSON是全球最受喜爱的数据交换格式。我在2001年发现了它。我并未声称发明了JSON,因为它早已存在于自然界中。我只是看到了它,认识到了它的价值,给它起了名字并加以描述,展示了它的优点,但我并未发明它。我也不是第一个发现它的人。后来我发现,在2000年已有其他人产生了同样的想法。

我找到的最早将JavaScript用作数据交换格式的实例是在1996年的网景公司。所以这个想法已经存在了一段时间。如果你看看其他的数据表示法,比如NeXT公司以及后来苹果公司使用的属性列表,除了几处表面上的改动,它们本质上也是JSON的表示法。因此,JSON似乎是一种不可避免的数据表示形式,至少对于旨在被编程语言消费的数据而言是如此。归根结底,所有数据都是如此。

我最初是从JavaScript开始的,但我的第一个应用是促进用JavaScript编写的程序与用Java编写的服务器之间的通信。因此我认识到,尽管它诞生于JavaScript,但它可以且应该是独立于编程语言的。所以我尽可能地简化它,去掉尽可能多的东西,试图为如何构建数据并通过网络传输制定出最简单的规范,这最终被称为JSON。


推广JSON:从想法到“标准”

上一节我们了解了JSON的起源,本节中我们来看看它是如何从一个想法变成被广泛接受的“标准”的。

2001年,我在一家名为State Software的公司。我们开发了一个平台,用于构建可以通过未经修改的网页浏览器交付的应用程序,也就是今天所谓的Ajax。但在2001年,这是一个相当激进的想法,没有多少人相信这是可能的,或者即使可能,也认为这是个好主意。

但我们做出了一些出色的演示,并开始取得一些进展,试图说服潜在客户采用这种应用程序开发风格。在描述过程中,我们会说:“然后我们使用这个叫JSON的东西来来回回地通信数据。”他们会问:“JSON是什么?”我会说:“这是我们在JavaScript中发现的一个东西,非常棒。”他们通常会说:“哦,我们不能用那个,我们刚刚承诺使用XML,所以不行。”我会解释:“但XML有诸多问题,它成本高昂,使用起来困难得多。”而他们会说:“我们不能用你做的那个东西,因为它不是标准。”我说:“它是一个标准。它是ECMA-262标准的一个真子集,而ECMA-262是一个标准。”他们反驳说:“不,那不算标准。”

于是我决定,如果我想使用这个东西,我需要让它成为一个标准。所以我购买了json.org域名,建立了一个网页,并宣布它是一个标准。就这样,这就是我所做的一切。我没有四处游说,试图说服工业界、政府等所有人都应该这么做。我只是建立了一个网站,基本上是一个单页网站。多年来,人们发现了它,并意识到:“哦,是的,这简单多了,我就要用这个。”


JSON vs. XML:设计哲学与优势

上一节我们看到了JSON如何被确立为“标准”,本节中我们来深入比较JSON与XML,理解其设计优势。

关于XML用于数据交换,我一直无法理解的一点是:通常的模式是,你发送一个查询到服务器,服务器交给数据库,然后你拿回一个XML文档,接着你必须再向这个XML文档发送查询才能把数据提取出来。我当时想,为什么不能直接给我一种我能立即知道它是什么并且能直接使用的形式呢?我认为这就是JSON的主要好处。

并不是说花括号比尖括号好得多。归根结底,这些都不重要。重要的是,JSON喜欢表示的数据结构,与编程语言表示的数据结构完全相同。你知道,当Ajax被提出时,其中的“X”本意是指XML。但聪明的开发者很快就意识到:“这太难了,我们不想在这里用XML。”其中一些人发现:“嘿,你可以在这里用JSON代替,这简单得多,也快得多。”于是他们开始这样做。

有一段时间存在一场辩论,一些人争论说:“Jesse James Garrett说过‘X’代表XML,所以你只能用XML。”但这种说法没有持续多久。在那个时代,人们考虑了许多XML的替代方案,但JSON是唯一一个专门为Ajax设计的。


JSON的设计决策与未来

上一节我们探讨了JSON相对于XML的优势,本节中我们来看看JSON一个关键的设计决策及其长远影响。

在设计JSON时,我做的最大胆的决定之一是不给它加上版本号。因此,没有修订它的机制。所以,JSON,我们就只能用它当前的样子,就这样了。而事实证明,这是它最好的特性,因为它希望成为一个底层的东西,它是基础架构,是你在其上构建一切的基础。它有点像语言中的字母表。我们可以创造很多单词和很多造句的方式,但创造新字母是非常罕见的。JSON就处于这样的位置。所以它不改变是件好事。

我预计也许有一天,我们会发现JSON确实无法处理一些非常重要的事情,比如循环结构。图结构在JSON中不容易表示,虽然可以,但需要更直接的交互和更多的工作。也许有一天,当我们不想做那些额外工作时,我们会用别的东西取代JSON。我们不会扩展JSON来实现那些功能,我们会替换掉JSON。即使在我们做了替换之后,所有曾经开发的、仍然使用JSON的东西将继续工作,因为JSON永远不会改变。


总结

本节课中我们一起学习了道格拉斯·克罗克福德发现和推广JSON的故事。我们了解到JSON并非被发明,而是从JavaScript中“发现”的一种自然的数据表示形式。我们追溯了它如何从一个简单的想法,通过建立一个简单的网站(json.org)而被确立为事实上的标准。我们比较了JSON与XML,理解了JSON因其与编程语言数据结构的天然契合、简单性和高效性而胜出。最后,我们探讨了JSON不加版本号这一关键设计决策的智慧,这确保了其稳定性和作为互联网基础“字母表”的长期价值。JSON的成功,是简洁、实用主义思想在技术领域的一次胜利。

044:面对面办公时间-克罗地亚萨格勒布

概述

在本节课中,我们将通过一段在克罗地亚萨格勒布进行的面对面办公时间实录,了解两位学生使用Coursera在线教育平台的经验、动机以及他们对平台未来的看法。这段对话将帮助我们理解在线课程如何补充传统教育。


对话实录与解析

查克在这里进行介绍。他身处克罗地亚萨格勒布,这是又一次办公时间的实例,属于扩展版。

学生访谈一:马可

查克首先请学生介绍自己。一位名叫马可的学生进行了回应。查克询问了马可当前的教育情况。

马可目前正在萨格勒布大学攻读电气工程与计算学院的研究生学位。

随后,查克询问了马可参与Coursera课程的数量。马可回答他参加了两门课程,一门是查克的课程,另一门是Python入门课程。

查克接着探讨了马可使用Coursera的原因。马可的解释包含两个主要点:

  • 首先,他希望拓宽自己的知识面
  • 其次,他希望在简历中展示除大学学业外,还学习了其他课程。

最后,查克征求马可对Coursera的改进建议。马可的建议非常直接:让平台运行得更快


上一节我们了解了工科学生马可的观点,接下来让我们看看另一位来自不同专业背景的学生的看法。

学生访谈二:安娜·玛丽亚

第二位学生名叫安娜·玛丽亚·克纳。她目前是哲学与社会人文信息学专业的本科及研究生

关于Coursera课程参与情况,安娜表示她参加了四门课程,分别是:

  • 非理性行为指南
  • 如何推理与争论
  • 人机交互
  • 斯坦福大学的人工智能课程

在这四门课程中,她只完成了“如何推理与争论”这一门并获得了证书。

查克询问她如何将Coursera学习与主业教育结合。安娜认为,她在Coursera上学到的东西超出了她本专业的学习范围。她对此的解释是,她对很多事物抱有浓厚兴趣,并且认为人们应该更倾向于跨学科学习。学习其他领域的知识能使她在就业市场上更具价值,同时她本人也对众多事物怀有真诚的好奇心。

随后,话题转向她对Coursera未来的担忧。安娜表达了一种忧虑:当资本介入后,平台可能无法继续保持开源和免费。她担心未来可能需要付费才能使用,这将导致很多人无法再享受到该平台的教育资源。

最后,基于她作为学习技术使用者的身份,查克请她提出改进建议。安娜的建议是:让学习过程变得更有趣,就像查克或唐·阿利(另一位教师)的课程那样


总结

本节课中,我们一起聆听了来自萨格勒布两位学生的声音。通过他们的分享,我们看到了在线教育平台如Coursera在拓宽知识视野增强简历竞争力促进跨学科学习方面扮演的角色。同时,学生们也表达了对平台可访问性及未来发展的关切。这次办公时间为我们提供了宝贵的用户视角。我们下次再见。

045:搜索功能与视图配置模式实战

在本节课中,我们将学习如何为Django应用添加搜索功能,并探索一种直接在URL配置文件中定义视图的新模式。我们将通过分析一个名为“Well”的示例应用来理解这些概念。

概述

“Well”应用是之前“My Arts”应用的一个变体,核心区别在于它增加了一个搜索框。用户可以通过搜索框查找包含特定关键词的文章。本节课将重点解析如何实现基于GET请求的搜索功能,以及如何通过urls.py文件更简洁地配置通用视图。

搜索功能的实现原理

上一节我们介绍了CRUD操作,本节中我们来看看如何实现一个不刷新页面的搜索功能。其核心在于使用GET请求来传递搜索参数。

当用户在搜索框中输入内容并提交时,表单会发起一个GET请求(因为表单没有指定method="post")。搜索关键词会作为查询参数附加在URL之后,例如:/well?search=yada

这种设计使得搜索结果是幂等的。这意味着相同的搜索URL总会返回符合该搜索条件的逻辑结果(即使底层数据发生了变化),并且这个URL可以被收藏或分享。

视图中的搜索逻辑

PostListView中,我们需要处理带有和不带有搜索参数的GET请求。

以下是处理搜索的核心代码逻辑:

def get(self, request):
    # 从GET请求中获取名为‘search’的参数,如果不存在则返回False
    search = request.GET.get('search', False)
    if search:
        # 如果存在搜索词,则构建一个查询集,查找标题或正文中包含该词的文章
        from django.db.models import Q
        query = Q(title__contains=search) | Q(text__contains=search)
        objects = Post.objects.filter(query).select_related('owner').order_by('-updated_at')[:10]
    else:
        # 如果没有搜索词,则获取全部文章
        objects = Post.objects.all().select_related('owner').order_by('-updated_at')[:10]

    # 对文章列表进行后续处理(如格式化时间)
    for obj in objects:
        obj.natural_updated = naturaltime(obj.updated_at)

    context = {'post_list': objects, 'search': search}
    return render(request, self.template_name, context)

代码解释

  1. request.GET.get('search', False) 用于安全地获取URL中的查询参数。
  2. search参数存在时,我们使用Django的Q对象来构建一个复杂的查询条件,它允许我们使用逻辑或(|)来组合多个过滤条件(例如:标题包含关键词 正文包含关键词)。
  3. select_related('owner') 是一个性能优化方法,它会在查询文章的同时,通过单次SQL查询获取关联的owner(用户)信息,避免后续逐个访问时产生额外的数据库查询(即“N+1查询问题”)。
  4. naturaltime 是一个方便的模板过滤器,用于将日期时间转换为“3小时前”这样的友好格式。

模板中的搜索表单

在列表页的模板中,我们需要一个表单来发起搜索请求,并显示当前的搜索词。

以下是模板中搜索表单部分的关键代码:

<form>
    <input type="text" name="search" placeholder="Search term..." value="{{ search|default_if_none:'' }}">
    <button type="submit"><i class="fas fa-search"></i></button>
    {% if search %}
        <a href="{% url 'well:all' %}"><i class="fas fa-undo"></i></a>
    {% endif %}
</form>

代码解释

  1. 表单默认使用GET方法提交,目标URL是当前页面。
  2. value="{{ search|default_if_none:'' }}" 确保在提交搜索后,搜索框内会回显刚才输入的关键词。
  3. 如果当前处于搜索状态(search变量为真),则会显示一个撤销图标(🔙),点击后会跳转回没有搜索参数的列表页URL。

在URL配置中定义视图的新模式

之前,我们通常在views.py中为每个功能创建视图类。对于简单的增删改查视图,有一种更简洁的模式:直接在项目的urls.py中配置Django的通用视图。

以下是urls.py中直接配置通用视图的示例:

from django.urls import path
from myarts.owner import OwnerListView, OwnerDetailView, OwnerCreateView, OwnerUpdateView, OwnerDeleteView
from . import models

app_name = 'well'

urlpatterns = [
    path('', PostListView.as_view(), name='all'), # 自定义的列表视图,包含搜索
    path('post/<int:pk>', OwnerDetailView.as_view(model=models.Post, template_name='well/detail.html'), name='post_detail'),
    path('post/create', OwnerCreateView.as_view(model=models.Post, template_name='well/form.html', fields=['title', 'text'], success_url=reverse_lazy('well:all')), name='post_create'),
    path('post/<int:pk>/update', OwnerUpdateView.as_view(model=models.Post, template_name='well/form.html', fields=['title', 'text'], success_url=reverse_lazy('well:all')), name='post_update'),
    path('post/<int:pk>/delete', OwnerDeleteView.as_view(model=models.Post, template_name='well/delete.html', success_url=reverse_lazy('well:all')), name='post_delete'),
]

模式优点

  1. 减少代码文件:无需在views.py中为简单的CRUD操作编写几乎相同的视图类。
  2. 集中配置:URL模式与视图的关键参数(如关联的模型、使用的模板、表单字段、成功后的跳转地址)定义在同一个地方,一目了然。
  3. 易于维护:当需要重命名应用或调整基础行为时,修改点更集中。这里通过app_name变量和字符串拼接(如‘well/detail.html’)来管理模板路径,使得应用更易于复用。

注意事项
这种模式适用于直接使用通用视图且无需额外自定义逻辑的场景。对于像PostListView这样需要复杂自定义逻辑(如搜索)的视图,仍然需要在views.py中编写自定义类。

总结

本节课中我们一起学习了两个核心内容:

  1. 实现搜索功能:我们了解了如何通过GET请求和查询参数实现幂等的搜索,如何在视图中使用Q对象进行复杂查询,以及如何在模板中渲染搜索表单和状态。
  2. 视图配置新模式:我们探索了一种直接在urls.py中配置通用视图类(如OwnerCreateView)的高效模式,这可以简化标准CRUD操作的代码结构,使项目更加清晰。

通过“Well”这个示例,你将掌握如何为你的Django应用添加实用的搜索特性,并学会根据场景选择最简洁的视图定义方式。

046:面对面办公时间 - 加利福尼亚州山景城

概述

在本节课中,我们将回顾一次在加利福尼亚州山景城举行的、规模空前的面对面办公时间活动。这次活动由课程讲师查克博士主持,并得到了首席助教苏的大力支持。

活动介绍

我们现在位于加利福尼亚州山景城。我是查克博士。这位是苏,我们勇敢的首席助教,她在多年的互联网历史与Python信息学课程中提供了巨大的帮助。

参与规模

本次办公小时活动拥有我们迄今为止规模最大的学生群体。我将向大家展示在场的每一位。

现场记录

我们立即上传了这次活动的记录。这是迄今为止我们举办过的、参与人数最多的办公小时活动,堪称一项世界纪录。

总结

本节课我们一起回顾了在加州山景城举行的一次创纪录的面对面办公时间活动,见证了课程社区的活跃与成长。

047:毕业典礼特邀演讲人-Curt Bonk博士 🎓

在本节课中,我们将学习Curt Bonk博士在Django课程毕业典礼上的演讲。他将分享关于成功完成在线课程以及规划未来学习路径的八个核心原则。

概述

上一节我们完成了Django应用部署的学习,本节我们将聆听特邀嘉宾Curt Bonk博士的毕业演讲。他将分享一系列以字母“P”和“C”开头的关键词,这些词概括了成功学习与个人成长的重要元素。

演讲内容

大家好,欢迎来到我的大规模开放在线课程毕业典礼。我是Charles Severance博士,你们的讲师。时机成熟时,我将为你们颁发证书。我衷心感谢你们坚持学习足够长的时间以获得证书。按照毕业典礼的传统,我们邀请了一位毕业演讲嘉宾。他是我的好友兼同事,来自印第安纳大学的Kurt Bonk博士。因此,在开始毕业典礼之前,我想请我们的毕业演讲嘉宾简短发言。

这将会是一场“PC”演讲。这是什么意思?一方面,我猜是指“政治正确”。我戴着我红色的印第安纳眼镜,穿着蓝色的“加油蓝色”衬衫。这部分内容不仅关乎个人电脑或其他“PC”词汇,更关乎“可能的课程”,即人生中可能选择的路径。这些并非特指你们接下来必定要跟Chuck博士学习的课程,而是我想引导你们思考的四个以“P”开头的词和四个以“C”开头的词,它们关乎你们人生中可能的道路。

这门MOOC课程关乎你们的热情与兴趣,关乎你们决定要做的事情。你们进入了一个领域,可能是一个舒适区内的新领域,也可能略微超出了舒适区,但你们都在努力寻找生活中的“天赋所在”。正如Ken Robinson爵士所说,我们都在寻找个人的天赋。所以,第一点是热情。你必须对所探索的课程和兴趣充满热情。

仅有热情,并不能指望在生活中取得成功。你还需要目标。你需要一个目的,一个努力的方向。正如斯坦福大学的一位教授所谈论的“最高目标”,也许你们学习的这门课程并非你们的最高目标,但它依然是一个目标,我们都在朝着它前进。因此,我们想要有目标,想要有热情。

但完成一门MOOC的唯一途径,是拥有毅力或恒心。你们需要努力推进,在课程中坚持不懈。找到内心额外的能量。我要祝贺你们所有人都找到了它。你们拥有了热情,找到了目标(哪怕是课程相关的目标),并且具备了毅力和恒心。这是当今心理学中最重要的一词。

第四点,当然,既然你们参加了这门课程,沿途也必须带点趣味性。当我制作我的MOOC时,我加入了一些趣味性,你们可以在线查看。记住这些以“P”开头的词,当你们步入人生和未来可能的课程时。

至于“C”开头的词,首先是选择。你们选择参与进来,这是你们的自主决定,是你们个人选择进入并完成这门课程。

第二个“C”开头的词是承诺。你们不仅决定或选择了,更承诺去完成这门课程。当你们这样做时,所有的同伴都在关注你们,与你们交流、分享。

下一个词是连接。一门连通主义的MOOC以及任何在线课程,都关乎连接与关系。希望你们不仅仅是完成了这门课,更建立了新的桥梁、新的关系、新的人生可能路径。你们建立的这些连接,未来可能在各个方面帮助到你们,不仅仅是这里学到的内容,也许能帮你们找到新工作或发现新的兴趣。

然后,这一切关于什么?你们中有些人想要证书,你们会想要这个。有些人想要百万美元,也许学习这门课或所有课程不会让你赚到一百万,但你们可能会像史蒂夫·乔布斯那样变得创新。我正在车里听的一本书《发现你的天赋》刚听完。你们完成了一门课程,可能会获得一个证书,这是另一个“C”开头的词。

因此,这就是四个“P”词:热情、毅力、目标和趣味性;以及我们经历的四个“C”词:选择、承诺、连接,最后是完成(一门课程、一个证书),然后继续人生旅程。

如果你们在进入其他课程时能牢记这八条原则,我想你们可能会在未来取得一些成功。我希望你们如此。我希望你们跟随内心,继续学习Chuck博士或其他人的更多课程。因此,我祝贺你们所有人,不仅注册了课程,更完成了尽可能多(也许是全部)的模块,从而不仅能获得这里的证书,更能获得作为开放学习者和非正式(如今已是正式)学习者的“人生证书”。

非常感谢。

太棒了,太棒了。我衷心感谢Kurt远道从布卢明顿来到安娜堡。Kurt和我一起做过不少事情,只是为了来这里和Chuck在一起。哦,不,这还不是来这里的另一个理由。所以我们决定录下这个。现在,是时候让你们在电脑前“走过”并接收你们的……烘焙毕业证书了。我的意思是,有个鲜为人知的事实:当你真正从大学毕业时,他们并不会给你真正的证书,他们会给你一张精心卷起来、系着小蝴蝶结的纸。然后你回到座位打开它,发现是空白的。但这就是我们要再做一次的事情,你们可以打印自己的证书或徽章,取决于你们所修的课程。

现在是时候让你们站起来,在电视或笔记本电脑前“走过”,我将亲手递给你们证书。我拿着我的哈利·波特魔杖(Severus Snape的),来让你们“受教育”。那么,来吧。我祝贺你们,你们可以伸手接过你们的证书。来吧,握握手,我们都来握握手。正如Kurt所说,这希望只是一系列精彩课程的开始。再次感谢你们付出的时间,感谢你们的毅力,感谢你们的承诺。希望我们都能在网络上、在互联网课堂上见到彼此教学。非常感谢,这是什么?开放。

posted @ 2026-03-29 09:41  布客飞龙I  阅读(5)  评论(0)    收藏  举报