将一个工具特性模块设计得高内聚、低耦合、可扩展,是Angular架构能力的体现。
作为资深前端架构师,会从 设计原则、具体方案、示例代码和注意事项 四个方面提供建议。
一、核心设计原则
- 单一职责原则 (Single Responsibility Principle) : 你的工具模块只应该做它被设计来做的事情------提供可复用的工具服务,不应包含任何业务逻辑或UI组件。
- 依赖倒置原则 (Dependency Inversion Principle) : 工具模块不应该直接依赖具体的业务模块。它应该定义抽象的接口或令牌(Token),让业务模块来适配和注入具体的实现。
- 开放封闭原则 (Open/Closed Principle) : 模块应该对扩展开放,对修改封闭。当需要添加新工具时,应通过扩展(如添加新服务)来实现,而非修改现有核心代码。
- 提供清晰的契约 (Contract) : 对外暴露的API(服务方法、注入令牌、配置选项)必须稳定、清晰、文档完备。
二、具体设计方案
我将你的"工具特性模块"理解为 CoreModule
或 SharedModule
的一个更纯粹的子集,专门用于工具。我建议将其命名为 ToolkitModule
或 UtilsModule
。
1. 模块定义 (toolkit.module.ts
)
这个模块的唯一目的是聚合和提供所有工具服务。它不应该有declarations
(不声明组件/指令/管道),只包含providers
。
2. 服务设计 (*.service.ts
)
每个工具功能都应该被封装在一个独立的、可注入的Service
类中。这是实现"职责单一"和"与业务分离"的关键。
3. 配置机制 (Optional)
对于需要配置的工具(如日志级别、API端点),使用 forRoot()
模式或 注入令牌(Injection Token) 来提供配置,使其在根模块中可配置。
4. 接口抽象 (Abstraction)
对于可能被业务模块扩展或有不同的实现(如日志服务、HTTP客户端装饰器),定义抽象类或接口,并使用依赖注入令牌(DI Token) 来提供实现,而不是直接依赖具体类。
三、示例代码参考
让我们构建一个包含 LoggerService
、StorageService
和 HttpHelperService
的 ToolkitModule
。
项目结构
text
src/app/core/toolkit/
├── index.ts // Barrel file for easy imports
├── toolkit.module.ts // NgModule definition
├── services // Directory for all services
│ ├── logger.service.ts // Logger service with injection token
│ ├── storage.service.ts // Abstract & concrete storage service
│ └── http-helper.service.ts // HTTP utility service
└── tokens // Directory for injection tokens
└── logger.config.ts // Logger configuration token
1. 定义配置令牌 (tokens/logger.config.ts
)
typescript
// tokens/logger.config.ts
import { InjectionToken } from '@angular/core';
export interface LoggerConfig {
logLevel: 'debug' | 'info' | 'warn' | 'error';
enableDebug: boolean;
}
// 创建一个注入令牌来传递配置
export const LOGGER_CONFIG = new InjectionToken<LoggerConfig>('logger.config', {
providedIn: 'root',
factory: () => ({
logLevel: 'debug',
enableDebug: true
})
});
2. 实现可配置的日志服务 (services/logger.service.ts
)
typescript
// services/logger.service.ts
import { Injectable, Inject, Optional } from '@angular/core';
import { LOGGER_CONFIG, LoggerConfig } from '../tokens/logger.config';
// 定义一个抽象类,为可能的多种实现提供契约
export abstract class Logger {
abstract debug(message: string, data?: any): void;
abstract info(message: string, data?: any): void;
abstract warn(message: string, data?: any): void;
abstract error(message: string, data?: any): void;
}
@Injectable({
providedIn: 'root' // 通常工具服务是应用级单例
})
export class LoggerService implements Logger {
private readonly config: LoggerConfig;
constructor(@Inject(LOGGER_CONFIG) config: LoggerConfig) {
this.config = config;
}
debug(message: string, data?: any): void {
if (this.config.enableDebug && this.config.logLevel === 'debug') {
console.debug(`DEBUG: ${message}`, data || '');
}
}
info(message: string, data?: any): void {
if (['debug', 'info'].includes(this.config.logLevel)) {
console.log(`INFO: ${message}`, data || '');
}
}
warn(message: string, data?: any): void {
if (['debug', 'info', 'warn'].includes(this.config.logLevel)) {
console.warn(`WARN: ${message}`, data || '');
}
}
error(message: string, data?: any): void {
// Error should always be logged
console.error(`ERROR: ${message}`, data || '');
}
}
3. 实现抽象存储服务 (services/storage.service.ts
)
typescript
// services/storage.service.ts
import { Injectable } from '@angular/core';
// 抽象类,定义存储契约,便于未来切换实现(e.g., LocalStorage -> IndexedDB)
export abstract class StorageService {
abstract getItem<T>(key: string): T | null;
abstract setItem<T>(key: string, value: T): void;
abstract removeItem(key: string): void;
abstract clear(): void;
}
@Injectable({
providedIn: 'root'
})
export class LocalStorageService extends StorageService {
getItem<T>(key: string): T | null {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
}
setItem<T>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value));
}
removeItem(key: string): void {
localStorage.removeItem(key);
}
clear(): void {
localStorage.clear();
}
}
4. 实现工具模块 (toolkit.module.ts
)
typescript
// toolkit.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoggerService, Logger } from './services/logger.service';
import { LocalStorageService, StorageService } from './services/storage.service';
import { HttpHelperService } from './services/http-helper.service';
import { LOGGER_CONFIG, LoggerConfig } from './tokens/logger.config';
@NgModule({
imports: [CommonModule],
// 没有 declarations
})
export class ToolkitModule {
// 使用 forRoot 模式来在根模块配置工具模块
static forRoot(config: Partial<LoggerConfig> = {}): ModuleWithProviders<ToolkitModule> {
return {
ngModule: ToolkitModule,
providers: [
// 提供具体的服务实现
{ provide: Logger, useClass: LoggerService }, // 依赖抽象,而不是具体类
{ provide: StorageService, useClass: LocalStorageService },
HttpHelperService,
// 覆盖默认的配置
{
provide: LOGGER_CONFIG,
useValue: {
logLevel: 'debug',
enableDebug: true,
...config // 允许根模块传入配置进行覆盖
}
}
]
};
}
}
5. 在根模块中导入并配置 (app.module.ts
)
typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { ToolkitModule } from './core/toolkit/toolkit.module';
import { environment } from '../environments/environment';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
// 导入工具模块并传入生产环境配置
ToolkitModule.forRoot({
logLevel: environment.production ? 'error' : 'debug',
enableDebug: !environment.production
})
],
bootstrap: [AppComponent]
})
export class AppModule { }
6. 在业务组件中使用
typescript
// some-business-component.component.ts
import { Component } from '@angular/core';
import { Logger, StorageService } from '../../core/toolkit'; // 通过barrel文件导入
@Component({
selector: 'app-some-business-component',
template: `...`
})
export class SomeBusinessComponent {
// 依赖注入的是抽象接口,工具模块与业务完全解耦
constructor(
private logger: Logger,
private storage: StorageService
) {
this.logger.info('Component initialized');
const userPrefs = this.storage.getItem('user-preferences');
}
}
7. 创建Barrel文件 (index.ts
)
typescript
// index.ts
export * from './toolkit.module';
// Services
export * from './services/logger.service';
export * from './services/storage.service';
export * from './services/http-helper.service';
// Tokens
export * from './tokens/logger.config';
四、关键注意事项
providedIn: 'root'
vs Module Providers : 对于真正的全局单例服务(如Logger,HttpHelper),使用providedIn: 'root'
更简单。对于需要多种实现或配置的,通过模块的providers
数组提供更灵活。示例中混合使用了两种方式以作演示。- 避免循环依赖 : 工具模块绝对不能导入任何业务模块。它的依赖流应该是单向的:业务模块 -> 工具模块。
- Tree-shaking : 确保你的服务如果没有被使用,可以被Angular的构建优化工具移除。使用
providedIn: 'root'
或providedIn: 'platform'
有助于实现这一点。 - 测试: 由于职责单一且与业务分离,这些工具服务非常容易进行单元测试。你可以轻松地Mock和替换这些服务来测试业务组件。
- 版本管理与变更日志: 如果这个工具模块被多个项目使用,考虑将其发布为私有NPM包,并严格遵守语义化版本控制(SemVer),维护清晰的变更日志。
通过以上设计,你的工具特性模块将成为一个强大、灵活且易于维护的基础设施,能够很好地支持你项目的长期演化。