欢迎光临
我们一直在努力

Flask 完整入门教程

Flask 完整入门教程

本教程基于 Flask 3.x + Flask-SQLAlchemy 3.x + Python 3.10+ 编写。如使用更早版本,部分 API(如 db.session.get、Query.get)行为可能不同。

每一节都有完整可运行的代码,照着敲一遍就能掌握。


目录

  • 1. Flask 是什么
  • 2. 环境搭建
  • 3. 第一个 Flask 程序
  • 4. 路由详解
  • 5. HTTP 请求方法
  • 6. request 对象:获取请求数据
  • 7. 返回响应
  • 8. 模板渲染 Jinja2
  • 9. 模板继承
  • 10. 静态文件
  • 11. url_for 与重定向
  • 12. Cookie 与 Session
  • 13. 表单处理 Flask-WTF
  • 14. 数据库 Flask-SQLAlchemy
  • 15. 蓝图 Blueprint
  • 16. 错误处理
  • 17. 请求钩子
  • 18. 实战项目:待办事项应用
  • 19. 项目部署
  • 20. 学习路线与常见问题

1. Flask 是什么

Flask 是 Python 的一个轻量级 Web 框架。

什么是 Web 框架? 简单说,你想做一个网站,需要处理"用户访问网址 → 返回网页内容"这件事。框架帮你把底层网络通信、路由匹配这些繁琐的事都做好了,你只需要写业务逻辑。

Flask 的特点:

特点说明
轻量 核心代码很少,只做最基本的事
灵活 想用什么组件自己选(数据库、表单库等)
易学 几行代码就能跑起一个网站
生态好 有大量官方扩展(Flask-SQLAlchemy、Flask-Login 等)

Flask vs Django:

  • Django:大而全,自带后台管理、ORM、模板等,适合大型项目。
  • Flask:小而美,自由组合,适合中小项目和快速入门。

⬆ 返回目录


2. 环境搭建

2.1 确认 Python 已安装

打开命令行(Windows 是 CMD/PowerShell,Mac/Linux 是终端),输入:

python –version

如果显示 Python 3.10.x 以上就 OK。如果没有,去 python.org 下载安装,安装时勾选 Add Python to PATH。

2.2 创建虚拟环境(强烈推荐)

虚拟环境可以让你每个项目的依赖互不干扰。

# 1. 创建一个项目文件夹
mkdir myflask
cd myflask

# 2. 创建虚拟环境
python -m venv venv

# 3. 激活虚拟环境
# Windows:
venv\\Scripts\\activate
# Mac / Linux:
source venv/bin/activate

激活成功后,命令行前面会出现 (venv) 字样。

⚠️ 每次重新打开命令行,都要重新激活虚拟环境。

2.3 安装 Flask

pip install flask

验证:

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

能看到版本号(应为 3.x)就成功了。

⬆ 返回目录


3. 第一个 Flask 程序

新建文件 app.py:

from flask import Flask

# 创建 Flask 应用实例
# __name__ 告诉 Flask 当前模块的名字,Flask 靠它找到模板和静态文件的位置
app = Flask(__name__)

# 装饰器:把 URL "/" 和下面的函数绑定起来
@app.route('/')
def index():
return '你好,Flask!这是我的第一个网站'

# 只有当直接运行这个文件时才启动服务器
if __name__ == '__main__':
app.run(debug=True)

运行:

python app.py

你会看到输出:

* Running on http://127.0.0.1:5000

打开浏览器访问 http://127.0.0.1:5000,就能看到「你好,Flask!」。

关键概念解释

① app = Flask(__name__) 创建一个 Flask 应用对象,相当于"建了一个网站"。

② @app.route('/') 这叫装饰器,作用是:当用户访问 / 这个网址时,就执行下面的函数。这个函数叫做视图函数(view function)。

③ return '…' 视图函数必须返回内容,Flask 会把它作为 HTTP 响应发给浏览器。

④ debug=True 调试模式,好处:

  • 改代码后自动重启服务器,不用手动重启
  • 出错时浏览器显示详细错误信息

⚠️ 上线时一定要关掉 debug,否则有安全风险。

⬆ 返回目录


4. 路由详解

路由就是"网址"和"处理函数"之间的对应关系。

4.1 基本路由

@app.route('/')
def index():
return '首页'

@app.route('/about')
def about():
return '关于我们'

@app.route('/contact')
def contact():
return '联系方式'

访问 /about 就执行 about() 函数。

💡 提示:Python 里函数名不能重复。但同一个函数可以绑定多个路由(见下方示例);反过来,多个不同的函数也可以绑定不同的 URL。

@app.route('/')
@app.route('/home')
@app.route('/index')
def home():
return '多个网址都能访问到我'

4.2 动态路由(URL 变量)

有时候网址里带有参数,比如用户 ID、文章标题。

@app.route('/user/<username>')
def show_user(username):
return f'你好,{username}!'

  • 访问 /user/zhangsan → 显示「你好,zhangsan!」
  • 访问 /user/lisi → 显示「你好,lisi!」

4.3 路由转换器(限定参数类型)

默认参数是字符串。可以指定类型:

转换器说明例子
string 字符串(默认),不能含 / <name>
int 整数 <int:id>
float 浮点数 <float:price>
path 字符串,可以含 / <path:filepath>
uuid UUID 字符串 <uuid:uid>

@app.route('/post/<int:post_id>')
def show_post(post_id):
# post_id 已经是 int 类型了,可以直接运算
return f'这是第 {post_id} 篇文章,下一篇是第 {post_id + 1} 篇'

访问 /post/100 ✅ 正常 访问 /post/abc ❌ 404 错误(因为 abc 不是整数)

4.4 路由的斜杠规则

@app.route('/test/') # 结尾有斜杠
def test1():
return 'test1'

  • 访问 /test/ ✅
  • 访问 /test ✅(Flask 会自动重定向到 /test/)

@app.route('/test2') # 结尾没斜杠
def test2():
return 'test2'

  • 访问 /test2 ✅
  • 访问 /test2/ ❌ 404

💡 建议:统一风格,要么都带斜杠,要么都不带。

⬆ 返回目录


5. HTTP 请求方法

浏览器访问网页时,会使用不同的"方法":

方法用途说明
GET 获取数据 最常见,参数在 URL 里
POST 提交数据 表单提交、上传文件
PUT 更新数据 一般用于 API
DELETE 删除数据 一般用于 API

默认情况下,@app.route() 只响应 GET 请求。

from flask import request

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
return '你提交了表单(POST)'
else:
return '你正在查看登录页(GET)'

⚠️ 如果只写 methods=['POST'],那么用浏览器直接访问(GET)就会返回 405 错误。 ⚠️ 修改数据的操作(新增、更新、删除)都应该用 POST/PUT/DELETE,不要用 GET,否则会被搜索引擎爬虫、浏览器预取误触发。

⬆ 返回目录


6. request 对象:获取请求数据

request 是 Flask 提供的一个全局对象,包含了本次请求的所有信息。使用前要导入:

from flask import request

6.1 获取 URL 查询参数(GET 参数)

网址:/search?keyword=python&page=2

@app.route('/search')
def search():
keyword = request.args.get('keyword') # 'python'
page = request.args.get('page', '1') # '2',默认值 '1'
page_int = request.args.get('page', 1, type=int) # 转成 int:2

return f'搜索关键词:{keyword},第 {page} 页'

要点:

  • request.args.get('key') 获取参数
  • 第二个参数是默认值,找不到时返回它
  • type=int 可以自动转换类型

6.2 获取表单数据(POST 参数)

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
return f'注册成功!用户名:{username}'

# GET 请求时返回一个简单的 HTML 表单
return '''
<form method="post">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<button type="submit">注册</button>
</form>
'''

request.args 和 request.form 的区别:

request.argsrequest.form
数据来源 URL 问号后面的参数 POST 请求体
方法 GET POST
例子 /search?q=abc 表单提交

6.3 获取 JSON 数据(用于 API)

@app.route('/api/user', methods=['POST'])
def create_user():
data = request.get_json() # 解析 JSON 请求体
name = data.get('name')
age = data.get('age')
return f'收到:{name}, {age} 岁'

用 curl 测试:

curl -X POST http://127.0.0.1:5000/api/user \\
-H "Content-Type: application/json" \\
-d '{"name":"小明","age":18}'

6.4 上传文件

import os
from werkzeug.utils import secure_filename

@app.route('/upload', methods=['GET', 'POST'])
def upload():
if request.method == 'POST':
file = request.files.get('myfile')
if file and file.filename:
# secure_filename 防止恶意文件名(如 ../../etc/passwd)
filename = secure_filename(file.filename)
os.makedirs('uploads', exist_ok=True) # 确保目录存在
file.save(os.path.join('uploads', filename))
return f'上传成功:{filename}'
return '''
<form method="post" enctype="multipart/form-data">
<input type="file" name="myfile">
<button type="submit">上传</button>
</form>
'''

⚠️ 表单必须加 enctype="multipart/form-data",否则文件传不上去。

6.5 其他常用属性

@app.route('/info')
def info():
return {
'method': request.method, # 请求方法
'url': request.url, # 完整 URL
'path': request.path, # 路径部分
'host': request.host, # 主机名
'headers': dict(request.headers), # 请求头
'ip': request.remote_addr, # 客户端 IP
}

💡 返回字典时,Flask 会自动把它转成 JSON。

⬆ 返回目录


7. 返回响应

7.1 返回字符串 / HTML

@app.route('/text')
def text():
return '纯文本'

@app.route('/html')
def html():
return '<h1>标题</h1><p>段落</p>'

7.2 返回 JSON

from flask import jsonify

@app.route('/api/data')
def api_data():
return jsonify({
'code': 200,
'message': 'success',
'data': [1, 2, 3]
})

💡 直接 return {'a': 1} 也可以,Flask 会自动调用 jsonify。

7.3 自定义状态码和响应头

from flask import make_response

@app.route('/custom')
def custom():
# 方式一:返回元组 (内容, 状态码, 响应头)
return '创建成功', 201, {'X-My-Header': 'hello'}

# 方式二:使用 make_response
# resp = make_response('创建成功', 201)
# resp.headers['X-My-Header'] = 'hello'
# return resp

常用状态码:

状态码含义
200 成功
201 创建成功
301/302 重定向
400 请求参数错误
401 未登录
403 无权限
404 资源不存在
500 服务器内部错误

⬆ 返回目录


8. 模板渲染 Jinja2

前面我们把 HTML 直接写在 Python 字符串里,很不优雅。Flask 使用 Jinja2 模板引擎 来解决这个问题。

8.1 目录结构

Flask 默认约定:

项目文件夹/
├── app.py
└── templates/ ← 模板文件夹(必须叫这个名字)
└── index.html

8.2 第一个模板

templates/index.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
</head>
<body>
<h1>你好,{{ name }}!</h1>
<p>欢迎来到我的网站</p>
</body>
</html>

app.py:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
# 把变量传给模板
return render_template('index.html', title='首页', name='小明')

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

要点:

  • render_template('文件名', 变量=值, …) 渲染模板
  • 模板里用 {{ 变量名 }} 显示变量

8.3 Jinja2 常用语法

① 变量

<p>{{ name }}</p>
<p>{{ user.age }}</p> <!– 访问属性 –>
<p>{{ user['name'] }}</p> <!– 访问字典 –>
<p>{{ items[0] }}</p> <!– 访问列表 –>

② 判断

{% if score >= 90 %}
<p>优秀</p>
{% elif score >= 60 %}
<p>及格</p>
{% else %}
<p>不及格</p>
{% endif %}

③ 循环

<ul>
{% for item in items %}
<li>{{ loop.index }}. {{ item }}</li> <!– loop.index 从 1 开始 –>
{% endfor %}
</ul>

循环中可用的变量:

  • loop.index:当前序号(从 1 开始)
  • loop.index0:当前序号(从 0 开始)
  • loop.first:是否第一个
  • loop.last:是否最后一个
  • loop.length:总长度
④ 过滤器

用 | 调用过滤器,用来格式化变量:

<p>{{ name|upper }}</p> <!– 转大写 –>
<p>{{ name|lower }}</p> <!– 转小写 –>
<p>{{ name|length }}</p> <!– 长度 –>
<p>{{ price|round(2) }}</p> <!– 保留 2 位小数 –>
<p>{{ text|default('暂无') }}</p> <!– 默认值 –>
<p>{{ html_content|safe }}</p> <!– 不转义 HTML(谨慎使用) –>
<p>{{ content|truncate(50) }}</p> <!– 截断 –>
<p>{{ nums|sum }}</p> <!– 求和 –>
<p>{{ nums|max }}</p> <!– 最大值 –>

⚠️ 默认情况下 Jinja2 会转义 HTML,防止 XSS 攻击。如果确实需要渲染 HTML,用 |safe,但要确保内容可信。

⑤ 注释

{# 这是注释,不会出现在最终 HTML 里 #}

8.4 完整示例

app.py:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
user = {'name': '小明', 'age': 18}
scores = [95, 88, 72, 60, 45]
return render_template('index.html', user=user, scores=scores)

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

templates/index.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>成绩单</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
.pass { color: green; }
.fail { color: red; }
</style>
</head>
<body>
<h1>{{ user.name }} 的成绩单</h1>
<p>年龄:{{ user.age }}</p>

<ul>
{% for score in scores %}
<li class="{{ 'pass' if score >= 60 else 'fail' }}">
{{ loop.index }}. {{ score }} 分
{% if score >= 90 %}(优秀){% endif %}
</li>
{% endfor %}
</ul>

<p>总分:{{ scores|sum }}</p>
<p>最高分:{{ scores|max }}</p>
<p>平均分:{{ ((scores|sum) / (scores|length))|round(1) }}</p>
</body>
</html>

💡 注意最后一行:Jinja2 中过滤器的优先级高于算术运算,所以 scores|sum / scores|length 的括号一定要写清楚,避免歧义。

⬆ 返回目录


9. 模板继承

多个页面往往有相同的头部、导航、底部。Jinja2 的模板继承可以避免重复代码。

9.1 创建基础模板

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') }}">
{% block head %}{% endblock %}
</head>
<body>
<nav>
<a href="{{ url_for('index') }}">首页</a>
<a href="{{ url_for('about') }}">关于</a>
</nav>

<main>
{% block content %}
<!– 子模板会覆盖这里的内容 –>
{% endblock %}
</main>

<footer>
<p>© 2025 我的网站</p>
</footer>

{% block script %}{% endblock %}
</body>
</html>

9.2 子模板继承

templates/index.html:

{% extends 'base.html' %}

{% block title %}首页 – {{ super() }}{% endblock %}

{% block content %}
<h1>欢迎来到首页</h1>
<p>这里是首页内容</p>
{% endblock %}

templates/about.html:

{% extends 'base.html' %}

{% block title %}关于我们{% endblock %}

{% block content %}
<h1>关于我们</h1>
<p>我们是一个专注 Flask 教程的团队</p>
{% endblock %}

要点:

  • {% extends 'base.html' %} 必须放在第一行
  • {% block 名字 %} 定义可替换的区域
  • {{ super() }} 表示保留父模板中该块的内容

⬆ 返回目录


10. 静态文件

CSS、JavaScript、图片等不需要动态渲染的文件叫静态文件。

Flask 默认约定放在 static 文件夹:

项目文件夹/
├── app.py
├── static/
│ ├── style.css
│ ├── main.js
│ └── images/
│ └── logo.png
└── templates/
└── index.html

10.1 在模板中引用

<!– CSS –>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">

<!– JS –>
<script src="{{ url_for('static', filename='main.js') }}"></script>

<!– 图片 –>
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="logo">

💡 用 url_for('static', filename='…') 而不是直接写 /static/…,这样路径更灵活可靠。

static/style.css 示例:

body {
font-family: -apple-system, "Microsoft YaHei", sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}

nav {
background: #333;
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
}

nav a {
color: #fff;
text-decoration: none;
margin-right: 16px;
}

nav a:hover {
text-decoration: underline;
}

⬆ 返回目录


11. url_for 与重定向

11.1 url_for:根据函数名生成 URL

from flask import url_for

@app.route('/')
def index():
return '首页'

@app.route('/user/<int:user_id>')
def user_profile(user_id):
return f'用户 {user_id}'

在视图函数里使用(最常见,直接用即可):

@app.route('/demo')
def demo():
home_url = url_for('index') # /
user_url = url_for('user_profile', user_id=5) # /user/5
return f'首页:{home_url},用户页:{user_url}'

💡 只有脱离请求上下文的场景(比如单元测试)才需要用 app.test_request_context() 包裹。日常写视图函数时,url_for 可以直接用。

在模板里使用:

<a href="{{ url_for('index') }}">首页</a>
<a href="{{ url_for('user_profile', user_id=5) }}">用户5</a>

为什么要用 url_for?

  • 如果以后改了路由地址,用 url_for 的地方会自动跟着变
  • 自动处理特殊字符转义
  • 处理静态文件路径

11.2 重定向 redirect

from flask import redirect, url_for

@app.route('/old')
def old_page():
return redirect(url_for('index')) # 重定向到首页

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# 处理登录逻辑…
return redirect(url_for('dashboard'))
return render_template('login.html')

@app.route('/dashboard')
def dashboard():
return '这是仪表盘页面'

💡 Post/Redirect/Get 模式:表单提交成功后应该重定向,而不是直接返回页面。这样用户刷新页面时不会重复提交表单。

11.3 abort:主动中断请求

from flask import abort

@app.route('/admin')
def admin():
if not is_admin():
abort(403) # 返回 403 无权限
return '管理员页面'

@app.route('/post/<int:post_id>')
def post(post_id):
post = find_post(post_id)
if post is None:
abort(404) # 返回 404 不存在
return render_template('post.html', post=post)

⬆ 返回目录


12. Cookie 与 Session

HTTP 是无状态协议,服务器默认记不住你是谁。Cookie 和 Session 用来解决这个问题。

CookieSession
存储位置 浏览器 服务器(Flask 默认是加密后存在 Cookie 里)
安全性 低(用户可见可改) 较高
容量 小(约 4KB)
用途 记住偏好设置 登录状态

12.1 使用 Session

必须先设置密钥:

from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'a-very-secret-key-please-change-it'

⚠️ secret_key 用来加密 session 数据,一定要随机且保密。生产环境应该从环境变量读取。

常用操作:

from flask import session, redirect, url_for, request, render_template

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
# 实际项目中应该验证密码
if username == 'admin':
session['username'] = username # 写入 session
session['role'] = 'admin'
return redirect(url_for('profile'))
return '用户名错误'
return render_template('login.html')

@app.route('/profile')
def profile():
# 读取 session
if 'username' not in session:
return redirect(url_for('login'))
return f'欢迎回来,{session["username"]}!'

@app.route('/logout')
def logout():
session.clear() # 清空所有 session
# 或删除单个:session.pop('username', None)
return redirect(url_for('login'))

Session 常用方法:

session['key'] = 'value' # 设置
value = session.get('key') # 获取(推荐,不会报 KeyError)
session['key'] # 获取(不存在会报错)
session.pop('key', None) # 删除
session.clear() # 清空
'key' in session # 判断是否存在
session.permanent = True # 设为持久化

12.2 设置 Session 有效期

要让下面的配置生效,必须同时把 session.permanent 设为 True:

from datetime import timedelta

app.permanent_session_lifetime = timedelta(days=7) # 7 天

@app.route('/login')
def login():
session.permanent = True # ⚠️ 必须设置,否则上面的有效期配置不生效
session['username'] = 'admin'
...

💡 如果 session.permanent 为 False(默认),session 会在浏览器关闭时过期,app.permanent_session_lifetime 不起作用。

12.3 使用 Cookie(简单场景)

from flask import make_response, request

@app.route('/set_cookie')
def set_cookie():
resp = make_response('Cookie 已设置')
resp.set_cookie('theme', 'dark', max_age=60*60*24*30) # 30 天
return resp

@app.route('/get_cookie')
def get_cookie():
theme = request.cookies.get('theme', 'light')
return f'当前主题:{theme}'

💡 建议:登录状态用 Session,用户偏好(主题、语言)用 Cookie。

⬆ 返回目录


13. 表单处理 Flask-WTF

原生表单处理比较麻烦(要手动验证、防 CSRF)。Flask-WTF 让这一切变得简单。

13.1 安装

pip install flask-wtf

13.2 定义表单类

from flask_wtf import FlaskForm
from wtforms import (
StringField, PasswordField, TextAreaField, SelectField,
SubmitField, IntegerField,
)
from wtforms.validators import DataRequired, Length, Email, EqualTo, NumberRange

class RegisterForm(FlaskForm):
username = StringField('用户名', validators=[
DataRequired(message='用户名不能为空'),
Length(min=3, max=20, message='用户名长度需在 3-20 之间')
])
email = StringField('邮箱', validators=[
DataRequired(message='邮箱不能为空'),
Email(message='邮箱格式不正确')
])
age = IntegerField('年龄', validators=[
NumberRange(min=1, max=120, message='年龄需在 1-120 之间')
])
gender = SelectField('性别', choices=[
('male', '男'),
('female', '女'),
('other', '其他')
])
bio = TextAreaField('个人简介', validators=[
Length(max=500, message='简介不能超过 500 字')
])
password = PasswordField('密码', validators=[
DataRequired(message='密码不能为空'),
Length(min=6, message='密码至少 6 位')
])
password2 = PasswordField('确认密码', validators=[
DataRequired(),
EqualTo('password', message='两次密码不一致')
])
submit = SubmitField('注册')

常用验证器:

验证器说明
DataRequired() 不能为空
Length(min, max) 长度范围
Email() 邮箱格式
EqualTo('field') 与另一个字段相同
NumberRange(min, max) 数值范围
Regexp(pattern) 正则匹配
URL() URL 格式

13.3 在视图里使用

from flask import Flask, render_template, redirect, url_for, flash

app = Flask(__name__)
app.secret_key = 'your-secret-key'

@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm()

# validate_on_submit() = 是 POST 请求 且 验证通过
if form.validate_on_submit():
username = form.username.data
email = form.email.data
age = form.age.data
# 这里应该保存到数据库
flash(f'注册成功!欢迎 {username}', 'success')
return redirect(url_for('register'))

# 验证失败时,form.errors 里会有错误信息
return render_template('register.html', form=form)

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

13.4 模板

templates/register.html:

{% extends 'base.html' %}

{% block content %}
<h1>用户注册</h1>

<!– flash 消息显示。category 就是 flash() 的第二个参数,直接拼到 class 上 –>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}

<form method="post">
<!– CSRF 令牌,必须加,否则提交会失败 –>
{{ form.hidden_tag() }}

<p>
{{ form.username.label }}<br>
{{ form.username(size=30) }}
{% for error in form.username.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</p>

<p>
{{ form.email.label }}<br>
{{ form.email(size=30) }}
{% for error in form.email.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</p>

<p>
{{ form.age.label }}<br>
{{ form.age() }}
{% for error in form.age.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</p>

<p>
{{ form.gender.label }}<br>
{{ form.gender() }}
</p>

<p>
{{ form.bio.label }}<br>
{{ form.bio(rows=4, cols=40) }}
{% for error in form.bio.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</p>

<p>
{{ form.password.label }}<br>
{{ form.password() }}
{% for error in form.password.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</p>

<p>
{{ form.password2.label }}<br>
{{ form.password2() }}
{% for error in form.password2.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</p>

<p>{{ form.submit() }}</p>
</form>
{% endblock %}

13.5 flash 消息

flash() 用来显示一次性提示消息(如"登录成功"):

flash('操作成功', 'success') # 第二个参数是分类
flash('操作失败', 'error')

模板里读取:

{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endwith %}

💡 flash 依赖 session,所以必须设置 secret_key。

⬆ 返回目录


14. 数据库 Flask-SQLAlchemy

14.1 安装

pip install flask-sqlalchemy

14.2 基本配置

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)

# SQLite 数据库文件会保存在当前目录下
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'app.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

常见数据库连接字符串:

# SQLite(最简单,适合开发)
'sqlite:///app.db'

# MySQL(需要 pip install pymysql)
'mysql+pymysql://用户名:密码@localhost:3306/数据库名'

# PostgreSQL(需要 pip install psycopg2-binary)
'postgresql://用户名:密码@localhost:5432/数据库名'

14.3 定义模型

模型就是 Python 类,对应数据库中的表。

from datetime import datetime, timezone

class User(db.Model):
__tablename__ = 'users' # 表名,不写默认是类名小写

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(200), nullable=False)
age = db.Column(db.Integer, default=0)
is_active = db.Column(db.Boolean, default=True)
# ⚠️ Python 3.12+ 弃用了 datetime.utcnow(),这里用带时区的写法
created_at = db.Column(
db.DateTime,
default=lambda: datetime.now(timezone.utc)
)

# 一对多关系:一个用户有多篇文章
# lazy 参数应为字符串('select'/'joined'/'dynamic'),默认就是 'select'
posts = db.relationship('Post', backref='author', lazy='select')

def __repr__(self):
return f'<User {self.username}>'

class Post(db.Model):
__tablename__ = 'posts'

id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text)
created_at = db.Column(
db.DateTime,
default=lambda: datetime.now(timezone.utc)
)

# 外键,指向 users 表的 id
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

def __repr__(self):
return f'<Post {self.title}>'

常用字段类型:

类型说明
Integer 整数
String(n) 字符串,最大长度 n
Text 长文本
Boolean 布尔值
Float 浮点数
DateTime 日期时间
Date 日期

常用选项:

选项说明
primary_key=True 主键
autoincrement=True 自增
unique=True 值唯一
nullable=False 不能为空
default=值 默认值
index=True 创建索引

14.4 创建数据库表

if __name__ == '__main__':
with app.app_context():
db.create_all() # 创建所有表(已存在则跳过)
app.run(debug=True)

⚠️ Flask 2.3+ 中 db.create_all() 必须在应用上下文里执行。 create_all() 只会新建表,不会修改已有表结构。改字段要删掉 app.db 重建,或用 Flask-Migrate 做迁移。 💡 一旦项目启用 Flask-Migrate,就不要再使用 db.create_all(),否则迁移系统会因为"表已存在"而混乱。

14.5 增删改查(CRUD)

💡 统一使用 db.session.get(Model, pk)。Query.get() 在 Flask-SQLAlchemy 3.x 中已弃用,会触发 LegacyAPIWarning。

新增

@app.route('/add_user', methods=['POST']) # 修改数据应该用 POST
def add_user():
user = User(username='小明', email='xm@example.com', password='123456', age=18)
db.session.add(user) # 添加到会话
db.session.commit() # 提交到数据库
return f'用户已添加,ID = {user.id}'

批量添加:

users = [
User(username='张三', email='zs@example.com', password='123'),
User(username='李四', email='ls@example.com', password='123'),
]
db.session.add_all(users)
db.session.commit()

查询

# 1. 查询所有
users = User.query.all()

# 2. 按主键查询(推荐新写法)
user = db.session.get(User, 1)

# 3. 查询第一条
user = User.query.first()

# 4. 条件查询
user = User.query.filter_by(username='小明').first()
users = User.query.filter(User.age > 18).all()
users = User.query.filter(User.age >= 18, User.is_active == True).all()

# 5. 模糊查询
users = User.query.filter(User.username.like('%小%')).all()
users = User.query.filter(User.username.contains('小')).all()

# 6. 排序
users = User.query.order_by(User.age.desc()).all() # 降序
users = User.query.order_by(User.age.asc()).all() # 升序

# 7. 分页
page = User.query.paginate(page=1, per_page=10, error_out=False)
# page.items 当前页数据
# page.total 总数
# page.pages 总页数
# page.has_next() / page.has_prev()

# 8. 计数
count = User.query.count()

# 9. 复杂条件(or_ / and_ / in_)
from sqlalchemy import or_, and_, not_

users = User.query.filter(or_(User.age < 18, User.age > 60)).all()
users = User.query.filter(User.id.in_([1, 2, 3])).all()

更新

@app.route('/update_user/<int:user_id>', methods=['POST'])
def update_user(user_id):
user = db.session.get(User, user_id)
if not user:
return '用户不存在', 404
user.age = 20
user.email = 'new@example.com'
db.session.commit() # 提交修改
return '更新成功'

删除

@app.route('/delete_user/<int:user_id>', methods=['POST'])
def delete_user(user_id):
user = db.session.get(User, user_id)
if not user:
return '用户不存在', 404
db.session.delete(user)
db.session.commit()
return '删除成功'

14.6 关系操作

# 一对多:给用户添加文章
user = db.session.get(User, 1)
post = Post(title='我的第一篇文章', content='内容…', author=user)
db.session.add(post)
db.session.commit()

# 或者
post = Post(title='第二篇', content='…', user_id=user.id)
db.session.add(post)
db.session.commit()

# 查询用户的文章
user = db.session.get(User, 1)
for post in user.posts:
print(post.title)

# 查询文章的作者
post = db.session.get(Post, 1)
print(post.author.username)

14.7 事务与错误处理

@app.route('/transfer', methods=['POST'])
def transfer():
try:
# 一系列数据库操作
user1 = db.session.get(User, 1)
user1.age += 1
db.session.commit()
except Exception as e:
db.session.rollback() # 出错回滚
return f'操作失败:{e}', 500
return '操作成功'

14.8 Flask-Migrate(数据库迁移)

开发中表结构会变化,create_all() 无法修改已有表,这时用 Flask-Migrate:

pip install flask-migrate

from flask_migrate import Migrate

db = SQLAlchemy(app)
migrate = Migrate(app, db)

初始化(只需一次):

flask db init

每次修改模型后:

flask db migrate -m "添加了 xxx 字段"
flask db upgrade

⬆ 返回目录


15. 蓝图 Blueprint

当项目变大时,把所有路由写在一个文件里会很难维护。蓝图可以把路由分组到不同文件。

15.1 项目结构

myproject/
├── app.py
├── models.py
├── config.py
├── extensions.py
├── templates/
│ ├── base.html
│ ├── index.html
│ └── blog/
│ └── list.html
├── static/
└── blueprints/
├── __init__.py
├── main.py
├── auth.py
└── blog.py

15.2 创建蓝图

blueprints/main.py:

from flask import Blueprint, render_template

main_bp = Blueprint('main', __name__)

@main_bp.route('/')
def index():
return render_template('index.html')

@main_bp.route('/about')
def about():
return '关于我们'

blueprints/auth.py:

from flask import Blueprint, render_template, request, redirect, url_for, session

auth_bp = Blueprint('auth', __name__, url_prefix='/auth')

@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form.get('username')
return redirect(url_for('main.index'))
return render_template('auth/login.html')

@auth_bp.route('/logout')
def logout():
session.clear()
return redirect(url_for('main.index'))

参数说明:

  • 第一个参数 'auth':蓝图的名字,用于 url_for('auth.login')
  • url_prefix='/auth':该蓝图下所有路由都加前缀
  • template_folder:可指定模板目录(可选)

15.3 注册蓝图

app.py:

from flask import Flask
from blueprints.main import main_bp
from blueprints.auth import auth_bp
from blueprints.blog import blog_bp

app = Flask(__name__)
app.secret_key = 'your-secret-key'

app.register_blueprint(main_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(blog_bp)

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

15.4 url_for 的变化

使用蓝图后,url_for 要加蓝图名前缀:

url_for('main.index') # 主蓝图的 index
url_for('auth.login') # auth 蓝图的 login
url_for('blog.post', post_id=1)

模板里也一样:

<a href="{{ url_for('main.index') }}">首页</a>
<a href="{{ url_for('auth.login') }}">登录</a>

15.5 使用工厂模式(推荐)

大型项目常用应用工厂模式,便于测试和配置切换。

extensions.py:

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()

app.py:

from flask import Flask
from extensions import db, migrate

def create_app(config_name='development'):
app = Flask(__name__)

# 配置
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# 初始化扩展
db.init_app(app)
migrate.init_app(app, db)

# 注册蓝图
from blueprints.main import main_bp
from blueprints.auth import auth_bp
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp)

return app

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

⬆ 返回目录


16. 错误处理

16.1 自定义错误页面

from werkzeug.exceptions import HTTPException

@app.errorhandler(404)
def page_not_found(error):
return render_template('errors/404.html'), 404

@app.errorhandler(500)
def internal_error(error):
db.session.rollback() # 出错时回滚数据库
return render_template('errors/500.html'), 500

@app.errorhandler(403)
def forbidden(error):
return render_template('errors/403.html'), 403

templates/errors/404.html:

{% extends 'base.html' %}

{% block content %}
<div style="text-align: center; padding: 60px 0;">
<h1 style="font-size: 80px; margin: 0;">404</h1>
<p>页面走丢了~</p>
<a href="{{ url_for('main.index') }}">返回首页</a>
</div>
{% endblock %}

16.2 捕获所有异常

from werkzeug.exceptions import HTTPException

@app.errorhandler(Exception)
def handle_exception(e):
# 如果是 HTTP 异常,交给对应的处理器
if isinstance(e, HTTPException):
return e
# 记录日志
app.logger.error(f'未处理的异常:{e}')
return render_template('errors/500.html'), 500

⚠️ HTTPException 必须从 werkzeug.exceptions 导入,否则会报 NameError。

16.3 使用日志

import logging
from logging.handlers import RotatingFileHandler

if not app.debug:
handler = RotatingFileHandler('app.log', maxBytes=1024*1024, backupCount=10)
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
handler.setFormatter(formatter)
app.logger.addHandler(handler)

# 使用
app.logger.info('用户登录成功')
app.logger.error('数据库连接失败')

⬆ 返回目录


17. 请求钩子

钩子函数可以在请求的不同阶段自动执行。

@app.before_request
def before_request():
"""每个请求处理之前执行"""
# 常用于:记录日志、检查登录状态、打开数据库连接
print(f'收到请求:{request.method} {request.path}')

@app.after_request
def after_request(response):
"""每个请求处理之后执行(必须返回 response)"""
# 常用于:添加响应头、压缩响应
response.headers['X-Powered-By'] = 'Flask Tutorial'
return response

@app.teardown_request
def teardown_request(exception):
"""请求结束后执行,无论是否出错"""
# 常用于:关闭连接、清理资源
pass

⚠️ Flask 2.3+ 移除了 before_first_request。替代方案:在 create_app() 里直接执行初始化代码。

实用例子:全局登录检查

@app.before_request
def require_login():
# 不存在的路由交给 Flask 的 404 处理,不要在这里拦
if request.endpoint is None:
return

# 白名单:不需要登录的 endpoint
allowed = {'auth.login', 'auth.register', 'main.index', 'static'}
if request.endpoint in allowed:
return

if 'username' not in session:
# 带上 next 参数,登录后可以跳回原页面
return redirect(url_for('auth.login', next=request.path))

登录视图里配合使用:

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# … 验证逻辑 …
session['username'] = 'admin'
next_page = request.args.get('next') or url_for('main.index')
return redirect(next_page)
return render_template('login.html')

⬆ 返回目录


18. 实战项目:待办事项应用

我们把学到的知识综合起来,做一个完整的待办事项(Todo)应用。

18.1 功能

  • 查看所有待办事项
  • 添加新的待办
  • 标记完成/未完成
  • 删除待办
  • 数据保存在 SQLite 数据库

18.2 项目结构

todo_app/
├── app.py
├── templates/
│ ├── base.html
│ ├── index.html
│ └── errors/
│ └── 404.html
└── static/
└── style.css

18.3 完整代码

app.py:

import os
from datetime import datetime, timezone
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy

# ———- 初始化 ———-
basedir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev-secret-key-change-in-production'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'todo.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# ———- 模型 ———-
class Todo(db.Model):
__tablename__ = 'todos'

id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String(200), nullable=False)
done = db.Column(db.Boolean, default=False)
created_at = db.Column(
db.DateTime,
default=lambda: datetime.now(timezone.utc)
)

def __repr__(self):
return f'<Todo {self.id} {self.content}>'

# ———- 路由 ———-
@app.route('/')
def index():
"""首页:显示所有待办"""
# 未完成的排在前面,然后按创建时间倒序
todos = Todo.query.order_by(Todo.done.asc(), Todo.created_at.desc()).all()
total = len(todos)
done_count = sum(1 for t in todos if t.done)
return render_template('index.html',
todos=todos,
total=total,
done_count=done_count)

@app.route('/add', methods=['POST'])
def add():
"""添加待办"""
content = request.form.get('content', '').strip()
if not content:
flash('内容不能为空', 'error')
elif len(content) > 200:
flash('内容不能超过 200 字', 'error')
else:
todo = Todo(content=content)
db.session.add(todo)
db.session.commit()
flash('添加成功', 'success')
return redirect(url_for('index'))

@app.route('/toggle/<int:todo_id>', methods=['POST'])
def toggle(todo_id):
"""切换完成状态"""
todo = db.session.get(Todo, todo_id)
if not todo:
flash('待办不存在', 'error')
else:
todo.done = not todo.done
db.session.commit()
return redirect(url_for('index'))

@app.route('/delete/<int:todo_id>', methods=['POST'])
def delete(todo_id):
"""删除待办"""
todo = db.session.get(Todo, todo_id)
if not todo:
flash('待办不存在', 'error')
else:
db.session.delete(todo)
db.session.commit()
flash('删除成功', 'success')
return redirect(url_for('index'))

@app.route('/clear_done', methods=['POST'])
def clear_done():
"""清空已完成"""
Todo.query.filter_by(done=True).delete()
db.session.commit()
flash('已清空所有完成的待办', 'success')
return redirect(url_for('index'))

# ———- 错误处理 ———-
@app.errorhandler(404)
def not_found(e):
return render_template('errors/404.html'), 404

# ———- 启动 ———-
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)

templates/base.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}我的待办{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<header>
<h1>📝 我的待办清单</h1>
</header>

{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}

<main>
{% block content %}{% endblock %}
</main>
</div>
</body>
</html>

templates/index.html:

{% extends 'base.html' %}

{% block content %}

<!– 添加表单 –>
<form action="{{ url_for('add') }}" method="post" class="add-form">
<input type="text" name="content" placeholder="今天要做什么?" maxlength="200" autofocus>
<button type="submit">添加</button>
</form>

<!– 统计信息 –>
<div class="stats">
<span>共 {{ total }} 项</span>
<span>已完成 {{ done_count }} 项</span>
{% if total > 0 %}
<span>完成率 {{ (done_count / total * 100)|round(0)|int }}%</span>
{% endif %}
</div>

<!– 待办列表 –>
{% if todos %}
<ul class="todo-list">
{% for todo in todos %}
<li class="todo-item {% if todo.done %}done{% endif %}">
<!– 切换状态改成 POST 表单提交 –>
<form action="{{ url_for('toggle', todo_id=todo.id) }}" method="post" class="inline">
<button type="submit" class="check" title="切换完成状态">
{% if todo.done %}✅{% else %}⬜{% endif %}
</button>
</form>

<span class="content">{{ todo.content }}</span>
<span class="time">{{ todo.created_at.strftime('%m-%d %H:%M') }}</span>

<!– 删除也改成 POST 表单 –>
<form action="{{ url_for('delete', todo_id=todo.id) }}" method="post" class="inline"
onsubmit="return confirm('确定删除吗?');">

<button type="submit" class="delete" title="删除">🗑️</button>
</form>
</li>
{% endfor %}
</ul>

{% if done_count > 0 %}
<div class="actions">
<form action="{{ url_for('clear_done') }}" method="post"
onsubmit="return confirm('确定清空所有已完成的待办吗?');">

<button type="submit" class="link-btn">清空已完成</button>
</form>
</div>
{% endif %}
{% else %}
<div class="empty">
<p>还没有待办事项</p>
<p>在上面的输入框里添加第一条吧!</p>
</div>
{% endif %}

{% endblock %}

templates/errors/404.html:

{% extends 'base.html' %}

{% block content %}
<div class="empty">
<h1 style="font-size: 64px; color: #ddd; margin: 0;">404</h1>
<p>页面走丢了~</p>
<a href="{{ url_for('index') }}">返回首页</a>
</div>
{% endblock %}

static/style.css:

* { box-sizing: border-box; margin: 0; padding: 0; }

body {
font-family: -apple-system, "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}

.container {
max-width: 640px;
margin: 0 auto;
background: #fff;
border-radius: 16px;
padding: 32px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
}

header h1 {
font-size: 24px;
color: #333;
margin-bottom: 24px;
text-align: center;
}

/* 添加表单 */
.add-form {
display: flex;
gap: 8px;
margin-bottom: 20px;
}

.add-form input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}

.add-form input:focus { border-color: #667eea; }

.add-form button {
padding: 12px 24px;
background: #667eea;
color: #fff;
border: none;
border-radius: 10px;
font-size: 15px;
cursor: pointer;
transition: background 0.2s;
}

.add-form button:hover { background: #5568d3; }

/* 统计 */
.stats {
display: flex;
gap: 16px;
font-size: 13px;
color: #888;
padding: 0 4px 16px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 16px;
}

/* 列表 */
.todo-list { list-style: none; }

.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 8px;
border-bottom: 1px solid #f5f5f5;
transition: background 0.15s;
}

.todo-item:hover { background: #fafafa; }

.todo-item .check {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
padding: 0;
}

.todo-item .content { flex: 1; font-size: 15px; color: #333; }

.todo-item.done .content {
text-decoration: line-through;
color: #bbb;
}

.todo-item .time { font-size: 12px; color: #ccc; }

.todo-item .delete {
background: none;
border: none;
cursor: pointer;
opacity: 0.3;
transition: opacity 0.2s;
font-size: 16px;
}

.todo-item .delete:hover { opacity: 1; }

/* 内联表单,让按钮和文字同行 */
form.inline {
display: inline;
margin: 0;
padding: 0;
}

/* 操作 */
.actions {
text-align: center;
margin-top: 20px;
}

.actions .link-btn {
background: none;
border: none;
color: #999;
font-size: 13px;
cursor: pointer;
}

.actions .link-btn:hover { color: #f56c6c; text-decoration: underline; }

/* 空状态 */
.empty {
text-align: center;
padding: 60px 20px;
color: #bbb;
}

.empty p:first-child { font-size: 18px; margin-bottom: 8px; }

/* 提示消息 */
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 14px;
}

.alert-success { background: #f0f9eb; color: #67c23a; }
.alert-error { background: #fef0f0; color: #f56c6c; }

18.4 运行

python app.py

访问 http://127.0.0.1:5000,一个完整的待办应用就跑起来了!

⬆ 返回目录


19. 项目部署

开发完成后,需要部署到服务器上让别人访问。

19.1 生产环境注意事项

项目开发环境生产环境
debug True False
SECRET_KEY 硬编码 从环境变量读取
服务器 Flask 内置 Gunicorn / uWSGI
数据库 SQLite MySQL / PostgreSQL
HTTPS 必须

19.2 用环境变量管理配置

import os

app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-fallback-key')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
app.config['DEBUG'] = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'

Linux 下设置环境变量:

export SECRET_KEY="your-random-secret-key"
export DATABASE_URL="mysql+pymysql://user:pass@localhost/dbname"

生成随机密钥:

python -c "import secrets; print(secrets.token_hex(32))"

19.3 用 Gunicorn 部署(Linux)

pip install gunicorn

情况 A:非工厂模式(第 3-18 节的写法)

app.py 里已经有一个现成的 app 对象,直接:

# wsgi.py
from app import app

情况 B:工厂模式(第 15.5 节的写法)

app.py 里只有 create_app,没有 app 变量,需要:

# wsgi.py
from app import create_app

app = create_app()

⚠️ 千万别混用,否则会报 ImportError: cannot import name 'app' from 'app'。

启动:

# 4 个 worker 进程,监听 8000 端口
gunicorn -w 4 -b 0.0.0.0:8000 wsgi:app

19.4 用 Nginx 做反向代理

server {
listen 80;
server_name yourdomain.com;

location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location /static {
alias /path/to/your/app/static;
expires 30d;
}
}

19.5 用 systemd 守护进程

创建 /etc/systemd/system/myflask.service:

[Unit]
Description=Flask App
After=network.target

[Service]
User=www-data
WorkingDirectory=/path/to/your/app
Environment="SECRET_KEY=your-key"
ExecStart=/path/to/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 wsgi:app
Restart=always

[Install]
WantedBy=multi-user.target

启动:

sudo systemctl daemon-reload
sudo systemctl start myflask
sudo systemctl enable myflask

19.6 免费平台部署

不想折腾服务器?可以用这些平台:

  • PythonAnywhere:免费额度,适合学习
  • Render:免费层,支持自动部署
  • Vercel / Railway / Fly.io:现代 PaaS,体验好

⬆ 返回目录


20. 学习路线与常见问题

20.1 推荐学习路线

第 1 周:基础
├── 路由、视图函数
├── request / response
└── Jinja2 模板

第 2 周:进阶
├── 表单处理 Flask-WTF
├── 数据库 Flask-SQLAlchemy
└── 蓝图 Blueprint

第 3 周:完善
├── 用户认证 Flask-Login
├── 数据库迁移 Flask-Migrate
└── 错误处理与日志

第 4 周:实战
├── 做一个完整项目(博客 / 商城)
└── 部署上线

20.2 常用扩展清单

扩展用途
Flask-SQLAlchemy 数据库 ORM
Flask-Migrate 数据库迁移
Flask-WTF 表单和验证
Flask-Login 用户登录管理
Flask-Mail 发送邮件
Flask-Caching 缓存
Flask-CORS 跨域支持
Flask-RESTful / Flask-Smorest 构建 REST API
Flask-Limiter 限流
Flask-Admin 后台管理

20.3 常见问题

Q1:改了代码不生效? 确保 debug=True,或者手动重启服务器。

Q2:TemplateNotFound 错误? 检查模板文件是否在 templates/ 文件夹(名字必须是这个),路径大小写是否一致。

Q3:RuntimeError: Working outside of application context? 在应用上下文之外调用了 db 等对象。解决:

with app.app_context():
db.create_all()

Q4:KeyError: 'SECRET_KEY' 或 session 不工作? 设置 app.secret_key = '随便一串字符'。

Q5:端口 5000 被占用(Mac 常见)?

python app.py –port 5001
# 或在代码里
app.run(debug=True, port=5001)

Mac 上 5000 端口被 AirPlay 占用,可以在「系统设置 → 通用 → 隔空投送与接力」里关闭「隔空播放接收器」。

Q6:表单提交报 CSRF token missing? Flask-WTF 表单里要加 {{ form.hidden_tag() }}。

Q7:修改模型后数据库没变化? db.create_all() 不会修改已有表。开发阶段直接删除 .db 文件重建,生产环境用 Flask-Migrate。

Q8:Query.get() 报警告? Flask-SQLAlchemy 3.x 已弃用 Query.get(),改用 db.session.get(Model, pk)。

Q9:datetime.utcnow() 报警告? Python 3.12+ 弃用了 datetime.utcnow(),用 lambda: datetime.now(timezone.utc) 替代。

Q10:如何返回中文不乱码? 确保 HTML 里有 <meta charset="UTF-8">,Python 文件保存为 UTF-8 编码。

20.4 调试技巧

① 使用 Flask Shell

flask shell

>>> from app import db, User
>>> User.query.all()

② 打印调试

app.logger.info(f'用户 ID: {user_id}')
print(request.args) # 调试时临时用

③ 用 Postman / curl 测试 API

curl http://127.0.0.1:5000/api/users
curl -X POST http://127.0.0.1:5000/api/users \\
-H "Content-Type: application/json" \\
-d '{"name":"test"}'

④ 查看路由列表

with app.app_context():
for rule in app.url_map.iter_rules():
print(f'{rule.endpoint:20s} {rule.methods} {rule}')

⬆ 返回目录


总结

恭喜你完成了这份 Flask 教程!回顾一下你学到的:

✅ Flask 基础:路由、视图函数、请求与响应 ✅ 模板:Jinja2 语法、模板继承、静态文件 ✅ 会话管理:Cookie、Session、Flash 消息 ✅ 表单:Flask-WTF 定义、验证、渲染 ✅ 数据库:SQLAlchemy 模型、增删改查、关系 ✅ 工程化:蓝图、工厂模式、错误处理、钩子 ✅ 实战:完整的待办应用 ✅ 部署:Gunicorn + Nginx + systemd

下一步建议:

  • 把待办应用扩展成支持多用户(加 Flask-Login)
  • 尝试做一个博客系统(文章、分类、评论)
  • 学习写 REST API(Flask-Smorest 或 FastAPI)
  • 阅读 Flask 官方文档
  • 最重要的:多写代码。 看十遍不如自己敲一遍。遇到报错不要怕,报错信息就是最好的老师。

    祝你学习顺利!🚀

    赞(0)
    未经允许不得转载:171主机测评 » Flask 完整入门教程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址