你好!我现在正经历一段有趣的旅程,尝试用2010年代的方式学习建网站——用SQL数据库,在后端渲染HTML。
这段经历挺有意思的,因为对我来说这种方式并不“容易”:我在2000年代或2010年代从未学过这种建站方法,有很多东西需要从头学起。
下面分享一些Django的特性,它们让构建这类网站变得比之前尝试用Go标准库或Flask时更可行。同时也会聊聊我在使用Django时遇到的一些问题。
为什么要学2010年代的建站方式?
之前我擅长的建站工具包括:
- 静态网站生成器(比如这个博客)
- 用JavaScript做些有趣功能的静态网站(比如这个SQL游乐场)
- 简单的Vue.js单页应用,后端用Lambda或Go(比如mess with dns)
对于这些超简单的应用,我很喜欢这种前端为主的方案。但当我开始考虑做一个包含很多不同页面(而不是只有一个页面)的项目时,那些需要大量前端代码的方案就没那么吸引我了。所以我决定尝试后端开发。
用尽可能少的JavaScript写一个后端为主的网站,和用尽可能少的后端代码写一个单页JS网站,虽然看起来截然相反,但对我来说感觉是相通的——都是想把尽可能多的逻辑集中在一个地方。
现在来聊聊Django!
我喜欢查询构建器
我发现在Django中,我可以定义一个“查询集”类,里面包含各种方法,每个方法对应不同的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 自身提供的接口:例如,如果要定义查询集,我需要写类似 class EventQuerySet(SearchableQuerySetMixin, models.QuerySet) 的代码。我不会想太多,它似乎也能正常工作。
(作为元评论:我一直在练习用“某件事对我来说感觉不好,我更喜欢另一件事”的方式来表达编程观点。我链接的那篇文章说基于函数的视图是“正确的方式”。我并不太在意它是否“正确”,但知道其他人对继承有类似感受,这让我感到被验证了。)
我不知道如何看待 Django 的性能
某个时刻,LLM爬虫发现了我们的网站,开始以每秒约10个请求的频率访问。我暂时封禁了它们,但这让我思考网站的实际承载能力。我习惯编写Go后端,性能情况通常很直观(基本够快),而Django站点则截然不同。
通过轻量负载测试(ab -n 1000 -c 1)发现,目前我们大约每秒能处理2-3个请求(在约10美元/月的虚拟机上运行)。
我很容易陷入这样的困境:花大量时间做性能分析找出瓶颈并尝试优化(有py-spy这样的工具,它很棒且极易使用,性能分析也很有趣!)。但我确实不清楚Django站点应有的性能预期,以及如何从更高层面进行思考。
我尚未厘清的问题包括:
- 如果网站偶尔遭遇流量高峰,是否需要具备扩展能力?
- 是否应该设计更多可缓存的内容?(但真的必须这样做吗?缓存机制往往很难正确实现!)
- Django性能文档提到Jinja模板引擎更快,是否需要考虑切换模板系统?
- 该文档还指出“
{% block %}比{% include %}更快”,我不确定差异是否显著,以及原因何在。
模板缓存可能至关重要
我认为在Django中我学到的一点是:由于它是一个“框架”,很容易意外配置错误。例如,当我思考网站为何缓慢时,阅读了Django性能文档,注意到这样一段话:
启用缓存模板加载器通常能大幅提升性能,因为它避免了每次渲染模板时都重新编译。
之前做CPU性能分析时,我发现大量时间花在模板渲染上!也许这能帮到我。
点击链接后,我发现缓存模板加载器本应默认启用,但我在尝试其他操作时意外关闭了它。这种“意外关闭默认缓存模板加载器”的情况,正说明我仍觉得Django设置文件令人困惑且难以操作。看来我以后修改设置时需要格外小心。
启用模板缓存后,网站现在似乎能轻松处理每秒约12个请求,且不会耗尽CPU。虽然我没有仔细对比前后基准测试数据,但效果显然非常显著。
关于 Django 性能,有一点让我挺意外的——我总听到这样的建议:“如果遇到性能问题,先检查数据库查询!或许加个索引!”但我实际遇到的各种性能问题(比如这个模板缓存问题),都不是因为查询慢导致的。所以对我来说,目前更有效的做法是先跑个 CPU 性能分析。而且因为我用的是 SQLite,任何慢查询问题最终也会在 CPU 分析中暴露出来。
不过我不想深入讨论网站性能。就像我说的,我很容易对性能分析产生兴趣,但实际上我对这方面已经相当了解了,它并不是我最需要学习的内容。
先写到这里吧!
以后可能会再聊聊我在 Django 上遇到的趣事(或难题!)。最近在尝试写些更短的博客文章。