扩展框架
本指南介绍如何为 AdonisJS 添加自定义功能。你将学到如何:
- 使用 macros 为框架类添加自定义方法
- 使用 getters 创建计算属性
- 通过 TypeScript 声明合并确保类型安全
- 在应用中组织扩展代码
- 扩展特定的框架模块,如 Hash、Session 和身份验证
概述
AdonisJS 提供了一个强大的扩展系统,让你无需修改框架源代码即可为框架类添加自定义方法和属性。这意味着你可以为 HttpRequest 类增加自定义验证逻辑,为 HttpResponse 类添加工具方法,或扩展任何其他框架类以满足应用的特定需求。
扩展系统建立在两个核心概念之上:macros(自定义方法)和 getters(计算属性)。两者都在运行时添加,并通过声明合并与 TypeScript 无缝集成,让你在编辑器中获得完整的类型安全和自动补全。
这套相同的扩展 API 也贯穿于 AdonisJS 自己的第一方包中,使其成为构建可复用功能的成熟模式。无论你是为应用添加几个辅助方法,还是构建一个与社区共享的包,扩展系统都提供了一种整洁、类型安全的方式来增强框架。
为什么要扩展框架?
在深入机制之前,让我们先理解你何时以及为何想要扩展框架类。
如果没有扩展,你就需要在整个应用中反复编写相同的逻辑。例如,检查请求是否期望 JSON 响应:
export default class PostsController {
async index({ request, response }: HttpContext) {
// Repeated in every action that returns different formats
const acceptHeader = request.header('accept', '')
const wantsJSON = acceptHeader.includes('application/json') ||
acceptHeader.includes('+json')
if (wantsJSON) {
return response.json({ posts: [] })
}
return view.render('posts/index')
}
}
使用 macro,你只需编写一次这段逻辑,就能在任何地方使用它:
import { HttpRequest } from '@adonisjs/core/http'
/**
* Check if the request expects a JSON response based on Accept header
*/
HttpRequest.macro('wantsJSON', function (this: HttpRequest) {
const firstType = this.types()[0]
if (!firstType) {
return false
}
return firstType.includes('/json') || firstType.includes('+json')
})
export default class PostsController {
async index({ request, response }: HttpContext) {
if (request.wantsJSON()) {
return response.json({ posts: [] })
}
return view.render('posts/index')
}
}
在以下情况下,扩展是理想之选:
- 你有在整个应用中复用的、与框架相关的逻辑
- 你想保持 AdonisJS 流畅的 API 风格
- 你正在构建一个与框架深度集成的包
- 你需要带有自动补全支持的类型安全自定义功能
理解 macros 与 getters
在开始添加扩展之前,让我们先厘清 macros 和 getters 是什么,以及各自何时使用。
Macros 是你添加到类中的自定义方法。它们的工作方式与普通方法一样,可以接受参数、执行计算并返回值。当你需要接受输入或执行操作的功能时,请使用 macros。
Getters 是看起来像普通属性的计算属性。它们按需计算,并可选地缓存结果。对于不需要参数的只读派生数据,请使用 getters。
Macros 和 getters 都使用声明合并——一种 TypeScript 特性,它扩展现有的类型定义以包含你的自定义添加内容。这确保你的扩展具有完整的类型安全和自动补全支持。
在底层,AdonisJS 使用 macroable 包实现这一功能。如果你想了解实现细节,可以参阅该包的文档。
创建你的第一个 macro
让我们一步步构建一个简单的 macro。我们将为 HttpRequest 类添加一个方法,用于检查传入请求是否来自移动设备。
-
创建扩展文件
创建一个专门的文件来存放所有框架扩展。这能将你的扩展代码集中组织在一处。
src/extensions.ts// This file contains all framework extensions for your application文件可以随意命名,但
extensions.ts清晰地表明了它的用途。 -
导入要扩展的类
导入你想为其添加功能的框架类。在我们的示例中,将扩展
HttpRequest类。src/extensions.tsimport { HttpRequest } from '@adonisjs/core/http' -
添加 macro 方法
使用
macro方法添加你的自定义功能。该方法会将类实例作为this接收,让你可以访问类现有的所有属性和方法。src/extensions.tsimport { HttpRequest } from '@adonisjs/core/http' HttpRequest.macro('isMobile', function (this: HttpRequest) { /** * Get the User-Agent header, defaulting to empty string if not present */ const userAgent = this.header('user-agent', '') /** * Check if the User-Agent contains common mobile identifiers */ return /mobile|android|iphone|ipad|phone/i.test(userAgent) })function (this: HttpRequest)这一语法很重要,因为它为你提供了正确的this上下文。这里不要使用箭头函数,因为它们不会保留this绑定。 -
添加 TypeScript 类型定义
使用声明合并将你的新方法告知 TypeScript。将以下内容添加到扩展文件的末尾。
src/extensions.tsdeclare module '@adonisjs/core/http' { interface HttpRequest { isMobile(): boolean } }declare module中的模块路径必须与你使用的导入路径完全一致。接口名称必须与类名完全一致。 -
在提供者中加载扩展
在某个服务提供者的
boot方法中导入你的扩展文件,以确保应用启动时注册这些扩展。providers/app_provider.tsexport default class AppProvider { async boot() { await import('../src/extensions.ts') } } -
使用你的 macro
现在你的 macro 已在整个应用中可用,并具有完整的类型安全和自动补全。
app/controllers/home_controller.tsimport type { HttpContext } from '@adonisjs/core/http' export default class HomeController { async index({ request, view }: HttpContext) { /** * TypeScript knows about isMobile() and provides autocomplete */ if (request.isMobile()) { return view.render('mobile/home') } return view.render('home') } }
创建你的第一个 getter
Getters 是像普通属性一样工作但按需计算的计算属性。让我们为 HttpRequest 类添加一个 getter,用于提供请求路径的清理版本。
import { HttpRequest } from '@adonisjs/core/http'
HttpRequest.getter('cleanPath', function (this: HttpRequest) {
/**
* Get the current URL path
*/
const path = this.url()
/**
* Remove trailing slashes and convert to lowercase
*/
return path.replace(/\/+$/, '').toLowerCase()
})
declare module '@adonisjs/core/http' {
interface HttpRequest {
cleanPath: string // Note: property, not a method
}
}
请注意,类型声明与 macros 不同。Getters 是属性而非方法,因此在类型定义中不要包含 ()。
你可以像使用普通属性一样使用 getters:
export default class LogMiddleware {
async handle({ request, logger }: HttpContext, next: NextFn) {
/**
* Access the getter like a property, not a method
*/
logger.info('Request path: %s', request.cleanPath)
await next()
}
}
Getter 回调不能是异步的,因为 JavaScript getters 在设计上是同步的。如果你需要异步计算,请改用 macro。
单例 getters
默认情况下,getters 每次被访问时都会重新计算其值。对于代价高昂的计算,你可以将 getter 设为单例,它会在首次计算后缓存结果。
import { HttpRequest } from '@adonisjs/core/http'
/**
* The third parameter (true) makes this a singleton getter
*/
HttpRequest.getter('ipAddress', function (this: HttpRequest) {
/**
* Check for proxy headers first, fall back to direct IP
* This only runs once per request instance
*/
return this.header('x-forwarded-for') ||
this.header('x-real-ip') ||
this.ips()[0] ||
this.ip()
}, true)
declare module '@adonisjs/core/http' {
interface HttpRequest {
ipAddress: string
}
}
使用单例 getters 时,该函数对类的每个实例只执行一次,并为该实例缓存返回值:
const ip1 = request.ipAddress // Executes the getter function
const ip2 = request.ipAddress // Returns cached value, doesn't re-execute
const ip3 = request.ipAddress // Still returns cached value
当计算出的值在实例生命周期内不会改变时,请使用单例 getters。例如,请求的 IP 地址在单个 HTTP 请求期间不会改变,因此缓存它是合理的。
不要将单例 getters 用于可能变化的值,例如基于可变状态的计算属性。
何时使用 macros,何时使用 getters
在 macros 和 getters 之间的选择取决于你的用例。这里是一份实用指南。
在需要以下情形时使用 macros:
- 接受参数
- 执行带有副作用的操作
- 根据输入返回不同的值
- 执行异步操作
/**
* Macro example: Accepts a role parameter
*/
HttpRequest.macro('hasRole', function (this: HttpRequest, role: string) {
const user = this.ctx.auth.user
return user?.role === role
})
// Usage: request.hasRole('admin')
在需要以下情形时使用 getters:
- 提供计算出的只读属性
- 从现有属性计算派生数据
- 缓存代价高昂的计算(使用单例)
- 保持类属性式的 API
/**
* Getter example: Computed property with no parameters
*/
HttpRequest.getter('isAuthenticated', function (this: HttpRequest) {
return this.ctx.auth.isAuthenticated
})
// Usage: request.isAuthenticated
两者可以共存于同一个类中。请根据你想提供的 API 来选择。
理解声明合并
声明合并是 TypeScript 了解你运行时扩展的方式。正确处理它对类型安全至关重要。
declare module 语句中的模块路径必须与你用来导入该类的路径完全一致:
// If you import like this:
import { HttpRequest } from '@adonisjs/core/http'
// You must declare like this (exact same path):
declare module '@adonisjs/core/http' {
interface HttpRequest {
isMobile(): boolean
}
}
为什么这很重要:TypeScript 使用模块路径来确定要与哪个类型定义合并。
会发生什么:如果路径不匹配,TypeScript 将无法识别你的扩展。即使你的代码运行正确,你也会看到诸如 “Property 'isMobile' does not exist on type 'Request'” 之类的错误。
解决方案:编写声明时,务必复制完全相同的导入路径:
// ✅ Correct: Paths match
import { HttpRequest } from '@adonisjs/core/http'
declare module '@adonisjs/core/http' { ... }
// ❌ Wrong: Paths don't match
import { HttpRequest } from '@adonisjs/core/http'
declare module '@adonisjs/http-server' { ... }
你可以在同一个 declare module 块中声明多个扩展:
declare module '@adonisjs/core/http' {
interface HttpRequest {
isMobile(): boolean
hasRole(role: string): boolean
cleanPath: string
ipAddress: string
}
}
如果你愿意,也可以将它们拆分到多个块中:
declare module '@adonisjs/core/http' {
interface HttpRequest {
isMobile(): boolean
}
}
declare module '@adonisjs/core/http' {
interface HttpRequest {
hasRole(role: string): boolean
}
}
两种方式的效果完全相同。请根据你的组织偏好来选择。
常见错误
以下是开发者在扩展框架时最常遇到的问题及其修复方法。
错误:为 macros 使用箭头函数
为什么会失败:箭头函数没有自己的 this 绑定,因此你无法访问类实例。
// ❌ Wrong: Arrow function
HttpRequest.macro('isMobile', () => {
return this.header('user-agent') // `this` is undefined!
})
// ✅ Correct: Regular function
HttpRequest.macro('isMobile', function (this: HttpRequest) {
return this.header('user-agent') // `this` is the Request instance
})
错误:忘记单例参数默认为 false
会发生什么:即使值不会改变,你的 getter 也会在每次被访问时重新计算。
// This executes the function every single time
HttpRequest.getter('expensiveCalculation', function (this: HttpRequest) {
return someExpensiveOperation()
})
// Add true for singleton to cache the result
HttpRequest.getter('expensiveCalculation', function (this: HttpRequest) {
return someExpensiveOperation()
}, true) // Caches after first access
错误:把 getters 当作方法来对待
会发生什么:你会遇到错误,因为 getters 是属性,不是函数。
HttpRequest.getter('ipAddress', function (this: HttpRequest) {
return this.ip()
})
// ❌ Wrong: Calling it like a method
const ip = request.ipAddress()
// ✅ Correct: Accessing it like a property
const ip = request.ipAddress
支持 macros 的类(Macroable classes)
以下框架类支持 macros 和 getters。每一项都包含导入路径和典型用例。
Application
主应用实例。扩展它以添加应用级别的工具。
常见用例:添加自定义环境检查、应用状态 getters,或全局配置访问器。
示例:添加一个 getter 来检查应用是否运行在特定模式下。
import app from '@adonisjs/core/services/app'
app.getter('isProduction', function () {
return this.inProduction
})
HttpRequest
HTTP 请求类。扩展它以添加请求验证或解析逻辑。
常见用例:添加用于检查请求特征、解析自定义头部或验证请求类型的方法。
示例:添加一个方法来检查请求是否为 AJAX 请求。
import { HttpRequest } from '@adonisjs/core/http'
HttpRequest.macro('isAjax', function (this: HttpRequest) {
return this.header('x-requested-with') === 'XMLHttpRequest'
})
HttpResponse
HTTP 响应类。扩展它以添加自定义响应方法或格式化器。
常见用例:添加用于发送格式化响应、设置常用头部或处理特定响应类型的方法。
示例:添加一个用于发送分页 JSON 响应的方法。
import { HttpResponse } from '@adonisjs/core/http'
HttpResponse.macro('paginated', function (this: HttpResponse, data: any, meta: any) {
return this.json({ data, meta })
})
HttpContext
传递给路由处理器和中间件的 HTTP 上下文类。扩展它以添加上下文级别的工具。
常见用例:添加结合请求和响应逻辑的辅助方法,或为常见操作添加快捷方式。
示例:添加一个获取当前用户或失败的方法。
import { HttpContext } from '@adonisjs/core/http'
HttpContext.macro('getCurrentUser', async function (this: HttpContext) {
return await this.auth.getUserOrFail()
})
Route
单个路由实例。扩展它以添加自定义路由配置方法。
常见用例:添加用于应用常见中间件模式、设置路由元数据,或以特定方式配置路由的方法。
示例:添加一个方法将路由标记为需要身份验证。
import { Route } from '@adonisjs/core/http'
Route.macro('protected', function (this: Route) {
return this.middleware('auth')
})
RouteGroup
路由组实例。扩展它以添加自定义的组级别配置。
常见用例:添加用于对一组路由应用常见模式的方法。
示例:添加一个方法为组应用 API 版本控制。
import { RouteGroup } from '@adonisjs/core/http'
RouteGroup.macro('apiVersion', function (this: RouteGroup, version: number) {
return this.prefix(`/api/v${version}`)
})
ExceptionHandler
全局异常处理器。扩展它以添加自定义错误处理方法。
常见用例:添加用于处理特定错误类型或格式化错误响应的方法。
示例:添加一个方法以一致的方式处理验证错误。
import { ExceptionHandler } from '@adonisjs/core/http'
ExceptionHandler.macro('handleValidationError', function (error: any) {
return this.ctx.response.status(422).json({ errors: error.messages })
})
MultipartFile
上传的文件实例。扩展它以添加文件验证或处理方法。
常见用例:添加用于验证文件类型、处理图像或生成缩略图的方法。
示例:添加一个方法来检查文件是否为图像。
import { MultipartFile } from '@adonisjs/core/bodyparser'
MultipartFile.macro('isImage', function (this: MultipartFile) {
return this.type?.startsWith('image/')
})
扩展特定模块
除了 macros 和 getters,许多 AdonisJS 模块还提供专门的扩展 API,用于添加自定义实现。它们是为更复杂的集成而设计的,例如自定义驱动或加载器。
以下模块可以用自定义实现进行扩展:
- 创建自定义哈希驱动 - 添加对自定义密码哈希算法的支持
- 创建自定义 session 存储 - 将会话存储在 MongoDB 或 Redis 等自定义后端中
- 创建自定义社交 auth 驱动 - 添加内置之外的 OAuth 提供商
- 添加自定义 REPL 方法 - 用自定义命令扩展 REPL
- 创建自定义翻译加载器 - 从自定义来源加载翻译
- 创建自定义翻译格式化器 - 使用自定义逻辑格式化翻译
这些扩展点超越了简单的方法和属性,让你能够将自定义功能深度集成到框架中。
下一步
现在你已了解如何扩展框架,你可以: