问题 使用Angular CLI和Angular 5在运行时动态加载新模块


目前我正在开发一个托管在客户端服务器上的项目。对于新的“模块”,有 无意重新编译 整个申请。也就是说,客户想要 在运行时更新路由器/延​​迟加载的模块。我已经尝试了几件事,但我无法让它发挥作用。我想知道你们中是否有人知道我还能尝试什么或者我错过了什么。

有一件事我注意到,在构建应用程序时,我尝试使用angular cli的大部分资源都被webpack捆绑到单独的块中。这似乎是合乎逻辑的,因为它使用了webpack代码拆分。但是如果在编译时模块还不知道怎么办(但是编译模块存储在服务器上的某个地方)?捆绑不起作用,因为它找不到要导入的模块。并且使用SystemJS将在系统上发现时加载UMD模块,但也通过webpack捆绑在一个单独的块中。

我已经尝试过的一些资源;

我已经尝试并实现了一些代码,但目前还没有工作;

使用普通的module.ts文件扩展路由器

     this.router.config.push({
    path: "external",
    loadChildren: () =>
      System.import("./module/external.module").then(
        module => module["ExternalModule"],
        () => {
          throw { loadChunkError: true };
        }
      )
  });

正常SystemJS导入UMD捆绑包

System.import("./external/bundles/external.umd.js").then(modules => {
  console.log(modules);
  this.compiler.compileModuleAndAllComponentsAsync(modules['External']).then(compiled => {
    const m = compiled.ngModuleFactory.create(this.injector);
    const factory = compiled.componentFactories[0];
    const cmp = factory.create(this.injector, [], null, m);

    });
});

导入外部模块,不使用webpack(afaik)

const url = 'https://gist.githubusercontent.com/dianadujing/a7bbbf191349182e1d459286dba0282f/raw/c23281f8c5fabb10ab9d144489316919e4233d11/app.module.ts';
const importer = (url:any) => Observable.fromPromise(System.import(url));
console.log('importer:', importer);
importer(url)
  .subscribe((modules) => {
    console.log('modules:', modules, modules['AppModule']);
    this.cfr = this.compiler.compileModuleAndAllComponentsSync(modules['AppModule']);
    console.log(this.cfr,',', this.cfr.componentFactories[0]);
    this.external.createComponent(this.cfr.componentFactories[0], 0);
});

使用SystemJsNgModuleLoader

this.loader.load('app/lazy/lazy.module#LazyModule').then((moduleFactory: NgModuleFactory<any>) => {
  console.log(moduleFactory);
  const entryComponent = (<any>moduleFactory.moduleType).entry;
  const moduleRef = moduleFactory.create(this.injector);

  const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(entryComponent);
});

尝试加载使用汇总制作的模块

this.http.get(`./myplugin/${metadataFileName}`)
  .map(res => res.json())
  .map((metadata: PluginMetadata) => {

    // create the element to load in the module and factories
    const script = document.createElement('script');
    script.src = `./myplugin/${factoryFileName}`;

    script.onload = () => {
      //rollup builds the bundle so it's attached to the window object when loaded in
      const moduleFactory: NgModuleFactory<any> = window[metadata.name][metadata.moduleName + factorySuffix];
      const moduleRef = moduleFactory.create(this.injector);

      //use the entry point token to grab the component type that we should be rendering
      const compType = moduleRef.injector.get(pluginEntryPointToken);
      const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(compType); 
// Works perfectly in debug, but when building for production it returns an error 'cannot find name Component of undefined' 
// Not getting it to work with the router module.
    }

    document.head.appendChild(script);

  }).subscribe();

SystemJsNgModuleLoader的示例仅在模块已在应用程序的RouterModule中作为“延迟”路由提供时(在使用webpack构建时将其转换为块)时才有效

我在StackOverflow上找到了很多关于这个主题的讨论,如果事先知道的话,提供的解决方案似乎非常适合动态加载模块/组件。但没有一个适合我们的项目用例。请让我知道我仍然可以尝试或深入研究。

谢谢!

编辑:我发现了; https://github.com/kirjs/angular-dynamic-module-loading 并试试看。

更新:我创建了一个存储库,其中包含使用SystemJS动态加载模块的示例(并使用Angular 6); https://github.com/lmeijdam/angular-umd-dynamic-example


12269
2018-05-03 07:15


起源



答案:


我遇到了同样的问题。据我所知,直到现在:

Webpack将所有资源放在一个包中并替换所有资源 System.import 同 __webpack_require__。因此,如果要在运行时使用SystemJsNgModuleLoader动态加载模块,则加载程序将在bundle中搜索模块。如果捆绑包中不存在该模块,则会出现错误。 Webpack不会向服务器询问该模块。这对我们来说是一个问题,因为我们想要在构建/编译时加载一个我们不知道的模块。 我们需要的是在运行时为我们加载模块的加载器(懒惰和动态)。在我的示例中,我使用的是SystemJS和Angular 6 / CLI。

  1. 安装SystemJS:npm install systemjs -save
  2. 将它添加到angular.json:“scripts”:[“node_modules / systemjs / dist / system.src.js”]

app.component.ts

import { Compiler, Component, Injector, ViewChild, ViewContainerRef } from '@angular/core';

import * as AngularCommon from '@angular/common';
import * as AngularCore from '@angular/core';

declare var SystemJS;

@Component({
  selector: 'app-root',
  template: '<button (click)="load()">Load</button><ng-container #vc></ng-container>'
})
export class AppComponent {
  @ViewChild('vc', {read: ViewContainerRef}) vc;

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

  load() {
    // register the modules that we already loaded so that no HTTP request is made
    // in my case, the modules are already available in my bundle (bundled by webpack)
    SystemJS.set('@angular/core', SystemJS.newModule(AngularCore));
    SystemJS.set('@angular/common', SystemJS.newModule(AngularCommon));

    // now, import the new module
    SystemJS.import('my-dynamic.component.js').then((module) => {
      this.compiler.compileModuleAndAllComponentsAsync(module.default)
            .then((compiled) => {
                let moduleRef = compiled.ngModuleFactory.create(this.injector);
                let factory = compiled.componentFactories[0];
                if (factory) {
                    let component = this.vc.createComponent(factory);
                    let instance = component.instance;
                }
            });
    });
  }
}

我-dynamic.component.ts

import { NgModule, Component } from '@angular/core';
import { CommonModule } from '@angular/common';

import { Other } from './other';

@Component({
    selector: 'my-dynamic-component',
    template: '<h1>Dynamic component</h1><button (click)="LoadMore()">LoadMore</button>'
})    
export class MyDynamicComponent {
    LoadMore() {
        let other = new Other();
        other.hello();
    }
}
@NgModule({
    declarations: [MyDynamicComponent],
    imports: [CommonModule],
})
export default class MyDynamicModule {}

other.component.ts

export class Other {
    hello() {
        console.log("hello");
    }
}

如您所见,我们可以告诉SystemJS我们的包中已经存在哪些模块。所以我们不需要再加载它们(SystemJS.set)。我们导入的所有其他模块 my-dynamic-component (在这个例子中 other)将在运行时从服务器请求。


5
2018-05-17 15:19



我会尝试一下,谢谢! - Lars Meijdam
您的示例中的动态组件,它位于何处?您是否首先创建了一个库(ng g lib <name>)并使用从构建该库收到的UMD模块?或者在提供/构建整个应用程序之前自己进行转换?否则你有一个plunkr或? - Lars Meijdam
我的设置有点复杂。因此我不确定。一旦你执行 SystemJS.import('my-dynamic.component.js').then() 您将看到浏览器在哪里寻找它。它应该位于应用的根文件夹中或旁边 AppComponent。我正在使用Visual Studio,Visual Studio正在转换我的 my-dynamic.component.ts。你可以使用tsc my-dynamic.component.ts如果我找到一些时间,我会创建一个plunkr。但在那之前我会在这里更新我的答案。 - Michael
我刚刚创建了一个github: github.com/mrmscmike/ngx-dynamic-module-loader  这只是一个草案,我们肯定需要改进它。也许我们可以收集我们的经验。欢迎任何帮助。 - Michael
@kamalnayan我想我知道“找不到'未定义'的NgModule元数据”是什么问题。如果你看看你的 *.umd.js 并将其与示例进行比较 my.module.js,你可以看到my.module执行以下操作 exports["default"] = MyModule;。 Angular 6 lib确实如此 exports.MyModule = MyModule;。如果你 compileModuleAndAllComponentsAsync(module['PluginModule']) 有用。不知道怎么弄 .default 加工 - Thomas Schneiter


答案:


我遇到了同样的问题。据我所知,直到现在:

Webpack将所有资源放在一个包中并替换所有资源 System.import 同 __webpack_require__。因此,如果要在运行时使用SystemJsNgModuleLoader动态加载模块,则加载程序将在bundle中搜索模块。如果捆绑包中不存在该模块,则会出现错误。 Webpack不会向服务器询问该模块。这对我们来说是一个问题,因为我们想要在构建/编译时加载一个我们不知道的模块。 我们需要的是在运行时为我们加载模块的加载器(懒惰和动态)。在我的示例中,我使用的是SystemJS和Angular 6 / CLI。

  1. 安装SystemJS:npm install systemjs -save
  2. 将它添加到angular.json:“scripts”:[“node_modules / systemjs / dist / system.src.js”]

app.component.ts

import { Compiler, Component, Injector, ViewChild, ViewContainerRef } from '@angular/core';

import * as AngularCommon from '@angular/common';
import * as AngularCore from '@angular/core';

declare var SystemJS;

@Component({
  selector: 'app-root',
  template: '<button (click)="load()">Load</button><ng-container #vc></ng-container>'
})
export class AppComponent {
  @ViewChild('vc', {read: ViewContainerRef}) vc;

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

  load() {
    // register the modules that we already loaded so that no HTTP request is made
    // in my case, the modules are already available in my bundle (bundled by webpack)
    SystemJS.set('@angular/core', SystemJS.newModule(AngularCore));
    SystemJS.set('@angular/common', SystemJS.newModule(AngularCommon));

    // now, import the new module
    SystemJS.import('my-dynamic.component.js').then((module) => {
      this.compiler.compileModuleAndAllComponentsAsync(module.default)
            .then((compiled) => {
                let moduleRef = compiled.ngModuleFactory.create(this.injector);
                let factory = compiled.componentFactories[0];
                if (factory) {
                    let component = this.vc.createComponent(factory);
                    let instance = component.instance;
                }
            });
    });
  }
}

我-dynamic.component.ts

import { NgModule, Component } from '@angular/core';
import { CommonModule } from '@angular/common';

import { Other } from './other';

@Component({
    selector: 'my-dynamic-component',
    template: '<h1>Dynamic component</h1><button (click)="LoadMore()">LoadMore</button>'
})    
export class MyDynamicComponent {
    LoadMore() {
        let other = new Other();
        other.hello();
    }
}
@NgModule({
    declarations: [MyDynamicComponent],
    imports: [CommonModule],
})
export default class MyDynamicModule {}

other.component.ts

export class Other {
    hello() {
        console.log("hello");
    }
}

如您所见,我们可以告诉SystemJS我们的包中已经存在哪些模块。所以我们不需要再加载它们(SystemJS.set)。我们导入的所有其他模块 my-dynamic-component (在这个例子中 other)将在运行时从服务器请求。


5
2018-05-17 15:19



我会尝试一下,谢谢! - Lars Meijdam
您的示例中的动态组件,它位于何处?您是否首先创建了一个库(ng g lib <name>)并使用从构建该库收到的UMD模块?或者在提供/构建整个应用程序之前自己进行转换?否则你有一个plunkr或? - Lars Meijdam
我的设置有点复杂。因此我不确定。一旦你执行 SystemJS.import('my-dynamic.component.js').then() 您将看到浏览器在哪里寻找它。它应该位于应用的根文件夹中或旁边 AppComponent。我正在使用Visual Studio,Visual Studio正在转换我的 my-dynamic.component.ts。你可以使用tsc my-dynamic.component.ts如果我找到一些时间,我会创建一个plunkr。但在那之前我会在这里更新我的答案。 - Michael
我刚刚创建了一个github: github.com/mrmscmike/ngx-dynamic-module-loader  这只是一个草案,我们肯定需要改进它。也许我们可以收集我们的经验。欢迎任何帮助。 - Michael
@kamalnayan我想我知道“找不到'未定义'的NgModule元数据”是什么问题。如果你看看你的 *.umd.js 并将其与示例进行比较 my.module.js,你可以看到my.module执行以下操作 exports["default"] = MyModule;。 Angular 6 lib确实如此 exports.MyModule = MyModule;。如果你 compileModuleAndAllComponentsAsync(module['PluginModule']) 有用。不知道怎么弄 .default 加工 - Thomas Schneiter


我用过了 https://github.com/kirjs/angular-dynamic-module-loading Angular 6的库支持解决方案,用于创建我在Github上共享的应用程序。由于公司政策,需要离线。一旦有关示例项目源的讨论结束,我将在Github上分享它!

更新:可以找到回购; https://github.com/lmeijdam/angular-umd-dynamic-example 


3
2018-05-17 06:25





我相信如果使用webpack构建和运行主应用程序,使用SystemJS加载UMD包是可能的。我使用了一个使用ng-packagr来构建动态插件/插件模块的UMD包的解决方案。这个github演示了描述的过程: https://github.com/nmarra/dynamic-module-loading


2
2018-06-06 15:17



这是一个非常酷的解决方案。虽然我试图通过定义InjectionToken的共享库将主应用程序提供的服务注入到插件中。到目前为止没有运气。有什么想法吗? - Gob
非常好!我做了几乎类似的解决方案。我稍后会把它放在github上! - Lars Meijdam


使用角度6库和汇总来做到这一点。我只是试验它,我可以与主应用程序共享独立的角度AOT模块,而无需重建最后。

  1. 在角度库中将angularCompilerOptions.skipTemplateCodegen设置为false,在构建库之后,您将获得模块工厂。

  2. 之后构建一个umd模块,汇总如下:汇总 dist / plugin / esm2015 / lib / plugin.module.ngfactory.js --file src / assets / plugin.module.umd.js --format umd --name plugin

  3. 在主应用程序中加载文本源umd包并使用模块上下文对其进行评估
  4. 现在,您可以从exports对象访问ModuleFactory

这里 https://github.com/iwnow/angular-plugin-example 你可以找到如何使用独立的建筑和AOT开发插件


1
2017-10-02 10:09





是的,您可以通过将模块引用为路由器中的模块来延迟加载模块。这是一个例子 https://github.com/start-angular/SB-Admin-BS4-Angular-6

  1. 首先将您使用的所有组件连接到一个模块中
  2. 现在在路由器中引用该模块,angular会将模块延迟加载到视图中。

0
2018-05-17 06:49



感谢您的评论,我已经知道使用路由器延迟加载。唯一发生的事情是Angular的构建过程使用Webpack从延迟加载的模块创建块文件。最后一部分是我不想发生的事情。因为我实际上正在寻找一种解决方案来在运行时加载模块而不预先知道模块。相反,我会从服务器检索配置,这应该使Angular知道要加载哪个模块/插件 - Lars Meijdam