路由

本指南介绍 AdonisJS 应用中的路由。你将学习如何:

  • 为不同的 HTTP 方法定义路由
  • 处理带验证的动态路由参数
  • 将路由按共享配置分组
  • 生成 RESTful 资源路由
  • 为路由应用中间件
  • 注册特定域名的路由
  • 使用 URL 构建器构建类型安全的 URL
  • 用自定义功能扩展路由器

概述

路由将传入的 HTTP 请求连接到应用中的特定处理器。当用户访问 //about/posts/1 等 URL 时,路由器会检查 HTTP 方法和 URL 模式,然后执行相应的处理器函数。这是你的应用响应 Web 请求的基础。

一个路由由三个主要部分组成:

  • HTTP 方法——请求的类型(GET、POST、PUT、DELETE 等)
  • URI 模式——应当匹配的 URL 路径,可以包含动态片段
  • 处理器——处理请求并返回响应的函数或 控制器 方法

路由还可以包含 中间件 ,用于身份验证、限流,或任何应在处理器执行前运行的逻辑。你的应用处理的每个 HTTP 请求都会经过路由系统,因此理解路由的工作原理以及如何有效组织路由至关重要。

基础示例

在 AdonisJS 中,路由使用 router 服务定义在 start/routes.ts 文件中。

路由处理器是路由匹配时运行的函数。它接收 HTTP 上下文,并可以返回字符串、对象,或调用服务来生成响应。

以下示例展示了静态路由,以及使用 :id 的动态路由,:id 会匹配该片段中传入的任何值。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/', () => 'Hello world from the home page.')

router.get('/about', () => 'This is the about page.')

router.get('/posts/:id', ({ params }) => {
  return `This is post with id ${params.id}`
})

router.post('/users', async ({ request }) => {
  const data = request.all()
  await createUser(data)
  return 'User created successfully'
})

使用控制器作为路由处理器

除了内联回调,你还可以将请求处理委托给控制器方法。控制器有助于将逻辑组织到专门的类中,并使处理器可以在多个路由间复用。

另请参阅: 控制器指南HTTP 上下文文档

start/routes.ts
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'

router.get('/posts/:id', [controllers.Posts, 'show'])

查看已注册的路由

你可以使用下面的 Ace CLI 命令查看应用注册的所有路由。这对于调试、验证路由名称,或检查附加到特定路由的中间件非常有用。

node ace list:routes

如果你使用的是 官方 VSCode 扩展 ,路由也可以直接在 VSCode 的活动栏中查看,方便你浏览应用的各个端点。

路由参数

路由参数允许 URL 的某些部分为动态,从特定片段中捕获值,并在处理器中可用。每个参数匹配该位置上的任意值,可通过 ctx.params 访问。

基础路由参数

基础路由参数使用冒号 : 后跟名称来定义。捕获的值可以在处理器中通过 params 对象访问。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/posts/:id', ({ params }) => {
  return `Showing post with id: ${params.id}`
})

当有人访问 /posts/42 时,值 42 被捕获,params.id 等于 "42"(字符串形式)。

多个路由参数

你可以在单个路由中包含多个参数。每个参数必须具有唯一名称,并用 / 分隔。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/posts/:id/comments/:commentId', ({ params }) => {
  console.log(params.id)        // Post ID
  console.log(params.commentId) // Comment ID
})

这会匹配 /posts/42/comments/7 这样的 URL,捕获两个值。

可选路由参数

有时某个参数并非总是必需的。你可以在名称后追加 ? 将其标记为可选。可选参数必须是路由模式中的最后一段。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/posts/:id?', ({ params }) => {
  if (!params.id) {
    return 'Showing all posts'
  }
  return `Showing post with id ${params.id}`
})

该路由同时匹配 /posts/posts/42

通配符路由参数

通配符参数将 URL 的所有剩余片段捕获为数组。它使用 * 定义,并且必须出现在模式的最后。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/docs/:category/*', ({ params }) => {
  console.log(params.category)  // 'guides'
  console.log(params['*'])      // ['sql', 'orm', 'query-builder']
})

当有人访问 /docs/guides/sql/orm/query-builder 时,通配符捕获 ['sql', 'orm', 'query-builder']

通配符参数适用于:

  • 包含嵌套区块的文档路径
  • 具有目录结构的文件浏览器
  • 需要捕获任意深度的全捕获路由

路由参数验证

默认情况下,路由参数接受任何值,并且始终以字符串形式传递给处理器。你可以使用 .where() 方法限制哪些值有效,并自动将其转换为正确的类型。

当参数验证失败时,路由器会跳过该路由并继续搜索其他匹配的路由。这允许你拥有相同模式但具有不同验证规则的多个路由。

为什么要验证参数

如果不进行验证,你需要在每个处理器中手动检查和转换参数。

未使用参数验证
router.get('/posts/:id', ({ params, response }) => {
  if (!/^[0-9]+$/.test(params.id)) {
    return response.badRequest('Invalid ID format')
  }
  const id = Number(params.id)
  // Now use id...
})

使用参数验证后,路由器会在处理器运行前自动处理这些逻辑。

使用参数验证
router
  .get('/posts/:id', ({ params }) => {
    console.log(typeof params.id) // 'number'
    // params.id 已经过验证并转换为 number
  })
  .where('id', {
    match: /^[0-9]+$/,
    cast: (value) => Number(value),
  })

使用参数验证可以:

  • 在查询数据库之前确保 ID 为数字
  • 验证 UUID 是否符合正确格式
  • 验证 slug 是否仅包含 URL 安全的字符
  • 防止无效数据进入处理器
  • 自动将字符串转换为适当的类型(number、boolean 等)

自定义匹配器

.where() 方法接受一个包含两个属性的对象:match 用于验证,cast 用于类型转换。

start/routes.ts
import router from '@adonisjs/core/services/router'

router
  .get('/posts/:id', ({ params }) => {
    console.log(typeof params.id) // 'number'
    console.log(params.id)        // 42(而非 "42")
  })
  .where('id', {
    match: /^[0-9]+$/,              // 仅允许数字
    cast: (value) => Number(value), // 将字符串转换为数字
  })

如果有人访问 /posts/abc,该路由不会匹配,因为 "abc" 未通过正则测试。路由器会继续搜索其他路由,如果没有匹配的则返回 404。

内置匹配器

对于数字、UUID 和 slug 等常见模式,AdonisJS 提供了简写匹配器,可同时处理验证和类型转换。

匹配器验证转换为使用示例
number()仅数字(/^\d+$/number数据库 ID、分页偏移量
uuid()有效的 UUID v4 格式string公开资源标识符、安全 ID
slug()URL 安全的字符串(/^[a-z0-9-_]+$/string对 SEO 友好的 URL、文章 slug
数字 ID
import router from '@adonisjs/core/services/router'

router
  .get('/posts/:id', ({ params }) => {
    console.log(typeof params.id) // 'number'
  })
  .where('id', router.matchers.number())
UUID 标识符
import router from '@adonisjs/core/services/router'

router
  .get('/users/:userId', ({ params }) => {
    console.log(params.userId) // '550e8400-e29b-41d4-a716-446655440000'
  })
  .where('userId', router.matchers.uuid())
URL 友好 slug
import router from '@adonisjs/core/services/router'

router
  .get('/articles/:slug', ({ params }) => {
    console.log(params.slug) // 'getting-started-with-adonisjs'
  })
  .where('slug', router.matchers.slug())

全局匹配器

你可以全局应用匹配器,使每个路由自动继承相同的验证规则。这在大多数路由都遵循某种约定(例如所有 ID 都使用 UUID)时非常有用。

start/routes.ts
import router from '@adonisjs/core/services/router'

// 设置全局默认值:所有 :id 参数必须为 UUID
router.where('id', router.matchers.uuid())

// 这些路由自动继承 UUID 匹配器
router.get('/posts/:id', () => {})
router.get('/users/:id', () => {})

// 为需要数字 ID 的特定路由重写
router
  .get('/categories/:id', () => {})
  .where('id', router.matchers.number())

全局匹配器先被应用,然后路由特定的匹配器会覆盖它们。这种模式有助于保持一致,同时允许在需要的地方设置例外。

HTTP 方法

路由器为每个标准 HTTP 动词提供了专门的方法。在 RESTful 应用中,每个方法对应特定类型的操作。

方法用途常见用例
GET检索资源显示用户列表、展示博客文章、获取数据
POST创建新资源提交表单、创建新用户、上传文件
PUT整体替换资源更新用户资料的所有字段
PATCH部分更新资源仅更新用户的邮箱字段
DELETE删除资源删除博客文章、移除用户账户
start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/users', () => {})           // 列出所有用户
router.post('/users', () => {})          // 创建新用户
router.put('/users/:id', () => {})       // 完全替换用户
router.patch('/users/:id', () => {})     // 更新用户的特定字段
router.delete('/users/:id', () => {})    // 删除用户

匹配多个方法

要匹配所有 HTTP 方法或指定自定义动词:

start/routes.ts
import router from '@adonisjs/core/services/router'

// 匹配 GET、POST、PUT、PATCH、DELETE 以及所有其他方法
router.any('/reports', () => {})

// 匹配特定的自定义 HTTP 方法
router.route('/', ['TRACE'], () => {})
router.route('/api/data', ['GET', 'POST', 'PUT'], () => {})

.any() 方法对于需要响应任意 HTTP 方法的端点(例如 Webhook 接收器或全捕获调试路由)非常有用。

路由中间件

中间件是在路由处理器之前执行的函数,允许你运行诸如身份验证检查、日志记录、限流或请求转换等代码。可以将中间件想象成请求在到达主处理器之前通过的一系列检查点。

另请参阅: 中间件指南

start/routes.ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'

router
  .get('/posts', () => {
    console.log('Inside route handler')
    return 'Viewing all posts'
  })
  .use(middleware.auth())

你也可以直接附加内联中间件。

start/routes.ts
router
  .get('/posts', () => {
    console.log('Inside route handler')
    return 'Viewing all posts'
  })
  .use((ctx, next) => {
    console.log('Inside middleware')
    return next()
  })

路由标识符

每个路由都可以有一个唯一的名称,你可以使用它来生成 URL 或重定向,而无需对路径进行硬编码。这让 URL 更易于维护——如果你更改了路由的路径,使用名称时所有引用都会自动更新。

命名路由对于以下场景至关重要:

  • 在模板中生成 URL 而无需硬编码路径
  • 创建在 URL 变更后仍然有效的重定向
  • 以编程方式构建导航菜单
  • 使用有意义的标识符组织路由

你可以使用 .as 方法为路由提供唯一名称。

Note

使用控制器时,路由会根据 控制器+方法 名称自动命名。

另请参阅: URL 构建器

start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/users', () => {}).as('users.index')
router.post('/users', () => {}).as('users.store')
router.get('/users/:id', () => {}).as('users.show')
router.delete('/users/:id', () => {}).as('users.destroy')

路由分组

路由分组让你能够一次性为多个路由应用共享配置,消除重复并使路由文件更易于维护。如果不使用分组,你需要在每个单独的路由上重复相同的前缀、中间件或命名约定,随着应用的增长,这种重复容易引发错误。

当多个路由共享以下任意一项时,使用路由分组:

  • URL 前缀——API 版本(/v1/v2)、管理后台区域(/admin)或语言代码(/en/es
  • 中间件——身份验证、授权、限流或 CORS 设置
  • 命名约定——用于组织路由名称的命名空间前缀
  • 域名——具有子域名路由的多租户应用
start/routes.ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'

router
  .group(() => {
    router.get('/users', () => {}).as('users.index')
    router.post('/users', () => {}).as('users.store')
    router.get('/posts', () => {}).as('posts.index')
    router.post('/posts', () => {}).as('posts.store')
  })
  .prefix('/api')
  .use(middleware.auth())
  .as('api')

路由前缀

前缀会添加到分组内所有路由的前面。这对于 API 版本控制、管理后台区域,或在公共路径片段下组织相关资源特别有用。

start/routes.ts
import router from '@adonisjs/core/services/router'

router
  .group(() => {
    router.get('/users', () => {})      // 变为:GET /api/users
    router.get('/payments', () => {})   // 变为:GET /api/payments
    router.get('/invoices', () => {})   // 变为:GET /api/invoices
  })
  .prefix('/api')

为分组内的路由命名

当分组内的路由具有名称时,你也可以为它们的名称添加前缀。这会创建有组织的路由命名空间,使哪些路由属于同一组变得清晰。

命名路由分组让 URL 生成更清晰,并帮助你避免应用不同部分之间的命名冲突。例如,你可能同时拥有 web.users.indexapi.users.index 两个服务于不同目的的路由。

另请参阅: URL 构建器

start/routes.ts
import router from '@adonisjs/core/services/router'

router
  .group(() => {
    router.get('/users', () => {}).as('users.index')      // 名称:api.users.index
    router.post('/users', () => {}).as('users.store')     // 名称:api.users.store
    router.get('/posts', () => {}).as('posts.index')      // 名称:api.posts.index
  })
  .prefix('/api')
  .as('api')

为分组应用中间件

你可以在分组级别附加中间件。分组中间件在任意路由级中间件之前执行,形成一条管道:共享逻辑先运行,随后是路由特定的逻辑。

另请参阅: 中间件指南

start/routes.ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'

router
  .group(() => {
    router
      .get('/posts', () => {
        console.log('3. Inside route handler')
      })
      .use((_, next) => {
        console.log('2. Route-level middleware')
        return next()
      })
  })
  .use((_, next) => {
    console.log('1. Group-level middleware')
    return next()
  })

资源路由

资源路由会自动为控制器生成所有标准的 RESTful 路由,无需手动定义每个 CRUD 路由。在构建具有完整 CRUD 接口的传统 Web 应用或 RESTful API 时,这尤其有价值。

这一行代码会生成以下所有路由,并带有遵循 RESTful 约定的正确 HTTP 方法、URL 模式和路由名称。

另请参阅: 资源驱动控制器

start/routes.ts
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'

router.resource('posts', controllers.Posts)
GET /posts
  • 操作: PostsController.index
  • 名称: posts.index
  • 用途: 显示所有文章的列表
GET /posts/create
  • 操作: PostsController.create
  • 名称: posts.create
  • 用途: 显示创建新文章的表单
POST /posts
  • 操作: PostsController.store
  • 名称: posts.store
  • 用途: 存储新创建的文章
GET /posts/:id
  • 操作: PostsController.show
  • 名称: posts.show
  • 用途: 显示单篇文章
GET /posts/:id/edit
  • 操作: PostsController.edit
  • 名称: posts.edit
  • 用途: 显示编辑文章的表单
PUT|PATCH /posts/:id
  • 操作: PostsController.update
  • 名称: posts.update
  • 用途: 更新单篇文章
DELETE /posts/:id
  • 操作: PostsController.destroy
  • 名称: posts.destroy
  • 用途: 删除单篇文章

为特定域名注册路由

有时,你可能希望某些路由只响应特定的域名或子域名。这对于多租户应用、独立的管理后台,或根据主机名提供不同内容非常有用。

使用 .domain() 按主机名对路由分组:

start/routes.ts
import router from '@adonisjs/core/services/router'

router
  .group(() => {
    router.get('/articles', () => {})
    router.get('/articles/:id', () => {})
  })
  .domain('blog.adonisjs.com')

这些路由仅在请求发往 blog.adonisjs.com 时才会响应。发往其他域名的请求不会匹配这些路由。

动态子域名

你可以像路由参数一样在域名中定义动态片段。这对于每个客户都拥有自己子域名的多租户应用至关重要。

将基于域名的路由用于:

  • 具有客户子域名多租户 SaaS 应用
  • 位于 admin.yourapp.com 的独立管理后台
  • 区域站点,如 uk.yourapp.comus.yourapp.com
start/routes.ts
import router from '@adonisjs/core/services/router'

router
  .group(() => {
    router.get('/users', ({ subdomains }) => {
      return `Listing users for ${subdomains.tenant}`
    })
    router.get('/dashboard', ({ subdomains }) => {
      return `Dashboard for ${subdomains.tenant}`
    })
  })
  .domain(':tenant.adonisjs.com')

当有人访问 acme.adonisjs.com/users 时,subdomains.tenant 等于 "acme"。当他们访问 bigcorp.adonisjs.com/users 时,它等于 "bigcorp"

从路由渲染 Edge 视图

如果某个路由的唯一目的是渲染视图而没有任何逻辑,可以使用 router.on().render() 以更简洁的方式实现。当你只是显示一个静态或简单的动态页面时,这避免了使用控制器的需要。

第一个参数是视图模板名称,可选的第二个参数是传递给视图的数据。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.on('/').render('home')
router.on('/about').render('about', { title: 'About us' })
router.on('/contact').render('contact', { title: 'Contact us', email: 'hello@example.com' })

从路由渲染 Inertia 视图

对于 Inertia.js 应用,使用类似的 router.on().renderInertia() 方法,无需控制器即可直接从路由渲染 Inertia 页面。

这会 through Inertia 使用提供的 props 渲染对应的 Vue、React 或 Svelte 组件。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.on('/').renderInertia('home')
router.on('/about').renderInertia('about', { title: 'About us' })
router.on('/contact').renderInertia('contact', { title: 'Contact us' })

从路由重定向

你可以使用 redirectToRoute()redirectToPath() 将一个路径重定向到另一个路径。这对于处理已废弃的 URL、缩短 URL 或创建别名很有用。

start/routes.ts
import router from '@adonisjs/core/services/router'

// 重定向到命名路由
router.on('/posts').redirectToRoute('articles.index')

// 重定向到静态 URL
router.on('/posts').redirectToPath('https://medium.com/my-blog')

转发与重写参数

如果你的路由带有动态参数,你可以将它们转发到目标路由,或使用特定值重写它们。

start/routes.ts
import router from '@adonisjs/core/services/router'

// 将 :id 参数转发到目标路由
router.on('/posts/:id').redirectToRoute('articles.show')

// 使用特定值重写参数
router.on('/featured').redirectToRoute('articles.show', { id: 1 })

当有人访问 /posts/42 时,他们会被重定向到 articles.show 路由,id42。当他们访问 /featured 时,他们会被重定向到 id1 的路由。

添加查询字符串

重定向也可以通过 qs 选项包含查询字符串:

start/routes.ts
import router from '@adonisjs/core/services/router'

router.on('/posts').redirectToRoute('articles.index', {}, {
  qs: { limit: 20, page: 1 },
})

这会重定向到 /articles?limit=20&page=1

访问当前路由

你可以从 HTTP 上下文 通过 ctx.route 访问当前匹配的路由。这对于调试、审计、日志记录,或实现路由感知逻辑(如面包屑或活动导航)很有用。

start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/payments', ({ route }) => {
  console.log(route.pattern)    // '/payments'
  console.log(route.name)       // 'payments.index'(如果已命名)
  console.log(route.methods)    // ['GET']
})

检查路由是否匹配

要检查当前请求是否匹配某个特定的命名路由,可使用 request.matchesRoute()

start/routes.ts
import router from '@adonisjs/core/services/router'

router
  .get('/posts/:id', ({ request }) => {
    if (request.matchesRoute('posts.show')) {
      console.log('Matched posts.show')
      // 针对该路由执行特定操作
    }
  })
  .as('posts.show')

这在中间件或共享逻辑中特别有用,因为你需要根据当前路由采取不同的行为。

AdonisJS 如何匹配路由

路由按照你注册的顺序进行匹配。当请求到来时,路由器会按顺序检查每个路由,直到找到第一个匹配项,然后停止搜索并执行该路由的处理器。

这种顺序匹配意味着当模式重叠时,路由顺序至关重要。如果你在具有相同前缀的静态路由之前定义了动态路由,动态路由将捕获本应发给静态路由的请求。

Note

路由顺序很重要:始终在动态路由之前定义静态路由。当有人访问 /posts/archived 时,先定义的 /posts/:id 模式会匹配并令 id = "archived",而不是让 /posts/archived 路由来处理它。

正确排序路由

下面是不正确排序时会发生的情况:

错误的顺序 - 先定义动态路由
router.get('/posts/:id', ({ params }) => {
  return `Showing post ${params.id}`
  // 访问 /posts/archived 时,这会匹配,params.id = "archived"
})

router.get('/posts/archived', () => {
  return 'Showing archived posts'
  // 上面的路由已经匹配,因此这一路由永远不会执行
})

正确的做法是在动态路由之前定义具体的路由:

正确的顺序 - 静态路由优先
router.get('/posts/archived', () => {
  return 'Showing archived posts'
})

router.get('/posts/trending', () => {
  return 'Showing trending posts'
})

router.get('/posts/:id', ({ params }) => {
  return `Showing post ${params.id}`
})

快速规则:将路由从最具体到最不具体排序。静态片段始终优先于动态片段,因此静态路由必须放在前面。

start/routes.ts
// 将所有静态操作路由分组在一起
router.get('/posts/archived', () => {})
router.get('/posts/trending', () => {})
router.get('/posts/search', () => {})

// 然后定义动态路由
router.get('/posts/:id', () => {})
router.get('/posts/:id/comments', () => {})

处理 404 请求

当没有路由匹配传入的请求时,AdonisJS 会抛出 E_ROUTE_NOT_FOUND 异常。你可以在全局异常处理器中捕获此异常,以渲染自定义的 404 页面或返回结构化的 JSON 错误。

另请参阅: 异常处理指南

app/exceptions/handler.ts
import { errors } from '@adonisjs/core'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'

export default class HttpExceptionHandler extends ExceptionHandler {
  async handle(error: unknown, ctx: HttpContext) {
    /**
     * 对于 API 请求,改为返回 JSON
     */
    if (error instanceof errors.E_ROUTE_NOT_FOUND && ctx.request.accepts(['json'])) {
      return ctx.response.status(404).json({
        error: 'Route not found',
        message: `Cannot ${ctx.request.method()} ${ctx.request.url()}`
      })
    }

    /**
     * 通过渲染自定义 404 页面处理路由未找到错误
     */
    if (error instanceof errors.E_ROUTE_NOT_FOUND) {
      return ctx.view.render('errors/404')
    }

    return super.handle(error, ctx)
  }
}

扩展路由器

AdonisJS 的 RouterRouteRouteGroup 等类可以使用宏(macro)或 getter 进行扩展。这允许你添加行为类似原生 API 的自定义方法,在构建可复用包或为路由层添加组织特定的约定时很有用。

Note

如果你是宏和 getter 概念的新手,请阅读 扩展 AdonisJS 指南。

Router

直接将方法或属性添加到 Router 类:

start/routes.ts
import { Router } from '@adonisjs/core/http'

Router.macro('health', function (this: Router) {
  this.get('/health', () => {
    return { status: 'ok' }
  })
})

Router.getter('version', function (this: Router) {
  return '1.0.0'
})

Route

扩展单个路由实例:

start/routes.ts
import { Route } from '@adonisjs/core/http'

Route.macro('tracking', function (this: Route, eventName: string) {
  return this.use((ctx, next) => {
    console.log(`Tracking event: ${eventName}`)
    return next()
  })
})

Route.getter('isPublic', function (this: Route) {
  return !this.middleware.includes('auth')
})

RouteGroup

扩展路由分组:

start/routes.ts
import { RouteGroup } from '@adonisjs/core/http'

RouteGroup.macro('apiVersion', function (this: RouteGroup, version: string) {
  return this.prefix(`/api/${version}`)
})

RouteGroup.getter('routeCount', function (this: RouteGroup) {
  return this.routes.length
})

RouteResource

扩展资源路由:

start/routes.ts
import { RouteResource } from '@adonisjs/core/http'

RouteResource.macro('softDeletes', function (this: RouteResource) {
  return this.except(['destroy'])
})

RouteResource.getter('hasDestroy', function (this: RouteResource) {
  return !this.except.includes('destroy')
})

BriskRoute

扩展渲染快捷方式(router.on().render()):

start/routes.ts
import { BriskRoute } from '@adonisjs/core/http'

BriskRoute.macro('withLayout', function (this: BriskRoute, layout: string) {
  return this.render(this.view, { ...this.data, layout })
})

BriskRoute.getter('hasData', function (this: BriskRoute) {
  return Object.keys(this.data).length > 0
})

下一步

既然你已经了解了路由,你可以: