知识点思维导图
29 个知识节点
参考资料
Python(18) - 中间件与依赖注入
读完后,你应能完成以下任务:
- 绘制“Python(18) - 中间件与依赖注入 / 先建直觉:你早就用过「中间件」了”的关键对象与数据流,解释“FastAPI 中间件必须 return response,而不是像 Express 那样调 next() 就完事——它更像 Koa 的 async (ctx, next) => { await next() } 风格(拿到下游结果再返回)。 -> call_next 返回的是一个 Response 对象,FastAPI 已经帮你把业务返回值序列化好了,你拿到的是成品响应,不是裸数据。 -> 真正的重头戏不是中间件,而是下面这个 Express 里没有的东西——依赖注入 Depends。”,并用源码位置、日志或 Trace 标注证据。
- 为“Python(18) - 中间件与依赖注入 / 依赖注入 Depends:把「准备工作」抽成可复用的函数”设计正常与异常输入,验证“FastAPI 的解法是依赖注入(Dependency Injection,DI):你把「准备工作」写成一个普通函数,路由函数在参数里用 Depends(那个函数) 声明「我需要它的结果」,FastAPI 会在调用路由前自动执行依赖函数、把返回值作为参数注入进来。”,输出首个偏差位置与回归测试结果。
- 实现“Python(18) - 中间件与依赖注入 / Depends(fn) vs Depends(fn()):最容易踩的前端坑”的最小代码或配置,检验“这是前端新手最高频的错误,单独拎出来讲。”,输出命令、结果与 Diff,并说明不适用边界。
每个接口都要校验登录、都要记日志、都要处理跨域——这些「横切关注点」你不想在每个路由里复制粘贴一遍。在 Express 里你用中间件解决,在 FastAPI 里你有两件武器:沿用你熟悉的「中间件(洋葱模型)」,外加一个更趁手的新东西——依赖注入
Depends。这篇讲清两者各管什么、认证怎么做、跨域(CORS)怎么配,并帮你避开「把Depends(fn)写成Depends(fn())」这类前端直觉陷阱。
一、先建直觉:你早就用过「中间件」了
在 Express/Koa 里,中间件是你每天都在写的东西——一个个函数串成「洋葱」,请求进来层层穿过,响应出去再层层穿回:
// Express:经典中间件,层层包裹
const app = express()
// 全局中间件:每个请求都先过这里(记日志)
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`)
next() // 关键:调用 next() 把控制权交给下一层,不调就卡死
})
// 业务路由
app.get('/users', (req, res) => res.json([]))
FastAPI 的 HTTP 中间件几乎是一对一翻译:
类比成立的地方:洋葱模型、call_next ≈ next()、可以在「进业务前」和「出业务后」两个时机插入逻辑——这些和 Express/Koa 一模一样。
边界(这里和 Express 不一样):
- FastAPI 中间件必须
return response,而不是像 Express 那样调next()就完事——它更像 Koa 的async (ctx, next) => { await next() }风格(拿到下游结果再返回)。 call_next返回的是一个Response对象,FastAPI 已经帮你把业务返回值序列化好了,你拿到的是成品响应,不是裸数据。- 真正的重头戏不是中间件,而是下面这个 Express 里没有的东西——依赖注入
Depends。FastAPI 官方更推荐用Depends处理认证、取数据库连接等大部分场景,中间件只留给「真正全局、跟具体路由无关」的逻辑。
二、依赖注入 Depends:把「准备工作」抽成可复用的函数
很多接口都要做同一件「准备工作」:解析当前登录用户、拿一个数据库会话、校验分页参数……在 Express 里你会写一串中间件挂上去,但中间件有个痛点——它把结果塞进 req 上,靠约定取值,没有类型、容易写错 key。
FastAPI 的解法是依赖注入(Dependency Injection,DI):你把「准备工作」写成一个普通函数,路由函数在参数里用 Depends(那个函数) 声明「我需要它的结果」,FastAPI 会在调用路由前自动执行依赖函数、把返回值作为参数注入进来。
前端类比:这和 React 的自定义 Hook 神似——
useAuth()帮你把「取当前用户」的逻辑封装成可复用单元,组件里调一下就拿到结果,不关心内部怎么实现。Depends(get_current_user)就是后端版的const user = useAuth():声明式地「要一个东西」,框架负责把它准备好递过来。如果你用过 Angular/Nest.js,那它就是你熟悉的构造器注入,只不过 FastAPI 是「按函数参数注入」。
并排看 Express 里你会怎么做:
// Express:靠中间件把结果挂到 req 上,无类型、靠 key 约定
function paginationParams(req, res, next) {
req.page = { // 塞进 req.page,下游靠这个 key 取——拼写错了不报错
skip: parseInt(req.query.skip) || 0,
limit: parseInt(req.query.limit) || 10,
}
next()
}
app.get('/users', paginationParams, (req, res) => {
res.json({ skip: req.page.skip, limit: req.page.limit }) // 取值靠记 key
})
Depends 比中间件强在哪:
| 维度 | Express 中间件 | FastAPI Depends |
|---|---|---|
| 结果怎么拿 | 塞进 req.xxx,靠 key 约定 |
作为函数参数注入,有类型、IDE 能补全 |
| 作用范围 | 挂在路由/全局上,全有或全无 | 精确到单个路由,要哪个声明哪个 |
| 能否声明参数 | 自己从 req.query/body 抠 |
依赖函数能直接声明查询/路径参数,自动校验 |
| 复用 | 函数复用 | 函数复用 + 自动出现在 OpenAPI 文档 |
| 嵌套 | 手动串 | 依赖可以依赖别的依赖(自动递归解析) |
关键差异(别套 Express 心智):
Depends不是「请求进来前跑一遍」的全局拦截器,它是**「这个路由声明了我要什么,框架就给我准备什么」**——是按需的、声明式的、带类型的。这才是 FastAPI 处理认证/数据库连接的首选,中间件退居二线。
三、Depends(fn) vs Depends(fn()):最容易踩的前端坑
这是前端新手最高频的错误,单独拎出来讲。看这两行:
WHY(为什么这里反直觉):作为前端,你习惯
useAuth()是要带括号调用的。但Depends要的是「待调用的函数对象」,不是「调用结果」——它需要拿到函数引用,好在每次请求时替你调用、注入当前请求的参数。写成Depends(fn())等于在模块加载那一刻就把函数执行了一次,再把它的返回值(这里是个dict)交给Depends;而Depends拿到的不是可调用对象,FastAPI 在启动 / 注册路由时就会直接报错(无法识别这个依赖)。退一步说,就算返回值碰巧是个函数侥幸没报错,它也只在导入时算过一次,每个请求都拿不到当前的 token、新的数据库会话,认证全失效。记住口诀:Depends里只写函数名,不加括号。
类比 React 你能秒懂:
// React:把函数本身传给 useMemo/useCallback,由 React 决定何时调用
useEffect(myEffect, []) // ✅ 传函数引用,React 在合适时机调
useEffect(myEffect(), []) // ❌ 立即调用,把返回值(很可能是 undefined)当成 effect 回调传进去
Depends(fn) 对应 useEffect(myEffect, []),Depends(fn()) 对应那个错误写法——是同一种「该传引用却传了调用结果」的坑。
四、用 Depends 做认证:解析当前登录用户
认证是 Depends 最典型的战场。思路:写一个 get_current_user 依赖,从请求头里取 token、校验、查出用户;任何需要登录的路由,参数里 Depends(get_current_user) 一挂即可。
并排看 Express 的等价写法:
// Express:认证中间件 + 手动挂到需要保护的路由上
function getCurrentUser(req, res, next) {
const auth = req.headers.authorization
if (!auth) return res.status(401).json({ detail: '未登录' })
if (!auth.startsWith('Bearer ')) return res.status(401).json({ detail: 'token 格式错误' })
req.user = { id: 1, name: 'imber' } // 挂到 req.user,下游靠 key 取
next()
}
// 在需要保护的路由上手动挂中间件
app.get('/me', getCurrentUser, (req, res) => {
res.json({ id: req.user.id, name: req.user.name })
})
差别就在前面那张表:FastAPI 版本里 user 是带类型的函数参数(IDE 能补全),且这个 get_current_user 会自动出现在 /docs 文档里标注「这个接口需要认证」。
五、依赖的进阶玩法:嵌套与资源收尾
5.1 依赖嵌套:依赖还能依赖别的依赖
Depends 的杀手锏:一个依赖函数里可以再用 Depends 声明它自己的依赖,FastAPI 会自动递归解析。这是中间件做不到的优雅。
执行顺序是自动的:get_current_user → require_admin → 路由函数。你只声明了 require_admin,FastAPI 看到它依赖 get_current_user,就自动先把那个也跑了。类比 React Hook 里一个自定义 Hook 内部调另一个 Hook(useAdmin 内部调 useAuth),调用链自动展开。
5.2 yield 依赖:带「收尾」的资源管理(数据库连接最常用)
如果依赖需要用完后收尾(关数据库连接、释放锁),用 yield 代替 return:yield 之前是「准备」,yield 之后是「收尾」,收尾会在请求处理完后自动执行。
前端类比:
yield依赖 ≈ ReactuseEffect里return () => cleanup()的「副作用 + 清理函数」模式——yield前是 effect 主体,yield后(finally)是 cleanup。生成器yield的语法对标 JSfunction*/yield(详见第 06 篇),这里借它「暂停—恢复」的能力把收尾逻辑挂在请求生命周期末尾。
六、CORS:跨域,前端工程师的老朋友
跨域你太熟了——前端 fetch 后端被浏览器拦下来、控制台一片红的 CORS 报错。后端要做的是主动放行:通过响应头告诉浏览器「这些来源我允许」。FastAPI 用一个现成的中间件 CORSMiddleware 搞定。
并排看 Express 的 cors 中间件,思路完全一致:
// Express:用 cors 中间件放行
const cors = require('cors')
app.use(cors({
origin: ['http://localhost:5173', 'https://my-frontend.com'],
credentials: true,
}))
必踩的坑(前端尤其会中招):当
allow_credentials=True(要带 Cookie)时,allow_origins不能用["*"]通配。这是浏览器的硬性安全规则——「带凭证」和「允许任意来源」不能同时成立,必须写出具体的来源列表。很多人本地图省事写allow_origins=["*"]+allow_credentials=True,结果带 Cookie 的请求依然跨域失败,排查半天。要么列具体域名,要么关掉allow_credentials。
七、中间件 vs 依赖注入:到底用哪个?
两件武器功能有重叠,给你一张决策表:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 认证、取当前用户 | Depends |
按路由精确控制、带类型、自动进文档 |
| 取数据库会话/连接 | Depends(yield 版) |
自动收尾关连接,生命周期对齐请求 |
| 分页/通用查询参数解析 | Depends |
可声明参数、自动校验、可复用 |
| 权限分级(admin/普通) | Depends(嵌套) |
依赖套依赖,组合清晰 |
| 全局日志、请求计时 | 中间件 | 真·全局、和具体路由无关 |
| 统一改写所有响应头 | 中间件 | 拿得到最终 response 对象 |
| 全局异常兜底 | 异常处理器(@app.exception_handler) |
比中间件更专门,详见后续 |
一句话经验:和「某些路由需要什么」相关的,用 Depends;和「所有请求都要做、且与业务无关」相关的,用中间件。 FastAPI 官方也是这个倾向——能用 Depends 就别用中间件。
八、前端新手最易踩的坑汇总
-
Depends(fn())写成了带括号调用(第三节重点):只写函数名Depends(fn),加了括号等于服务启动时执行一次、结果冻死,认证/DB 全失效。 -
allow_credentials=True还配allow_origins=["*"]:浏览器禁止「带凭证 + 通配来源」组合,带 Cookie 的跨域必然失败。要列具体域名。 -
以为中间件能像
Depends那样按路由挂:@app.middleware("http")是全局的,对所有请求生效。想「只保护部分路由」请用Depends,别试图在中间件里写if request.url.path == ...硬判断。 -
中间件里忘了
return response:Express 调next()就行,FastAPI 中间件必须把await call_next(request)拿到的response返回出去,否则请求挂起/报错。 -
在
async def依赖里调同步阻塞代码:和第 17 篇说的一样,依赖函数若是async def,里面别调time.sleep或同步阻塞 IO,会卡住事件循环。纯同步的依赖直接用普通def,FastAPI 会自动丢到线程池跑,反而更安全。 -
同一请求里依赖被重复执行的误解:默认情况下,同一个请求内多个路由参数依赖了同一个函数,FastAPI 只执行一次并缓存结果(
use_cache=True)。别担心get_current_user被调好几遍——不会。
九、总结
- 先建直觉:你早就用过「中间件」了:FastAPI 中间件必须 return response,而不是像 Express 那样调 next() 就完事——它更像 Koa 的 async (ctx, next) => { await next() } 风格(拿到下游结果再返回)。 -> call_next 返回的是一个 Response 对象,FastAPI 已经帮你把业务返回值序列化好了,你拿到的是成品响应,不是裸数据。 -> 真正的重头戏不是中间件,而是下面这个 Express 里没有的东西——依赖注入 Depends。
- 依赖注入 Depends:把「准备工作」抽成可复用的函数:FastAPI 的解法是依赖注入(Dependency Injection,DI):你把「准备工作」写成一个普通函数,路由函数在参数里用 Depends(那个函数) 声明「我需要它的结果」,FastAPI 会在调用路由前自动执行依赖函数、把返回值作为参数注入进来。
- Depends(fn) vs Depends(fn()):最容易踩的前端坑:这是前端新手最高频的错误,单独拎出来讲。
- 用 Depends 做认证:解析当前登录用户:认证是 Depends 最典型的战场。
- 依赖的进阶玩法:嵌套与资源收尾:这是中间件做不到的优雅。
- CORS:跨域,前端工程师的老朋友:后端要做的是主动放行:通过响应头告诉浏览器「这些来源我允许」。
学完自测
选择所有正确答案;提交后逐项核对判断依据。