Load new modules dynamically at runtime using Angular CLI and Angular 5

I am currently working on a project that is hosted on a client server. For new "modules" there is no intention of recompiling the entire application. However, the client wants to update the router / lazy modules loaded at runtime . I've tried several things but I can't seem to get it to work. I was wondering if any of you know what else I can try or what I missed.

One thing I've noticed, most of the resources I've tried using angular cli are bundled into separate chunks by webpack by default when building the app. This seems logical since it uses webpack code splitting. but what if the module is not yet known at compile time (but the compiled module is stored somewhere on the server)? The assembly does not work because it cannot find the module to import. And using SystemJS will load UMD modules whenever found on the system, but also shipped in a separate block using webpack.

Some of the resources I've already tried;

Some code that I have already tried and implemented, but not working at this time;

Extending the Router with a Regular Module.ts File

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

      

Normal SystemJS Import UMD Package

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

    });
});

      

Import external module without working with 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);
});

      

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

      

Tried loading a module made with a drive

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

      

The SystemJsNgModuleLoader example only works when the module is already provided as a "lazy" route in the RouterModule of the application (which turns it into a chunk when built with webpack)

I found a lot of discussion on this topic on StackOverflow here and there, and assuming the solutions are really good for loading modules / components dynamically if known beforehand. but none of them fit our project use case. Please let me know what else I can try or dive in.

Thank!

EDIT: I found; https://github.com/kirjs/angular-dynamic-module-loading and try this.

UPDATE: I created a repository with an example of loading modules dynamically using SystemJS (and using Angular 6); https://github.com/lmeijdam/angular-umd-dynamic-example

+27


source to share


7 replies


I had the same problem. As I understand it so far:

Webpack puts all resources in a bundle and replaces everything System.import

with __webpack_require__

. So if you want to dynamically load a module at runtime using SystemJsNgModuleLoader, the loader will look for the bundled module. If the module does not exist in the bundle, you will receive an error message. Webpack will not ask the server for this module. This is a problem for us since we want to load a module that we don't know at build / compile time. We need a loader that will load the module for us at runtime (lazy and dynamic). In my example, I am using SystemJS and Angular 6 / CLI.

  • Install SystemJS: npm install systemjs -save
  • Add it to 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;
                }
            });
    });
  }
}

      



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

      

As you can see, we can tell SystemJS which modules already exist in our bundle. So we don't need to load them ( SystemJS.set

) again . All other modules that we import in our my-dynamic-component

(in this example other

) will be requested from the server at runtime.

+10


source


I used the solution https://github.com/kirjs/angular-dynamic-module-loading with Angular 6 library support to build the app that I used on Github. Due to company policy, it had to be turned off. Once the project source examples have been discussed, I'll share them on Github!



UPDATE: the repo can be found; https://github.com/lmeijdam/angular-umd-dynamic-example

+4


source


I believe it is possible using SystemJS to load the UMD package if you build and run the main application using webpack. I used a solution that uses ng-packagr to create a dynamic plugin / addon UMD package. This github demonstrates the described procedure: https://github.com/nmarra/dynamic-module-loading

+2


source


Do it with angular library 6 and do your thing. I just experimented with this and I can share the standalone angular AOT module with the main application without rebuilding the latter.

  1. In angular library set to angularCompilerOptions.skipTemplateCodegen

    false and after building library you will get module factory.
  2. After that, create the umd module using the rollup as follows: rollup dist/plugin/esm2015/lib/plugin.module.ngfactory.js --file src/assets/plugin.module.umd.js --format umd --name plugin

  3. Load the original umd text package into the main application and evaluate it using the module context.
  4. You can now access the ModuleFactory from the export object.

Here https://github.com/iwnow/angular-plugin-example you can find how to develop a plugin with offline build and AOT

+2


source


Yes, you can use lazy load modules by referring to them as modules in the router. Here is an example https://github.com/start-angular/SB-Admin-BS4-Angular-6

  • First add all the components you are using into one module.
  • Now refer to this module in the router and angular will be lazy to load your module into the view.
0


source


I tested in Angular 6, below solution works to load module dynamically from external package or internal module.

1. If you want to dynamically load a module from a library or package project:

I have an "admin" library project (or you can use a package) and an "app" application project. In my "admin" library project, I have an AdminModule and an AdminRoutingModule. In my "app" project:

and. Make changes to tsconfig.app.json:

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

      

b. In 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.if you want to load a module from the same project.

There are 4 different options:

and. In 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 {
}
''


      

0


source


Does not work

System.import('http://lab.azaas.com:52048/my-component-library.umd.js').then(module => {
        console.log(module);
 });

      

at work

System.import('./../../assets/umds/my-component-library.umd.js').then(module => {
        console.log(module);
});

      

0


source







All Articles