我看到奇怪的行为,因为通过解构赋值(或Object.assign
)添加到对象的属性在传递给forRoot
时会出现,但在注入服务时不存在。此外,初始化后进行的更新在传递给forRoot
时会出现,但在注入服务时不存在。仅在使用AOT构建时才会发生这种情况。
我创建了一个可以重现问题的最小项目: https://github.com/bygrace1986/wat
创建一个具有静态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
级别提供,而不是通过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
标志来重现。angular-cli
项目中打开了一个错误:https://github.com/angular/angular-cli/issues/10610 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;
中显示。答案 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
中 <案例1const 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
]
请注意,参数只是静态对象值。
<案例2const 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
<案例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 只需要能够生成对值的引用。
我们可以看到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 })