☰
Django项目目录结构详解:从根目录到应用分层一次讲透
2026/10/13 4:47:12 网站建设 项目流程

做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, name="list"), path("detail/<int:pk>/", views.detail_view, name="detail"), ]

这样访问/app1/list/和/app1/detail/2/就分别对应两个视图函数了。路由匹配的顺序是自上而下的,Django遇到第一个匹配成功的模式就停止查找。所以如果你有两个路径分别为"list/"和"<str:name>/",务必把更具体的list/写在前面,否则list会被当作name参数捕获,导致页面渲染异常。这个坑我踩过一次,排查了很久才发现是路由顺序问题。

2.3 wsgi.py 与 asgi.py:项目上线的入口

wsgi.py和asgi.py这两个文件平时开发几乎用不到,但在部署时它们就是项目的"大门"。

  • wsgi.py:Web Server Gateway Interface,传统同步服务器(如Gunicorn)通过这个文件加载Django应用。
  • asgi.py:Asynchronous 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.py:Python包的身份证

__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_length=200) content = models.TextField() created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at"] def __str__(self): return self.title

模型写好之后,需要依次执行两条命令,让数据库真正发生变化:

python manage.py makemigrations python manage.py migrate

makemigrations会根据你的模型生成迁移文件,相当于把当前的数据结构"拍了拍照片";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目录下寻找模板文件。

这里面有一个非常经典的坑:模板文件命名冲突。假如你有两个app(blog和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_ENV=prod这个环境变量,其他配置自动切换,省心很多。

4.3 环境变量与依赖管理:目录之外的必修课

前面多次提到环境变量,这里展开讲一下。settings.py里那些不该硬编码的值——密钥、数据库密码、第三方API Key——最好通过环境变量注入。常用工具有python-dotenv,它的用法很简单:

# .env 该文件不要提交进代码仓库 SECRET_KEY=your-secret-key-here DB_NAME=mydb DB_USER=myuser DB_PASSWORD=mypassword

然后在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 Django==5.0.2 gunicorn==21.2.0 psycopg2-binary==2.9.9 # requirements-dev.txt -r requirements.txt django-debug-toolbar==4.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执行时。

我通常这样处理:

  1. 先不要惊慌,git status查看迁移文件变化,看看哪些文件是被别人新加的。
  2. 如果冲突是"两个人改的是不同字段",可以用python manage.py makemigrations --merge自动合并迁移,Django会生成一个依赖这两个分支的迁移文件。
  3. 如果冲突是"同一个字段改成不同值",需要手动解决:先反向迁移到共同的祖先版本,再按顺序重新生成。

还有一个非常实用的习惯:提交代码前先跑一遍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\activate,Linux/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这些基础设施文件准备好,再动手写业务代码。这样做虽然开头稍稍慢一点,但后续不管是本地开发还是上线部署,都会顺很多。目录结构这件事没有太多高深的理论,把它当作项目的地基就好——地基打牢了,上面盖什么楼都不慌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询