Angular2 (CLI) tree shaking removing dynamically c

2020-03-01 06:29发布

问题:

I assume the question at Angular-cli tree-shaking exclude component from removal is very similar but I can't seem to get anything out of it.

Essentially I have a dynamic component factory as described in How can I use/create dynamic template to compile dynamic Component with Angular 2.0?.

When I build it using the latest Angular CLI with a non-production setting, it all works fine. However, once I use the production setting I get the following error trace in the browser immediately when trying to load a page that has dynamically created content:

EXCEPTION: No NgModule metadata found for 'e'.
ORIGINAL STACKTRACE:
main.dc05ae9….bundle.js:formatted:4731
Error: No NgModule metadata found for 'e'.
at f (vendor.c18e6df….bundle.js:formatted:76051)
at t.resolve (vendor.c18e6df….bundle.js:formatted:20624)
at t.getNgModuleMetadata (vendor.c18e6df….bundle.js:formatted:20169)
at t._loadModules (vendor.c18e6df….bundle.js:formatted:40474)
at t._compileModuleAndAllComponents (vendor.c18e6df….bundle.js:formatted:40462)
at t.compileModuleAndAllComponentsSync (vendor.c18e6df….bundle.js:formatted:40436)
at e.createComponentFactory (main.dc05ae9….bundle.js:formatted:4789)

Here is my component factory class:

@Injectable()
export class DynamicTypeBuilder {    
  constructor() {
  }

  private _cacheOfFactories: {[templateKey: string]: ComponentFactory<any>} = {};
  private compiler: Compiler = new JitCompilerFactory([{useDebug: false, useJit: true}]).createCompiler();

  public createComponentFactory<COMPONENT_TYPE>(type: any, template: string, additionalModules: any[] = []): Observable<ComponentFactory<COMPONENT_TYPE>> {

    let factory = this._cacheOfFactories[template];
    if (factory) {
      return Observable.of(factory);
    }

    // unknown template ... let's create a Type for it
    let module = this.createComponentModule(type, additionalModules);

    // compiles and adds the created factory to the cache
    return Observable.of(this.compiler.compileModuleAndAllComponentsSync(module))
                     .map((moduleWithFactories: ModuleWithComponentFactories<COMPONENT_TYPE>) => {
                       factory = moduleWithFactories.componentFactories.find(value => value.componentType == type);
                       this._cacheOfFactories[template] = factory;                           
                       return factory;
                     });
  }

  protected createComponentModule(componentType: any, additionalModules: any[]): Type<any> {
    @NgModule({
      imports: [
        FormsModule,
        ReactiveFormsModule,
        BrowserModule,
        PipesModule,
        ...additionalModules
      ],
      declarations: [
        componentType
      ],
      schemas:[CUSTOM_ELEMENTS_SCHEMA]
    })
    class RuntimeComponentModule {
    }

    return RuntimeComponentModule;
  }
}

which is being transpiled to

var _ = function() {
    function e() {
        this._cacheOfFactories = {},
        this.compiler = new i.a([{
            useDebug: !1,
            useJit: !0
        }]).createCompiler()
    }
    return e.prototype.createComponentFactory = function(e, t, n) {
        var i = this;
        var _ = this._cacheOfFactories[t];
        if (_)
            r.Observable.of(_);
        var a = this.createComponentModule(e, n);
        return r.Observable.of(this.compiler.compileModuleAndAllComponentsSync(a)).map(function(n) {
            return _ = n.componentFactories.find(function(t) {
                return t.componentType == e
            }),
            i._cacheOfFactories[t] = _,
            _
        })
    }
    ,
    e.prototype.createComponentModule = function(e, t) {
        var n = function() {
            function e() {}
            return e
        }();
        return n
    }
    ,
    e.ctorParameters = function() {
        return []
    }
    ,
    e
}()

The 'e' in the error message is the function e() from createComponentModule which, as you can see, is empty even though is should contain the @NgModule content.

How can I create a new NgModule dynamically and still use the production mode of Angular CLI?

Versions:
Angular2: 2.4.8
Angular CLI: 1.0.0-beta.32.3
TypeScript: 2.1.6

回答1:

Unfortunately it really seems like this is impossible at the moment (I will try to keep the answer up to date), neither with Angular 2.x nor with Angular 4 beta.
The problem being that a dynamic component definition contains file references (template, style sheets) which cannot be resolved any more at run time with the AOT compiler having run before.
But also if the component or module would not contain file references the current Angular code does not allow for a truly dynamic creation of components. It just doesn't find the metadata that is being created at runtime.

Summarizing the problem, there are 3 levels of dynamic component creation:

  1. Define a component statically and include it in an NgModule that the AOT compiler can find at AOT compile time. Such a component can at any point be instantiated without problems. (See ComponentFactoryResolver etc)
  2. Define the body of a component statically (code etc) but allow to have dynamic templates and/or styles (i.e. template is being created in code just when needed). This also requires an NgModule to be compiled at runtime. This is currently only possible when not using the AOT compiler and represents the issue I posted here.
  3. Define the full component dynamically, including code and templates. This is not what is intended here and might even go to far. But possibly someone has this issue as well.

In my opinion the number 2 issue can be solved. The Angular team says since it is AOT it can only compile those things that are statically known at AOT compile time, but I disagree.
I could think of the possibility to AOT compile a 'stub' of such a component which is then being instantiated with a dynamic template or style sheet when needed. There might be the need to use a new property for the @Component annotation or a totally new annotation like @DynamicComponent but it seems feasible to me. I do not know if the same changes would be required for @NgModule declaration, but I assume they would.



回答2:

I have the same error message. The workaround I found is not to use the decorator for the runtime module.

protected createComponentModule(componentType: any, additionalModules: any[]): Type<any> {
  return NgModule({
    imports: [
      FormsModule,
      ReactiveFormsModule,
      BrowserModule,
      PipesModule,
      ...additionalModules
    ],
    declarations: [
      componentType
    ],
    schemas:[CUSTOM_ELEMENTS_SCHEMA]
  })(class RuntimeComponentModule {});
}

Okay, I did not fully understand why the error occurs. The error message basically says that module e does not have metadata. Metadata of a module in Angular is usually declared as decorator.

Decorators in ES7 are equivalent to curry functions. It means

@NgModule({})
class A {}

is equal to

NgModule({})(class A {})

Personally I think the curry way is much better...

Updated 22 Match: The answer from official repo https://github.com/angular/angular-cli/issues/5359#issuecomment-287500703 is simply not using AOT. Please build the code with ng build --prod --no-aot In my case, everything is solved.