在 Angular 应用开发中,“核心模块(CoreModule)” 是实现代码解耦、配置集中管理、服务单例化的关键设计模式。尤其在中大型项目中,合理的核心模块设计能让应用架构更清晰、维护成本更低。本文将从设计思路到落地实践,详解如何基于核心模块封装单例服务与应用核心配置。
一、为什么需要核心模块?
在未引入核心模块时,开发者常遇到这些问题:
- 全局单例服务(如认证、API 请求、全局状态)被重复导入,导致多实例问题;
- 应用核心配置(如 API 基础地址、环境常量、路由守卫)散落在各个模块中,维护困难;
- 根模块(AppModule)充斥大量服务、配置导入,代码臃肿且耦合度高。
核心模块的核心目标就是解决上述问题:集中管理应用级的单例服务、核心配置,且仅在根模块导入一次,避免重复加载。
二、核心模块设计核心思路
1. 单例服务的封装逻辑
Angular 中服务的实例化规则:
- 服务添加
providedIn: 'root'时,默认在根注入器中创建单例; - 服务在模块的
providers数组中声明时,模块每被导入一次,服务就会创建一个新实例。
核心模块封装单例服务的思路:
- 将应用级全局服务(如 AuthService、ApiService)在核心模块的
providers中声明; - 核心模块仅在根模块(AppModule)导入一次,确保服务全局单例;
- 核心模块通过
forRoot()静态方法暴露配置,避免重复初始化。
2. 核心配置的集中管理思路
应用核心配置(如环境变量、API 配置、全局常量)的设计原则:
- 配置与业务逻辑解耦:配置独立封装,服务仅依赖配置接口,不硬编码常量;
- 配置可扩展:支持不同环境(开发 / 测试 / 生产)的配置切换;
- 配置注入化:通过 Angular 依赖注入(DI)提供配置,便于测试和替换。
三、核心模块落地实践
1. 目录结构设计
先规划清晰的目录结构,区分核心模块与业务模块:
src/ ├── app/ │ ├── core/ # 核心模块目录 │ │ ├── config/ # 核心配置目录 │ │ │ ├── api.config.ts │ │ │ └── app.config.ts │ │ ├── services/ # 全局单例服务目录 │ │ │ ├── auth.service.ts │ │ │ └── api.service.ts │ │ ├── guards/ # 全局路由守卫 │ │ │ └── auth.guard.ts │ │ ├── interceptors/ # 全局拦截器 │ │ │ └── token.interceptor.ts │ │ ├── core.module.ts # 核心模块入口 │ │ └── index.ts # 导出核心模块,简化导入 │ ├── shared/ # 共享模块(组件/指令/管道) │ ├── features/ # 业务功能模块 │ └── app.module.ts # 根模块2. 核心配置封装
首先定义配置接口,确保类型安全,再封装不同环境的配置:
// src/app/core/config/app.config.ts // 应用核心配置接口 export interface AppConfig { appName: string; env: 'dev' | 'test' | 'prod'; api: ApiConfig; } // API配置接口 export interface ApiConfig { baseUrl: string; timeout: number; } // 开发环境配置 export const devConfig: AppConfig = { appName: 'Angular Demo', env: 'dev', api: { baseUrl: 'http://localhost:3000/api', timeout: 10000 } }; // 生产环境配置 export const prodConfig: AppConfig = { appName: 'Angular Demo', env: 'prod', api: { baseUrl: 'https://api.example.com', timeout: 10000 } }; // 配置注入令牌(用于DI) export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG'); // 根据环境获取配置 export function getAppConfig(): AppConfig { return environment.production ? prodConfig : devConfig; }3. 单例服务封装
基于核心配置封装全局单例服务,以 API 服务为例:
// src/app/core/services/api.service.ts import { Injectable, Inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { APP_CONFIG, AppConfig } from '../config/app.config'; import { Observable } from 'rxjs'; // 全局API服务(单例) @Injectable() // 不设置providedIn,由CoreModule的providers管理 export class ApiService { private baseUrl: string; private timeout: number; constructor( private http: HttpClient, @Inject(APP_CONFIG) private config: AppConfig // 注入核心配置 ) { this.baseUrl = config.api.baseUrl; this.timeout = config.api.timeout; } // 封装GET请求 get<T>(url: string, params?: any): Observable<T> { return this.http.get<T>(`${this.baseUrl}/${url}`, { params }); } // 封装POST请求 post<T>(url: string, data: any): Observable<T> { return this.http.post<T>(`${this.baseUrl}/${url}`, data); } }4. 核心模块入口实现
核心模块的关键是:禁止被多次导入,通过forRoot()方法暴露配置和服务,且添加防重复导入的校验:
// src/app/core/core.module.ts import { NgModule, Optional, SkipSelf, ModuleWithProviders } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { APP_CONFIG, AppConfig, getAppConfig } from './config/app.config'; import { ApiService } from './services/api.service'; import { AuthService } from './services/auth.service'; import { TokenInterceptor } from './interceptors/token.interceptor'; import { AuthGuard } from './guards/auth.guard'; // 核心模块(仅根模块导入) @NgModule({ imports: [ CommonModule, HttpClientModule // 核心模块导入HttpClientModule,全局复用 ], declarations: [], exports: [] // 核心模块不导出任何组件/指令,仅提供服务和配置 }) export class CoreModule { // 防止核心模块被多次导入(关键) constructor(@Optional() @SkipSelf() parentModule: CoreModule) { if (parentModule) { throw new Error('CoreModule 已导入,请勿重复导入!'); } } // 静态方法:提供配置和服务(确保单例) static forRoot(): ModuleWithProviders<CoreModule> { return { ngModule: CoreModule, providers: [ // 注入核心配置 { provide: APP_CONFIG, useFactory: getAppConfig }, // 全局单例服务 AuthService, ApiService, // 路由守卫 AuthGuard, // HTTP拦截器 { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true } ] }; } }5. 根模块导入核心模块
在根模块(AppModule)中导入核心模块的forRoot()方法,确保全局单例:
// src/app/app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { CoreModule } from './core'; // 简化导入(基于index.ts) import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, AppRoutingModule, CoreModule.forRoot() // 仅根模块导入一次 ], bootstrap: [AppComponent] }) export class AppModule { }四、核心模块设计的最佳实践
1. 明确核心模块的职责边界
核心模块只放应用级、全局、单例的内容:
- ✅ 全局单例服务(认证、API、全局状态);
- ✅ 应用核心配置(环境、API 地址、常量);
- ✅ 全局路由守卫、HTTP 拦截器;
- ❌ 业务组件、通用指令 / 管道(放 SharedModule);
- ❌ 页面级服务(放对应业务模块)。
2. 禁止核心模块导出内容
核心模块的作用是 “提供服务 / 配置”,而非 “共享组件”,因此不要在exports数组中导出任何内容,避免被误用作共享模块。
3. 防重复导入校验
通过@Optional() @SkipSelf()装饰器校验父模块是否已导入 CoreModule,避免重复导入导致服务多实例。
4. 配置与服务解耦
通过注入令牌(InjectionToken)注入配置,而非硬编码,便于不同环境切换和单元测试(可模拟配置)。
5. 简化导入路径
在核心模块目录下创建index.ts,导出核心模块和常用服务 / 配置,简化其他模块的导入:
// src/app/core/index.ts export * from './core.module'; export * from './services/auth.service'; export * from './services/api.service'; export * from './config/app.config';五、核心模块与共享模块的区别
很多开发者容易混淆 CoreModule 和 SharedModule,两者的核心区别如下:
| 维度 | CoreModule(核心模块) | SharedModule(共享模块) |
|---|---|---|
| 导入次数 | 仅根模块导入一次 | 可被多个业务模块重复导入 |
| 核心作用 | 提供全局单例服务、核心配置 | 共享组件、指令、管道 |
| 导出内容 | 不导出任何内容 | 导出共享的组件 / 指令 / 管道 |
| 服务管理 | 声明全局单例服务 | 不声明服务(避免多实例) |
总结
Angular 核心模块的设计核心是 **“集中管理、单例保障、一次导入”**,通过核心模块我们可以:
- 集中封装应用级核心配置,实现配置与业务逻辑解耦,便于环境切换和维护;
- 保障全局服务的单例性,避免重复导入导致的多实例问题;
- 简化根模块代码,明确应用架构边界,提升中大型项目的可维护性。
核心模块的设计本质是 Angular 依赖注入和模块化思想的落地,遵循本文的设计思路和最佳实践,能让你的 Angular 应用架构更优雅、更健壮。