You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular中Loader与c3.js饼图子组件加载状态异常排查

问题描述

我尝试实现CSS Loader的显示逻辑:当API获取数据完成、isLoading变为false时,Loader停止显示,子组件生成的c3.js饼图在父组件HTML中渲染。

饼图基于c3.js生成,问题在于API获取数据后isLoading未变为false,Loader持续加载,饼图无法显示。

调试发现isLoading = false并未触发Loader隐藏和饼图展示,不清楚具体原因。

相关代码

子组件 un-member-chart.component.ts

负责通过API获取数据并生成饼图:

import { Component, OnInit } from '@angular/core';
import { CountryService } from '../services/country.service';
import { BehavioralChangesService } from '../services/behavioral-changes.service';
import * as c3 from 'c3';

@Component({
  selector: 'app-un-member-chart',
  templateUrl: './un-member-chart.component.html',
  styleUrls: ['./un-member-chart.component.scss']
})
export class UnMemberChartComponent implements OnInit {
  unMemberCount = 0;
  nonUnMemberCount = 0;
  constructor(private countryService: CountryService, 
    private behavioralChangesService: BehavioralChangesService) { }

  ngOnInit(): void {
    this.fetchUnMemberData();
  }

  fetchUnMemberData() {
    this.behavioralChangesService.setIsLoading(true);
    this.countryService.getCountriesData().subscribe(undata => {
      this.unMemberCount = undata.filter(country => country.unMember === true).length;
      this.nonUnMemberCount = undata.filter(country => country.unMember === false).length;
      
      this.behavioralChangesService.setIsLoading(false); // comment no. 1 for reference
      this.generateChartData(); // comment no. 2  for reference
    },
    error => {
      console.error('Error fetching data:', error);
      this.behavioralChangesService.setIsLoading(false);
    });
  }

  generateChartData(): void {
    const chart = c3.generate({
      bindto: '#UnPieChart',
      data: {
        columns: [
          ['UN Members', this.unMemberCount],
          ['non-UN Members', this.nonUnMemberCount],
        ],
        type: 'pie',
        colors: {
          'UN Members': '#9bc400',
          'non-UN Members': '#9080ff',
        },
      },
      title: {
        text: '% of UN Members in the World',
      }
    });
  }
}

子组件模板 un-member-chart.component.html

<div id="UnPieChart"></div>

API服务 country.service.ts

const allDataUrl = 'https://restcountries.com/v3.1/all';
export class CountryService {
// to fetch all data of the World
  getCountriesData(): Observable<any[]> {
    return this.http.get<any[]>(allDataUrl);
  }
}

状态管理服务 behavioral-changes.service.ts

用于管理isLoading状态:

import { ChangeDetectorRef, Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class BehavioralChangesService {
  private isLoadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
  public isLoading$ = this.isLoadingSubject.asObservable();

  constructor(private cdr: ChangeDetectorRef) { }

  setIsLoading(isLoading: boolean):void {
    this.isLoadingSubject.next(isLoading);
    this.cdr.detectChanges(); 
  }
}

父组件 home.component.ts

使用OnPush变更检测策略:

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { CountryService } from '../services/country.service';
import { BehavioralChangesService } from '../services/behavioral-changes.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeComponent implements OnInit {
  isSidebarOpen = true; // sidebar toggle initial
  isLoading:boolean = false;

  constructor(private SidebarToggleService: CountryService,
    private behavioralChangesService: BehavioralChangesService) {}

  ngOnInit(): void {
    this.SidebarToggleService.isOpen$.subscribe(isOpen => {
      this.isSidebarOpen = isOpen;
    });

    // for loader behaviour
    this.behavioralChangesService.isLoading$.subscribe(isLoading => {
      this.isLoading = isLoading;
    })
  }

  getSidebarWidth() {
    return this.isSidebarOpen ? '250px' : '0'; // Set width based on open/close state
  }
  getFlexContainerMargin() {
    return this.isSidebarOpen ? '250px' : '0'; // Adjust margin based on sidebar state
  }
}

父组件模板 home.component.html

控制Loader和子组件的显示:

<div class="card">
    <div *ngIf="isLoading">
    <!-- Loader template -->
        <div class="loader"></div>
    </div>

    <!-- Show <app-un-member-chart> when isLoading is false -->
    <ng-container *ngIf="!isLoading">
        <app-un-member-chart></app-un-member-chart>
    </ng-container>
</div>

已尝试方案

使用@Output和EventEmitter传递isLoading状态,但同样无法将isLoading=false传递给父组件:

修改后的子组件代码片段

isLoading:boolean = true;
@Output() isLoadingChange:EventEmitter<boolean> = new EventEmitter<boolean>();

fetchUnMemberData() {
    this.isLoading = true;
    this.emitIsLoadingState();
    this.countryService.getCountriesData().subscribe(undata => {
      this.unMemberCount = undata.filter(country => country.unMember === true).length;
      this.nonUnMemberCount = undata.filter(country => country.unMember === false).length;
      
      this.isLoading = false;    // 8
      this.emitIsLoadingState(); // 9
      this.generateChartData(); // 10
    },
}

emitIsLoadingState() {
    this.isLoadingChange.emit(this.isLoading);
}

修改后的父组件模板

<div class="card">
<div *ngIf="isLoading">
<!-- Loader template -->
<div class="loadImage"></div>
</div>
<ng-container *ngIf="!isLoading">
<!-- Show <app-un-member-chart> when isLoading is false -->
<app-un-member-chart (isLoadingChange)="handleIsLoadingState($event)"></app-un-member-chart>
</ng-container>
</div>

期望效果

API数据获取完成后,Loader立即停止显示,子组件生成的饼图在父组件HTML中正常渲染。


解决方案

核心问题分析

问题出在父组件使用了ChangeDetectionStrategy.OnPush,但状态更新时没有触发变更检测:

  1. 当通过BehavioralChangesService更新isLoading时,父组件的isLoading变量虽然被赋值,但OnPush策略下Angular不会自动检测变更;
  2. 使用@Output方案时,同样因为OnPush,父组件接收事件后没有触发变更检测,UI不会更新。

具体修复方案

方案一:优化状态管理服务(推荐)

移除服务中不必要的ChangeDetectorRef,改为在父组件中利用async管道自动触发变更检测:

  1. 修改home.component.ts,直接使用isLoading$ Observable,无需手动订阅:
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { CountryService } from '../services/country.service';
import { BehavioralChangesService } from '../services/behavioral-changes.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeComponent implements OnInit {
  isSidebarOpen = true;
  isLoading$: Observable<boolean>; // 直接使用Observable

  constructor(private SidebarToggleService: CountryService,
    private behavioralChangesService: BehavioralChangesService) {}

  ngOnInit(): void {
    this.SidebarToggleService.isOpen$.subscribe(isOpen => {
      this.isSidebarOpen = isOpen;
    });

    // 直接赋值Observable,无需手动订阅
    this.isLoading$ = this.behavioralChangesService.isLoading$;
  }

  // 原有方法保持不变
  getSidebarWidth() {
    return this.isSidebarOpen ? '250px' : '0';
  }
  getFlexContainerMargin() {
    return this.isSidebarOpen ? '250px' : '0';
  }
}
  1. 修改home.component.html,使用async管道:
<div class="card">
    <div *ngIf="isLoading$ | async">
        <div class="loader"></div>
    </div>

    <ng-container *ngIf="!(isLoading$ | async)">
        <app-un-member-chart></app-un-member-chart>
    </ng-container>
</div>
  1. 清理behavioral-changes.service.ts中的冗余代码:
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class BehavioralChangesService {
  private isLoadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
  public isLoading$ = this.isLoadingSubject.asObservable();

  constructor() { } // 移除ChangeDetectorRef

  setIsLoading(isLoading: boolean):void {
    this.isLoadingSubject.next(isLoading);
    // 移除detectChanges调用
  }
}

方案二:手动触发变更检测(如果坚持手动订阅)

如果不想使用async管道,在父组件中注入ChangeDetectorRef,订阅时手动触发变更检测:

import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { CountryService } from '../services/country.service';
import { BehavioralChangesService } from '../services/behavioral-changes.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeComponent implements OnInit {
  isSidebarOpen = true;
  isLoading:boolean = false;

  constructor(private SidebarToggleService: CountryService,
    private behavioralChangesService: BehavioralChangesService,
    private cdr: ChangeDetectorRef) {} // 注入ChangeDetectorRef

  ngOnInit(): void {
    this.SidebarToggleService.isOpen$.subscribe(isOpen => {
      this.isSidebarOpen = isOpen;
      this.cdr.markForCheck(); // 手动标记需要检测变更
    });

    this.behavioralChangesService.isLoading$.subscribe(isLoading => {
      this.isLoading = isLoading;
      this.cdr.markForCheck(); // 手动标记需要检测变更
    })
  }

  // 原有方法保持不变
  getSidebarWidth() {
    return this.isSidebarOpen ? '250px' : '0';
  }
  getFlexContainerMargin() {
    return this.isSidebarOpen ? '250px' : '0';
  }
}

额外注意事项

  • 确保countryService.getCountriesData()确实能正常返回数据,可以在订阅的next回调中加入console.log(undata)验证;
  • c3.js生成饼图时,要确保#UnPieChart元素已经存在于DOM中(因为子组件只有在isLoading为false时才会渲染,所以这一点已经满足);
  • 如果使用@Output方案,同样需要在父组件的handleIsLoadingState方法中调用this.cdr.markForCheck()来触发变更检测。

内容的提问来源于stack exchange,提问作者shubham79

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 19:49:49