Angular 18+ 高级教程 – EventManagerPlugin & Hammer.js Gesture
前言
今天来揭秘一下 Angular 的 Event Listening,看看它底层有什么好玩的地方🤪。
(keydown.enter) 语法
在 Component 组件 の Template Binding Syntax 文章中我们就学过了最基本的 Event Listening。
<button (click)="window.alert('click')" (mouseenter)="window.alert('mouseenter')" (mouseleave)="window.alert('mouseleave')" >click me</button>
上面这些都是常见的 DOM 事件。
如果 Angular 就只有这点能耐就弱爆了,我们来看看 Angular 的扩展功能。
<input (keydown.enter)="window.alert('enter')" (keydown.a)="window.alert('a')" (keydown.arrowDown)="window.alert('arrowDown')" >
监听 keydown 事件的同时,还可以指定监听某一个 Key,比如 Enter,Escape,ArrayDown 等等等。
它甚至还支持 modifier keys 哦。
<input (keydown.control.enter)="window.alert('enter')" (keydown.alt.escape)="window.alert('escape')" (keydown.shift.arrowDown)="window.alert('arrowDown')" (keydown.control.alt.shift.a)="window.alert('a')"
>
是不是很方便?
注:Key 不区分大小写,keydown.arrayDown,keydown.ArrayDown,keydown.arraydown 都可以。
Event Listening 源码逛一逛
Angular 是怎么做到监听 keydown.enter 的呢?难道是 compilation 黑魔法?
after compilation
App Template
<input (click)="window.alert('click')" (keydown.enter)="window.alert('enter')" >
run compilation
yarn run ngc -p tsconfig.json
app.component.js
监听 keydown.enter 和监听普通的 click 事件写法是一样的。也就是说,它并不是用 compilation 黑魔法实现的。
ɵɵlistener & renderer.listen
ɵɵlistener 函数的源码在 listener.ts
listenerInternal 函数我们在 <<Signal-based Output 源码逛一逛>> 曾经看见过,不过那时是针对监听 @Output 事件,而不是 DOM 事件。
关键只有一句 renderer.listen。
这个 renderer 是 Root Level Provider,我们日常也可以使用它。
<input #input (click)="window.alert('click')" (keydown.enter)="window.alert('enter')" >
相等于
export class AppComponent { readonly inputElementRef = viewChild.required('input', { read: ElementRef }); constructor() { const renderer = inject(Renderer2); afterNextRender(() => renderer.listen(this.inputElementRef().nativeElement, 'keydown.enter', () => window.alert('enter')), ); } window = window; }
DefaultDomRenderer2
Angular 其实有好几款 Renderer,在 Animation 动画 文章中我们见过 AnimationRenderer,它是其中一款。
Angular 在启动时会提供一些 built-in 的 Provider (BROWSER_MODULE_PROVIDERS) 给 Root Injector,其中一个是 DomRendererFactory2。
注:上图是 browser 环境下 Angular 默认会提供的 Provider。源码在 browser.ts。以前我们逛 NgModule 和 NodeInjector 源码时也见过它的。
DomRendererFactory2 的源码在 dom_renderer.ts
初始化默认 Renderer 是 DefaultDomRenderer2。
创建 Renderer 时它会依据 RenderType2 选择创建哪一款 Renderer。这里 RenderType2 具体传入的是 ComponentDef。
getOrCreateRenderer 方法
有 3 款 Renderer:EmulatedEncapsulationDomRenderer2,ShadowDomRenderer 和 NoneEncapsulationDomRenderer。
EmulatedEncapsulationDomRenderer2 继承自 NoneEncapsulationDomRenderer
ShadowDomRenderer 和 NoneEncapsulationDomRenderer 都继承自 DefaultDomRenderer2
这些派生类都没有 override listen 方法,所以到头来,renderer.listen 就是 DefaultDomRenderer2.listen 方法,我们追她就是了。
DefaultDomRenderer2.listen 方法
实现代码不在这里,它内部也只是调用了 eventManager.addEventListener 方法而已...追了个寂寞😔。
EventManager
EventManager 也包含在 BROWSER_MODULE_PROVIDERS 里头
EventManager 源码在 event_manager.ts
里头依然没有具体的 addEventListener,它依赖 EventManagerPlugin...又追了个寂寞😔。
EventManagerPlugin
EventManagerPlugin 也是包含在 BROWSER_MODULE_PROVIDERS 里头
它是 multiple Provider,一共有 2 个,顾名思义:
-
DomEventsPlugin
负责 (click), (mouseenter) 这些
-
KeyEventsPlugin
负责 (keydown.enter), (keyup.escape) 这些
KeyEventsPlugin
那我们继续追 KeyEventsPlugin,它的源码在 key_events.ts
关键就在这里了,'keydown.enter' 会被拆解,element.addEventListener 监听的是 'keydown',然后 'enter' 被用作于 callback 的 filter。
用代码来表达大概长这样
const key = 'keydown.enter'; const eventName = key.split('.')[0]; // 'keydown' const specifyKey = key.split('.')[1]; // 'enter' const callbackFn = (e: KeyboardEvent) => { console.log('enter', e); }; input.addEventListener(eventName, e => { const keyboardEvent = e as KeyboardEvent; if (keyboardEvent.key === specifyKey) { callbackFn(keyboardEvent); } });
好,(keydown.enter) 揭秘完成。
(document.click) 语法
Angular 无法超脱三界之外,按理说它是没办法监听到 document 和 body 的,但是它却可以这样写
@if (show()) { <button (body:click)="click()">click me</button> <button (document:click)="click()">click me</button> }
虽然声明 event listener 在 button 但实际上监听的是 body 和 document。
当 button 被销毁,它也会 remove body 和 document 的 event listener。
是不是很神奇?
通常它会搭配 host binding 使用。
Not working in Renderer2.listen
(document:click) 语法不适用于 Renderer2.listen,它只适用于 Template Binding Syntax。
export class HelloWorldComponent { constructor() { const hostElement: HTMLElement = inject(ElementRef).nativeElement; const renderer = inject(Renderer2); afterNextRender(() => { // 这是错误的,不要这样写 !!! renderer.listen(hostElement, 'document:click', () => console.log('click')); }); } }
原理是这样的,下图是 compile 之后的 <button (document:click)="log()" >
比平常多了一个参数 ɵɵresolveDocument (源码在 misc_utils.ts)
它是一个函数,接收一个 element 返回 element.ownerDocument。
ɵɵlistener 内部会调用 listenerInternal 函数 (源码在 listener.ts)
可以看到,document 是在 Renderer2.listen 之前就弄好的。
Hammer.js Gesture
Angular 可以搭配 Hammer.js 来监听手势 Gesture。
Hammer.js 是一个用来监听 Gesture 手势的库。
虽然它早在 2016 年就已经停止维护了,但时至今日它依然是许多人的选择 (npm 7 days download 1.3m)
Angular 也选择了它作为监听手势的底层实现。
由于不是很多项目需要支持手势操作,所以它默认是不开启的,我们需要做一些 setup 才能使用它。
我们先来看看它的使用方式,之后才讲解如何 setup。
App Template
<div (swipe)="display.textContent = display.textContent + ' swipe'" class="slider"> <p>gesture me</p> </div> <div class="display" #display></div>
swipe 是 Hammer.js 其中一个手势事件。
效果
Hammer.js 还支持很多其它的手势 (比如:Pan,Pinch,Press,Rotate,Swipe,Tap 等等)
想了解更多的朋友可以看官网 Docs – Hammer.js (或许有一天我会写一篇文章来讲解 Hammer.js)
Setup Hammer.js
首先在 app.config.ts 提供相关的 Provider。
import { importProvidersFrom, type ApplicationConfig } from '@angular/core'; import { HammerModule } from '@angular/platform-browser'; export const appConfig: ApplicationConfig = { providers: [importProvidersFrom(HammerModule)], };
没有 provideHammer 函数,我们只能用 importProvidersFrom + HammerModule 的方式来提供 Provider。
HammerModule 源码在 hammer_gestures.ts
没错,它也是一个 EventManagerPlugin。至于它背地里做了什么,我想大家应该推测的出来,我们就不再逛源码了。
Load Hammer.js
单单提供 Provider 是不够的。
它会响警报。
原因是 Angular 不会替我们加载 Hammer.min.js。
我们需要自己来
yarn add hammerjs
yarn add @types/hammerjs --dev
然后 import 'hammerjs'
只要 Angular 在 listening 时,window 有 Hammer 属性就可以了
另外,它也支持 lazy loading 哦。
import { importProvidersFrom, type ApplicationConfig } from '@angular/core'; import { HAMMER_LOADER, HammerModule } from '@angular/platform-browser'; export const appConfig: ApplicationConfig = { providers: [ importProvidersFrom(HammerModule), { // 1. 提供 HAMMER_LOADER Token provide: HAMMER_LOADER, // 2. 一个返回 Promise<void> 的函数 useValue: () => import('hammerjs'), }, ], };
等到要 addEventListener 时才去加载 Hammer.min.js
总结
严格来说 Angular 并没有 built-in 支持手势监听,它只是 built-in 了一个基于 Hammer.js 的 EventManagerPlugin 而已。
如果我们不使用 Hammer.js 那基本上 Angular 完全没有帮上忙😅。
Custom EventManagerPlugin
Angular built-in 有 DomEventsPlugin,KeyEventsPlugin,HammerGesturesPlugin。
我们也可以提供自定义的 EventManagerPlugin 去扩展事件监听。
<div (control.click)="window.alert('control click')" class="slider" role="presentation"> <p>gesture me</p> </div>
监听 control 键 + mouse click 事件。
自定义 EventManagerPlugin
@Injectable() export class ControlClickEventManagerPlugin extends EventManagerPlugin { constructor() { super(inject(DOCUMENT)); } override supports(eventName: string): boolean { return eventName === 'control.click'; } override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { const callback = (e: MouseEvent) => { if (e.ctrlKey) { handler(e); } }; element.addEventListener('click', callback); return () => { element.removeEventListener('click', callback); }; } }
提供 Provider
export const appConfig: ApplicationConfig = { providers: [ { provide: EVENT_MANAGER_PLUGINS, useClass: ControlClickEventManagerPlugin, multi: true, }, ], };
搞定!
提醒:EventManagerPlugin 必须提供给 Root Level Injector 哦 (它目前还不支持 lazy loading),因为依赖 EventManagerPlugin 的 EventManger 和 Renderer 都是 Root Level 的。
相关 Github Issue – Support lazy-loaded event plugins
目录
上一篇 Angular 18+ 高级教程 – Routing 路由 (功能篇)
下一篇 Angular 18+ 高级教程 – Library
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻