使用 Angular CLI 和 Angular 5 在运行时动态加载新module

IT技术 javascript angular webpack angular-cli angular5
2021-01-22 05:24:06

目前我正在处理一个托管在客户端服务器上的项目。对于新的“module”,不打算重新编译整个应用程序。也就是说,客户端想要在运行时更新路由器/延​​迟加载的module我已经尝试了几件事,但我无法让它工作。我想知道你们中是否有人知道我还可以尝试什么或我错过了什么。

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

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

我已经尝试并实现了一些代码,但此时不起作用;

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

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

UMD 包的普通 SystemJS 导入

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);
    });
});

导入外部module,不适用于 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);
  });

尝试加载一个用 rollup 制作的module

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 示例仅在module已经在应用程序的 RouterModule 中作为“惰性”路由提供时才有效(使用 webpack 构建时将其变成块)

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

谢谢!

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

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

6个回答

我面临着同样的问题。据我目前的理解:

Webpack 将所有资源放在一个包中,并将所有资源替换System.import__webpack_require__. 因此,如果您想在运行时使用 SystemJsNgModuleLoader 动态加载module,加载器将在包中搜索module。如果捆绑包中不存在该module,您将收到错误消息。Webpack 不会向服务器询问该module。这对我们来说是一个问题,因为我们想要加载一个在构建/编译时我们不知道的module。我们需要的是加载器,它会在运行时为我们加载一个module(惰性和动态)。在我的示例中,我使用 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 我们的包中已经存在哪些module。所以我们不需要再次加载它们 ( SystemJS.set)。我们在我们的my-dynamic-component(在本例中other导入的所有其他module将在运行时从服务器请求。

我刚刚创建了一个 github:github.com/mrmscmike/ngx-dynamic-module-loader 这只是一个草案,我们肯定需要改进它。也许我们可以收集我们的经验。欢迎任何帮助。
2021-03-15 05:24:06
您示例中的动态组件,它位于何处?您是否首先创建了一个库 (ng g lib <name>) 并使用了从构建该库中收到的 UMD module?还是在服务/构建整个应用程序之前自己转译它?否则你有一个plunkr还是?
2021-03-16 05:24:06
我有同样的问题,仍然无法修复它。我已经使用 angular 6 CLI 创建了一个库,现在需要动态加载它。我已将该库处理到主 Angular 应用程序的 dist 文件夹中。但是 SystemJS.import('/sf-ws1/bundles/sf-ws1.umd.js') 不起作用。获取错误:错误:未找到“未定义”的 NgModule 元数据。关于如何实现这一点的任何建议,如果有人可以创建一个工作的 git repo,都会非常有帮助。
2021-03-19 05:24:06
我的设置有点复杂。因此我不确定。执行后,SystemJS.import('my-dynamic.component.js').then()您将看到浏览器正在寻找它的位置。它应该在您的应用程序的根文件夹中或旁边AppComponent我正在使用 Visual Studio,Visual Studio 正在将我的my-dynamic.component.ts. 你可以使用 tsc my-dynamic.component.ts 如果有时间我会创建一个 plunkr。但在那之前我会在这里更新我的答案。
2021-04-06 05:24:06
@kamalnayan 我想我知道“未找到用于‘未定义’的 NgModule 元数据”的问题所在。如果您查看您的*.umd.js并将其与示例进行比较my.module.js,您可以看到 my.module 执行以下操作exports["default"] = MyModule;Angular 6 lib 可以exports.MyModule = MyModule;如果你compileModuleAndAllComponentsAsync(module['PluginModule'])它有效。不知道如何开始.default工作
2021-04-12 05:24:06

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

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

我们在项目中搜索的解决方案更多地基于编译module (JS),从服务器动态加载它们(这仍然有争议)。所以它只会加载放置在特定文件夹中的“完成”module,而不是typescript
2021-03-20 05:24:06
此解决方案将脚本放在assets. 链接到typescriptmodule怎么样?必须先转换成js手动复制。
2021-03-21 05:24:06

我已经在 Angular 6 中进行了测试,以下解决方案适用于从外部包或内部module动态加载module。

1.如果要从库项目或包中动态加载module:

我有一个库项目“admin”(或者您可以使用包)和一个应用程序项目“app”。在我的“admin”库项目中,我有 AdminModule 和 AdminRoutingModule。在我的“应用程序”项目中:

一个。在 tsconfig.app.json 中进行更改:

  "compilerOptions": {
    "module": "esNext",
  },

在 app-routing.module.ts 中:

const routes: Routes = [
    {
        path: 'admin',
        loadChildren: async () => {
            const a = await import('admin')
            return a['AdminModule'];
        }
    },
    {
        path: '',
        redirectTo: '',
        pathMatch: 'full'
    }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule {
}

2.如果你想从同一个项目中加载一个module。

有4种不同的选择:

一个。在 app-routing.module.ts 中:

const routes: Routes = [
    {
        path: 'example',
        /* Options 1: Use component */
        // component: ExampleComponent,  // Load router from component
        /* Options 2: Use Angular default lazy load syntax */
        loadChildren: './example/example.module#ExampleModule',  // lazy load router from module
        /* Options 3: Use Module */
        // loadChildren: () => ExampleModule, // load router from module
        /* Options 4: Use esNext, you need to change tsconfig.app.json */
        /*
        loadChildren: async () => {
            const a = await import('./example/example.module')
            return a['ExampleModule'];
        }
        */
    },
    {
        path: '',
        redirectTo: '',
        pathMatch: 'full'
    }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule {
}
``

您是否检查了问题末尾注明的我的 github 项目?:) 这可能会有所帮助!;) github.com/lmeijdam/angular-umd-dynamic-example
2021-03-15 05:24:06
非常感谢你。它运作良好。从 URL 加载是一个非常好的解决方案。:)
2021-03-15 05:24:06
@LarsMeijdam,太好了。我一直在寻找与您的情况类似的解决方案。但不同的是,我想要一个配置文件来列出所有module,然后动态加载到应用程序中。不幸的是,我上面的方法(案例 1)仍然存在一些问题。即为了使用await import(...),我不能使用变量,这意味着我仍然无法实现动态加载任何包的目标。但情况 2 工作正常。我还在用webpack做研究,看看有没有其他线索。
2021-03-20 05:24:06
不错的答案!老实说,我见过几个使用延迟加载机制,我实际尝试实现的一件事是在运行时加载它们,而不重建整个应用程序(因此只需刷新浏览器就足够了)。我知道这对于 loadChildren 是不可能的,因为您需要预先知道所有路线才能使其正常工作。
2021-04-11 05:24:06

用 angular 6 库和 rollup 来做这件事。我刚刚尝试过它,我可以与主应用程序共享独立的 angular AOT module,而无需最后重建。

  1. 在 angular 库中设置angularCompilerOptions.skipTemplateCodegen为 false 并且在构建库之后您将获得module工厂。
  2. 之后构建一个带有汇总的 umd module,如下所示: rollup dist/plugin/esm2015/lib/plugin.module.ngfactory.js --file src/assets/plugin.module.umd.js --format umd --name plugin
  3. 在主应用程序中加载文本源 umd 包并使用module上下文对其进行评估
  4. 现在您可以从导出对象访问 ModuleFactory

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

我喜欢你的解决方案,我在我的版本中实际上阻止的一件事是使用'eval',我看到你仍在使用它。我用来摆脱它的解决方案之一是使用 SystemJS。
2021-03-25 05:24:06
这种设置的一个问题是,当插件项目导入一个列出了入口组件的 Angular module时,生成的插件 UMD 将最终包含入口组件中列出的所有组件的工厂。
2021-03-25 05:24:06

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

这是一个非常酷的解决方案。尽管我尝试通过定义 InjectionToken 的共享库将主应用程序提供的服务注入到插件中。到目前为止没有运气。对此有何想法?
2021-03-28 05:24:06