Django 从入门到精通:知行博客实战(6.1) · 第 7 / 36 节

静态资源与移动端布局

吴秀峰 4 次阅读 0 条评论 ↓

学习目标

用 static 标签加载 CSS,区分开发资源、生产收集目录与用户上传文件;让博客在手机宽度下仍能阅读和操作。

为什么独立管理静态文件

CSS、站点图标和 JavaScript 由开发者维护,属于静态资源;文章封面由用户上传,属于媒体文件。它们的权限、缓存和备份方式不同,不能放在同一个随意可写的目录里。开发时 Django 能查找源 CSS,生产时 collectstatic 会把各应用资源统一收集到 STATIC_ROOT,由 Nginx 提供。

本章使用 Django 风格的深绿,配合浅灰绿背景。视觉选择服务于阅读:正文保持足够对比度,长标题换行,表单控件不小于手指点击范围,焦点轮廓让键盘用户看见当前位置。响应式不只是把字号缩小,也要让导航换行、输入框适应宽度。

具体怎么做

  1. 创建 static/blog/site.css,使用应用名前缀减少命名冲突。
  2. 配置 STATICFILES_DIRS 为源目录,STATIC_ROOT 为生产产物目录,二者不能相同。
  3. 基础模板先 {% load static %},再通过标签生成 CSS 地址。
  4. 本章 CSS 已包含以后文章卡片、表单和分页需要的基础规则,后续只要使用对应语义结构。

本章新增或替换的完整文件

以下路径均相对于练习项目根目录。替换同名文件的全部内容;未列出的文件保持上一章版本。

config/settings.py

import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured

BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = os.environ.get("DJANGO_DEBUG", "1") == "1"
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "development-only-never-use-this-key-in-production")
if not DEBUG and (SECRET_KEY.startswith("development-") or len(SECRET_KEY) < 50):
    raise ImproperlyConfigured("生产环境必须设置至少 50 字符的独立 DJANGO_SECRET_KEY")
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,[::1]").split(",")
INSTALLED_APPS = [
    "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes",
    "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles",
    "accounts.apps.AccountsConfig", "blog.apps.BlogConfig",
]
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [{
    "BACKEND": "django.template.backends.django.DjangoTemplates",
    "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True,
    "OPTIONS": {"context_processors": [
        "django.template.context_processors.request",
        "django.contrib.auth.context_processors.auth",
        "django.contrib.messages.context_processors.messages",
    ]},
}]
WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"}}
AUTH_PASSWORD_VALIDATORS = [
    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "zh-hans"
TIME_ZONE = "Asia/Shanghai"
USE_I18N = True
USE_TZ = True
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
MAILERS = {"default": {"BACKEND": "django.core.mail.backends.console.EmailBackend"}}

static/blog/site.css

:root { color-scheme: light; --green: #0c4b33; --border: #d9e5dd; }
* { box-sizing: border-box; }
body { margin: 0; color: #243b2d; background: #f5f8f6; font: 17px/1.8 system-ui, sans-serif; }
a { color: #096943; text-underline-offset: .2em; overflow-wrap: anywhere; }
a:hover { color: #06472d; }
body > a, header, main, footer { display: block; max-width: 1000px; margin: auto; padding: 16px 24px; }
nav { display: flex; align-items: center; flex-wrap: wrap; gap: 12px 24px; }
nav form { margin: 0; }
main { min-height: 65vh; }
h1 { font-size: clamp(26px, 5vw, 38px); line-height: 1.4; }
h2 { font-size: 24px; }
article, .panel { padding: 24px; margin: 16px 0; border: 1px solid var(--border); background: white; border-radius: 12px; }
p, h1, h2 { overflow-wrap: anywhere; }
input, textarea, select, button { font: inherit; max-width: 100%; }
input:not([type=checkbox]), textarea, select { width: 100%; padding: 10px; border: 1px solid #829c8b; border-radius: 6px; }
button, .button { display: inline-block; padding: 9px 18px; min-height: 44px; border: 0; border-radius: 6px; background: var(--green); color: white; cursor: pointer; }
:focus-visible { outline: 3px solid #61b08b; outline-offset: 3px; }
label { display: block; font-weight: 600; }
.helptext, .muted { color: #576d5e; font-size: 15px; }
.errorlist { color: #a62222; padding-left: 24px; }
.messages { padding: 12px 24px; background: #e3f2e8; }
img { max-width: 100%; height: auto; }
pre { max-width: 100%; overflow: auto; padding: 16px; background: #eef3f0; }
.pagination { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; margin: 24px 0; }
.pagination a { padding: 8px 12px; }
@media (max-width: 600px) { header, main, footer { padding: 12px 16px; } article, .panel { padding: 18px; } nav { gap: 12px; } }

templates/base.html

{% load static %}
<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}知行博客{% endblock %}</title>
<link rel="stylesheet" href="{% static 'blog/site.css' %}"></head>
<body>
<a href="#main">跳到正文</a>
<header><nav aria-label="主导航"><a href="{% url 'blog:home' %}">知行博客</a> <a href="{% url 'blog:about' %}">关于</a></nav></header>
<main id="main">{% block content %}{% endblock %}</main>
<footer>知行博客 · 记录、实践、分享</footer>
</body></html>

如何确认成功

python manage.py findstatic blog/site.css --verbosity 2
python manage.py runserver

findstatic 应找到项目里的文件;浏览器 Network 中 CSS 状态为 200。将视口缩小到 375 像素和 320 像素,确认正文不横向溢出,导航能换行。不要通过 overflow-x:hidden 掩盖超宽内容,真正需要横向滚动的代码块可以独立滚动。

练习:只修改 --green,观察按钮和主题色的关联;随后恢复原色。用键盘 Tab 遍历页面,确认焦点轮廓始终可见。尝试把标题写成长中文句子,确认不会撑破容器。

常见问题

CSS 修改后没变化? 检查加载地址和浏览器缓存,开发时可强制刷新;生产资源缓存策略在部署章处理。DEBUG=False 时静态文件 404? 开发服务器默认不承担生产静态服务,需要收集文件并配置 Nginx。可以把上传图片放到 static 吗? 不建议,部署更新或收集操作可能改变它们,媒体应独立持久化。

官方参考

把学习连起来

登录后可保存阅读位置、标记章节完成,并在其他设备继续学习。

登录并保存学习进度 →
联系站长

微信:VIPdjango。技术问题建议先在 社区讨论 中交流,方便更多人参与和查阅。

章节讨论 0