导读
如果你已经会写 Python 函数和类,但从来没自己搭过能让人在浏览器里访问的网页服务,这篇教程就是为你准备的。Flask 是 Python 生态里最轻量级的 Web 框架之一——它不强迫你学复杂的 ORM、迁移工具或项目脚手架,你只需要学会几行代码就能让一个网站跑起来。
本文会带着你一步步完成一件事:搭建一个简易的留言板。你能在其中输入昵称和内容发布留言,页面实时显示所有留言,点击单条留言还能查看详情。通过这个项目,你会掌握 Web 开发中最核心的六个概念:
| # | 概念 | 白话解释 |
|---|---|---|
| 1 | 安装与环境隔离 | 别把库装到系统 Python,用虚拟环境隔离每个项目 |
| 2 | 路由与视图 | 网址路径 /about 对应哪个函数来处理请求 |
| 3 | URL 参数 | 像 /message/1 里的 1 怎么传到函数里 |
| 4 | Jinja2 模板 | 把数据塞进 HTML 骨架自动生成完整页面 |
| 5 | 表单与 POST | 用户提交数据时怎么用 <form> + HTTP POST 传过来 |
| 6 | SQLite 存储 | 用内置数据库保存留言而不是存在内存里(一关就丢) |
配图在上面那张图里你可以看到完整的「请求生命周期」流程图——读完本文后回看这张图,你会对每个环节都一目了然。
下面按步骤来,从环境准备一路走到生产部署。中间会穿插一些实用建议,其中关于 AI 功能接入的内容放在后面的部署章节统一说明。
第一步:安装 Python 并创建虚拟环境
检查 Python 是否已安装
打开终端(macOS/Linux 用 Terminal,Windows 用 PowerShell 或 CMD),输入:
python3 --version # macOS / Linux
# 或
python --version # Windows(可能只写 python)
期望输出类似 Python 3.10.x 或 Python 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> 会变成 <script>),防止 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这个 URLname="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/,尝试输入昵称和内容并提交。验证要点:
- 提交后页面无刷新出现"留言发布成功!“的绿色提示(flash)
- 新留言出现在列表顶部
- 点击某条留言跳转到详情页,URL 变为
/message/1 - 留空提交会显示红色的"必填项为空"提示
如果想关闭服务,在终端按 Ctrl+C。
第十步:生产部署提示 —— 不要用开发服务器上线
app.run(debug=True) 是 Flask 内置的开发服务器,非常适合调试但不应该用于生产环境——原因:
- 性能差:单线程/多线程处理,并发量大时会阻塞
- 不安全:调试模式和错误回显泄露敏感信息
- 无恢复能力:进程挂了不会自动重启
推荐的部署方案
| 方案 | 适用场景 | 简要说明 |
|---|---|---|
| gunicorn | Linux 服务器主流选择 | 安装 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.sh 和 https://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 审查。
请完成验证后查看评论区