使用 AOT 时,传递给 forRoot 的对象的更改在注入时会被丢弃
When using AOT, changes to objects passed to forRoot are discarded when injected
概述
我看到奇怪的行为,因为通过解构赋值(或 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
提供并注入到服务中。
测试模块
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 }
]
}
}
}
测试服务
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
会正确地注入到依赖它的服务中。
要设置引用 app.component.html
中的 TestService
(或它将被注入的地方)。将依赖项传递给 TestModule
.
上的 forRoot
方法
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
上的 属性 并将其传递给 TestModule
.
上的 forRoot
方法
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"}
问题
为什么当对象被注入依赖 class 时,对通过 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;
. 中得到这个
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 so that it can understand a subset of javascript expressions that is listen in docs
在运行时计算所有元数据
需要注意的是,编译器支持扩展运算符,但仅在array literals not in objects
案例一
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
]
因此我们将获得如下元数据:
请注意,参数只是静态对象值。
案例二
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
这是我们现在得到的:
案例三
providers: [
{ provide: TEST_DEPENDENCY, useValue: dependencyCopy }
],
在第 5 版中 angular 移动到(几乎)使用转换器的原生 TS 编译过程并引入了所谓的 Lower Expressions transformer 这基本上意味着我们现在可以在装饰器元数据中使用箭头函数,例如:
providers: [{provide: SERVER, useFactory: () => TypicalServer}]
将由 angular 编译器自动转换为以下内容:
export const ɵ0 = () => new TypicalServer();
...
providers: [{provide: SERVER, useFactory: ɵ0}]
现在让我们阅读documentation:
The compiler treats object literals containing the fields useClass,
useValue, useFactory, and data specially. The compiler converts the
expression initializing one of these fields into an exported variable,
which replaces the expression. This process of rewriting these
expressions removes all the restrictions on what can be in them
because the compiler doesn't need to know the expression's value—it
just needs to be able to generate a reference to the value.
现在让我们看看在第三种情况下它是如何工作的:
我们可以看到 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 })
概述
我看到奇怪的行为,因为通过解构赋值(或 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
提供并注入到服务中。
测试模块
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 }
]
}
}
}
测试服务
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
会正确地注入到依赖它的服务中。
要设置引用 app.component.html
中的 TestService
(或它将被注入的地方)。将依赖项传递给 TestModule
.
forRoot
方法
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
上的 属性 并将其传递给 TestModule
.
forRoot
方法
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"}
问题
为什么当对象被注入依赖 class 时,对通过 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;
. 中得到这个
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 so that it can understand a subset of javascript expressions that is listen in docs
在运行时计算所有元数据需要注意的是,编译器支持扩展运算符,但仅在array literals not in objects
案例一
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
]
因此我们将获得如下元数据:
请注意,参数只是静态对象值。
案例二
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
这是我们现在得到的:
案例三
providers: [
{ provide: TEST_DEPENDENCY, useValue: dependencyCopy }
],
在第 5 版中 angular 移动到(几乎)使用转换器的原生 TS 编译过程并引入了所谓的 Lower Expressions transformer 这基本上意味着我们现在可以在装饰器元数据中使用箭头函数,例如:
providers: [{provide: SERVER, useFactory: () => TypicalServer}]
将由 angular 编译器自动转换为以下内容:
export const ɵ0 = () => new TypicalServer();
...
providers: [{provide: SERVER, useFactory: ɵ0}]
现在让我们阅读documentation:
The compiler treats object literals containing the fields useClass, useValue, useFactory, and data specially. The compiler converts the expression initializing one of these fields into an exported variable, which replaces the expression. This process of rewriting these expressions removes all the restrictions on what can be in them because the compiler doesn't need to know the expression's value—it just needs to be able to generate a reference to the value.
现在让我们看看在第三种情况下它是如何工作的:
我们可以看到 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 })