网站静态文件上线方案:三个层级和一条缓存铁律

本地开发时页面样式齐全、图标正常,一部署上线把 DEBUG 关掉,整站 CSS、JS、图片集体 404——这是 Django 项目上线路上最经典的一次翻车。原因不复杂:开发期框架顺手帮你把静态文件服务了,生产环境它明确撒手不管,得自己安排。这篇文章把静态文件的上线方案理成三个层级,按项目规模对号入座,最后给一条绕不开的缓存铁律。
谈方案之前先做一次概念分家,因为静态文件和媒体文件是两种东西,处理方式完全不同:
| 维度 | 静态文件(static) | 媒体文件(media) |
|---|---|---|
| 内容 | CSS、JS、字体、图标,跟着代码走 | 用户上传的图片、附件 |
| 变化频率 | 每次发版可能变 | 随时上传,只增不减 |
| 能否改文件名 | 能,改了反而更好 | 不能,URL 是数据的一部分 |
| 丢了怎么办 | 重新收集一遍就有 | 真丢了,找不回来 |
静态文件可以玩「内容一变、文件名就换」的把戏,媒体文件不行——用户上传后拿到的图片地址不能因为发个版就失效。很多后续麻烦,根源都是这两摊东西混在了一起。本文先讲静态文件的三个层级,媒体文件的归宿放在第三层级里一起说。
Django 的 staticfiles 应用自带一个开发用的文件服务视图,官方文档写得直白:它效率低下且不安全,只在 DEBUG=True 时工作,绝不能用于生产。也就是说,生产环境的默认分工是 Django 只管动态请求,静态文件交给别的东西。
第一步是把散落在各个应用目录里的静态文件收拢——collectstatic 命令把它们全部复制到 STATIC_ROOT 指定的目录。这一步没有争议;真正的问题是「谁来服务这个目录」,也就是三个层级的选型。
层级一:Web 服务器直接服务。 在 nginx 或 Caddy 里加一段配置,把 /static/ 路径指向 STATIC_ROOT,静态请求完全不进应用层。最传统、依赖最少,但缓存头要自己配,而且隐含一个前提:静态文件和 Web 服务器在同一个部署边界里。Docker 场景下这个前提容易破——容器里跑应用,Web 服务器在宿主机或另一个容器,STATIC_ROOT 目录就得跨边界共享,比较别扭。
层级二:WhiteNoise。 一个 pip 包加两行配置,静态文件由 Django 进程自己吐出来:
它在 collectstatic 时就把每个文件预压缩好 gzip 版本(装了 brotli 扩展包还有 brotli 版本),按内容生成指纹文件名,并自动给指纹文件发「永久缓存」响应头。请求不再依赖 Web 服务器的静态配置,Docker 部署下尤其省心:静态文件跟着镜像走,不存在目录共享问题。官方立场很克制:中等流量站点这个方案的性能已经够用,流量大了再加 CDN——因为缓存头是对的,CDN 加上去就能直接命中。
层级三:对象存储加 CDN。 静态文件和媒体文件都放进对象存储(OSS、COS、S3 都是一个思路),前面套一层 CDN 回源加速。这层的动机不只是快:媒体文件的可靠性问题在这个层级才被真正解决——单机磁盘和容器可写层都不可靠,对象存储才有冗余。但国内接入有一条准入红线要提前知道:阿里云和腾讯云的官方文档都写明,CDN 加速区域选「仅中国内地」或「全球」时,加速域名必须已完成 ICP 备案;只选境外节点则不作要求。域名没备案又想上 CDN,要么先补备案,要么接受境外节点在国内的访问质量。
| 维度 | Web 服务器直服 | WhiteNoise | 对象存储+CDN |
|---|---|---|---|
| 改造成本 | 低 | 最低(两行配置) | 高(存储+域名+备案) |
| 额外依赖 | 无 | 一个 pip 包 | 云服务按量付费 |
| 媒体文件可靠性 | 跟宿主机磁盘 | 跟宿主机磁盘 | 对象存储冗余 |
| Docker 适配 | 别扭 | 天然适配 | 天然适配 |
| 适用规模 | 单机传统部署 | 单机/容器小站 | 流量上量或多机 |
三个层级共用同一条铁律:文件名带指纹的可以永久缓存,不带的几乎不能缓存。
指纹文件名(形如 base.a4ef2389.css)意味着内容一变 URL 就变,浏览器和 CDN 缓存一年也不怕过期,响应头就是这句:
反过来,URL 固定而内容会变时,缓存发猛了用户就停在旧版本上。没有指纹机制只能发极短缓存——WhiteNoise 对未指纹文件的默认 max-age 只有 60 秒。CDN 的性能优势主要来自缓存命中率而不是带宽,这正是指纹机制如此重要的原因。
两条配套的坑,都和指纹机制的实现有关。其一,指纹查找依赖 collectstatic 生成的映射文件 staticfiles.json,运行时模板里查不到条目会直接抛 ValueError,最常见的根因是构建顺序:前端编译产物(比如 Tailwind 的输出)必须先构建、后收集;Docker 构建期跑 collectstatic 不需要数据库,但导入 settings 可能要求几个环境变量,给占位值即可。其二,回滚发布要小心:映射表只描述最近一次收集的结果,如果新版本删过某个文件,回滚后旧模板会查不到——回滚后重新跑一次 collectstatic 更稳。
静态文件方案没有高下,只有规模:单机小站用 WhiteNoise 两行配置,就能拿到接近专业方案的缓存行为;流量上来了再补对象存储和 CDN,缓存策略一行不用改。真正不能省的是那条缓存铁律——它决定了后面每一层加速能不能吃到。如果你也在上线时被「页面全裸」吓过,把这篇转给同样在路上的人。
#Django #Python #Web开发 #网站部署 #CDN #后端开发