普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月1日首页

Angular的Service创建多个实例的总结

作者 ze_juejin
2025年12月1日 16:17

在 Angular 中,Service 默认是单例的,但可以通过不同的方式创建多个实例。以下是几种实现多实例 Service 的方法:

1. 在组件级别提供 Service

// my-service.service.ts
@Injectable()
export class MyService {
  private data = Math.random(); // 用于区分不同实例

  getData() {
    return this.data;
  }
}

// component-a.component.ts
@Component({
  selector: 'app-component-a',
  templateUrl: './component-a.component.html',
  providers: [MyService] // 在这里提供,每个组件实例都有自己的 service
})
export class ComponentA {
  constructor(public myService: MyService) {}
}

// component-b.component.ts
@Component({
  selector: 'app-component-b',
  templateUrl: './component-b.component.html',
  providers: [MyService] // 这里会创建另一个实例
})
export class ComponentB {
  constructor(public myService: MyService) {}
}

2. 使用工厂函数创建不同实例

// configurable.service.ts
@Injectable()
export class ConfigurableService {
  constructor(private config: { prefix: string }) {}

  process(value: string): string {
    return `${this.config.prefix}: ${value}`;
  }
}

// 在模块中注册多个实例
@NgModule({
  providers: [
    // 实例1
    {
      provide: 'ServiceInstanceA',
      useFactory: () => {
        return new ConfigurableService({ prefix: 'InstanceA' });
      }
    },
    // 实例2
    {
      provide: 'ServiceInstanceB',
      useFactory: () => {
        return new ConfigurableService({ prefix: 'InstanceB' });
      }
    }
  ]
})
export class AppModule {}

// 在组件中注入特定实例
@Component({
  selector: 'app-example',
  template: `
    <div>Service A: {{ resultA }}</div>
    <div>Service B: {{ resultB }}</div>
  `
})
export class ExampleComponent {
  resultA: string;
  resultB: string;

  constructor(
    @Inject('ServiceInstanceA') private serviceA: ConfigurableService,
    @Inject('ServiceInstanceB') private serviceB: ConfigurableService
  ) {
    this.resultA = this.serviceA.process('Hello');
    this.resultB = this.serviceB.process('World');
  }
}

3. 使用 @Injectable({ providedIn: 'any' })

在 Angular 6+ 中,可以使用 providedIn: 'any'

// any-scope.service.ts
@Injectable({
  providedIn: 'any' // 每个懒加载模块都会得到新实例,但模块内是单例
})
export class AnyScopeService {
  instanceId = Math.random();
}

// 在不同懒加载模块中使用
// LazyModule1 和 LazyModule2 会得到不同的实例

4. 使用继承创建子类实例

// base.service.ts
@Injectable()
export class BaseService {
  protected baseValue = 'Base';

  getValue(): string {
    return this.baseValue;
  }
}

// 创建多个继承的子类
@Injectable()
export class ExtendedServiceA extends BaseService {
  constructor() {
    super();
    this.baseValue = 'ServiceA';
  }
}

@Injectable()
export class ExtendedServiceB extends BaseService {
  constructor() {
    super();
    this.baseValue = 'ServiceB';
  }
}

// 在模块中注册
@NgModule({
  providers: [
    ExtendedServiceA,
    ExtendedServiceB
  ]
})
export class AppModule {}

5. 动态创建实例

// dynamic-instance.service.ts
@Injectable()
export class DynamicInstanceService {
  private static instanceCounter = 0;
  private instanceId: number;

  constructor() {
    this.instanceId = ++DynamicInstanceService.instanceCounter;
  }

  getId(): number {
    return this.instanceId;
  }
}

// instance-factory.service.ts
@Injectable({
  providedIn: 'root'
})
export class InstanceFactoryService {
  createServiceInstance(): DynamicInstanceService {
    return new DynamicInstanceService();
  }
}

// 使用
@Component({
  selector: 'app-dynamic',
  template: `
    <button (click)="createInstance()">创建新实例</button>
    <div *ngFor="let service of services">
      实例 ID: {{ service.getId() }}
    </div>
  `
})
export class DynamicComponent {
  services: DynamicInstanceService[] = [];

  constructor(private factory: InstanceFactoryService) {}

  createInstance() {
    const instance = this.factory.createServiceInstance();
    this.services.push(instance);
  }
}

6. 使用 InjectionToken 和工厂模式

// 创建 InjectionToken
export const MULTI_SERVICE = new InjectionToken<MultiService[]>('MultiService');

// 服务接口
export interface MultiService {
  process(data: string): string;
}

// 实现类
@Injectable()
export class MultiServiceImpl implements MultiService {
  private id = Math.random();

  process(data: string): string {
    return `${this.id}: ${data}`;
  }
}

// 注册多个实例
@NgModule({
  providers: [
    {
      provide: MULTI_SERVICE,
      useFactory: () => new MultiServiceImpl(),
      multi: true // 关键:允许多个 provider
    },
    {
      provide: MULTI_SERVICE,
      useFactory: () => new MultiServiceImpl(),
      multi: true
    },
    {
      provide: MULTI_SERVICE,
      useFactory: () => new MultiServiceImpl(),
      multi: true
    }
  ]
})
export class AppModule {}

// 注入所有实例
@Component({
  selector: 'app-multi',
  template: `
    <div *ngFor="let service of services">
      {{ service.process('test') }}
    </div>
  `
})
export class MultiComponent {
  services: MultiService[];

  constructor(@Inject(MULTI_SERVICE) services: MultiService[]) {
    this.services = services; // 获取所有实例的数组
  }
}

7. 实际应用场景示例

场景:每个标签页需要独立的数据服务

// tab-data.service.ts
@Injectable()
export class TabDataService {
  private data: any[] = [];

  addItem(item: any) {
    this.data.push(item);
  }

  getItems() {
    return [...this.data];
  }

  clear() {
    this.data = [];
  }
}

// tab.component.ts
@Component({
  selector: 'app-tab',
  template: `
    <div class="tab">
      <input [(ngModel)]="newItem" placeholder="输入内容">
      <button (click)="addItem()">添加</button>
      <ul>
        <li *ngFor="let item of items">{{ item }}</li>
      </ul>
    </div>
  `,
  providers: [TabDataService] // 每个标签页有自己的实例
})
export class TabComponent {
  newItem = '';
  items: any[] = [];

  constructor(private tabDataService: TabDataService) {
    this.items = this.tabDataService.getItems();
  }

  addItem() {
    this.tabDataService.addItem(this.newItem);
    this.items = this.tabDataService.getItems();
    this.newItem = '';
  }
}

// tabs-container.component.ts
@Component({
  selector: 'app-tabs-container',
  template: `
    <button (click)="addTab()">添加标签页</button>
    <div *ngFor="let tab of tabs; let i = index">
      <h3>标签页 {{ i + 1 }}</h3>
      <app-tab></app-tab>
    </div>
  `
})
export class TabsContainerComponent {
  tabs: number[] = [1, 2, 3];

  addTab() {
    this.tabs.push(this.tabs.length + 1);
  }
}

8. 注意事项

内存管理

  • 组件级别的服务实例会随着组件销毁而销毁
  • 手动创建的实例需要手动管理生命周期

性能考虑

  • 多个实例会增加内存使用
  • 根据实际需求选择合适的方式

依赖注入层级

// 不同层级的注入器会创建不同的实例
@NgModule()              // 模块级单例
@Component()            // 组件级实例
@Directive()            // 指令级实例

总结

选择哪种方式取决于具体需求:

  1. 组件级隔离 → 使用 providers: [Service]
  2. 配置不同的实例 → 使用工厂函数
  3. 模块级多实例 → 使用 providedIn: 'any'
  4. 动态创建 → 使用工厂服务手动创建
  5. 同一接口多个实现 → 使用 multi: true

最常用的是在组件级别提供 Service,这样每个组件实例都会得到自己的 Service 实例,实现了完全的隔离。

注意事项

如果是急加载模块(非懒加载模块),无论在多少个模块的 providers 中声明,Service 实例都是同一个(单例)。 Angular 的依赖注入系统在应用启动时创建了一个 根注入器(Root Injector) 。所有急加载模块的 providers 都会被合并到根注入器中。

模块类型 在多个模块的 providers 中声明 实例情况
急加载模块 同一个实例(单例)
懒加载模块 不同实例(每个模块新实例)
混合情况 急加载+懒加载都有声明 急加载用根实例,懒加载用新实例

关键点:

  • 所有急加载模块共享根注入器
  • providers 声明会被合并
  • Service 在应用启动时实例化一次
  • 要实现多实例,需要使用不同的 Token 或在更细粒度级别提供

Angular中懒加载模块的加载顺序总结

作者 ze_juejin
2025年12月1日 15:58

在 Angular 应用中,懒加载模块的加载顺序是基于路由导航的按需加载机制。以下是详细的加载顺序和过程:

1. 初始加载阶段

当用户首次访问 Angular 应用时:

  • 主模块(AppModule)和所有急加载模块被打包到 main.js 中
  • 懒加载模块被分离成独立的 chunk 文件

2. 路由导航触发加载

当用户导航到懒加载路由时:

触发时机:

const routes: Routes = [
  {
    path: 'lazy',
    loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)
  }
];

加载流程:

用户点击链接/导航 → Angular 路由检测到懒加载配置 → 
开始下载对应的 chunk 文件 → 文件下载完成 → 
模块被实例化 → 组件渲染

3. 实际加载顺序示例

项目结构:

src/
├── app/
│   ├── app.module.ts
│   ├── app-routing.module.ts
├── feature1/
│   ├── feature1.module.ts
│   └── feature1-routing.module.ts
├── feature2/
│   ├── feature2.module.ts
│   └── feature2-routing.module.ts

编译后的文件结构:

dist/
├── index.html
├── main.js                    # 主应用包
├── polyfills.js
├── runtime.js
├── styles.css
├── chunk-feature1.js         # 懒加载模块1
├── chunk-feature2.js         # 懒加载模块2
├── chunk-common.js           # 共享依赖
└── assets/

4. 具体加载顺序

第一次访问应用:

  1. 加载主包文件

    runtime.js → polyfills.js → main.js → styles.css
    
  2. 应用初始化

    • AppComponent 被初始化
    • 急加载的组件和模块被注册

导航到 /feature1:

// 当路由匹配到 feature1 时
http://localhost:4200/feature1

加载顺序:

  1. 检查路由配置 - 发现是懒加载路由
  2. 下载 chunk 文件 - 开始下载 chunk-feature1.js
  3. 模块实例化 - Feature1Module 被 Angular 编译器实例化
  4. 组件渲染 - Feature1Component 被渲染

预加载策略的影响

Angular 提供了不同的预加载策略:

默认策略(NoPreloading)

  • 只在需要时才加载懒加载模块
  • 顺序:用户导航 → 下载 → 实例化

预加载所有模块(PreloadAllModules)

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      preloadingStrategy: PreloadAllModules
    })
  ]
})

加载顺序:

  1. 主应用加载完成
  2. 空闲时后台下载所有懒加载模块
  3. 用户导航时立即实例化,无需等待下载

自定义预加载策略

@Injectable()
export class CustomPreloading implements PreloadingStrategy {
  preload(route: Route, load: Function): Observable<any> {
    return route.data && route.data.preload ? load() : of(null);
  }
}

5. Chunk 文件的命名和依赖

默认命名:

  • chunk-[contenthash].js(基于内容哈希)

自定义命名:

// angular.json
{
  "projects": {
    "my-app": {
      "architect": {
        "build": {
          "options": {
            "outputHashing": "all",
            "namedChunks": true  // 启用命名chunks
          }
        }
      }
    }
  }
}

共享依赖:

如果多个懒加载模块使用相同的第三方库,Webpack 会:

  1. 提取公共依赖到单独的 chunk(如 chunk-vendors.js
  2. 确保依赖只加载一次

6. 网络瀑布流示例

浏览器开发者工具 Network 标签显示:

Initial Load:
├── runtime.js (立即)
├── polyfills.js (并行)
├── main.js (并行)
└── styles.css (并行)

Navigation to /feature1:
└── chunk-feature1.js (按需)

Navigation to /feature2:
└── chunk-feature2.js (按需)

7. 优化建议

代码分割:

// 将大型组件单独懒加载
const routes: Routes = [
  {
    path: 'reports',
    loadChildren: () => import('./reports/reports.module')
      .then(m => m.ReportsModule)
  }
];

预加载关键模块:

const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule),
    data: { preload: true }  // 自定义预加载
  }
];

使用路由守卫控制加载:

const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module')
      .then(m => m.AdminModule),
    canLoad: [AuthGuard]  // 条件加载
  }
];

总结

懒加载模块的加载顺序原则:

  1. 按需加载 - 只在路由导航时触发
  2. 异步下载 - 通过网络获取 chunk 文件
  3. 按序实例化 - 下载完成后 Angular 实例化模块
  4. 缓存机制 - 已加载的模块不会重复下载

这种机制显著改善了:

  • 初始加载性能(减小主包体积)
  • 用户体验(快速首屏显示)
  • 资源利用率(只加载需要的代码)
❌
❌