路由与终结点
本章只讲四件事:
- 如何注册终结点
- 如何使用路由分组
- 请求如何从匹配走到执行
RoutePattern支持哪些常用语法
最小示例
推荐写法是直接注册终结点:
cangjie
import soulsoft_web_http.*
import soulsoft_web_hosting.*
import soulsoft_web_routing.*
import soulsoft_extensions_injection.*
main(args: Array<String>): Int64 {
let builder = WebHost.createBuilder(args)
builder.services.addRouting()
let host = builder.build()
host.mapGet("/hello") { context =>
context.response.write("hello")
}
host.run()
return 0
}在 WebHost 场景下,host.run() 时如果发现已经注册了终结点,会自动补齐 useRouting() 和 useEndpoints()。
只有当你需要在“路由之后、终结点执行之前”插入授权、CORS 或其它中间件时,才需要手动调用 useRouting();useEndpoints() 一般不需要显式调用。
构建终结点
HTTP 方法映射
cangjie
let app = builder.build()
app.mapGet("/items") { ctx =>
ctx.response.write("GET items")
}
app.mapPost("/items") { ctx =>
ctx.response.write("POST items")
}
app.mapPut("/items/{id}") { ctx =>
ctx.response.write("PUT item")
}
app.mapPatch("/items/{id}") { ctx =>
ctx.response.write("PATCH item")
}
app.mapDelete("/items/{id}") { ctx =>
ctx.response.write("DELETE item")
}
app.map("/health") { ctx =>
ctx.response.write("ok")
}
app.run()其中:
mapGet到mapDelete会给终结点附加明确的 HTTP 方法约束。map不限制请求方法。- 同一路径可以按不同方法注册多个终结点。
路由分组
分组的意义不是只省一个前缀,而是把一组接口作为一个整体组织起来。
它通常表达三类共享信息:
- 共享前缀,例如
/api、/admin - 共享策略,例如统一授权、统一 CORS
- 共享元数据,例如标签、审计标记、自定义 metadata
如果一组接口本来就属于同一个边界,更合理的写法是先建组,再把共同约定挂到组上:
cangjie
import soulsoft_web_authorization.*
import soulsoft_web_cors.*
let admin = app.mapGroup("/admin")
admin.requireAuthorization(["admin"]).requireCors("admin-api")
admin.mapGet("/users") { ctx =>
ctx.response.write("users")
}
admin.mapPost("/users") { ctx =>
ctx.response.write("create user")
}这样做的重点是:
- 前缀只写一次
- 共享策略只写一次
- 新增组内终结点时会自动继承组约定
基础分组:
cangjie
let api = app.mapGroup("/api")
api.mapGet("/hello") { ctx =>
ctx.response.write("hello")
}
api.mapGet("/users/{id}") { ctx =>
let id = ctx.request.routeValues.get("id") ?? ""
ctx.response.write("user: ${id}")
}嵌套分组:
cangjie
let v1 = app.mapGroup("/api/v1")
let items = v1.mapGroup("/items")
items.mapGet("") { ctx =>
ctx.response.write("list")
}
items.mapGet("/{id}") { ctx =>
let id = ctx.request.routeValues.get("id") ?? ""
ctx.response.write("detail: ${id}")
}上面最终对应:
GET /api/v1/itemsGET /api/v1/items/{id}
除了现成的 requireAuthorization(...)、requireCors(...),组还支持通用约定:
cangjie
let admin = app.mapGroup("/admin")
admin.add { builder =>
builder.metadata.add("requires-admin")
}这个 add(...) 表达的是:组内所有终结点在构建时,都统一追加这条约定。
时序图
执行链路只要抓住一句话:
Endpoint是匹配后的结果对象,里面带着 delegate 和 metadata(metadata可以理解为mvc中action上的注解)EndpointRoutingMiddleware负责匹配EndpointMiddleware负责执行- 两者之间的空档留给授权、CORS 等中间件
只需要记住四条:
- 路由中间件匹配成功后,不会立刻执行终结点,而是继续往后传。
- 匹配成功后,请求上下文里会保存一个
Endpoint对象。 - 后续中间件可以通过
context.getEndpoint()读取这个Endpoint,再决定是否做授权、CORS 或其它处理。 - 真正调用
Endpoint.delegate的是EndpointMiddleware。
这也是为什么当你要插入授权、CORS 或自定义检查时,顺序应该是:
useRouting()- 你的中间件
mapGet/mapPost/mapGroup
路由模式
RoutePattern 常用语法如下。
基础模式
- 字面量路径:
/users/me - 标准参数:
/users/{id} - 可选参数:
/users/{id?} - 内联默认值:
/{controller=Home}/{action=Index} - 单星 catch-all:
/files/{*path} - 双星 catch-all:
/files/{**path}
参数约束
- 单个约束:
/users/{id:int} - 多个约束:
/{id:int:regex(^\\d+$)} - 带括号参数:
/{v:range(1,100)} - 约束与默认值组合:
/{id:int=1}
复合段
- 前缀字面量:
/v{version} - 后缀字面量:
/{name}.html - 多参数复合段:
/{a}-{b} - 可选尾部扩展名:
/{name}.{ext?}
使用限制
- 前导
/会被规范化处理 ~/合法,单独~非法- 不能出现连续
/ - catch-all 只能出现在最后一个段
- 含 catch-all 的段不能再混合其它部分
- 可选参数必须位于段尾;在复合段里,它前面只能是
. - 同一个段里不能出现连续参数,如
{a}{b} - 同一个模板里不能定义重名参数