在 Angular Universal 中内联 SVG 的最佳方法

cpp*_*udy 6 icons svg universal server-side-rendering angular

我们有一个Angular 16 Universal项目,我们希望找到使用 SVG 图标的最佳方式。性能对于我们的网络应用程序至关重要。我们不能使用 Icomoon 等字体,因为图标是多色的,并且很难定制和维护。

首先,我们开发了一个Angular 指令,可以在运行时内联图标。我们尝试了以下模式:

  • 仅客户端:当应用程序在浏览器中运行时,通过 HttpClient.get() 内联图标。但是,在加载整个 main.js(包含该指令)之前,不会开始下载图标。这会导致明显的闪烁。

  • SSR + 客户端:激活 Angular Hydration 后,服务器执行 get 调用来获取图标,客户端不会重复所述调用。这解决了闪烁问题,因为返回的页面已经包含 SVG。但是,我担心在服务器端引入这些请求时会产生瓶颈。

此外,图标目前由资产服务器提供服务,我们希望能够将我们的组件作为库发送给其他团队,以便他们可以重用它们。如果这些团队从不同的主机名 (CORS) 向我们的资产服务器发出请求,这可能会产生问题。因此,提出了一些建议:

  • 在构建时内联 SVG,特别是对于那些关键且必须始终显示的 SVG。这将解决我们指令的潜在问题,但会增加脚本的大小。而且,我还没有找到通过 Webpack 配置它的简单方法。将它们直接粘贴到我们的模板中似乎是一个不可取的解决方案。

  • 使用 asset 文件夹,以便在将我们的库传递给其他团队时将图标包含在 dist 文件夹中。

考虑到所有这些想法,将图标包含为 SVG 的最佳方式是什么?

Von*_*onC 2

扩展Eliseo答案,您可能会考虑一种在应用程序初始化期间预加载 SVG 图标的方法,然后利用Angular 组件内联显示这些图标,从而允许 CSS 自定义。
这将提供另一种方法来实现内联 SVG,而无需深入研究Webpack 配置

您的 Angular 项目的结构将是:

src/
|-- app/
    |-- components/
        |-- header/
            |-- header.component.html  // Template file for the header component
            |-- header.component.ts  // TypeScript file for the header component
            |-- header.component.css  // CSS file for the header component
    |-- core/
        |-- services/
            |-- svg.service.ts  // Service for fetching and storing SVG icons
    |-- shared/
        |-- components/
            |-- svg-icon/
                |-- svg-icon.component.ts  // Component for rendering SVG icons inline
    |-- modules/
        # other modules
    |-- app.module.ts  // Main application module where SVG_ICON_INITIALIZER is provided
|-- assets/
    |-- symbol-defs.svg  // SVG file containing symbol definitions for icons
|-- environments/
    |-- environment.ts
    # environment configuration files
|-- main.ts  // Main entry file for the application
|-- index.html  // Main HTML file
|-- styles.css  // Global styles
|-- # other files
Run Code Online (Sandbox Code Playgroud)

创建一个服务来处理获取和存储 SVG 图标。
并用于APP_INITIALIZER在应用程序初始化期间预加载 SVG 图标:这应该可以减轻“仅客户端”模式中提到的可察觉的闪烁,并可能缓解“SSR + 客户端”模式中的服务器瓶颈问题。

src/app/core/services/svg.service.ts

import { Injectable, APP_INITIALIZER, Provider } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class SvgService {
  private svgDefs: string;

  constructor(private http: HttpClient) {}

  loadSvgIcons(): Promise<void> {
    return this.http.get('assets/symbol-defs.svg', { responseType: 'text' })
      .toPromise()
      .then(svgDefs => {
        this.svgDefs = svgDefs;
      });
  }

  getIcon(iconId: string): string {
    const match = this.svgDefs.match(new RegExp(`<symbol id="${iconId}"[^>]*>((.|\\n)*)<\\/symbol>`));
    return match ? match[0] : '';
  }
}

export const SVG_ICON_INITIALIZER: Provider = {
  provide: APP_INITIALIZER,
  useFactory: (svgService: SvgService) => () => svgService.loadSvgIcons(),
  deps: [SvgService],
  multi: true,
};
Run Code Online (Sandbox Code Playgroud)

loadSvgIcons方法从本地资源文件 ('assets/symbol-defs.svg') 中获取 SVG 图标,并将 SVG 标记存储在属性中svgDefs。该getIcon(iconId: string)方法用于从svgDefs属性中检索特定的 SVG 图标数据。

然后创建一个 Angular 组件来内联渲染 SVG 图标。

src/shared/components/svg-icon/svg-icon.component.ts

import { Component, Input } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { SvgService } from './svg.service';

@Component({
  selector: 'svg-icon',
  template: `<div [innerHTML]="iconSvg"></div>`,
})
export class SvgIconComponent {
  @Input() icon: string;
  iconSvg: SafeHtml;

  constructor(private svgService: SvgService, private sanitizer: DomSanitizer) {}

  ngOnChanges() {
    const iconSvgString = this.svgService.getIcon(this.icon);
    this.iconSvg = this.sanitizer.bypassSecurityTrustHtml(iconSvgString);
  }
}
Run Code Online (Sandbox Code Playgroud)

旨在SvgIconComponent内联渲染 SVG 图标。输入icon属性用于指定图标 ID。该iconSvg属性用于存储 SVG 图标字符串。ngOnChanges生命周期钩子用于在iconSvg输入icon属性更改时更新属性。

在您的 Angular模块文件(例如app.module.ts)中,您可以SVG_ICON_INITIALIZERsvg.service.ts文件中导入 并将其添加到providers数组中,以确保在应用程序初始化期间预加载 SVG 图标。

src/app/app.module.ts

// app.module.ts
import { SVG_ICON_INITIALIZER } from './svg.service';

@NgModule({
  declarations: [SvgIconComponent],
  imports: [/* */],
  providers: [SVG_ICON_INITIALIZER],
  bootstrap: [/* */]
})
export class AppModule { }
Run Code Online (Sandbox Code Playgroud)

例如,在典型的 Angular 项目中,您可能在目录中或直接在目录下有一个header目录,其中包含标头组件模板的文件。componentsappheader.component.html

在该header.component.html文件中,您将使用该svg-icon组件来内联渲染 SVG 图标:

src/app/components/header/header.component.html(标头组件的模板文件):

<div class="header">
  <svg-icon icon="icon-1"></svg-icon>
  <!-- other header content -->
</div>
Run Code Online (Sandbox Code Playgroud)

现在 SVG 图标是内联渲染的,您可以根据需要应用 CSS 样式。

svg-icon svg {
  fill: currentColor;
}

svg-icon:hover svg {
  fill: gold;
}
Run Code Online (Sandbox Code Playgroud)

你得到:

 +------------------------+       +-------------------------+       +-------------------+
 | Angular Initialization |       | SvgService              |       | SvgIconComponent  |
 |                        |       |                         |       |                   |
 | APP_INITIALIZER        |       | loadSvgIcons()          |       | ngOnChanges()     |
 | (SVG_ICON_INITIALIZER) | ----> | getIcon(iconId: string) | ----> | render SVG inline |
 |                        |       |                         |       | with [innerHTML]  |
 +------------------------+       +-------------------------+       +-------------------+
                                      |                                          
                                      | Fetch SVG icons                           
                                      v                                          
                              +--------------------------+                             
                              | External Asset File      |                             
                              | (assets/symbol-defs.svg) |                             
                              +--------------------------+
Run Code Online (Sandbox Code Playgroud)
  • ( APP_INITIALIZERusing SVG_ICON_INITIALIZER)在Angular 的初始化阶段触发该loadSvgIcons()方法。SvgService
  • SvgService从外部资源文件 ( ) 中获取 SVG 图标assets/symbol-defs.svg
  • SvgIconComponent每当输入属性发生变化(触发)时,都会SvgService通过该方法获取特定的 SVG 图标数据。getIcon(iconId: string)iconngOnChanges()
  • SvgIconComponent[innerHTML]使用 Angular 的绑定将 SVG 标记注入到 DOM 中,在其模板内内联渲染 SVG 图标。

通过将 SVG 处理封装在服务和组件中,可以促进组件的可重用性以及与其他团队的共享:您应该能够将库传递给其他团队


注意:这确实使用了本地资源文件(“ assets/symbol-defs.svg”),该文件在构建过程中与应用程序捆绑在一起。
这很简单,并且避免了运行时获取 SVG 图标的任何网络请求,从而减轻了潜在的 CORS 问题。但是,它不利用外部资产服务器。

在 SVG 图标预计会频繁更改或需要在多个项目之间共享图标的情况下,使用外部资源服务器可能会有所帮助。

您需要更新SvgService以从资源服务器而不是本地“ assets/symbol-defs.svg”文件获取 SVG 图标。

src/app/core/services/svg.service.ts

@Injectable({ providedIn: 'root' })
export class SvgService {
  // previous code
  loadSvgIcons(): Promise<void> {
    // Update the URL to point to the asset server
    return this.http.get('https://assets-server.com/symbol-defs.svg', { responseType: 'text' })
      .toPromise()
      .then(svgDefs => {
        this.svgDefs = svgDefs;
      });
  }
  // rest of the code
}
Run Code Online (Sandbox Code Playgroud)

但您还需要处理从外部资源服务器获取 SVG 图标时可能出现的 CORS 问题。
确保资产服务器配置为允许来自托管 Angular 应用程序的域的跨域请求。这通常可以通过在资产服务器上设置适当的 CORS 标头来完成。

https://dev-academy.com/angular-cors/cors-request-and-response-flow.png

// Example CORS headers on the asset server
Access-Control-Allow-Origin: https://your-angular-app.com
Access-Control-Allow-Methods: GET, OPTIONS
Run Code Online (Sandbox Code Playgroud)

更多信息请参阅“ Angular CORS 指南:修复错误”,来自Saujan Ghimire

您可能还需要实施版本控制和缓存清除策略,以确保在有更新时从资产服务器获取最新版本的 SVG 图标。这可以通过在获取 SVG 图标时将版本查询参数附加到 URL 来完成。

version变量可以被硬编码为“ v1”。每当 SVG 图标有更新时,您都会将此值更新为新版本字符串,例如“ ” v2、“ v3”等。URL 中的更改会触发浏览器从服务器获取更新的 SVG 图标,绕过任何缓存版本。

src/environments/environment.ts

// src/environments/environment.ts
export const environment = {
  production: false,
  SVG_ICON_VERSION: 'v1',
  // other environment-specific configurations
};
Run Code Online (Sandbox Code Playgroud)

通过将SVG_ICON_VERSION变量放置在环境文件中,您可以对 SVG 图标进行特定于环境的版本控制。例如,与生产环境相比,您的开发环境中可能有不同版本的 SVG 图标。

然后,您将更新loadSvgIcons()中的方法,以便SvgService在获取 SVG 图标时将版本查询参数附加到 URL。
src/app/core/services/svg.service.ts

import { environment } from '../../../environments/environment';

@Injectable({ providedIn: 'root' })
export class SvgService {
  // previous code
  loadSvgIcons(): Promise<void> {
    const version = 'v1';  // Update this value whenever the SVG icons are updated
    return this.http.get(`https://assets-server.com/symbol-defs.svg?version=${version}`, { responseType: 'text' })
      .toPromise()
      .then(svgDefs => {
        this.svgDefs = svgDefs;
      });
  }
  // rest of the code
}
Run Code Online (Sandbox Code Playgroud)

这将利用外部资产服务器来提供 SVG 图标,从而促进更轻松的跨项目更新和共享图标。然而,它确实在运行时引入了网络请求来获取 SVG 图标,这可能会带来瓶颈。

+--------------------+          +-----------------+          +--------------------+
| Angular            |          | SvgService      |          | External Asset     |
| Application        |          |                 |          | Server             |
|                    |          | loadSvgIcons()  |          |                    |
| 1. Bootstraps      |          | 1. Fetches SVG  |          | 1. Serves SVG      |
| 2. Calls           |          |    icons from   |          |    icons           |
|    APP_INITIALIZER | -------> | external server | -------> | 2. Checks CORS     |
|    (loadSvgIcons)  |          |    with version |          |    headers         |
| 3. Renders         |          |    query param  |          | 3. Returns SVG     |
|    components      |          | 2. Stores SVG   |          |    icons           |
|    with SVG icons  | <------  |    icons        | <------- |                    |
+--------------------+          +-----------------+          +--------------------+
Run Code Online (Sandbox Code Playgroud)
  • Angular 应用程序引导并调用APP_INITIALIZER(触发loadSvgIcons中的方法SvgService)。
  • SvgService发送请求以从外部资源服务器获取 SVG 图标,并将版本查询参数附加到 URL。
  • 在检查 CORS 标头以确保请求来自允许的域后,外部资产服务器提供 SVG 图标。
  • SvgService存储获取的 SVG 图标以供以后使用。
  • Angular 应用程序渲染组件,这些组件使用存储的 SVG 图标来SvgService内联显示 SVG 图标。

我正在研究这个APP_INITIALIZER选项,但如果我是对的,在main.js完全加载之前,代码根本不会被执行。
这仍然会导致一些明显的闪烁,特别是如果main.js文件在未来继续增长的话。”

确实,APP_INITIALIZER令牌用于在应用程序启动时执行函数,但它会在main.js文件加载后运行。
如果main.js变大并且需要大量时间来加载,则在获取和显示 SVG 图标之前仍然会存在明显的延迟,这可能会导致闪烁。

为了缓解这种情况,您可以采取以下几种方法:

  • 您可以尝试优化捆绑包大小:减小捆绑包的大小main.js将导致更快的加载时间。这可以通过代码分割、tree-shaking 和其他捆绑优化技术来实现。

  • 通过服务器端渲染,可以在将 SVG 图标发送到客户端之前将其获取并内联到服务器上的 HTML 中。这将消除闪烁,因为图标已经存在于初始渲染中。

  • 另一种方法是在第一次加载后实现 Service Worker 来缓存 SVG 图标,从而消除后续加载中的网络延迟。Service Worker 可以预加载必要的资源,包括 SVG 图标,确保它们在应用程序加载后立即可用。

  • 如果 SVG 图标托管在内容交付网络 (CDN) 上,则它们的加载速度可能比从单个服务器加载得更快,尤其是在 CDN 针对交付静态资产进行了优化的情况下。

  • HTTP/2 或 HTTP/3 协议还可以帮助并行加载资源,从而减少总体加载时间。