路由
本指南介绍 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 会匹配该片段中传入的任何值。
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 上下文文档
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 对象访问。
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"(字符串形式)。
多个路由参数
你可以在单个路由中包含多个参数。每个参数必须具有唯一名称,并用 / 分隔。
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,捕获两个值。
可选路由参数
有时某个参数并非总是必需的。你可以在名称后追加 ? 将其标记为可选。可选参数必须是路由模式中的最后一段。
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 的所有剩余片段捕获为数组。它使用 * 定义,并且必须出现在模式的最后。
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 用于类型转换。
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 |
import router from '@adonisjs/core/services/router'
router
.get('/posts/:id', ({ params }) => {
console.log(typeof params.id) // 'number'
})
.where('id', router.matchers.number())
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())
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)时非常有用。
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 | 删除资源 | 删除博客文章、移除用户账户 |
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 方法或指定自定义动词:
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 接收器或全捕获调试路由)非常有用。
路由中间件
中间件是在路由处理器之前执行的函数,允许你运行诸如身份验证检查、日志记录、限流或请求转换等代码。可以将中间件想象成请求在到达主处理器之前通过的一系列检查点。
另请参阅: 中间件指南
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())
你也可以直接附加内联中间件。
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 方法为路由提供唯一名称。
使用控制器时,路由会根据 控制器+方法 名称自动命名。
另请参阅: URL 构建器
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 设置
- 命名约定——用于组织路由名称的命名空间前缀
- 域名——具有子域名路由的多租户应用
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 版本控制、管理后台区域,或在公共路径片段下组织相关资源特别有用。
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.index 和 api.users.index 两个服务于不同目的的路由。
另请参阅: URL 构建器
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')
为分组应用中间件
你可以在分组级别附加中间件。分组中间件在任意路由级中间件之前执行,形成一条管道:共享逻辑先运行,随后是路由特定的逻辑。
另请参阅: 中间件指南
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 模式和路由名称。
另请参阅: 资源驱动控制器
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() 按主机名对路由分组:
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.com和us.yourapp.com
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() 以更简洁的方式实现。当你只是显示一个静态或简单的动态页面时,这避免了使用控制器的需要。
第一个参数是视图模板名称,可选的第二个参数是传递给视图的数据。
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 组件。
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 或创建别名很有用。
import router from '@adonisjs/core/services/router'
// 重定向到命名路由
router.on('/posts').redirectToRoute('articles.index')
// 重定向到静态 URL
router.on('/posts').redirectToPath('https://medium.com/my-blog')
转发与重写参数
如果你的路由带有动态参数,你可以将它们转发到目标路由,或使用特定值重写它们。
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 路由,id 为 42。当他们访问 /featured 时,他们会被重定向到 id 为 1 的路由。
添加查询字符串
重定向也可以通过 qs 选项包含查询字符串:
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 访问当前匹配的路由。这对于调试、审计、日志记录,或实现路由感知逻辑(如面包屑或活动导航)很有用。
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():
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 如何匹配路由
路由按照你注册的顺序进行匹配。当请求到来时,路由器会按顺序检查每个路由,直到找到第一个匹配项,然后停止搜索并执行该路由的处理器。
这种顺序匹配意味着当模式重叠时,路由顺序至关重要。如果你在具有相同前缀的静态路由之前定义了动态路由,动态路由将捕获本应发给静态路由的请求。
路由顺序很重要:始终在动态路由之前定义静态路由。当有人访问 /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}`
})
快速规则:将路由从最具体到最不具体排序。静态片段始终优先于动态片段,因此静态路由必须放在前面。
// 将所有静态操作路由分组在一起
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 错误。
另请参阅: 异常处理指南
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 的 Router、Route 和 RouteGroup 等类可以使用宏(macro)或 getter 进行扩展。这允许你添加行为类似原生 API 的自定义方法,在构建可复用包或为路由层添加组织特定的约定时很有用。
如果你是宏和 getter 概念的新手,请阅读 扩展 AdonisJS 指南。
Router
直接将方法或属性添加到 Router 类:
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
扩展单个路由实例:
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
扩展路由分组:
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
扩展资源路由:
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()):
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
})
下一步
既然你已经了解了路由,你可以: