Some more things about Django I've been enjoying

Julia Evans

再聊聊我喜欢的 Django 的几个地方

原文由 Julia Evans 发布,订阅该博客

你好!我最近开启了一段有点好玩的探索:尝试用一种类似 2010 年的方式来做网站——用上 SQL 数据库,在后端渲染 HTML。

这段经历还挺有意思的,因为对我来说,用这种方式做网站并不一定算“简单”:我在 2000 年代或 2010 年代并没有学过怎么做,如今还有很多东西要学。

所以下面想聊聊 Django 的一些特性,它们让我觉得做这类网站比之前用 Go 标准库或 Flask 反复尝试却做不出来要可行得多。也会说说我在使用 Django 时遇到的几个问题。

为什么要学得像回到 2010 年那样做网站?

之前我做网站时比较有把握的工具组合是:

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

我挺喜欢这种偏前端的做法来做这些超级简单的应用,但当我想做一个包含很多不同页面(而不是真的就只有一个页面)的东西时,那些需要写大量前端代码的方案就让我提不起劲了。所以我想,不如试试后端。

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

接下来聊聊我对 Django 的一些想法!

我很喜欢查询构造器

我了解到,在 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

定义过滤条件的语法不是我最喜欢的,但我大部分时间只是在调用这些方法,读起来非常清晰、也很好用,这让我以后想去研究一下其他的查询构造器库。以前我总觉得“我会 SQL,还要什么查询构造器?”,但这种结构确实让代码变得很好读。

我还看到一个例子,有人用 Python 自己写了一个小型的查询构造器,我想之后读一读,看看自己会不会更喜欢这种更精简的版本。

模板过滤器太棒了

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),结果显示目前我们在一台约 10 美元/月的虚拟机上,大概每秒能处理 2 到 3 个请求。

我很想一头扎进去做一堆性能分析,搞清楚哪里慢、再想办法让它更快(有 py-spy 可以用,而且 py-spy 非常好用、做性能分析也很有趣!)但我真的不太清楚对于一个 Django 网站,我应该对性能有什么样的预期,又该从更宏观的层面怎么去思考。

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

  • 如果网站会偶尔迎来突发流量,我是不是需要具备扩容的能力?
  • 要不要把网站设计成可以缓存更多内容?(而且真的有必要吗?缓存要做对可太烦人了!)
  • Django 性能文档说 Jinja 在模板渲染上更快,我要不要考虑换模板系统?
  • 那些文档还说“{% block %} is faster than using {% include %}”,我在想差距到底有多大,如果真有差距又是为什么

模板缓存可能很重要

我对 Django 的一个体会是,正因为它是一个(所谓的)框架,很容易一不小心就配错了。比如刚才我在思考网站为什么变慢时,去读了 Django 性能文档,注意到里面有这样一句:

启用缓存模板加载器通常能大幅提升性能,因为它避免了每次渲染时都重新编译模板。

之前做 CPU 性能分析时,我就注意到渲染模板花了不少时间!也许这个能帮到我!

点进去一看,发现缓存模板加载器本应该是默认开启的,却被我在尝试做别的事情时不小心关掉了。我觉得这种“我把默认开启的缓存模板加载器关掉了”的情况,正说明我到现在还是觉得 Django 的配置文件相当混乱、难以理解。以后进去改的时候得多小心。

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

关于 Django 性能,有一点让我挺意外的:我一直听到的建议都是“如果有性能问题,就去查数据库查询!也许加个索引就好了!”但我遇到的各种性能问题(比如这次的模板缓存)都不是因为查询慢,所以对我来说,先跑个 CPU 性能分析反而更有用。何况我用的是 SQLite,就算有慢查询问题,在 CPU 分析里也能看出来。

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

先说到这里!

以后也许还会再聊聊我喜欢(或者觉得吃力)的 Django 的地方。最近在尝试写一些更短的博文。

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

评论