Angular 5:在自定义装饰器函数中使用服务
Got it, let's tackle this problem head-on. The core issue here is that decorators run before Angular initializes module injectors—so your static injector cache works for lazy-loaded modules (since their injectors initialize later, after the app is up) but fails for preloaded ones, where the decorator executes before the module's injector is ready.
Here are two robust solutions that work for both preloaded and lazy-loaded modules:
Solution 1: Metadata + App Initializer (No Forced Dependencies)
This approach separates the decorator's metadata marking from the actual logging logic, ensuring we only access the LoggingService after Angular's injector is fully initialized.
Step 1: Define a Metadata Token
First, create an injection token to track which methods need logging:
import { InjectionToken } from '@angular/core'; export const LOGGED_METHODS = new InjectionToken<Map<any, string[]>>('LOGGED_METHODS');
Step 2: Update the @Log() Decorator
Instead of trying to access the service immediately, the decorator will just mark methods and collect decorated classes:
export function Log() { return (target: any, propertyKey: string) => { // Collect the decorated class in a global array (we'll use this later) (window as any).__LOGGED_CLASSES = (window as any).__LOGGED_CLASSES || []; const classConstructor = target.constructor; if (!(window as any).__LOGGED_CLASSES.includes(classConstructor)) { (window as any).__LOGGED_CLASSES.push(classConstructor); } // Add method metadata to the class let loggedMethods = Reflect.getMetadata(LOGGED_METHODS, classConstructor) || []; if (!loggedMethods.includes(propertyKey)) { loggedMethods.push(propertyKey); Reflect.defineMetadata(LOGGED_METHODS, loggedMethods, classConstructor); } }; }
Step 3: Create a Decorator Initialization Service
This service will run on app startup (after injectors are ready) to wrap the marked methods with logging logic:
import { Injectable, APP_INITIALIZER } from '@angular/core'; import { LoggingService } from './logging.service'; import { LOGGED_METHODS } from './logged-methods.token'; @Injectable({ providedIn: 'root' }) export class LoggingDecoratorInitializer { constructor(private loggingService: LoggingService) {} init(): void { const decoratedClasses = (window as any).__LOGGED_CLASSES || []; decoratedClasses.forEach((cls: any) => { const methodsToLog = Reflect.getMetadata(LOGGED_METHODS, cls) || []; methodsToLog.forEach((methodName: string) => { const originalMethod = cls.prototype[methodName]; if (!originalMethod) return; // Wrap the original method with logging cls.prototype[methodName] = function (...args: any[]) { this.loggingService.log(`Calling method: ${methodName} | Args: ${JSON.stringify(args)}`); const result = originalMethod.apply(this, args); this.loggingService.log(`Method ${methodName} returned: ${JSON.stringify(result)}`); return result; }; }); }); } } // Register the initializer to run on app startup export const LOGGING_INITIALIZER = { provide: APP_INITIALIZER, useFactory: (initializer: LoggingDecoratorInitializer) => () => initializer.init(), deps: [LoggingDecoratorInitializer], multi: true, };
Step 4: Register the Initializer in Your App Module
Add the initializer to your root module's providers:
import { NgModule } from '@angular/core'; import { LOGGING_INITIALIZER } from './logging-decorator.initializer'; @NgModule({ providers: [LOGGING_INITIALIZER], }) export class AppModule {}
Solution 2: Inject LoggingService via Constructor Modification
If you don't mind adding a LoggingService dependency to decorated classes, this is a more concise approach. The decorator modifies the class constructor to ensure the service is injected:
import { Injector } from '@angular/core'; import { LoggingService } from './logging.service'; export function Log() { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; const originalConstructor = target.constructor; // Override the constructor to inject LoggingService target.constructor = function (...args: any[]) { // Check if LoggingService is already in constructor args let loggingService = args.find(arg => arg instanceof LoggingService); // If not, fetch it from the global injector if (!loggingService) { loggingService = Injector.get(Injector).get(LoggingService); } this.loggingService = loggingService; // Call the original constructor with existing args originalConstructor.apply(this, args); }; // Wrap the method with logging logic descriptor.value = function (...args: any[]) { this.loggingService.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`); const result = originalMethod.apply(this, args); this.loggingService.log(`${propertyKey} returned: ${JSON.stringify(result)}`); return result; }; return descriptor; }; }
Why These Work
- Solution 1 delays all logging logic until after the app's injector is ready (via
APP_INITIALIZER), so it works for both preloaded and lazy-loaded modules. - Solution 2 ensures the
LoggingServiceis available either via constructor injection or the global injector, which is initialized by the time the class is instantiated (even for preloaded modules).
You can pick whichever fits your project's dependency structure better—both avoid the static injector cache limitation you ran into.
内容的提问来源于stack exchange,提问作者Olivier Clément

