路由
Finch 中的路由使用 FinchRoute 对象定义,这些对象收集在一个路由函数中。路由函数通过 app.addRouting() 注册,并在每个传入请求时被调用。
路由函数
import 'package:finch/finch_route.dart';
import '../controllers/home_controller.dart';
final homeController = HomeController();
Future<List<FinchRoute>> getWebRoute() async {
return [
FinchRoute(
key: 'root',
path: '/',
methods: Methods.ONLY_GET,
index: homeController.index,
),
];
}
在 app.dart 中注册:
app.addRouting(getWebRoute);
FinchRoute 参数
| 参数 | 类型 | 描述 |
|---|---|---|
key |
String |
唯一路由标识符。用于生成 URL($e.routeUrl('key')) |
path |
String |
URL 路径段。支持 {param} 或 :param 以及通配符 * |
methods |
List<String> |
允许的 HTTP 方法。使用 Methods 常量 |
index |
Future<String> Function()? |
处理函数(无 rq 参数;使用 Context.rq 或控制器方法) |
controller |
Controller? |
控制器实例。未设置 index 时会调用其 index() |
children |
List<FinchRoute> |
嵌套子路由。路径相对于父路由 |
extraPath |
List<String> |
映射到同一路由的额外路径 |
auth |
AuthController? |
此路由的身份验证守卫 |
permissions |
List<String> |
身份验证通过后检查的权限字符串 |
middlewares |
List<Middleware> |
在处理函数之前执行的中间件链 |
hosts |
List<String> |
限制到特定主机名(['*'] 表示全部) |
ports |
List<int> |
限制到特定端口([] 表示全部) |
params |
Map<String, dynamic> |
默认模板参数 |
excludePaths |
List<String> |
从此路由中排除的子路径 |
apiDoc |
Future<ApiDoc>? Function()? |
用于 Swagger 的 API 文档 |
widget |
String |
要直接渲染的模板文件路径,无需控制器或 index(参见下方的 Widget 路由) |
title |
String |
页面标题,可在模板中通过 {{ $e.pageTitle }} 获取 |
每个路由只应使用 widget、index、controller 三者之一。如果设置了多个,会优先检查 widget,然后是 index,最后才回退到 controller.index()。
HTTP 方法
// Methods 类中的便捷常量:
Methods.ONLY_GET // ['GET']
Methods.ONLY_POST // ['POST']
Methods.ONLY_PUT // ['PUT']
Methods.ONLY_DELETE // ['DELETE']
Methods.GET_POST // ['POST', 'GET']
Methods.ALL // 所有标准 HTTP 方法
// 或自定义列表:
methods: [Methods.GET, Methods.POST, Methods.DELETE]
路径参数
使用 {name} 语法捕获 URL 段。在控制器中使用 rq.getParam('name') 读取:
FinchRoute(
key: 'users.show',
path: 'users/{id}', // or 'users/:id'
methods: Methods.ONLY_GET,
index: userController.show,
),
注意: 动态路径段可以使用 {param} 或 :param 语法定义。两者都受支持且功能相同。
// 在 UserController 中:
Future<String> show() async {
var id = rq.getParam('id');
return rq.renderData(data: {'id': id});
}
Widget 路由
当一个路由只需要渲染模板而不需要任何自定义逻辑时,可以将 widget 设置为模板路径,而不必编写控制器:
FinchRoute(
key: 'about',
path: 'about',
title: 'About Us',
widget: 'pages/about',
),
widget 的检查优先于 index 和 controller,并且——与这两者不同——它不会先等待该路由自身的 auth 运行;只会检查从已通过身份验证的父路由继承来的权限。如果某个 widget 路由自身需要身份验证,应将 auth 附加到父路由上,而不要依赖 widget 路由本身的 auth 字段,或者改用 index/controller 以便正常运行身份验证检查。
通配符路径与排除项
以 * 结尾的路径会匹配该路由本身及其下的每一个子路径——这对通配处理函数非常有用,例如单页应用的兜底路由或通用的文件代理:
FinchRoute(
key: 'spa.fallback',
path: 'app/*',
index: homeController.spaIndex,
excludePaths: ['app/api'], // 该子路径仍会继续尝试匹配其他路由
),
excludePaths 列出了不应被通配符吞并的子路径,因此通配符之下更具体的路由(或静态文件)仍然可以被访问到。
嵌套路由(children)
子路由路径相对于父路由路径:
FinchRoute(
key: 'example',
path: 'example',
index: homeController.redirectToRoot,
children: [
FinchRoute(
key: 'example.form.get',
path: 'form',
methods: Methods.ONLY_GET,
index: homeController.exampleForm,
),
FinchRoute(
key: 'example.form.post',
path: 'form',
methods: Methods.ONLY_POST,
index: authController.loginPost,
),
FinchRoute(
key: 'example.panel',
path: 'panel',
methods: Methods.ALL,
auth: authController,
permissions: ['admin'],
index: homeController.exampleAuth,
),
],
),
额外路径
extraPath 将额外的 URL 前缀映射到同一路由及其子路由:
FinchRoute(
key: 'root.mysql',
path: 'example/mysql',
extraPath: ['api/example/mysql'],
methods: Methods.GET_POST,
index: homeController.exampleMysql,
),
创建多个相似路由
FinchRoute.makeList 可以创建多个共享相同配置、仅路径不同的路由——非常适合用于别名或几乎相同的接口:
var routes = FinchRoute.makeList(
paths: ['users', 'members', 'people'],
methods: Methods.ONLY_GET,
controller: UserController(),
auth: AppAuthController(),
permissions: ['user.read'],
key: 'user.alias', // 会生成 'user.alias.1'、'user.alias.2'、'user.alias.3'
);
中间件
为路由附加一个或多个 Middleware 实例(或使用链式的 .middleware() 调用),可以在处理函数执行之前运行请求过滤逻辑——例如日志记录、请求头检查、速率限制、CORS 以及类似的横切关注点。完整的 API 及其相对于 auth 和 permissions 的执行顺序,请参阅 Middleware Guide。
主机和端口过滤
将路由限制到特定主机或端口:
FinchRoute(
key: 'root.localhost',
path: 'example/host',
hosts: ['localhost'],
ports: [80, 8085],
index: homeController.renderLocalhost,
methods: Methods.ALL,
),
FinchRoute(
key: 'root.host',
path: 'example/host',
ports: [80, 8085],
hosts: ['127.0.0.1'],
index: homeController.render127001,
methods: Methods.ALL,
),
身份验证守卫
附加 AuthController 来保护路由。如果 auth() 返回 false,请求会在到达处理函数之前被拒绝:
FinchRoute(
key: 'admin.panel',
path: 'admin/panel',
auth: AppAuthController(),
permissions: ['admin'],
index: adminController.panel,
),
实现详情请参阅 Auth Controller。
路由缓存
使用 .cache() 扩展来缓存响应:
FinchRoute(
key: 'root.route',
path: 'example/route',
methods: Methods.ONLY_GET,
index: homeController.exampleRoute,
).cache(
cacheDuration: Duration(minutes: 10),
cacheType: [CacheParam.path, CacheParam.method, CacheParam.language],
cacheSource: CacheSource.file,
),
完整详情请参阅 Route Cache。
从路由键生成 URL
在模板中,使用 $e.routeUrl('key') 生成 URL:
<a href="{{ $e.routeUrl('example.panel') }}">Panel</a>
<a href="{{ $e.routeUrl('users.show', {'id': user.id}) }}">View User</a>
获取所有路由
在运行时获取完整的路由列表:
var routes = await app.getAllRoutes();
匹配是如何工作的
对于每一个传入的请求,Finch 会按照你通过 app.addRouting() 注册的路由列表按顺序从上到下遍历:
- 主机/端口过滤 — 如果
hosts或ports与请求不匹配,该路由会被立即跳过。 - 路径匹配 — 路由的
path(以及每个extraPath)会与请求路径进行比较,并在此过程中解析{param}/:param占位符以及通配符*后缀。 - 如果路径匹配,但
methods不包含请求所用的 HTTP 方法,该路由会被视为未找到,匹配会继续尝试下一个路由。 - 如果该路由有
children,一旦父路由的路径前缀匹配,Finch 就会递归进入子路由——父路由上的auth和middlewares仍会先运行,并应用于每一个子路由。 - 第一个完全匹配(路径、方法、主机、端口、身份验证、权限、中间件)的路由会处理该请求;不会再尝试其他路由。
- 如果没有任何路由匹配,Finch 会回退到从
publicDir提供静态文件;如果该文件也不存在,则返回404。
由于匹配采用"先匹配者获胜"的规则,因此顺序很重要:应将更具体的路由放在可能吞并它们的更宽泛的通配符路由之前。