Skip to content

路由与终结点

本章只讲四件事:

  1. 如何注册终结点
  2. 如何使用路由分组
  3. 请求如何从匹配走到执行
  4. 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()

其中:

  • mapGetmapDelete 会给终结点附加明确的 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/items
  • GET /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 或自定义检查时,顺序应该是:

  1. useRouting()
  2. 你的中间件
  3. mapGet / mapPost / mapGroup

路由模式

RoutePattern 常用语法如下。

基础模式

  1. 字面量路径:/users/me
  2. 标准参数:/users/{id}
  3. 可选参数:/users/{id?}
  4. 内联默认值:/{controller=Home}/{action=Index}
  5. 单星 catch-all:/files/{*path}
  6. 双星 catch-all:/files/{**path}

参数约束

  1. 单个约束:/users/{id:int}
  2. 多个约束:/{id:int:regex(^\\d+$)}
  3. 带括号参数:/{v:range(1,100)}
  4. 约束与默认值组合:/{id:int=1}

复合段

  1. 前缀字面量:/v{version}
  2. 后缀字面量:/{name}.html
  3. 多参数复合段:/{a}-{b}
  4. 可选尾部扩展名:/{name}.{ext?}

使用限制

  • 前导 / 会被规范化处理
  • ~/ 合法,单独 ~ 非法
  • 不能出现连续 /
  • catch-all 只能出现在最后一个段
  • 含 catch-all 的段不能再混合其它部分
  • 可选参数必须位于段尾;在复合段里,它前面只能是 .
  • 同一个段里不能出现连续参数,如 {a}{b}
  • 同一个模板里不能定义重名参数