在Python生态下,虽然有Flask、FastAPI等后来者居上的Web框架;但不可否认的是,依然有很多企业、组织和开发者仍在使用Django框架,或是有遗留的项目在使用Django。
本文汇总整理几个Django生态的开发框架。
Django Admin
Django-Compressor
开源(GitHub,2.9K Star,608 Fork)静态资源压缩器,可在Django模板里把CSS、JS合并并压缩成可缓存的静态文件,也支持离线压缩,把资源处理从请求链路里提前挪到部署阶段,官方文档。
特点
- 接入位置自然:直接工作在Django模板层。用
{% compress css %}和{% compress js %}包住资源片段,自动解析link、style、script,再输出压缩后的静态文件引用; - 缓存友好:压缩后的文件名和内容有关,内容变化才会生成新的文件名。浏览器可以放心做长缓存,线上资源更新时也不容易遇到"缓存没刷新"的问题。
- 扩展空间足:支持CSS/JS过滤器、预编译器、不同HTML解析器、远程存储和离线压缩。简单项目可先用默认配置,复杂项目也能继续扩展到SASS、LESS、S3、CDN等场景。
适用场景
- Django项目里CSS/JS较多,希望减少请求数和静态文件体积
- 需要把资源文件名和内容绑定,方便浏览器做长缓存
- 部署链路里可以提前执行
manage.py compress - 项目需要接入SASS、LESS、JS minifier或自定义资源处理流程
实战
基于pip安装:pip install django-compressor
用模板标签压缩CSS和JS:当一个页面里同时引用多个 CSS/JS 文件和少量内联代码时,希望最终输出变成更少的静态资源请求,同时让文件名随内容变化。
py
from pathlib import Path
from tempfile import TemporaryDirectory
from django.conf import settings
import django
from django.template.loader import render_to_string
with TemporaryDirectory() as tmp:
root = Path(tmp)
static_dir = root / "static"
out_dir = root / "compressed"
tpl_dir = root / "templates"
(static_dir / "css").mkdir(parents=True)
(static_dir / "js").mkdir(parents=True)
tpl_dir.mkdir()
(static_dir / "css" / "base.css").write_text("body { color: #333; }\n.card { padding: 16px; }\n", encoding="utf-8")
(static_dir / "js" / "app.js").write_text("function hello(name) { console.log('hello ' + name); }\nhello('mumu');\n", encoding="utf-8")
(tpl_dir / "demo.html").write_text("""{% load compress %}
{% compress css %}
<link rel="stylesheet" href="/static/css/base.css">
<style>.title { font-weight: 700; color: #0b5; }</style>
{% endcompress %}
{% compress js %}
<script src="/static/js/app.js"></script>
<script>console.log("inline js");</script>
{% endcompress %}
""", encoding="utf-8")
settings.configure(
SECRET_KEY="demo",
DEBUG=False,
INSTALLED_APPS=["django.contrib.staticfiles", "compressor"],
STATIC_URL="/static/",
STATICFILES_DIRS=[str(static_dir)],
STATIC_ROOT=str(out_dir),
COMPRESS_ROOT=str(out_dir),
COMPRESS_URL="/static/",
STATICFILES_FINDERS=[
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
"compressor.finders.CompressorFinder",
],
TEMPLATES=[{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [str(tpl_dir)],
"APP_DIRS": True,
}],
STORAGES={
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
"compressor": {"BACKEND": "compressor.storage.CompressorFileStorage"},
},
COMPRESS_ENABLED=True,
COMPRESS_OFFLINE=False,
)
django.setup()
print(render_to_string("demo.html").strip())
compressor.finders.CompressorFinder让compressor能找到静态资源,COMPRESS_ROOT决定压缩后的文件写到哪里。真实项目里通常还会配合 collectstatic、CDN和静态文件存储一起使用。
功能二:把小段样式直接内联压缩
有些首屏关键样式很短,不一定值得单独生成文件。使用inline模式,让压缩结果以内联方式输出。
py
from pathlib import Path
from tempfile import TemporaryDirectory
from django.template.loader import render_to_string
with TemporaryDirectory() as tmp:
root = Path(tmp)
out_dir = root / "compressed"
tpl_dir = root / "templates"
tpl_dir.mkdir()
(tpl_dir / "inline.html").write_text("""{% load compress %}
{% compress css inline %}
<style>
.notice { color: #0b5; padding: 12px; }
.notice strong { font-weight: 700; }
</style>
{% endcompress %}
""", encoding="utf-8")
// 省略部分重复代码片段
print(render_to_string("inline.html").strip())
内联模式适合很小的关键样式或特殊片段。不要把大量 CSS 都塞进 HTML 里,否则页面体积会变大,也会削弱浏览器缓存静态文件的收益。
线上环境更推荐把压缩过程提前到部署阶段,用manage.py compress生成压缩文件和manifest。这样请求到来时不必再临时解析模板和写文件,运行时行为更稳定。开启 COMPRESS_OFFLINE=True,在部署时统一生成资源文件。
py
from pathlib import Path
from tempfile import TemporaryDirectory
from django.core.management import call_command
with TemporaryDirectory() as tmp:
root = Path(tmp)
static_dir = root / "static"
out_dir = root / "compressed"
tpl_dir = root / "templates"
(static_dir / "css").mkdir(parents=True)
tpl_dir.mkdir()
(static_dir / "css" / "dashboard.css").write_text(".card { padding: 16px; color: #245; }\n", encoding="utf-8")
(tpl_dir / "dashboard.html").write_text("""{% load compress %}
{% compress css %}
<link rel="stylesheet" href="/static/css/dashboard.css">
<style>.badge { border-radius: 999px; padding: 4px 8px; }</style>
{% endcompress %}
""", encoding="utf-8")
settings.configure(
// 省略部分代码
COMPRESS_ENABLED=True,
COMPRESS_OFFLINE=True,
)
django.setup()
call_command("compress", verbosity=1, force=True)
离线压缩的核心产物是压缩文件和manifest。部署时需要把这些产物一起发布到静态文件存储或CDN。
注意事项
- 确认
compressor.finders.CompressorFinder已加入STATICFILES_FINDERS。 - 确认
COMPRESS_ROOT、STATIC_ROOT、COMPRESS_URL和 CDN 地址一致。 - 线上启用离线压缩时,把
manage.py compress放进部署流程。 - 检查压缩输出目录、manifest 和静态文件存储是否一起发布。
- 避免在模板里混入过多动态上下文,减少离线压缩找不到缓存的风险。
Django-Grappelli
官网,开源(GitHub,3.9K Star,651 Fork)Django Admin增强:一边把交互和布局做得更顺手,一边把很多后台常见的小痛点,比如原始字段联想、可拖拽 inline、dashboard 组合这些能力,一起收拢成了成体系的扩展点,官方文档。
特点
- 后台体验:界面更顺眼,调整后台导航、关联选择、过滤器等交互;
- 很多增强点都做成了可配置接口:从后台标题、切换用户,到 autocomplete 查找和 change list 模板,Grappelli 不是靠魔改 admin 模板硬怼,而是留了清晰的 settings、ModelAdmin 属性和 dashboard API。
- 已有Django Admin项目很友好:迁移成本比重新做管理后台小得多。
适用场景
- 已经在用 Django Admin,但觉得默认后台体验太素、太散
- 想增强关联选择、inline 排序、dashboard 这些后台高频交互
- 团队希望保留原生 admin 的稳定性,又想把管理后台做得更像产品
- 需要一套比"纯换主题"更完整、但又不至于重做后台的方案
实战
基于pip安装:pip install django-grappelli
环境验证:
py
import django
import tempfile
from django.conf import settings
from pathlib import Path
from django.contrib import admin
from django.urls import include, path, reverse
from grappelli import settings as grp_settings
DB = Path(tempfile.gettempdir()) / "grappelli_demo1.sqlite3"
if DB.exists():
DB.unlink()
settings.configure(
SECRET_KEY="demo",
ROOT_URLCONF=__name__,
STATIC_URL="/static/",
USE_TZ=True,
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": str(DB)}},
INSTALLED_APPS=[
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"grappelli",
],
MIDDLEWARE=[],
TEMPLATES=[{"BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, "OPTIONS": {"context_processors": []}}],
GRAPPELLI_ADMIN_TITLE="Mumu Ops Hub",
GRAPPELLI_SWITCH_USER=True,
)
django.setup()
urlpatterns = [
path("grappelli/", include("grappelli.urls")),
path("admin/", admin.site.urls),
]
print("admin title :", grp_settings.ADMIN_TITLE)
print("admin index :", reverse("admin:index"))
print("lookup url :", reverse("grp_related_lookup"))
print("switch user :", reverse("grp_switch_user", args=[7]))
GrappelliSortableHiddenMixin
py
from django import forms
from grappelli.forms import GrappelliSortableHiddenMixin
class DummyBase:
def formfield_for_dbfield(self, db_field, request, **kwargs):
widget = kwargs.get("widget") or forms.TextInput()
return type("Field", (), {"widget": widget})()
class DemoInline(GrappelliSortableHiddenMixin, DummyBase):
sortable_field_name = "position"
class DbField:
def __init__(self, name):
self.name = name
inline = DemoInline()
print("position widget:", type(inline.formfield_for_dbfield(DbField("position"), None).widget).__name__)
print("title widget :", type(inline.formfield_for_dbfield(DbField("title"), None).widget).__name__)
print("sortable field :", inline.sortable_field_name)
把后台首页当成了可以编排的 dashboard,而不是一块固定模板。你可以把快捷入口、监控分组、最近操作这些模块自由拼起来,做出更像"团队工作台"的管理首页。
原生 admin 首页通常更像入口列表,但很多团队真正想要的是"打开后台就先看到关键入口和关键状态"。Grappelli 的 dashboard API 正是为了把首页做成可组合模块。
py
from django.conf import settings
if not settings.configured:
settings.configure(SECRET_KEY="demo", USE_I18N=False, INSTALLED_APPS=["grappelli"])
import django
django.setup()
from grappelli.dashboard import Dashboard, modules
class OpsDashboard(Dashboard):
title = "Ops Dashboard"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.children.append(modules.LinkList(
title="Quick Links",
column=1,
children=[
("Django Docs", "https://docs.djangoproject.com/", True, "official docs", "_blank"),
("Release Board", "/admin/releases/"),
],
))
self.children.append(modules.Group(
title="Watchers",
column=2,
children=[
modules.LinkList(title="Core Services", column=2, children=[("Nginx", "/admin/nginx/")]),
modules.LinkList(title="Task Queue", column=2, children=[("Celery", "/admin/celery/")]),
],
))
board = OpsDashboard()
quick = board.children[0]
quick.init_with_context({})
group = board.children[1]
print("dashboard title:", board.title)
print("module count :", len(board.children))
print("first link :", quick.children[0]["title"], quick.children[0]["target"])
print("group css :", group.render_css_classes())
注意事项
- 先确认 grappelli 在 INSTALLED_APPS 里的位置和 urls.py 的挂载顺序正确,再去改更细的 admin 行为。
- 用少量 ModelAdmin 和 inline 先试跑排序、autocomplete、change list 模板,别一口气全后台替换。
- 如果要做 dashboard,自定义模块时优先保证入口清晰和权限边界明确,不要把首页堆成信息垃圾场。
django-rules
权限规则编排方向开源(GitHub,2K Star,152 Fork)项目,把用户权限拆成可组合的谓词(predicate) ,可把谓词挂到Django的权限检查链路里。相比把判断逻辑散落在view、serializer和model各个层,更利于复用、审计、问题排查。
特点
- 规则组合:基于谓词,可把每个判断拆成最小单元,再用
&、|、~组合成真正的业务规则; - 可独立运行:底层是通用规则引擎,适用于各种Python项目;
- 不依赖额外权限表:更偏向运行时判定,基于用户、对象和上下文信息直接判断,维护成本会轻不少。
适用场景
- Django 项目已经有认证体系,但对象级权限逻辑开始变复杂
- 想把权限判断从业务代码里抽出来统一管理
- 需要一套既能独立测试、又能挂到
has_perm上的规则表达方式 - 后续再接Admin、CBV、DRF时还能延续同一套规则
实战
安装:pip install rules
接入 Django,别忘了把 rules 放进 INSTALLED_APPS,并把 rules.permissions.ObjectPermissionBackend 加到 AUTHENTICATION_BACKENDS 里。
用 predicate 把这些条件收拢起来
py
import rules
from dataclasses import dataclass, field
@dataclass
class Ticket:
owner: str
reviewers: set[str] = field(default_factory=set)
closed: bool = False
@rules.predicate
def is_owner(user, ticket):
return ticket.owner == user
@rules.predicate
def is_reviewer(user, ticket):
return user in ticket.reviewers
@rules.predicate
def not_closed(user, ticket):
return not ticket.closed
rules.set_rule("can_edit_ticket", (is_owner | is_reviewer) & not_closed)
draft = Ticket(owner="mumu", reviewers={"ops", "qa"})
archived = Ticket(owner="mumu", reviewers={"ops"}, closed=True)
for actor, ticket in [
("mumu", draft),
("ops", draft),
("guest", draft),
("ops", archived),
]:
print(f"{actor:>5} -> {rules.test_rule('can_edit_ticket', actor, ticket)}")
把同一套规则直接接进 Django 权限链路
py
import django
import rules
import tempfile
from django.conf import settings
from django.contrib.auth.models import Group, User
from django.core.management import call_command
from pathlib import Path
DB = Path(tempfile.gettempdir()) / "rules_demo2.sqlite3"
if DB.exists():
DB.unlink()
settings.configure(
SECRET_KEY="demo",
USE_TZ=True,
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": str(DB)}},
INSTALLED_APPS=[
"django.contrib.auth",
"django.contrib.contenttypes",
"rules",
],
AUTHENTICATION_BACKENDS=(
"rules.permissions.ObjectPermissionBackend",
"django.contrib.auth.backends.ModelBackend",
),
)
django.setup()
call_command("migrate", run_syncdb=True, verbosity=0)
class Book:
def __init__(self, author, state):
self.author = author
self.state = state
@rules.predicate
def is_book_author(user, book):
return bool(user and book and book.author == user)
is_editor = rules.is_group_member("editors")
rules.add_perm("library.change_book", is_book_author | is_editor)
rules.add_perm("library.publish_book", is_book_author & rules.is_staff)
alice = User.objects.create_user("alice")
bob = User.objects.create_user("bob")
carol = User.objects.create_user("carol")
editors = Group.objects.create(name="editors")
bob.groups.add(editors)
book = Book(author=alice, state="draft")
print("alice change :", alice.has_perm("library.change_book", book))
print("bob change :", bob.has_perm("library.change_book", book))
print("carol change :", carol.has_perm("library.change_book", book))
print("alice publish:", alice.has_perm("library.publish_book", book))
invocation context,可以让同一轮规则求值过程共享上下文数据。很适合把一次昂贵计算缓存下来,后面的 predicate 直接复用。当你的权限判断依赖上下文信息时,不一定要在每个 predicate 里重复计算。先缓存,再复用,组合表达式会更清晰。
py
import rules
@rules.predicate(bind=True)
def cache_region(self, user, service):
region = service["region"]
self.context["region"] = region
print("cache region :", region)
return True
@rules.predicate(bind=True)
def same_region(self, user, service):
print("reuse cached data :", self.context.get("region"))
return user["region"] == self.context.get("region")
can_deploy = cache_region & same_region
print("cn user ->", can_deploy.test({"region": "cn"}, {"region": "cn"}))
print("us user ->", can_deploy.test({"region": "us"}, {"region": "cn"}))
前一个 predicate 把 region 放进上下文,后一个 predicate 直接复用,不需要把相同数据再查一遍。
最佳实践
- 先把 predicate 命名成业务语言,再组合成 rule,避免后面只剩一坨布尔表达式。
- 接入 Django 时确认
AUTHENTICATION_BACKENDS顺序和INSTALLED_APPS配置正确,再做一次真实的has_perm回归。 - 对复杂规则补充单元测试,尤其是 owner、group、上下文条件叠加时的边界分支。