角度中的延迟加载技术
介绍
Angular 是一个非常强大的框架。它提供了很多功能,可以大大简化你的产品开发过程。但是,强大的功能也意味着更大的责任。
在我目前参与的CodeGym项目中,我们面临着 Angular 生成非常大的 JavaScript 包这一事实,这会对我们的用户体验和页面速度指标产生负面影响。
您可以在Web Vitals上了解更多相关信息。
我想您应该已经了解通过路由器的 loadChildren实现的懒加载技术,以及通过每个组件一个模块来实现共享模块的代码拆分。
在本文中,我想向您介绍另一种可以帮助您改进项目的技巧。
我们走吧!
我假设您已经安装了@angular/cli。
我们将从零开始。首先创建一个新项目:
ng new example
cd example
在src/app文件夹中创建我们的惰性模块,其中包含一个组件。
懒惰模块
@NgModule({
declarations: [LazyComponent],
})
export class LazyModule {}
懒惰组件
@Component({
selector: "app-lazy",
template: `
<div> Hello, I am lazy component!</div>
`,
})
export class LazyComponent {}
接下来,我们需要创建一个延迟加载组件。它将作为我们懒加载组件的包装器。
@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"]
};
}
}
我们需要同时加载模块和组件,因为我想向你们展示如何处理的不是单个组件,而是包含其自身服务和子组件的整个小部件。
遗憾的是,我们不能直接加载代码并开始使用,因为每个 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;
}
}
点击按钮后,JavaScript 代码加载、编译,Angular 渲染一个全新的惰性组件。
挑战 - 1
如果我们想从lazy.component传递一些数据,甚至与app.component进行交互,该怎么办?
我不知道这是否是处理这种情况的最佳方法,但它确实有效:
- 修改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!`)
}
}
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}!`);
}
}
- 然后将 app.component 和lazy.component与deferred-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();
}
}
现在我们可以将数据传递给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;
}`]
})
然后,编辑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);
}
}
}
这是在“延迟加载图像和视频”中引入的标准技术。
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