写在前面
Flask 是 Python Web 开发里很适合作为入门的框架。它足够轻,核心概念不多,但又没有把 Web 开发的关键问题藏起来:路由、请求、响应、模板、数据库、登录态、配置、测试、部署,每一环都可以被清楚地拆开学习。
这篇笔记的目标不是把 Flask 所有 API 背下来,而是建立一条可以执行的学习路线:先把 Web 请求如何进入程序看明白,再把一个小项目从零搭起来,最后补齐数据库、测试、部署和工程化习惯。学完之后,至少应该能独立写出一个带登录、文章管理、数据库持久化和基础测试的 Flask 小站。
学习路线
第一阶段:先理解 Web 程序的骨架
这一阶段只需要关心四个问题:
- 浏览器发来的请求是什么?
- Flask 如何把 URL 分发给对应的 Python 函数?
- 视图函数如何返回 HTML、JSON 或重定向?
- 模板如何把 Python 数据渲染成页面?
建议学习顺序:
- HTTP 基础:方法、状态码、请求头、表单、Cookie。
- Flask 最小应用:
Flask(__name__)、@app.route()、return。 - 请求与响应:
request、jsonify、redirect、url_for。 - Jinja2 模板:变量、循环、条件、模板继承。
- 静态资源:CSS、图片、JavaScript 的组织方式。
这个阶段不要急着接数据库。先把“一个请求进来,经过路由,执行函数,再返回响应”的链路讲清楚,后面的认证、数据库和接口设计才不会变成一团雾。
第二阶段:做一个最小博客
最小博客是学习 Flask 的好练习,因为它同时覆盖了增删改查、模板渲染、表单提交和页面跳转。可以先做三个页面:
- 首页:展示文章列表。
- 详情页:根据文章 ID 展示文章内容。
- 发布页:提交标题和正文,并保存到内存列表。
此时的数据可以先写在 Python 列表中,不必一开始就引入数据库。这样能把注意力集中在路由和模板上。
第三阶段:接入数据库和工程结构
当页面流程跑通之后,再引入 SQLite 和 SQLAlchemy。此时重点从“能跑”转向“可维护”:
- 用应用工厂
create_app()组织项目。 - 用 Blueprint 拆分模块。
- 用配置对象区分开发、测试、生产环境。
- 用数据库模型表达业务数据。
- 用迁移工具管理表结构变化。
这是 Flask 从脚本变成项目的关键一步。很多初学者卡在这里,是因为一开始把所有代码都写进 app.py,后续想拆分时会牵扯太多隐式依赖。
第四阶段:补齐登录、权限和安全
真实 Web 项目绕不开用户系统。Flask 本身不强制你使用某套认证方案,所以这一阶段要主动学习:
- 密码哈希,不保存明文密码。
- Session 与 Cookie 的关系。
- 登录态校验。
- CSRF 防护。
- 表单校验。
- 权限边界,例如只能编辑自己的文章。
安全不是最后才加的装饰,而是需求建模时就要考虑的约束。比如“删除文章”这个动作,除了写一个 DELETE 或 POST 路由,还要确认当前用户是否拥有这篇文章。
第五阶段:测试、部署和持续迭代
能本地跑起来只是开始。真正稳定的项目至少要具备:
- 单元测试:测试路由、表单校验和权限判断。
- 集成测试:测试从请求到数据库变更的完整流程。
- 部署配置:使用 Gunicorn 或 uWSGI 运行 WSGI 应用。
- 反向代理:用 Nginx 处理 HTTPS、静态资源和转发。
- 日志与错误处理:出错时能定位问题,而不是只看到 500。
学习 Flask 的最终目标不是“会写几个路由”,而是能把一个小型 Web 应用交付到可访问、可维护、可排查的状态。
环境搭建
建议使用虚拟环境隔离依赖:
mkdir flask-note
cd flask-note
python3 -m venv .venv
source .venv/bin/activate
pip install flask创建最小应用:
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello, Flask!"启动开发服务器:
flask --app app run --debug浏览器访问 http://127.0.0.1:5000,看到 Hello, Flask! 就说明最小应用已经跑通。
这里有一个值得记住的点:--debug 只适合开发环境。它会开启自动重载和调试器,方便排错,但生产环境不能暴露调试器。
路由:URL 与视图函数的映射
Flask 最核心的写法就是路由装饰器:
@app.route("/posts/<int:post_id>")
def post_detail(post_id):
return f"当前文章 ID:{post_id}"/posts/<int:post_id> 中的 int 是转换器,表示这段路径必须能转成整数。常见转换器包括:
| 转换器 | 说明 |
|---|---|
string | 默认字符串,不包含斜杠 |
int | 整数 |
float | 浮点数 |
path | 可以包含斜杠的路径 |
uuid | UUID 字符串 |
路由设计建议遵循资源语义:
GET /posts 查看文章列表
GET /posts/1 查看文章详情
GET /posts/new 显示新建表单
POST /posts 创建文章
GET /posts/1/edit 显示编辑表单
POST /posts/1/edit 更新文章
POST /posts/1/delete 删除文章HTML 表单天然只支持 GET 和 POST,所以传统 Flask 页面项目里常用 POST /posts/1/delete 表达删除动作。如果做纯 API,则可以使用 DELETE /api/posts/1。
请求与响应
Flask 通过 request 读取请求数据:
from flask import Flask, request
app = Flask(__name__)
@app.route("/search")
def search():
keyword = request.args.get("q", "")
return f"搜索关键词:{keyword}"
@app.route("/posts", methods=["POST"])
def create_post():
title = request.form.get("title", "").strip()
content = request.form.get("content", "").strip()
if not title or not content:
return "标题和内容不能为空", 400
return f"已创建文章:{title}", 201常见读取方式:
| 场景 | 写法 |
|---|---|
| 查询参数 | request.args.get("q") |
| 表单字段 | request.form.get("title") |
| JSON 请求体 | request.get_json() |
| 上传文件 | request.files.get("avatar") |
| 请求头 | request.headers.get("User-Agent") |
| Cookie | request.cookies.get("name") |
返回 JSON 时使用 jsonify:
from flask import jsonify
@app.route("/api/health")
def health():
return jsonify({"status": "ok"})返回重定向时使用 redirect 和 url_for:
from flask import redirect, url_for
@app.route("/go-home")
def go_home():
return redirect(url_for("index"))url_for("index") 根据视图函数名反向生成 URL,比手写 "/" 更稳。如果以后首页路径变化,只要路由定义更新,调用方不用跟着到处改字符串。
模板:把数据渲染成 HTML
Flask 默认从 templates/ 目录读取模板。一个简单结构如下:
flask-note/
├── app.py
├── static/
│ └── style.css
└── templates/
├── base.html
├── index.html
└── detail.html注意:下面的 Jinja 控制语句为了避开当前博客 Markdown 插件的解析冲突,在 { 和 % 之间、% 和 } 之间加了空格。实际复制到 Flask 项目时,需要去掉这些空格。
基础模板:
<!-- templates/base.html -->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{ % block title % }Flask Note{ % endblock % }</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<a href="{{ url_for('index') }}">Flask Note</a>
</header>
<main>
{ % block content % }{ % endblock % }
</main>
</body>
</html>首页模板:
<!-- templates/index.html -->
{ % extends "base.html" % }
{ % block title % }文章列表{ % endblock % }
{ % block content % }
<h1>文章列表</h1>
{ % if posts % }
<ul>
{ % for post in posts % }
<li>
<a href="{{ url_for('post_detail', post_id=post.id) }}">
{{ post.title }}
</a>
</li>
{ % endfor % }
</ul>
{ % else % }
<p>暂时还没有文章。</p>
{ % endif % }
{ % endblock % }对应视图:
from flask import Flask, render_template
app = Flask(__name__)
posts = [
{"id": 1, "title": "第一篇 Flask 笔记", "content": "从路由开始。"},
{"id": 2, "title": "第二篇 Flask 笔记", "content": "模板继承很好用。"},
]
@app.route("/")
def index():
return render_template("index.html", posts=posts)
@app.route("/posts/<int:post_id>")
def post_detail(post_id):
post = next((item for item in posts if item["id"] == post_id), None)
if post is None:
return "文章不存在", 404
return render_template("detail.html", post=post)Jinja2 默认会对变量进行 HTML 转义,这是安全设计。不要随便使用 |safe,除非你非常确定内容已经经过可信清洗。
表单:从页面提交数据
新建文章页面:
<!-- templates/new.html -->
{ % extends "base.html" % }
{ % block title % }发布文章{ % endblock % }
{ % block content % }
<h1>发布文章</h1>
<form action="{{ url_for('create_post') }}" method="post">
<label>
标题
<input type="text" name="title" required>
</label>
<label>
内容
<textarea name="content" rows="8" required></textarea>
</label>
<button type="submit">发布</button>
</form>
{ % endblock % }处理表单:
from flask import Flask, redirect, render_template, request, url_for
app = Flask(__name__)
posts = []
@app.route("/posts/new")
def new_post():
return render_template("new.html")
@app.route("/posts", methods=["POST"])
def create_post():
title = request.form.get("title", "").strip()
content = request.form.get("content", "").strip()
if not title or not content:
return "标题和内容不能为空", 400
post = {
"id": len(posts) + 1,
"title": title,
"content": content,
}
posts.append(post)
return redirect(url_for("post_detail", post_id=post["id"]))这段代码适合教学,但不适合生产。原因很简单:内存列表会在进程重启后丢失,也不适合多进程部署。下一步就需要数据库。
项目结构:从单文件到应用工厂
当项目开始变大,推荐使用应用工厂:
flask-note/
├── app/
│ ├── __init__.py
│ ├── config.py
│ ├── extensions.py
│ ├── models.py
│ ├── posts/
│ │ ├── __init__.py
│ │ └── routes.py
│ └── templates/
├── tests/
└── pyproject.tomlapp/__init__.py:
from flask import Flask
from .config import Config
from .extensions import db
from .posts.routes import posts_bp
def create_app(config_object=Config):
app = Flask(__name__)
app.config.from_object(config_object)
db.init_app(app)
app.register_blueprint(posts_bp)
return appapp/extensions.py:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()为什么要把 db = SQLAlchemy() 放到 extensions.py,而不是直接绑定某个 app?因为应用工厂会在不同环境中创建不同 app,例如开发、测试和生产。扩展先创建,后初始化,可以避免循环导入,也方便测试。
数据库:用模型描述业务
安装依赖:
pip install flask-sqlalchemy配置 SQLite:
# app/config.py
class Config:
SECRET_KEY = "dev-only-change-me"
SQLALCHEMY_DATABASE_URI = "sqlite:///flask-note.db"
SQLALCHEMY_TRACK_MODIFICATIONS = False定义模型:
# app/models.py
from datetime import datetime
from .extensions import db
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)初始化数据库:
from app import create_app
from app.extensions import db
app = create_app()
with app.app_context():
db.create_all()写入文章:
from flask import Blueprint, redirect, render_template, request, url_for
from ..extensions import db
from ..models import Post
posts_bp = Blueprint("posts", __name__)
@posts_bp.route("/")
def index():
posts = Post.query.order_by(Post.created_at.desc()).all()
return render_template("index.html", posts=posts)
@posts_bp.route("/posts", methods=["POST"])
def create_post():
title = request.form.get("title", "").strip()
content = request.form.get("content", "").strip()
if not title or not content:
return "标题和内容不能为空", 400
post = Post(title=title, content=content)
db.session.add(post)
db.session.commit()
return redirect(url_for("posts.post_detail", post_id=post.id))注意 Blueprint 下的端点名会带上蓝图名前缀,所以这里使用 url_for("posts.post_detail", post_id=post.id)。
Blueprint:按业务模块拆分
Blueprint 可以理解为“可注册的一组路由”。例如文章模块:
# app/posts/routes.py
from flask import Blueprint
posts_bp = Blueprint("posts", __name__, url_prefix="/posts")
@posts_bp.route("/")
def post_list():
return "文章列表"
@posts_bp.route("/<int:post_id>")
def post_detail(post_id):
return f"文章详情:{post_id}"注册到应用:
# app/__init__.py
from .posts.routes import posts_bp
def create_app():
app = Flask(__name__)
app.register_blueprint(posts_bp)
return app拆分建议:
auth:注册、登录、退出、用户状态。posts:文章列表、详情、创建、编辑、删除。admin:后台管理。api:前后端分离接口。
不要为了拆分而拆分。一个只有十几个路由的小项目,用两三个 Blueprint 就够了;模块过细反而会增加跳转成本。
配置:把环境差异集中管理
配置不要散落在业务代码中。推荐写成类:
import os
class BaseConfig:
SECRET_KEY = os.getenv("SECRET_KEY", "dev-only-change-me")
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(BaseConfig):
DEBUG = True
SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db"
class TestingConfig(BaseConfig):
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
class ProductionConfig(BaseConfig):
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]生产环境中,SECRET_KEY、数据库连接串、第三方密钥都应该从环境变量读取,不要写进代码仓库。开发时可以用 .env 辅助加载,但 .env 本身不应提交。
登录:Session 与密码哈希
Flask 提供 Session 机制,但用户登录的完整方案通常还需要密码哈希和登录态判断。最小示例:
from functools import wraps
from flask import redirect, request, session, url_for
from werkzeug.security import check_password_hash, generate_password_hash
def login_required(view):
@wraps(view)
def wrapped_view(**kwargs):
if session.get("user_id") is None:
return redirect(url_for("auth.login", next=request.path))
return view(**kwargs)
return wrapped_view
password_hash = generate_password_hash("plain-password")
is_valid = check_password_hash(password_hash, "plain-password")登录成功后写入 Session:
session.clear()
session["user_id"] = user.id退出登录时清空 Session:
session.clear()这里的关键原则是:数据库里只保存密码哈希,不保存明文密码。Session 中只保存必要的用户标识,不要塞入大量用户信息。
错误处理与用户反馈
不要让用户直接看到一段冷冰冰的 500。Flask 可以注册错误处理函数:
@app.errorhandler(404)
def not_found(error):
return render_template("errors/404.html"), 404
@app.errorhandler(500)
def internal_error(error):
return render_template("errors/500.html"), 500表单操作完成后,可以使用 flash 给用户反馈:
from flask import flash
flash("文章发布成功")模板中读取消息:
{ % with messages = get_flashed_messages() % }
{ % if messages % }
<ul class="messages">
{ % for message in messages % }
<li>{{ message }}</li>
{ % endfor % }
</ul>
{ % endif % }
{ % endwith % }错误处理的价值不只是“页面更好看”,更重要的是把用户体验和系统排错分开:用户看到可理解的提示,开发者通过日志定位真正原因。
测试:让改动有回头路
安装 pytest:
pip install pytest测试配置:
# tests/conftest.py
import pytest
from app import create_app
from app.config import TestingConfig
from app.extensions import db
@pytest.fixture()
def app():
app = create_app(TestingConfig)
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture()
def client(app):
return app.test_client()测试首页:
# tests/test_posts.py
def test_index_page(client):
response = client.get("/")
assert response.status_code == 200
assert "文章列表".encode("utf-8") in response.data测试创建文章:
def test_create_post(client):
response = client.post(
"/posts",
data={
"title": "Flask 测试文章",
"content": "测试可以帮助我们放心重构。",
},
follow_redirects=True,
)
assert response.status_code == 200
assert "Flask 测试文章".encode("utf-8") in response.data测试不是为了追求覆盖率数字,而是为了保护核心行为。对博客项目来说,至少应该覆盖:
- 首页能打开。
- 详情页能打开。
- 空标题不能创建文章。
- 未登录不能进入后台。
- 用户不能编辑别人的文章。
部署:从开发服务器到生产环境
开发服务器适合本地调试,不适合生产。生产环境可以使用 Gunicorn:
pip install gunicorn
gunicorn "app:create_app()" -w 4 -b 127.0.0.1:8000常见部署结构:
浏览器
↓ HTTPS
Nginx
↓ 反向代理
Gunicorn
↓ WSGI
Flask 应用
↓
数据库生产部署检查清单:
DEBUG=False。SECRET_KEY使用强随机值,并通过环境变量注入。- 数据库连接串不提交到仓库。
- 静态资源由 Nginx 或 CDN 处理。
- 日志写入文件或日志系统。
- 关键接口有权限校验。
- 上传文件限制大小和类型。
- 数据库定期备份。
部署是工程闭环的一部分。只会本地运行,项目还停留在练习;能被稳定访问、能排错、能更新,才接近真正的应用。
一个可执行的四周学习方案
第 1 周:Flask 基础与模板
目标:写出一个单文件 Flask 小站。
任务:
- 完成最小应用。
- 写 5 个以上路由。
- 熟悉
request、redirect、url_for。 - 用 Jinja2 写首页、详情页和发布页。
- 用
static/加一份简单 CSS。
验收标准:
- 能解释一个请求从浏览器到视图函数再到响应的完整过程。
- 能不用复制粘贴写出
@app.route()和render_template()。 - 能通过表单提交数据并跳转到详情页。
第 2 周:数据库与应用结构
目标:把单文件脚本改造成可维护项目。
任务:
- 引入应用工厂
create_app()。 - 使用 Blueprint 拆分文章模块。
- 接入 Flask-SQLAlchemy。
- 建立
Post模型。 - 完成文章增删改查。
验收标准:
- 重启服务后文章仍然存在。
- 能说明
db.session.add()和db.session.commit()的作用。 - 能解释为什么扩展对象要先创建、后初始化。
第 3 周:用户系统与安全
目标:让项目具备基础登录和权限。
任务:
- 建立
User模型。 - 注册时保存密码哈希。
- 登录成功后写入 Session。
- 给后台路由添加
login_required。 - 限制用户只能编辑或删除自己的文章。
验收标准:
- 数据库中看不到明文密码。
- 未登录访问后台会被重定向到登录页。
- 尝试编辑别人的文章会被拒绝。
第 4 周:测试与部署
目标:让项目具备交付意识。
任务:
- 为首页、详情页、创建文章写测试。
- 为登录权限写测试。
- 整理
.env.example。 - 使用 Gunicorn 本地模拟生产运行。
- 写一份部署说明。
验收标准:
pytest可以通过。- 生产配置不依赖开发环境变量。
- 能说明 Nginx、Gunicorn、Flask 各自负责什么。
常见坑位
把所有代码写在一个文件里
单文件适合学习,不适合长期维护。随着路由、模型、表单和工具函数变多,一个文件会迅速失控。解决方式是尽早引入应用工厂和 Blueprint,但不要过度拆分。
在生产环境开启 debug
Debug 模式会暴露调试能力,生产环境必须关闭。开发便利和线上安全是两件事,不能混在一起。
明文保存密码
密码必须哈希后保存。即使是练习项目,也应该从一开始养成这个习惯,因为安全问题往往来自“先这样写,以后再改”的遗留代码。
滥用全局变量保存业务数据
内存列表适合第一天练习,不能作为真实存储。多进程部署时,不同进程之间的内存并不共享;进程重启后数据也会丢失。
不写测试就重构
Flask 项目很容易从单文件演进到多模块。重构前如果没有测试,只能靠手动点击页面确认,效率低且容易漏。测试不是仪式感,而是给修改留回头路。
学习方法:用问题驱动框架学习
学 Flask 不建议按 API 文档从头背到尾,而是围绕问题推进:
- 我如何让
/posts/1显示第一篇文章? - 我如何让表单提交后保存数据?
- 我如何让未登录用户不能发布文章?
- 我如何让测试环境使用内存数据库?
- 我如何把本地应用部署到服务器?
每解决一个问题,就把背后的框架机制补上。这样学习路线会更接近真实项目,而不是停留在零散语法。
收束
Flask 的价值在于“少而清楚”。它不会替你决定所有事情,所以学习它时也不能只停在表面写法。路由、模板、数据库、登录、测试、部署,这些模块连起来之后,你会看到一个 Web 应用从请求入口到线上交付的完整骨架。
如果把这篇笔记当作学习锚点,推荐先完成一个最小博客,再逐步加上用户系统、后台管理、测试和部署。不要一开始就追求功能很多,先让每一层都能解释清楚;当你知道每一行代码为什么存在,Flask 才真正变成手里的工具。
- 本文链接:https://sacrellfarch.github.io/posts/flask-learning-anchor
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。