Django生态:Django-admin、Django-Compressor、Django-grappelli、django-rules 在Python生态下虽然有Flask、FastAPI等后来者居上的Web框架但不可否认的是依然有很多企业、组织和开发者仍在使用Django框架或是有遗留的项目在使用Django。本文汇总整理几个Django生态的开发框架。Django AdminDjango-Compressor开源GitHub2.9K Star608 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 文件和少量内联代码时希望最终输出变成更少的静态资源请求同时让文件名随内容变化。frompathlibimportPathfromtempfileimportTemporaryDirectoryfromdjango.confimportsettingsimportdjangofromdjango.template.loaderimportrender_to_stringwithTemporaryDirectory()astmp:rootPath(tmp)static_dirroot/staticout_dirroot/compressedtpl_dirroot/templates(static_dir/css).mkdir(parentsTrue)(static_dir/js).mkdir(parentsTrue)tpl_dir.mkdir()(static_dir/css/base.css).write_text(body { color: #333; }\n.card { padding: 16px; }\n,encodingutf-8)(static_dir/js/app.js).write_text(function hello(name) { console.log(hello name); }\nhello(mumu);\n,encodingutf-8)(tpl_dir/demo.html).write_text({% load compress %} {% compress css %} link relstylesheet href/static/css/base.css style.title { font-weight: 700; color: #0b5; }/style {% endcompress %} {% compress js %} script src/static/js/app.js/script scriptconsole.log(inline js);/script {% endcompress %} ,encodingutf-8)settings.configure(SECRET_KEYdemo,DEBUGFalse,INSTALLED_APPS[django.contrib.staticfiles,compressor],STATIC_URL/static/,STATICFILES_DIRS[str(static_dir)],STATIC_ROOTstr(out_dir),COMPRESS_ROOTstr(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_ENABLEDTrue,COMPRESS_OFFLINEFalse,)django.setup()print(render_to_string(demo.html).strip())compressor.finders.CompressorFinder让compressor能找到静态资源COMPRESS_ROOT决定压缩后的文件写到哪里。真实项目里通常还会配合collectstatic、CDN和静态文件存储一起使用。功能二把小段样式直接内联压缩有些首屏关键样式很短不一定值得单独生成文件。使用inline模式让压缩结果以内联方式输出。frompathlibimportPathfromtempfileimportTemporaryDirectoryfromdjango.template.loaderimportrender_to_stringwithTemporaryDirectory()astmp:rootPath(tmp)out_dirroot/compressedtpl_dirroot/templatestpl_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 %} ,encodingutf-8)//省略部分重复代码片段print(render_to_string(inline.html).strip())内联模式适合很小的关键样式或特殊片段。不要把大量 CSS 都塞进 HTML 里否则页面体积会变大也会削弱浏览器缓存静态文件的收益。线上环境更推荐把压缩过程提前到部署阶段用manage.py compress生成压缩文件和manifest。这样请求到来时不必再临时解析模板和写文件运行时行为更稳定。开启COMPRESS_OFFLINETrue在部署时统一生成资源文件。frompathlibimportPathfromtempfileimportTemporaryDirectoryfromdjango.core.managementimportcall_commandwithTemporaryDirectory()astmp:rootPath(tmp)static_dirroot/staticout_dirroot/compressedtpl_dirroot/templates(static_dir/css).mkdir(parentsTrue)tpl_dir.mkdir()(static_dir/css/dashboard.css).write_text(.card { padding: 16px; color: #245; }\n,encodingutf-8)(tpl_dir/dashboard.html).write_text({% load compress %} {% compress css %} link relstylesheet href/static/css/dashboard.css style.badge { border-radius: 999px; padding: 4px 8px; }/style {% endcompress %} ,encodingutf-8)settings.configure(//省略部分代码 COMPRESS_ENABLEDTrue,COMPRESS_OFFLINETrue,)django.setup()call_command(compress,verbosity1,forceTrue)离线压缩的核心产物是压缩文件和manifest。部署时需要把这些产物一起发布到静态文件存储或CDN。注意事项确认compressor.finders.CompressorFinder已加入STATICFILES_FINDERS。确认COMPRESS_ROOT、STATIC_ROOT、COMPRESS_URL和 CDN 地址一致。线上启用离线压缩时把manage.py compress放进部署流程。检查压缩输出目录、manifest 和静态文件存储是否一起发布。避免在模板里混入过多动态上下文减少离线压缩找不到缓存的风险。Django-Grappelli官网开源GitHub3.9K Star651 ForkDjango Admin增强一边把交互和布局做得更顺手一边把很多后台常见的小痛点比如原始字段联想、可拖拽 inline、dashboard 组合这些能力一起收拢成了成体系的扩展点官方文档。特点后台体验界面更顺眼调整后台导航、关联选择、过滤器等交互很多增强点都做成了可配置接口从后台标题、切换用户到 autocomplete 查找和 change list 模板Grappelli 不是靠魔改 admin 模板硬怼而是留了清晰的 settings、ModelAdmin 属性和 dashboard API。已有Django Admin项目很友好迁移成本比重新做管理后台小得多。适用场景已经在用 Django Admin但觉得默认后台体验太素、太散想增强关联选择、inline 排序、dashboard 这些后台高频交互团队希望保留原生 admin 的稳定性又想把管理后台做得更像产品需要一套比“纯换主题”更完整、但又不至于重做后台的方案实战基于pip安装pip install django-grappelli环境验证importdjangoimporttempfilefromdjango.confimportsettingsfrompathlibimportPathfromdjango.contribimportadminfromdjango.urlsimportinclude,path,reversefromgrappelliimportsettingsasgrp_settings DBPath(tempfile.gettempdir())/grappelli_demo1.sqlite3ifDB.exists():DB.unlink()settings.configure(SECRET_KEYdemo,ROOT_URLCONF__name__,STATIC_URL/static/,USE_TZTrue,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_TITLEMumu Ops Hub,GRAPPELLI_SWITCH_USERTrue,)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]))GrappelliSortableHiddenMixinfromdjangoimportformsfromgrappelli.formsimportGrappelliSortableHiddenMixinclassDummyBase:defformfield_for_dbfield(self,db_field,request,**kwargs):widgetkwargs.get(widget)orforms.TextInput()returntype(Field,(),{widget:widget})()classDemoInline(GrappelliSortableHiddenMixin,DummyBase):sortable_field_namepositionclassDbField:def__init__(self,name):self.namename inlineDemoInline()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 正是为了把首页做成可组合模块。fromdjango.confimportsettingsifnotsettings.configured:settings.configure(SECRET_KEYdemo,USE_I18NFalse,INSTALLED_APPS[grappelli])importdjango django.setup()fromgrappelli.dashboardimportDashboard,modulesclassOpsDashboard(Dashboard):titleOps Dashboarddef__init__(self,**kwargs):super().__init__(**kwargs)self.children.append(modules.LinkList(titleQuick Links,column1,children[(Django Docs,https://docs.djangoproject.com/,True,official docs,_blank),(Release Board,/admin/releases/),],))self.children.append(modules.Group(titleWatchers,column2,children[modules.LinkList(titleCore Services,column2,children[(Nginx,/admin/nginx/)]),modules.LinkList(titleTask Queue,column2,children[(Celery,/admin/celery/)]),],))boardOpsDashboard()quickboard.children[0]quick.init_with_context({})groupboard.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权限规则编排方向开源GitHub2K Star152 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 把这些条件收拢起来importrulesfromdataclassesimportdataclass,fielddataclassclassTicket:owner:strreviewers:set[str]field(default_factoryset)closed:boolFalserules.predicatedefis_owner(user,ticket):returnticket.owneruserrules.predicatedefis_reviewer(user,ticket):returnuserinticket.reviewersrules.predicatedefnot_closed(user,ticket):returnnotticket.closed rules.set_rule(can_edit_ticket,(is_owner|is_reviewer)not_closed)draftTicket(ownermumu,reviewers{ops,qa})archivedTicket(ownermumu,reviewers{ops},closedTrue)foractor,ticketin[(mumu,draft),(ops,draft),(guest,draft),(ops,archived),]:print(f{actor:5}-{rules.test_rule(can_edit_ticket,actor,ticket)})把同一套规则直接接进 Django 权限链路importdjangoimportrulesimporttempfilefromdjango.confimportsettingsfromdjango.contrib.auth.modelsimportGroup,Userfromdjango.core.managementimportcall_commandfrompathlibimportPath DBPath(tempfile.gettempdir())/rules_demo2.sqlite3ifDB.exists():DB.unlink()settings.configure(SECRET_KEYdemo,USE_TZTrue,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_syncdbTrue,verbosity0)classBook:def__init__(self,author,state):self.authorauthor self.statestaterules.predicatedefis_book_author(user,book):returnbool(userandbookandbook.authoruser)is_editorrules.is_group_member(editors)rules.add_perm(library.change_book,is_book_author|is_editor)rules.add_perm(library.publish_book,is_book_authorrules.is_staff)aliceUser.objects.create_user(alice)bobUser.objects.create_user(bob)carolUser.objects.create_user(carol)editorsGroup.objects.create(nameeditors)bob.groups.add(editors)bookBook(authoralice,statedraft)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 里重复计算。先缓存再复用组合表达式会更清晰。importrulesrules.predicate(bindTrue)defcache_region(self,user,service):regionservice[region]self.context[region]regionprint(cache region :,region)returnTruerules.predicate(bindTrue)defsame_region(self,user,service):print(reuse cached data :,self.context.get(region))returnuser[region]self.context.get(region)can_deploycache_regionsame_regionprint(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、上下文条件叠加时的边界分支。