【问题标题】:Angular AoT Custom Decorator Error encountered resolving symbol values staticallyAngular AoT 自定义装饰器错误遇到静态解析符号值
【发布时间】:2018-02-19 20:43:24
【问题描述】:

我创建了一个装饰器来帮助我处理桌面/移动事件

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

怎么了?我该如何解决?

【问题讨论】:

  • 问题之一是我不能使用const foo = () =>,使用function 导出工作。但是还有一个问题,我不能使用typeof,为什么?有什么办法吗?
  • 您知道 Hostlistener 装饰器能够处理移动事件并将鼠标事件转换为触摸事件吗?你只需要导入'hammerjs。
  • @cyrix 你能解释一下hammerjs 究竟是如何解决这个问题的吗?

标签: angular typescript


【解决方案1】:

这正是错误所说的。在您执行函数调用的地方不支持函数调用。 Angular内置装饰器isn't supported行为的扩展。

AOT 编译(由--prod 选项触发)允许静态分析现有代码并将某些部分替换为其评估的预期结果。这些地方的动态行为意味着应用程序不能使用 AOT,这是应用程序的一个主要缺点。

如果您需要自定义行为,则不应使用 HostListener。因为它基本上在元素上设置了一个监听器,所以这应该通过渲染器提供者手动完成,这比 DOM 更适合 Angular 抽象。

这可以通过自定义装饰器解决:

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 以提高表现力。

MobileAwareListenerHostListener 的不同之处在于后者接受参数名称列表(包括神奇的$event),而前者只接受事件对象并绑定到类实例。这可以在需要时更改。

这里是a demo

这里有几个问题需要额外解决。事件侦听器应在ngOnDestroy 中删除。类继承可能存在潜在问题,需要额外测试。

【讨论】:

  • 似乎这种方法不适用于继承(父类上的装饰器未执行)。有没有办法做到这一点? (可能是因为我在动态创建组件?)我应该问一个新问题吗?
  • 如果您将if (!classProto['_maPatched']) { 更改为同时检查hasOwnProperty,那么它将适用于继承。
  • 是的,但不止于此。 classProto['_maEventsMap'] 也可以在原型上继承,当然不应该在类原型上修改它。另一件事是 ngOnInit 可以已经被父构造函数修补,并且在子构造函数中修补它也可能会设置一些侦听器两次。所有这些问题都应该考虑在内。
【解决方案2】:

estus answer 的完整实现。这适用于继承。唯一的缺点是仍然需要组件在构造函数中包含injector

Full code on 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());
    };
  };
}

【讨论】:

  • StackBlitz 上不再有:Yikes! The page you requested couldn't be found.
猜你喜欢
  • 2017-09-06
  • 2023-03-12
  • 1970-01-01
  • 1970-01-01
  • 2017-02-13
  • 1970-01-01
  • 2018-01-03
  • 2019-03-21
  • 1970-01-01
相关资源
最近更新 更多