码上问学问代码之理,学编程之道

第 1 章 · 全栈开发与 Flask 起步

数据库里已经躺着「古典文学网」的六篇作品(课程一),但用户打开浏览器时,看到的不能是一堆 SQL 结果。中间缺的这个人,就是后端:它接收前端的请求、查库、算出结果,再用 JSON 交回去。本章让你把第一行 Flask 代码跑起来,并在浏览器里看到它的响应——从这一章起,「古典文学网」正式开始长成。

全栈开发与前后端分离

先说清楚谁干什么
前后端分离:前端是「点单员」,后端是「厨房」 浏览器 / 前端(Vue) 菜单 + 点单员 展示数据、收集操作 后端 / Flask 厨房 查库、算结果、出成品 ① HTTP 请求:GET /api/article/list ② JSON:{ code, msg, data } 订单 = 接口契约:双方事先约定好格式,换厨师 / 换设计都不影响对方

前端只管「点单」(发请求、收 JSON),后端只管「做菜」(查库、出结果);两边靠事先约定好的接口契约对接。

先讲为什么你去餐厅吃饭,坐下来先看菜单,指着图片说「我要这个」。厨房收到订单,做好端上来——你完全不需要知道厨房里有几个灶、火开多大。
前后端分离就是这个分工:前端是「菜单 + 点单员」,负责把信息漂亮地摆给用户看、收集用户的操作;后端是「厨房」,负责真正处理数据、做业务判断,再把成品端出来。
这个分工能成立,靠的是一件很朴素的事:两边事先说好订单长什么样。订单格式定死了,厨房换厨师、菜单换设计,都不影响对方。
动手做「订单」在前后端分离里就是一次 HTTP 交互:前端发一个请求(要哪一页的作品),后端回一段 JSON(作品数据)。先看看这对「订单」长什么样——这就是你们双方要签的接口契约。
接口契约.httphttp
GET /api/article/list?page=1&page_size=2 HTTP/1.1
Host: 127.0.0.1:5000
报文内容HTTP 报文
运行结果: HTTP/1.1 200 OK Content-Type: application/json { "code": 200, "msg": "success", "data": { "list": [ { "id": 1, "title": "静夜思", "author": "李白", "dynasty": "唐", "category_name": "唐诗", "views": 328, "likes": 42 }, { "id": 2, "title": "春望", "author": "杜甫", "dynasty": "唐", "category_name": "唐诗", "views": 256, "likes": 31 } ], "total": 6, "page": 1, "page_size": 2 } }
讲透
  • 前端(Vue 3,课程三):负责界面、交互、路由、把数据渲染成页面。代码跑在用户的浏览器里,用户按 F12 就能看见,所以它不可信。
  • 后端(Flask,本课程):负责业务规则、数据库读写、登录鉴权、文件存储。代码跑在服务器上,用户看不见,所以校验必须做在这一层。
  • 两者之间只有一件事:HTTP 请求 → JSON 响应。不传 HTML、不传对象,只传「能变成文本的东西」。
  • 全栈开发不是「什么都会一点」,而是知道这条边界在哪里:哪些事该前端做、哪些必须后端做、两边怎么对接。你在课程一学的 SQL,就是「厨房内部的一步」。
避坑新手最容易犯的错是两边都渲染一遍:在 Flask 的 return 里拼 HTML 字符串,前端又用 Vue 渲染一遍同样的内容。结果是改一处漏一处,两份代码谁也说不清谁对。本项目定死一条:后端只返回 JSON,页面的样子全部由 Vue 决定,后端不管「长什么样」,只管「是什么」。

后端在项目里的位置

一张图看清分层
后端在哪一层:承上启下的「接口层」 表现层 浏览器 / Vue 单页应用,只负责「看得见」 接口层(后端在这里) Flask:路由 · 请求解析 · 鉴权 · ORM 查库 数据层 MySQL:把数据真正存下来 后端 = 接口层 承上启下

整个系统分三层:表现层(前端)在最上,数据层(数据库)在最下,后端就是中间那层「接口层」,负责把上层的请求翻译成对下层的操作。

动手做「古典文学网」整个项目由三部分组成:数据库脚本、后端 Flask 项目、前端 Vue 项目。先把它们的目录摆出来看一眼,你才知道自己接下来要动的是哪一块。
查看整体结构bash
cd ~/pyweb
tree -L 2
终端输出命令行执行结果
运行结果: . ├── hiwen.sql # 数据库脚本:建库建表 + 种子数据(课程一) ├── hiflask/ # 后端:Flask 项目(课程二,本章要跑起来的就是它) │ └── ... └── hiweb/ # 前端:Vue 3 项目(课程三) └── ...
讲透:一次点击数据走了几站
  • 第 1 站 浏览器:用户点开「作品列表」,Vue 发出一条 GET /api/article/list?page=1。
  • 第 2 站 Flask:后端接到请求,决定「这次要查什么、要不要登录、参数合不合法」。
  • 第 3 站 MySQL:后端把查询交给数据库,拿到作品行(课程一学的 SQL 就发生在这一步)。
  • 第 4 站 回到 Flask:后端把结果整理成统一格式 {code, msg, data},转成 JSON 返回。
  • 第 5 站 Vue:前端拿到 data.list,渲染成卡片列表。
  • 所以后端的定位很清楚:它是唯一同时接触数据库和外部请求的那一层,也是安全的关口。
避坑数据库账号密码只能待在后端。前端代码会原封不动下载到用户机器上,你把 root/123456 写进 Vue 里,等于把库的管理权送出去。前端能看到的,只有后端愿意给的接口路径;连接数据库这件事,永远只发生在 hiflask/ 里。

虚拟环境:每个项目一口独立的锅

venv 是标配不是可选项
虚拟环境:每个项目一口独立的锅 项目 A · 古典文学网 venv/(A 的锅) Flask 2.3 · SQLAlchemy 2.0 项目 B · 红影赏析 venv/(B 的锅) Flask 3.0 · 另一套版本 系统 Python(公共灶台) 基于它创建各自的 venv,但项目之间互不干扰 版本互不干扰 同一台机器上,A 用 Flask 2.x、B 用 Flask 3.x 也能并存——这就是 venv 的意义

每个项目有自己的 venv(一口独立的锅),装各自的依赖版本。它们都基于系统 Python 创建,但彼此隔离,不会出现「装了新包把旧项目搞坏」的情况。

先讲为什么直接 pip install flask 会装到系统 Python 里,那是全机器共用的一口锅。你今天做的项目要 Flask 3,下学期的老项目只认 Flask 2——同一个锅里煮不出两种口味,最后必然打架。更糟的是,系统里的 Python 常常被操作系统自己用着,你把它搞乱了,机器就出毛病。
虚拟环境(venv)就是给每个项目发一口独立的锅:里面有自己的 Python 解释器和自己的第三方包,你在这个项目里怎么折腾,都不会影响别的项目。
动手做三步:建环境 → 激活 → 装包。顺序不能颠倒,装包必须在激活之后。
创建虚拟环境bash
# 1. 在项目根目录里创建虚拟环境,会生成一个 venv/ 文件夹
python3 -m venv venv

# 2. 激活它(激活成功后命令行前面会出现 (venv))
source venv/bin/activate          # macOS / Linux
venv\Scripts\Activate.ps1         # Windows PowerShell

# 3. 现在再装包,就装进这个项目的 venv 里了
pip install flask

# 4. 不用了,退出虚拟环境
deactivate
终端输出命令行执行结果
运行结果: $ python3 -m venv venv $ source venv/bin/activate (venv) $ python -V Python 3.11.6 (venv) $ pip install flask Collecting flask Downloading flask-3.0.3-py3-none-any.whl (101 kB) Collecting Werkzeug>=3.0.0 Downloading werkzeug-3.0.6-py3-none-any.whl (227 kB) Collecting Jinja2>=3.1.2 Downloading jinja2-3.1.4-py3-none-any.whl (133 kB) Collecting itsdangerous>=2.1.2 Downloading itsdangerous-2.2.0-py3-none-any.whl (17 kB) Collecting click>=8.1.3 Downloading click-8.1.7-py3-none-any.whl (97 kB) Collecting blinker>=1.6.2 Downloading blinker-1.8.2-py3-none-any.whl (9.2 kB) Collecting MarkupSafe>=2.1.1 Downloading MarkupSafe-2.1.5-cp311-cp311-macosx_11_0_arm64.whl (14 kB) Installing collected packages: MarkupSafe, itsdangerous, click, blinker, Werkzeug, Jinja2, flask Successfully installed Jinja2-3.1.4 MarkupSafe-2.1.5 Werkzeug-3.0.6 blinker-1.8.2 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
讲透
  • venv/ 是一个文件夹,不是一个命令。它就在你的项目目录里,里面装着这个项目专用的解释器和包,删掉它就等于「把这口锅扔了」,重新建一个就好。
  • 怎么确认自己处在虚拟环境里?看命令行提示符有没有 (venv),或者执行 which python,路径应该指向你项目里的 venv/bin/python,而不是 /usr/bin/python3。
  • venv/ 不进版本库:它体积大、还跟机器相关。团队协作靠的是 requirements.txt(下面第 5 节就写),别人照着装一份即可。
  • 每次开机重进项目都要重新激活:source venv/bin/activate。这是每天上班的第一个动作。
避坑两个高频事故:① 忘了激活就装包——包装进系统 Python 了,你以为装好了,运行代码却报 ModuleNotFoundError: No module named 'flask',因为 Flask 跑在系统解释器上、装却在别处;② 把 venv/ 提交到 Git——仓库瞬间膨胀几百 MB,同事拉下来还可能因为路径不同直接报错。建项目第一件事:在 .gitignore 里写上 venv/ 和 __pycache__/。

第一个 Flask 程序

四行代码,一个能响应的服务
最小闭环:一次请求走了哪几步 浏览器 访问 http://127.0.0.1:5000/ WSGI 服务器 把请求交给 Flask 路由 @app.route('/') 匹配到 index() 返回响应 Hello, Flask! 响应回到浏览器,页面上显示出文字

四行代码跑起来的最小闭环:浏览器发请求 → WSGI 服务器转交 → 路由匹配到函数 → 返回值变成响应回到浏览器。

动手做在 hiflask/ 目录里新建 app.py,把下面这几行敲进去(不要复制粘贴之后就再不看,一边敲一边猜每行在干什么),然后在虚拟环境里运行它。
app.pypython
from flask import Flask, jsonify

app = Flask(__name__)
app.json.ensure_ascii = False   # 让 jsonify 直接输出中文,而不是 \uXXXX 转义


@app.route("/")
def index():
    return jsonify({"msg": "古典文学网 API 已启动", "version": "1.0"})


if __name__ == "__main__":
    app.run(debug=True)
运行输出Python 运行结果
运行结果: (venv) $ python app.py * Serving Flask app 'app.py' * Debug mode: on WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000 Press CTRL+C to quit * Restarting with stat * Debugger is active! * Debugger PIN: 423-166-905
讲透:逐行拆开看
  • app = Flask(__name__):创建 Flask 应用实例。__name__ 是当前模块的名字,Flask 靠它算出「项目的根目录在哪」,以后找模板、找静态文件、读配置都从这儿出发。这句几乎是所有 Flask 项目的第一行。
  • @app.route("/"):装饰器,作用是把「URL 路径」和「紧跟后面的函数」绑在一起。下面那个函数叫视图函数(view function),它的返回值就是响应体。下一章整章都在讲它。
  • jsonify({...}):把 Python 字典转成 JSON 响应,并自动把 Content-Type 设成 application/json。不写它、只返回普通字符串的话,浏览器会当成纯文本,前端很难解析。
  • app.run(debug=True):启动 Flask 自带的开发服务器。debug=True 有两个好处——改代码自动重启、报错时在浏览器显示调试页面。这个服务器只能自己开发时用,它性能差、不安全,上线要换成 Gunicorn/uWSGI(第 13 章讲)。
  • if __name__ == "__main__"::只有「直接运行这个文件」时才启动。以后用 flask run 或生产服务器启动时,它不会被重复执行。

项目目录规范

一开始就把架子搭对
项目目录规范:一开始就把架子搭对 flask_demo/ app.py 主程序:路由与业务逻辑 requirements.txt 依赖清单,换机器能一键还原 venv/ 虚拟环境(不要提交到仓库) static/ · templates/ 静态资源与页面模板 为什么要这样分层? · 主程序、依赖、环境、资源分开 · 换台机器 pip install -r 就能跑 · venv 不入库,仓库干净 · 后续加蓝图/模型有地方放 · 别人接手一眼看懂结构

目录规范的本质是「分层」:主程序、依赖清单、虚拟环境、静态资源各归各位。venv 不入库,换机器时 pip install -r requirements.txt 就能还原环境。

先讲为什么「古典文学网」后端要处理用户、作品、书评、收藏、点赞、轮播图六七摊事,全塞进一个 app.py 里,写到第三章就会变成几千行的怪物:找一个接口要翻半天,改一处不知道会不会碰坏别处。
目录规范不是为了好看,是为了「能改」。把「配置」「数据模型」「接口」分开,每类东西只有一处地方写,第 5 章我们再用蓝图(Blueprint)把它们真正拆成模块——但架子现在就得立起来。
动手做照着下面把 hiflask/ 的骨架建出来。这一章 models/ 和 views/ 里先放空的 __init__.py,第 5、6 章再往里填东西。
hiflask 骨架bash
hiflask/
├── app.py                 # 入口:创建 app、注册蓝图、启动
├── config.py              # 配置:数据库地址、密钥、上传目录
├── extensions.py          # 扩展实例:db / jwt / cors(先留空,第 6 章填)
├── models/                # 数据模型,一张表一个文件
│   ├── __init__.py
│   ├── user.py            # user 表
│   ├── article.py         # article 表
│   └── comment.py         # comment 表
├── views/                 # 接口(蓝图),一个模块一个文件
│   ├── __init__.py
│   ├── user.py            # /api/user/...
│   └── article.py         # /api/article/...
├── static/
│   └── upload/            # 用户上传的头像、封面(第 9 章用)
├── requirements.txt       # 依赖清单,别人照这个装
├── .gitignore             # 忽略 venv/、__pycache__/、.env
└── venv/                  # 虚拟环境(不进版本库)
终端输出命令行执行结果
运行结果: $ cd ~/pyweb/hiflask $ mkdir models views static static/upload $ touch app.py config.py extensions.py requirements.txt .gitignore $ touch models/__init__.py views/__init__.py $ ls app.py config.py extensions.py models requirements.txt static views .gitignore
动手做再把依赖记下来。requirements.txt 是给「另一个程序员、另一台电脑」看的说明书:写清用了哪些包、什么版本,别人一条命令就能装出和你一模一样的环境。
requirements.txtbash
Flask==3.0.3
Flask-SQLAlchemy==3.1.1
Flask-Cors==4.0.1
PyJWT==2.8.0
PyMySQL==1.1.1
终端输出命令行执行结果
运行结果: # 把当前环境的包导出来(团队里用) (venv) $ pip freeze > requirements.txt # 别人拿到项目后,一条命令装齐 (venv) $ pip install -r requirements.txt Collecting Flask==3.0.3 ... Successfully installed Flask-3.0.3 Flask-Cors-4.0.1 Flask-SQLAlchemy-3.1.1 PyJWT-2.8.0 PyMySQL-1.1.1
讲透:每个文件管一件事
  • config.py:配置集中放。数据库连接串、JWT 密钥、上传目录大小限制都写这儿。密钥不要硬编码进代码再提交到 Git——用环境变量或 .env(第 5 章细讲)。
  • extensions.py:扩展实例(db = SQLAlchemy() 这类)单独放,好处是 models/ 里的文件可以 import 它,而不会和 app.py 互相 import 形成循环引用。这是 Flask 项目最常见的坑,先留个印象。
  • models/:一张表一个文件,对应课程一里建的那七张表(user / category / article / comment / favorite / user_like / banner)。
  • views/:接口按业务模块分文件,路径也对齐接口文档——views/article.py 就负责 /api/article/... 那一组。
  • 记住这个判断标准:新加的功能,能一眼看出该放哪个文件,就说明架子搭对了。

验证接口通不通

浏览器与 curl 两种方式
动手做服务跑起来之后,先用浏览器打开 http://127.0.0.1:5000/ 看看。浏览器默认发 GET 请求,能直接看到返回的 JSON。
再打开一个终端(不要关掉跑着服务的那一个),用 curl 发一次同样的请求——这是后端开发里最常用的验证方式,因为它能精确控制方法、请求头和请求体。
用 curl 验证bash
# 最简写法,看返回内容
curl http://127.0.0.1:5000/

# 带 -s 安静模式(不显示进度条),管道给 python 格式化,中文更清楚
curl -s http://127.0.0.1:5000/ | python -m json.tool --indent 2

# 只看 HTTP 状态码
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:5000/
终端输出命令行执行结果
运行结果: $ curl http://127.0.0.1:5000/ {"msg":"古典文学网 API 已启动","version":"1.0"} $ curl -s http://127.0.0.1:5000/ | python -m json.tool --indent 2 { "msg": "古典文学网 API 已启动", "version": "1.0" } $ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:5000/ 200
讲透:为什么每写一个接口都要先 curl 一次
  • 把问题按层切开:接口 curl 通了,说明后端没问题;这时前端还报错,就一定是前端的事。要是从不验证,两边就会互相甩锅。
  • 前端真的需要这个接口:文档里写了「作品列表支持 page、page_size、category_id、keyword、order_by」,你 curl 一遍就等于替前端试过了,比等联调时才发现少参数划算得多。
  • curl 能看到浏览器看不到的东西:请求头、状态码、响应头、POST 请求体——这些在浏览器地址栏里根本没地方写。
  • 以后每天的开场动作:激活虚拟环境 → 跑起服务 → curl 一下首页 → 开始写代码。
避坑报 Connection refused 或访问不到时,按这个顺序查:① 5000 端口被占——macOS 的「隔空播放接收器」(AirPlay Receiver)默认就占着 5000,报错 Address already in use,去「系统设置 → 通用 → 隔空投送」关掉它,或者改用 app.run(debug=True, port=5001);② 服务没在跑——跑服务的那个终端窗口被关了或按了 Ctrl+C;③ 装错解释了——pip install flask 时没激活 venv,跑 python app.py 就报 ModuleNotFoundError;④ 端口写错——服务在 5000,你却 curl 了 8000,注意别把 Flask 的 5000 和 Vue 开发服务器的 5173 搞混。

Flask 与其他 Web 框架对比

为什么课程选它,而不是 Django / FastAPI

后端框架名字听着都像:Flask、Django、FastAPI,再往外还有 Node 的 Express、Java 的 Spring Boot。它们不是谁比谁「高级」,而是解决不同规模、不同场景的问题。下面按几个常用维度把主流框架摆在一起,重点不是记结论,而是看懂「每种选择背后在权衡什么」。

维度Flask(本课)DjangoFastAPIExpress(Node)Spring Boot(Java)
定位 / 哲学微框架:只给核心,其余你来拼全家桶:电池已备齐异步优先的 API 框架极简、自由组合的 Node 框架企业级一站式方案
学习曲线平缓:先跑起来,再学扩展较陡:概念多(MTV / ORM / 表单)中等:需懂类型注解与异步平缓,但 JS 生态杂陡:强类型、配置多
内置功能路由、请求解析;ORM / Admin 靠扩展ORM、Admin、表单、鉴权开箱即用依赖注入、数据校验、自动文档几乎都要自己装中间件安全、事务、消息队列等齐全
异步支持3.x 起原生支持 async 视图新版本支持,但非默认天生异步,是性能强项事件循环,天然异步支持,靠协程 / 响应式
性能 / 吞吐中(同步默认,教学够用)中高(异步 + Pydantic)中高高且稳,企业首选
生态 / 社区庞大,扩展多,中文资料多极大,教程与包极丰富较新但增长快,AI 圈爱用最大(npm),但质量参差成熟庞大,企业文档全
典型适用场景教学、中小项目、微服务、API 网关内容站、后台、快速建站高并发 API、ML 服务JS 全栈、实时应用大型分布式系统

横向看每一列:没有「最好」的框架,只有「最贴合当前问题」的框架。

讲透:怎么选,以及为什么是本课的「Flask」
  • 想最快跑通一个接口、又想完全看懂每一行 → Flask。它不替你做决定,路由、ORM、鉴权都明明白白摆在你眼前,最适合教学。Django 把太多东西藏进了「约定」,初学者反而看不清数据怎么走的。
  • 要一周搭出带后台管理的内容站 → Django。它的 Admin 和 ORM 开箱即用,省下的时间换来的是「黑盒变多」。
  • 接口要扛高并发、还要给前端自动生成文档 → FastAPI。本课第 7、8 章讲的 JWT、RESTful 它也支持,但它对类型注解要求更高,入门台阶更陡。
  • 「古典文学网」为什么选 Flask:它是中等规模的教学项目,要让学生逐行看懂「请求 → 查库 → JSON」这条链路,又要能平滑扩展到多台机器(用 JWT 无状态)。Flask 的「轻」恰好让我们把每一层都摊开讲,而不是被框架的魔法挡住视线。
动手练习
1在你自己的机器上建一个 hiflask 项目,创建并激活虚拟环境,装上 Flask,然后执行 pip freeze > requirements.txt。写出你用到的全部命令。
2给 app.py 加一个接口 /health,返回 {"status": "ok"},用来做「服务还活着吗」的检查。想清楚:如果直接 return "ok" 会有什么问题?
3迁移练习:「红影赏析」(电影主题)也要做前后端分离。请为它设计后端最小骨架目录(项目名 himovie),并写出一个返回电影列表的接口 /api/movie/list——先不连数据库,直接返回两条假数据,验证链路能通。
参考答案
练习答案.pypython
# 第 1 题(命令行)
# mkdir hiflask && cd hiflask
# python3 -m venv venv
# source venv/bin/activate
# pip install flask
# pip freeze > requirements.txt

# 第 2、3 题:app.py
from flask import Flask, jsonify

app = Flask(__name__)
app.json.ensure_ascii = False


# 第 2 题:健康检查
@app.route("/health")
def health():
    return jsonify({"status": "ok"})


# 第 3 题:红影赏析最小骨架(himovie/ 目录同 hiflask)
# himovie/
# ├── app.py  ├── config.py  ├── extensions.py
# ├── models/ ├── views/     ├── requirements.txt  └── venv/
@app.route("/api/movie/list")
def movie_list():
    # 还没连数据库,先返回两条假数据,只为验证「请求 → 响应」这条链路
    movies = [
        {"id": 1, "title": "流浪地球", "genre": "科幻", "rating": 8.4},
        {"id": 2, "title": "星际穿越", "genre": "科幻", "rating": 9.3},
    ]
    return jsonify({
        "code": 200,
        "msg": "success",
        "data": {"list": movies, "total": 2, "page": 1, "page_size": 10},
    })


if __name__ == "__main__":
    app.run(debug=True)
运行输出Python 运行结果
运行结果: $ curl -s http://127.0.0.1:5000/health | python -m json.tool --indent 2 { "status": "ok" } $ curl -s http://127.0.0.1:5000/api/movie/list | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "list": [ { "id": 1, "title": "流浪地球", "genre": "科幻", "rating": 8.4 }, { "id": 2, "title": "星际穿越", "genre": "科幻", "rating": 9.3 } ], "total": 2, "page": 1, "page_size": 10 } }

第 2 题的答案:return "ok" 返回的是纯文本,Content-Type 是 text/html,前端拿到的是字符串不是对象,还得自己再解析一遍。要返回结构化数据,就用 jsonify()。第 3 题的关键:换的是项目名、表名和字段名,不变的还是那一套「建环境 → 写路由 → 返回 JSON → curl 验证」的流程。

本章小结

📝 本节自测

做完点「检查本节」看解析与得分;做错可重做。