运行时将组件或模块加载到 angular2 中的模块中

runtime load components or modules into a module in angular2

我有一个 angular 应用程序,它是使用 Typescript 构建并与 webpack 捆绑在一起的。这里没有什么不寻常的。 我想要做的是在 运行 时间允许插件,这意味着包外的组件 and/or 模块也应该能够在应用程序中注册。 到目前为止,我已经尝试在 index.html 中包含另一个 webpack 包,并使用隐式数组将所述模块/组件推送到其中,并在我的模块中导入这些。

看到导入正在使用隐式变量。这适用于捆绑包中的模块,但其他捆绑包中的模块将不起作用。

@NgModule({
  imports: window["app"].modulesImport,
  declarations: [
      DYNAMIC_DIRECTIVES,
      PropertyFilterPipe,
      PropertyDataTypeFilterPipe,
      LanguageFilterPipe,      
      PropertyNameBlackListPipe      
  ],
  exports: [
      DYNAMIC_DIRECTIVES,
      CommonModule,
      FormsModule,
      HttpModule
  ]
})
export class PartsModule {

    static forRoot()
    {
        return {
            ngModule: PartsModule,
            providers: [ ], // not used here, but if singleton needed
        };
    }
}

我也试过使用 es5 代码创建一个模块和一个组件,如下所示,并将相同的东西推送到我的模块数组:

var HelloWorldComponent = function () {

};

HelloWorldComponent.annotations = [
    new ng.core.Component({
        selector: 'hello-world',
        template: '<h1>Hello World!</h1>',
    })
];

window["app"].componentsLazyImport.push(HelloWorldComponent);

两种方法都会导致以下错误:

ncaught Error: Unexpected value 'ExtensionsModule' imported by the module 'PartsModule'. Please add a @NgModule annotation.
    at syntaxError (http://localhost:3002/dist/app.bundle.js:43864:34) [<root>]
    at http://localhost:3002/dist/app.bundle.js:56319:44 [<root>]
    at Array.forEach (native) [<root>]
    at CompileMetadataResolver.getNgModuleMetadata (http://localhost:3002/dist/app.bundle.js:56302:49) [<root>]
    at CompileMetadataResolver.getNgModuleSummary (http://localhost:3002/dist/app.bundle.js:56244:52) [<root>]
    at http://localhost:3002/dist/app.bundle.js:56317:72 [<root>]
    at Array.forEach (native) [<root>]
    at CompileMetadataResolver.getNgModuleMetadata (http://localhost:3002/dist/app.bundle.js:56302:49) [<root>]
    at CompileMetadataResolver.getNgModuleSummary (http://localhost:3002/dist/app.bundle.js:56244:52) [<root>]
    at http://localhost:3002/dist/app.bundle.js:56317:72 [<root>]
    at Array.forEach (native) [<root>]
    at CompileMetadataResolver.getNgModuleMetadata (http://localhost:3002/dist/app.bundle.js:56302:49) [<root>]
    at JitCompiler._loadModules (http://localhost:3002/dist/app.bundle.js:67404:64) [<root>]
    at JitCompiler._compileModuleAndComponents (http://localhost:3002/dist/app.bundle.js:67363:52) [<root>]

请注意,如果我尝试使用组件而不是模块,我会将它们放在声明中,这会导致组件出现相应的错误,说我需要添加 @pipe/@component 注释。

我觉得这应该是可行的,但我不知道我错过了什么。我正在使用 angular@4.0.0

2017 年 11 月 5 日更新

所以我决定退后一步,从头开始。我没有使用 webpack,而是决定尝试使用 SystemJS,因为我在 Angular 中找到了一个核心组件。这次我使用以下组件和服务来插入组件:

typebuilder.ts

import { Component, ComponentFactory, NgModule, Input, Injectable, CompilerFactory } from '@angular/core';
import { JitCompiler } from '@angular/compiler';
import {platformBrowserDynamic} from "@angular/platform-browser-dynamic";

export interface IHaveDynamicData { 
    model: any;
}

@Injectable()
export class DynamicTypeBuilder {

    protected _compiler : any;
         // wee need Dynamic component builder
    constructor() {
        const compilerFactory : CompilerFactory = platformBrowserDynamic().injector.get(CompilerFactory);
        this._compiler = compilerFactory.createCompiler([]);
    }

  // this object is singleton - so we can use this as a cache
    private _cacheOfFactories: {[templateKey: string]: ComponentFactory<IHaveDynamicData>} = {};

    public createComponentFactoryFromType(type: any) : Promise<ComponentFactory<any>> {
        let module = this.createComponentModule(type);
            return new Promise((resolve) => {
            this._compiler
                .compileModuleAndAllComponentsAsync(module)
                .then((moduleWithFactories : any) =>
                {
                    let _ = window["_"];
                    let factory = _.find(moduleWithFactories.componentFactories, { componentType: type });
                    resolve(factory);
                });
        });
    }

    protected createComponentModule (componentType: any) {
        @NgModule({
        imports: [
        ],
        declarations: [
            componentType
        ],
        })
        class RuntimeComponentModule
        {
        }
        // a module for just this Type
        return RuntimeComponentModule;
    }
}

Dynamic.component.ts

import { Component, Input, ViewChild, ViewContainerRef, SimpleChanges, AfterViewInit, OnChanges, OnDestroy, ComponentFactory, ComponentRef } from "@angular/core";
import { DynamicTypeBuilder } from "../services/type.builder";

@Component({
    "template": '<h1>hello dynamic component <div #dynamicContentPlaceHolder></div></h1>',
    "selector": 'dynamic-component'
})
export class DynamicComponent implements AfterViewInit, OnChanges, OnDestroy {

    @Input() pathToComponentImport : string;

    @ViewChild('dynamicContentPlaceHolder', {read: ViewContainerRef}) 
    protected dynamicComponentTarget: ViewContainerRef;
    protected componentRef: ComponentRef<any>;

    constructor(private typeBuilder: DynamicTypeBuilder) 
    {

    }  

    protected refreshContent() : void {
        if (this.pathToComponentImport != null && this.pathToComponentImport.indexOf('#') != -1) {
          let [moduleName, exportName] = this.pathToComponentImport.split("#");
          window["System"].import(moduleName)
            .then((module: any) => module[exportName])
            .then((type: any) => {
                this.typeBuilder.createComponentFactoryFromType(type)
                .then((factory: ComponentFactory<any>) =>
                {
                    // Target will instantiate and inject component (we'll keep reference to it)
                    this.componentRef = this
                        .dynamicComponentTarget
                        .createComponent(factory);

                    // let's inject @Inputs to component instance
                    let component = this.componentRef.instance;

                    component.model = { text: 'hello world' };

                    //...
                });
            });
      }
    }

    ngOnDestroy(): void {
    }

    ngOnChanges(changes: SimpleChanges): void {
    }

    ngAfterViewInit(): void {
        this.refreshContent();
    }

}

现在我可以 link 像这样的任何给定组件:

<dynamic-component pathToComponentImport="/app/views/components/component1/extremely.dynamic.component.js#ExtremelyDynamicComponent"></dynamic-component>

打字稿配置:

 {
  "compilerOptions": {
    "target": "es5",
    "module": "system",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "allowJs": true,
    "experimentalDecorators": true,
    "lib": [ "es2015", "dom" ],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  },
    "exclude": [
      "node_modules",
      "systemjs-angular-loader.js",
      "systemjs.config.extras.js",
      "systemjs.config.js"
  ]
}

在我的打字稿配置之上。所以这可行,但是我不确定我是否对使用 SystemJS 感到满意。我觉得 webpack 也应该可以做到这一点,并且不确定这是否是 TC 编译 webpack 不理解的文件的方式......如果我尝试 运行 这段代码,我仍然会遇到缺少的装饰器异常在 webpack 包中。

此致 莫腾

所以我一直在努力寻找解决方案。最后我做到了。 这是否是一个 hacky 解决方案,还有更好的方法我不知道...... 现在,这就是我解决它的方式。但我确实希望将来或即将出现更现代的解决方案。

这个方案本质上是SystemJS和webpack的混合模型。在您 运行 的时候,您需要使用 SystemJS 来加载您的应用程序,并且您的 webpack 包需要由 SystemJS 使用。为此,您需要一个用于 webpack 的插件 使这成为可能。开箱即用的 systemJS 和 webpack 不兼容,因为它们使用不同的模块定义。不过这个插件不行。

  1. 在您的核心应用程序和插件中,您需要安装一个名为
  2. 的 webpack 扩展

"webpack-system-register".

我有2.2.1版的webpack和1.5.0版的WSR。 1.1 在您的 webpack.config.js 中,您需要将 WebPackSystemRegister 添加为 core.plugins 中的第一个元素,如下所示:

config.plugins = [
  new WebpackSystemRegister({
    registerName: 'core-app', // optional name that SystemJS will know this bundle as. 
    systemjsDeps: [
    ]
  }) 
  //you can still use other plugins here as well
];

由于现在使用 SystemJS 加载应用程序,因此您还需要一个 systemjs 配置。我的看起来像这样。

(function (global) {
System.config({
paths: {
  // paths serve as alias
  'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
  // our app is within the app folder
  'app': 'app',

  // angular bundles
  // '@angular/core': 'npm:@angular/core/bundles/core.umd.min.js',
  '@angular/core': '/dist/fake-umd/angular.core.fake.umd.js',
  '@angular/common': '/dist/fake-umd/angular.common.fake.umd.js',
  '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.min.js',
  '@angular/platform-browser': '/dist/fake-umd/angular.platform.browser.fake.umd.js',
  '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.min.js',
  '@angular/http': '/dist/fake-umd/angular.http.fake.umd.js',
  '@angular/router': 'npm:@angular/router/bundles/router.umd.min.js',
  '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.min.js',
  '@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.min.js',
  '@angular/material': 'npm:@angular/material/bundles/material.umd.js',
  '@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.min.js',
  '@angular/animations': 'npm:@angular/animations/bundles/animations.umd.min.js',
  'angular2-grid/main': 'npm:angular2-grid/bundles/NgGrid.umd.min.js',      
  '@ng-bootstrap/ng-bootstrap': 'npm:@ng-bootstrap/ng-bootstrap/bundles/ng-bootstrap.js',            
  // other libraries
  'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js', 
  "rxjs": "npm:rxjs",          


},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
  app: {
    defaultExtension: 'js',
    meta: {
      './*.html': {
        defaultExension: false,
      },
      './*.js': {
        loader: '/dist/configuration/systemjs-angular-loader.js'
      },
    }
  },
  rxjs: {
    defaultExtension: 'js'
  },
},
  });
 })(this);

稍后我会在回答中回到地图元素,描述为什么 angular 在那里以及它是如何完成的。 在您的 index.html 中,您的参考文献需要像这样:

<script src="node_modules/systemjs/dist/system.src.js"></script> //system
<script src="node_modules/reflect-metadata/reflect.js"></script>
<script src="/dist/configuration/systemjs.config.js"></script> // config for system js
<script src="/node_modules/zone.js/dist/zone.js"></script>
<script src="/dist/declarations.js"></script> // global defined variables
<script src="/dist/app.bundle.js"></script> //core app
<script src="/dist/extensions.bundle.js"></script> //extensions app

现在,这让我们可以 运行 随心所欲。然而,这有一点扭曲,即您仍然 运行 进入原始 post 中描述的异常。 要解决这个问题(虽然我仍然不知道为什么会这样),我们需要在插件源代码中做一个小技巧,它是使用 webpack 和 webpack-system-register 创建的:

plugins: [
  new WebpackSystemRegister({
      registerName: 'extension-module', // optional name that SystemJS will know this bundle as. 
      systemjsDeps: [
        /^@angular/,
        /^rx/
      ]
  })

上面的代码使用 webpack 系统寄存器从扩展包中排除 Angular 和 RxJs 模块。将要发生的是 systemJS 在导入模块时将 运行 转换为 angular 和 RxJs。它们被排除在外,因此系统将尝试使用 System.config.js 的地图配置加载它们。有趣的部分来了。:

在核心应用程序中,我在 webpack 中导入所有 angular 模块并将它们公开在 public 变量中。这可以在您的应用程序中的任何位置完成,我已经在 main.ts 中完成了。示例如下:

lux.bootstrapModule = function(module, requireName, propertyNameToUse) {
    window["lux"].angularModules.modules[propertyNameToUse] = module;
    window["lux"].angularModules.map[requireName] = module;
}

import * as angularCore from '@angular/core';
window["lux"].bootstrapModule(angularCore, '@angular/core', 'core');
platformBrowserDynamic().bootstrapModule(AppModule);

在我们的 systemjs 配置中,我们创建了一个这样的映射,让 systemjs 知道在哪里加载我们的依赖项(它们被排除在扩展包中,如上所述):

'@angular/core': '/dist/fake-umd/angular.core.fake.umd.js',
'@angular/common': '/dist/fake-umd/angular.common.fake.umd.js',

因此,每当 systemjs 遇到 angular 核心或 angular 通用时,它就会被告知从我定义的假 umd 包中加载它。它们看起来像这样:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define([], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS-like
        module.exports = factory();
    }
}(this, function () {

    //    exposed public method
    return window["lux"].angularModules.modules.core;
}));

最终,使用 运行time 编译器,我现在可以使用外部加载的模块了:

所以系统现在可以在Angular中使用来导入和编译模块。每个模块只需要发生一次。不幸的是,这会阻止您遗漏非常繁重的 运行time 编译器。

我有一个可以加载模块和 return 工厂的服务,最终使您能够延迟加载 在核心转译时 不知道的模块.这对于商业平台、CMS、CRM 系统等软件供应商或其他开发人员在没有源代码的情况下为此类系统创建插件的软件供应商来说非常有用。

window["System"].import(moduleName) //module name is defined in the webpack-system-register "registerName"
            .then((module: any) => module[exportName])
            .then((type: any) => {
                let module = this.createComponentModuleWithModule(type);
                this._compiler.compileModuleAndAllComponentsAsync(module).then((moduleWithFactories: any) => {
                    const moduleRef = moduleWithFactories.ngModuleFactory.create(this.injector);

                    for (let factory of moduleWithFactories.componentFactories) {

                        if (factory.selector == 'dynamic-component') { //all extension modules will have a factory for this. Doesn't need to go into the cache as not used.
                            continue;
                        }

                        var factoryToCache = {
                            template: null,
                            injector: moduleRef.injector,
                            selector: factory.selector,
                            isExternalModule: true,
                            factory: factory,
                            moduleRef: moduleRef,
                            moduleName: moduleName,
                            exportName: exportName
                        }

                        if (factory.selector in this._cacheOfComponentFactories) {
                            var existingFactory = this._cacheOfComponentFactories[factory.selector]
                            console.error(`Two different factories conflicts in selector:`, factoryToCache, existingFactory)
                            throw `factory already exists. Did the two modules '${moduleName}-${exportName}' and '${existingFactory.moduleName}-${existingFactory.exportName}' share a component selector?: ${factory.selector}`;
                        }

                        if (factory.selector.indexOf(factoryToCache.exportName) == -1) {
                            console.warn(`best practice for extension modules is to prefix selectors with exportname to avoid conflicts. Consider using: ${factoryToCache.exportName}-${factory.selector} as a selector for your component instead of ${factory.selector}`);
                        }

                        this._cacheOfComponentFactories[factory.selector] = factoryToCache;
                    }
                })
                resolve();
            })

总结一下:

  1. 在您的核心应用程序和扩展模块中安装 webpack-system-register
  2. 在您的扩展包中排除 angular 依赖项
  3. 在您的核心应用中,在全局变量中公开 angular 依赖项
  4. 通过return公开的依赖项
  5. 为每个依赖项创建一个假包
  6. 在你的 systemjs 映射中,添加要加载到假 js 包中的依赖项
  7. 运行Angular 中的时间编译器现在可用于使用 webpack-system-register
  8. 加载已使用 webpack 打包的模块