路由 · 后端开发
Web 服务器拿到一个请求后,到底做了什么?
课程目录为什么需要它
你用的每个应用都在和一个后端通信:一个接收请求、查找数据、返回答案的程序。Flask、FastAPI、Express、Spring 看起来各不相同,但底层都在做同样的三件事。看懂了这三件事,任何后端框架都好读了。
直觉理解
请求带着一个方法和一个路径到达:GET /users/42。服务器会:
- 路由:找到方法和路径模式都匹配的处理函数。
/users/<id>匹配/users/42,得到id = "42"。 - 执行处理函数,干活。
- 响应:返回一个状态码和响应体(通常是 JSON)。
| 状态码 | 意思 |
|---|---|
| 200 OK | 这是你要的东西 |
| 201 Created | 新东西我建好了 |
| 400 Bad Request | 你的输入有问题 |
| 401 / 403 | 你是谁?/ 你没有权限 |
| 404 Not Found | 没有这个东西 |
| 405 Method Not Allowed | 这个路径存在,但不支持这个方法 |
| 500 Internal Server Error | 服务器出错了 |
在 Flask 里同样的路由是这样写的,匹配由框架替你完成:
@app.get("/users/<id>")
def get_user(id):
return {"id": int(id)}本课程里我们自己写匹配,用返回 (状态码, 响应体) 的普通函数。
定义
路由
A method plus a path pattern, connected to the handler that serves it: GET /users/<id> → get_user.一个方法加一个路径模式,连着负责处理它的函数:GET /users/<id> → get_user。
处理函数
The function that does the work for one route and returns the response.为某个路由干活并返回响应的函数。
状态码
A three-digit number at the start of every response. 2xx success, 4xx the client's mistake, 5xx the server's.每个响应开头的三位数字。2xx 成功,4xx 是客户端的错,5xx 是服务器的错。
示例
一个处理函数
动手试试处理函数查找数据,然后选一个状态码。
USERS = {1: "Ada", 2: "Linus"}
def get_user(user_id):
if user_id not in USERS:
return 404, {"error": "no such user"}
return 200, {"id": user_id, "name": USERS[user_id]}
print(get_user(1))
print(get_user(7))(200, {'id': 1, 'name': 'Ada'})
(404, {'error': 'no such user'})
- L4先处理找不到的情况。
- L8找到了:200,以及用户字典(它会变成 JSON)。
(200, {'id': 1, 'name': 'Ada'}) - L9没找到:404,带一条错误信息。
(404, {'error': 'no such user'})
匹配路径模式
动手试试把两边都切成几段,逐段比较;<名字> 这样的段负责取值。
def match(pattern, path):
want = pattern.strip("/").split("/")
got = path.strip("/").split("/")
if len(want) != len(got):
return None
params = {}
for w, g in zip(want, got):
if w.startswith("<") and w.endswith(">"):
params[w[1:-1]] = g
elif w != g:
return None
return params
print(match("/users/<id>", "/users/42"))
print(match("/users/<id>", "/posts/42"))
print(match("/users/<id>/posts", "/users/7/posts"))
print(match("/users/<id>", "/users/7/posts")){'id': '42'}
None
{'id': '7'}
None
- L2"/users/<id>" 变成 ["users", "<id>"]。
- L4段数不同:不可能匹配。
- L8<名字> 段匹配任何内容并记下来。
- L10固定的段必须相等。
- L14id 被取出来,是文字 "42"。
{'id': '42'}
一个迷你路由器
动手试试一张路由表,和一个找到第一个匹配项的循环。
def match(pattern, path):
want = pattern.strip("/").split("/")
got = path.strip("/").split("/")
if len(want) != len(got):
return None
params = {}
for w, g in zip(want, got):
if w.startswith("<") and w.endswith(">"):
params[w[1:-1]] = g
elif w != g:
return None
return params
def health(params):
return 200, {"ok": True}
def get_user(params):
return 200, {"id": int(params["id"])}
ROUTES = [
("GET", "/health", health),
("GET", "/users/<id>", get_user),
]
def handle(method, path):
for m, pattern, handler in ROUTES:
params = match(pattern, path)
if params is not None and m == method:
return handler(params)
return 404, {"error": "not found"}
print(handle("GET", "/health"))
print(handle("GET", "/users/5"))
print(handle("GET", "/nope"))(200, {'ok': True})
(200, {'id': 5})
(404, {'error': 'not found'})
- L20每条路由:方法、模式、处理函数。
- L26按顺序尝试每条路由。
- L30没有任何匹配。
常见错误
什么都返回 200
如果错误也返回 200 加 {"error": …},客户端、缓存和监控都会以为成功了。要用和实际情况对应的状态码。
忘了路径参数是文字
在 /users/42 里,id 收到的是字符串 "42"。用 int() 转换,失败时返回 400 或 404。
路由的顺序
第一个匹配的路由胜出。/users/me 要放在 /users/<id> 前面,否则 "me" 会被当成 id。
小测验
1. 客户端发送 POST /orders,订单保存成功。哪个状态码最合适?
查看答案
答案:A
201 表示创建了新资源;响应体里通常带着它和它的新 id。
2. 这段代码会输出什么?
def match(pattern, path):
want = pattern.strip("/").split("/")
got = path.strip("/").split("/")
if len(want) != len(got):
return None
params = {}
for w, g in zip(want, got):
if w.startswith("<") and w.endswith(">"):
params[w[1:-1]] = g
elif w != g:
return None
return params
print(match("/shop/<item>/reviews", "/shop/lamp/reviews"))
print(match("/shop/<item>", "/shop"))查看答案
答案:A
lamp 落进 <item>。"/shop" 只有一段,而模式有两段,所以不匹配。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出给请求找路由
准确写出它打印的内容。
- 改需求该是 405,不是 404
路径存在但方法不对时,服务器应该返回 405
{"error": "method not allowed"},而不是 404。修改handle,做到:- 路径和方法都匹配 → 返回处理函数的响应
- 路径能匹配某条路由,但没有这个方法的路由 →
405 - 没有任何路由匹配这个路径 →
404
- 从零写带类型的路径参数
写一个
match(pattern, path),和例子里的一样,再加一个功能:写成<int:名字>的段只匹配数字,而且值要转换成int。match("/users/<int:id>", "/users/42") # {'id': 42} match("/users/<int:id>", "/users/me") # None match("/tags/<name>", "/tags/python") # {'name': 'python'}
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| Backend | 后端 | The server side of an app: it handles requests and stores data. |
| Route | 路由 | A method and path pattern connected to a handler. |
| Status code | 状态码 | The number that says how a request went: 200, 404, 500… |