| Day 29 | Flask 网页:路由、模板、表单与 JSON 接口 |

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 传的变量名和模板占位符不一致 → 问题:页面显示空白,且不报错,难排查 → 正确写法:两边名字严格一致(titletitle)。
  • 错误写法:模板里写 {{ 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.argsrequest.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:2050app.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 调用是否正常
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇