Angular最近几个版本都更新了哪些有意思的功能? Part 1

414 阅读15分钟

1. Deferrable views • Angular

defer 是 Angular 17 中引入的一种新的模块加载方式。它可以在组件模板中使用,延迟加载该模板中的某些内容。这包括组件、指令和管道,以及任何相关的 CSS。

你可以将模板的一部分包裹在一个 @defer 控制块中,并且指定加载条件。

@defer (when cond) {
  <large-component />
}

defer 可以用来减少应用加载的初始包大小,或延迟加载那些可能直到以后也不会加载的重量级组件。这将会带来更快的初始加载速度。

为了使 @defer 控制块内的依赖项能够延迟加载,它们需要满足两个条件:

  1. 必须是standalone的。非 standalone 的依赖项无法延迟加载,即使在 @defer 控制块内也会被急性加载。
  2. 不能在 defer 块之外的同一文件中直接引用要延迟加载的内容,包括 ViewChild 查询,举个例子,假设你已经在组件A内使用 defer 定义了B组件要延迟加载,那就不能在组件 A 内再引用组件 B了,这会导致代码在访问 B 的引用时被提前加载)。

另外 defer 块中使用的组件、指令、管道和传递依赖项可以是 standalone 或基于 NgModule 的,都会被延迟加载。

那什么是传递依赖项呢?

其实它指的是这些依赖项不是由组件、指令或管道直接导入的,但这些组件、指令或管道的依赖项需要这些依赖项。例如,如果组件 A 依赖于服务 B,而服务 B 依赖于服务 C,则服务 C 是组件 A 的传递依赖项。让我们考虑一个例子来说明这一点:

  • 组件 A 在延迟块中使用。

  • 组件 A 依赖于指令 B 和管道 C。

  • 指令 B 依赖于服务 D。

  • 管道 C 依赖于服务 E。

说明

  • 组件 A 是被推迟的主要组件。

  • 指令 B 和管道 C 是组件 A 的直接依赖项。

  • 服务 D 和服务 E 是传递依赖项(依赖项的依赖项)。

Standalone 与基于 NgModule

指令 B 和管道 C 可以是 standalone 的,也可以在 NgModule 中声明。

服务 D 和服务 E 可以在 standalone 组件中单独提供,也可以在 NgModule 中提供。

当组件 A 被延迟时,Angular 还将延迟加载指令 B、管道 C、服务 D 和服务 E。

无论这些依赖项是 standalone 的还是 NgModule 的一部分,此延迟都适用。

总结一下这句话的意思是:

当使用 defer 块在 Angular 中延迟加载组件、指令和管道时,它们的所有依赖项(直接依赖和传递依赖)也将被延迟。无论这些依赖项是 standalone 的还是 NgModule 的一部分,这都适用。defer 会确保整个依赖关系图都被延迟加载,仅在需要时加载必要的内容,从而优化应用程序的性能。

注意: 尽量将任何延迟加载的组件放置在对用户不可见的位置。一旦依赖项加载完毕,布局可能会发生变化,导致布局抖动、重排重绘。

defer包裹的内容什么时候被加载呢

  1. trigger触发器被触发时加载
  2. prefetching 预加载
  3. when 条件表达式满足时加载
  4. prefetch when, prefetch on 自定义条件预加载 

除此之外,defer还支持配置 placeholder、 loading,分别提供未加载加载中显示的内容。在加载失败时你还可以配置 error 提供异常时显示的内容。

下面我们会逐个介绍这些配置。

@placeholder

@placeholder 是一个可选的块,用于声明触发加载之前要显示的内容,一旦加载完成,占位符内容将被主内容替换,像是为 input 标签设置 placeholder。你可以在占位符部分使用任何内容,包括纯 HTML、组件、指令和管道,但是占位符块的依赖项是急性加载的。

@defer {
  <large-component />
} @placeholder (minimum 500ms) {
  <p>Placeholder content</p>
}

minimum 参数指定此占位符应显示的最小时间,可以毫秒(ms)或秒(s)为单位。

@loading

@loading 是一个可选的块,允许你定义在触发加载后,加载过程中显示的视图,一般用于显示一个加载动画。一旦触发加载, @loading 的内容将替换 @placeholder的内容。其依赖项被急性加载,与 @placeholder 类似。

@defer {
  <large-component />
} @loading (after 100ms; minimum 1s) {
  <img alt="loading..." src="loading.gif" />
}
  • after 用于指定在加载开始后, 显示加载模板前的等待时间。
  • minimum 与在 @placeholder中的使用方式相同,用于指定 loading 内容应显示的最短时间。

@error

@error 是一个可选的控制块,允许你声明在延迟加载失败时显示的内容。与 @placeholder 和 @loading 类似, @error 控制块的依赖项也会被急性加载。

@defer {
  <large-component />
} @error {
  <p>Failed to load the component</p>
}

Trigger

@defer 支持两种触发器: on 和 when

on支持如下几种情况:

  • idle

在浏览器空闲时(使用 requestIdleCallback API 检测)触发延迟加载。这也是 @defer 的默认行为。

  • viewport

在指定内容进入视口时(使用 IntersectionObserver API)触发延迟加载。 默认情况下,只要该占位符是单个根元素节点,它将作为进入视口的被观察元素。

@defer (on viewport) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}

或者,你可以在与 @defer 控制块相同的模板中指定一个模板引用变量,作为被观察进入视口的元素。此变量作为参数传递给视口触发器。

<div #greeting>Hello!</div>
@defer (on viewport(greeting)) {
  <greetings-cmp />
}
  • interaction 

将在用户通过 click 或 keydown 事件与指定元素交互时触发延迟块。

@defer (on interaction) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}

或者与 viewport 类似,或者你可以指定一个模板引用变量,作为触发交互的元素。

<button type="button" #greeting>Hello!</button>
@defer (on interaction(greeting)) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}
  • hover 

在鼠标悬停于触发区域时触发延迟加载, 与上面两个类似,你可以指定一个模板引用变量,作为触发交互的元素,这里就不再提供示例了。

@defer (on hover) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}
  • immediate 

立即触发延迟加载,这意味着客户端完成渲染后,延迟块将立即开始获取。

@defer (on immediate) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}
  • timer(x) 

将在指定的持续时间后触发,可以以 ms 或 s 为单位指定。

@defer (on timer(500ms)) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}

when

when 作为一个返回布尔值的表达式来指定条件。当此表达式变为真值时,占位符将被延迟加载的内容替换, 表达式可以是异步的。

但如果 condition 的初始值为 true, 后续 when 条件切换回 false,延迟控制块不会恢复到占位符。因为替换是一次性操作。如果控制块内的内容应有条件地渲染,可以在控制块内使用 if

@defer (when cond) {
  <calendar-cmp />
}

你也可以在一个语句中同时使用 when 和 on,只要任一条件满足,替换就会被触发, 多个 on 触发器也是一样,同时使用时,判断条件始终是 OR 。

@defer (on viewport; on hover; when cond) {
  <calendar-cmp />
} @placeholder {
  <img src="placeholder.png" />
}

prefetching

@defer 允许指定何时应该触发依赖项的预加载。你可以使用 prefetch 关键字。 prefetch 语法与之前的延迟条件类似,接受 when 和/或 on 来声明触发器。

prefetch when 和 prefetch on 控制何时预先获取资源。这允许你定义更高级的行为,例如在用户实际看到之前,或者与延迟块交互之前开始预取资源。 在下面的示例中,当浏览器变为空闲时开始预取,而块内容在交互时渲染。

@defer (on interaction; prefetch on idle) {
  <calendar-cmp />
} @placeholder {
  <img src="placeholder.png" />
}

2. Image Optimization • Angular

NgOptimizedImage 支持通过配置自动生成 srcset 属性,同时支持配置延迟加载或者急性加载,显著提升LCP指标(Largest Contentful Paint (LCP)  |  Articles  |  web.dev),并且提供占位符等功能,提升用户体验。

注意:尽管 NgOptimizedImage 指令在 Angular 版本 15 中成为了一个稳定特性,但它已被回迁,并在 13.4.0 和 14.3.0 版本中也作为一个稳定特性提供。

什么是 srcset 呢?

srcset 属性一般用于 <img> 标签,它提供一组图像资源,以便浏览器根据设备的显示特性(如屏幕分辨率和视口大小)选择最合适的图像,从而提升性能和用户体验。

<img 
  src="small.jpg" 
  srcset="
    small.jpg 480w, 
    medium.jpg 800w, 
    large.jpg 1200w, 
    xlarge.jpg 1600w" 
    sizes="(max-width: 600px) 480px, (max-width: 900px) 800px, (max-width: 1200px) 1200px, 1600px" 
    alt="Example image"
  >

在这个示例中:

  • src 提供默认图像,当浏览器不支持 srcset 时使用。
  • 如果视口宽度小于或等于 600px,浏览器会选择 small.jpg
  • 如果视口宽度在 601px 到 900px 之间,浏览器会选择 medium.jpg
  • 如果视口宽度在 901px 到 1200px 之间,浏览器会选择 large.jpg
  • 如果视口宽度大于 1200px,浏览器会选择 xlarge.jpg

srcset的工作原理

  1. 浏览器根据视口宽度和 sizes 属性计算出需要的图像尺寸。
  2. 浏览器从 srcset 属性中选择最接近计算尺寸的图像资源进行加载。

较为完整的使用 NgOptimizedImage 的例子

import { Component } from '@angular/core';
import {
  IMAGE_CONFIG,
  IMAGE_LOADER,
  ImageLoaderConfig,
  NgOptimizedImage,
} from '@angular/common';

@Component({
  selector: 'app-optimize-img',
  standalone: true,
  imports: [NgOptimizedImage],
  templateUrl: './optimize-img.component.html',
  styleUrl: './optimize-img.component.scss',
  providers: [
    {
      provide: IMAGE_LOADER,
      useValue: (config: ImageLoaderConfig) => {
        if (config.width === undefined) {
          return config.src;
        }
        return `${config.width}-${config.src}`;
      },
    },
    {
      provide: IMAGE_CONFIG,
      useValue: {
        // placeholder img width
        placeholderResolution: 40,
      },
    },
  ],
})
export class OptimizeImgComponent {}

<img
  class="image-class"
  ngSrc="SpongeBob.png"
  width="3840"
  height="2160"
  priority
  ngSrcset="400w,600w,900w,1200w,1920w,3840w"
  sizes="(max-width: 800px) 70vw, (max-width: 1200px) 100vw"
  placeholder
/>

除此之外我们还需要提供不同尺寸的图片, 作为示例我这里只提供一部分图片

image.png

img标签最终会被编译为下面这样

<img 
    ngsrc="SpongeBob.png" 
    width="3840" 
    height="2160" 
    priority="" 
    ngsrcset="ngsrcset="400w,600w,900w,1200w,1920w,3840w"" 
    sizes="(max-width: 800px) 70vw, (max-width: 1200px) 100vw" 
    placeholder="" 
    class="image-class"
    loading="eager" 
    fetchpriority="high" ng-img="true" 
    src="SpongeBob.png" 
    srcset="400-SpongeBob.png 400w, 600-SpongeBob.png 600w, 900-SpongeBob.png 900w, 1200-SpongeBob.png 1200w, 1920-SpongeBob.png 1920w, 3840-SpongeBob.png 3840w" 
>

我们可以看一下效果,浏览器自动根据视口大小,获取了不同尺寸的图片。

Recording 2024-11-29 102834.gif

上面的示例中,总共做了如下配置:

  • 模板中配置width,height,priority,ngSrcset,sizes, placeholder属性
  • class 中配置 loader

分别解释下这些配置的作用

width,height

如果不预先提供宽高,浏览器不会为图片预留大小,当图片被加载完后,会导致布局发生变化,进行重排,所以NgOptimizedImage 要求必须为图片指定高度和宽度。

对于 响应式图片(你已经根据视口大小调整了大小的图片), width 和 height 属性应该设置为图片的原始大小。另外还需要 为 sizes 设置一个值。size 的用法我们下面再说。

对于固定大小的图片width 和 height 属性应该设置为你所需渲染的大小。但是要注意宽高比应该始终与图片文件的原始宽高比一致,不然会产生形变,angular也会在开发模式的控制台中发出警告。

如果你有一个具有已知宽高的父容器,并且希望将图片放到该容器中,那也可以不设置宽高,添加 fill 属性即可,不过为了为了正确渲染fill图片,其父元素必须使用 position: "relative"position: "fixed" 或 position: "absolute" 来设置样式。

另外根据图片的样式,添加 width 和 height 属性可能会导致图片的渲染方式不同。如果你的图片样式正在以扭曲的纵横比渲染图片,NgOptimizedImage 会在控制台发出警告(仅在开发环境下)。

image.png

可以通过将 height: auto 或 width: auto 添加到图片的样式中来解决此问题。

priority

将图片标记为 priority后,该指令会自动应用以下优化:

  • img 标签设置 fetchpriority=high (阅读有关优先级的解释)
  • 设置 loading=eager (阅读有关惰性加载的解释)
  • 如果在服务器上渲染,则自动生成一个 preload 链接元素

在开发环境下,如果 LCP 元素是一个没有 priority 属性的图像, Angular 会在控制台中抛出一个 Error, 提示你这可能会严重影响加载性能。

常见的LCP元素

  • 图像 (<img> 标签)
  • 视频 (<video> 标签)
  • 包含背景图像的块级元素 (<div><section> 等)
  • 大块文本 (<h1><p> 等)

ngSrcset sizes

这两个属性通常会一起使用, NgOptimizedImage 会根据ngSrcsetSizes 自动生成 srcset 属性。

ngSrcset 指定需要根据视口宽度自适应的尺寸有哪些,Sizes指定一个百分比,那么ngSrcset 是怎么配合 sizes 使用的呢?我们先来看一个例子:

假设需求是在视口宽度为1366px时,使用一张宽高为1366×768的图片,在视口宽度为 1920px时,使用宽高为 1920×1080的图片,那么我们需要配置

<img
 ...
 srcset="./xxxpath/1366-example.png 1366w, ./xxxpath/1920-example.png 1920w"
 sizes="100vw"
>

当我们使用 NgOptimizedImage 指令时

  1. 通过 ngSrc提供一个默认的图片源,
  2. 配置 sizes 为 100vw
  3. ngSrcset 声明我们需要的尺寸。
<img
 ...
  ngSrc="example.png"
  sizes="100vw"
  ngSrcset="1366w,1920w"
>
  1. 提供一个自定义的 loader,去生成图片的URL
providers: [
    {
      provide: IMAGE_LOADER,
      useValue: (config: ImageLoaderConfig) => {
        // width from ngSrcset config
        if (config.width === undefined) {
          // return default src
          return config.src;
        }
        return `./xxxpath/${config.width}-${config.src}`;
      },
    }
  ],
  
 // 这里贴出 ImageLoaderConfig的属性
export declare interface ImageLoaderConfig {
    /**
     * Image file name to be added to the image request URL.
     */
    src: string;
    /**
     * Width of the requested image (to be used when generating srcset).
     */
    width?: number;
    /**
     * Whether the loader should generate a URL for a small image placeholder instead of a full-sized
     * image.
     */
    isPlaceholder?: boolean;
    /**
     * Additional user-provided parameters for use by the ImageLoader.
     */
    loaderParams?: {
        [key: string]: any;
    };
}

然后该指令就会生成一模一样的配置,如果你想要遵循一份标准的尺寸配置,那么可以不指定 ngSrcset,只保留 sizes 配置,angular 默认会帮你以 [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840] 进行设置,从而生成 srcset

如果你想要替换这些默认值,除了手动设置 ngSrcset 以外,还可以用 IMAGE_CONFIG 来实现:

providers: [
  {
    provide: IMAGE_CONFIG,
    useValue: {
      breakpoints: [16, 48, 96, 128, 384, 640, 750, 828, 1080, 1200, 1920]
    }
  },
],

怎么理解 sizes 中配置的 100vw 呢

这里的 100vw 相当于 100%,举个例子,你配置了 ngSrcset ="600w, 900w, 1000w,1200w", sizes = "80vw", 那么当用户的视口宽度为 1000px时,浏览器会尝试去找最接近 1000 * 80% 宽度的照片, 最终使用 900-xxxx.png,而不是对应视口宽度的 1000-xxxx.png。

你可能注意到我们最开始的示例代码中,配置了 sizes="(max-width: 800px) 70vw, (max-width: 1200px) 100vw",是的,sizes 也支持媒体查询。

placeholder

NgOptimizedImage 可以为你的图像提供一个自动低分辨率占位符, 只需给你的图像添加 placeholder 属性即可

<img ngSrc="SpongeBob.png" width="400" height="200" placeholder>

添加此属性将自动请求图像的较小版本,使用你指定的图像 loader 。这个小图像将以 CSS 模糊的 background-image 样式应用,而你的图像加载时。如果没有提供图像 loader,则无法生成占位符图像,并将抛出错误。

图例中请求40-SpongeBob.png 就是因为我提供的 loader中返回的 url是 ${config.width}-${config.src}

image.png

生成的占位符的默认大小为 30px 宽,这个值会变成参数传递到你提供的 loader中,你可以通过在 IMAGE_CONFIG 提供者中指定一个值来使其更大或更小,如下所示

{
      provide: IMAGE_CONFIG,
      useValue: {
        placeholderResolution: 40,
      },
},

默认情况下,NgOptimizedImage 对图像占位符应用 CSS 模糊效果。要呈现一个没有模糊效果的占位符,可以配置 placeholderConfig 参数

<img ngSrc="cat.jpg" width="400" height="200" placeholder [placeholderConfig]="{blur: false}">

其它知识点

  1. 要禁用单个图片的 srcset 生成,你可以在图片上添加 disableOptimizedSrcset 属性。
  2. 默认情况下,NgOptimizedImage 为所有未标记 priority 的图片设置 loading=lazy。你可以通过设置 loading 属性来为非优先图片禁用此行为。此属性会接受值:eager 、 auto 和 lazy
<img ngSrc="cat.jpg" width="400" height="200" loading="eager">
  1. NgOptimizedImage指令中支持的另一个属性是 loaderParams,专门设计用于自定义加载器的使用。 loaderParams属性接受一个带有任意属性的对象作为值,本身不会执行任何操作。 loaderParams中的数据被添加到传递给自定义加载器的 ImageLoaderConfig对象中,可用于控制 loader 的行为。

未完待续...