Angular服务中监听Esc按键无效,如何实现该功能?
问题
我在用于对话框的HomeDialogService服务中添加了@HostListener('document:keydown.escape')来监听Esc按键,并将该服务注入到SdTicketComponent组件中使用。但按下Esc键时控制台无任何输出,若将该监听代码移至组件中则能正常工作。请问如何让服务中的Esc按键监听生效?
相关代码
HomeDialogService
@Injectable() export class HomeDialogService { public constructor(private readonly dialogService: DialogService, private readonly userAgent: UserAgentService) {} @HostListener('document:keydown.escape', ['$event']) onKeydownHandler(event: KeyboardEvent) { console.log(event); } public open<TComponentParams, TResult>( content: Type<DialogBaseComponent<TComponentParams, TResult>>, params?: TComponentParams, dialogOptions?: HomeDialogSettings | undefined, ): { dialogRef: DialogRef; result$: Observable<IDialogResult<TResult>> } { const desktopDialogWidth = dialogOptions.kendoDialogSettings.width ?? DESKTOP_DEFAULT_DIALOG_WIDTH; const desktopDialogMaxHeight = dialogOptions.kendoDialogSettings.maxHeight ?? DESKTOP_MAX_POPUP_HEIGHT; const isMobile = this.userAgent.mobile; const dialogRef = this.dialogService.open({ content, ...dialogOptions.kendoDialogSettings, width: isMobile ? WIDTH_FULL : desktopDialogWidth, maxHeight: isMobile ? MOBILE_MAX_POPUP_HEIGHT : desktopDialogMaxHeight, }); const component = dialogRef.content.instance as DialogBaseComponent<TComponentParams, TResult>; if (params) { component.params = params; } if (isMobile) { this.configureDialogStylesForNonDesktop(dialogRef); } const result$ = dialogRef.result.pipe( map((result: DialogCloseResult | TResult) => { if (result instanceof DialogCloseResult) { return { status: DialogResultStatus.Cancel, }; } return { status: DialogResultStatus.Ok, data: result }; }), ); return { dialogRef, result$, }; } }
SdTicketComponent
export class SdTicketComponent { @Input() public ticket!: ServiceDeskTicket; constructor( public readonly userAgent: UserAgentService, public readonly startScreenSdTicketsSectionService: StartScreenSdTicketsSectionService, private readonly serviceDeskService: ServiceDeskService, private readonly dialogService: HomeDialogService, private readonly alertService: AlertService, ) {} public onTicketClick(): void { this.dialogService .open( SdTicketPopupComponent, { ticket: this.ticket }, { kendoDialogSettings: { title: this.ticket.summary, width: 880, cssClass: 'sd-ticket-popup-wrapper' } }, ) .result$.pipe( filter(result => result.status === DialogResultStatus.Ok), map(result => result.data as IServiceDeskIssueRateData), switchMap(result => this.serviceDeskService.closeTicket(this.ticket, result)), switchMap(() => this.startScreenSdTicketsSectionService.initSectionData(true)), untilDestroyed(this), ) .subscribe(_ => { this.alertService.showSuccess(`${this.ticket.key} has been closed`); }); } }
解决方案
@HostListener是Angular专为组件/指令设计的装饰器,用于绑定宿主元素的事件,但服务没有对应的宿主元素,所以这个装饰器在服务中完全不会生效。要在服务里实现全局Esc键监听,有两种实用方案:
方案一:手动注册/移除全局事件
在服务构造函数中直接给document添加事件监听,同时要在服务销毁时移除监听,避免内存泄漏:
@Injectable() export class HomeDialogService { private escapeHandler: (event: KeyboardEvent) => void; constructor(private readonly dialogService: DialogService, private readonly userAgent: UserAgentService) { // 提前绑定函数上下文,避免this指向问题 this.escapeHandler = (event) => this.handleEscKey(event); document.addEventListener('keydown', this.escapeHandler); } private handleEscKey(event: KeyboardEvent) { if (event.key === 'Escape') { console.log(event); // 这里可以添加你的业务逻辑,比如关闭当前打开的对话框 } } ngOnDestroy() { // 服务销毁时移除监听 document.removeEventListener('keydown', this.escapeHandler); } // 原有open方法保持不变... }
注意:如果服务是全局单例(比如在根模块providers中注册),
ngOnDestroy可能不会触发,这种情况可以考虑在应用退出时移除监听,或者结合RxJS的生命周期管理方案。
方案二:用RxJS实现响应式监听
利用RxJS的fromEvent创建事件流,这种方式更符合Angular的响应式编程风格,也更容易管理订阅生命周期:
import { fromEvent, Subject, takeUntil, filter } from 'rxjs'; @Injectable() export class HomeDialogService { private destroy$ = new Subject<void>(); constructor(private readonly dialogService: DialogService, private readonly userAgent: UserAgentService) { fromEvent<KeyboardEvent>(document, 'keydown') .pipe( takeUntil(this.destroy$), // 服务销毁时停止监听 filter(event => event.key === 'Escape') // 只过滤Esc键 ) .subscribe(event => { console.log(event); // 业务逻辑处理 }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } // 原有open方法保持不变... }
进阶优化:仅在对话框打开时监听
如果你的需求是只有当对话框处于打开状态时才响应Esc键,可以在open方法中动态创建监听,对话框关闭时自动取消:
import { fromEvent, takeUntil, filter } from 'rxjs'; public open<TComponentParams, TResult>(...): { dialogRef: DialogRef; result$: Observable<IDialogResult<TResult>> } { const dialogRef = this.dialogService.open(...); // 监听Esc键,直到对话框关闭 const escSubscription = fromEvent<KeyboardEvent>(document, 'keydown') .pipe( filter(event => event.key === 'Escape'), takeUntil(dialogRef.result) // 对话框关闭时自动停止订阅 ) .subscribe(() => { console.log('Esc pressed, closing dialog'); dialogRef.close(); // 自动关闭对话框 }); // 可选:对话框关闭时手动取消订阅(takeUntil已经处理,这里是双重保障) dialogRef.result.subscribe(() => escSubscription.unsubscribe()); // 原有其他逻辑保持不变... }
内容的提问来源于stack exchange,提问作者Eugene Sukh
相关产品推荐
相关产品推荐

