1 Web 的本质 —— 请求与响应
是什么?为什么学?
Web(万维网)是浏览器(客户端)向服务器(服务端)发请求、服务器返回响应的循环。你每天打开网页,本质都是浏览器发 HTTP 请求(”我要这个页面”)→ 服务器处理 → 返回响应(”这就是内容”)。学 Web 开发第一件事就是建立这个模型——没有请求就没有响应,Flask 的每个函数都在处理”某个请求”。
底层原理
HTTP(超文本传输协议)是浏览器和服务器对话的”语言”。一次对话包含:请求行(方法 + 路径,如 GET /about)、请求头(浏览器类型、Cookie 等元信息)、请求体(POST 时携带的数据);响应则有状态码(200 成功、404 找不到、405 方法不允许、500 服务器错误)、响应头、响应体(HTML / JSON 内容)。Flask 把”解析请求、组装响应”的脏活全干了——你只管写路由函数,HTTP 细节框架处理。
生活类比
像去餐厅点菜:你(浏览器)对服务员(服务器)说”来一份宫保鸡丁”(请求),厨房做好后端上来(响应)。每次点菜都是一次独立的”请求-响应”,服务员不会因为你上次点过就记得——HTTP 也是无状态的(每次请求独立,所以要靠 Cookie / 数据库记住用户)。
注意事项总结
1 不启动服务器,用 test_client 模拟一次完整的请求-响应(可离线运行):
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "你好,Flask!"
client = app.test_client() # 模拟"浏览器"
resp = client.get("/") # 发送请求
print("状态码:", resp.status_code) # 200 = 成功
print("响应内容:", resp.get_data(as_text=True))
运行结果:
状态码: 200
响应内容: 你好,Flask!
app.test_client() 创建”假浏览器”——不发网络请求,直接在程序内部模拟完整请求-响应循环;client.get("/") 发出请求,Flask 走路由表匹配到 home 函数并调用;resp.status_code 看状态码、resp.get_data(as_text=True) 看响应体。
2 真实启动服务器:
if __name__ == "__main__":
app.run(host="127.0.0.1", port=2050, debug=True)
python app.py
运行结果(节选):
* Running on http://127.0.0.1:2050
debug=True 是开发模式:改代码保存后服务器自动重启,报错时浏览器能看到详细错误(上线时记得关掉)。
易错点
- 错误写法:只看页面内容,不看状态码 → 问题:404/500 也可能返回内容,误以为成功 → 正确写法:先看
status_code再处理内容,必要时raise_for_status()。 - 错误写法:以为服务器记得你上次的请求 → 问题:HTTP 无状态,每次请求相互独立 → 正确写法:需要”记忆”就用数据库、Cookie、Session。
记忆口诀:请求响应成一对,状态码里看成败;HTTP 无记忆,数据靠存储。
2 路由 —— URL 与函数的对应关系
是什么?为什么学?
路由(route)是 Web 框架最核心的概念:把「网址路径」映射到「Python 函数」。@app.route("/about") 的意思是——用户访问 /about 这个网址,就执行它下面那个函数。没有路由,服务器就不知道”用户要什么、该执行哪段代码”;有了路由,一个网址 = 一段逻辑,网站的每个页面都是这么来的。
底层原理
Flask 内部维护一张路由表(URL 规则 → 函数的映射)。收到请求后,Flask 拿请求路径和路由表匹配(支持 /hello/<name> 这类动态规则——尖括号是变量,匹配任意一段文本,匹配到的值自动作为参数传给函数);匹配成功就调用函数并返回响应,匹配不到就返回 404。”路由”二字,就是为请求指路。
生活类比
路由像公司前台的分机表:拨 8001 转财务、拨 8002 转人事。你(浏览器)访问 /about 就像拨分机,前台(Flask)查表找到对应的人(函数)接通并转达结果;表里没有的号码,前台只能回”查无此号”(404)。
注意事项总结
1 静态路由 + 动态路由:
from flask import Flask
app = Flask(__name__)
@app.route("/") # 访问根路径 http://127.0.0.1:2050/
def home():
return "你好,我是 Flask!"
@app.route("/hello/<name>") # 尖括号是变量,匹配任意一段文本
def hello(name): # 匹配到的值自动传入函数
return f"你好,{name}!"
@app.route(...) 是装饰器(Day 18)——把函数”登记”进路由表;访问 /hello/小明 时,Flask 把 小明 作为 name 参数传入 hello,函数返回的字符串就是响应内容。装饰器在这里的真实用途:给函数附加”URL 地址”这个元信息。
易错点
- 错误写法:路由函数名重复 → 问题:后面的会覆盖前面的(同名函数) → 正确写法:函数名保持唯一。
- 错误写法:动态路由
/hello/<name>拼写和函数参数不一致 → 问题:报TypeError:参数对不上 → 正确写法:尖括号里的名字必须和函数参数名一致。 - 错误写法:忘了
@app.route装饰器 → 问题:函数还在,但访问网址永远 404 → 正确写法:检查每个”页面函数”上方都有@app.route。
记忆口诀:网址绑函数,路由来指路;尖括号是变量,参数要对齐。
3 render_template —— Python 和 HTML 的结合
是什么?为什么学?
render_template 让 Flask 读取 HTML 模板文件,把 Python 变量填进去,再返回完整的 HTML 页面。网站页面几乎都是 HTML 文件而不是字符串——用模板,页面样式(HTML/CSS)和逻辑(Python 数据)分离,维护起来清晰得多。这是前后端交互的基础:模板里留占位符,Python 负责填值。
底层原理
Flask 使用 Jinja2 模板引擎:模板文件里写普通 HTML,{{ 变量 }} 是”输出占位符”(把变量值插入页面),{% if %} / {% for %} 是”逻辑标签”(控制片段显示/循环)。render_template("home.html", title=..., today=...) 的流程:找到 templates/ 下的文件 → 解析成模板 → 用关键字参数填充占位符 → 输出最终 HTML。**模板是”母版”,渲染是”填字游戏”**。
生活类比
像填空题试卷:HTML 模板是印好的卷子,{{ title }} 是横线上的空;render_template 是老师拿答案(Python 变量)填进去,填完的卷子(完整 HTML)才发给学生(浏览器)。同一个模板反复填不同答案——这就是”一个模板渲染不同数据”。
注意事项总结
1 **模板文件 templates/home.html**({{ title }} 是占位符,由 Python 填值):
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<title>首页</title>
</head>
<body>
<h1>{{ title }}</h1>
<p>今天是 {{ today }}</p>
</body>
</html>
2 **后端 app.py**:
from flask import Flask, render_template
from datetime import date
app = Flask(__name__)
@app.route("/")
def home():
return render_template(
"home.html", # 模板文件名(默认在 templates/ 目录找)
title="我的 Flask 网站", # 关键字参数 = 模板里的变量名
today=date.today(), # 传日期对象,Jinja2 自动转成字符串
)
模板里 {{ title }} 和 {{ today }} 是占位符;Python 侧 render_template 的关键字参数与占位符同名对应——名字对不上,页面就空着(不报错,显示空)。
3 目录约定:Flask 默认从 templates/ 目录找模板文件:
project/backend/
├── app.py
└── templates/
└── home.html
易错点
- 错误写法:模板文件没放在
templates/目录 → 问题:报TemplateNotFound→ 正确写法:目录结构必须是项目/templates/xxx.html。 - 错误写法:
render_template传的变量名和模板占位符不一致 → 问题:页面显示空白,且不报错,难排查 → 正确写法:两边名字严格一致(title↔title)。 - 错误写法:模板里写
{{ user.name }}但传的是字典 → 问题:字典要用下标{{ user["name"] }}→ 正确写法:传对象用点号,传字典用下标(Jinja2 都支持,别混)。
记忆口诀:模板是母版,变量来填空;名字要对齐,目录别放错。
4 GET 与 POST —— 两种传数据的姿势
是什么?为什么学?
网页让用户输入内容,最常见的是表单(form)。表单提交数据有两种方式:GET 把数据拼在 URL 问号后面(?city=北京),适合查询、书签可收藏;POST 把数据放在请求体里,适合提交操作、隐私数据。区分两者是写任何 Web 程序的基本功——接口设计成 GET 还是 POST,直接决定别人怎么调用它。
底层原理
GET 和 POST 是 HTTP 的两种请求方法(method)。GET 语义是”取资源”:参数编码进 URL(长度受限、地址栏可见、留在历史记录和日志里);POST 语义是”提交数据”:数据放请求体(body)里,容量大、URL 不可见、可传文件。Flask 侧对应两种读取:GET 用 request.args.get("city"),POST 用 request.form.get("city")。注意:POST 不是加密,只是不在 URL 上暴露——要保密得用 HTTPS。
生活类比
GET 像在超市门口对讲机喊话:”有没有牛奶?”(声音穿过门缝,谁路过都听得见);POST 像填一张购物清单递给店员:”我要这个这个这个”(清单内容在柜台里面传递,外面看不见)。喊话适合”问一句”,递单子适合”下订单”。
注意事项总结
1 后端两种取参方式对照:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/weather")
def weather_get():
city = request.args.get("city", "") # GET:从 URL 问号后面取
return render_template("search.html", result={"city": city, "max": 33, "min": 25})
@app.route("/weather/post", methods=["POST"]) # 声明只接受 POST
def weather_post():
city = request.form.get("city", "") # POST:从请求体取
return f"你 POST 提交的城市是:{city}"
2 表单模板:
<!-- GET 表单:提交后浏览器跳转到 /weather?city=北京 -->
<form method="get" action="/weather">
<input type="text" name="city" placeholder="输入城市名">
<button type="submit">查询</button>
</form>
<!-- POST 表单:method 改成 post,提交后数据放进请求体 -->
<form method="post" action="/weather/post">
<input type="text" name="city" placeholder="输入城市名">
<button type="submit">提交</button>
</form>
表单的 method 决定提交方式,action 决定提交到哪个路径,name="city" 决定参数名——**name 是参数名的唯一来源**;路由加了 methods=["POST"] 后,用 GET 访问会返回 405 Method Not Allowed。
3 三种取参方式对照:
request.args.get("名字"):GET 参数(URL 问号后面)request.form.get("名字"):POST 表单参数(请求体)request.json/request.get_json():POST 的 JSON 数据
易错点
- 错误写法:POST 表单提交报 405 → 问题:路由没声明接受 POST → 正确写法:装饰器加
methods=["POST"]。 - 错误写法:
request.args和request.form用反 → 问题:参数取不到,得到None/ 默认值 → 正确写法:GET 用 args,POST 用 form,别混。 - 错误写法:表单
name和后端取的名字不一致 → 问题:取不到值(拼写错是 Web 开发高频 bug) → 正确写法:name="city"↔request.form.get("city")严格一致。
记忆口诀:GET 拼在问号后,POST 藏在身体里;name 是钥匙,args 配 form。
5 jsonify —— 给程序用的”页面”
是什么?为什么学?
前面做的都是返回 HTML 给浏览器看。但程序之间通信要的是数据,不是页面。jsonify 把 Python 字典/列表变成 JSON 字符串返回,并自动设置 Content-Type: application/json——这就是一个”API 接口”(应用程序编程接口,Application Programming Interface)。现在很多网站都是”前端页面 + 后端 API”的架构,学会 jsonify,你就从”API 的使用者”变成了”API 的提供方”。
底层原理
jsonify 内部做三件事:把 Python 对象序列化成 JSON 字符串(原理同 Day 27 的 json.dumps)、设置响应头 Content-Type: application/json(告诉客户端”这是 JSON 不是 HTML”)、返回 Response 对象。**返回 JSON 的接口,本质就是”返回一段有约定格式的文本”**——调用方用 resp.json() 解析,和解析任何 API 一样。状态码用第二个参数指定:return jsonify({...}), 404,让调用方”看码识结果”。
生活类比
HTML 页面像给顾客看的菜单(图文并茂,人能看懂);JSON 接口像给后厨的电子订单(结构化数据,程序直接处理)。你(调用方)对着后厨喊一句标准格式的话,后厨(API)按格式回一段数据——双方都按约定格式交流,这就是”接口协议”。
注意事项总结
1 写一个 JSON API 接口:
from flask import Flask, jsonify, request
app = Flask(__name__)
CITY_WEATHER = { # 模拟的"城市天气数据库"
"北京": {"max": 35, "min": 25, "desc": "晴"},
"上海": {"max": 33, "min": 26, "desc": "多云"},
"广州": {"max": 34, "min": 27, "desc": "阵雨"},
}
@app.route("/api/weather")
def api_weather():
city = request.args.get("city", "")
if city not in CITY_WEATHER:
return jsonify({"error": f"没有 {city} 的天气数据"}), 404 # 查无此城:404 + 错误信息
return jsonify({"city": city, **CITY_WEATHER[city]}) # 查到了:200 + 数据
2 用 requests 调用自己的接口:
import requests
resp = requests.get("http://127.0.0.1:2050/api/weather", params={"city": "上海"})
print("状态码:", resp.status_code)
data = resp.json()
print(data["city"], data["desc"], f"{data['min']}~{data['max']}℃")
运行结果:
状态码: 200
上海 多云 26~33℃
**CITY_WEATHER[city] 是字典展开(Day 8 学过)。接口设计黄金法则:正常数据 + 200,异常情况 + 明确错误信息 + 合适状态码——调用方不用猜。注意先启动服务端,再运行调用脚本。
易错点
- 错误写法:只返回
jsonify(...)不传状态码 → 问题:出错时也是 200,调用方无法判断 → 正确写法:异常场景加状态码:jsonify({...}), 404。 - 错误写法:忘了
resp.json()直接把响应对象当数据用 → 问题:拿到的是 Response 对象不是字典 → 正确写法:先.json()再访问键。 - 错误写法:JSON 里塞了 datetime 对象 → 问题:序列化报错(JSON 不认识日期对象) → 正确写法:先转成字符串再放进字典。
记忆口诀:jsonify 出数据,状态码表结果;先看码再看数据,接口约定要清楚。
6 动手实践:天气查询网页(前后端交互)
把 Open-Meteo 接进来,做一个完整的天气查询网页。后端代码放 project/backend(端口 2050),前端页面放 project/frontend(端口 1050)。
**后端 Flask 应用 project/backend/app.py**:
from flask import Flask, jsonify, render_template, request
import requests
app = Flask(__name__)
# 城市名 -> (纬度, 经度)
CITIES = {
"北京": (39.9042, 116.4074),
"上海": (31.2304, 121.4737),
"广州": (23.1291, 113.2644),
"深圳": (22.5431, 114.0579),
"成都": (30.5728, 104.0668),
}
def fetch_from_open_meteo(city):
"""调用 Open-Meteo 免费天气 API,返回最近 3 天数据"""
lat, lon = CITIES[city]
params = {
"latitude": lat,
"longitude": lon,
"daily": "temperature_2m_max,temperature_2m_min,weather_code",
"timezone": "Asia/Shanghai",
"forecast_days": 3,
}
resp = requests.get("https://api.open-meteo.com/v1/forecast",
params=params, timeout=10)
resp.raise_for_status()
daily = resp.json()["daily"]
return [
{"date": d, "max": tmax, "min": tmin}
for d, tmax, tmin in zip(
daily["time"],
daily["temperature_2m_max"],
daily["temperature_2m_min"],
)
]
@app.route("/")
def index():
return render_template("weather.html", cities=CITIES.keys())
@app.route("/weather", methods=["POST"])
def weather():
city = request.form.get("city", "").strip()
if city not in CITIES:
return render_template("weather.html",
cities=CITIES.keys(),
error=f"不支持的城市:{city}")
try:
days = fetch_from_open_meteo(city)
return render_template("weather.html",
cities=CITIES.keys(),
city=city,
days=days)
except requests.exceptions.RequestException as e:
return render_template("weather.html",
cities=CITIES.keys(),
error=f"天气服务请求失败:{e}")
@app.route("/api/weather")
def api_weather():
"""JSON 接口:给程序调用的版本,比如前端页面用 fetch 调用"""
city = request.args.get("city", "").strip()
if city not in CITIES:
return jsonify({"error": f"不支持的城市:{city}"}), 400
try:
return jsonify({"city": city, "days": fetch_from_open_meteo(city)})
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 502
if __name__ == "__main__":
app.run(host="127.0.0.1", port=2050, debug=True)
**模板 project/backend/templates/weather.html**:下拉框选城市 + POST 表单 + {% if %} / {% for %} 控制显示,页面上展示 {{ city }} 未来 3 天天气 表格(日期/最高温/最低温),错误用 .error 样式显示。
启动与访问:
cd project/backend
pip install flask requests
python app.py
浏览器访问 http://127.0.0.1:2050/,选择城市、点”查询”就能看到真实天气。整个流程:表单 POST → Flask 接收 → 后端调 Open-Meteo → 数据传入模板 → 渲染成 HTML 返回。
扩展:前端独立页面用 fetch 调后端接口(project/frontend/weather.html,前后端彻底分离)——用两个终端启动:
# 终端 1:启动后端
cd project/backend && python app.py # http://127.0.0.1:2050
# 终端 2:启动前端静态页面
cd project/frontend && python -m http.server 1050 # http://127.0.0.1:1050
浏览器打开 http://127.0.0.1:1050/weather.html,输入城市名点”查询”——浏览器里的 JavaScript 向后端 /api/weather 发请求,拿到 JSON 后渲染成表格。这就是真正的前后端分离:前端管页面(1050),后端管数据和逻辑(2050),中间用 API 通信。
升级挑战:在 JSON 接口里加上 weather_code 并翻译成”晴/多云/雨”这样的中文;或者把 day28 的 SQLite 接进来,查询过的城市记录到数据库。
今日总结
理解 Web 本质:浏览器发请求、服务器回响应,HTTP 无状态,状态码(200/404/405/500)里看成败
路由 = URL 与函数的对应关系:@app.route 装饰器登记,/hello/<name> 动态参数,名字要和函数参数一致
render_template 渲染 HTML 模板:{{ 变量 }} 占位符 + 关键字参数同名对应,模板放 templates/ 目录
GET 与 POST:GET 参数在 URL 问号后(request.args),POST 数据在请求体(request.form),路由要声明 methods=["POST"]
jsonify 做 JSON API 接口:正常 200 + 数据,异常 4xx/5xx + 错误信息,用 requests 调用自己的接口
天气查询网页:后端 2050(表单 POST → 调 Open-Meteo → 渲染模板),前端 1050(fetch 调 JSON 接口),前后端分离
| 报错 | 原因 | 修复 |
|---|---|---|
ModuleNotFoundError: No module named 'flask' | 没安装 Flask | 命令行执行 pip install flask |
| 浏览器打不开 127.0.0.1:2050 | app.py 没启动,或端口被占用 | 先运行 python app.py;端口被占就换端口,或关掉占用端口的程序 |
TemplateNotFound: weather.html | 模板不在 templates/ 目录,或文件名不对 | 确认目录结构 project/backend/templates/weather.html,检查文件名大小写 |
| 改代码后页面没变化 | 没开 debug=True,服务器没自动重启 | 加 debug=True 重启,或手动重启服务器 |
| 表单提交后 405 Method Not Allowed | 路由没声明 POST 方法 | 路由装饰器加 methods=["POST"] |
sqlite3.OperationalError / 接口 500 | 后端内部报错(比如没启动天气 API 依赖) | 看终端堆栈信息;检查 CITIES 字典、requests 调用是否正常 |