Skip to content

Codegen App Injector / remove ReflectiveInjector and Reflector for offline compile #8997

Closed

Description

@tbosch

Angular is already generating Injectors on elements for components. We would like to have the same approach for the injector used for bootstrapping and the injector used for dynamic component loading.

With this, we no longer need any metadata about classes / functions in offline compile mode, as all of the metadata (decorators) have been read during offline compile and converted into appropriate code. This means:

  • Download size impact:
    • We are no longer using ReflectiveInjector nor Reflector / ReflectionCapabilities
      • this drops about 30kb minified / 6.49k minified gzipped from angular core
    • we don't need the reflect-metadata polyfill anymore when during offline compile (2.1kb min gzipped)
    • we don't need to keep the decorators anymore during runtime.
    • we don't need to store the constructor arguments any more
    • we are generting code for the injectors instead, which needs to be measured how much.
  • Bootstrap speed
    • analyzing the dependencies of classes, normalizing them and creating factories for them during bootstrap of an application can be costly depending on the number of services used. This especially affects bootstrap as the JavaScript vm most probably did not have time to optimize the code.
    • this was proven by internal benchmarks for the last bigger change to DI for elements (see #8b000a7f0e9fade5e6a9354532597018c5ad9a55).
  • Better global static analyzability of injection of angular apps. In the future, this would allow to check which providers are actually used in an application and report missing providers that services / directives that are used depend upon.

Impact for users:

  • This change does not affect runtime compile, only offline compilation.
  • This change only affects how users add additional providers to bootstrap / when components are dynamically loaded. Bootrapping / dynamically loading components without additional providers does not change.
  • This change does not change how components use providers

Part of #6270

Input and output of the compilation

Declare injectors via a config class like this:

@InjectorConfig({
  providers: [SomeService]
})
class MyAppConfig {
  constructor(public @Provides(SomeToken) someProp: string) {
  }
}

Via runtime or offline compile, we create a InjectorFactory that is usable like this:

var injectorFactory: InjectorFactory<MyAppConfig>;
var parentInjector: Injector;

var injector: Injector = injectorFactory.create(parentInjector, new MyAppConfig('hello'));
expect(injector.get(SomeService)).toBeAndInstanceOf(SomeService);
expect(injector.get(SomeToken)).toBe('hello');

Usage with offline compilation

// Name: drop suffix `Config` if existing and add `InjectorFactory`
import {MyAppInjectorFactory} from 'my_config.ngfactory';

MyAppInjectorFactory.create(...);

Usage with runtime compilation

var componentResolver: ComponentResolver;

// This would also jit the generated code, similar to our component compiler.
componentResolver.resolveInjector(MyAppConfig).then( 
    (injectorFactory: InjectorFactory<MyAppConfig>) => {
      injectorFactory.create(...);
    }
);

Consequences

  • We can still support the bootstrap(type, providers) call in @angular/browser as this uses the RuntimeCompiler already, so it can also create a injector config class on the fly with the given providers. -> plunkers stay simpler.
  • To create the ComponentResolver, we can't use DI any more, as it provides the factory for injectors.
  • As the compiler creates the injector factories, the compiler can't be configured via DI, but we rather need factory methods that take the CompilerConfig and maybe some other parameters.
    • e.g. platform directives / platform pipes need to become part of the CompilerConfig,
      and not provided via DI (this also unifies their configuration between offline and runtime compile).

Proposed changes to Angular besides the compiler changes

  • [BREAKING] move ReflectiveInjector and move it to @angular/common or into a
    separate module as Angular itself does no more use it.
    • It is still used by e.g. benchpress so we need to keep it.
    • Instead, users have to create injector config classes as shown above for both, runtime and offline compile. For runtime compile, we could create these injector config classes on the fly, but to make it easier for users to switch to offline compile they should already use them.
    • or maybe just deprecate it?
  • move Reflector and ReflectionCapabilities from @angular/core to @angular/compiler as the
    compiler is the only place where these are used.
  • make ComponentResolver (an RuntimeCompiler) part of PlatformRef and don't use DI to create the providers of PlatformRef. Still expose an injector in PlatformRef which is just based on a Map.
  • [BREAKING] make platform pipes / platform directives part of CompilerConfig and define them as arrays of CompileIdentifierMetadata so they can be used for offline and runtime compile.
  • [BREAKING] pass CompilerConfig to browserPlatform() as argument instead of getting it via DI.
    • we might consider using a special BrowserCompileConfig class as some parts of CompilerConfig are implied by using the browser platform (e.g. that platform directives are always defined via their runtime types, i.e. the user only needs to provide an array of types instead of CompileIdentifierMetadata)

Activity

  1. tbosch commented on Jun 3, 2016

    @tbosch
    ContributorAuthor
  2. tbosch commented on Jun 3, 2016

    @tbosch
    ContributorAuthor

    Other changes agreed upon:

    • Rename ComponentResolver to FactoryResolver
    • Change the exported name of generated ComponentFactorys to not include Ng in the name, e.g. MyComponent -> MyComponentFactory, but keep the filename as is (i.e. ...ngfactory.ts).
    • Move ReflectiveInjector into benchpress for now until we decide what to do with it.
  3. mhevery commented on Jun 3, 2016

    @mhevery
    Contributor

    Reviewed with @tbosch and agreeing with the approach.

  4. wardbell commented on Jun 3, 2016

    @wardbell
    Contributor

    Please provide an illustration of how this affects the way I bootstrap my app and how I provide to components (especially AppComponent).

    Imagine this (partial) application (semi-pseudo-coded):

    // main.ts
    bootstrap(AppComponent, [HTTP_PROVIDERS]);
    
    // hero.service.ts
    export class HeroService {
      constructor(private http: Http) { }
      // ... the rest of it
    }
    
    // hero-list.component.ts
    @Component({
      selector: 'hero-list',
      template: '<div *ngFor="let hero of heroes>{{hero.name}}</div>'
    })
    export class HeroListComponent() { 
      constructor(private heroService: HeroService) { }
      // ... the rest of it
    }
    
    // app.component.ts
    @Component({
      selector: 'my-app',
      template: '<hero-list></hero-list>',
      providers: [HeroService]
    })
    export class AppComponent() { }
    
  5. mhevery commented on Jun 3, 2016

    @mhevery
    Contributor

    @wardbell bootstrap forces in browser compilation, so the above would not change. This will only come into play with offline-compilation.

  6. wardbell commented on Jun 4, 2016

    @wardbell
    Contributor

    That's what we all wanted to hear :-)

  7. tbosch commented on Jun 6, 2016

    @tbosch
    ContributorAuthor

    The pattern of a config that combines a set of statically known providers with a set of unknown providers is also usable for defining the configuration of a library.

    E.g.

    @InjectorConfig({
      providers: [locationProvider, ...]
    })
    class RouterConfig {
      @Provides(RouterBasePath)
      basePath: string;
    }
    

    This corresponds to the pattern of creating provider functions like this

    function routerProviders(basePath: string):any[] {
      return [
        locationProvider, ...,
        provide(RouterBasePath, useValue: basePath)
      ];
    }
    

    However, in this case, the config is only part of the total config of an application. The encapsulation into a Config class is used because:

    • the user only needs to know about 1 thing to configure DI for a library, even if the library needs some input tokens that the user should provide
    • the typechecker can check that all input tokens are provided and are of the right type

    These benefits show more the more input tokens a library has

    The following proposes a way to compose configurations:

    @InjectorConfig()
    class AppConfig {
      @Configures(RouterConfig)
      routerConfig(dep: SomeDependency): RouterConfig {
        return new RouterConfig('/...');
      }
    }
    

    I.e. we support method injection to create Config objects and automatically include the static providers of these configs in the main injector as well.

    In components, configs can also be used as follows:

    @Component({
      providers: [{
        configure: RouterConfig,
        useValue: new RouterConfig('/...')
      }]
    })
    class SomeComp {}
    

    Configs can be included in other configs or providers of components / directives via @Configures (to include into configs) or {configures: ...} for components / directives. Syntax:
    @Configures(<ConfigClass>, {useValue: ..., useFactory: ...});

  8. tbosch commented on Jun 6, 2016

    @tbosch
    ContributorAuthor

    Note from Igor: Right now we only have @Inject as a verb for a decorator. However, we can't use @Provider or @Injector as Angular already exports the classes Injector and Provider which would collide.

  9. tbosch commented on Jun 6, 2016

    @tbosch
    ContributorAuthor

    Maybe we can determine the argument to @Configures and @Provides using the result of the method by default, so the user has less to write.

    • TS does store the return type as __metadata('design:returntype', ...)
    • however, we don't rely on reading the return type anywhere else
  10. wardbell commented on Jun 6, 2016

    @wardbell
    Contributor

    This is jibberish to me. I have no idea what problem we're solving or what the consequences are.

    I'm getting feedback from community confirming that this mystifies ... and frightens ... that community.

    This proposal adds new terms and complexities that we were previously unaware of.

    Where do we go to find out whether this is important and what it means?

  11. shlomiassaf commented on Jun 6, 2016

    @shlomiassaf
    Contributor

    The proposal provides better encapsulation and better static analysis for libraries/modules, the tradeoff is adding more depth into libraries making it harder to understand them which has an impact since their usually OS projects.

    The result might be simple library integration for the end user.

    I admit it looks complex and a lot of new terms for DI.

  12. changed the title [-]Codegen App Injector / remove ReflectiveInjector and Reflector[/-] [+]Codegen App Injector / remove ReflectiveInjector and Reflector for offline compile[/+] on Jun 6, 2016
  13. 21 remaining items

  14. tbosch commented on Jul 7, 2016

    @tbosch
    ContributorAuthor

    We have AppModules now, which generate the bootstrap injector. See #9726.

  15. angular-automatic-lock-bot commented on Sep 9, 2019

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  16. locked and limited conversation to collaborators on Sep 9, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions