T 教程 Tutorials

Flask Web 开发从入门到实战(详细版)

对照官方文档风格的中文长文:环境与应用工厂、路由与请求响应、Jinja2/表单、Blueprint、配置与 .env、SQLAlchemy CRUD、Session 登录、错误页与 pytest、免费档部署思路与常见坑。

教程

Flask 是一个轻量、显式、可扩展的 Python Web 微框架:核心只做路由、请求/响应与 WSGI 粘合,模板、ORM、表单、认证等按需用扩展或自己写。本篇按官方文档的写法组织,尽量给出可直接粘贴改跑的片段;默认假设 Python 3.10+。

和 Django / FastAPI 比一句:Django 电池齐全(Admin、ORM、用户系统开箱即用),适合大而全的统一约定;FastAPI 以类型注解 + OpenAPI 见长,异步与 API 文档友好;Flask 最小内核、自由度高,原型、教学、中小业务与「自己拼积木」的场景最常见。选库看团队约定与需求形状,不是谁更「高级」。

1. 简介与适用

适合:

  • 快速搭 CRUD、管理后台、Webhook、内部工具
  • 想清楚「每个请求怎么进来、怎么出去」,而不是先吞一整套全家桶
  • 用 Blueprint / 扩展逐步长大,而不是一开始就被脚手架绑死

不太适合或要慎重:

  • 需要开箱 Admin + 强约定的大型单体 → Django 往往更省心
  • 以 JSON API + 严格 schema / 异步为主 → FastAPI / Starlette 更对口
  • 极致性能的纯 ASGI 流式服务 → 也要评估是否还用同步 WSGI 路径

对多数「先跑起来再演进」的项目,先把应用工厂、蓝图和配置分层想清楚,再谈扩展。

2. 环境:venv、pip、安装;应用工厂 vs 单文件

2.1 虚拟环境与安装

python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
python -m pip install -U pip
pip install flask

验证:

python -c "import flask; print(flask.__version__)"

建议把依赖钉死:

pip freeze > requirements.txt
# 之后:pip install -r requirements.txt

2.2 最小单文件应用(适合入门)

hello.py:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "Hello, Flask!"

if __name__ == "__main__":
    app.run(debug=True)

开发时推荐用 CLI(比直接 python hello.py 更贴近文档):

export FLASK_APP=hello
flask run --debug
# 浏览器打开 http://127.0.0.1:5000/

2.3 应用工厂(推荐进阶 / 多配置)

单文件在测试、多环境、扩展初始化时容易缠在一起。官方推荐 application factory:用函数创建 app,配置与扩展在工厂里绑定。

myapp/
  __init__.py      # create_app()
  config.py
  views.py
  templates/
  static/
wsgi.py            # 生产入口:from myapp import create_app; app = create_app()

myapp/__init__.py:

from flask import Flask

def create_app(config_object="myapp.config.DevConfig"):
    app = Flask(__name__)
    app.config.from_object(config_object)

    from myapp import views
    app.register_blueprint(views.bp)

    return app

wsgi.py:

from myapp import create_app

app = create_app()
export FLASK_APP=wsgi:app
flask run --debug

何时用单文件:教程、脚本、极小工具。
何时用工厂:要测多套配置、挂多个 Blueprint、生产/开发切换。

3. 路由、变量规则、HTTP 方法、request / response / redirect / abort

3.1 基础路由与变量

from flask import Flask, abort

app = Flask(__name__)

@app.route("/")
def index():
    return "home"

@app.route("/user/<username>")
def show_user(username):
    return f"User: {username}"

@app.route("/post/<int:post_id>")
def show_post(post_id):
    return f"Post #{post_id}"

@app.route("/path/<path:subpath>")
def show_subpath(subpath):
    return f"Subpath: {subpath}"

常用转换器:string(默认)、int、float、path、uuid。

3.2 HTTP 方法

from flask import request

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        return "do login"
    return "show login form"

只允许某些方法时,其它方法会自动 405。

3.3 request / 响应 / redirect / abort

from flask import request, jsonify, make_response, redirect, url_for, abort

@app.route("/api/echo", methods=["POST"])
def echo():
    data = request.get_json(silent=True) or {}
    # request.args  → 查询串  ?q=1
    # request.form  → 表单字段
    # request.files → 上传文件
    # request.headers / request.cookies
    return jsonify({"ok": True, "data": data})

@app.route("/old")
def old():
    return redirect(url_for("index"), code=302)

@app.route("/secret")
def secret():
    token = request.headers.get("X-Token")
    if token != "s3cr3t":
        abort(401)  # 或 abort(403)、abort(404)
    resp = make_response("ok", 200)
    resp.headers["X-App"] = "demo"
    return resp

url_for("endpoint_name", **params) 按端点名生成 URL,避免硬编码路径;Blueprint 里端点通常是 blueprint_name.view_func_name。

4. Jinja2 模板、静态文件、表单

4.1 模板与继承

默认模板目录:templates/。

templates/base.html:

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title>{% block title %}站点{% endblock %}</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
  <header><a href="{{ url_for('index') }}">首页</a></header>
  <main>{% block content %}{% endblock %}</main>
</body>
</html>

templates/hello.html:

{% extends "base.html" %}
{% block title %}你好 · {{ name }}{% endblock %}
{% block content %}
  <h1>你好,{{ name }}!</h1>
  <ul>
    {% for item in items %}
      <li>{{ item }}</li>
    {% else %}
      <li>暂无条目</li>
    {% endfor %}
  </ul>
{% endblock %}

视图:

from flask import render_template

@app.route("/hello/<name>")
def hello(name):
    return render_template("hello.html", name=name, items=["a", "b"])

常用语法:{{ expr }}、{% if %} / {% for %}、过滤器 |e(默认转义)、{% include %}、宏 macro。

4.2 静态文件

默认 static/,URL 前缀 /static/...:

<img src="{{ url_for('static', filename='logo.png') }}" alt="logo">

4.3 表单:原生 HTML 或 WTForms

原生(教学最直观):

<form method="post" action="{{ url_for('subscribe') }}">
  <input name="email" type="email" required>
  <button type="submit">订阅</button>
</form>
from flask import request, redirect, url_for, flash, render_template

@app.route("/subscribe", methods=["GET", "POST"])
def subscribe():
    if request.method == "POST":
        email = (request.form.get("email") or "").strip()
        if "@" not in email:
            flash("邮箱格式不对", "error")
            return redirect(url_for("subscribe"))
        flash(f"已订阅:{email}", "ok")
        return redirect(url_for("index"))
    return render_template("subscribe.html")

记得设 app.secret_key,flash / session 才可用。

WTForms + Flask-WTF(校验与 CSRF 更省心):

pip install flask-wtf
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Email

class SubscribeForm(FlaskForm):
    email = StringField("邮箱", validators=[DataRequired(), Email()])
    submit = SubmitField("订阅")

模板里用 form.hidden_tag() 输出 CSRF,字段用 {{ form.email() }}。生产务必配置可靠的 SECRET_KEY。

5. Blueprint:组织大项目

Blueprint 把路由、模板、静态资源按「功能包」切开,再在工厂里注册。

myapp/blog/__init__.py:

from flask import Blueprint

bp = Blueprint(
    "blog",
    __name__,
    url_prefix="/blog",
    template_folder="templates",  # 可选:包内模板
)

from myapp.blog import routes  # noqa: E402  注册视图

myapp/blog/routes.py:

from flask import render_template
from myapp.blog import bp

@bp.route("/")
def index():
    return render_template("blog/index.html")

@bp.route("/<int:post_id>")
def detail(post_id):
    return render_template("blog/detail.html", post_id=post_id)

工厂里:

def create_app(config_object="myapp.config.DevConfig"):
    app = Flask(__name__)
    app.config.from_object(config_object)

    from myapp.blog import bp as blog_bp
    app.register_blueprint(blog_bp)

    return app

链接:url_for("blog.detail", post_id=1) → /blog/1。

经验法则:按领域拆(auth、blog、api),不要按「文件大小」随便切;API 可用独立 Blueprint + jsonify。

6. 配置、.env、flask run 与生产提示

6.1 配置类

myapp/config.py:

import os

class Config:
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-change-me")
    SQLALCHEMY_DATABASE_URI = os.environ.get(
        "DATABASE_URL", "sqlite:///app.db"
    )
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevConfig(Config):
    DEBUG = True

class ProdConfig(Config):
    DEBUG = False

6.2 .env(开发)

pip install python-dotenv

项目根 .env(不要提交密钥到 Git):

FLASK_APP=wsgi:app
SECRET_KEY=please-use-a-long-random-string
DATABASE_URL=sqlite:///app.db

flask run 在安装了 python-dotenv 时会自动加载 .env / .flaskenv。也可在工厂里:

from dotenv import load_dotenv
load_dotenv()

6.3 开发 vs 生产

场景做法
本地开发flask run --debug(或 FLASK_DEBUG=1)
生产不要用内置服务器对外;用 Gunicorn / uWSGI 等 WSGI 服务器
密钥SECRET_KEY 用足够长的随机串,且每环境不同
HTTPS由反向代理 / 平台终止 TLS
Debug生产关闭 debug,避免交互式调试器暴露
# 开发
flask run --host=127.0.0.1 --port=5000 --debug

7. SQLite + Flask-SQLAlchemy:CRUD 小例子

pip install flask-sqlalchemy

myapp/extensions.py:

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

模型与路由(可放在 myapp/models.py / myapp/notes.py):

from datetime import datetime, timezone
from flask import Blueprint, request, redirect, url_for, render_template, abort
from myapp.extensions import db

bp = Blueprint("notes", __name__, url_prefix="/notes")

class Note(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(120), nullable=False)
    body = db.Column(db.Text, default="")
    created_at = db.Column(
        db.DateTime, default=lambda: datetime.now(timezone.utc)
    )

@bp.route("/")
def list_notes():
    notes = Note.query.order_by(Note.created_at.desc()).all()
    return render_template("notes/list.html", notes=notes)

@bp.route("/new", methods=["GET", "POST"])
def create_note():
    if request.method == "POST":
        title = (request.form.get("title") or "").strip()
        body = request.form.get("body") or ""
        if not title:
            abort(400)
        note = Note(title=title, body=body)
        db.session.add(note)
        db.session.commit()
        return redirect(url_for("notes.list_notes"))
    return render_template("notes/form.html", note=None)

@bp.route("/<int:note_id>")
def read_note(note_id):
    note = Note.query.get_or_404(note_id)
    return render_template("notes/detail.html", note=note)

@bp.route("/<int:note_id>/edit", methods=["GET", "POST"])
def update_note(note_id):
    note = Note.query.get_or_404(note_id)
    if request.method == "POST":
        note.title = (request.form.get("title") or "").strip() or note.title
        note.body = request.form.get("body") or ""
        db.session.commit()
        return redirect(url_for("notes.read_note", note_id=note.id))
    return render_template("notes/form.html", note=note)

@bp.route("/<int:note_id>/delete", methods=["POST"])
def delete_note(note_id):
    note = Note.query.get_or_404(note_id)
    db.session.delete(note)
    db.session.commit()
    return redirect(url_for("notes.list_notes"))

工厂里初始化:

from myapp.extensions import db

def create_app(config_object="myapp.config.DevConfig"):
    app = Flask(__name__)
    app.config.from_object(config_object)
    db.init_app(app)

    from myapp.notes import bp as notes_bp
    app.register_blueprint(notes_bp)

    with app.app_context():
        db.create_all()  # 演示用;正式项目用迁移(Flask-Migrate/Alembic)

    return app

list.html 片段:

{% for n in notes %}
  <article>
    <a href="{{ url_for('notes.read_note', note_id=n.id) }}">{{ n.title }}</a>
  </article>
{% else %}
  <p>还没有笔记。</p>
{% endfor %}
<a href="{{ url_for('notes.create_note') }}">新建</a>

SQLite 文件路径注意:sqlite:///app.db 相对当前工作目录;部署时用绝对路径或平台提供的磁盘/托管数据库。

Flask 的 session 是签名的 Cookie(默认),不是服务端存储;改 SECRET_KEY 会使旧 session 失效。

from functools import wraps
from flask import session, request, redirect, url_for, render_template, flash

def login_required(view):
    @wraps(view)
    def wrapped(*args, **kwargs):
        if not session.get("user_id"):
            return redirect(url_for("auth.login", next=request.path))
        return view(*args, **kwargs)
    return wrapped

@bp.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username") or ""
        password = request.form.get("password") or ""
        # 演示:硬编码;实战用哈希(Werkzeug generate_password_hash / check_password_hash)
        if username == "admin" and password == "admin":
            session.clear()
            session["user_id"] = 1
            session["username"] = username
            nxt = request.args.get("next") or url_for("index")
            return redirect(nxt)
        flash("用户名或密码错误", "error")
    return render_template("auth/login.html")

@bp.route("/logout", methods=["POST"])
def logout():
    session.clear()
    return redirect(url_for("index"))

@bp.route("/me")
@login_required
def me():
    return f"你好,{session.get('username')}"

设置 Cookie(非 session):

from flask import make_response

@app.route("/theme/<name>")
def set_theme(name):
    resp = make_response(redirect(url_for("index")))
    resp.set_cookie(
        "theme",
        name,
        max_age=30 * 24 * 3600,
        httponly=True,
        samesite="Lax",
        # secure=True,  # 仅 HTTPS 时打开
    )
    return resp

安全提示:生产用强 SECRET_KEY;密码只存哈希;登录表单加 CSRF(Flask-WTF);敏感 Cookie 加 Secure / HttpOnly / 合适的 SameSite。

9. 错误页、日志、测试(pytest)

9.1 错误页

from flask import render_template

@app.errorhandler(404)
def not_found(e):
    return render_template("errors/404.html"), 404

@app.errorhandler(500)
def server_error(e):
    return render_template("errors/500.html"), 500

9.2 日志

import logging
from logging.handlers import RotatingFileHandler

def configure_logging(app):
    if not app.debug and not app.testing:
        handler = RotatingFileHandler(
            "app.log", maxBytes=1_000_000, backupCount=3
        )
        handler.setLevel(logging.INFO)
        app.logger.addHandler(handler)
    app.logger.setLevel(logging.INFO)
    app.logger.info("app started")

开发时 flask run --debug 已有请求日志;生产交给进程管理器 / 平台采集 stdout。

9.3 pytest(可运行最小例)

pip install pytest

tests/conftest.py:

import pytest
from myapp import create_app
from myapp.extensions import db

@pytest.fixture
def app():
    app = create_app("myapp.config.DevConfig")
    app.config.update(
        {
            "TESTING": True,
            "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
            "WTF_CSRF_ENABLED": False,
            "SECRET_KEY": "test",
        }
    )
    with app.app_context():
        db.create_all()
        yield app
        db.session.remove()
        db.drop_all()

@pytest.fixture
def client(app):
    return app.test_client()

tests/test_notes.py:

def test_list_empty(client):
    rv = client.get("/notes/")
    assert rv.status_code == 200

def test_create_note(client):
    rv = client.post(
        "/notes/new",
        data={"title": "hello", "body": "world"},
        follow_redirects=True,
    )
    assert rv.status_code == 200
    assert b"hello" in rv.data
pytest -q

更多见官方 Testing Flask Applications。

10. 部署到免费档:思路简述(不绑付费)

原则:应用用 Gunicorn(或等价 WSGI)跑,前面用反向代理 / 平台边缘提供 HTTPS;SQLite 在多实例或无持久盘时不可靠,免费档可先单实例 + 持久卷,或换平台免费 Postgres。

10.1 本机 / VPS 思路:Gunicorn + Cloudflare Tunnel

pip install gunicorn
gunicorn -w 2 -b 127.0.0.1:8000 wsgi:app

Cloudflare Tunnel(cloudflared)可把内网 localhost:8000 暴露到你的域名,免开公网端口、顺带 HTTPS。适合演示、个人小工具;进程守护用 systemd,密钥走环境变量。

10.2 Render / Fly.io 简述

  • Render:连 Git 仓库,选 Web Service,启动命令例如 gunicorn wsgi:app,设环境变量 SECRET_KEY 等;免费档有休眠与限制,适合 demo。
  • Fly.io:用 Dockerfile 或 buildpack 部署容器,同样跑 Gunicorn;注意免费额度与机器休眠策略。

Dockerfile 示意(可按平台文档调整):

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt gunicorn
COPY . .
ENV FLASK_APP=wsgi:app
CMD ["gunicorn", "-b", "0.0.0.0:8000", "wsgi:app"]

无论哪家:关 debug、强密钥、静态文件可走 CDN/平台、数据库与会话按平台限制选型。本文不绑定任何付费方案。

11. 常见坑与官方文档

常见坑

  1. 生产仍开 debug / 内置服务器对外 → 调试器与性能风险。
  2. 忘记 SECRET_KEY → session / flash / CSRF 异常或可预测签名。
  3. 在 import 时创建全局 app 上乱连扩展 → 测试与多 app 困难;优先工厂 + init_app。
  4. 硬编码 URL → 用 url_for;改路径时少踩坑。
  5. SQLite 路径 / 多 worker 写库 → 工作目录变化导致「找不到库」;多进程写 SQLite 易锁。
  6. 模板注入 → Jinja 默认转义,慎用 |safe;用户 HTML 要消毒。
  7. 只拷贝视图不注册 Blueprint → 路由 404。
  8. 在请求外用 session / url_for / db.session → 需要应用或请求上下文(app.app_context())。
  9. CORS / Cookie 跨站 → 前后端分离时单独配置;SameSite 与 HTTPS 要一起想。
  10. 依赖没进 requirements.txt → 线上 ImportError。

官方与延伸链接


把上面的工厂、Blueprint、笔记 CRUD 与登录串起来,就是一个可继续长成「个人笔记站」的骨架。下一步通常是:Flask-Migrate 做迁移、把用户存进数据库、API Blueprint 与前端分离,以及按官方 Deploying换成真正的进程模型。

评论