运行时将组件或模块加载到angular2的模块中 [英] runtime load components or modules into a module in angular2

查看:205
本文介绍了运行时将组件或模块加载到angular2的模块中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个使用Typescript构建并与webpack捆绑在一起的角度应用程序.这里没什么异常. 我要做的是允许运行时上的插件,这意味着捆绑包之外的组件和/或模块也应该能够在应用程序中注册. 到目前为止,我已经尝试在index.html中包含另一个webpack捆绑包,并使用隐式数组将所述模块/组件推入其中,然后在我的模块中将其导入.

I have an angular app that are built using Typescript and bundled together with webpack. Nothing unusual here. What i want to do is to allow plugins on runtime, which means that components and/or modules outside the bundle should be able to be registered in the app as well. So far I've tried to include another webpack bundle in index.html and using an implict array to push said module / component into that, and in my module import these.

请参阅导入使用隐式变量.此方法适用于捆绑软件中的模块,但另一个捆绑软件中的模块将不起作用.

See the imports are using an implict variable. This works for modules inside the bundle, but modules in the other bundle will not work.

@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代码创建模块和组件,如下所示,并将相同的内容推送到我的模块数组中:

I've also tried creating a module and a component using es5 code, like below, and push the same thing to my modules array:

var HelloWorldComponent = function () {

};

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

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

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

Both approaches result in the following error:

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注释.

Please note that if i try with a component instead of a module, i put them in declarations instead, which results in the corresponding error for the components saying i need to add a @pipe/@component annotation instead.

我认为这应该可行,但是我不知道我要缺少什么.我正在使用angular@4.0.0

I feel this should be doable, but i don't know what I'm missing. Im using angular@4.0.0

更新2017年11月5日

因此,我决定退后一步,从头开始.我决定不尝试使用 webpack ,而是尝试使用 SystemJS ,因为我在Angular中找到了一个核心组件.这次我使用以下组件和服务来插入组件,使它正常工作:

So i decided to take a step back from this and start from scratch. Instead of using webpack I decided to try with SystemJS instead as i found a core component in Angular. This time i got it working using the following component and service to insert components:

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

}

现在,我可以像这样链接到任何给定的组件:

Now i can link to any given component like this:

<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捆绑包中运行此代码,我仍然会遇到缺少的装饰器异常.

And above my typescript config. So this works, however I'm not sure that I'm happy with using SystemJS. I feel like this should be possible with webpack as well and unsure if it's the way that TC compiles the files that webpack does not understand... I'm still getting the missing decorator exception if i try to run this code in a webpack bundle.

最诚挚的问候 莫滕(Morten)

Best regards Morten

推荐答案

所以我一直在努力寻找解决方案.最后我做到了. 这是否是一个骇人听闻的解决方案,还有我不知道的更好的方法... 现在,这就是我解决的方法.但我确实希望将来或指日可待有更现代的解决方案.

So I was hammering through trying to find a solution. And in the end i did. Whether or not this is a hacky solution and there's a better way i don't know... For now, this is how i solved it. But i do hope there's a more modern solution in the future or just around the corner.

此解决方案实质上是SystemJS和Webpack的混合模型.在运行时中,您需要使用SystemJS来加载您的应用程序,并且Webpack捆绑包必须由SystemJS消耗.为此,您需要一个用于webpack的插件 使这成为可能.系统JS和webpack不兼容,因为它们使用不同的模块定义.不过不支持此插件.

This solution is essentially a hybrid model of SystemJS and webpack. In your runtime you need to use SystemJS to load your app, and your webpack bundle needs to be consumable by SystemJS. To do this you need a plugin for webpack that makes this possible. Out of the box systemJS and webpack are not compatible as they use different module definitions. Not with this plugin though.

  1. 在您的核心应用和插件中,您都需要安装一个名为
  2. 的webpack扩展程序
  1. In both your core app and your plugins, you need to install an extension for webpack called

"webpack-system-register".

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

I have version 2.2.1 of webpack and 1.5.0 of WSR. 1.1 In your webpack.config.js you need to add WebPackSystemRegister as the first element in your core.plugins like so:

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配置.我的看起来像这样.

Since SystemJS is now used to load the app, you need a systemjs config as well. Mine looks like this.

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

稍后,我将在答案中返回map元素,描述为什么在其中存在角度以及如何完成角度. 在您的index.html中,您需要这样的引用:

I will get back to the map element later on in the answer, describing why angular is in there and how it is done. In your index.html you need to have your references kinda like this:

<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

现在,这使我们可以根据需要运行所有内容.但是,这样做有一点曲折,那就是您仍然会遇到原始帖子中所述的异常. 要解决此问题(尽管我仍然不知道为什么会这样),我们需要在插件源代码中做一个简单的技巧,即使用webpack和webpack-system-register创建的

For now, this allows us to run everything as we want. However there's a little twist to this, which is that you still run into the exceptions as described in the original post. To fix this (i still don't know why this happens though), we need to do a single trick in the plugin source code, that are created using webpack and 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将尝试使用System.config.js的映射配置来加载它们.现在是有趣的部分.

Code above uses webpack system register to exclude Angular and RxJs modules from the extension bundle. What is going to happen is that systemJS will run into angular and RxJs when importing the module. They are left out, so System will try to load them, using the map configuration of System.config.js. Now here comes the fun part.:

在核心应用程序中,在webpack中,我导入所有角度模块并将它们公开在公共变量中.这可以在您的应用程序中的任何地方完成,我已经在main.ts中完成了.下面给出的示例:

In the core app, in webpack i import all angular modules and expose them in a public variable. This can be done anywhere in your app, I've done it in main.ts. Example given below:

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知道将我们的penpencens加载到哪里(如上所述,它们在扩展束中是不包括在内的):

In our systemjs config we create a map like this, to let systemjs know where to load our depencenies (they are excluded in the extenion bundles, like described above):

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

因此,每当systemjs遇到角核或角公共点时,都会被告知从我定义的伪umd捆绑包中加载它.他们看起来像这样:

So whenever systemjs stumples upon angular core or angular common, it is told to load it from the fake umd bundles I've defined. They look like this:

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

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

Eventually, using the runtime compiler, I can now use modules that are loaded externally:

因此,现在可以在Angular中使用系统来导入和编译模块.每个模块只需执行一次.不幸的是,这使您无法省去繁重的运行时编译器.

So system can now be used in Angular to import and compile modules. This only needs to happen once per module. Unfortunately this prevents you from leaving out the runtime compiler which is quite heavy.

我有一项服务,可以加载模块并返回工厂,最终使您能够延迟加载 在内核中不知道的模块.对于商业平台,CMS,CRM系统或其他开发人员无需源代码即可为这类系统创建插件的软件供应商而言,这非常有用.

I have a service That can load modules and return factories, eventually giving you the ability to lazy load modules that are not know on transpile time in the core. This is great for software vendors like commerce platforms, CMS, CRM systems, or other where developers create plugins for those kind of systems without having the source code.

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. 在扩展束中排除角度依赖性
  3. 您核心应用中的
  4. 在全局变量中公开角度依赖性
  5. 通过返回公开的依赖项,为每个依赖项创建伪造的捆绑包
  6. 在您的systemjs映射中,添加要在伪造的js捆绑包中加载的依赖项
  7. Angular中的运行时编译器现在可以用于加载使用webpack-system-register与webpack打包在一起的模块

这篇关于运行时将组件或模块加载到angular2的模块中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆