简体   繁体   English

在 Angular 中记录服务时使用装饰器的正确方法

[英]The correct way to use decorators when logging services in Angular

I have two services one is an Auth service and the other is an Analytics service and they exist in different libraries.我有两种服务,一种是 Auth 服务,另一种是 Analytics 服务,它们存在于不同的库中。 The analytics service is used to do event logging.分析服务用于进行事件记录。 One obvious way I could use it to log the auth.service is injecting the analytics service in the auth service but I'd don't want to use this method, I'd like to use the decorator strategy.我可以使用它来记录auth.service的一种明显方法是在 auth 服务中注入分析服务,但我不想使用这种方法,我想使用装饰器策略。 What is the correct approach to achieve this?实现这一目标的正确方法是什么? Also is it possible to use a decorator without interfering with the auth.service codebase?是否可以在不干扰 auth.service 代码库的情况下使用装饰器?

[update] : I've implemented this decorator as shown below (snippet A). [更新] :我已经实现了这个装饰器,如下所示(片段 A)。 The decorator is located @ libs/state/analytics/src/lib/decorators/analytics.decorator.ts I would like to use it on the Auth service as shown in (snippet B).装饰器位于 @ libs/state/analytics/src/lib/decorators/analytics.decorator.ts我想在 Auth 服务上使用它,如(片段 B)所示。

Then in the decorator there is an analytics function I will call from the analytics.service.ts ie the logEvent() function.然后在装饰器中有一个分析函数,我将从analytics.service.ts调用,即logEvent()函数。 How do I inject the logEvent() function from analytics.service in this decorator( The main idea behind this is to log the errors and send them to segment for analytics).如何在此装饰器中从 analytics.service 注入logEvent()函数(这背后的主要思想是记录错误并将它们发送到分段进行分析)。

Snippet A片段 A

export const Log = () => {

    return function catchError(target: any, propertyName: any, descriptor: any) {
        const method = descriptor.value;

        descriptor.value = function (...args: any) {
            try {
                return method.apply(target, args);
            } catch (error) {
                throw new Error(`Special error message: ${error}`);
       
            }
        };
    }
}

Snippet B : The usecase would be something like this.片段 B :用例将是这样的。

  @Log()
  public async loginWithEmailAndPassword(email: string, password: string)
  {
    return this.afAuth.signInWithEmailAndPassword(email, password)
              .then(() => {
                this._logger.log(() => `AuthService.signInWithEmailAndPassword: Successfully logged user in with Email and Password.`);
              })
              .catch((error) => {
                this._throwError(error);
              });
  }

Auth Service libs/state/analytics/src/lib/services/auth.service';验证服务libs/state/analytics/src/lib/services/auth.service';

    @Injectable({ providedIn: 'root' })
    export class AuthService {
      constructor(private afAuth: AngularFireAuth,
                  private afs: AngularFirestore,
                  private router: Router,
                  private _toastService: ToastService,
                  @Inject('ENVIRONMENT') private _env: AuthEnvironment
                  )
      {
       }
    
      public getAuth() {
        return this.afAuth;
      }
    
      public async resetPassword(email: string, langCode: string = 'en' )
      {
    
        firebase.auth().languageCode = langCode;
       
        const actionCodeSettings : firebase.auth.ActionCodeSettings = {
          url: this._env.baseUrl
        }
    
        return firebase.auth()
                   .sendPasswordResetEmail( email, actionCodeSettings )
                   .then(() => this._toastService.doSimpleToast('A password reset link has been sent to your email address.'))
                   .catch(() => this._toastService.doSimpleToast('An error occurred while attempting to reset your password. Please contact support.'));
      }



  public async loginWithEmailAndPassword(email: string, password: string)
  {
    return this.afAuth.signInWithEmailAndPassword(email, password)
              .then(() => {
                this._logger.log(() => `AuthService.signInWithEmailAndPassword: Successfully logged user in with Email and Password.`);
              })
              .catch((error) => {
                this._throwError(error);
              });
  }
    
  public createUserWithEmailAndPassword(displayName: string, email: string, password: string, userProfile: UserProfile, roles: Roles)
      {
        return this.afAuth
                   .createUserWithEmailAndPassword(email, password)
                   .then((res) => {
                      this._checkUpdateUserData(res.user, displayName, userProfile, roles);
                      return <User> <unknown> res.user;
                   })
                   .catch((error) => {
                     this._throwError(error);
                   });
      }
...

Analytics libs/authentication/auth/src/lib/services/analytics.service';分析libs/authentication/auth/src/lib/services/analytics.service';

export class AnalyticsService {

  user$: Observable<User>;

  constructor(
    private _analytics: SegmentService,
    private _cacheService: CacheService,
    private _userService: UserService<User>) {
    this.user$ = this._userService.getUser().pipe(take(1));
  }


  public logEvent(event: TrackEvent) {
    this.user$.subscribe(user => {
      const userId = user?user.id: null;
      const userEmail = user?user.email: null;
      const displayName = user?user.displayName: null;
      const roles = user?user.roles: null;
      this._analytics.track(event.name, {
        ...event,
        property_id: propID,
        user_id: userId,
        email: userEmail,
        displayName: displayName,
        roles: roles,
      })
    });
  }

  identifyUser() {
    this.user$.subscribe((user: User) => {
      if (user) {
        const cachedUser = this._cacheService.getValueByKey('ajs_user_id');
        if (!cachedUser) {
          const traits = { userId: user.id, email: user.email, displayName: user.displayName }
          this._analytics.identify(user.id, traits);
        }
      }
    });
  };

...


}

I wouldn't overcomplicate this.我不会把这件事复杂化。 I would think of EventListeners .我会想到EventListeners You dispatch a log event from one component and the other components listen for such events.您从一个组件调度日志事件,而其他组件侦听此类事件。 You will create a single listener and by the properties they carry - you decide how to use them in analytics service.您将创建一个侦听器并通过它们携带的属性 - 您决定如何在分析服务中使用它们。

You could look into different decorators (HookLogger, MethodLogging) etc. By the looks of your needs, I think Method Decorator to log method params or return value would be needed.您可以查看不同的装饰器(HookLogger、MethodLogging)等。根据您的需求,我认为需要方法装饰器来记录方法参数或返回值。

So we make a @Log logger decorator - which is a method decorator, it will log and emit an loggingEvent with the method's properties it was attached to.所以我们制作了一个@Log logger 装饰器——它是一个方法装饰器,它将记录并发出一个带有它所附加的方法属性的loggingEvent

Service will listen to loggingEvent , read the props and then call methods on service based on the item passed along.服务将监听loggingEvent ,读取道具,然后根据传递的项目调用服务上的方法。

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {

  constructor(private analyticsService: AnalyticsService) {}

  user = { username: 'Amber', id: '123' };

  @Log({ type: 'info', inputs: true, outputs: true })
  onClick(user = this.user): any {
    //do stuff
    return user;
  }
}

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class AnalyticsService {
  
  constructor() {
    document.addEventListener('loggingEvent', (e) => {
      this.logEvent(e) 
    });
  }

  logEvent(event: any) {
    // do stuff
    console.log('anaylitics got logging event: ', event);
  }
}

Decorator from: https://dev.to/gaetanrdn/logger-decorator-47ob装饰器来自: https ://dev.to/gaetanrdn/logger-decorator-47ob

interface LoggerParams {
  type?: 'log' | 'trace' | 'warn' | 'info' | 'debug';
  inputs?: boolean;
  outputs?: boolean;
}

const defaultParams: Required<LoggerParams> = {
  type: 'debug',
  inputs: true,
  outputs: true,
};

export function Log(params?: LoggerParams) {

  const options: Required<LoggerParams> = {
    type: params?.type || defaultParams.type,
    inputs: params?.inputs === undefined ? defaultParams.inputs : params.inputs,
    outputs:
      params?.outputs === undefined ? defaultParams.outputs : params.outputs,
  };

  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    const original = descriptor.value;

    descriptor.value = function (...args: any[]) {
      if (options.inputs) {
        console[options.type]('Logged inputs:', args);
      }

      const result = original.apply(this, args);

      if (options.outputs) {
        console[options.type]('Logged outputs', result);
      }

      const event = new CustomEvent('loggingEvent', { detail: {args, result} });
      document.dispatchEvent(event);

      return result;
    };
  };
}

This could be further enhanced to disabled/enable console logging by getting environment.ts property prod (ie disable console logging when in production).这可以通过获取environment.ts属性prod进一步增强为禁用/启用控制台日志记录(即在生产中禁用控制台日志记录)。

Working example: https://stackblitz.com/edit/angular-ivy-vibhq5?file=src%2Fapp%2Fanayltics.service.ts工作示例: https ://stackblitz.com/edit/angular-ivy-vibhq5?file=src%2Fapp%2Fanayltics.service.ts

I took some tries about this and ended with that result (simplified some classes of yours):我对此进行了一些尝试并以该结果结束(简化了您的某些课程):

First, we have AnalyticalService (logs user and logged metod params)首先,我们有AnalyticalService (记录用户和记录的方法参数)

@Injectable({providedIn: 'root'})
export class AnalyticsService {
  user$: Observable<User> = of({ name: 'Yoda' });

  constructor() {}

  public logEvent(event: any) {
    this.user$.subscribe(user => {
      console.log('event', event);
      console.log('user', user);
    });
  }
}

export interface User {
  name: string;
}

In AuthService we are using decoratorAuthService我们使用装饰器

@Injectable({providedIn: 'root'})
export class AuthService {

  constructor(private as: AnalyticsService) {}

  @Log()
  public async loginWithEmailAndPassword(email: string, password: string) {
    console.log('logged in');
  }
}

The main challenge here is passing injectable service into decorator :这里的主要挑战是将可注入服务传递给装饰器


export function Log() {
  return (
    target: Object,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) => {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args) {
      const service = SharedModule.injector.get<AnalyticsService>(AnalyticsService);

      service.logEvent(args);
      const result = originalMethod.apply(this, args);
      return result;
    };
    return descriptor;
  };
}

To make this work we need additional tricky module which need to be imported in ie app.module为了完成这项工作,我们需要在app.module中导入额外的棘手模块

@NgModule({
  declarations: [],
  imports: [],
  providers: [AnalyticsService]
})
export class SharedModule {
  static injector: Injector;

  constructor(injector: Injector) {
    SharedModule.injector = injector;
  }
}

The trick of service injection in decorator comes from https://stackoverflow.com/a/66921030/2677292装饰器中服务注入的技巧来自https://stackoverflow.com/a/66921030/2677292

Sample code of logging user along with method params by decorator is here登录用户的示例代码以及装饰器的方法参数在这里

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM