静态文件管理:优雅地加载 CSS、JavaScript 和本地图片
📂 所属阶段:第一阶段 — 破冰启航(基础篇)
🔗 相关章节:Jinja2 模板引擎(下) · 环境搭建
之前咱们学了Jinja2模板渲染动态内容,可页面光有文字和变量可太朴素了!样式、交互、logo、banner、甚至用户传的头像/附件,全靠静态文件撑颜值和功能~今天就把Flask静态文件的从入门到避坑全捋一遍👇
1. 静态文件目录
Flask默认会给静态文件开绿灯,不用额外写路由就能直接访问,但得放在约定好的位置哦。
1.1 Flask 默认目录结构
项目根目录/
└── app/ # 你的应用主包
├── static/ ← 静态文件专属目录(必用默认名!自定义另说)
│ ├── css/ # 样式文件
│ │ └── style.css
│ ├── js/ # 交互脚本
│ │ └── main.js
│ ├── images/ # 网站自带图片
│ │ ├── logo.png
│ │ └── banner.jpg
│ └── uploads/ # 🚨 基础阶段:用户上传的公开文件放这里
│ ├── avatars/
│ └── attachments/
└── templates/ # Jinja2 模板文件(上节课的主场)
1.2 注册额外静态文件夹(可选)
如果有项目自带的“媒体库”不想塞static,或者隐私文件临时开个非公开转公开预览的静态路由,可以用这两种方式:
方式1:Flask原生(适合临时/小范围)
# app/__init__.py
from flask import Flask, send_from_directory
import os
def create_app():
app = Flask(__name__)
# 假设media目录在项目根目录
MEDIA_FOLDER = os.path.join(os.path.dirname(app.root_path), "media")
# 自定义路由 /media/<path:filename> 映射到 MEDIA_FOLDER
@app.route("/media/<path:filename>")
def media(filename):
return send_from_directory(MEDIA_FOLDER, filename)
return app
方式2:Werkzeug中间件(适合稳定/大文件量)
# app/__init__.py
from flask import Flask
from werkzeug.middleware.shared_data import SharedDataMiddleware
import os
def create_app():
app = Flask(__name__)
MEDIA_FOLDER = os.path.join(os.path.dirname(app.root_path), "media")
# 直接在WSGI层加静态服务,性能比原生高一点点
app.wsgi_app = SharedDataMiddleware(
app.wsgi_app,
{"/media": MEDIA_FOLDER}
)
return app
2. 在模板中引用静态文件
千万不要硬写 /static/css/style.css! 用 url_for 生成的URL更灵活——如果以后改了static的默认路由或项目部署到子目录,不用挨个改链接~
2.1 基础引用
<!-- 放在 base.html 的<head> 或<body>底部 -->
<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" width="120">
2.2 带版本号的引用(生产环境必做!)
浏览器默认会缓存静态文件,如果更新了CSS/JS,用户可能看不到效果。加版本号可以强制浏览器重新加载:
手动加版本号(小项目临时用)
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}?v=1.0.2">
自动加哈希值(最优雅,推荐)
写个小工具函数,用文件内容生成8位哈希,文件变了哈希才变,不影响其他没改的文件缓存:
# app/utils.py
from flask import current_app, url_for
import hashlib
import os
def static_hash(filename):
"""生成带内容哈希的静态文件 URL"""
filepath = os.path.join(
current_app.root_path, "static", filename
)
if os.path.exists(filepath):
with open(filepath, "rb") as f:
# 取前8位足够防止重复
version_hash = hashlib.md5(f.read()).hexdigest()[:8]
return f"{url_for('static', filename=filename)}?v={version_hash}"
# 文件不存在就返回普通URL
return url_for('static', filename=filename)
然后在 app/__init__.py 里注册成全局模板函数,所有模板都能用:
# app/__init__.py
from app.utils import static_hash
def create_app():
app = Flask(__name__)
# 注册全局模板函数
app.add_template_global(static_hash, 'static_hash')
return app
模板里直接用:
<link rel="stylesheet" href="{{ static_hash('css/style.css') }}">
3. CSS 组织与覆盖
3.1 通用主样式表
建议把全站通用的变量、重置样式、布局容器放在 static/css/style.css,子页面的单独样式可以用 Jinja2 的块(block)来加,避免所有样式堆在一起:
/* static/css/style.css */
/* 1. 变量定义(方便一键换主题) */
:root {
--primary-color: #10b981; /* 绿色主题 */
--text-dark: #1f2937;
--bg-light: #f9fafb;
--border-gray: #e5e7eb;
}
/* 2. 轻量重置(清除浏览器默认边距、字体) */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--text-dark);
background: var(--bg-light);
}
/* 3. 通用布局 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
3.2 快速集成 Bootstrap
不想自己写布局?Bootstrap 是个好帮手,两种引入方式:
方式1:CDN(开发/小项目首选,不用下载)
<!-- base.html 的<head>里加CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- base.html 的</body>之前加JS(放在底部不阻塞渲染) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
方式2:本地文件(无外网环境/生产环境优化用)
去Bootstrap官网下载压缩包,把 bootstrap.min.css 放到 static/css/,bootstrap.bundle.min.js 放到 static/js/,然后用 url_for 引入。
3.3 子模板覆盖/添加样式
在 base.html 预留 extra_head 块,子模板可以加自己的 <style> 或 <link>:
<!-- base.html -->
<head>
...
{% block extra_head %}{% endblock %}
</head>
<!-- 子模板 article.html -->
{% extends "base.html" %}
{% block extra_head %}
<!-- 引入文章专属样式 -->
<link rel="stylesheet" href="{{ static_hash('css/article.css') }}">
<!-- 或者直接写内联(仅小范围修改用) -->
<style>
/* 覆盖Bootstrap的导航栏颜色 */
.navbar {
background-color: var(--primary-color) !important;
}
</style>
{% endblock %}
4. 文件上传(公开文件版)
4.1 配置安全参数
在 app/__init__.py 里加上上传限制,防止恶意文件:
# app/__init__.py
import os
def create_app():
app = Flask(__name__)
# 配置项:上传相关
app.config["SECRET_KEY"] = "your-secret-key-here" # 必须要!flash消息要用
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 最大16MB
app.config["ALLOWED_EXTENSIONS"] = {"png", "jpg", "jpeg", "gif", "webp"} # 仅允许图片
# 公开上传路径(static下才能直接访问)
app.config["UPLOAD_FOLDER"] = os.path.join(
app.root_path, "static", "uploads"
)
return app
# 辅助函数:检查文件扩展名
def allowed_file(filename):
return "." in filename and \
filename.rsplit(".", 1)[1].lower() in current_app.config["ALLOWED_EXTENSIONS"]
4.2 写上传路由
用 werkzeug.utils.secure_filename 处理文件名,防止路径穿越攻击(比如用户传 ../../../../etc/passwd),最好再加个UUID替换原文件名,避免重名覆盖:
# app/routes/upload.py
from flask import Blueprint, request, redirect, url_for, flash, current_app
from werkzeug.utils import secure_filename
from flask_login import current_user, login_required
import os
import uuid
upload_bp = Blueprint("upload", __name__)
@upload_bp.route("/upload/avatar", methods=["POST"])
@login_required # 需要登录才能传
def upload_avatar():
# 1. 基础检查
if "file" not in request.files:
flash("请先选择文件!", "danger")
return redirect(url_for("profile.index"))
file = request.files["file"]
if file.filename == "":
flash("文件名不能为空!", "danger")
return redirect(url_for("profile.index"))
# 2. 检查扩展名
if not allowed_file(file.filename):
flash("仅支持 PNG/JPG/JPEG/GIF/WEBP 格式!", "danger")
return redirect(url_for("profile.index"))
# 3. 生成安全+唯一的文件名
secure_name = secure_filename(file.filename)
ext = secure_name.rsplit(".", 1)[1].lower()
unique_name = f"{uuid.uuid4().hex}.{ext}"
# 拼接最终保存路径
avatar_path = os.path.join(
current_app.config["UPLOAD_FOLDER"], "avatars", unique_name
)
# 确保目录存在
os.makedirs(os.path.dirname(avatar_path), exist_ok=True)
# 4. 保存文件
file.save(avatar_path)
# 5. 更新用户数据库(假设你有User模型)
# 注意存相对static的路径,方便模板直接用url_for或/static访问
current_user.avatar_url = f"/static/uploads/avatars/{unique_name}"
db.session.commit() # 需要导入db哦
flash("头像上传成功!", "success")
return redirect(url_for("profile.index"))
4.3 带预览的上传表单
表单必须加 enctype="multipart/form-data",否则传不了文件~再加个JS预览,体验更好:
<!-- templates/profile/index.html -->
<form method="POST" action="{{ url_for('upload.upload_avatar') }}" enctype="multipart/form-data">
<div class="mb-3">
<label for="avatar" class="form-label">更换头像</label>
<input type="file" class="form-control" id="avatar" name="file" accept="image/*" required>
</div>
<!-- 预览区域 -->
<div class="mb-3">
<img id="avatar_preview" class="img-thumbnail" style="max-width: 200px; display: none;">
</div>
<button type="submit" class="btn btn-primary">上传</button>
</form>
<!-- JS预览 -->
<script>
document.getElementById('avatar').addEventListener('change', function() {
const preview = document.getElementById('avatar_preview');
const file = this.files[0];
if (file) {
preview.src = URL.createObjectURL(file);
preview.style.display = 'block';
} else {
preview.style.display = 'none';
}
});
</script>
5. favicon设置
浏览器默认会找网站根目录下的 favicon.ico,但建议显式放在 static/ 目录下,然后在 base.html 的 <head> 里指定,避免404错误:
<!-- base.html 的<head>顶部 -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 显式指定favicon -->
<link rel="icon" type="image/x-icon" href="{{ static_hash('favicon.ico') }}">
<title>{% block title %}我的Flask博客{% endblock %}</title>
...
</head>
6. 小结与最佳实践
小结速查表
最佳实践
- 静态文件分类存放:别把CSS、JS、图片、上传文件混在static根目录
- 生产环境用Nginx/Apache服务静态文件:Flask自带的静态服务性能很差,仅适合开发
- 隐私文件别放static:用户的私密附件应该放在static外,用路由+权限验证后再
send_from_directory
- 生产环境压缩合并静态文件:可以用工具把CSS/JS合并成一个文件并压缩,减少请求次数
🔗 扩展阅读