Assembler 钩子
本指南介绍 AdonisJS 中的 Assembler 钩子。你将学习如何:
- 注册钩子以响应开发、测试和构建期间的声明周期事件
- 在 watch 模式下对文件更改做出反应
- 钩入路由扫描管道
- 使用 IndexGenerator 生成 barrel 文件和类型声明
- 创建自定义代码生成工作流
概述
Assembler 是 AdonisJS 的构建工具层,它将你的应用作为一个子进程来管理。它负责启动开发服务器、运行测试以及创建生产构建。钩子让你能够接入这个生命周期,以在特定时刻运行自定义操作,例如在控制器发生变化时生成 barrel 文件、在路由被扫描时创建类型声明,或在服务器启动时显示自定义信息。
由于 Assembler 在与你的 AdonisJS 应用不同的进程中运行,钩子无法访问 IoC 容器、路由服务或数据库连接等框架特性。相反,钩子会接收到专门构建的工具,例如用于代码生成的 IndexGenerator 以及用于路由分析的扫描器实例。
Assembler 钩子的常见用例包括:为惰性加载控制器生成 barrel 文件、从路由元数据创建类型安全的 API 客户端、在文件更改时运行代码生成器,以及显示自定义的启动信息。
钩子参考
下表列出了所有可用的钩子、它们的触发时机,以及它们接收的参数。
| Hook | Triggered by | Description |
|---|---|---|
init | DevServer, TestRunner, Bundler | 执行的第一个钩子。用于初始化任务 |
devServerStarting | DevServer | 在子进程启动之前 |
devServerStarted | DevServer | 在子进程运行之后 |
testsStarting | TestRunner | 在测试开始执行之前 |
testsFinished | TestRunner | 在测试完成之后 |
buildStarting | Bundler | 在生产构建开始之前 |
buildFinished | Bundler | 在生产构建完成之后 |
fileChanged | DevServer, TestRunner | 在 watch 模式下文件被修改时 |
fileAdded | DevServer, TestRunner | 在文件被创建时 |
fileRemoved | DevServer, TestRunner | 在文件被删除时 |
routesCommitted | DevServer | 当应用注册了路由时 |
routesScanning | DevServer | 在路由类型扫描开始之前 |
routesScanned | DevServer | 在路由类型扫描完成之后 |
创建和注册钩子
钩子在 adonisrc.ts 文件的 hooks 属性下注册。每个钩子接受一个惰性加载的导入数组,使你能够将钩子逻辑拆分到单独的文件中,并仅在需要时加载它们。
import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({
hooks: {
devServerStarted: [() => import('./hooks/on_server_started.ts')],
fileChanged: [() => import('./hooks/on_file_changed.ts')],
},
})
钩子文件必须导出一个默认函数,该函数接收钩子的参数。每个钩子都有一个来自 @adonisjs/core/app 的带类型辅助函数,为参数提供完整的 TypeScript 支持。
import { hooks } from '@adonisjs/core/app'
export default hooks.devServerStarted((devServer, info, instructions) => {
/**
* info.host - 服务器绑定的主机地址
* info.port - 服务器运行的端口号
* instructions - 用于显示格式化输出的 UI 辅助函数
*/
console.log(`Server running at http://${info.host}:${info.port}`)
})
你可以为同一个事件注册多个钩子。它们按注册的顺序执行。
import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({
hooks: {
devServerStarted: [
() => import('./hooks/log_server_info.ts'),
() => import('./hooks/notify_external_service.ts'),
],
},
})
Assembler 钩子在与你的 AdonisJS 应用不同的进程中运行。它们无法访问 IoC 容器、路由器、数据库或任何其他框架服务。如果你需要与你的应用交互,请使用路由扫描钩子来提取元数据,或通过 HTTP/IPC 进行通信。
Init 钩子
init 钩子是 Assembler 启动任何操作时执行的第一个钩子。它接收父实例(DevServer、TestRunner 或 Bundler)、一个用于注册额外运行时钩子的钩子管理器,以及用于代码生成任务的 IndexGenerator。
import { hooks } from '@adonisjs/core/app'
export default hooks.init((parent, hooksManager, indexGenerator) => {
/**
* 通过检查父类型来确定正在运行的操作。
* 使用 indexGenerator 来设置 barrel 文件或类型生成。
*/
console.log('Assembler initialized')
})
init 钩子是为 IndexGenerator 配置 barrel 文件和类型生成的推荐位置,因为它在任何其他操作开始之前运行。
开发服务器钩子
开发服务器钩子在启动和运行开发服务器时执行。devServerStarting 钩子在子进程启动之前触发,devServerStarted 在服务器开始接受连接后触发。
import { hooks } from '@adonisjs/core/app'
export default hooks.devServerStarting((devServer) => {
/**
* 在服务器启动之前执行设置任务。
* 子进程尚未生成。
*/
console.log('Preparing to start dev server...')
})
import { hooks } from '@adonisjs/core/app'
export default hooks.devServerStarted((devServer, info, instructions) => {
/**
* 服务器现在正在运行并接受连接。
* 使用 instructions 来添加自定义的 UI 输出。
*/
instructions.add('custom', `API docs: http://${info.host}:${info.port}/docs`)
})
这些钩子会在每次子进程重启时重新触发,例如由于文件更改而发生完全重载时。
测试运行器钩子
测试运行器钩子会在运行你的测试套件之前和之后执行。使用 testsStarting 来设置测试固件或数据库,使用 testsFinished 来生成报告或清理资源。
import { hooks } from '@adonisjs/core/app'
export default hooks.testsStarting((testRunner) => {
console.log('Preparing test environment...')
})
import { hooks } from '@adonisjs/core/app'
export default hooks.testsFinished((testRunner) => {
console.log('Tests complete, generating coverage report...')
})
当在 watch 模式下运行测试时,这些钩子会在测试套件每次重新运行时重新触发。
打包器钩子
打包器钩子(Bundler hooks)在使用 node ace build 创建生产构建时执行。使用 buildStarting 来执行构建前任务(如资源优化),使用 buildFinished 来显示构建统计信息或运行构建后脚本。
import { hooks } from '@adonisjs/core/app'
export default hooks.buildStarting((bundler) => {
console.log('Starting production build...')
})
import { hooks } from '@adonisjs/core/app'
export default hooks.buildFinished((bundler, instructions) => {
instructions.add('deploy', 'Run `npm run start` in the build folder to start the server')
})
观察器钩子
观察器钩子(Watcher hooks)在开发或测试 watch 模式期间文件发生更改时触发。在 HMR 模式下,Assembler 依赖 hot-hook 来检测更改;否则,内置的文件观察器会处理检测。
每个观察器钩子都会接收到相对路径(从你的应用根目录开始,使用类 Unix 风格斜杠)和受影响文件的绝对路径。
import { hooks } from '@adonisjs/core/app'
export default hooks.fileChanged((relativePath, absolutePath, info, parent) => {
/**
* info.source - 要么是 'hot-hook',要么是 'watcher'
* info.hotReloaded - 如果文件是在不重启的情况下热重载的则为 true
* info.fullReload - 如果需要完全重启服务器则为 true
*/
if (relativePath.startsWith('app/controllers/')) {
console.log(`Controller changed: ${relativePath}`)
}
})
import { hooks } from '@adonisjs/core/app'
export default hooks.fileAdded((relativePath, absolutePath, parent) => {
console.log(`New file created: ${relativePath}`)
})
import { hooks } from '@adonisjs/core/app'
export default hooks.fileRemoved((relativePath, absolutePath, parent) => {
console.log(`File deleted: ${relativePath}`)
})
路由钩子
路由钩子提供了对你的应用路由定义及其关联类型的访问。这些钩子仅在开发服务器运行期间执行,而不会在构建或测试期间执行。
路由已提交
当你的 AdonisJS 应用注册其路由时,routesCommitted 钩子会触发。路由会通过 IPC 从子进程传输到 Assembler,使你无需自行解析文件即可访问路由元数据。
import { hooks } from '@adonisjs/core/app'
export default hooks.routesCommitted((devServer, routes) => {
/**
* routes 是一个以域名为键的对象。
* 每个域都包含一个路由定义数组。
*/
const defaultRoutes = routes['root'] || []
console.log(`${defaultRoutes.length} routes registered`)
})
路由扫描中
routesScanning 钩子在 Assembler 开始分析你的路由以提取请求和响应类型之前触发。使用此钩子来配置扫描器,例如跳过某些不需要分析的路由。
import { hooks } from '@adonisjs/core/app'
export default hooks.routesScanning((devServer, routesScanner) => {
/**
* 跳过不需要类型提取的路由,
* 例如具有复杂流程的身份验证端点。
*/
routesScanner.skip(['session.create', 'session.store', 'oauth.callback'])
})
路由已扫描
routesScanned 钩子在路由分析完成之后触发。扫描器包含提取的类型信息,你可以用它来生成 API 客户端或类型声明。
import { hooks } from '@adonisjs/core/app'
export default hooks.routesScanned((devServer, routesScanner) => {
const routes = routesScanner.getRoutes()
const controllers = routesScanner.getControllers()
/**
* 使用此元数据来生成类型安全的 API 客户端。
* 这些类型是内部导入引用,而非独立的类型。
*/
console.log(`Scanned ${routes.length} routes from ${controllers.length} controllers`)
})
路由扫描器提取的类型是指向你的控制器和验证器的内部导入引用。它们不是完全解析的、可在独立项目中使用的独立类型。生成独立类型需要额外的工具,例如 Tuyau 。
IndexGenerator
IndexGenerator 是一个用于观察目录并根据其内容生成 barrel 文件或类型声明的工具。它会自动处理文件观察,并在源文件被添加或移除时重新生成输出文件。
配置 IndexGenerator
在 init 钩子中注册 IndexGenerator 配置。每个配置指定一个要观察的源目录、一个要生成的输出文件,以及如何转换源文件。
import { hooks } from '@adonisjs/core/app'
export default hooks.init((parent, hooksManager, indexGenerator) => {
indexGenerator.add('controllers', {
source: './app/controllers',
importAlias: '#controllers',
as: 'barrelFile',
exportName: 'controllers',
removeSuffix: 'controller',
output: './.adonisjs/server/controllers.ts',
})
})
配置选项如下:
| Option | Description |
|---|---|
source | 用于扫描源文件的目录 |
importAlias | 在生成的导入中使用的导入别名(例如 #controllers) |
as | 要么是 'barrelFile' 以自动生成,要么是一个用于自定义输出的回调函数 |
exportName | barrel 文件中导出的常量名 |
removeSuffix | 在生成属性键时要从文件名中剥离的后缀 |
output | 生成文件写入的路径 |
Barrel 文件生成
当 as 设置为 'barrelFile' 时,IndexGenerator 会递归地扫描源目录,并生成一个导出惰性加载导入的 barrel 文件。目录结构会作为嵌套对象被保留。
给定如下控制器结构:
app/controllers/
├── auth/
│ ├── login_controller.ts
│ └── register_controller.ts
├── blog/
│ ├── posts_controller.ts
│ └── post_comments_controller.ts
└── users_controller.ts
IndexGenerator 会生成:
export const controllers = {
auth: {
Login: () => import('#controllers/auth/login_controller'),
Register: () => import('#controllers/auth/register_controller'),
},
blog: {
Posts: () => import('#controllers/blog/posts_controller'),
PostComments: () => import('#controllers/blog/post_comments_controller'),
},
Users: () => import('#controllers/users_controller'),
}
这个 barrel 文件使你能够在路由中惰性加载控制器,而无需手动管理导入。当你添加或移除控制器时,该文件会自动更新。
自定义类型生成
要生成类型声明或其他自定义输出,请向 as 选项传入一个回调函数。该回调接收一个文件集合以及一个用于构建输出字符串的 writer 工具。
文件集合是一个键值对象,其中每个键是来自源目录的相对路径(不含扩展名),每个值包含文件的 importPath、relativePath 和 absolutePath。
import { hooks } from '@adonisjs/core/app'
export default hooks.init((parent, hooksManager, indexGenerator) => {
indexGenerator.add('inertiaPages', {
source: './inertia/pages',
as: (files, writer) => {
writer.writeLine(`declare module '@adonisjs/inertia' {`).indent()
writer.writeLine(`export interface Pages {`).indent()
for (const [filePath, file] of Object.entries(files)) {
writer.writeLine(
`'${filePath}': InferPageProps<typeof import('${file.importPath}').default>`
)
}
writer.dedent().writeLine('}')
writer.dedent().writeLine('}')
return writer.toString()
},
output: './.adonisjs/server/inertia_pages.d.ts',
})
})
这会生成一个类型声明文件,将页面组件路径映射到它们的 prop 类型,从而实现类型安全的 Inertia.js 页面渲染。
完整的 IndexGenerator 示例
以下示例展示了一个完整的 init 钩子,它为控制器、事件和监听器配置了 barrel 文件生成——这正是 AdonisJS 框架使用的默认设置。
import { hooks } from '@adonisjs/core/app'
export default hooks.init((parent, hooksManager, indexGenerator) => {
/**
* 为控制器生成 barrel 文件。
* 在路由中实现惰性加载:[controllers.Posts, 'index']
*/
indexGenerator.add('controllers', {
source: './app/controllers',
importAlias: '#controllers',
as: 'barrelFile',
exportName: 'controllers',
removeSuffix: 'controller',
output: './.adonisjs/server/controllers.ts',
})
/**
* 为事件类生成 barrel 文件。
* 实现类型安全的事件发出。
*/
indexGenerator.add('events', {
source: './app/events',
importAlias: '#events',
as: 'barrelFile',
exportName: 'events',
output: './.adonisjs/server/events.ts',
})
/**
* 为事件监听器生成 barrel 文件。
* 在事件绑定中实现监听器的惰性加载。
*/
indexGenerator.add('listeners', {
source: './app/listeners',
importAlias: '#listeners',
as: 'barrelFile',
exportName: 'listeners',
output: './.adonisjs/server/listeners.ts',
})
})