Some more things about Django I've been enjoying

Julia Evans

关于 Django,我最近喜欢上的一些功能

你好!我最近踏上了一段有趣的旅程,尝试学习用一种大约 2010 年的风格来做网站——有一个 SQL 数据库,并在后端渲染 HTML。

这段旅程还挺有意思的,因为用这种方式做网站对我来说并不一定觉得“容易”:我在 2000 年代或 2010 年代从未学过怎么做,有很多东西要学。

所以下面我想分享一些 Django 的功能,它们让我觉得搭建这类网站比之前尝试用 Go 标准库或 Flask 却屡屡失败时更容易实现。我也会谈谈在使用 Django 时遇到的几个问题。

为什么要像在 2010 年那样学做网站?

以前我做网站时比较得心应手的工具集是:

  • 静态网站生成器(就像这个博客用的)
  • 会用 Javascript 做些有趣交互的静态网站(比如这个 sql playground
  • 简单的 Vue.js 单页应用,后端用 Lambda 或 Go(比如 mess with dns

我真的很喜欢这种以前端为重的方式来做这些超级简单的应用,但当我开始想做一个有很多不同页面(而不是真的就只有一个页面)的东西时,对于那些需要大量前端代码的方案就没那么兴奋了。所以我想试试以后端为主的方式。

在某种意义上,写一个尽可能少用 JS 的、以后端为中心的网站,和写一个尽可能少用后端逻辑的单页 JS 网站,给我的感觉是一样的,尽管它们看起来像是相反的两端。在两种情况下,我都只是想把尽可能多的逻辑集中在一处。

接下来聊聊关于 Django 的一些想法!

我喜欢上了 query builder(查询构造器)

我了解到,在 Django 中我可以定义一个“query set”类,在里面放上一堆方法,分别对应构造查询时可能用到的不同 WHERE 语句:

在定义好这些方法的含义后,我在视图代码中是这样使用它的:

Events.objects.approved()
  .for_tab(tab)
  .with_festivals(tab_params.festival_slugs)
  .is_free(tab_params.free)
  .is_outdoors(tab_params.outdoors)

而这些方法我是这样定义的:

class EventQuerySet(SearchableQuerySetMixin, models.QuerySet):
    def approved(self):
        return self.filter(approved_at__isnull=False)

    def future(self):
        today = timezone.localdate()
        return self.filter(end__gt=self._midnight(today))

    def with_tags(self, tags):
        if tags:
            return self.filter(tags__name__in=tags).distinct()
        return self

定义过滤器的语法不是我最喜欢的,但我大部分时间只是在调用这些方法,感觉非常易读、很好用,也让我想以后去研究一下其他的 query builder 库。过去我曾想“我懂 SQL,还要什么 query builder?”,但这种结构确实让代码读起来非常舒服。

我还发现了一个用 Python 自己写了一个精简的 query builder的例子,我想稍后再去读一读,思考一下自己是否会更喜欢这种更极简的版本。

模板过滤器太棒了

Django 模板里有一堆能提升幸福感的小Django 模板中可用的过滤器,生成 HTML 时超级有用。到目前为止我用过的有:

  • 把纯文本 URL 转成链接,或把换行转成 <br>{{ event.description|urlize|linebreaksbr }}
  • 格式化日期({{ row.date|date:"M j" }}
  • json_script,它接收一个 Python 字典,自动将其转换为 JSON,并以安全的方式作为 <script> 标签插入到 HTML 中

单看每一项都是小事,但感觉能直接拿来用就带来了很大的不同。

querystring 很酷

我觉得我最喜欢的模板过滤器是 querystring:在这个网站里,我们有时会用像 ?date=2026-06-01 这样的过滤条件来决定显示什么内容。querystring 可以生成一个指向相同查询字符串、但只改动一项的链接,比如这样来链接到前一个日期:

<a href="{% querystring date=nav.prev_date%}">

或者用来移除 outdoors 参数:

<a href="{% querystring outdoors=None %}">

自动数据库迁移依然很棒

我依然非常喜欢 Django 的自动数据库系统。只需编辑模型来添加一个新字段之类的东西,然后 Django 就能自动生成迁移,这太神奇了。

到目前为止我们已经做了 19 次数据库迁移,我觉得以后肯定还会有更多!能够随着我对问题的理解变化而轻松地修改数据库,对我来说帮助巨大。

我不想用继承来组织代码

Django 的文档有时会提供使用基于类的视图和继承来组织视图代码的选项。例如,我有四个共享大量代码的视图,本可以通过定义一个父类,然后让其他视图继承它来管理。

我试了一下,并不喜欢用继承在视图之间共享代码的体验。后来我改成了用函数的方式,就像这篇文章所倡导的那样,这样要直截了当得多。我在 Python 中使用继承从未有过愉快的体验,我想以后也不会再尝试了。

不过,用继承来使用 Django 本身提供的接口我倒不介意:例如,如果我想定义一个 query set,就需要写类似 class EventQuerySet(SearchableQuerySetMixin, models.QuerySet) 这样的代码。我不会太纠结,它似乎也能正常工作。

(顺带一提:我一直在尝试用“某件事让我感觉不太舒服,我更喜欢另一件事”这种方式来表达我的编程观点。我链接的那篇文章说基于函数的视图是“正确的方式”。我并不是很在意它是否“正确”,但知道有其他人对继承也有类似的感受,还是挺让人安心的)

我还不知道该如何看待 Django 的性能

某个时候,LLM 爬虫发现了我们的网站,开始以每秒大约 10 个请求的速度向我们发送请求。我把它们屏蔽了,目前还算有效,但这让我开始思考网站的承载能力。我习惯于写 Go 后端,那里的性能状况相当直观(通常一切都够快),而 Django 网站则完全不同。

一些轻量的负载测试(使用 ab -n 1000 -c 1)显示,目前我们大约每秒能处理 2 到 3 个请求(在一台约 10 美元/月的虚拟机上)。

我很想一头扎进去做大量的性能分析,弄清楚哪里慢并尝试让它更快(有 py-spy 可以做到,而且 py-spy 非常好用、超级简单,性能分析本身也很有趣!)但我真的不明白对于一个 Django 网站,我应该对性能抱有什么样的预期,以及应该如何从更宏观的角度去思考它。

还有一些问题我还没想明白:

  • 如果我的网站会偶尔迎来流量高峰,我是否需要能够扩容?
  • 我是否应该把网站设计成可以缓存更多内容?(而且我真的必须这么做吗?缓存要做对真的很烦人!)
  • Django 性能文档说 Jinja 在模板渲染上更快,我是否应该考虑切换模板系统?
  • 那些文档还说“{% block %} 比使用 {% include %} 更快”,我在想这个差距是否很大,如果是,为什么会这样

模板缓存可能很重要

我觉得关于 Django 我正在学到的一点是,正因为它是一个框架(Framework),很容易不小心把它配置错。例如,就在刚才思考为什么我的网站很慢时,我读了 Django 性能文档,注意到里面有这样一句评论:

启用缓存的模板加载器通常会显著提升性能,因为它避免了每次需要渲染时都重新编译每个模板。

点进链接后,我看到缓存的模板加载器本应默认开启,但我在尝试做别的事情时不小心把它关掉了。我觉得这种“我把默认开启的缓存模板加载器关掉了”的事情,正说明了我依然觉得 Django 的设置文件相当令人困惑和棘手。我想以后进去修改时应该更小心一些。

开启模板缓存后,网站现在似乎可以相当轻松地处理每秒约 12 个请求,而不会占满 CPU。我没有仔细对比开启前后的基准测试,但看起来确实带来了相当大的提升。

关于 Django 性能,让我感到惊讶的一点是,我一直听到的建议是“如果有性能问题,就去检查数据库查询!也许加个索引!”。但我遇到的各种性能问题(比如这个模板缓存的问题)并不是因为查询慢,所以到目前为止,对我来说更有用的反而是从运行 CPU 性能分析开始。而且因为我用的是 SQLite,任何慢查询问题本来也会在 CPU 分析中体现出来。

总之,我不想在网站性能上陷得太深。就像我说的,我很容易就对性能分析产生兴趣,但其实我对性能分析已经了解不少,这并不是我现在最需要学习的东西。

暂时就到这里!

以后我可能会再聊聊关于 Django 我喜欢(或者觉得吃力)的地方。最近在尝试写一些更短的博客文章。

原文由 Julia Evans 发布

本文章由 muse-spark-1.2-contributor 进行翻译