发布于 2026-01-06 1 阅读
0

Deferred loading technique in Angular

角度中的延迟加载技术

介绍

Angular 是一个非常强大的框架。它提供了很多功能,可以大大简化你的产品开发过程。但是,强大的功能也意味着更大的责任。

在我目前参与的CodeGym项目中,我们面临着 Angular 生成非常大的 JavaScript 包这一事实,这会对我们的用户体验和页面速度指标产生负面影响。

您可以在Web Vitals上了解更多相关信息。

我想您应该已经了解通过路由器的 loadChildren实现的懒加载技术,以及通过每个组件一个模块来实现共享模块的代码拆分。

在本文中,我想向您介绍另一种可以帮助您改进项目的技巧。

我们走吧!

我假设您已经安装了@angular/cli

我们将从零开始。首先创建一个新项目:

ng new example
cd example
Enter fullscreen mode Exit fullscreen mode

src/app文件夹中创建我们的惰性模块,其中包含一个组件。

懒惰模块

@NgModule({
  declarations: [LazyComponent],
})
export class LazyModule {}
Enter fullscreen mode Exit fullscreen mode

懒惰组件

@Component({
  selector: "app-lazy",
  template: `
    <div> Hello, I am lazy component!</div>
  `,
})
export class LazyComponent {}
Enter fullscreen mode Exit fullscreen mode

接下来,我们需要创建一个延迟加载组件。它将作为我们懒加载组件的包装器。

@Component({
  selector: "app-deferred-loading",
  template: `<div #container></div>`,
})
export class DeferredLoadingComponent implements OnInit {
  @ViewChild("container", {read: ViewContainerRef}) container: ViewContainerRef;

  constructor(
    private compiler: Compiler,
    private injector: Injector,
  ) { }

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

  async load(): Promise<void> {
    const { module, component } = await this.getContent();
    const moduleFactory = await this.compiler.compileModuleAsync(module);
    const moduleRef = moduleFactory.create(this.injector);
    const componentFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(component);
    const { hostView, instance } = componentFactory.create(this.injector);
    this.container.insert(hostView);
  }

  private async getContent(): Promise<{ module: any, component: any }> {
    const [moduleChunk, componentChunk] = await Promise.all([
      import("./lazy/lazy.module"),
      import("./lazy/lazy.component")
    ]);
    return {
      module: moduleChunk["LazyModule"],
      component: componentChunk["LazyComponent"]
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

我们需要同时加载模块和组件,因为我想向你们展示如何处理的不是单个组件,而是包含其自身服务和子组件的整个小部件。

遗憾的是,我们不能直接加载代码并开始使用,因为每个 Angular 模块都有自己的编译上下文。这就是为什么我们需要使用JIT 编译器来解决这个问题。

首先,我们编译模块并解析其提供程序。
其次,我们解析组件并将其动态注入到 DOM 中。

现在我们可以在app.component.ts中使用它了。

@Component({
  selector: 'app-root',
  template: `
    <app-deferred-loading *ngIf="isReadyForLazyComponent"></app-deferred-loading>
    <button (click)="load()">Load and bootstrap</button>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  isReadyForLazyComponent: boolean;

  load(): void {
    this.isReadyForLazyComponent = true;
  }
}
Enter fullscreen mode Exit fullscreen mode

点击按钮后,JavaScript 代码加载、编译,Angular 渲染一个全新的惰性组件。

挑战 - 1

如果我们想从lazy.component传递一些数据,甚至与app.component进行交互,该怎么办?

我不知道这是否是处理这种情况的最佳方法,但它确实有效:

  1. 修改app.component,使其向输入端发送数据并监听输出。
@Component({
  selector: 'app-root',
  template: `
    <button (click)="load()">Load and bootstrap</button>
    <app-deferred-loading *ngIf="isReadyForLazyComponent" [props]="props"></app-deferred-loading>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  isReadyForLazyComponent: boolean;

  props = {
    name: "Spike",
    onClick: this.handleLazyComponentClick.bind(this),
  };

  load(): void {
    this.isReadyForLazyComponent = true;
  }

  handleLazyComponentClick(val): void {
    console.log(`${val}: from lazy component!`)
  }
}
Enter fullscreen mode Exit fullscreen mode

2.修改lazy.component以接收和发出数据

@Component({
  selector: "app-lazy",
  template: `
    <div>
      <hr>
      <div> Hello, I am lazy component!</div>
      <button (click)="handleClick()">Data from child</button>
      <hr>
    </div>
  `,
})
export class LazyComponent {
  @Output() onClick: EventEmitter<string> = new EventEmitter();
  @Input() name: string;

  handleClick(): void {
    this.onClick.emit(`My name is ${this.name}!`);
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. 然后将 app.component 和lazy.componentdeferred-loading.component连接起来。
@Component({
  selector: "app-deferred-loading",
  template: `<div #container></div>`,
})
export class DeferredLoadingComponent implements OnInit, OnDestroy {
  ...

  @Input() props: any;

  private isDestroyed$: Subject<void> = new Subject();

  ...

  async load(): Promise<void> {
    ...

    Object.entries(this.props).forEach(([key, value]: [string, any]) => {
      if (instance[key] && instance[key].observers) {
        instance[key]
          .pipe(takeUntil(this.isDestroyed$))
          .subscribe((e) => value(e));
      } else {
        instance[key] = value;
      }
    });

    this.container.insert(hostView);
  }

  private async getContent(): Promise<{ module: any, component: any }> {
    ...
  }

  ngOnDestroy(): void {
    this.isDestroyed$.next();
    this.isDestroyed$.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以将数据传递给lazy.component 的输入并监听其输出,
这真是太棒了。

挑战 - 2

如果我们需要加载内容的方式不是通过点击,而是通过进入视口,该怎么办?

这时,交叉路口观察器就派上用场了。

首先,我们需要准备app.component。

  @Component({
  selector: 'app-root',
  template: `
    <button (click)="load()">Load and bootstrap</button>
    <div class="first-content"></div>
    <app-deferred-loading [props]="props"></app-deferred-loading>
  `,
  styles: [`.first-content {
    background-color: cornflowerblue;
    width: 100%;
    height: 120vh;
  }`]
})
Enter fullscreen mode Exit fullscreen mode

然后,编辑deferred-loading.component

...
export class DeferredLoadingComponent implements OnInit, OnDestroy {
  ....

  private intersectionObserver: IntersectionObserver;
  private isDestroyed$: Subject<void> = new Subject();

  constructor(
    private compiler: Compiler,
    private injector: Injector,
    private element: ElementRef,
    @Inject(PLATFORM_ID) private platformId: Object,
  ) { }

  ngOnInit(): void {
    if (isPlatformBrowser(this.platformId)) {
      if ("IntersectionObserver" in window) {
        this.intersectionObserver = this.createIntersectionObserver();
        this.intersectionObserver.observe(this.element.nativeElement);
      } else {
        this.load();
      }
    }
  }

  ...

  private createIntersectionObserver(): IntersectionObserver {
    return new IntersectionObserver(entries => this.checkForIntersection(entries));
  }

  private checkForIntersection(entries: IntersectionObserverEntry[]) {
    entries.forEach((entry: IntersectionObserverEntry) => {
      if (this.isIntersecting(entry)) {
        this.load();
        this.intersectionObserver.unobserve(this.element.nativeElement);
      }
    });
  }

  private isIntersecting(entry: IntersectionObserverEntry): boolean {
    return (<any>entry).isIntersecting && entry.target === this.element.nativeElement;
  } 

  ngOnDestroy(): void {
    ...
    if (this.intersectionObserver) {
      this.intersectionObserver.unobserve(this.element.nativeElement);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

这是在“延迟加载图像和视频”中引入的标准技术。

Now, lazy.component will be bootstrapped on the page, only when it gets into the viewport.

I hope my article will help somebody to make his product better. :)

P.S. Source code can be found at github .

文章来源:https://dev.to/igorfilippov3/deferred-loading-technique-in-angular-3o90