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 в конструкторе.

Полный код на StackBlitz

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());
    };
  };
}
Другие вопросы по тегам