做Django开发这几年我经常被问到同一个问题新项目一创建出来那一堆文件夹到底是干什么的说实话我自己刚入门那会儿也对着目录发过呆——manage.py、settings.py、urls.py每个文件单独看都认识合在一起就不知道它们之间怎么配合了。后来带过几个新人发现大家踩的坑都很相似要么往settings.py里堆业务逻辑要么把模板文件放错位置导致渲染不到要么迁移文件搞乱了干脆删库重来。这些问题的根源几乎都指向一件事——没有真正理解Django项目目录结构的设计逻辑。这篇内容我就把自己对典型Django项目目录结构的理解、使用经验、以及实际项目中踩过的坑一次性梳理清楚。不管你是刚装好Django准备写第一个项目的新手还是已经写了一段时间但总觉得目录组织得别扭的开发者这篇都值得花几分钟看完。理解了这个结构你后续写模型、配路由、拆应用都会顺畅很多。1. 项目整体形态与创建方式1.1 先搞清楚两个命令的区别Django项目一开始会执行两条命令django-admin startproject和python manage.py startapp。很多人刚开始分不清这两者的关系其实一句话就能记住startproject创建的是整个项目的骨架startapp创建的是项目里的一个功能模块。举个例子你开了一家餐厅startproject相当于把餐厅的营业执照、房屋产权、水电总闸这些基础设施办好了startapp相当于在餐厅里划分出后厨、前台、仓库这些功能区域。一个项目可以有很多个app就像一家餐厅可以有很多个功能区域但整个餐厅只有一个总的管理入口。我见过不少新手把业务代码直接写在项目的根目录文件里比如在urls.py里写视图函数在settings.py里写业务判断。这样做小项目跑得通但一旦功能变多所有代码堆在一起后面改一行代码可能要翻半天文件。正确做法是项目目录只负责配置和调度业务逻辑全部下沉到各个app里。1.2 一个典型项目的完整目录长什么样用最新版本的Django创建项目后目录结构大致如下myproject/ # 项目根目录名字可以自定义 ├── manage.py # 命令行工具入口 ├── myproject/ # 项目配置目录与外层同名容易懵 │ ├── __init__.py # 标识这是一个Python包 │ ├── settings.py # 全局配置文件 │ ├── urls.py # 根路由配置文件 │ ├── asgi.py # ASGI服务器入口 │ └── wsgi.py # WSGI服务器入口 ├── app1/ # 第一个应用 │ ├── __init__.py │ ├── admin.py # 后台管理配置 │ ├── apps.py # 应用配置类 │ ├── models.py # 数据模型 │ ├── tests.py # 测试代码 │ ├── views.py # 视图函数 │ └── migrations/ # 数据迁移记录 │ └── __init__.py ├── templates/ # 模板目录通常手动创建 ├── static/ # 静态文件目录通常手动创建 └── requirements.txt # 依赖清单通常手动创建看到这里你可能会问myproject目录下又套了一个myproject这不是多此一举吗我刚学的时候也疑惑过。其实外层myproject是你在命令行里cd进去操作的那个目录可以理解为项目的外壳内层myproject才是Django真正读取配置的地方。我习惯把内层目录看作配置中心所有全局性的设置都从这里发出指令。1.3 为什么是这个形态一切为了分离Django目录结构的设计初衷可以用一句话概括配置与业务分离、项目与应用分离。这个理念听起来抽象实际操作中非常直观。比如你的站点上线后需要改数据库连接、调调试模式、换密钥这些都属于配置层面的改动集中在settings.py里处理就好而如果你要加一个用户评论功能这是业务层面的需求新建一个app并在里面写模型和视图就好。两者互不干扰团队协作时也可以让不同人负责不同的app互不冲突。我在实际项目里带过两个新人并行开发一个负责订单模块、一个负责支付模块每人各开一个app几个月下来代码几乎没冲突过——这就是目录结构清晰带来的直接好处。2. 项目配置目录逐文件拆解2.1 settings.py整个项目的总控室settings.py是Django项目里被引用最多的文件它几乎决定了项目运行的所有基础参数。我把里面最常用、也最容易出问题的几个配置项单独拿出来讲。INSTALLED_APPS已安装应用的列表。每次新建app后你需要把app名字加到这里面Django才会识别它。我见过太多新手建好app却忘了注册结果python manage.py makemigrations提示No changes detected跑起来页面404。实际上Django 4.0以后新建的app在apps.py里会自动生成一个配置类INSTALLED_APPS里直接写app1.apps.App1Config也是常见的写法但大多数人还是习惯直接写app1两种写法都行重要的是别漏。DATABASES数据库连接配置。默认是sqlite3开发阶段够用部署时换成postgresql或mysql很常见。这里有个经验之谈不要把数据库配置直接写死在settings.py里尤其涉及密码、主机名这些敏感信息时更推荐用环境变量管理。后面我会专门讲环境变量这块。TEMPLATES模板引擎配置。里面有一个DIRS选项默认是空列表。如果你打算用全局模板目录而不是每个app内部的templates/需要在这里加上BASE_DIR / templates。我自己的习惯是小型项目全都放全局模板目录项目大了以后每个app维护自己的模板目录DIRS留空。两种方式各有优劣关键在于团队约定一致。STATIC_URL / MEDIA_URL静态文件和媒体文件的前缀路径。这一块容易串简单说static放的是CSS、JS、图片这些不会变的资源media放的是用户上传的文件比如头像、附件。开发阶段需要用django.contrib.staticfiles这个app帮你在调试模式下去各app的static/里找文件生产环境则交给Nginx这类服务器处理。2.2 urls.py路由分发的中枢根路由配置文件urls.py里做的最主要事情就是把不同路径的请求分发给不同app处理。典型写法是这样from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(app1/, include(app1.urls)), ]这里有个很多人忽略的细节include()把请求继续下放到app自己的urls.py处理app内部的URL配置可以按功能模块继续拆分实现两级路由。比如app1/urls.py里可以继续写urlpatterns [ path(list/, views.list_view, namelist), path(detail/int:pk/, views.detail_view, namedetail), ]这样访问/app1/list/和/app1/detail/2/就分别对应两个视图函数了。路由匹配的顺序是自上而下的Django遇到第一个匹配成功的模式就停止查找。所以如果你有两个路径分别为list/和str:name/务必把更具体的list/写在前面否则list会被当作name参数捕获导致页面渲染异常。这个坑我踩过一次排查了很久才发现是路由顺序问题。2.3 wsgi.py 与 asgi.py项目上线的入口wsgi.py和asgi.py这两个文件平时开发几乎用不到但在部署时它们就是项目的大门。wsgi.pyWeb Server Gateway Interface传统同步服务器如Gunicorn通过这个文件加载Django应用。asgi.pyAsynchronous Server Gateway Interface支持异步的服务器如Uvicorn、Daphne通过这个文件加载应用也兼容传统的WSGI模式。它们的代码结构几乎相同默认都是get_wsgi_application()或get_asgi_application()。需要记住的实操点部署命令里指的就是这个文件路径。比如用Gunicorn启动时命令通常是gunicorn myproject.wsgi:application如果你把项目目录名改了启动命令里的路径也要对应修改。我还遇到过一种情况服务器上同时跑了两个Django项目因为PYTHONPATH环境变量没配好Gunicorn加载到了错误的wsgi文件结果两个项目互相串配置。后来统一用虚拟环境并显式指定--chdir和--pythonpath才彻底解决。2.4init.pyPython包的身份证__init__.py在Django项目里很容易被忽略因为它的内容通常是空的。但它的存在意义重大标识当前目录是一个Python包这样Python才能用import语句把你项目里的模块加载进来。Python 3.3以后引入了隐式命名空间包机制理论上没有__init__.py也能导入但Django官方以及绝大多数第三方库仍然依赖它所以新建app时生成的__init__.py建议保留不要动它。还有个冷门技巧如果你需要在app加载时执行某些初始化操作可以在这个文件里写代码比如注册信号处理器default_app_config app1.apps.App1Config # 或者直接导入信号模块 from . import signals实测下来这种方式在部分Django版本里会给出弃用警告新版本更推荐在AppConfig.ready()方法里做初始化这个我放到apps.py里详细说。3. 应用目录与关键文件详解3.1 models.py数据结构的定义场所模型Model是Django里最核心的业务代码之一它定义了你需要什么样的数据表、字段有哪些、表之间如何关联。一个简单的模型示例from django.db import models class Article(models.Model): title models.CharField(max_length200) content models.TextField() created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at] def __str__(self): return self.title模型写好之后需要依次执行两条命令让数据库真正发生变化python manage.py makemigrations python manage.py migratemakemigrations会根据你的模型生成迁移文件相当于把当前的数据结构拍了拍照片migrate会把这张照片冲洗到数据库里。这两条命令的关系我习惯用一个餐厅的比喻makemigrations是厨师把新菜的做法写成菜谱migrate是照着菜谱把菜真正做出来。菜谱迁移文件可以反复修改菜数据库表结构一旦定型改动成本会成倍上升。3.2 migrations目录数据库的版本管理migrations/目录里保存着一个app全部迁移历史0001_initial.py、0002_alter_xxx.py这类文件就是地层层层的地质记录。它们的核心价值在于让数据库结构可以像代码一样版本化。团队协作时你拉取同事的新代码往往需要跑一下python manage.py migrate把数据库结构更新到最新。如果迁移文件冲突两个人同时修改了同一个模型解决起来比较麻烦。我的经验是尽量让每个ap的迁移文件保持线性增长不要随意删改已提交的迁移文件。很多人遇到字段对不上的问题第一反应是删掉migrations目录重新生成。我强烈不建议这样操作因为生产环境一旦执行过migrate你删掉迁移文件等于破坏了数据库结构的历史记录后续部署时Django会认为没有迁移可执行但数据库里其实表还在最终导致模型字段和真实表结构脱节。正确做法是新增一条反向迁移migrate app_name 上一个迁移编号先回退再修改模型生成新的迁移。3.3 views.py请求与响应的处理中心视图函数或视图类负责接收浏览器请求处理业务逻辑最后返回响应内容。最简单的视图长这样from django.http import JsonResponse def health_check(request): return JsonResponse({status: ok})当业务逻辑变复杂我建议把views.py按功能拆分成多个模块而不是在一个文件里堆几百个函数。比如在app目录下新建views/文件夹内部放account_views.py、order_views.py等然后把原来的views.py删除——注意from .views import xxx这种写法会失效需要改成from .views.account_views import xxx。这里有个设计层面的建议视图层尽量保持薄也就是不要在视图里堆大量复杂业务逻辑。你可以把重计算、数据库操作封装到services.py或utils.py里让视图只负责收请求→调服务→返回结果。这样做的好处是写测试时可以直接针对服务层测试不需要通过HTTP请求效率高很多。3.4 admin.py 与 apps.py容易被忽略的两个文件admin.py是Django自带的后台管理功能你注册模型后就能在/admin/页面里增删改查数据后台界面简单又好用。注册模型的方式from django.contrib import admin from .models import Article admin.site.register(Article)对很多内部管理系统来说Django admin甚至可以直接当作完整的后台来用。我之前给一个小型团队做过一个内部数据管理工具只写了模型和admin配置没写任何前端页面两周搞定上线——这就是Django admin的价值。apps.py里定义了app的配置类。Django 4.0之后INSTALLED_APPS默认推荐写app1.apps.App1Config这种完整路径这样Django能正确识别配置类。如果你需要在应用启动时执行一些初始化代码比如定时任务注册、缓存预热、信号连接在AppConfig里重写ready()方法是最标准的方式from django.apps import AppConfig class App1Config(AppConfig): default_auto_field django.db.models.BigAutoField name app1 def ready(self): from . import signals需要注意的一个坑ready()在项目启动时会被多次调用比如运行测试、执行manage.py命令时所以信号连接要写成幂等的形式避免重复注册导致信号处理函数被执行两遍。3.5 模板目录与静态文件目录Django的装修队模板Template就是HTML文件负责数据如何展示静态文件Static是CSS、JavaScript、图片负责页面长得怎么样。Django默认约定每个app内部的templates/和static/目录会被自动识别模板引擎会到各个app的templates目录下寻找模板文件。这里面有一个非常经典的坑模板文件命名冲突。假如你有两个appblog和news它们都有templates/index.html这个文件Django在渲染index.html时会根据INSTALLED_APPS的顺序找到第一个匹配就停止结果可能渲染出错误的模板。解决方法是在模板目录里再加一层以app名字命名的子目录即templates/blog/index.html、templates/news/index.html。这也是Django官方推荐的模板组织方式我强烈建议从一开始就养成这个习惯。静态文件的组织方式同理static/目录下先按app或模块分层比如static/blog/css/style.css这样{% static blog/css/style.css %}引用时就不会冲突。4. 实际项目中的目录演进与组织策略4.1 从单app到多app的拆解思路很多初学者会犯一个错误把整个项目所有功能都写在一个app里。刚开始还好但一旦功能模块增多models.py里可能堆了几十个类views.py上千行维护难度直线上升。我个人的拆分标准很简单如果两个功能的数据模型关联度不高或者它们会分别被不同的团队维护就应该拆成不同的app。例如一个典型的商城项目我通常会拆成accounts用户、products商品、orders订单、payments支付等几个app。这样每个app内有自己独立的models.py、views.py、urls.py、migrations/团队协作时每个人的工作边界很清晰。拆app的核心难点在于app之间怎么互相引用模型。我的建议是app之间通过服务接口调用不要直接跨app访问对方的模型或views。比如订单模块需要获取商品信息可以在orders里写一个get_product_info(product_id)函数内部调用products的模型查询但不要把products的模型直接暴露给orders的视图层。这样各个app的耦合度能控制在最低程度后续重构某个app时影响面小很多。4.2 配置文件的包化改造项目跑起来之后你会发现settings.py越来越长——数据库、缓存、日志、邮件、第三方密钥全挤在一起。Django官方没有强制要求配置文件必须是一个文件所以很多实际项目会把settings.py改造成一个配置包。具体做法是把原来的settings.py删除新建一个settings/目录内部拆成默认配置、开发配置、生产配置等模块myproject/settings/ ├── __init__.py # 根据环境变量选择加载哪个配置 ├── base.py # 公共配置 ├── dev.py # 开发环境配置 └── prod.py # 生产环境配置在__init__.py里通过环境变量动态选择加载目标import os DJANGO_ENV os.getenv(DJANGO_ENV, dev) if DJANGO_ENV prod: from .prod import * elif DJANGO_ENV dev: from .dev import * else: from .base import *这样改动的好处是开发环境和生产环境的DEBUG、DATABASES、ALLOWED_HOSTS各自独立不会出现本地跑得好好的一到服务器就500这类因为配置串台引发的问题。按这个思路改动后项目部署时只需要设置DJANGO_ENVprod这个环境变量其他配置自动切换省心很多。4.3 环境变量与依赖管理目录之外的必修课前面多次提到环境变量这里展开讲一下。settings.py里那些不该硬编码的值——密钥、数据库密码、第三方API Key——最好通过环境变量注入。常用工具有python-dotenv它的用法很简单# .env 该文件不要提交进代码仓库 SECRET_KEYyour-secret-key-here DB_NAMEmydb DB_USERmyuser DB_PASSWORDmypassword然后在settings.py里读取from dotenv import load_dotenv import os load_dotenv() SECRET_KEY os.getenv(SECRET_KEY) DATABASES { default: { ENGINE: django.db.backends.postgresql, NAME: os.getenv(DB_NAME), USER: os.getenv(DB_USER), PASSWORD: os.getenv(DB_PASSWORD), HOST: os.getenv(DB_HOST, localhost), PORT: os.getenv(DB_PORT, 5432), } }这样一来代码里不会出现任何明文密码即使代码泄露也不会直接连累生产数据库。另外别忘了在.gitignore里把.env加进去避免误提交到版本库。依赖管理方面建议用requirements.txt加requirements-dev.txt区分生产依赖和开发依赖# requirements.txt Django5.0.2 gunicorn21.2.0 psycopg2-binary2.9.9 # requirements-dev.txt -r requirements.txt django-debug-toolbar4.3.0按我的经验固定依赖版本号比用更稳妥。有时第三方库更新会引入不兼容的变化锁版本可以保证项目在任意环境里能稳定复现。升级依赖是独立的一项工作需要专门测试而不是在写某个功能时顺手做。4.4 常见扩展目录apis、services、tests、fixtures当项目功能越来越多你可能会在app目录下看到这样一些额外目录services/业务服务层负责处理复杂的业务编排。比如下单流程涉及库存扣减、订单生成、优惠券核销就可以写一个OrderService来编排这组动作。apis/如果你在做前后端分离项目可以把接口相关的视图、序列化器放这里。Django REST Framework的项目里我习惯把serializers.py、viewsets.py放在这个子层。tests/把测试拆成包按模块区分test_models.py、test_views.py、test_services.py。测试文件会被Django自动发现只要文件名以test开头即可。fixtures/放初始数据或测试数据一般是JSON文件用python manage.py loaddata加载。这些目录不是Django强制要求的但它们能让代码结构更有条理。我的判断标准是当某个文件超过三四百行或者一个目录里文件数量超过十个就该考虑拆分子结构了。这不是硬性规则而是基于可读性和可维护性的经验阈值。5. 实操经验与常见问题排查5.1 经典报错与解决思路STATIC_ROOT、ALLOWED_HOSTS、模板加载失败DisallowedHost部署上线时没配置ALLOWED_HOSTS浏览器访问会直接报错。Django这么做是出于安全考虑防止HTTP主机头攻击。解决办法是把它设为你的域名列表ALLOWED_HOSTS [yourdomain.com, www.yourdomain.com, 127.0.0.1]如果暂时不知道域名可以先填*方便本地调试但上线前一定改成具体域名不要长期用通配符。TemplateDoesNotExist模板加载失败页面白屏或404。排查思路依次是模板目录是否在TEMPLATES的DIRS里注册、文件名是否拼错、目录层级是否带上了app名。我遇到过最离奇的一个案例模板文件名字就叫index.html但页面还是找不到模板后来发现是文件放在templates/根目录而Django默认在app内的templates/app_name/里找路径对不上。解决办法就是在模板引用时把子目录写全。OperationalError: no such table数据库表不存在。十有八九是你改了模型但忘了跑迁移。执行python manage.py makemigrations python manage.py migrate就没问题了。如果已经跑过migrate却仍然报这个错检查一下你当前连接的数据库是不是你想要的那一个——这个问题在配置了多个数据库连接的项目里尤其常见。5.2 迁移文件冲突处理手册多人协作时两个开发者先后修改了同一个模型的同一个字段makemigrations不会自动检查迁移文件是否一致——它只负责生成新迁移冲突发生在migrate执行时。我通常这样处理先不要惊慌git status查看迁移文件变化看看哪些文件是被别人新加的。如果冲突是两个人改的是不同字段可以用python manage.py makemigrations --merge自动合并迁移Django会生成一个依赖这两个分支的迁移文件。如果冲突是同一个字段改成不同值需要手动解决先反向迁移到共同的祖先版本再按顺序重新生成。还有一个非常实用的习惯提交代码前先跑一遍makemigrations --check它会检查模型和迁移文件是否同步如果输出了pending migration changes说明你忘了提交迁移文件。这个检查命令我加了每天定时任务的脚本里能极大减少测试环境数据库字段缺失这类事故。5.3 体验与自动化优化manage.py 的补充命令manage.py本身是Django命令的入口除了runserver、makemigrations、migrate这几个常用命令外有几个命令也很实用python manage.py shell进入交互式Python环境并且已经帮你加载了Django配置。适合调试数据模型、验证查询逻辑。python manage.py check检查项目配置是否有错误不用启动服务器就能发现潜在问题。python manage.py collectstatic把各app的静态文件收集到一个统一目录中生产环境部署时必备。python manage.py showmigrations查看每个app迁移执行的情况显示[X]表示已执行[ ]表示未执行排查迁移问题很好用。在requirements-dev.txt里加上django-extensions这个库后还能获得更多便捷命令比如shell_plus会自动加载所有模型对象省去手动import的步骤调试效率明显提升。注意manage.py和django-admin的区别也值得清楚。django-admin是全局命令适合创建项目和执行管理任务而manage.py在项目内使用时会自动把当前项目加入sys.path能用DJANGO_SETTINGS_MODULE环境变量指定配置文件。开发中尽量用python manage.py避免全局命令带来的歧义。5.4 环境隔离与虚拟环境使用细节最后再分享一个环境管理方面的经验。刚开始写Django时为了图省事我直接把项目依赖装在系统全局的Python环境里结果同时开发几个项目时A项目要用Django 4.0、B项目要用Django 3.2依赖冲突让我复制了好几份全局环境来回切换非常痛苦。后来老老实实用venv或conda做环境隔离每个项目独立虚拟环境每个环境装好requirements.txt里锁定的版本再也没因为这个翻过车。创建虚拟环境的命令很简单python -m venv venv然后激活它Windows下是venv\Scripts\activateLinux/macOS下是source venv/bin/activate之后再执行pip install -r requirements.txt就干净利落了。把venv目录加入.gitignore不要提交到版本库。我在实际项目中还发现一个细节虚拟环境的Python版本和服务器上的Python版本尽量保持一致。如果本地用Python 3.12写代码服务器上跑的是Python 3.8经常会出现本地测试通过、部署到服务器却因为语法特性不兼容而报错的情况。这一点在新手阶段很容易被忽略等部署时再调整就晚了。写在最后回头看看Django项目目录结构看起来有一堆文件夹和文件其实每一层的职责都非常清晰项目配置目录管的是总开关各app管的是业务模板和静态文件管的是展示迁移文件管的是数据演进。把这套结构理解透了你会发现Django项目无论怎么扩展都不会乱。我个人在实际操作中比较深的体会是项目骨架不是建好就固定不变的它应该随着业务复杂度不断演进。刚开始可以单app跑通功能等模块清晰了再拆配置文件也可以从单文件演进为配置包模板和静态文件的组织方式更是要跟着团队习惯走。关键不是追求某种标准目录结构而是保证每一层都有清晰边界改动一个地方不牵连其他地方新成员加入后也能快速定位代码在哪里。最后再分享一个小技巧每次新建项目我会先花十分钟把requirements.txt、.env、.gitignore这些基础设施文件准备好再动手写业务代码。这样做虽然开头稍稍慢一点但后续不管是本地开发还是上线部署都会顺很多。目录结构这件事没有太多高深的理论把它当作项目的地基就好——地基打牢了上面盖什么楼都不慌。