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

Angular项目中部分Google Material Icons无法在浏览器显示

Angular自定义Material图标库部分图标无法渲染问题排查

我正在为Angular项目开发自定义图标库,由于Font Awesome多数图标不再免费,选择采用Google Material Icons并通过样式表集成,同时创建了IconService和IconDirective来简化图标调用。但实际运行时,部分图标(如progress_activity)无法在浏览器中正常渲染,已尝试更换浏览器、调整CSS字体设置等方法,问题仍未解决。

相关代码

icon.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class IconsService {
  constructor(private http: HttpClient, private sanitizer: DomSanitizer) {}

  getIcon(name: string): Observable<SafeHtml> {
    return this.http.get(`assets/icons/${name}.svg`, { responseType: 'text' }).pipe(
      map(svgString => this.sanitizer.bypassSecurityTrustHtml(svgString) as SafeHtml) // assert non-null
    );
  }
}

IconDirective

import {Directive, ElementRef, Input, Renderer2} from '@angular/core';

@Directive({
  selector: '[icon]',
  standalone: true,
})
export class IconDirective {
  private _iconName: string = '';
  private _spin: boolean = false;
  private _size: string | undefined;
  private _flipHorizontal: boolean = false;
  private _flipVertical: boolean = false;
  private _color: string | undefined;

  @Input('icon')
  set icon(value: string) {
    const parts = value.split(' ');
    this._iconName = parts[0];

    this._spin = parts.includes('spin');

    this._flipHorizontal = parts.includes('flip-horizontal');

    this._flipVertical = parts.includes('flip-vertical');

    this._size = parts.find(part => part.match(/^[0-5]x$/)) || '1x'; // Default to 1x

    const colorPart = parts.find(part => part.startsWith('color-'));
    this._color = colorPart ? colorPart.split('-')[1] : undefined;

    this.updateIcon();
  }

  constructor(private el: ElementRef, private renderer: Renderer2) {
  }

  ngAfterViewInit(): void {
    this.setupLazyLoading();
  }

  private setupLazyLoading(): void {
    const options = {
      root: null, // observes changes in the viewport
      threshold: 0.01, // trigger when 1% of the element is visible
    };

    const observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.updateIcon();
          observer.unobserve(entry.target); // Stop observing once loaded
        }
      });
    }, options);

    observer.observe(this.el.nativeElement);
  }

  private updateIcon(): void {
    console.log('Updating icon');
    // Clear previous classes
    this.el.nativeElement.className = '';

    // Set text content to the icon name
    this.renderer.setProperty(this.el.nativeElement, 'textContent', this._iconName);
    this.renderer.addClass(this.el.nativeElement, 'material-icons');

    // Apply size, spin, and flip classes
    if (this._spin) {
      this.renderer.addClass(this.el.nativeElement, 'spin');
    }
    if (this._flipHorizontal) {
      this.renderer.addClass(this.el.nativeElement, 'flip-horizontal');
    }
    if (this._flipVertical) {
      this.renderer.addClass(this.el.nativeElement, 'flip-vertical');
    }
    if (this._size) {
      this.renderer.addClass(this.el.nativeElement, `icon-${this._size}`);
    }
    if (this._color) {
      this.renderer.setStyle(this.el.nativeElement, 'color', this._color);
    }
  }
}

HTML代码

<h1>Icon examples</h1>
<h2>Sizing: <i icon="search"></i><i icon="search 2x"></i><i icon="search 3x"></i></h2>
<h2>Spinning: <i icon="search spin"></i></h2>
<h2>Flipping horizontally and vertically: <i icon="search flip-horizontal"></i><i icon="search flip-vertical"></i></h2>
<h2>Colors: <i icon="search color-blue"></i><i icon="search color-red"></i><i icon="search color-green"></i></h2>
<h1>Button example: <button><i icon="donut_large spin"></i>Loading</button></h1>
<button><i icon="settings 2x"></i></button>
<button><i icon="menu 2x"></i></button>
<!--<button><i icon="stacks 2x"></i></button>-->
<button><i icon="print 2x"></i></button>
<button><i icon="open_with 2x"></i></button>
<button><i icon="add 2x"></i></button>
<button><i icon="public 2x"></i></button>
<!--<button><i icon="captive_portal 2x"></i></button>-->
<button><i icon="filter_alt 2x"></i></button>
<button><i icon="filter_alt_off 2x"></i></button>
<button><i icon="arrow_forward_ios 2x"></i></button>
<button><i icon="arrow_back_ios 2x"></i></button>
<button><i icon="pan_tool 2x"></i></button>
<button><i icon="progress_activity 2x"></i></button>

CSS样式

/*Icon base styles */
[icon] {
  display: inline-block;
  font-size: 16px;
  padding: 2px;
  fill-opacity: 0;
  vertical-align: middle;
  horiz-align: center;
}

 @font-face {
   font-family: 'Material Icons';
   font-style: normal;
   font-weight: 400;
   src: url(https://example.com/MaterialIcons-Regular.eot); /* For IE6-8 */
   src: local('Material Icons'),
   local('MaterialIcons-Regular'),
   url(https://example.com/MaterialIcons-Regular.woff2) format('woff2'),
   url(https://example.com/MaterialIcons-Regular.woff) format('woff'),
   url(https://example.com/MaterialIcons-Regular.ttf) format('truetype');
 }


/* Icon size variations */
@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.spin {
  animation: spin 2s infinite linear;
  padding: 2px;
}

 .flip-horizontal {
   transform: scaleX(-1);
 }

 .flip-vertical {
   transform: scaleY(-1);
 }

 .flip-left {
   transform: rotateY(180deg);
 }

 .flip-right {
   transform: rotateY(-180deg);
 }

 .material-icons.icon-1x { font-size: 16px; } /* Default Material Icons size */
 .material-icons.icon-2x { font-size: 32px; }
 .material-icons.icon-3x { font-size: 48px; }
 .material-icons.icon-4x { font-size: 64px; }

问题排查与解决方案

1. 图标名称不匹配官方规范

progress_activity并非Google Material Icons官方提供的字体图标名称,先去官方图标库确认对应功能的正确图标名(比如progress_bar、circular_progress等),或者检查是否存在拼写错误。

2. 字体文件加载失败

CSS中@font-face使用的是占位地址https://example.com,导致字体文件无法正常加载。替换为有效的CDN地址或本地字体路径:

@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/materialicons/v140/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2) format('woff2');
}

如果用本地字体,确保路径正确,比如url('assets/fonts/MaterialIcons-Regular.woff2')。

3. 指令未正确使用IconService

你创建了IconsService用于加载SVG图标,但IconDirective的updateIcon方法只是把图标名设为文本内容,实际是按字体图标逻辑实现的。如果要使用SVG,修改updateIcon并注入服务:

// 先在构造函数注入IconsService
constructor(private el: ElementRef, private renderer: Renderer2, private iconsService: IconsService) {}

private updateIcon(): void {
  console.log('Updating icon');
  this.el.nativeElement.innerHTML = '';
  this.el.nativeElement.className = '';

  this.iconsService.getIcon(this._iconName).subscribe(svg => {
    this.el.nativeElement.innerHTML = svg;
    // 给SVG元素添加样式类
    const svgEl = this.el.nativeElement.firstChild;
    if (this._spin) this.renderer.addClass(svgEl, 'spin');
    if (this._size) this.renderer.addClass(svgEl, `icon-${this._size}`);
    // ...其他样式逻辑
  });
}

4. CSS属性冲突

基础样式中设置了fill-opacity: 0,如果是SVG图标会直接透明,移除该属性,或针对SVG单独设置:

[icon] {
  display: inline-block;
  font-size: 16px;
  padding: 2px;
  vertical-align: middle;
  /* 移除 fill-opacity: 0; */
}

[icon] svg {
  fill: currentColor;
}

5. 懒加载重复触发问题

@Input设置时和懒加载触发时会重复调用updateIcon,添加标志位避免重复操作:

private isLoaded = false;

private setupLazyLoading(): void {
  const options = { root: null, threshold: 0.01 };
  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting && !this.isLoaded) {
        this.updateIcon();
        this.isLoaded = true;
        observer.unobserve(entry.target);
      }
    });
  }, options);
  observer.observe(this.el.nativeElement);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 17:54:53