如何在 Angular 应用程序中以编程方式加载“惰性”模块?

Bri*_*ite 6 lazy-loading angular

我有一个不使用 Router 的整体 Angular 15 应用程序。它内部的组件数量不断增加,我想将大部分组件分解成一个单独的模块并单独加载它们。

我的应用程序已经有一个带有“加载”进度条的启动屏幕,该进度条随着从服务器获取数据而前进。我希望主 AppModule 包含一组最小的组件来开始工作,然后我将加载其余组件作为进度条监视的启动任务之一。

目前的情况...

应用程序模块.ts:

import {NgModule, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from "@angular/forms";
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatSidenavModule} from "@angular/material/sidenav";

import {SharedModule} from "./shared.module";

import {AppComponent} from './app.component';
import {ResizeableSidenavDirective} from "../components/resizeable-sidenav.directive";
import {SplashScreenComponent} from "../components/splash-screen/splash-screen.component";

@NgModule({
    declarations: [
        AppComponent,
        SplashScreenComponent,
        ResizeableSidenavDirective,
    ],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        FormsModule,
        MatProgressBarModule,
        MatSidenavModule,
        SharedModule
    ],
    providers: [],
    bootstrap: [AppComponent],
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule { }
Run Code Online (Sandbox Code Playgroud)

共享.模块.ts:

import {NgModule, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {CommonModule} from "@angular/common";
import {LeafletModule} from "@asymmetrik/ngx-leaflet";

import {AppComponent} from './app.component';
import {MapViewComponent} from "../components/map-view/map-view.component";
import {Toaster} from "../components/toaster";

@NgModule({
    declarations: [
        MapViewComponent,
        Toaster,
    ],
    imports: [
        BrowserAnimationsModule,
        CommonModule,
        LeafletModule,
    ],
    exports: [
        MapViewComponent,
        Toaster,
    ],
    providers: [],
    bootstrap: [AppComponent],
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SharedModule { }
Run Code Online (Sandbox Code Playgroud)

惰性模块.ts:

import {NgModule, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {CommonModule} from "@angular/common";
import {FormsModule} from "@angular/forms";
import {MatDialogModule} from "@angular/material/dialog";
import {MatSliderModule} from "@angular/material/slider";

import {SharedModule} from "./shared.module";

... big list of component imports ...

@NgModule({
    declarations: [
        MyFirstComponent,
        MySecondComponent,
        MyThirdComponent,
        ...
    ],
    imports: [
        CommonModule,
        FormsModule,
        MatDialogModule,
        MatSliderModule,
        SharedModule
    ],
    exports: [
        MyFirstComponent,
        MySecondComponent,
        MyThirdComponent,
        ...
    ],
    providers: [],
    bootstrap: [],
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class LazyModule { }
Run Code Online (Sandbox Code Playgroud)

虽然我想问的问题是如何从启动屏幕内进行延迟加载,但我什至无法构建上述内容。组件中存在许多构建错误,LazyModule如下所示:

“mat-slider”不是已知元素

(然而 MatSliderModule 是 LazyModule 的包含导入)

未找到名称为“number”的管道。

(然而 CommonModule 也是包含的导入)

无法绑定到“ngModel”,因为它不是“input”的已知属性。

(但 FormsModule 是包含的导入)

'my-second' 不是已知元素:

(惰性组件 MyFirstComponent 的 HTML 引用 MySecondComponent)

如果我在导入中添加LazyModuleafter ,所有上述问题都会消失。奇怪的是,它也将仅通过导入来构建,甚至无需添加到“导入”列表中。但是,当然,这让我回到了我试图分解的单个整体文件。那么两个问题:SharedModuleAppModule./lazy.moduleLazyModulemain.js

我如何分离LazyModuleAppModule构建它?

我在初始化中调用什么函数来加载 LazyModule,以及如何收到加载完成的通知?

Update1:​​我设法通过将from的导入移至来修复第一个错误(mat-slider未知)。这对我来说毫无意义,因为除了一个惰性组件之外,它没有在任何地方使用。此技术对于“未找到数字管道”( ) 或“无法绑定 ngModel”( )没有帮助。MatSliderModulelazy.moduleshared.modulemat-sliderCommonModuleFormsModule

Bri*_*ite 3

尤金给出了非常好的答案,这对我弄清楚我想做什么有很大帮助。

正如我所说的,我的问题实际上并不是我想知道的。它应该是这样的:

我可以将源代码分成几个部分,以便在“启动”启动过程中先下载必需的内容,然后再下载其余部分吗?

这个问题的简短答案是“不”。我的调查显示,可能一个很长的答案,围绕文件中的包含/排除指令angular.json,但它似乎很复杂,可能会带来维护问题,并且通常与 Angular 的设计相反。

最后,我使用内置的 Angular 支持来创建和加载模块。我下面将介绍的内容与我在其他地方找到的片段没有显着不同。然而,这些对我来说都没有意义,因为我错过了一个每个人似乎都认为理所当然的非常基本的概念:

代码会自动分离到独立加载的 JavaScript 文件中。

Angular 就是为你做这件事的。与 C/C++ 手动将文件分组到.a库中或将 Java 分组到.jar文件不同,使用 Angular,您不需要显式实例化任何想要单独加载的内容。只要不直接创建该类型的对象,就可以导入new它以便访问类的字段和方法。

无论组件是分组为模块还是声明standalone:true,技巧都是不直接引用它们。独立的片段被收集到单独的.js文件中,包括仅由它们使用的代码node_modules,然后可以按需加载(也称为“延迟加载”)。

这是一个例子:

发布.模块.ts:

import {CommonModule} from "@angular/common";
import {FormsModule} from "@angular/forms";
import {NgModule} from "@angular/core";

import {PublishStartComponent} from "./publish-start/publish-start.component";
import {PublishContinueComponent} from "./publish-continue/publish-continue.component";
import {PublishFinalComponent} from "./publish-final/publish-final.component";
import {PublishResultsComponent} from "./publish-results/publish-results.component";
import {RegionPlacerComponent} from "./region-placer.component";

@NgModule({
    declarations: [
        PublishStartComponent,
        PublishContinueComponent,
        PublishFinalComponent,
        PublishResultsComponent,
        RegionPlacerComponent,
    ],
    imports: [
        CommonModule,  // |number
        FormsModule,   // ngModel
    ]
})
export class PublishModule {
    getPublishStartFactory()    { return PublishStartComponent    }
    getPublishContinueFactory() { return PublishContinueComponent }
    getPublishFinalFactory()    { return PublishFinalComponent    }
    getPublishResultsFactory()  { return PublishResultsComponent  }
    getRegionPlacerFactory()    { return RegionPlacerComponent    }
}
Run Code Online (Sandbox Code Playgroud)

在 Angular 15 中,延迟加载文件并访问该文件只需两行:

    const {PublishModule} = await import ("../../components/publish/publish.module")
    const pminstance = createNgModule(PublishModule, this.injector).instance
Run Code Online (Sandbox Code Playgroud)

然后可以使用“工厂”返回类型来实例化这些类:

    let thing = new (pmintstance.getPublishStartFactory())(...)
Run Code Online (Sandbox Code Playgroud)

或者它可以传递给需要类型的函数:

    this.dialogService.open(pmintstance.getPublishStartFactory(), {...})
Run Code Online (Sandbox Code Playgroud)

就我而言,它看起来像这样:

myapp.ts:

...
import {PublishModule} from "../../components/publish/publish.module";
...
@Component({...})
export class MyApp {
    ...
    private pmInstance: PublishModule|undefined
    ...
    constructor(dialogService: MatDialog, injector: Injector) {...}
    ...
    private async onPublishButtonFirstClick() {
        const {PublishModule} = await import ("../../components/publish/publish.module")
        this.pmInstance = createNgModule(PublishModule, this.injector).instance

        let dref = this.dialogService.open(this.pmInstance.getPublishStartFactory(), {
            ...
        })

        dref.afterClosed().subscribe((rid: string) => {
            if (rid == null || rid == "") return
            this.regionName = dref.componentInstance.regionName
            this.regionProjection = dref.componentInstance.regionType
            this.imageUrl = dref.componentInstance.imageUrl!
            this.imageSize = dref.componentInstance.imageSize!

            const rpc = document.getElementById("overlay-container")!
            const injector = Injector.create({
                providers: [
                    {provide: 'imageUrl', useValue: this.imageUrl!},
                    {provide: 'imageSize', useValue: this.imageSize!},
                ]
            })
            this.regionPlacerView = this.injector.get<ViewContainerRef>(ViewContainerRef);
            const rp = this.regionPlacerView.createComponent(this.pmInstance!.getRegionPlacerFactory(), {
                injector: injector
            })

            this.regionPlacer = rp.instance
        })
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

除此之外,还有更多内容,例如为基本代码和惰性代码使用的那些组件创建“共享”模块,但无需更改其使用方式。通常,只需在基本/惰性侧访问共享模块的组件,Angular 就会负责做正确的事情(在本例中:将“共享”拆分为自己的文件,但将其作为 的一部分加载index.html)。