Angular 9和Ivy:惰性组件加载

在Angular中加载延迟组件? 也许我们正在谈论使用Angular路由器的延迟加载模块? 不,我们正在谈论组件。 当前版本的Angular仅支持延迟模块加载。 但是Ivy为开发人员提供了使用组件的新机会。



到目前为止,我们使用的延迟负载:路线


延迟加载是一个很好的机制。 在Angular中,您可以通过声明支持延迟加载的路由来使用这种机制,而几乎无需付出任何努力。 这是Angular 文档中的一个示例,说明了这一点:

const routes: Routes = [     { path: 'customer-list',       loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule) }     ]; 

由于上面的代码,将为customers.module创建一个单独的片段,当用户沿着customer-list路线导航时将加载该片段。

这是减小项目主捆绑包大小并加快应用程序初始加载速度的一种很好的方法。

但是,尽管如此,能够更准确地控制延迟加载不是很好吗? 例如,如果我们可以组织各个组件的延迟加载呢?

到目前为止,这还不可能。 但是随着常春藤的出现,这一切都改变了。

常春藤和“地方性”的概念


模块是每个Angular应用程序的基本概念和基本构建块。 这些模块声明组件,指令,管道,服务。

没有模块,现代Angular应用程序将不复存在。 原因之一是ViewEngine将所有必要的元数据添加到模块中。

另一方面,常春藤采取不同的方法。 在Ivy中,没有模块就可以存在组件。 这要归功于本地性的概念。 其实质是所有元数据都位于组件本地。

让我们通过分析使用Ivy生成的es2015捆绑包对此进行解释。


使用Ivy生成的ES2015捆绑包

在“ Component code部分,您可以看到Ivy系统已保存了组件代码。 这里没有什么特别的。 但是随后Ivy将一些元数据添加到代码中。

图中的第一部分元数据显示为Component factory 。 工厂知道如何实例化组件。 在“ Component metadata部分中,Ivy放置了其他属性,例如typeselectors ,即组件在程序执行过程中所需的一切。

值得一提的是,Ivy在此处添加了template功能。 它显示在Compiled version of your template的“ Compiled version of your template部分中。 让我们更详细地讨论这个有趣的事实。

template函数是HTML代码的编译版本。 她遵循Ivy的说明创建DOM。 这与ViewEngine的工作方式不同。

ViewEngine系统采用代码并绕过它。 如果我们使用Angular,它将执行代码。

通过Ivy使用的方法,调用Angular命令的组件将负责所有工作。 此更改允许组件独立存在,这导致了可以将摇树算法应用于Angular基本代码的事实。

延迟组件加载的真实示例


现在我们知道可以进行惰性组件加载了,下面举一个真实的例子来考虑一下。 即,我们将创建一个测验应用程序,向用户询问带​​有答案选项的问题。

该应用程序显示城市图像和选项,您需要从中选择该城市的名称。 一旦用户通过单击相应的按钮选择一个选项,该按钮将立即更改,指示答案是对还是错。 如果按钮的背景变为绿色,则答案是正确的。 如果背景变成红色,则表示答案不正确。

收到当前问题的答案后,程序将显示以下问题。 这是它的外观。


测验演示

该程序询问用户的问题由QuizCardComponent组件表示。

惰性组件加载概念


让我们首先说明延迟加载QuizCardComponent组件的一般想法。


QuizCardComponent组件过程

用户通过单击“ Start quiz按钮Start quiz ,我们开始延迟加载组件。 组件处理完毕后,我们将其放入容器中。

我们以questionAnsvered常规组件事件的响应相同的方式对“惰性”组件的questionAnsvered事件做出反应。 即,事件发生后,我们在屏幕上显示带有问题的下一张卡片。

代码分析


为了理解在延迟加载组件期间会发生什么,我们从QuizCardComponent的简化版本开始,该版本显示问题的属性。

然后,我们将在其中使用Angular Material组件来扩展此组件。 最后,我们将调整对组件产生的事件的反应。

让我们组织QuizCardComponent组件的简化版本的延迟加载,该组件具有以下模板:

 <h1>Here's the question</h1> <ul>    <li><b>Image: </b> {{ question.image }}</li>    <li><b>Possible selections: </b> {{ question.possibleSelections.toString() }}</li>    <li><b>Correct answer: </b> {{ question.correctAnswer }}</li> </ul> 

第一步是创建一个容器元素。 为此,我们可以使用诸如<div>类的真实元素,也可以使用ng-container ,这使我们无需额外的HTML代码。 这就是容器元素的声明,其中放置了“惰性”组件:

 <mat-toolbar color="primary">  <span>City quiz</span> </mat-toolbar> <button *ngIf="!quizStarted"        mat-raised-button color="primary"        class="start-quiz-button"        (click)="startQuiz()">Start quiz</button> <ng-container #quizContainer class="quiz-card-container"></ng-container> 

在组件中,您需要访问容器。 为此,我们使用@ViewChild批注,让我们知道我们想阅读ViewContainerRef

请注意,在Angular 9中, @ViewChild批注中的static属性默认情况下设置为false

 @ViewChild('quizContainer', {read: ViewContainerRef}) quizContainer: ViewContainerRef; 

现在,我们有一个容器,我们想在其中添加“惰性”组件。 接下来,我们需要ComponentFactoryResolverInjector 。 可以通过依赖注入的方法来获取它们两者。

ComponentFactoryResolver实体是一个简单的注册表,用于设置组件与自动生成的ComponentFactory类之间的关系,这些类可用于实例化组件:

 constructor(private quizservice: QuizService,            private cfr: ComponentFactoryResolver,            private injector: Injector) { } 

现在,我们拥有实现目标所需的一切。 startQuiz研究startQuiz方法的内容并组织组件的延迟加载:

 const {QuizCardComponent} = await import('./quiz-card/quiz-card.component'); const quizCardFactory = this.cfr.resolveComponentFactory(QuizCardComponent); const {instance} = this.quizContainer.createComponent(quizCardFactory, null, this.injector); instance.question = this.quizservice.getNextQuestion(); 

我们可以使用import ECMAScript命令来组织QuizCardComponent的延迟加载。 导入表达式返回承诺。 您可以使用async/await构造或.then处理程序来使用它。 在解决了诺言之后,我们使用分解来创建组件的实例。

这些天,您必须使用ComponentFactory提供向后兼容性。 将来,不需要相应的行,因为我们将能够直接使用该组件。

ComponentFactory工厂为我们提供了componentRef 。 我们将componentRefInjector传递给容器的createComponent方法。

createComponent方法返回一个ComponentRef ,其中包含组件实例。 我们使用instance传递组件@Input

将来,所有这些可能都可以使用Angular renderComponent方法完成。 此方法仍处于实验阶段。 但是,很有可能会变成常规的Angular方法。 以下是有关此主题的有用材料。

这是组织组件的延迟加载所需要的全部。


惰性组件加载

按下Start quiz按钮后,组件的延迟加载开始。 如果打开开发人员工具的“ Network标签,则可以看到延迟加载quiz-card-quiz-card-component.js文件表示的代码片段的过程。 在显示了加载和处理组件之后,用户会看到一个问题卡。

组件扩展


当前,我们正在加载QuizCardComponent组件。 很好 但是我们的应用程序还没有特别的功能。

让我们通过添加其他功能和Angular Material组件来解决此问题:

 <mat-card class="quiz-card">  <mat-card-header>    <div mat-card-avatar class="quiz-header-image"></div>    <mat-card-title>Which city is this?</mat-card-title>    <mat-card-subtitle>Click on the correct answer below</mat-card-subtitle>  </mat-card-header>  <img class="image" mat-card-image [src]="'assets/' + question.image" alt="Photo of a Shiba Inu">  <mat-card-actions class="answer-section">    <button [disabled]="answeredCorrectly !== undefined" *ngFor="let selection of question.possibleSelections"            mat-stroked-button color="primary"            [ngClass]="{              'correct': answeredCorrectly && selection === question.correctAnswer,              'wrong': answeredCorrectly === false && selection === question.correctAnswer             }"            (click)="answer(selection)">      {{selection}}    </button>  </mat-card-actions> </mat-card> 

我们在组件中包含了一些漂亮的Material组件。 相应的模块呢?

当然可以将它们添加到AppModule 。 但这意味着这些模块将以“贪婪”模式加载。 这不是一个好主意。 此外,该项目的组装将失败,并显示以下消息:

 ERROR in src/app/quiz-card/quiz-card.component.html:9:1 - error TS-998001: 'mat-card' is not a known element: 1. If 'mat-card' is an Angular component, then verify that it is part of this module. 2. If 'mat-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. 

怎么办 如您可能已经了解的,此问题是完全可以解决的。 这可以使用模块来完成。

但是这次我们将使用它们与以前有所不同。 我们将在与QuizCardComponent相同的文件中添加一个小模块(在quizcard.component.ts文件中):

 @NgModule({  declarations: [QuizCardComponent],  imports: [CommonModule, MatCardModule, MatButtonModule] }) class QuizCardModule { } 

请注意,没有export声明。

该模块规范仅属于我们的组件,以延迟模式加载。 结果,模块中唯一声明的组件将是QuizCardComponentimport部分仅导入我们组件所需的模块。

我们不会导出新模块,因此以贪婪模式加载的模块无法导入它。

重新启动该应用程序,并查看其在单击“ Start quiz时的行为。


修改的应用程序分析

太好了! QuizCardComponent组件以惰性模式加载,并添加到ViewContainer 。 所有必需的依赖项都随它一起加载。

我们将使用相应的npm模块提供的webpack-bundle-analyze工具来分析应用程序包。 它使您可以可视化Webpack生成的文件的内容并检查生成的方案。


应用程序包分析

该应用程序的主包大小约为260 Kb。 如果我们将QuizCardComponent组件与它一起下载,那么下载数据的大小将约为270 Kb。 事实证明,我们能够将主捆绑包的大小减少10 Kb,在惰性模式下仅加载一个组件。 好结果!

QuizCardComponent后的QuizCardComponent代码进入一个单独的文件。 如果您分析此文件的内容,结果表明不仅存在QuizCardComponent代码,而且此组件中还使用了Material模块。

即使QuizCardComponent使用MatButtonModuleMatCardModule ,也只有MatCardModule进入完成的代码MatCardModule 。 原因是我们也使用MatButtonModule中的AppModule ,显示“ Start quiz按钮。 结果,相应的代码落入捆绑包的另一个片段中。

现在,我们组织了QuizCardComponent的延迟加载。 该组件显示以材料风格设计的卡片,其中包含图片,问题和带有答案选项的按钮。 如果您单击其中一个按钮,现在会发生什么?

按下该按钮时,该按钮将变为绿色或红色,具体取决于其答案是对还是错。 还有什么事吗? 没事 而且我们需要在回答一个问题之后,显示另一个问题的卡片。 修复它。

事件处理


当您单击“答案”按钮时,该应用程序不会显示新的问题卡,原因是我们尚未设置机制来响应以惰性模式加载的组件的事件。 我们已经知道QuizCardComponent组件使用EventEmitter生成事件。 如果查看EventEmitter类的定义,您会发现它是Subject的后代:

 export declare class EventEmitter<T extends any> extends Subject<T> 

这意味着EventEmitter有一个EventEmitter方法,它允许您配置系统对发生的事件的响应:

 instance.questionAnswered.pipe(    takeUntil(instance.destroy$) ).subscribe(() => this.showNewQuestion()); 

在这里,我们订阅questionAnswered流,并调用showNextQuestion方法,该方法执行lazyLoadQuizCard逻辑:

 async showNewQuestion() {  this.lazyLoadQuizCard(); } 

需要takeUntil(instance.destroy$)构造来清除在销毁组件之后执行的预订。 如果ngOnDestroyngOnDestroy组件生命周期的ngOnDestroy挂钩,则使用nextcomplete调用destroy$

由于该组件已经加载,因此系统不会执行其他HTTP请求。 我们使用已经拥有的代码片段的内容,创建一个新组件并将其放入容器中。

组件生命周期挂钩


使用延迟加载技术处理QuizCardComponent组件时,几乎所有组件生命周期挂钩都会自动调用。 但是一个钩子还不够。 能听懂哪一个吗?


组件生命周期挂钩

这是ngOnChanges最重要的钩子。 由于我们自己更新了组件实例的输入属性,因此我们还负责调用ngOnChanges生命周期挂钩。

要调用ngOnChanges实例的ngOnChanges挂钩,您需要自己构造一个SimpleChange对象:

 (instance as any).ngOnChanges({    question: new SimpleChange(null, instance.question, true) }); 

我们手动调用ngOnChanges组件实例,并将其传递给SimpleChange对象。 该对象指示此更改为第一个更改,先前的值为null ,并且当前值为一个问题。

太好了! 我们使用第三方模块加载了组件,响应了它生成的事件,并设置了组件生命周期的必要挂钩的调用。

这是我们在这里完成的项目源代码。

总结


惰性组件加载为Angular开发人员提供了优化应用程序性能的绝佳机会。 他可以使用的工具可以非常微调以惰性模式加载的材料的成分。 以前,当可以仅以惰性模式加载路由时,我们没有这种准确性。

不幸的是,在组件中使用第三方模块时,我们还需要照顾好这些模块。 但是,值得记住的是,将来这可能会改变。

常春藤引擎引入了局部性的概念,这归功于组件可以独立存在。 这种变化是Angular未来的基础。

亲爱的读者们! 您打算在Angular项目中使用惰性组件加载技术吗?

Source: https://habr.com/ru/post/zh-CN484618/


All Articles