Angular AoT Custom Decorator Произошла ошибка при статическом разрешении значений символов
Я создал декоратор, чтобы помочь мне с обработкой настольных / мобильных событий
import { HostListener } from '@angular/core';
type MobileAwareEventName =
| 'clickstart'
| 'clickmove'
| 'clickend'
| 'document:clickstart'
| 'document:clickmove'
| 'document:clickend'
| 'window:clickstart'
| 'window:clickmove'
| 'window:clickend';
export const normalizeEventName = (eventName: string) => {
return typeof document.ontouchstart !== 'undefined'
? eventName
.replace('clickstart', 'touchstart')
.replace('clickmove', 'touchmove')
.replace('clickend', 'touchend')
: eventName
.replace('clickstart', 'mousedown')
.replace('clickmove', 'mousemove')
.replace('clickend', 'mouseup');
};
export const MobileAwareHostListener = (
eventName: MobileAwareEventName,
args?: string[],
) => {
return HostListener(normalizeEventName(eventName), args);
};
Проблема в том, что когда я пытаюсь скомпилировать --prod
Я получаю следующую ошибку
typescript error
Error encountered resolving symbol values statically. Function calls are not supported. Consider replacing
the function or lambda with a reference to an exported function (position 26:40 in the original .ts file),
resolving symbol MobileAwareHostListener in
.../event-listener.decorator.ts, resolving symbol HomePage in
.../home.ts
Error: The Angular AoT build failed. See the issues above
Что случилось? Как я могу это исправить?
2 ответа
Это означает именно то, что говорит ошибка. Вызовы функций не поддерживаются там, где вы их делаете. Расширение поведения встроенных декораторов Angular не поддерживается.
AOT-сборник (запускается --prod
опция) позволяет статически анализировать существующий код и заменять некоторые фрагменты ожидаемыми результатами их оценки. Динамическое поведение в этих местах означает, что AOT не может использоваться для приложения, что является основным недостатком для приложения.
Если вам нужно нестандартное поведение, HostListener
не должен использоваться Так как он в основном настраивает прослушиватель на элементе, это должно быть сделано вручную с помощью провайдера рендеринга, что является предпочтительной угловой абстракцией по сравнению с DOM.
Это можно решить с помощью специального декоратора:
interface IMobileAwareDirective {
injector: Injector;
ngOnInit?: Function;
ngOnDestroy?: Function;
}
export function MobileAwareListener(eventName) {
return (classProto: IMobileAwareDirective, prop, decorator) => {
if (!classProto['_maPatched']) {
classProto['_maPatched'] = true;
classProto['_maEventsMap'] = [...(classProto['_maEventsMap'] || [])];
const ngOnInitUnpatched = classProto.ngOnInit;
classProto.ngOnInit = function(this: IMobileAwareDirective) {
const renderer2 = this.injector.get(Renderer2);
const elementRef = this.injector.get(ElementRef);
const eventNameRegex = /^(?:(window|document|body):|)(.+)/;
for (const { eventName, listener } of classProto['_maEventsMap']) {
// parse targets
const [, eventTarget, eventTargetedName] = eventName.match(eventNameRegex);
const unlisten = renderer2.listen(
eventTarget || elementRef.nativeElement,
eventTargetedName,
listener.bind(this)
);
// save unlisten callbacks for ngOnDestroy
// ...
}
if (ngOnInitUnpatched)
return ngOnInitUnpatched.call(this);
}
// patch classProto.ngOnDestroy if it exists to remove a listener
// ...
}
// eventName can be tampered here or later in patched ngOnInit
classProto['_maEventsMap'].push({ eventName, listener: classProto[prop] });
}
}
И использовал как:
export class FooComponent {
constructor(public injector: Injector) {}
@MobileAwareListener('clickstart')
bar(e) {
console.log('bar', e);
}
@MobileAwareListener('body:clickstart')
baz(e) {
console.log('baz', e);
}
}
IMobileAwareDirective
интерфейс играет важную роль здесь. Это заставляет класс иметь injector
собственность и этот способ имеет доступ к своему инжектору и собственным зависимостям (включая ElementRef
, который является локальным и, очевидно, недоступен для корневого инжектора). Это соглашение является предпочтительным способом для декораторов взаимодействовать с зависимостями экземпляра класса. class ... implements IMobileAwareDirective
также могут быть добавлены для выразительности.
MobileAwareListener
отличается от HostListener
в том, что последний принимает список имен аргументов (включая магический $event
), тогда как первый просто принимает объект события и привязывается к экземпляру класса. Это может быть изменено при необходимости.
Вот демо.
Есть несколько проблем, которые должны быть рассмотрены дополнительно здесь. Слушатели событий должны быть удалены в ngOnDestroy
, Могут быть потенциальные проблемы с наследованием классов, это требует дополнительной проверки.
Полная реализация ответа Estus. Это работает с наследованием. Единственным недостатком является то, что компонент все еще требует включения injector
в конструкторе.
import { ElementRef, Injector, Renderer2 } from '@angular/core';
function normalizeEventName(eventName: string) {
return typeof document.ontouchstart !== 'undefined'
? eventName
.replace('clickstart', 'touchstart')
.replace('clickmove', 'touchmove')
.replace('clickend', 'touchend')
: eventName
.replace('clickstart', 'mousedown')
.replace('clickmove', 'mousemove')
.replace('clickend', 'mouseup');
}
interface MobileAwareEventComponent {
_macSubscribedEvents?: any[];
injector: Injector;
ngOnDestroy?: () => void;
ngOnInit?: () => void;
}
export function MobileAwareHostListener(eventName: string) {
return (classProto: MobileAwareEventComponent, prop: string) => {
classProto._macSubscribedEvents = [];
const ngOnInitUnmodified = classProto.ngOnInit;
classProto.ngOnInit = function(this: MobileAwareEventComponent) {
if (ngOnInitUnmodified) {
ngOnInitUnmodified.call(this);
}
const renderer = this.injector.get(Renderer2) as Renderer2;
const elementRef = this.injector.get(ElementRef) as ElementRef;
const eventNameRegex = /^(?:(window|document|body):|)(.+)/;
const [, eventTarget, eventTargetedName] = eventName.match(eventNameRegex);
const unlisten = renderer.listen(
eventTarget || elementRef.nativeElement,
normalizeEventName(eventTargetedName),
classProto[prop].bind(this),
);
classProto._macSubscribedEvents.push(unlisten);
};
const ngOnDestroyUnmodified = classProto.ngOnDestroy;
classProto.ngOnDestroy = function(this: MobileAwareEventComponent) {
if (ngOnDestroyUnmodified) {
ngOnDestroyUnmodified.call(this);
}
classProto._macSubscribedEvents.forEach((unlisten) => unlisten());
};
};
}