runtime load components or modules into a module in angular2

前端 未结 1 1899
时光说笑
时光说笑 2021-01-21 03:41

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 tha

相关标签:
1条回答
  • 2021-01-21 04:04

    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.

    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. In both your core app and your plugins, you need to install an extension for webpack called

    "webpack-system-register".

    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
    ];
    

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

    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
    

    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/
          ]
      })
    

    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.:

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

    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',
    

    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:

    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.

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

    To sum it up:

    1. install webpack-system-register in both your core app and your extension modules
    2. exlude angular dependencies in your extension bundles
    3. in your core app expose angular dependencies in a global variable
    4. create a fake bundle per dependency by returning the exposed dependency
    5. in your systemjs map, add dependencies to be loaded in the fake js bundle
    6. runtime compiler in Angular can now be used to load modules that has been packaged with webpack using webpack-system-register
    0 讨论(0)
提交回复
热议问题