使用AOT时,注入

时间:2018-05-02 18:39:22

标签: angular angular-cli angular2-aot

概述

我看到奇怪的行为,因为通过解构赋值(或Object.assign)添加到对象的属性在传递给forRoot时会出现,但在注入服务时不存在。此外,初始化后进行的更新在传递给forRoot时会出现,但在注入服务时不存在。仅在使用AOT构建时才会发生这种情况。

我创建了一个可以重现问题的最小项目: https://github.com/bygrace1986/wat

包版本

  • angular:5.2.0
  • angular-cli:1.6.4(我的应用),1.7.4(我的测试)
  • 打字稿:2.4.2

设置

创建一个具有静态forRoot方法的模块,该方法将接受一个对象,然后该对象将通过InjectionToken提供并注入服务。

TestModule

import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';

import { TestDependency, TEST_DEPENDENCY, TestService } from './test.service';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class TestModule {
  static forRoot(dependency?: TestDependency): ModuleWithProviders {
    return {
      ngModule: TestModule,
      providers: [
        TestService,
        { provide: TEST_DEPENDENCY, useValue: dependency }
      ]
    }
  }
}

TestService的

import { Injectable, InjectionToken, Inject } from '@angular/core';

export interface TestDependency {
    property: string;
}

export const TEST_DEPENDENCY = new InjectionToken<TestDependency>('TEST_DEPENDENCY');

@Injectable()
export class TestService {
    constructor(@Inject(TEST_DEPENDENCY) dependency: TestDependency) {
        console.log('[TestService]', dependency);
    }
}

非突变对象场景

此方案说明将未变异的对象传递给forRoot已正确注入依赖它的服务。

要设置引用TestService app.component.html(或注入它的某个位置)。将依赖项传递给forRoot上的TestModule方法。

的AppModule

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { TestModule } from './test/test.module';
import { TestDependency } from './test/test.service';

const dependency = {
  property: 'value'
} as TestDependency;
console.log('[AppModule]', dependency)

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    TestModule.forRoot(dependency)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

运行ng serve --aot

输出

[AppModule] {property: "value"}
[TestService] {property: "value"}

变异对象场景

此方案说明,当提供的对象注入依赖于它的服务时,将忽略在初始化期间通过对象解构分配或在初始化之后进行的更改对对象所做的更改。

要设置创建具有其他属性的新对象,并使用对象解构将旧对象的属性分配给新对象。然后更新dependency上的媒体资源,并将其传递给forRoot上的TestModule方法。

的AppModule

const dependency = {
  property: 'value'
} as TestDependency;
const dependencyCopy = { id: 1, name: 'first', ...dependency };
dependencyCopy.name = 'last';
console.log('[AppModule]', dependencyCopy);
...
TestModule.forRoot(dependencyCopy)

运行ng serve --aot

输出

[AppModule] {id: 1, name: "last", property: "value"}
[TestService] {id: 1, name: "first"}

意外结果

删除了通过对象解构分配添加的任何属性,并且在将对象传递给forRoot并注入TestService之间时,在初始化之后进行的任何更新都将被还原。事实上,它不是同一个对象(我使用===进行了调试和检查)。就好像在使用分配或变异之前创建的原始对象......不知何故。

在AppModule场景中提供的变异对象

此方案说明,如果在AppModule级别提供,而不是通过forRoot,则不会还原对象的突变。

设置不要将任何内容传递给forRoot。而是使用注入令牌在AppModule提供者列表中提供对象。

的AppModule

imports: [
  BrowserModule,
  TestModule.forRoot()
],
providers: [
  { provide: TEST_DEPENDENCY, useValue: dependencyCopy }
],

运行ng serve --aot

输出

[AppModule] {id: 1, name: "last", property: "value"}
[TestService] {id: 1, name: "last", property: "value"}

问题

为什么在将对象注入依赖类时,对通过forRoot提供的对象所做的更改会被还原?

更新

  • 意识到初始化后的更新也会被恢复。
  • 意识到这只能用--aot标志而不是更广泛的--prod标志来重现。
  • 更新到最新的稳定cli版本(1.7.4)并仍有问题。
  • angular-cli项目中打开了一个错误:https://github.com/angular/angular-cli/issues/10610
  • 从阅读生成的代码开始,我认为问题出在第2阶段元数据重写中。仅在forRoot中引用变量时,我会得到:i0.ɵmpd(256, i6.TEST_DEPENDENCY, { id: 1, name: "first" }, [])]);。在AppModule提供商列表中引用后,我会收到:i0.ɵmpd(256, i6.TEST_DEPENDENCY, i1.ɵ0, [])]);,然后在应用模块var ɵ0 = dependencyCopy; exports.ɵ0 = ɵ0;中显示。

1 个答案:

答案 0 :(得分:8)

AOT的核心是 Metadata collector

它需要ts.SourceFile,然后递归遍历此文件的所有AST节点,并将所有节点转换为JSON表示。

收集器checks文件顶层的以下类型的AST节点:

ts.SyntaxKind.ExportDeclaration
ts.SyntaxKind.ClassDeclaration
ts.SyntaxKind.TypeAliasDeclaration
ts.SyntaxKind.InterfaceDeclaration
ts.SyntaxKind.FunctionDeclaration
ts.SyntaxKind.EnumDeclaration
ts.SyntaxKind.VariableStatement

Angular编译器还尝试使用所谓的 Evaluator 在运行时计算所有元数据,以便它可以理解在docs <中监听的javascript表达式的子集/ p>

应该注意的是,编译器支持扩展运算符,但仅在array literals不在objects

中 <案例1
const dependency = {
  property: 'value'               ===========> Non-exported VariableStatement
} as TestDependency;                           with value { property: 'value' }

imports: [
  ...,
  TestModule.forRoot(dependency)  ===========> Call expression with 
                                               ts.SyntaxKind.Identifier argument 
                                               which is calculated above
]

因此我们将获得如下元数据: enter image description here

请注意,参数只是静态对象值。

<案例2
const dependency = {
  property: 'value'               ===========> Non-exported VariableStatement
} as TestDependency;                           with value { property: 'value' }

const dependencyCopy = { 
  id: 1,                         ============> Non-exported VariableStatement
  name: 'first',                               with value { id: 1, name: 'first' }                       
  ...dependency                                (Spread operator is not supported here)
};
dependencyCopy.name = 'last';     ===========> ExpressionStatement is skipped
                                               (see list of supported types above)

...
TestModule.forRoot(dependencyCopy) ===========> Call expression with 
                                                ts.SyntaxKind.Identifier argument 
                                                which is calculated above

这是我们现在得到的: enter image description here

<案例3
providers: [
  { provide: TEST_DEPENDENCY, useValue: dependencyCopy }
],

在版本5中,使用变换器将角度移动到(几乎)原生TS编译过程并引入所谓的 Lower Expressions transformer ,这基本上意味着我们现在可以在装饰器元数据中使用箭头函数,例如:

providers: [{provide: SERVER, useFactory: () => TypicalServer}]

将由角度编译器自动转换为以下内容:

export const ɵ0 = () => new TypicalServer();
...
providers: [{provide: SERVER, useFactory: ɵ0}]

现在让我们阅读documentation

  

编译器处理包含字段 useClass 的对象文字,    useValue useFactory 数据编译器转换   表达式将其中一个字段初始化为导出变量,   它取代了表达式。这个重写这些过程   表达式删除所有限制   因为编译器不需要知道表达式的值 -it   只需要能够生成对值的引用

现在让我们看看它在第三种情况下是如何工作的: enter image description here

我们可以看到angular现在使用对dependencyCopy对象的引用。正如您已经注意到它将在生成的工厂中用作var ɵ0 = dependencyCopy;

可能的解决方案:

<强> test.module.ts

export class TestModule {
  static forRoot(dependency?: { data: TestDependency }): ModuleWithProviders {
    return {
      ngModule: TestModule,
      providers: [
        TestService,
        { provide: TEST_DEPENDENCY, useValue: dependency.data }
      ]
    };
  }
}

<强> app.module.ts

TestModule.forRoot({ data: dependencyCopy })