使用AOT时,注入时会丢弃对传递给forRoot的对象的更改

byg*_*ace 7 angular-cli angular2-aot angular

概观

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

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

包版本

  • 角度: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 }
      ]
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

非变异对象场景

此方案说明将非突变对象传递到forRoot正确注入依赖于它的服务.

TestServiceapp.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 { }
Run Code Online (Sandbox Code Playgroud)

ng serve --aot.

产量

[AppModule] {property: "value"}
[TestService] {property: "value"}
Run Code Online (Sandbox Code Playgroud)

变异对象场景

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

要设置创建具有其他属性的新对象,并使用对象解构将旧对象的属性分配给新对象.然后更新一个属性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)
Run Code Online (Sandbox Code Playgroud)

ng serve --aot.

产量

[AppModule] {id: 1, name: "last", property: "value"}
[TestService] {id: 1, name: "first"}
Run Code Online (Sandbox Code Playgroud)

意外结果

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

AppModule场景中提供的变异对象

这种情况说明,当在AppModule级别而不是通过级别提供时,对象的突变不会被还原forRoot.

设置不要传递任何东西forRoot.而是使用注入令牌在提供AppModule者列表中提供对象.

的AppModule

imports: [
  BrowserModule,
  TestModule.forRoot()
],
providers: [
  { provide: TEST_DEPENDENCY, useValue: dependencyCopy }
],
Run Code Online (Sandbox Code Playgroud)

ng serve --aot.

产量

[AppModule] {id: 1, name: "last", property: "value"}
[TestService] {id: 1, name: "last", property: "value"}
Run Code Online (Sandbox Code Playgroud)

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

更新

  • 意识到初始化后的更新也会被恢复.
  • 意识到这只能用--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, [])]);然后在app模块中var ?0 = dependencyCopy; exports.?0 = ?0;.

yur*_*zui 10

AOT的核心是元数据收集器.

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

收集器在文件的顶级检查以下类型的AST节点:

ts.SyntaxKind.ExportDeclaration
ts.SyntaxKind.ClassDeclaration
ts.SyntaxKind.TypeAliasDeclaration
ts.SyntaxKind.InterfaceDeclaration
ts.SyntaxKind.FunctionDeclaration
ts.SyntaxKind.EnumDeclaration
ts.SyntaxKind.VariableStatement
Run Code Online (Sandbox Code Playgroud)

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

应该注意的是,编译器支持扩展运算符,但仅支持不在对象中的数组文字

情况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
]
Run Code Online (Sandbox Code Playgroud)

所以我们将获得如下元数据: 在此输入图像描述

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

案例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
Run Code Online (Sandbox Code Playgroud)

这是我们现在得到的: 在此输入图像描述

案例3

providers: [
  { provide: TEST_DEPENDENCY, useValue: dependencyCopy }
],
Run Code Online (Sandbox Code Playgroud)

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

providers: [{provide: SERVER, useFactory: () => TypicalServer}]
Run Code Online (Sandbox Code Playgroud)

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

export const ?0 = () => new TypicalServer();
...
providers: [{provide: SERVER, useFactory: ?0}]
Run Code Online (Sandbox Code Playgroud)

现在让我们阅读文档:

编译器特别处理包含字段useClass, useValue,useFactorydata的对象文字.编译器将 初始化其中一个字段表达式转换为导出的变量,该变量将替换表达式.这个重写这些表达式的过程消除了对它们内容的所有限制,因为编译器不需要知道表达式的值 - 它只需要能够生成对值的引用.

现在让我们看看它在第三种情况下是如何工作的: 在此输入图像描述

我们可以看到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 }
      ]
    };
  }
}
Run Code Online (Sandbox Code Playgroud)

app.module.ts

TestModule.forRoot({ data: dependencyCopy })
Run Code Online (Sandbox Code Playgroud)