路由
你的网站或 Web 应用的用户会访问不同的 URL,例如 /、/about 或 /posts/1。要让这些 URL 能够正常工作,你必须定义路由。
在 AdonisJS 中,路由定义在 start/routes.ts 文件内。一个路由由URI 模式与处理该特定路由请求的处理器组合而成。例如:
import router from '@adonisjs/core/services/router'
router.get('/', () => {
return 'Hello world from the home page.'
})
router.get('/about', () => {
return 'This is the about page.'
})
router.get('/posts/:id', ({ params }) => {
return `This is post with id ${params.id}`
})
上面示例中的最后一个路由使用了动态 URI 模式。:id 是告诉路由接收方 id 可以是任意值的一种方式。我们称它们为路由参数。
查看已注册的路由列表
你可以运行 list:routes 命令来查看应用注册的路由列表。
node ace list:routes
此外,如果你正在使用我们的官方 VSCode 扩展,也可以从 VSCode 的活动栏查看路由列表。
路由参数
路由参数允许你定义可以接收动态值的 URI。每个参数会捕获一个 URI 片段的值,你可以在路由处理器中访问这个值。
路由参数始终以冒号 : 开头,后跟参数名称。
import router from '@adonisjs/core/services/router'
router.get('/posts/:id', ({ params }) => {
return params.id
})
| URL | Id |
|---|---|
/posts/1 | 1 |
/posts/100 | 100 |
/posts/foo-bar | foo-bar |
一个 URI 也可以接收多个参数。每个参数应当有唯一的名称。
import router from '@adonisjs/core/services/router'
router.get('/posts/:id/comments/:commentId', ({ params }) => {
console.log(params.id)
console.log(params.commentId)
})
| URL | Id | Comment Id |
|---|---|---|
/posts/1/comments/4 | 1 | 4 |
/posts/foo-bar/comments/22 | foo-bar | 22 |
可选参数
路由参数也可以通过在其名称末尾追加问号 ? 来变为可选。可选参数应当放在必填参数之后。
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}`
})
通配符参数
要捕获 URI 的所有片段,你可以定义一个通配符参数。通配符参数使用特殊的 * 关键字指定,并且必须定义在最后的位置。
import router from '@adonisjs/core/services/router'
router.get('/docs/:category/*', ({ params }) => {
console.log(params.category)
console.log(params['*'])
})
| URL | Category | Wildcard param |
|---|---|---|
/docs/http/context | http | ['context'] |
/docs/api/sql/orm | api | ['sql', 'orm'] |
参数匹配器
路由并不知道你想要接收的参数数据的格式。例如,URI 为 /posts/foo-bar 和 /posts/1 的请求会匹配到同一个路由。不过,你可以使用参数匹配器显式校验参数值。
匹配器通过链式调用 where() 方法注册。第一个参数是参数名称,第二个参数是匹配器对象。
在下面的示例中,我们定义一个正则表达式来校验 id 是否为有效的数字。如果校验失败,该路由将被跳过。
import router from '@adonisjs/core/services/router'
router
.get('/posts/:id', ({ params }) => {})
.where('id', {
match: /^[0-9]+$/,
})
除了 match 正则之外,你还可以定义 cast 函数将参数值转换为正确的数据类型。在这个示例中,我们可以将 id 转换为数字。
import router from '@adonisjs/core/services/router'
router
.get('/posts/:id', ({ params }) => {
console.log(typeof params.id)
})
.where('id', {
match: /^[0-9]+$/,
cast: (value) => Number(value),
})
内置匹配器
路由内置了以下针对常用数据类型的辅助方法。
import router from '@adonisjs/core/services/router'
// 校验 id 为数字 + 转换为 number 数据类型
router.where('id', router.matchers.number())
// 校验 id 为有效的 UUID
router.where('id', router.matchers.uuid())
// 校验 slug 匹配给定的 slug 正则:regexr.com/64su0
router.where('slug', router.matchers.slug())
全局匹配器
路由匹配器可以在路由实例上全局定义。除非在路由级别被显式覆盖,否则全局匹配器会应用到所有路由上。
import router from '@adonisjs/core/services/router'
// 全局匹配器
router.where('id', router.matchers.uuid())
router
.get('/posts/:id', () => {})
// 在路由级别覆盖
.where('id', router.matchers.number())
HTTP 方法
router.get() 方法创建一个响应 GET HTTP 方法 的路由。类似地,你可以使用以下方法为不同的 HTTP 方法注册路由。
import router from '@adonisjs/core/services/router'
// GET 方法
router.get('users', () => {})
// POST 方法
router.post('users', () => {})
// PUT 方法
router.put('users/:id', () => {})
// PATCH 方法
router.patch('users/:id', () => {})
// DELETE 方法
router.delete('users/:id', () => {})
你可以使用 router.any() 方法创建一个响应所有标准 HTTP 方法的路由。
router.any('reports', () => {})
最后,你可以使用 router.route() 方法为自定义 HTTP 方法创建路由。
router.route('/', ['TRACE'], () => {})
路由处理器
路由处理器通过返回响应或抛出异常来中止请求,从而处理请求。
处理器可以是一个内联回调(如本指南所示),也可以是对某个控制器方法的引用。
router.post('users', () => {
// Do something
})
路由处理器可以是异步函数,AdonisJS 会自动处理 Promise 的解析。
在下面的示例中,我们导入 UsersController 类并将其绑定到路由。在 HTTP 请求期间,AdonisJS 会使用 IoC 容器创建控制器类的实例,并执行 store 方法。
另请参阅:关于 控制器 的专门指南。
const UsersController = () => import('#controllers/users_controller')
router.post('users', [UsersController, 'store'])
路由中间件
你可以通过链式调用 use() 方法在路由上定义中间件。该方法接受内联回调或对具名中间件的引用。
以下是定义路由中间件的最小示例。我们建议你阅读 关于中间件的专门指南,以了解所有可用的选项以及中间件的执行流程。
router
.get('posts', () => {
console.log('Inside route handler')
return 'Viewing all posts'
})
.use((_, next) => {
console.log('Inside middleware')
return next()
})
路由标识符
每个路由都有一个唯一的标识符,你可以在应用的其他地方用它来引用该路由。例如,你可以使用 URL 构建器 为路由生成 URL,或者使用 response.redirect() 方法跳转到该路由。
默认情况下,路由模式即路由标识符。不过,你可以使用 route.as 方法为路由指定一个唯一、易记的名称。
router.get('users', () => {}).as('users.index')
router.post('users', () => {}).as('users.store')
router.delete('users/:id', () => {}).as('users.delete')
现在你可以在模板中或使用 URL 构建器,通过路由名称来构造 URL。
const url = router.builder().make('users.delete', [user.id])
<form
method='POST'
action="{{
route('users.delete', [user.id], { formAction: 'delete' })
}}"
></form>
路由分组
路由分组提供了一种便捷层,用于对组内的嵌套路由进行批量配置。你可以使用 router.group 方法创建一组路由。
router.group(() => {
/**
* All routes registered inside the callback
* are part of the surrounding group
*/
router.get('users', () => {})
router.post('users', () => {})
})
路由组可以相互嵌套,AdonisJS 会根据所应用设置的行为合并或覆盖属性。
router.group(() => {
router.get('posts', () => {})
router.group(() => {
router.get('users', () => {})
})
})
为组内的路由添加前缀
组内路由的 URI 模式可以使用 group.prefix 方法添加前缀。下面的示例将为 /api/users 和 /api/payments 这两个 URI 模式创建路由。
router
.group(() => {
router.get('users', () => {})
router.get('payments', () => {})
})
.prefix('/api')
对于嵌套组,前缀会从外层组应用到内层组。下面的示例将为 /api/v1/users 和 /api/v1/payments 这两个 URI 模式创建路由。
router
.group(() => {
router
.group(() => {
router.get('users', () => {})
router.get('payments', () => {})
})
.prefix('v1')
})
.prefix('api')
为组内的路由命名
与为路由模式添加前缀类似,你也可以使用 group.as 方法为组内的路由名称添加前缀。
在添加前缀之前,组内的路由必须已经拥有名称。
router
.group(() => {
router
.get('users', () => {})
.as('users.index') // 最终名称 - api.users.index
})
.prefix('api')
.as('api')
对于嵌套组,名称会从外层组向内层组添加前缀。
router
.group(() => {
router
.get('users', () => {})
.as('users.index') // api.users.index
router
.group(() => {
router
.get('payments', () => {})
.as('payments.index') // api.commerce.payments.index
})
.as('commerce')
})
.prefix('api')
.as('api')
为组内的路由应用中间件
你可以使用 group.use 方法为组内的路由分配中间件。组中间件会在组内各个路由上应用的中间件之前执行。
对于嵌套组,最外层组的中间件会最先运行。换句话说,组会将中间件前置到路由中间件栈中。
另请参阅:中间件指南
router
.group(() => {
router
.get('posts', () => {})
.use((_, next) => {
console.log('logging from route middleware')
return next()
})
})
.use((_, next) => {
console.log('logging from group middleware')
return next()
})
为特定域名注册路由
AdonisJS 允许你在特定的域名下注册路由。当你有一个映射到多个域名、并希望为每个域名提供不同路由的应用时,这会很有用。
在下面的示例中,我们定义了两组路由。
- 针对任意域名/主机名解析的路由。
- 当域名/主机名与预定义的域名值匹配时才匹配的路由。
router.group(() => {
router.get('/users', () => {})
router.get('/payments', () => {})
})
router.group(() => {
router.get('/articles', () => {})
router.get('/articles/:id', () => {})
}).domain('blog.adonisjs.com')
一旦你部署了应用,带有显式域名的组下的路由,仅当请求的主机名是 blog.adonisjs.com 时才会匹配。
动态子域名
你可以使用 group.domain 方法指定动态子域名。与路由参数类似,域名的动态片段以冒号 : 开头。
在下面的示例中,tenant 片段接受任意子域名,你可以使用 HttpContext.subdomains 对象访问其值。
router
.group(() => {
router.get('users', ({ subdomains }) => {
return `Listing users for ${subdomains.tenant}`
})
})
.domain(':tenant.adonisjs.com')
从路由渲染 Edge 视图
如果你有一个仅渲染视图的路由处理器,可以使用 router.on().render() 方法。这是渲染视图的便捷快捷方式,无需定义显式的处理器。
render 方法接受要渲染的 edge 模板名称。可选地,你可以将模板数据作为第二个参数传入。
route.on().render() 方法仅在你已配置 Edge service provider 时存在。
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' })
从路由渲染 Inertia 视图
如果你使用的是 Inertia.js 适配器,可以使用 router.on().renderInertia() 方法来渲染 Inertia 视图。这是渲染视图的便捷快捷方式,无需定义显式的处理器。
renderInertia 方法接受要渲染的 Inertia 组件名称。可选地,你可以将组件数据作为第二个参数传入。
route.on().renderInertia() 方法仅在你已配置 Inertia service provider 时存在。
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' })
从路由重定向
如果你要定义一个将请求重定向到另一个路径或路由的路由处理器,可以使用 router.on().redirect() 或 router.on().redirectToPath() 方法。
redirect 方法接受路由标识符。而 redirectToPath 方法接受一个静态路径/URL。
import router from '@adonisjs/core/services/router'
// 重定向到路由
router.on('/posts').redirect('/articles')
// 重定向到 URL
router.on('/posts').redirectToPath('https://medium.com/my-blog')
转发参数
在下面的示例中,原始请求中 id 的值会被用来构造 /articles/:id 路由。因此,如果有一个对 /posts/20 的请求,它将被重定向到 /articles/20。
import router from '@adonisjs/core/services/router'
router.on('/posts/:id').redirect('/articles/:id')
显式指定参数
你也可以将路由参数作为第二个参数显式指定。这种情况下,当前请求中的参数将被忽略。
import router from '@adonisjs/core/services/router'
// 始终重定向到 /articles/1
router.on('/posts/:id').redirect('/articles/:id', {
id: 1
})
携带查询字符串
重定向 URL 的查询字符串可以在 options 对象中定义。
import router from '@adonisjs/core/services/router'
router.on('/posts').redirect('/articles', {
qs: {
limit: 20,
page: 1,
}
})
当前请求的路由
可以使用 HttpContext.route 属性访问当前请求的路由。它包含路由模式、名称、对其中间件存储的引用,以及对路由处理器的引用。
router.get('payments', ({ route }) => {
console.log(route)
})
你也可以使用 request.matchesRoute 方法检查当前请求是否针对某个特定路由。该方法接受路由 URI 模式或路由名称。
router.get('/posts/:id', ({ request }) => {
if (request.matchesRoute('/posts/:id')) {
}
})
router
.get('/posts/:id', ({ request }) => {
if (request.matchesRoute('posts.show')) {
}
})
.as('posts.show')
你也可以对多个路由进行匹配。只要找到第一个匹配项,方法就会返回 true。
if (request.matchesRoute(['/posts/:id', '/posts/:id/comments'])) {
// do something
}
AdonisJS 如何匹配路由
路由按照它们在路由文件中注册的顺序依次匹配。我们从最顶部的路由开始匹配,并在找到第一个匹配的路由时停止。
如果你有两个相似的路由,必须先注册最具体的那个路由。
在下面的示例中,对 URL /posts/archived 的请求将由第一个路由(即 /posts/:id)处理,因为动态参数 id 会捕获 archived 这个值。
import router from '@adonisjs/core/services/router'
router.get('posts/:id', () => {})
router.get('posts/archived', () => {})
这种行为可以通过将路由重新排序来修复——把最具体的路由放在带有动态参数的路由之前。
router.get('posts/archived', () => {})
router.get('posts/:id', () => {})
处理 404 请求
当为当前请求 URL 找不到匹配的路由时,AdonisJS 会抛出一个 404 异常。
要向用户显示一个 404 页面,你可以在 全局异常处理器 中捕获 E_ROUTE_NOT_FOUND 异常并渲染一个模板。
// 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) {
if (error instanceof errors.E_ROUTE_NOT_FOUND) {
return ctx.view.render('errors/404')
}
return super.handle(error, ctx)
}
}
URL 构建器
你可以使用 URL 构建器为应用中预定义的路由创建 URL。例如,在 Edge 模板内创建一个表单 action URL,或者构造一个用于跳转到另一个路由的 URL。
router.builder 方法会创建 URL 构建器 类的实例,你可以使用构建器流畅的 API 来查找路由并为其创建 URL。
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
router
.get('posts/:id', [PostsController, 'show'])
.as('posts.show')
你可以按如下方式为 posts.show 路由生成 URL。
import router from '@adonisjs/core/services/router'
router
.builder()
.params([1])
.make('posts.show') // /posts/1
router
.builder()
.params([20])
.make('posts.show') // /posts/20
参数可以指定为位置参数的数组。或者你也可以将它们定义为键值对。
router
.builder()
.params({ id: 1 })
.make('posts.show') // /posts/1
定义查询参数
可以使用 builder.qs 方法定义查询参数。该方法接受一个键值对对象,并将其序列化为查询字符串。
router
.builder()
.qs({ page: 1, sort: 'asc' })
.make('posts.index') // /posts?page=1&sort=asc
查询字符串使用 qs 这个 npm 包进行序列化。你可以在 config/app.ts 文件的 http 对象下配置其设置。
http: defineConfig({
qs: {
stringify: {
//
}
}
})
为 URL 添加前缀
你可以使用 builder.prefixUrl 方法为输出添加一个基础 URL 前缀。
router
.builder()
.prefixUrl('https://blog.adonisjs.com')
.params({ id: 1 })
.make('posts.show')
生成签名 URL
签名 URL 是带有签名查询字符串的 URL。该签名用于验证 URL 自生成后是否被篡改过。
例如,你有一个用于让用户退订邮件简报的 URL。该 URL 包含 userId,可能如下所示。
/unsubscribe/231
为了防止有人将用户 id 从 231 改为其他值,你可以对这个 URL 进行签名,并在处理该路由的请求时校验签名。
router.get('unsubscribe/:id', ({ request, response }) => {
if (!request.hasValidSignature()) {
return response.badRequest('Invalid or expired URL')
}
// 移除订阅
}).as('unsubscribe')
你可以使用 makeSigned 方法来创建一个签名 URL。
router
.builder()
.prefixUrl('https://blog.adonisjs.com')
.params({ id: 231 })
.makeSigned('unsubscribe')
签名 URL 的过期时间
你可以使用 expiresIn 选项生成在指定时长后过期的签名 URL。该值可以是一个以毫秒为单位的数字,也可以是一个时间表达式字符串。
router
.builder()
.prefixUrl('https://blog.adonisjs.com')
.params({ id: 231 })
.makeSigned('unsubscribe', {
expiresIn: '3 days'
})
禁用路由查找
URL 构建器会对传给 make 和 makeSigned 方法的路由标识符执行一次路由查找。
如果你想为在 AdonisJS 应用之外定义的路由创建 URL,可以禁用路由查找,并将路由模式直接传给 make 和 makeSigned 方法。
router
.builder()
.prefixUrl('https://your-app.com')
.disableRouteLookup()
.params({ token: 'foobar' })
.make('/email/verify/:token') // /email/verify/foobar
为特定域名下的路由生成 URL
你可以使用 router.builderForDomain 方法为在特定域名下注册的路由创建 URL。该方法接受你在定义路由时使用的路由模式。
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
router.group(() => {
router
.get('/posts/:id', [PostsController, 'show'])
.as('posts.show')
}).domain('blog.adonisjs.com')
你可以为 blog.adonisjs.com 域名下的 posts.show 路由创建 URL,如下所示。
router
.builderForDomain('blog.adonisjs.com')
.params({ id: 1 })
.make('posts.show')
在模板中生成 URL
你可以在模板中使用 route 和 signedRoute 方法,借助 URL 构建器来生成 URL。
另请参阅:Edge 辅助方法参考
<a href="{{ route('posts.show', [post.id]) }}">
View post
</a>
<a href="{{
signedRoute('unsubscribe', [user.id], {
expiresIn: '3 days',
prefixUrl: 'https://blog.adonisjs.com'
})
}}">
Unsubscribe
</a>
扩展路由
你可以使用宏(macro)和 getter 为不同的路由类添加自定义属性。如果你还不熟悉宏的概念,请务必先阅读 扩展 AdonisJS 指南。
以下是你可以扩展的类列表。
Router
Router 类包含用于创建路由、路由组或路由资源的高层方法。该类的实例通过 router 服务提供。
import { Router } from '@adonisjs/core/http'
Router.macro('property', function (this: Router) {
return value
})
Router.getter('propertyName', function (this: Router) {
return value
})
declare module '@adonisjs/core/http' {
export interface Router {
property: valueType
}
}
Route
Route 类表示单个路由。一旦你调用 router.get、router.post 及其他类似方法,就会创建 Route 类的实例。
import { Route } from '@adonisjs/core/http'
Route.macro('property', function (this: Route) {
return value
})
Router.getter('property', function (this: Route) {
return value
})
declare module '@adonisjs/core/http' {
export interface Route {
property: valueType
}
}
RouteGroup
RouteGroup 类表示一组路由。一旦你调用 router.group 方法,就会创建 RouteGroup 类的实例。
在你的宏或 getter 实现中,可以使用 this.routes 属性访问组内的路由。
import { RouteGroup } from '@adonisjs/core/http'
RouteGroup.macro('property', function (this: RouteGroup) {
return value
})
RouteGroup.getter('property', function (this: RouteGroup) {
return value
})
declare module '@adonisjs/core/http' {
export interface RouteGroup {
property: valueType
}
}
RouteResource
RouteResource 类表示一组针对某个资源的路由。一旦你调用 router.resource 方法,就会创建 RouteResource 类的实例。
在你的宏或 getter 实现中,可以使用 this.routes 属性访问资源的路由。
import { RouteResource } from '@adonisjs/core/http'
RouteResource.macro('property', function (this: RouteResource) {
return value
})
RouteResource.getter('property', function (this: RouteResource) {
return value
})
declare module '@adonisjs/core/http' {
export interface RouteResource {
property: valueType
}
}
BriskRoute
BriskRoute 类表示没有显式处理器的路由。一旦你调用 router.on 方法,就会创建 BriskRoute 类的实例。
你可以在宏或 getter 中调用 this.setHandler 方法来指定一个路由处理器。
import { BriskRoute } from '@adonisjs/core/http'
BriskRoute.macro('property', function (this: BriskRoute) {
return value
})
BriskRouter.getter('property', function (this: BriskRoute) {
return value
})
declare module '@adonisjs/core/http' {
export interface BriskRoute {
property: valueType
}
}