路由

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() 注册的路由列表按顺序从上到下遍历:

  1. 主机/端口过滤 — 如果 hosts 或 ports 与请求不匹配,该路由会被立即跳过。
  2. 路径匹配 — 路由的 path(以及每个 extraPath)会与请求路径进行比较,并在此过程中解析 {param}/:param 占位符以及通配符 * 后缀。
  3. 如果路径匹配,但 methods 不包含请求所用的 HTTP 方法,该路由会被视为未找到,匹配会继续尝试下一个路由。
  4. 如果该路由有 children,一旦父路由的路径前缀匹配,Finch 就会递归进入子路由——父路由上的 auth 和 middlewares 仍会先运行,并应用于每一个子路由。
  5. 第一个完全匹配(路径、方法、主机、端口、身份验证、权限、中间件)的路由会处理该请求;不会再尝试其他路由。
  6. 如果没有任何路由匹配,Finch 会回退到从 publicDir 提供静态文件;如果该文件也不存在,则返回 404。

由于匹配采用"先匹配者获胜"的规则,因此顺序很重要:应将更具体的路由放在可能吞并它们的更宽泛的通配符路由之前。