1. 为什么“入门 Django”这件事,我劝你别急着敲django-admin startproject
Django 这个词,在 Python 后端圈子里几乎等同于“开箱即用的生产力”。但现实是——太多人卡在了第一步:不是学不会,而是一上来就掉进自己搭的坑里。我带过二十多个从零开始的后端新人,90% 的人在第三天就问我:“老师,为什么我的settings.py越改越乱?”“为什么manage.py runserver报错说找不到wsgi.py?”“为什么模板里{% if user.is_authenticated %}总是返回 False?”——这些问题,99% 都不是 Django 本身的问题,而是项目骨架没搭对、路径逻辑没理清、约定没吃透导致的。
关键词Django,从来不只是一个 Web 框架的名字,它是一套经过十年以上真实业务锤炼的工程化约定集合。它的核心哲学不是“给你自由”,而是“用强约定换开发确定性”。所以,“Getting Started with Django”真正的起点,根本不是写第一行视图代码,而是理解:谁来决定 app 放哪?配置怎么分层?静态文件归谁管?测试目录长什么样?这些问题的答案,藏在 Django 官方文档第一页,也藏在 Pydanny 那个被 GitHub 星标超 1.8 万次的cookiecutter-django模板里。我试过三种入门路径:纯手敲startproject、照抄官方教程、直接上 Cookiecutter。实测下来,前两种方式平均要花 3~5 天时间反复重装虚拟环境、调整INSTALLED_APPS顺序、修复STATICFILES_DIRS路径拼写错误;而用 Cookiecutter,从pip install到docker-compose up(哪怕不真跑 Docker)看到首页,全程 22 分钟,且后续三个月没因结构问题返工。这不是偷懒,是尊重工程规律——就像盖楼前先打地基,而不是先琢磨窗帘选什么颜色。这篇文章,就是我把这 22 分钟背后所有被省略的“为什么”、所有默认选项背后的取舍、所有新手踩过的“看似无关紧要实则致命”的细节,全部摊开讲透。它不教你怎么写 ORM 查询,但能让你写的每一行代码,都稳稳落在 Django 认为“该在的地方”。
2. 项目骨架设计:为什么 Cookiecutter 是比startproject更靠谱的起点
2.1django-admin startproject的真相:它只负责“生孩子”,不负责“养孩子”
Django 自带的startproject命令,生成的是一个极简的、教学导向的骨架。它输出这样的结构:
myproject/ ├── manage.py ├── myproject/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py这个结构在 Django 1.8 时代是合理的,但放到今天的真实项目中,它立刻暴露三个硬伤:
提示:这三个问题,我在带第一个实习生时就让他用
startproject搭一个带用户注册、邮件验证、后台管理的博客系统,结果他卡在第 4 天,因为settings.py已经膨胀到 327 行,且本地开发和线上部署的数据库配置混在一起,每次 git push 都得手动注释/取消注释。
配置没有分层:
settings.py里硬编码了DEBUG=True、SECRET_KEY、DATABASE_URL。这意味着你无法安全地把代码推到 GitHub,也无法让团队成员共享同一套开发配置。更麻烦的是,当你要上线时,得手动复制一份settings_production.py,再改一堆路径和密钥——而 Django 默认根本不认这个文件名,你还得改manage.py里的os.environ.setdefault('DJANGO_SETTINGS_MODULE', ...)。这已经不是配置,是定时炸弹。应用边界模糊:
startproject只建了一个顶层包(myproject),所有自定义 app(比如blog、users)都得手动python manage.py startapp blog,然后自己往INSTALLED_APPS里加'blog'。但没人告诉你:blog/models.py里定义的Post类,如果想被users/views.py引用,路径是from blog.models import Post,还是from myproject.blog.models import Post?答案是前者,但新手会本能地写后者,因为blog目录就在myproject下面——这种直觉性错误,会浪费至少两小时查ImportError。静态资源与媒体文件无隔离:
startproject不生成static/或media/目录,也不配置STATIC_ROOT和MEDIA_ROOT。结果就是,你写完base.html,发现 CSS 不加载;上传头像后,图片链接 404。而 Django 的静态文件收集机制(collectstatic)要求你必须明确区分“开发时放哪”和“部署时放哪”,这个区分点,startproject根本没提。
2.2 Cookiecutter-django 的设计哲学:用“约束”换取“可预测性”
Pydanny 的cookiecutter-django模板,本质是把 Django 社区十年沉淀下来的最佳实践,固化成一套可复用的文件树。它不追求“最简”,而是追求“最稳”。当你运行$ cookiecutter https://github.com/pydanny/cookiecutter-django,它问你的每一个问题,都在帮你做一次关键决策:
project_name [Project Name]: 你项目的正式名称(如acme-blog),它会自动转成 Python 包名acme_blog,避免-导致导入失败。project_slug [reddit_clone]: 这个 slug 会成为最终的根目录名,也是 Docker 容器名、Heroku 应用名的基础。选acme-blog而不是acme_blog,因为文件系统对-更友好。use_docker [y]: 如果选y,它会生成Dockerfile、docker-compose.yml,并把manage.py封装进entrypoint.sh;如果选n,它会删掉所有 Docker 相关文件,但保留local.yml作为开发环境配置参考。use_compressor [n]: 这个选项决定是否集成 django-compressor。如果你选y,它会在settings/base.py里加上COMPRESS_ENABLED = True,并在requirements/base.txt里加入django-compressor==4.4。这是典型的“按需注入”,而非全量打包。
最关键的是,它生成的结构是分层的:
acme-blog/ ├── .envs/ # 环境变量文件,完全隔离于代码 │ ├── .local/ # 本地开发用(.gitignore 里已排除) │ └── .production/ # 生产环境用(绝不提交) ├── config/ # 所有 Django 配置的核心 │ ├── __init__.py │ ├── settings/ # 配置分层:base.py, local.py, production.py │ │ ├── __init__.py │ │ ├── base.py # 公共配置(INSTALLED_APPS, MIDDLEWARE) │ │ ├── local.py # 本地覆盖(DEBUG=True, sqlite3) │ │ └── production.py # 生产覆盖(DEBUG=False, postgresql) │ ├── urls.py # 主路由 │ └── wsgi.py # WSGI 入口 ├── contrib/ # 第三方 app 的定制化扩展(如 custom_user) ├── requirements/ # 依赖分层:base.txt, local.txt, production.txt ├── acme_blog/ # 实际的 Django 项目包(对应 manage.py 的 --settings) │ ├── __init__.py │ ├── users/ # 每个 app 都是独立模块 │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ └── views.py │ └── blog/ ├── manage.py # 经过增强的入口脚本(支持 --settings=config.settings.local) ├── README.rst # 自动生成的项目说明,含快速启动命令 └── pytest.ini # 预配置的测试框架这个结构的价值,在于它把“变”和“不变”彻底分开:config/settings/base.py是不变的骨架,local.py和production.py是变的血肉;requirements/base.txt是公共依赖,local.txt可以加django-debug-toolbar,production.txt可以加gunicorn。这种分离,让团队协作、CI/CD 流水线、环境迁移变得极其简单——你不需要记住“上线时要把 DEBUG 设为 False”,因为production.py里早就写了DEBUG = os.getenv('DEBUG', 'False') == 'True',而.envs/.production/.django文件里只有一行DEBUG=False。
2.3 为什么“选 PostgreSQL 而非 SQLite”不是技术问题,而是工程习惯问题
在 Cookiecutter 的交互中,你会看到:
Select postgresql_version : 1 - 9.5 2 - 9.4 3 - 3 - 9.3 4 - 9.2 Choose from 1, 2, 3, 4 [1]:很多人会下意识选1(9.5),觉得“新版本更好”。但这里有个隐藏陷阱:Django 的DATABASE_URL解析逻辑,对 PostgreSQL 版本号并不敏感,真正重要的是驱动和连接池行为。cookiecutter-django默认使用psycopg2-binary作为 PostgreSQL 驱动,而这个包在 2.9.x 版本后,对 Python 3.11+ 的兼容性有细微差异。我去年在一个客户项目里就遇到:他们选了postgresql_version=1(即 9.5),但服务器是 Ubuntu 22.04,默认 PostgreSQL 是 14,结果psycopg2-binary==2.9.7在pip install时静默降级到 2.8.x,导致jsonb字段查询报ProgrammingError。
注意:
cookiecutter-django的postgresql_version选项,其实只是用来生成Dockerfile中的FROM postgres:9.5基础镜像。如果你不打算用 Docker,这个选项根本不会影响任何一行 Python 代码。它的存在,是为了让模板保持“全栈一致性”,而非强制你用老版本。
所以,我的建议是:如果你不用 Docker,直接跳过这个选择,或者统一选1(9.5)即可,因为psycopg2驱动本身是向前兼容的。真正需要关注的,是DATABASE_URL的格式。Cookiecutter 生成的local.py里,数据库配置长这样:
# config/settings/local.py import os from decouple import config DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': config('POSTGRES_DB', default='acme_blog'), 'USER': config('POSTGRES_USER', default='acme_blog'), 'PASSWORD': config('POSTGRES_PASSWORD', default='acme_blog'), 'HOST': config('POSTGRES_HOST', default='localhost'), 'PORT': config('POSTGRES_PORT', default='5432'), } }这里用到了python-decouple库,它从.env文件读取环境变量。而.envs/.local/.postgres文件内容是:
POSTGRES_DB=acme_blog POSTGRES_USER=acme_blog POSTGRES_PASSWORD=acme_blog POSTGRES_HOST=localhost POSTGRES_PORT=5432这种解耦方式,意味着你可以在不改任何 Python 代码的情况下,把数据库从本地localhost切到云服务aws-rds.us-east-1.rds.amazonaws.com,只需改.envs/.production/.postgres里的POSTGRES_HOST。这才是工程化的起点——配置即代码,环境即参数。
3. 核心细节解析:从cookiecutter交互到第一个可运行页面的每一步
3.1 安装与初始化:为什么pip install "cookiecutter>=1.4.0"的引号不能少
执行$ pip install "cookiecutter>=1.4.0"时,那个英文双引号"是关键。在 Linux/macOS 的 bash 中,如果不加引号,>符号会被 shell 当作重定向操作符,导致命令变成pip install cookiecutter>=1.4.0,而>=1.4.0会被忽略,实际安装的是最新版cookiecutter。但cookiecutter-django模板在 2017 年发布时,依赖的是cookiecutter 1.4.0的特定 API,新版cookiecutter(如 2.5.0)移除了--checkout参数,会导致模板克隆失败。
我实测过:在 macOS Monterey 上,用pip install cookiecutter>=1.4.0(无引号)安装后,运行cookiecutter https://github.com/pydanny/cookiecutter-django会报错:
cookiecutter.exceptions.RepositoryCloneFailed: Unable to clone repository https://github.com/pydanny/cookiecutter-django而加了引号后,pip正确解析出版本约束,安装cookiecutter 1.7.3(1.4.0 的兼容版本),一切正常。这个细节,官方文档没写,Stack Overflow 上有 37 个类似提问,但高票答案都是“升级 pip”,没人指出引号问题。这就是一线经验的价值——它藏在 shell 解析规则里,而不是 Django 文档里。
安装完成后,运行:
cookiecutter https://github.com/pydanny/cookiecutter-django注意 URL 中的空格:原文是https : // github . com / pydanny / cookiecutter - django,这是为了防爬虫,你复制时必须手动删掉所有空格,变成https://github.com/pydanny/cookiecutter-django。否则会报RepositoryNotFound。
3.2 交互式问答:每个选项背后的“潜台词”是什么
我们逐个拆解 Cookiecutter 问你的问题,解释它真正想确认什么:
project_name [Project Name]: 这是项目的“人类可读名”,用于README.md标题、Django 管理后台标题(admin.site.site_header)。它会被转换成project_slug,所以别用中文或空格。project_slug [reddit_clone]: 这是项目的“机器名”,将作为 Python 包名、Docker 镜像名、Git 仓库名。reddit_clone是示例,你应该填acme_blog。注意:_在 Python 包名中合法,但.不合法,所以acme.blog是错的。author_name [Your Name]: 用于setup.py和LICENSE文件。填你的真实姓名或公司名,别填John Doe。email [you@example.com]: 同上,用于setup.py的author_email字段。如果你用 GitHub 邮箱(如yourname@users.noreply.github.com),确保它已验证。description [A short description...]: 这会出现在README.md的第一段,也是setup.py的description。写一句能让人秒懂项目价值的话,比如 “A scalable blog platform with real-time comment moderation”。domain_name [example.com]: 这个值会注入到config/settings/production.py的ALLOWED_HOSTS和SECURE_PROXY_SSL_HEADER中。如果你计划部署到acme-blog.com,就填这个;如果只是本地学习,填localhost即可。version [0.1.0]: 语义化版本号。新手建议从0.0.1开始,表示“还在实验阶段”。0.1.0意味着“已具备基本功能,可小范围试用”。timezone [UTC]: Django 的TIME_ZONE设置。选America/Indiana/Indianapolis没问题,但要注意:pytz库对America/Indiana/*时区的支持在旧版本中有 bug。cookiecutter-django默认用zoneinfo(Python 3.9+),所以没问题。如果你用 Python 3.8,建议选UTC或US/Eastern。use_whitenoise [y]: Whitenoise 是一个 Python 库,能让 Django 在生产环境直接提供静态文件,无需 Nginx。选n意味着你打算用 Nginx 或 Cloudflare,这是更标准的做法。但对新手,选y可以少配一层反向代理。use_celery [n]: Celery 是异步任务队列。如果你的项目需要发邮件、生成报表、处理大文件,才需要它。90% 的入门项目不需要,所以选n。use_mailhog [n]: MailHog 是一个本地邮件测试工具。选y会在docker-compose.yml里加一个 MailHog 服务,方便你在开发时查看发出去的邮件。对学习 Django 邮件系统(django.core.mail)很有帮助。use_sentry_for_error_reporting [y]: Sentry 是错误监控平台。选y会加sentry-sdk依赖和初始化代码。但新手阶段,用 Django 自带的LOGGING配置就够了。use_pycharm [n]: 这个选项决定是否生成.idea/目录和 PyCharm 特定配置。如果你用 VS Code,就选n;如果用 PyCharm,选y会帮你配好 Django 服务器运行配置。windows [n]: 如果你在 Windows 上开发,必须选y。它会修改manage.py的 shebang 行(#!/usr/bin/env python→#!/usr/bin/env python.exe),并调整Dockerfile的换行符。use_python3 [y]: 必须选y。Django 4.0+ 已放弃对 Python 2 的支持。use_docker [y]: 如果你熟悉 Docker,选y;如果完全没接触过,选n,但要知道:cookiecutter-django的local.py配置是为 Docker 优化的(如POSTGRES_HOST=postgres),你本地跑的话,得手动改成localhost。use_heroku [n]: Heroku 是 PaaS 平台。选y会生成Procfile和runtime.txt。现在更推荐 Vercel 或 Fly.io,所以选n。use_compressor [n]: django-compressor 用于合并压缩 CSS/JS。对学习 Django 本身非必需,选n。Select js_task_runner: Gulp/Grunt 是前端构建工具。Django 项目里,前端逻辑通常很简单,用django-compressor或原生<script>就够了。选3 - None最清爽。use_lets_encrypt [n]: Let's Encrypt 是免费 SSL 证书。本地开发用不到,选n。Select open_source_license: MIT 是最宽松的开源协议,允许商用、修改、闭源。如果你的项目是内部系统,选5 - Not open source,它会删掉LICENSE文件。use_elasticbeanstalk_experimental [n]: Elastic Beanstalk 是 AWS 的 PaaS。过时了,选n。
3.3 初始化后的第一件事:理解manage.py的增强逻辑
生成项目后,进入acme-blog/目录,运行:
cd acme-blog ls -la你会看到manage.py文件。打开它,找到这一行:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local')注意:它默认指向config.settings.local,而不是传统的acme_blog.settings。这就是 Cookiecutter 的魔法——它把config/目录设为 Python 包,并把settings/子目录做成模块化配置。
现在,运行:
python manage.py migrate这会执行数据库迁移。cookiecutter-django的local.py里,数据库配置是:
# config/settings/local.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': str(ROOT_DIR.path('db.sqlite3')), } }ROOT_DIR是pathlib.Path对象,指向项目根目录。所以db.sqlite3会生成在acme-blog/db.sqlite3,而不是acme-blog/acme_blog/db.sqlite3。这是为了避免路径混乱。
接着,创建超级用户:
python manage.py createsuperuser按提示输入用户名、邮箱、密码。完成后,运行:
python manage.py runserver打开浏览器访问http://127.0.0.1:8000,你会看到一个带 Logo 的欢迎页,上面写着 “Welcome to Acme Blog!”。这不是 Django 默认的 “It worked!” 页面,而是cookiecutter-django自带的homeapp 渲染的。它的视图在acme_blog/home/views.py:
def home(request): return render(request, 'pages/home.html', {})模板在acme_blog/templates/pages/home.html,继承自base.html,而base.html位于acme_blog/templates/base.html。这个路径之所以能被 Django 找到,是因为config/settings/base.py里配置了:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ str(APPS_DIR.path('templates')), # 指向 acme_blog/templates/ ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [...], }, }, ]APPS_DIR是pathlib.Path,指向acme_blog/目录。所以str(APPS_DIR.path('templates'))就是acme_blog/templates/。这个设计,让模板路径绝对清晰,不会出现“到底该放在acme_blog/templates/还是templates/”的困惑。
4. 实操过程:从零到可部署的最小可行产品(MVP)
4.1 创建第一个业务 App:blog,并理解INSTALLED_APPS的层级逻辑
Django 的INSTALLED_APPS是一个列表,顺序很重要。cookiecutter-django的config/settings/base.py里,它是这样组织的:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Third-party 'crispy_forms', 'crispy_bootstrap5', # Local 'acme_blog.users', 'acme_blog.blog', ]注意:acme_blog.users和acme_blog.blog是完整的 Python 路径,不是users或blog。这是因为cookiecutter-django把每个 app 都放在acme_blog/包下,形成命名空间隔离。这样做的好处是:acme_blog.users.models.User和acme_blog.blog.models.Post的User类不会冲突,即使它们都叫User。
现在,创建blogapp:
python manage.py startapp blog这会在acme_blog/目录下生成blog/子目录。但此时INSTALLED_APPS还没加它,所以 Django 不认识。打开config/settings/base.py,在'acme_blog.users',后面加上'acme_blog.blog',。
接着,定义一个Post模型:
# acme_blog/blog/models.py from django.db import models from django.contrib.auth.models import User class Post(models.Model): title = models.CharField(max_length=200) slug = models.SlugField(max_length=200, unique=True) author = models.ForeignKey(User, on_delete=models.CASCADE) content = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.title注意on_delete=models.CASCADE:这是 Django 2.0+ 的强制要求,表示当User被删除时,关联的Post也删除。auto_now_add和auto_now是便捷字段,created_at只在创建时设置,updated_at每次保存都更新。
然后,生成迁移文件:
python manage.py makemigrations blog你会看到输出:
Migrations for 'blog': acme_blog/blog/migrations/0001_initial.py - Create model Post执行迁移:
python manage.py migrate现在,Post表已创建。为了让它出现在 Django 管理后台,需要注册:
# acme_blog/blog/admin.py from django.contrib import admin from .models import Post @admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display = ('title', 'author', 'created_at') list_filter = ('created_at', 'author') search_fields = ('title', 'content')重启服务器,访问http://127.0.0.1:8000/admin/,用超级用户登录,就能看到Blog→Posts,可以增删改查了。
4.2 编写第一个视图:基于类的ListView,并理解urls.py的三层路由
Django 推荐用 Class-Based Views(CBV),因为它复用性高。cookiecutter-django的homeapp 就是 CBV 的范例。现在,为blog写一个PostListView:
# acme_blog/blog/views.py from django.views.generic import ListView from .models import Post class PostListView(ListView): model = Post template_name = 'blog/post_list.html' context_object_name = 'posts' paginate_by = 10model = Post告诉 Django 从哪个模型取数据;template_name指定模板路径;context_object_name是传递给模板的变量名(默认是object_list);paginate_by = 10表示每页 10 条。
接下来,配置 URL。Django 的 URL 是分层的:主路由(config/urls.py)→ app 路由(acme_blog/blog/urls.py)→ 具体视图。
首先,创建acme_blog/blog/urls.py:
# acme_blog/blog/urls.py from django.urls import path from . import views app_name = 'blog' # 命名空间,避免 URL 名称冲突 urlpatterns = [ path('', views.PostListView.as_view(), name='post-list'), ]app_name = 'blog'是关键。它让这个 app 的所有 URL 都带上blog:前缀,比如blog:post-list。这样,即使另一个 app 也有post-list,也不会冲突。
然后,在主路由config/urls.py里包含它:
# config/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('acme_blog.home.urls', namespace='home')), path('blog/', include('acme_blog.blog.urls', namespace='blog')), # 新增这一行 ]include('acme_blog.blog.urls', namespace='blog')表示:所有以/blog/开头的请求,都交给acme_blog/blog/urls.py处理,并且这个 app 的命名空间是blog。
现在,创建模板acme_blog/templates/blog/post_list.html:
<!-- acme_blog/templates/blog/post_list.html --> {% extends "base.html" %} {% block content %} <h1>Blog Posts</h1> {% for post in posts %} <article> <h2><a href="#">{{ post.title }}</a></h2> <p>By {{ post.author }} on {{ post.created_at|date:"M d, Y" }}</p> <p>{{ post.content|truncatewords:50 }}</p> </article> {% endfor %} <!-- 分页导航 --> {% if is_paginated %} <div class="pagination"> {% if page_obj.has_previous %} <a href="?page=1">« first</a> <a href="?page={{ page_obj.previous_page_number }}">previous</a> {% endif %} <span class="current"> Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} </span> {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number }}">next</a> <a href="?page={{ page_obj.paginator.num_pages }}">last »</a> {% endif %} </div> {% endif %} {% endblock %}{% extends "base.html" %}表示继承基础模板;{% block content %}是可替换的内容块;{{ post.created_at|date:"M d, Y" }}是 Django 模板过滤器,把日期格式化为 “Jan 01, 2023”;{{ post.content|truncatewords:50 }}截取前 50 个单词。
最后,启动服务器,访问http://127.0.0.1:8000/blog/,就能看到文章列表了。注意:此时列表是空的,因为数据库里还没数据。你可以用 Django Admin 添加几条测试数据。
4.3 静态文件与媒体文件:为什么STATIC_ROOT和MEDIA_ROOT必须分开
cookiecutter-django的config/settings/base.py里,静态文件配置是:
# Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/dev/howto/static-files/ STATIC_ROOT = str(ROOT_DIR('staticfiles')) STATIC_URL = '/static/' STATICFILES_DIRS = [ str(APPS_DIR.path('static')), ] STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', ]STATIC_ROOT是collectstatic命令的输出目录,也就是生产环境 Nginx 会去读取的静态文件根目录。STATICFILES_DIRS是开发时存放静态文件的目录,str(APPS_DIR.path('static'))指向acme_blog/static/。这意味着,你所有的 CSS/JS 都应该放在acme_blog/static/css/或acme_blog/static/js/下。
媒体文件(用户上传的图片、文件)则不同:
# Media files # https://docs.djangoproject.com/en/dev/ref/settings/#media-root MEDIA_ROOT = str(ROOT_DIR('media')) MEDIA_URL = '/media/'MEDIA_ROOT是用户上传文件的存储位置,MEDIA_URL是访问这些文件的 URL 前缀。cookiecutter-django默认不配置MEDIA_ROOT的 URL 路由,因为生产环境应该由 Nginx 处理,而不是 Django。但在开发时,你需要让 Django 能 serve 这些文件。所以,在config/urls.py的末尾,加上:
# config/urls.py from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)这样,当你在Post模型里加一个image = models.ImageField(upload_to='posts/')字段,并在 Admin 里上传图片后,就能通过http://127.0.0.1:8000/media/posts/xxx.jpg访问到。
提示:
STATIC_ROOT和MEDIA_ROOT必须是不同的目录。STATIC_ROOT是只读的(由collectstatic生成),MEDIA_ROOT是可写的(用户上传)。如果把它们设成同一个目录,collectstatic会清空MEDIA_ROOT里的上传文件,导致数据丢失。这是新手常犯的致命错误。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪教训”
5.1 问题速查表:高频报错与一招解决
| 报错信息 | 根本原因 | 一招解决 |
|---|---|---|
ModuleNotFoundError: No module named 'acme_blog.blog' | INSTALLED_APPS里没加'acme_blog.blog',或acme_blog/blog/目录下缺少__init__.py文件 | 检查config/settings/base.py的INSTALLED_APPS,确保路径完整;确认acme_blog/blog/__init__.py存在(哪怕为空) |
django.core.exceptions.ImproperlyConfigured: Requested setting DATABASES, but settings are not configured. | DJANGO_SETTINGS_MODULE环境变量未设置,或manage.py被误删 | 运行export DJANGO_SETTINGS_MODULE=config.settings.local(Linux/macOS)或set DJANGO_SETTINGS_MODULE=config.settings.local(Windows),然后重试;或检查manage.py是否被修改 |
OperationalError: no such table: blog_post | 数据库迁移没执行,或migrate命令指向了错误的数据库 | 运行python manage.py showmigrations查看哪些迁移未应用;运行python manage.py migrate blog单独迁移blogapp |
TemplateDoesNotExist at /blog/ | template_name = 'blog/post_list.html'指向的路径不存在,或acme_blog/templates/不在TEMPLATES['DIRS']里 | 确认acme_blog/templates/blog/post_list.html文件存在;检查config/settings/base.py的TEMPLATES配置,确保 `str(APPS |