#静态文件管理:优雅地加载 CSS、JavaScript 和本地图片
📂 所属阶段:第一阶段 — 破冰启航(基础篇)
🔗 相关章节:Jinja2 模板引擎(下) · 环境搭建
#1. 静态文件目录
#1.1 Flask 默认目录结构
项目根目录/
└── app/
├── static/ ← 静态文件目录(Flask 自动服务)
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── main.js
│ ├── images/
│ │ ├── logo.png
│ │ └── banner.jpg
│ └── uploads/ ← 用户上传文件
│ ├── avatars/
│ └── attachments/
└── templates/#1.2 注册额外静态文件夹
# app/__init__.py
from flask import Flask
def create_app():
app = Flask(__name__)
# 添加额外的静态文件夹
app.add_url_rule(
'/media',
endpoint='media',
view_func=lambda: None
)
# 更简单的方式:
from werkzeug.middleware.shared_data import SharedDataMiddleware
app.wsgi_app = SharedDataMiddleware(
app.wsgi_app,
{"/media": "path/to/media"}
)
return app#2. 在模板中引用静态文件
#2.1 基础引用
<!-- 使用 url_for 生成静态文件 URL -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">#2.2 带版本号的引用(缓存控制)
<!-- 手动加版本号(修改后改版本号,强制浏览器重新加载) -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css?v=1.0.1') }}">#2.3 自动加版本号
# app/utils.py
import hashlib
import os
def static_url(filename):
"""生成带哈希的静态文件 URL(用于缓存控制)"""
filepath = os.path.join(
current_app.root_path, "static", filename
)
if os.path.exists(filepath):
with open(filepath, "rb") as f:
version = hashlib.md5(f.read()).hexdigest()[:8]
return url_for("static", filename=filename) + f"?v={version}"
return url_for("static", filename=filename)#3. CSS 组织
#3.1 主样式表
/* static/css/style.css */
/* 全站通用样式 */
:root {
--primary-color: #3498db;
--text-color: #333;
--bg-color: #f5f5f5;
}
body {
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--text-color);
background: var(--bg-color);
margin: 0;
padding: 0;
}
/* 容器 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* 导航栏 */
.navbar {
background: #2c3e50;
padding: 15px 0;
}
.navbar a {
color: white;
text-decoration: none;
margin: 0 15px;
}#3.2 Bootstrap 集成
<!-- 在 base.html 中引入 Bootstrap -->
<!-- 方式一:CDN(推荐,简单快捷) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 方式二:本地(自行下载到 static/css/) -->
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>#3.3 自定义 CSS 覆盖 Bootstrap
{% extends "base.html" %}
{% block extra_head %}
<!-- 在子模板中添加额外样式 -->
<style>
body { background: #fafafa; }
.navbar { background: #2c3e50 !important; }
</style>
{% endblock %}#4. JavaScript 组织
#4.1 基础使用
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}
{{ super() }} <!-- 调用父模板 scripts 块 -->
<script src="{{ url_for('static', filename='js/article.js') }}"></script>
{% endblock %}#4.2 页面加载后执行
// static/js/main.js
// 等待 DOM 加载完成
document.addEventListener("DOMContentLoaded", function() {
console.log("页面加载完成!");
// 搜索框防抖
const searchInput = document.getElementById("search");
if (searchInput) {
let timer;
searchInput.addEventListener("input", function() {
clearTimeout(timer);
timer = setTimeout(() => {
console.log("搜索:", this.value);
// 触发搜索
}, 300);
});
}
// AJAX 提交表单
document.querySelectorAll(".ajax-form").forEach(form => {
form.addEventListener("submit", async function(e) {
e.preventDefault();
const formData = new FormData(this);
const response = await fetch(this.action, {
method: "POST",
body: formData
});
const result = await response.json();
alert(result.message);
});
});
});#5. 文件上传
#5.1 上传配置
# app/__init__.py
import os
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 最大 16MB
app.config["ALLOWED_EXTENSIONS"] = {"png", "jpg", "jpeg", "gif", "webp"}
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static", "uploads")
def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in app.config["ALLOWED_EXTENSIONS"]#5.2 上传路由
# app/routes/upload.py
from flask import Blueprint, request, redirect, url_for, flash
from werkzeug.utils import secure_filename
from flask_login import current_user
import os
import uuid
upload_bp = Blueprint("upload", __name__)
@upload_bp.route("/upload/avatar", methods=["POST"])
def upload_avatar():
if not current_user.is_authenticated:
return redirect(url_for("auth.login"))
if "file" not in request.files:
flash("没有文件!")
return redirect(url_for("profile"))
file = request.files["file"]
if file.filename == "":
flash("没有选择文件!")
return redirect(url_for("profile"))
if file and allowed_file(file.filename):
# 生成安全文件名(UUID + 原扩展名)
ext = file.filename.rsplit(".", 1)[1].lower()
filename = f"{uuid.uuid4().hex}.{ext}"
filepath = os.path.join(app.root_path, "static", "uploads", "avatars", filename)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
file.save(filepath)
# 更新用户头像
current_user.avatar = f"/static/uploads/avatars/{filename}"
db.session.commit()
flash("头像上传成功!")
return redirect(url_for("profile"))
flash("不支持的文件类型!")
return redirect(url_for("profile"))#5.3 上传表单
<!-- templates/profile/upload_avatar.html -->
<form method="POST" action="{{ url_for('upload.upload_avatar') }}" enctype="multipart/form-data">
<input type="file" name="file" accept="image/*" required>
<button type="submit">上传头像</button>
</form>
<!-- 预览上传图片 -->
<img id="preview" style="max-width: 200px; display: none;">
<script>
document.querySelector('input[name="file"]').addEventListener("change", function() {
const preview = document.getElementById("preview");
preview.src = URL.createObjectURL(this.files[0]);
preview.style.display = "block";
});
</script>#6. favicon
<!-- 在 base.html 中添加 -->
<head>
<!-- favicon.ico 放在 static/ 目录,浏览器自动查找 -->
<!-- 或者显式指定 -->
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
</head>#7. 小结
静态文件使用速查:
目录:项目根目录/static/
引用:{{ url_for('static', filename='css/style.css') }}
上传:werkzeug.utils.secure_filename() 安全处理文件名
限制:app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024💡 最佳实践:CSS 和 JS 文件在生产环境应该启用浏览器缓存,方法是在 URL 后加版本号或让 Nginx 配置 Cache-Control。
🔗 扩展阅读

