有范 發表於 2022-5-16 00:09:00

Angular中懒加载一个模块并动态创建显示该模块下声明的组件

<blockquote>
<p>环境: Angular 13.x.x</p>
</blockquote>
<p>angular中支持可以通过路由来懒加载某些页面模块已达到减少首屏尺寸, 提高首屏加载速度的目的. 但是这种通过路由的方式有时候是无法满足需求的.</p>
<p>比如, 点击一个按钮后显示一行工具栏, 这个工具栏组件我不希望它默认打包进<code>main.js</code>, 而是用户点按钮后动态把组件加载并显示出来.</p>
<p>那为什么要动态加载呢? 如果直接在目标页面组件引入工具栏组件, 那么工具栏组件中的代码就会被打包进目标页面组件所在的模块, 这会导致目标页面组件所在的模块生成的js体积变大; 通过动态懒加载的方式, 可以让工具栏组件只在用户点了按钮后再加载, 这样就可以达到减少首屏尺寸的目的.</p>
<p>为了演示, 新建一个angular项目, 然后再新建一个<code>ToolbarModule</code>, 项目的目录结构如图</p>
<p><img src="https://img2022.cnblogs.com/blog/1596066/202205/1596066-20220515214814694-515682803.png" alt="img" loading="lazy"></p>
<p>为了达到演示的目的, 我在<code>ToolbarModule</code>的html模板中放了个将近1m的base64图片, 然后直接在<code>AppModule</code>中引用<code>ToolbarModule</code>, 然后执行<code>ng build</code>, 执行结果如图</p>
<p><img src="https://img2022.cnblogs.com/blog/1596066/202205/1596066-20220515215049494-352068778.png" alt="img" loading="lazy"></p>
<p>可以看到打包尺寸到达了<code>1.42mb</code>, 也就是说用户每次刷新这个页面, 不管用户有没有点击显示工具栏按钮, 工具栏组件相关的内容都会被加载出来, 这造成了资源的浪费, 所以下面将<code>ToolbarModule</code>从<code>AppModule</code>的<code>imports</code>声明中移除, 然后在用户点击首次点击显示时懒加载工具栏组件.</p>
<h2 id="懒加载工具栏组件">懒加载工具栏组件</h2>
<p>首先, 新建一个<code>ToolbarModule</code>和<code>ToolbarComponent</code>, 并在<code>ToolbarModule</code>声明<code>ToolbarComponent</code></p>
<p><img src="https://img2022.cnblogs.com/blog/1596066/202205/1596066-20220515234629202-1954386544.png" alt="img" loading="lazy"></p>
<details open="">
<summary>toolbar.module.ts</summary>
<pre><code class="language-typescript">import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ToolbarComponent } from './toolbar.component';

@NgModule({
    declarations: ,
    imports: ,
    exports: ,
})
class ToolbarModule {}

export { ToolbarComponent, ToolbarModule };
</code></pre>
</details>
<details open="">
<summary>toolbar.component.ts</summary>
<pre><code class="language-typescript">import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'toolbar',
    templateUrl: './toolbar.component.html',
    styles: [
      `
    svg {
      width: 64px;
      height: 64px;
    }
    img {
      width: 64px;
      height: 64px;
      object-fit: cover;
    }
    `,
    ],
})
export class ToolbarComponent implements OnInit {
    constructor() {}

    ngOnInit(): void {}
}
</code></pre>
</details>
<details>
<summary>toolbar.component.html</summary>
<pre><code class="language-html">&lt;div class="flex"&gt;
&lt;svg t="1652618923451" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2104" width="200" height="200"&gt;&lt;path d="M412 618m-348 0a348 348 0 1 0 696 0 348 348 0 1 0-696 0Z" fill="#C9F4EB" p-id="2105"&gt;&lt;/path&gt;&lt;path d="M673.19 393h-333a25 25 0 0 1 0-50h333a25 25 0 0 1 0 50zM600.89 235H423.11C367.91 235 323 190.28 323 135.32v-12.5a25 25 0 0 1 50 0v12.5c0 27.39 22.48 49.68 50.11 49.68h177.78c27.63 0 50.11-22.29 50.11-49.68v-16.5a25 25 0 1 1 50 0v16.5c0 54.96-44.91 99.68-100.11 99.68zM673.19 585.5h-333a25 25 0 0 1 0-50h333a25 25 0 0 1 0 50zM467 778H340a25 25 0 0 1 0-50h127a25 25 0 0 1 0 50z" fill="#087E6A" p-id="2106"&gt;&lt;/path&gt;&lt;path d="M739.76 952H273.62a125.14 125.14 0 0 1-125-125V197a125.14 125.14 0 0 1 125-125h466.14a125.14 125.14 0 0 1 125 125v630a125.14 125.14 0 0 1-125 125zM273.62 122a75.08 75.08 0 0 0-75 75v630a75.08 75.08 0 0 0 75 75h466.14a75.08 75.08 0 0 0 75-75V197a75.08 75.08 0 0 0-75-75z" fill="#087E6A" p-id="2107"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;svg t="1652618941842"
       class="icon"
       viewBox="0 0 1024 1024"
       version="1.1"
       xmlns="http://www.w3.org/2000/svg"
       p-id="2247"
       width="200"
       height="200"&gt;
    &lt;path d="M415 624m-348 0a348 348 0 1 0 696 0 348 348 0 1 0-696 0Z"
          fill="#C9F4EB"
          p-id="2248"&gt;&lt;/path&gt;
    &lt;path d="M695 790H362a25 25 0 0 1 0-50h333a25 25 0 0 1 0 50zM583 649H362a25 25 0 0 1 0-50h221a25 25 0 0 1 0 50zM262 287H129a25 25 0 0 1 0-50h133a25 25 0 0 1 0 50zM262 455.33H129a25 25 0 1 1 0-50h133a25 25 0 0 1 0 50zM262 623.67H129a25 25 0 0 1 0-50h133a25 25 0 0 1 0 50zM262 792H129a25 25 0 0 1 0-50h133a25 25 0 0 1 0 50z"
          fill="#087E6A"
          p-id="2249"&gt;&lt;/path&gt;
    &lt;path d="M761.76 964H295.62a125.14 125.14 0 0 1-125-125V209a125.14 125.14 0 0 1 125-125h466.14a125.14 125.14 0 0 1 125 125v630a125.14 125.14 0 0 1-125 125zM295.62 134a75.09 75.09 0 0 0-75 75v630a75.08 75.08 0 0 0 75 75h466.14a75.08 75.08 0 0 0 75-75V209a75.09 75.09 0 0 0-75-75z"
          fill="#087E6A"
          p-id="2250"&gt;&lt;/path&gt;
    &lt;path d="M617 376H443a25 25 0 0 1 0-50h174a25 25 0 0 1 0 50z"
          fill="#087E6A"
          p-id="2251"&gt;&lt;/path&gt;
    &lt;path d="M530 463a25 25 0 0 1-25-25V264a25 25 0 0 1 50 0v174a25 25 0 0 1-25 25z"
          fill="#087E6A"
          p-id="2252"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;img src="&lt;这里应该是一张大小将近1M的base64图片, 内容较大, 就略去了...&gt;" alt=""&gt;
&lt;/div&gt;
</code></pre>
</details>
<p>然后再<code>AppComponent</code>的中按钮点击事件处理程序中写加载工具栏模块的代码:</p>
<details open="">
<summary>app.component.ts</summary>
<pre class="language-typescript" data-lines-highlight=""><code class="language-typescript" data-lines-highlight="">import { Component, createNgModuleRef, Injector, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
    selector: 'root',
    template: `
               &lt;div class="container h-screen flex items-center flex-col w-100 justify-center"&gt;
               &lt;div class="mb-3"
                      ="{ hidden: !isToolbarVisible }"&gt;
                   &lt;ng-container #toolbar&gt;&lt;/ng-container&gt;
               &lt;/div&gt;
               &lt;div&gt;
                   &lt;button (click)="toggleToolbarVisibility()"
                           class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"&gt;{{ isToolbarVisible ? '隐藏' : '显示' }}&lt;/button&gt;
                   &lt;p class="mt-3"&gt;首屏内容&lt;/p&gt;
               &lt;/div&gt;
               &lt;/div&gt;
             `,
})
export class AppComponent {
    title = 'ngx-lazy-load-demo';
    toolbarLoaded = false;
    isToolbarVisible = false;
    @ViewChild('toolbar', { read: ViewContainerRef }) toolbarViewRef!: ViewContainerRef;

    constructor(private _injector: Injector) {}

    toggleToolbarVisibility() {
      this.isToolbarVisible = !this.isToolbarVisible;
      this.loadToolbarModule().then();
    }

    private async loadToolbarModule() {
      if (this.toolbarLoaded) return;
      this.toolbarLoaded = true;
      const { ToolbarModule, ToolbarComponent } = await import('./toolbar/toolbar.module');
      const moduleRef = createNgModuleRef(ToolbarModule, this._injector);
      const { injector } = moduleRef;
      const componentRef = this.toolbarViewRef.createComponent(ToolbarComponent, {
            injector,
            ngModuleRef: moduleRef,
      });
    }
}</code></pre></details>
<p>关键在于其中的第32-42行, 首先通过一个动态<code>import</code>导入<code>toolbar.module.ts</code>中的模块, 然后调用<code>createNgModuleRef</code>并传入当前组件的<code>Injector</code>作为<code>ToolbarModule</code>的父级<code>Injector</code>, 这样就实例化了<code>ToolbarModule</code>得到了<code>moduleRef</code>对象, 最后就是调用html模板中声明的<code>&lt;ng-container #toolbar&gt;&lt;/ng-container&gt;</code>的<code>ViewContainerRef</code>对象的<code>createComponent</code>方法创建<code>ToolbarComponent</code>组件</p>
<pre><code class="language-typescript">private async loadToolbarModule() {
    if (this.toolbarLoaded) return;
    this.toolbarLoaded = true;
    const { ToolbarModule, ToolbarComponent } = await import('./toolbar/toolbar.module');
    const moduleRef = createNgModuleRef(ToolbarModule, this._injector);
    const { injector } = moduleRef;
    const componentRef = this.toolbarViewRef.createComponent(ToolbarComponent, {
      injector,
      ngModuleRef: moduleRef,
    });
}
</code></pre>
<p>此时再来看下这番操作后执行<code>ng build</code>打包的尺寸大小</p>
<p><img src="https://img2022.cnblogs.com/blog/1596066/202205/1596066-20220516000144035-2029351282.png" alt="img" loading="lazy"></p>
<p>可以看到首屏尺寸没有开头那么离谱了, 原因是没有在<code>AppModule</code>和<code>AppComponent</code>直接导入<code>ToolbarModule</code>和<code>ToolbarComponent</code>, <code>ToolbarModule</code>被打进了另外的js文件中(Lazy Chunk Files), 当首次点击<code>显示</code>按钮时, 就会加载这个包含<code>ToolbarModule</code>的js文件</p>
<p>注意看下面的gif演示中, 首次点击<code>显示</code>按钮, 浏览器网络调试工具中会多出一个对<code>src_app_toolbar_toolbar_module_ts.js</code>文件的请求</p>
<p><img src="https://img2022.cnblogs.com/blog/1596066/202205/1596066-20220516000601076-1697872480.gif" alt="img" loading="lazy"></p>


</div>
<div id="MySignature" role="contentinfo">
    <p>作者:Laggage</p>
<p>出处:https://www.cnblogs.com/laggage/p/16275367.html</p>
<p>说明:转载请注明来源</p><br><br>
来源:https://www.cnblogs.com/laggage/p/16275367.html
頁: [1]
查看完整版本: Angular中懒加载一个模块并动态创建显示该模块下声明的组件