Angular日志服务中装饰器的正确使用与服务注入方案
实现方案
完全可以在不侵入auth.service原有业务代码的前提下,通过装饰器完成日志埋点,核心是通过Angular根注入器动态获取Analytics服务实例,避免在Auth服务中显式依赖Analytics服务。
核心原理
TypeScript方法装饰器在类编译加载阶段就会执行,此时Angular依赖注入容器还未完成初始化,且装饰器本身不在Angular的DI上下文内,无法直接通过构造器注入服务。因此需要提前缓存Angular根注入器实例,在被装饰的方法实际调用时,再从注入器中获取Analytics服务,既不破坏原有依赖关系,也能正常调用服务方法。
具体实现步骤
1. 改造Log装饰器
修改装饰器文件libs/state/analytics/src/lib/decorators/analytics.decorator.ts,代码如下:
import { Injector } from '@angular/core'; import { AnalyticsService } from 'libs/authentication/auth/src/lib/services/analytics.service'; // 缓存根注入器实例 let rootInjector: Injector; // 暴露注入器初始化方法 export const initLogDecoratorInjector = (injector: Injector) => { rootInjector = injector; }; export const Log = (eventName?: string) => { return function errorCatch(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; // 重写原方法 descriptor.value = async function (...args: any[]) { // 方法执行时再获取服务实例,保证DI容器已初始化完成 const analyticsService = rootInjector?.get(AnalyticsService, null); try { // 执行原方法,await保证能捕获异步错误 const result = await originalMethod.apply(this, args); return result; } catch (error) { // 存在Analytics服务时上报错误 if (analyticsService) { analyticsService.logEvent({ name: eventName || `${target.constructor.name}.${propertyKey}_error`, errorMessage: (error as Error)?.message, errorStack: (error as Error)?.stack, triggerParams: args }); } // 抛出原始错误,不改变原方法的错误逻辑 throw error; } }; return descriptor; }; };
2. 应用启动时初始化注入器
在应用启动入口文件(通常是main.ts)中,启动完成后将根注入器传入装饰器:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { initLogDecoratorInjector } from 'libs/state/analytics/src/lib/decorators/analytics.decorator'; platformBrowserDynamic().bootstrapModule(AppModule) .then(moduleRef => { // 初始化装饰器依赖的注入器 initLogDecoratorInjector(moduleRef.injector); }) .catch(err => console.error(err));
3. 原有装饰器的问题修复
之前实现的装饰器存在两个会导致功能失效的问题,已在上述代码中修复:
- 原代码用
method.apply(target, args)执行原方法,target是类的原型而非类实例,会导致原方法内部this指向错误,无法访问Auth服务中注入的afAuth、_toastService等属性,改为originalMethod.apply(this, args)即可,方法执行时的this指向当前Auth服务实例 - 原代码的同步
try/catch无法捕获Promise/async方法的异步错误,给原方法执行加await即可捕获到登录、注册等异步方法抛出的reject错误
使用方式
直接在Auth服务需要埋点的方法上加@Log()装饰器即可,不需要修改Auth服务的构造函数,也不需要改动原有业务逻辑,完全符合非侵入要求:
@Log('Auth_login_failed') 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); }); }
注意事项
- 禁止在装饰器工厂执行阶段(即
return errorCatch外层逻辑)获取服务实例,此时Angular尚未完成启动,注入器为空,无法获取到服务- 调用
injector.get时传入第二个参数null作为默认值,避免Analytics服务未加载时直接抛出错误阻断正常业务流程- 如果需要上报成功事件,直接在
try块内原方法执行完成后调用logEvent即可
内容的提问来源于stack exchange,提问作者Philip Mutua
相关产品推荐
相关产品推荐

