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,但状态更新时没有触发变更检测:
- 当通过
BehavioralChangesService更新isLoading时,父组件的isLoading变量虽然被赋值,但OnPush策略下Angular不会自动检测变更; - 使用
@Output方案时,同样因为OnPush,父组件接收事件后没有触发变更检测,UI不会更新。
具体修复方案
方案一:优化状态管理服务(推荐)
移除服务中不必要的ChangeDetectorRef,改为在父组件中利用async管道自动触发变更检测:
- 修改
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'; } }
- 修改
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>
- 清理
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
相关产品推荐
相关产品推荐

