导读

如果你已经会写 Python 函数和类,但从来没自己搭过能让人在浏览器里访问的网页服务,这篇教程就是为你准备的。Flask 是 Python 生态里最轻量级的 Web 框架之一——它不强迫你学复杂的 ORM、迁移工具或项目脚手架,你只需要学会几行代码就能让一个网站跑起来。

本文会带着你一步步完成一件事:搭建一个简易的留言板。你能在其中输入昵称和内容发布留言,页面实时显示所有留言,点击单条留言还能查看详情。通过这个项目,你会掌握 Web 开发中最核心的六个概念:

#概念白话解释
1安装与环境隔离别把库装到系统 Python,用虚拟环境隔离每个项目
2路由与视图网址路径 /about 对应哪个函数来处理请求
3URL 参数/message/1 里的 1 怎么传到函数里
4Jinja2 模板把数据塞进 HTML 骨架自动生成完整页面
5表单与 POST用户提交数据时怎么用 <form> + HTTP POST 传过来
6SQLite 存储用内置数据库保存留言而不是存在内存里(一关就丢)

配图在上面那张图里你可以看到完整的「请求生命周期」流程图——读完本文后回看这张图,你会对每个环节都一目了然。

下面按步骤来,从环境准备一路走到生产部署。中间会穿插一些实用建议,其中关于 AI 功能接入的内容放在后面的部署章节统一说明。


第一步:安装 Python 并创建虚拟环境

检查 Python 是否已安装

打开终端(macOS/Linux 用 Terminal,Windows 用 PowerShell 或 CMD),输入:

python3 --version   # macOS / Linux
# 或
python --version    # Windows(可能只写 python)

期望输出类似 Python 3.10.xPython 3.11.x如果输出了版本号就说明 OK。如果提示 command not found 或未识别命令,先去 python.org/downloads 下载安装(Windows 用户记得勾选"Add Python to PATH")。

什么是虚拟环境?

想象你的电脑是一个大厨房,系统自带的 Python 是"公共灶台"——所有人都往上面装东西。如果你在 A 项目装了 Flask 2.x,又在 B 项目装了 Django 4.x,它们可能会互相打架。

虚拟环境(venv) 就是在公共厨房里给你划出一块专属小灶,A 项目和小 B 项目互不影响。每个项目都有自己的依赖包版本,干净又安全。

创建并激活

# 1. 为 Flask 项目新建一个文件夹
mkdir flask-app && cd flask-app

# 2. 创建虚拟环境(名字 .venv 是业界惯例,点前缀表示隐藏)
python3 -m venv .venv

# 3. 激活它
# Linux / macOS:
source .venv/bin/activate
# Windows PowerShell:
.venv\Scripts\Activate.ps1

激活成功后,终端前面会出现 (.).venv 标记,表示你现在在小灶上干活了。之后任何 pip install 都只会装到这个虚拟环境里,不会影响系统 Python。

国内镜像加速:清华 PyPI 镜像 https://pypi.tuna.tsinghua.edu.cn/simple/ 比官方源快很多。如果后续安装慢或超时,可以加 -i 参数指定镜像源;清华不可用时回退阿里源 https://mirrors.aliyun.com/pypi/simple/,最后再试官方源。


第二步:安装 Flask

pip install flask

一行搞定。装完后可以用以下命令确认版本:

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

当前稳定版一般在 3.x 区间。如果你的输出是 3.1.x 之类的数字就对了。


第三步:编写第一个程序 —— “Hello World”

在项目根目录下创建一个文件 app.py,写入以下内容:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "你好,世界!这是第一条 Flask 页面。"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

逐行解释:

代码作用
from flask import Flask导入框架核心类
app = Flask(__name__)实例化一个应用对象(__name__ 告诉 Flask 去哪找模板和静态文件)
@app.route("/")装饰器:把 URL /(首页)绑到下面的 hello() 函数
def hello()视图函数:收到 / 的请求时执行这个函数,返回值直接变成网页内容
app.run(...)启动本地服务器,监听 5000 端口

运行它:

python app.py

然后在浏览器打开 http://localhost:5000/,你会看到那行中文。

⚠️ host="0.0.0.0" 表示监听所有网卡(包括局域网 IP),不只是本机。如果你是做开发调试也可以写成 "127.0.0.1"(仅本机可访问)。


第四步:理解路由与视图函数

刚才的 @app.route("/") 是最简单的例子。Flask 的路由非常灵活,支持多种 URL 模式:

@app.route("/")                                       # 固定路径
def index():
    return "首页"

@app.route("/about")                                  # 另一条固定路径
def about():
    return "关于我们"

@app.route("/user/<username>")                        # 可变路径(字符串)
def show_user(username):
    return f"欢迎,{username}!"

@app.route("/post/<int:post_id>")                     # 可变路径(整数)
def show_post(post_id):
    return f"文章编号:{post_id}"

@app.route("/page/<float:price>")                     # 可变路径(浮点数)
def show_price(price):
    return f"价格:¥{price:.2f}"

关键点:

  • <变量名> 中的类型前缀(int:float:)是可选的约束——不写默认接受任意字符串
  • URL 参数自动以同名传入视图函数
  • @app.route 可以多次使用来注册多条路径,顺序不重要(Flask 内部有匹配表)

回到我们的留言板项目,我们需要三条路由:

  • / — 展示所有留言(GET)
  • /add — 接收新留言提交(POST)
  • /message/<int:id> — 查看单条留言详情(GET)

第五步:Jinja2 模板引擎 —— 让 HTML 带上数据

纯字符串返回适合做 API,但要做页面必须用模板。Flask 内置 Jinja2 模板引擎——它的语法简单,本质就是在 HTML 里插${{变量}}

创建目录 templates/(注意这个名字不能改,Flask 默认只认它):

flask-app/
├── app.py              ← 业务逻辑
└── templates/          ← Jinja2 模板
    └── index.html      ← 首页模板

模板写法示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>{{ title }}</title>
</head>
<body>
  <h1>{{ title }}</h1>

  <!-- 循环遍历列表 -->
  {% for msg in messages %}
    <div class="msg-card">
      <strong>{{ msg.name }}</strong>:{{ msg.content }}
    </div>
  {% endfor %}

  <!-- 条件判断 -->
  {% if messages %}
    <p>共 {{ messages|length }} 条留言</p>
  {% else %}
    <p>还没有留言,来做第一个留言的人吧!</p>
  {% endif %}
</body>
</html>

对应的 Python 代码这样传递数据:

@app.route("/")
def index():
    messages = [{"name": "小明", "content": "你好呀!"}, {"name": "小红", "content": "很高兴认识你"}]
    return render_template("index.html", title="留言板", messages=messages)

这里 render_template() 就是 Flask 提供的胶水函数——它找到 templates/index.html,把 {% %} 占位符替换成真实数据,返回完整的 HTML 给浏览器。

{{ msg.content }} 会自动转义 HTML 特殊字符(比如用户输入 <script> 会变成 &lt;script&gt;),防止 XSS 攻击。如果你想手动渲染可信 HTML 可以用 {{ content|safe }},但对用户输入的内容不要用 safe 过滤器。


第六步:处理表单提交 —— GET 与 POST

现在我们来加留言功能。用户填写昵称和内容后点击提交,这触发的是一个 HTTP POST 请求。HTML 表单长这样:

<form method="POST" action="/add">
  <label>昵称:</label>
  <input type="text" name="name" required>

  <label>留言内容:</label>
  <textarea name="content" required></textarea>

  <button type="submit">发布留言</button>
</form>

关键属性:

  • method="POST" —— 数据放在请求体里发送(不是拼在 URL 上),适合提交敏感或大量数据
  • action="/add" —— 提交后发到 /add 这个 URL
  • name="xxx" —— 每个字段的 name 属性会被 Flask 收集到 request.form 字典中

视图函数接收方式:

from flask import request, redirect, url_for, flash

@app.route("/add", methods=["POST"])
def add_message():
    # 从表单取数据
    name = request.form.get("name", "").strip()[:30]     # 最多 30 个字符
    content = request.form.get("content", "").strip()[:200]  # 最多 200 个字符

    # 校验
    if not name or not content:
        flash("昵称和内容都不能为空!", "error")            # 闪现消息
        return redirect(url_for("index"))                  # 跳回首页

    # TODO: 存数据库...

    flash("留言发布成功!")
    return redirect(url_for("index"))                      # 提交成功也跳回首页

几个新概念:

  • flash():闪现消息机制——设置一条一次性的消息,下次渲染页面时取出来显示(比如"留言成功"或"必填项为空")
  • url_for():不要硬编码 URL //add,用 url_for('函数名') 动态生成。以后改路由地址不用满篇找替换
  • methods=["POST"]:默认路由只接受 GET。明确声明只接受 POST 意味着如果有人用浏览器直接访问 /add(那是 GET 请求),Flask 会返回 405 Method Not Allowed

第七步:数据存储 —— 用 SQLite 持久化留言

内存里的变量关了服务就没了,所以需要数据库。Python 内置 sqlite3 模块(无需额外安装),配合 Flask-SQLAlchemy 可以更优雅地操作数据库。这里我们先用最简单的原生 sqlite3 方式理解原理。

import sqlite3
import os

DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "messages.db")

def get_db():
    """获取数据库连接"""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row       # 让结果能用列名访问(dict-like)
    return conn

def init_db():
    """建表(幂等:表已存在就跳过)"""
    conn = get_db()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            content TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    conn.commit()
    conn.close()

写入留言时:

conn = get_db()
conn.execute(
    "INSERT INTO messages (name, content) VALUES (?, ?)",
    (name, content)           # 元组传参,自动防 SQL 注入
)
conn.commit()
conn.close()

重点:一定要用 ? 占位符传参(即上面的 (name, content)),不要把字符串拼接进 SQL(如 f"INSERT ... VALUES ('{name}')")——后者容易被 SQL 注入攻击

读取留言用于首页展示:

conn = get_db()
rows = conn.execute(
    "SELECT * FROM messages ORDER BY created_at DESC LIMIT 20"
).fetchall()
conn.close()
messages = [dict(r) for r in rows]     # Row → dict,传给模板

如果是更复杂的项目,建议安装 Flask-SQLAlchemy 扩展(pip install flask-sqlalchemy),用 ORM 方式写 CRUD——不需要手写 SQL,代码也更简洁。但对于学习理解底层流程,原生 sqlite3 反而更直白。

顺便提一句,当你把这个留言板部署上线后,如果将来想加入 AI 对话功能(比如给每条留言智能回复),接入 OpenAI 兼容接口会很方便。像云间 API 中转站(https://cloudzone-api.cyou/)就提供 OpenAI 兼容接口,同时支持 Anthropic 系列模型,国内直连无需翻墙,90+ 模型 0.05x 起的价格让它成为 Flask 项目中调用大模型的省钱选择。


第八步:完整代码一览

把上面的零散片段组合起来,就是本教程的完整 app.py

import sqlite3
import os
from flask import Flask, render_template, request, redirect, url_for, flash

app = Flask(__name__)
app.secret_key = "dev-change-in-production"     # flash() 需要密钥

DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "messages.db")

def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

def init_db():
    conn = get_db()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            content TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    conn.commit()
    conn.close()

@app.route("/")
def index():
    conn = get_db()
    rows = conn.execute(
        "SELECT * FROM messages ORDER BY created_at DESC LIMIT 20"
    ).fetchall()
    conn.close()
    return render_template(
        "index.html",
        title="留言板",
        messages=[dict(r) for r in rows]
    )

@app.route("/add", methods=["POST"])
def add_message():
    name = request.form.get("name", "").strip()[:30]
    content = request.form.get("content", "").strip()[:200]
    if not name or not content:
        flash("昵称和内容都不能为空!", "error")
        return redirect(url_for("index"))
    conn = get_db()
    conn.execute(
        "INSERT INTO messages (name, content) VALUES (?, ?)",
        (name, content)
    )
    conn.commit()
    conn.close()
    flash("留言发布成功!")
    return redirect(url_for("index"))

@app.route("/message/<int:message_id>")
def message_detail(message_id):
    conn = get_db()
    row = conn.execute(
        "SELECT * FROM messages WHERE id = ?", (message_id,)
    ).fetchone()
    conn.close()
    if row is None:
        flash("该留言不存在。")
        return redirect(url_for("index"))
    return render_template(
        "index.html",
        title=f"留言 #{message_id}",
        messages=[dict(row)],
        single_mode=True
    )

if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=5000, debug=True)

debug=True 开启调试模式——代码修改后自动重载,报错时会在浏览器显示交互式错误页。但记住:只能开发时用,上线要关掉!

模板文件 templates/index.html 包含完整的 HTML/CSS 结构(表单、留言列表、闪烁消息展示)。一键脚本中已附带完整模板源码,这里不再重复粘贴——见文末下载链接。


第九步:启动与测试

# 确保已激活虚拟环境 (.venv)
python app.py

控制台输出类似:

 * Serving Flask app 'app'
 * Debug mode: on
 * Running on http://0.0.0.0:5000

此时打开浏览器访问 http://localhost:5000/,尝试输入昵称和内容并提交。验证要点:

  1. 提交后页面无刷新出现"留言发布成功!“的绿色提示(flash)
  2. 新留言出现在列表顶部
  3. 点击某条留言跳转到详情页,URL 变为 /message/1
  4. 留空提交会显示红色的"必填项为空"提示

如果想关闭服务,在终端按 Ctrl+C


第十步:生产部署提示 —— 不要用开发服务器上线

app.run(debug=True) 是 Flask 内置的开发服务器,非常适合调试但不应该用于生产环境——原因:

  • 性能差:单线程/多线程处理,并发量大时会阻塞
  • 不安全:调试模式和错误回显泄露敏感信息
  • 无恢复能力:进程挂了不会自动重启

推荐的部署方案

方案适用场景简要说明
gunicornLinux 服务器主流选择安装 pip install gunicorn,命令行启动:gunicorn -w 4 -b 0.0.0.0:5000 app:app
uWSGI + nginx高流量生产环境nginx 做反向代理 + uWSGI 作为 WSGI 服务器
Docker容器化部署Dockerfile + docker-compose.yml,一套配置到处运行
云平台 PaaS不想管服务器Heroku / Railway / Render 等平台一键部署

对于小型博客或个人项目,gunicorn 最简单:

pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 app:app

其中 -w 4 表示启动 4 个工作进程(根据你的 CPU 核心数调整),app:app 格式是 <文件名>:<变量名>——第一个 app 指 app.py 文件,第二个 app 是该文件中创建的 Flask 实例变量名。

如果你需要一个既简单又能自动处理的部署方案,可以考虑用 Docker 打包后推送到云平台。而如果需要在此基础上加上 AI 对话功能——比如让用户提问后自动调用大模型生成回复——云间 API 中转站(https://cloudzone-api.cyou/)的香港节点直连速度快、价格低(0.05x 起,支持 OpenAI 和 Anthropic 兼容协议),把它配到你的 gunicorn 部署里几乎没有额外成本。


一键脚本

如果你觉得手动敲命令太繁琐,文末提供了完整的一键脚本(.sh + .ps1 双版本)。它会自动检测网络环境(国内/海外)、选择最优 PyPI 镜像、创建虚拟环境、安装 Flask、生成完整的留言簿项目并启动验证。

不方便下载的同学可以直接复制下方完整源码,新建文本文档粘贴后改为 .sh.ps1 运行。也可从 https://cleanresolver.com/scripts/install-flask-guide.shhttps://cleanresolver.com/scripts/install-flask-guide.ps1 下载。

Linux / macOS / WSL 一键脚本

#!/usr/bin/env bash
set -u
# ============================================================
#  Flask Web 应用开发实战 一键脚本(macOS / Linux / WSL)
#  公开源码,欢迎审查 —— 不放心可先复制给 AI 判断
#  本教程不涉及 API Key,无需云间中转站配置
# ============================================================

GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m'
info()  { echo -e "${GREEN}[INFO]${NC} $1"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
step()  { echo -e "${CYAN}[STEP]${NC} $1"; }

detect_network() {
  step "检测网络环境(国内 / 国外)..."
  if curl -fsI --max-time 5 "https://claude.ai" >/dev/null 2>&1; then
    info "可直连 claude.ai,判定为海外网络"
    echo "overseas"
  else
    warn "无法直连 claude.ai,判定为国内网络环境(将自动切换镜像源)"
    echo "domestic"
  fi
}

check_python3() {
  step "检查 Python 3 环境..."
  local found=""
  for p in python3 python; do
    if command -v "$p" >/dev/null 2>&1 && "$p" --version 2>&1 | grep -qE "Python 3\.[0-9]+"; then
      found="$p"; break
    fi
  done
  if [ -n "$found" ]; then
    info "找到 $found$($found --version 2>&1)"
    return 0
  fi
  error "未找到 Python 3。请先安装:"
  echo "  · Ubuntu/Debian:sudo apt update && sudo apt install -y python3 python3-venv"
  echo "  · CentOS/RHEL:sudo yum install -y python3"
  echo "  · macOS:brew install python3"
  echo "  · 官网:https://www.python.org/downloads/"
  return 1
}

setup_project() {
  local net="$1"
  step "创建项目目录和虚拟环境..."
  mkdir -p flask-app && cd flask-app || return 1

  if [ -d ".venv" ]; then
    warn "发现已有的 .venv,删除后重新创建..."
    rm -rf .venv
  fi

  ${PY} -m venv .venv || { error "创建虚拟环境失败"; return 1; }

  # shellcheck disable=SC1091
  source .venv/bin/activate || { error "激活虚拟环境失败"; return 1; }
  info "虚拟环境已创建并激活"

  step "升级 pip 包管理器..."
  if ! pip install --upgrade pip >/dev/null 2>&1; then
    warn "pip 升级失败,尝试备用源..."
    pip install --upgrade pip -i "https://mirrors.aliyun.com/pypi/simple/" >/dev/null 2>&1 || \
      warn "pip 升级仍有问题,尝试继续安装依赖"
  fi
  info "pip 已就绪"

  step "安装 Flask 及其依赖..."
  if pip install flask --timeout 120 -i "https://pypi.tuna.tsinghua.edu.cn/simple" >/dev/null 2>&1; then
    info "Flask 安装成功(清华镜像)"
  elif pip install flask --timeout 120 -i "https://mirrors.aliyun.com/pypi/simple" >/dev/null 2>&1; then
    info "Flask 安装成功(阿里镜像)"
  elif pip install flask --timeout 120 >/dev/null 2>&1; then
    info "Flask 安装成功(官方源)"
  else
    error "所有 PyPI 源均失败。请手动执行:"
    echo "  pip install flask -i https://pypi.tuna.tsinghua.edu.cn/simple"
    return 1
  fi

  FLASK_VER=$(python3 -c "import flask; print(flask.__version__)")
  info "Flask 版本:$FLASK_VER"
}

generate_app() {
  step "生成 Flask 示例应用(留言簿)..."
  mkdir -p templates

  cat > app.py <<'APPEOF'
"""
Flask 留言簿示例 — 涵盖路由、视图、URL 参数、
Jinja2 模板渲染、表单 POST 请求、SQLite 存储。
"""
import sqlite3
import os
from flask import Flask, render_template, request, redirect, url_for, flash

app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", "dev-change-in-production")

DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "messages.db")


def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    conn = get_db()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            content TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    conn.commit()
    conn.close()


@app.route("/")
def index():
    conn = get_db()
    rows = conn.execute("SELECT * FROM messages ORDER BY created_at DESC LIMIT 20").fetchall()
    conn.close()
    messages = [dict(r) for r in rows]
    return render_template("index.html", title="留言板", messages=messages)


@app.route("/add", methods=["POST"])
def add_message():
    name = request.form.get("name", "").strip()[:30]
    content = request.form.get("content", "").strip()[:200]
    if not name or not content:
        flash("昵称和内容都不能为空!", "error")
        return redirect(url_for("index"))
    conn = get_db()
    conn.execute("INSERT INTO messages (name, content) VALUES (?, ?)", (name, content))
    conn.commit()
    conn.close()
    flash("留言发布成功!")
    return redirect(url_for("index"))


@app.route("/message/<int:message_id>")
def message_detail(message_id):
    conn = get_db()
    row = conn.execute("SELECT * FROM messages WHERE id = ?", (message_id,)).fetchone()
    conn.close()
    if row is None:
        flash("该留言不存在或已被删除。")
        return redirect(url_for("index"))
    return render_template(
        "index.html", title=f"留言 #{message_id}",
        messages=[dict(row)], single_mode=True
    )


if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=5000, debug=True)
APPEOF

  cat > templates/index.html <<'HTMLEOF'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ title }}</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #f0f2f5; color: #333; line-height: 1.6; padding: 1rem; }
    .container { max-width: 720px; margin: 0 auto; }
    h1 { text-align: center; margin-bottom: 1rem; color: #1a1a2e; }
    form { background: #fff; border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: 0 2px 4px rgba(0,0,0,.08); }
    label { display: block; margin-top: .8rem; font-weight: 600; }
    input[type=text], textarea { width: 100%; padding: .5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; margin-top: .3rem; }
    textarea { height: 80px; resize: vertical; }
    button { margin-top: 1rem; padding: .6rem 1.5rem; background: #4361ee; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 15px; }
    button:hover { background: #3a56d4; }
    .flash-error { background: #ffe0e0; color: #c0392b; padding: .6rem; border-radius: 4px; margin-bottom: 1rem; }
    .flash-ok { background: #e0ffe0; color: #27ae60; padding: .6rem; border-radius: 4px; margin-bottom: 1rem; }
    .msg-card { background: #fff; border-radius: 8px; padding: 1rem 1.2rem; margin-bottom: .8rem; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
    .msg-card a { color: #4361ee; text-decoration: none; font-weight: 600; }
    .msg-card a:hover { text-decoration: underline; }
    .meta { font-size: 12px; color: #999; margin-top: .3rem; }
  </style>
</head>
<body>
<div class="container">
  <h1>{{ title }}</h1>
  {% with messages = get_flashed_messages(with_categories=true) %}
    {% for category, msg in messages %}
      <div class="{{ 'flash-error' if category == 'error' else 'flash-ok' }}">{{ msg }}</div>
    {% endfor %}
  {% endwith %}
  <form method="POST" action="{{ url_for('add_message') }}">
    <label for="name">你的昵称:</label>
    <input type="text" id="name" name="name" placeholder="例如:小明" required>
    <label for="content">留言内容:</label>
    <textarea id="content" name="content" placeholder="说点什么吧……" required></textarea>
    <button type="submit">发布留言</button>
  </form>
  {% if messages %}
    {% for m in messages %}
      <div class="msg-card">
        <a href="{{ url_for('message_detail', message_id=m.id) }}">{{ m.name }}:</a>{{ m.content }}
        <div class="meta">{{ m.created_at }}</div>
      </div>
    {% endfor %}
  {% endif %}
</div>
</body>
</html>
HTMLEOF

  info "已生成 app.py 与 templates/index.html"
}

verify_site() {
  step "启动服务并在后台验证页面可达性..."
  python app.py &
  APP_PID=$!

  local waited=0
  while [ $waited -lt 10 ]; do
    if curl -fsI --max-time 3 "http://localhost:5000/" >/dev/null 2>&1; then break; fi
    sleep 1
    waited=$((waited + 1))
  done

  if [ $waited -ge 10 ]; then
    kill $APP_PID 2>/dev/null
    error "Flask 服务未能启动,请检查上方报错信息"
    return 1
  fi

  local STATUS
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost:5000/")
  if [ "$STATUS" = "200" ]; then
    info "首页 http://localhost:5000/ 返回 $STATUS —— 一切正常!"
  else
    error "首页返回 $STATUS(期望 200),请检查 Flask 日志"
    kill $APP_PID 2>/dev/null
    return 1
  fi

  STEP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \
    -X POST "http://localhost:5000/add" \
    -d "name=测试用户&content=这是一条自动化测试留言" 2>/dev/null)
  if [ "$STEP_STATUS" = "302" ] || [ "$STEP_STATUS" = "200" ]; then
    info "POST /add 返回 $STEP_STATUS —— 表单提交流程正常"
  else
    warn "POST /add 返回 $STEP_STATUS(期望 302/200),可能仍需手动验证"
  fi

  kill $APP_PID 2>/dev/null
  wait $APP_PID 2>/dev/null
}

main() {
  echo "============================================"
  echo "  Flask Web 应用开发实战 一键脚本"
  echo "  适用于 macOS / Linux / WSL"
  echo "  功能:环境检查 → 安装 Flask → 生成留言簿 → 自动验证"
  echo "============================================"
  echo ""

  if ! check_python3; then exit 1; fi
  for p in python3 python; do
    if command -v "$p" >/dev/null 2>&1 && "$p" --version 2>&1 | grep -qE "Python 3\.[0-9]+"; then
      PY="$p"; break
    fi
  done

  local NET
  NET=$(detect_network)

  setup_project "$NET"
  generate_app
  verify_site

  echo ""
  echo "============================================"
  info "全部完成!"
  echo "  进入项目目录:cd flask-app"
  echo "  激活虚拟环境:source .venv/bin/activate"
  echo "  启动服务:    python app.py"
  echo "  访问首页:    浏览器打开 http://localhost:5000/"
  echo "  退出环境:    deactivate"
  echo "============================================"
}

main "$@"

Windows PowerShell 一键脚本

# ============================================================
#  Flask Web 应用开发实战 一键脚本(Windows PowerShell)
#  公开源码,欢迎审查 -- 不放心可先复制给 AI 判断
#  本教程不涉及 API Key,无需云间中转站配置
# ============================================================
$ErrorActionPreference = "Stop"

function Write-Info  { Write-Host "[INFO] $args" -ForegroundColor Green }
function Write-Warn  { Write-Host "[WARN] $args" -ForegroundColor Yellow }
function Write-Err   { Write-Host "[ERROR] $args" -ForegroundColor Red }
function Write-Step  { Write-Host "[STEP] $args" -ForegroundColor Cyan }

function Detect-Network {
    Write-Step "检测网络环境(国内 / 国外)..."
    try {
        $null = Invoke-WebRequest -Uri "https://claude.ai" -Method Head -TimeoutSec 5 -UseBasicParsing
        Write-Info "可直连 claude.ai,判定为海外网络"
        return "overseas"
    } catch {
        Write-Warn "无法直连 claude.ai,判定为国内网络环境(将自动切换镜像源)"
        return "domestic"
    }
}

function Check-Python3 {
    Write-Step "检查 Python 3 环境..."
    foreach ($ver in @("python3", "python")) {
        $info = $null
        try { $info = & $ver --version 2>&1 } catch {}
        if ($info -and $info -match "Python 3\.\d+") {
            Write-Info "找到 $ver$info"
            return $ver
        }
    }
    Write-Err "未找到 Python 3。请先安装:"
    Write-Host "  · 官网:https://www.python.org/downloads/"
    Write-Host "  · Windows 安装时请勾选 'Add Python to PATH'"
    Write-Host "  · 或使用 Microsoft Store 搜索 'Python 3' 安装"
    return $null
}

function Setup-Project {
    param([string]$Net)
    Write-Step "创建项目目录和虚拟环境..."
    $projectDir = Join-Path (Get-Location) "flask-app"
    if (Test-Path $projectDir) {
        Write-Warn "发现已有的 flask-app 目录,删除后重新创建..."
        Remove-Item -Recurse -Force $projectDir
    }
    New-Item -ItemType Directory -Path $projectDir | Out-Null
    Set-Location $projectDir
    $py = if ($env:_FLASK_PY) { $env:_FLASK_PY } else { "python3" }

    & $py -m venv .venv | Out-Null
    if ($LASTEXITCODE -ne 0) { Write-Err "创建虚拟环境失败"; return $false }

    $envPath = Join-Path $projectDir ".venv\Scripts\Activate.ps1"
    if (Test-Path $envPath) {
        . $envPath
        Write-Info "虚拟环境已激活"
    } else {
        Write-Warn "未找到激活脚本,尝试继续..."
    }

    Write-Step "升级 pip 包管理器..."
    try { & python -m pip install --upgrade pip 2>&1 | Out-Null } catch { Write-Warn "pip 升级有警告" }

    $sources = @()
    if ($Net -eq "domestic") {
        $sources += "https://mirrors.aliyun.com/pypi/simple/"
        $sources += "https://pypi.tuna.tsinghua.edu.cn/simple/"
        $sources += "https://pypi.org/simple"
    } else {
        $sources += "https://pypi.tuna.tsinghua.edu.cn/simple/"
        $sources += "https://pypi.org/simple"
        $sources += "https://mirrors.aliyun.com/pypi/simple/"
    }

    Write-Step "安装 Flask 及其依赖..."
    foreach ($src in $sources) {
        try {
            & python -m pip install flask --timeout 120 -i $src 2>&1 | Out-Null
            $ver = & python -c "import flask; print(flask.__version__)" 2>&1
            Write-Info "Flask 安装成功!版本:$ver (源:$src.TrimEnd('/'))"
            return $true
        } catch {
            Write-Warn "源 $src 失败:$($_.Exception.Message)"
        }
    }

    Write-Err "所有 PyPI 源均失败。请手动执行:"
    Write-Host "  python -m pip install flask -i https://pypi.tuna.tsinghua.edu.cn/simple"
    return $false
}

function Generate-App {
    Write-Step "生成 Flask 示例应用(留言簿)..."
    $currentDir = Get-Location
    $templatesDir = Join-Path $currentDir "templates"
    New-Item -ItemType Directory -Path $templatesDir | Out-Null

    $appContent = @'
"""
Flask 留言簿示例 — 涵盖路由、视图、URL 参数、
Jinja2 模板渲染、表单 POST 请求、SQLite 存储。
"""
import sqlite3
import os
from flask import Flask, render_template, request, redirect, url_for, flash

app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", "dev-change-in-production")

DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "messages.db")


def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    conn = get_db()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            content TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    conn.commit()
    conn.close()


@app.route("/")
def index():
    conn = get_db()
    rows = conn.execute("SELECT * FROM messages ORDER BY created_at DESC LIMIT 20").fetchall()
    conn.close()
    messages = [dict(r) for r in rows]
    return render_template("index.html", title="留言板", messages=messages)


@app.route("/add", methods=["POST"])
def add_message():
    name = request.form.get("name", "").strip()[:30]
    content = request.form.get("content", "").strip()[:200]
    if not name or not content:
        flash("昵称和内容都不能为空!", "error")
        return redirect(url_for("index"))
    conn = get_db()
    conn.execute("INSERT INTO messages (name, content) VALUES (?, ?)", (name, content))
    conn.commit()
    conn.close()
    flash("留言发布成功!")
    return redirect(url_for("index"))


@app.route("/message/<int:message_id>")
def message_detail(message_id):
    conn = get_db()
    row = conn.execute("SELECT * FROM messages WHERE id = ?", (message_id,)).fetchone()
    conn.close()
    if row is None:
        flash("该留言不存在或已被删除。")
        return redirect(url_for("index"))
    return render_template(
        "index.html", title=f"留言 #{message_id}",
        messages=[dict(row)], single_mode=True
    )


if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=5000, debug=True)
'@
    Set-Content -Path (Join-Path $currentDir "app.py") -Value $appContent -Encoding UTF8

    $htmlContent = @'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ title }}</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #f0f2f5; color: #333; line-height: 1.6; padding: 1rem; }
    .container { max-width: 720px; margin: 0 auto; }
    h1 { text-align: center; margin-bottom: 1rem; color: #1a1a2e; }
    form { background: #fff; border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: 0 2px 4px rgba(0,0,0,.08); }
    label { display: block; margin-top: .8rem; font-weight: 600; }
    input[type=text], textarea { width: 100%; padding: .5rem; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; margin-top: .3rem; }
    textarea { height: 80px; resize: vertical; }
    button { margin-top: 1rem; padding: .6rem 1.5rem; background: #4361ee; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 15px; }
    button:hover { background: #3a56d4; }
    .flash-error { background: #ffe0e0; color: #c0392b; padding: .6rem; border-radius: 4px; margin-bottom: 1rem; }
    .flash-ok { background: #e0ffe0; color: #27ae60; padding: .6rem; border-radius: 4px; margin-bottom: 1rem; }
    .msg-card { background: #fff; border-radius: 8px; padding: 1rem 1.2rem; margin-bottom: .8rem; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
    .msg-card a { color: #4361ee; text-decoration: none; font-weight: 600; }
    .msg-card a:hover { text-decoration: underline; }
    .meta { font-size: 12px; color: #999; margin-top: .3rem; }
  </style>
</head>
<body>
<div class="container">
  <h1>{{ title }}</h1>
  {% with messages = get_flashed_messages(with_categories=true) %}
    {% for category, msg in messages %}
      <div class="{{ 'flash-error' if category == 'error' else 'flash-ok' }}">{{ msg }}</div>
    {% endfor %}
  {% endwith %}
  <form method="POST" action="{{ url_for('add_message') }}">
    <label for="name">你的昵称:</label>
    <input type="text" id="name" name="name" placeholder="例如:小明" required>
    <label for="content">留言内容:</label>
    <textarea id="content" name="content" placeholder="说点什么吧……" required></textarea>
    <button type="submit">发布留言</button>
  </form>
  {% if messages %}
    {% for m in messages %}
      <div class="msg-card">
        <a href="{{ url_for('message_detail', message_id=m.id) }}">{{ m.name }}:</a>{{ m.content }}
        <div class="meta">{{ m.created_at }}</div>
      </div>
    {% endfor %}
  {% endif %}
</div>
</body>
</html>
'@
    Set-Content -Path (Join-Path $templatesDir "index.html") -Value $htmlContent -Encoding UTF8
    Write-Info "已生成 app.py 与 templates/index.html"
}

function Verify-Site {
    Write-Step "启动服务并在后台验证页面可达性..."
    $job = Start-Job -ScriptBlock {
        param($dir)
        Set-Location $dir
        & python app.py 2>&1 | Out-File -FilePath (Join-Path $dir "flask.log") -Encoding UTF8
    } -ArgumentList (Get-Location)

    $waited = 0
    while ($waited -lt 15) {
        try {
            $resp = Invoke-WebRequest -Uri "http://localhost:5000/" -TimeoutSec 3 -UseBasicParsing
            if ($resp.StatusCode -eq 200) { break }
        } catch { Start-Sleep -Seconds 1; $waited++ }
    }

    if ($waited -ge 15) { Stop-Job $job; Write-Err "Flask 未能启动,检查 flask.log"; return $false }

    try {
        $status = (Invoke-WebRequest -Uri "http://localhost:5000/" -TimeoutSec 5 -UseBasicParsing).StatusCode
        if ($status -eq 200) {
            Write-Info "首页返回 $status —— 一切正常!"
        } else {
            Write-Err "首页返回 $status(期望 200),检查 flask.log"
            Stop-Job $job; return $false
        }
    } catch {
        Write-Err "无法访问首页:$($_.Exception.Message)"; Stop-Job $job; return $false
    }

    try {
        $postResp = Invoke-WebRequest -Uri "http://localhost:5000/add" -Method Post `
            -Body @{ name="测试用户"; content="自动化测试留言" } -TimeoutSec 5 -UseBasicParsing
        $postStatus = if ($postResp.StatusCode -in @(200, 302)) { $postResp.StatusCode } else { "未知:$($postResp.StatusCode)" }
        Write-Info "POST /add 返回 $postStatus —— 表单正常"
    } catch {
        Write-Warn "POST /add 异常:$($_.Exception.Message),需手动验证"
    }

    Stop-Job $job; Receive-Job $job 2>&1 | Out-Null; Remove-Job $job -Force | Out-Null
    return $true
}

Write-Host "============================================"
Write-Host "  Flask Web 应用开发实战 一键脚本"
Write-Host "  适用于 Windows"
Write-Host "============================================"
Write-Host ""

$pythonExe = Check-Python3
if (-not $pythonExe) { exit 1 }
$env:_FLASK_PY = $pythonExe

$net = Detect-Network
if (-not (Setup-Project -Net $net)) { Write-Err "项目搭建失败"; exit 1 }

Generate-App
if (-not (Verify-Site)) { Write-Err "验证出错"; exit 1 }

Write-Host ""
Write-Host "============================================"
Write-Info "全部完成!"
Write-Host "  进入项目目录:cd flask-app"
Write-Host "  激活虚拟环境:.venv\Scripts\Activate.ps1"
Write-Host "  启动服务:    python app.py"
Write-Host "  访问首页:    http://localhost:5000/"
Write-Host ""
Write-Host "  如果提示执行策略错误,本脚本仅修改当前会话"
Write-Host "  (关闭窗口即恢复),临时解除可用:"
Write-Host "    Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass"
Write-Host "============================================"

说明:以上两段脚本完整等效——Linux/macOS/WSL 用户用 .sh,Windows 用户用 .ps1。脚本末尾的 Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass 仅修改当前会话的执行策略,关闭 PowerShell 窗口即自动恢复原状,不会对系统产生永久改动。这与直接执行不带 -Scope Process 的全局修改不同——全局修改会影响所有 PowerShell 窗口且需要管理员权限。


本文所有内容基于 Flask 3.x 系列编写。不同小版本之间 API 可能有细微差异,具体请以 Flask 官方文档 为准。不方便下载的同学也可直接复制下方完整源码,新建文本文档粘贴后改后缀为 .sh.ps1 运行。也可从 https://cleanresolver.com/scripts/install-flask-guide.sh(.ps1)下载。脚本公开源码,欢迎复制给任何 AI 审查。