非常教程

AngularJS参考手册

环境准备与部署

预先(AOT)编译

Angular 的“预先(AOT)编译器”会在构建期间把 Angular 应用的 HTML 和 TypeScript 代码编译成高效的 JavaScript 代码,之后浏览器就可以下载并快速运行这些代码。

本章描述了如何使用 AOT 编译器,以及如何书写能被 AOT 编译的 Angular 元数据。

Watch compiler author Tobias Bosch explain the Angular Compiler at AngularConnect 2016.

观看编译器作者 Tobias Bosch 在 AngularConnect 2016 大会里,对Angular 编译器的演讲。

Angular 中的编译

Angular 应用由大量组件及其 HTML 模板组成。 在浏览器渲染应用之前,组件和模板必须由 Angular 编译器转换成可执行的 JavaScript 代码。

Angular 提供了两种方式来编译你的应用:

  1. 即时(JIT)编译,它会在浏览器中运行时编译你的应用
  2. 预先(AOT)编译,它会在构建时编译你的应用。

当你运行 buildserve 这两个 CLI 命令时 JIT 编译是默认选项:

content_copyng build
ng serve

要进行 AOT 编译只要给这两个 CLI 命令添加 --aot 标志就行了:

content_copyng build --aot
ng serve --aot

--prod 标志也会默认使用 AOT 编译。

要了解更多,请参见CLI 文档,特别是build 这个主题。

为什么需要 AOT 编译?

渲染得更快

使用 AOT,浏览器下载预编译版本的应用程序。 浏览器直接加载运行代码,所以它可以立即渲染该应用,而不用等应用完成首次编译。

需要的异步请求更少

编译器把外部 HTML 模板和 CSS 样式表内联到了该应用的 JavaScript 中。 消除了用来下载那些源文件的 Ajax 请求。

需要下载的 Angular 框架体积更小

如果应用已经编译过了,自然不需要再下载 Angular 编译器了。 该编译器差不多占了 Angular 自身体积的一半儿,所以,省略它可以显著减小应用的体积。

提早检测模板错误

AOT 编译器在构建过程中检测和报告模板绑定错误,避免用户遇到这些错误。

更安全

AOT 编译远在 HTML 模版和组件被服务到客户端之前,将它们编译到 JavaScript 文件。 没有模版可以阅读,没有高风险客户端 HTML 或 JavaScript 可利用,所以注入攻击的机会较少。

Angular 编译器选项

你可以通过在 tsconfig.json 文件中随 TypeScript 编译选项一起提供模板编译选项来控制应用的编译方式。 这些模板编译选项都是作为 "angularCompilerOptions" 对象的成员指定的,代码如下:

content_copy{
  "compilerOptions": {
    "experimentalDecorators": true,
    ...
  },
  "angularCompilerOptions": {
    "fullTemplateTypeCheck": true,
    "preserveWhitespaces": true,
    ...
  }
}

enableResourceInlining

这个选项告诉编译器把所有 @Component 装饰器中的 templateUrlstyleUrls 属性内联成 templatestyles 属性。 当启用时,ngc 的 .js 输出中将不会有惰性加载的 templateUrlstyleUrls

skipMetadataEmit

这个选项告诉编译器不要生成 .metadata.json 文件,它默认是 false

.metadata.json 文件中包含模板编译器所需的信息,这些信息来自于 .ts文件中,但是没有包含在由 TypeScript 编译器生成的 .d.ts 文件中。 比如,这个信息包括 TypeScript 发出的注解内容(如组件的模板),TypeScript 把它生成到了 .js 文件中,但是没有生成到 .d.ts 文件中。

如果使用了 TypeScript 的 --outFile 选项,那就要同时设置这个选项。因为在 TypeScript 的这种输出方式下,metadata 文件是无效的。 Angular 中不建议使用 --outFile,请改用 webpack 之类的打包器代替。

当使用工厂汇总器(factory summary)时,这个选项也要设置为 true,因为工厂汇总器在自己的 .metadata.json 中也包含了这些信息的一个副本。

strictMetadataEmit

这个选项告诉模板编译器如果 "skipMetadataEmit"false,那就把错误信息汇报到 .metadata.json 中。 只有当 "skipMetadataEmit"false"skipTemplateCodegen"true 时才应该使用这个选项。

它的设计意图是要验证为打包 npm 而生成的 .metadata.json 文件。 这种验证过于严格,在使用模板编译器时甚至可能会对那些铁定不会出错的元数据文件报告一些错误。 你可以用 @dynamic 在注释中指定一些要导出的符号,来禁止对它们报告错误。

对于 .metadata.json 文件来说,包含错误是正常的。如果这些元数据被用来确定注解的内容,模板编译器就会报告这些错误。 元数据收集器无法判断这些符号的设计目的是用在注解中,所以它将会自作主张,在元数据中为这些导出的符号添加错误节点。 如果这些符号被用到了,模板编译器就会根据这些错误节点报告错误。 如果某个库的使用者只是在注解中(而不是普通代码中)使用这些符号,模板编译器通常不会报错。 这个选项允许在该库(比如 Angular 自身这些库)的构建和使用过程中检测这类错误。

skipTemplateCodegen

这个选项告诉编译器忽略从 .ngfactory.js.ngstyle.js 文件中发出的错误。 如果为 true,它就会关闭大多数的模板编译器,并禁止汇报模板诊断信息。 这个选项用于指示模板编译器为通过 npm 包分发而生成 .metadata.json 文件,同时避免生成无法分发到 npm.ngfactory.js.ngstyle.js 文件。

strictInjectionParameters

当设置为 true 时,该选项会告诉编译器为那些无法确定其类型的注入参数报告错误。 当该值没有提供或未 false 时,那些带有 @Injectable 的类,如果其构造参数的类型无法解析,就会生成一个警告。

注意:建议把该选项显式改为 true,因为将来这个选项的默认值会是 true

flatModuleOutFile

当为 true 时,该选项告诉模板编译器生成一个指定名字的扁平模块索引和相应的扁平模块元数据。 当要创建像 @angular/core@angular/common 这样的扁平模块包时,请使用本选项。 当使用本选项时,库的 package.json 文件就会引用生成的扁平模块索引,而不是库的索引文件。 当使用本选项时,只会生成一个 .metadata.json 文件,其中包含从库索引中导出的符号所需的全部元数据。 在生成的 .ngfactory.js文件中,扁平模块索会用来导入包括库的公共 API 和隐藏的内部符号在内的全部符号。

默认情况下,files 字段中提供的 .ts 文件会被当做库索引。 如果指定了多个 .ts 文件,就要用 libraryIndex 来选择要作为库索引的文件。 扁平模块索引会用 flatModuleOutFile 中给出的名字创建 .d.ts.js 文件,并放在和库索引的 .d.ts 文件相同的位置。 比如,如果某个库使用 public_api.ts 文件作为该模块的库索引,那么 tsconfig.jsonfiles字段就应该是 ["public_api.ts"]。 然后可以把 flatModuleOutFile 选项设置为 "index.js",它就会生成 index.d.tsindex.metadata.json 文件。 该库的 package.json 文件的 module 字段将会是 "index.js",而 typings 字段会是 "index.d.ts"

flatModuleId

该选项指定建议的模块 ID,这个 ID 用于导入扁平模块。 从扁平模块中导入符号时,由模板编译器生成的引用将使用这个模块名称。 它仅在同时提供了 flatModuleOutFile 选项时才有意义,否则,编译器将忽略此选项。

generateCodeForLibraries

这个选项告诉模板编译器也为与 .metadata.json 文件对应的 .d.ts 文件生成工厂文件(.ngfactory.js.ngstyle.js)。 这个选项默认为 true。当该选项为 false 时,只会为 .ts 文件生成工厂文件。

当使用工厂汇总器时,这个选项应该设置为 false

fullTemplateTypeCheck

该选项告诉编译器要为模板编译器启用绑定表达式验证阶段,它会使用 TypeScript 来验证绑定表达式。

该选项默认是 false

注意:建议把它设置为 true,因为将来它会默认为 true

annotateForClosureCompiler

该选项告诉编译器使用 Tsickle 来为生成的 JavaScript 添加供 Closure Compiler 使用的 JsDoc 注解。 该选项默认为 false

annotationsAs

使用这个选项来修改生成 Angular 特有注解的方式,以提升摇树优化(tree-shaking)的效果。它对 Angular 自身之外的注解和装饰器无效。 默认值是 static fields

说明

decorators

原地保留装饰器。这会让编译过程更快。TypeScript 将会生成对 __decorate 助手函数的调用。使用 --emitDecoratorMetadata 进行运行时反射。不过,生成的代码将无法正常进行摇树优化。

staticfields

使用类的静态字段代替装饰器。它允许像 Closure Compiler 这样的高级摇树优化器移除未使用的类。

trace

它告诉编译器在编译模板时打印额外的信息。

disableExpressionLowering

Angular 的模板编译器会转换注解中使用或可能使用的代码,以便能从模板的工厂模块中导入它。 参见元数据重写以了解更多信息。

把该选项设置为 false 将会禁止这种重写,如果需要重写就去得人工完成了。

preserveWhitespaces

该选项会告诉编译器是否要从编译后的模板中移除空白的文本节点。 对于 Angular v6,该选项默认为 false,它会移除空白节点,以生成更小的模板工厂模块。

allowEmptyCodegenFiles

告诉编译器生成所有可能生成的文件 —— 即使是空文件。 该选项默认为 false。 这是供 bazel 构建规则使用的选项,它用于简化 bazel 规则跟踪文件依赖的方式。 除了 bazel 规则之外不建议使用该选项。

enableIvy

告诉编译器使用 Render3 风格的代码生成器来来生成各种定义。 该选项默认为 false

当开启该选项时,有些特性不受支持。它仅仅用来为试验和测试 Render3 风格的代码生成提供支持。

注意:不建议使用该选项,因为它在使用 Render2 的代码生成器时还缺少一些特性。

Angular 元数据与 AOT

Angular 的 AOT 编译器会提取并解释应用中由 Angular 管理的各个部件的元数据

Angular 的元数据会告诉 Angular 如何创建应用中类的实例以及如何在运行期间与它们交互。

你通过装饰器来指定元数据,比如 @Component()@Input()。 你还可以在这些带装饰器的类的构造函数中隐式指定元数据。

在下列范例中,@Component() 元数据对象和类的构造函数会告诉 Angular 如何创建和显示 TypicalComponent 的实例。

content_copy@Component({
  selector: 'app-typical',
  template: '<div>A typical component for {{data.name}}</div>'
)}
export class TypicalComponent {
  @Input() data: TypicalData;
  constructor(private someService: SomeService) { ... }
}

Angular 编译器只提取一次元数据,并且为 TypicalComponent 生成一个工厂。 当它需要创建 TypicalComponent 的实例时,Angular 调用这个工厂,工厂会生成一个新的可视元素,并且把它(及其依赖)绑定到组件类的一个新实例上。

元数据的限制

你只能使用 TypeScript 的一个子集书写元数据,它必须满足下列限制:

  1. 表达式语法只支持 JavaScript 的一个有限的子集。
  2. 只能引用代码收缩后导出的符号。
  3. 只能调用编译器支持的那些函数。
  4. 被装饰和用于数据绑定的类成员必须是公共(public)的。

下一节将会详细解释这些问题。

AOT 工作原理

可以把 AOT 编译器看做两个阶段:在代码分析阶段,它只记录源代码,而在代码生成阶段,编译器的 StaticReflector 会解释这些结果,并为这些结果加上限制。

阶段 1:分析

TypeScript 编译器会做一些初步的分析工作,它会生成类型定义文件.d.ts,其中带有类型信息,Angular 编译器需要借助它们来生成代码。

同时,AOT 收集器(collector) 会记录 Angular 装饰器中的元数据,并把它们输出到.metadata.json文件中,和每个 .d.ts 文件相对应。

你可以把 .metadata.json 文件看做一个包括全部装饰器的元数据的全景图,就像抽象语法树 (AST) 一样。

Angular 的 schema.ts 把这个 JSON 格式表示成了一组 TypeScript 接口。

表达式语法

这个收集器只能理解 JavaScript 的一个子集。 请使用下列受限语法定义元数据对象:

语法

范例

对象字面量

{cherry: true, apple: true, mincemeat: false}

数组字面量

['cherries', 'flour', 'sugar']

字面量数组展开

['apples', 'flour', ...the_rest]

调用

bake(ingredients)

创建对象

new Oven()

属性访问

pie.slice

数组索引

ingredients[0]

标识符引用

Component

模板字符串

` pie is ${multiplier} times better than cake `

字符串字面量

'pi'

数字字面量

3.14153265

逻辑字面量

true

空字面量

null

受支持的前缀操作符

!cake

受支持的二元操作符

a + b

条件操作符

a ? b : c

括号

(a + b)

如果表达式使用了不支持的语法,收集器就会往 .metadata.json 文件中写入一个错误节点。稍后,如果编译器用到元数据中的这部分内容来生成应用代码,它就会报告这个错误。

如果你希望 ngc 立即汇报这些语法错误,而不要生成带有错误信息的 .metadata.json 文件,可以到 tsconfig 中设置 strictMetadataEmit 选项。

content_copy"angularCompilerOptions": {
  ...
  "strictMetadataEmit" : true
}

Angular 库通过这个选项来确保所有的 .metadata.json 文件都是干净的。当你要构建自己的代码库时,这也同样是一项最佳实践。

不要有箭头函数

AOT 编译器不支持 函数表达式 和 箭头函数(也叫 Lambda 函数)。

考虑如下组件装饰器:

content_copy@Component({
  ...
  providers: [{provide: server, useFactory: () => new Server()}]
})

AOT 的收集器不支持在元数据表达式中出现箭头函数 () => new Server()。 它会在该函数中就地生成一个错误节点。

稍后,当编译器解释该节点时,它就会报告一个错误,让你把这个箭头函数转换成一个导出的函数

你可以把它改写成这样来修复这个错误:

content_copyexport function serverFactory() {
  return new Server();
}

@Component({
  ...
  providers: [{provide: server, useFactory: serverFactory}]
})

从 Angular v5 开始,编译器会在生成 .js 文件时自动执行这种改写。

受限函数调用

只要语法有效,收集器就可以支持函数调用或使用 new 来创建对象。收集器只在乎语法是否正确。

但要注意。编译器稍后可能拒绝调用特定的函数或拒绝创建特定的对象。 编译器值仅支持调用一小部分函数,也只能 new 一小部分类。这些函数和类列在了后面的表格中。

折叠(fold)

编译器只能解析对导出的符号的引用。 幸运的是,收集器支持通过折叠来有限的使用那些未导出的符号。

收集器可以在收集期间执行表达式,并用其结果代替原始表达式,记录到 .metadata.json 中。

比如,收集器可以执行表达式 1 + 2 + 3 + 4,并使用它的结果 10 替换它。

这个过程被称为折叠。能用这种方式进行简化的表达式就是可折叠的

收集器可以计算对模块局部变量的 const 声明和初始化过的 varlet 声明,并从 .metadata.json 文件中移除它们。

考虑下列组件定义:

content_copyconst template = '<div>{{hero.name}}</div>';

@Component({
  selector: 'app-hero',
  template: template
})
export class HeroComponent {
  @Input() hero: Hero;
}

编译器不能引用 template 常量,因为它是未导出的。

但是折叠器可以通过内联 template 常量的方式把它折叠进元数据定义中。 最终的结果和你以前的写法是一样的:

content_copy@Component({
  selector: 'app-hero',
  template: '<div>{{hero.name}}</div>'
})
export class HeroComponent {
  @Input() hero: Hero;
}

这里没有对 template 的引用,因此,当编译器稍后对位于 .metadata.json 中的收集器输出进行解释时,不会再出问题。

你还可以通过把 template 常量包含在其它表达式中来让这个例子深入一点:

content_copyconst template = '<div>{{hero.name}}</div>';

@Component({
  selector: 'app-hero',
  template: template + '<div>{{hero.title}}</div>'
})
export class HeroComponent {
  @Input() hero: Hero;
}

收集器把该表达式缩减成其等价的已折叠字符串:

'<div>{{hero.name}}</div><div>{{hero.title}}</div>'.

可折叠的语法

下表中描述了哪些表达式是否能被收集器折叠:

语法

可折叠的

对象字面量

数组字面量

字面量数组展开

调用

创建对象

属性访问

是(如果目标也是可折叠的)

数组索引

是(如果目标和索引也是可折叠的)

标识符引用

是(如果引用的是局部变量)

没有内嵌表达式的模板

带内嵌表达式的模板

是(如果内嵌表达式是可折叠的)

字符串字面量

数字字面量

逻辑字面量

空字面量

受支持的前缀操作符

是(如果运算数是可折叠的)

受支持的二元操作符

是(如果左右运算数都是可折叠的)

条件操作符

是(如果条件是可折叠的)

括号

是(如果表达式是可折叠的)

如果表达式是不可折叠的,那么收集器就会把它作为一个 AST(抽象语法树) 写入 .metadata.json 中,留给编译器去解析。

阶段 2:代码生成

收集器不会试图理解它收集并输出到 .metadata.json 中的元数据,它所能做的只是尽可能准确的表述这些元数据,并在检测到元数据中的语法违规时记录这些错误。

解释这些 .metadata.json 是编译器在代码生成阶段要承担的工作。

编译器理解收集器支持的所有语法形式,但是它也可能拒绝那些虽然语法正确语义违反了编译器规则的元数据。

编译器只能引用已导出的符号

带有装饰器的类成员必须是公开的。你不可能制作一个私有或内部使用的 @Input() 属性。

数据绑定的属性同样必须是公开的。

content_copy// BAD CODE - title is private
@Component({
  selector: 'app-root',
  template: '<h1>{{title}}</h1>'
})
export class AppComponent {
  private title = 'My App'; // Bad
}

最重要的是,编译器生成代码时,只支持为下面列出的某些类创建实例、只支持某些装饰器、只会调用某些函数。

新建实例

编译器只允许创建来自 @angular/coreInjectionToken 类创建实例。

注解 / 装饰器

编译器只支持下列 Angular 装饰器的元数据。

装饰器

所在模块

Attribute

@angular/core

Component

@angular/core

ContentChild

@angular/core

ContentChildren

@angular/core

Directive

@angular/core

Host

@angular/core

HostBinding

@angular/core

HostListener

@angular/core

Inject

@angular/core

Injectable

@angular/core

Input

@angular/core

NgModule

@angular/core

Optional

@angular/core

Output

@angular/core

Pipe

@angular/core

Self

@angular/core

SkipSelf

@angular/core

ViewChild

@angular/core

宏函数或静态宏函数

编译器也支持在返回表达式的函数或静态函数中使用

考虑下面的函数:

content_copyexport function wrapInArray<T>(value: T): T[] {
  return [value];
}

你可以在元数据定义中调用 wrapInArray,因为它所返回的表达式的值满足编译器支持的 JavaScript 受限子集。

你还可以这样使用 wrapInArray()

content_copy@NgModule({
  declarations: wrapInArray(TypicalComponent)
})
export class TypicalModule {}

编译器会把这种用法处理成你以前的写法:

content_copy@NgModule({
  declarations: [TypicalComponent]
})
export class TypicalModule {}

收集器决定哪些函数是宏函数是很简单的 —— 它只能包含一个 return 语句。

Angular 的 RouterModule 导出了两个静态宏函数 forRootforChild,以帮助声明根路由和子路由。 查看这些方法的源码,以了解宏函数是如何简化复杂的 NgModule 配置的。

元数据重写

编译器会对含有 useClassuseValueuseFactorydata 的对象字面量进行特殊处理。 编译器会把用这些字段之一初始化的表达式转换成一个导出为一个变量,并用它替换该表达式。 这个重写表达式的过程,会消除它们受到的所有限制,因为编译器并不需要知道该表达式的值,它只要能生成对该值的引用就行了。

你可以这样写:

content_copyclass TypicalServer {

}

@NgModule({
  providers: [{provide: SERVER, useFactory: () => TypicalServer}]
})
export class TypicalModule {}

如果不重写,这就是无效的,因为这里不支持 Lambda 表达式,而且 TypicalServer 也没有被导出。

为了允许这种写法,编译器自动把它重写成了这样:

content_copyclass TypicalServer {

}

export const ɵ0 = () => new TypicalServer();

@NgModule({
  providers: [{provide: SERVER, useFactory: ɵ0}]
})
export class TypicalModule {}

这就让编译器能在工厂中生成一个对 ɵ0 的引用,而不用知道 ɵ0 中包含的值到底是什么。

编译器会在生成 .js 文件期间进行这种重写。它不会重写 .d.ts 文件,所以 TypeScript 也不会把这个变量当做一项导出,因此也就不会污染 ES 模块中导出的 API。

元数据错误

你可能遇到一些元数据错误,下面是对它们的解释和纠正建议。

Expression form not supported【不支持此表达式格式】 Reference to a local (non-exported) symbol【引用了局部(未导出的)符号】 Only initialized variables and constants【只允许初始化过的变量和常量】 Reference to a non-exported class【引用了未导出的类】 Reference to a non-exported function【引用了未导出的函数】 Function calls are not supported【不支持函数调用】 Destructured variable or constant not supported【不支持解构变量或常量】 Could not resolve type【不能解析此类型】 Name expected【期待是名字】 Unsupported enum member name【不支持的枚举成员名】 Tagged template expressions are not supported【不支持带标签函数的模板表达式】 Symbol reference expected【期待是符号引用】


不支持这种表达式格式

编译器在对 Angular 元数据求值时遇到了一个它不能理解的表达式。

除编译器允许的表达式语法之外的语言特性可能导致这个错误,比如下面的例子:

content_copy// ERROR
export class Fooish { ... }
...
const prop = typeof Fooish; // typeof is not valid in metadata
  ...
  // bracket notation is not valid in metadata
  { provide: 'token', useValue: { [prop]: 'value' } };
  ...

你可以在普通的应用代码中使用 typeof 和方括号标记法来指定属性名,但是这些特性不能在定义 Angular 元数据的表达式中使用。

在写 Angular 的元数据时,严格遵循编译器的受限表达式语法可以避免这个错误,此外还要小心那些新的或罕见的 TypeScript 特性。


引用了局部(未导出的)符号

Reference to a local (non-exported) symbol 'symbol name'. Consider exporting the symbol.

编译器遇到了局部定义的未导出或未初始化的符号。

下面就是存在该问题的 provider 范例。

content_copy// ERROR
let foo: number; // neither exported nor initialized

@Component({
  selector: 'my-component',
  template: ... ,
  providers: [
    { provide: Foo, useValue: foo }
  ]
})
export class MyComponent {}

编译器会在单独的模块中生成这个 userValue 提供商的代码。那个工厂模块不能访问这个源码模块,无法访问这个(未导出的)foo 变量。

你可以通过初始化 foo 来修正这个错误。

content_copylet foo = 42; // initialized

编译器将会把这个表达式折叠进 providers 中,就像你以前的写法一样。

content_copyproviders: [
  { provide: Foo, useValue: 42 }
]

另外,你也可以通过导出 foo 来解决它,这样 foo 将会在运行期间你真正知道它的值的时候被赋值。

content_copy// CORRECTED
export let foo: number; // exported

@Component({
  selector: 'my-component',
  template: ... ,
  providers: [
    { provide: Foo, useValue: foo }
  ]
})
export class MyComponent {}

添加 export 的方式通常用于需要在元数据中引用变量时,如 providersanimations,这样编译器就可以在这些表达式中生成对已导出变量的引用了。它不需要知道这些变量的

当编译器需要知道真正的值已生成代码时,添加 export 的方式就是无效的。比如这里的 template 属性。

content_copy// ERROR
export let someTemplate: string; // exported but not initialized

@Component({
  selector: 'my-component',
  template: someTemplate
})
export class MyComponent {}

编译器现在就需要 template 属性的值来生成组件工厂。 仅仅有对该变量的引用是不够的。 给这个声明加上 export 前缀只会生成一个新的错误 "Only initialized variables and constants can be referenced【只能引用初始化过的变量和常量】"。


只允许使用初始化过的变量和常量

Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler.

编译器发现某个到已导出的变量或静态字段的引用是没有初始化过的。而它需要根据那个变量的值来生成代码。

下面的例子试图把组件的 template 属性设置为已导出的 someTemplate变量的值,而这个值虽然声明过,却没有初始化过。

content_copy// ERROR
export let someTemplate: string;

@Component({
  selector: 'my-component',
  template: someTemplate
})
export class MyComponent {}

如果你从其它模块中导入了 someTemplate,但那个模块中忘了初始化它,就会看到这个错误。

content_copy// ERROR - not initialized there either
import { someTemplate } from './config';

@Component({
  selector: 'my-component',
  template: someTemplate
})
export class MyComponent {}

编译器不能等到运行时才得到该模板的信息。 它必须从源码中静态获得这个 someTemplate 变量的值,以便生成组件工厂,组件工厂中需要包含根据这个模板来生成元素的代码。

要纠正这个错误,请在同一行的初始化子句中初始化这个变量的值。

content_copy// CORRECTED
export let someTemplate = '<h1>Greetings from Angular</h1>';

@Component({
  selector: 'my-component',
  template: someTemplate
})
export class MyComponent {}

引用了未导出的类

Reference to a non-exported class . Consider exporting the class.

元数据引用了一个未导出的类。

比如,你可能定义了一个类并在某个 providers 数组中把它用作了依赖注入令牌,但是忘了导出这个类。

content_copy// ERROR
abstract class MyStrategy { }

  ...
  providers: [
    { provide: MyStrategy, useValue: ... }
  ]
  ...

Angular 会在一个单独的模块中生成类工厂,而那个工厂只能访问已导出的类。 要纠正这个问题,就要导出所引用的类。

content_copy// CORRECTED
export abstract class MyStrategy { }

  ...
  providers: [
    { provide: MyStrategy, useValue: ... }
  ]
  ...

引用了未导出的函数

元数据中引用了未导出的函数。

比如,你可能已经把某个服务提供商的 useFactory 属性设置成了一个局部定义但忘了导出的函数。

content_copy// ERROR
function myStrategy() { ... }

  ...
  providers: [
    { provide: MyStrategy, useFactory: myStrategy }
  ]
  ...

Angular 会在一个单独的模块中生成类工厂,那个工厂只能访问已导出的函数。 要纠正这个错误,请导出该函数。

content_copy// CORRECTED
export function myStrategy() { ... }

  ...
  providers: [
    { provide: MyStrategy, useFactory: myStrategy }
  ]
  ...

不支持函数调用

Function calls are not supported. Consider replacing the function or lambda with a reference to an exported function.

编译器目前不支持函数表达式或 Lambda 表达式。 比如,你不能把某个服务提供商的 useFactory 设置成如下匿名函数或函数表达式。

content_copy// ERROR
  ...
  providers: [
    { provide: MyStrategy, useFactory: function() { ... } },
    { provide: OtherStrategy, useFactory: () => { ... } }
  ]
  ...

如果你在某个提供商的 useValue 中调用函数或方法,也会导致这个错误。

content_copy// ERROR
import { calculateValue } from './utilities';

  ...
  providers: [
    { provide: SomeValue, useValue: calculateValue() }
  ]
  ...

要改正这个问题,就要从模块中导出这个函数,并改成在服务提供商的 useFactory 中引用该函数。

content_copy// CORRECTED
import { calculateValue } from './utilities';

export function myStrategy() { ... }
export function otherStrategy() { ... }
export function someValueFactory() {
  return calculateValue();
}
  ...
  providers: [
    { provide: MyStrategy, useFactory: myStrategy },
    { provide: OtherStrategy, useFactory: otherStrategy },
    { provide: SomeValue, useFactory: someValueFactory }
  ]
  ...

不支持解构变量或常量

Referencing an exported destructured variable or constant is not supported by the template compiler. Consider simplifying this to avoid destructuring.

编译器不支持引用通过解构赋值的方式得到的变量。

比如,你不能这么写:

content_copy// ERROR
import { configuration } from './configuration';

// destructured assignment to foo and bar
const {foo, bar} = configuration;
  ...
  providers: [
    {provide: Foo, useValue: foo},
    {provide: Bar, useValue: bar},
  ]
  ...

要纠正这个错误,就要引用非解构方式的变量。

content_copy// CORRECTED
import { configuration } from './configuration';
  ...
  providers: [
    {provide: Foo, useValue: configuration.foo},
    {provide: Bar, useValue: configuration.bar},
  ]
  ...

不能解析类型

编译器遇到了某个类型,但是不知道它是由哪个模块导出的。

这通常会发生在你引用环境类型时。 比如,Window 类型就是在全局 .d.ts 文件中声明的环境类型。

如果你在组件的构造函数中引用它就会导致一个错误,因为编译器必须对构造函数进行静态分析。

content_copy// ERROR
@Component({ })
export class MyComponent {
  constructor (private win: Window) { ... }
}

TypeScript 能理解这些环境类型,所以你不用导入它们。 但 Angular 编译器不理解你没有导入或导出过的类型。

这种情况下,编译器就无法理解如何使用这个 Window 令牌来进行注入。

不要在元数据表达式中引用环境类型。

如果你必须注入某个环境类型的实例,可以用以下四步来巧妙解决这个问题:

  1. 为环境类型的实例创建一个注入令牌。
  2. 创建一个返回该实例的工厂函数。
  3. 使用该工厂函数添加一个 useFactory 提供商。
  4. 使用 @Inject 来注入这个实例。

下面的例子说明了这一点。

content_copy// CORRECTED
import { Inject } from '@angular/core';

export const WINDOW = new InjectionToken('Window');
export function _window() { return window; }

@Component({
  ...
  providers: [
    { provide: WINDOW, useFactory: _window }
  ]
})
export class MyComponent {
  constructor (@Inject(WINDOW) private win: Window) { ... }
}

对于编译器来说,构造函数中出现 Window 类型已不再是个问题,因为它现在使用 @Inject(WINDOW) 来生成注入代码。

Angular 也用 DOCUMENT 令牌做了类似的事情,所以你也可以注入浏览器的 document 对象(或它的一个抽象层,取决于该应用运行在哪个平台)。

content_copyimport { Inject }   from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';

@Component({ ... })
export class MyComponent {
  constructor (@Inject(DOCUMENT) private doc: Document) { ... }
}

期待是名字

编译器期待它正在求值的表达式中是一个名字。

content_copy// ERROR
provider: [{ provide: Foo, useValue: { 0: 'test' } }]

把该属性的名字改为非数字类型。

content_copy// CORRECTED
provider: [{ provide: Foo, useValue: { '0': 'test' } }]

不支持的枚举成员名

Angular 不能确定你在元数据中引用的枚举成员的值。

编译器可以理解简单的枚举值,但不能理解复杂的,比如从那些计算属性中派生出来的。

content_copy// ERROR
enum Colors {
  Red = 1,
  White,
  Blue = "Blue".length // computed
}

  ...
  providers: [
    { provide: BaseColor,   useValue: Colors.White } // ok
    { provide: DangerColor, useValue: Colors.Red }   // ok
    { provide: StrongColor, useValue: Colors.Blue }  // bad
  ]
  ...

避免引用那些使用了复杂初始化对象或计算属性的枚举。


不支持带标签函数的模板表达式

Tagged template expressions are not supported in metadata.

当编译器遇到这样的带标签函数的模板表达式 时:

content_copy// ERROR
const expression = 'funky';
const raw = String.raw`A tagged template ${expression} string`;
 ...
 template: '<div>' + raw + '</div>'
 ...

String.raw() 是一个 ES2015 原生的标签函数

AOT 编译器不支持带标签函数的模板表达式,避免在元数据表达式中使用它们。


期待是符号引用

编译器期待在错误信息指出的位置是一个符号引用。

当你在类的 extends 子句中使用表达式时就会出现这个错误。

阶段 3:验证绑定表达式

在验证阶段,Angular 的模板编译器会使用 TypeScript 编译器来验证模板中的绑定表达式。 通过在项目的 tsconfig.json(参见 Angular Compiler Options)的 "angularCompilerOptions" 中添加编译选项 "fullTemplateTypeCheck" 可以启用这个阶段。

当模板绑定表达式中检测到类型错误时,进行模板验证时就会生成错误。这和 TypeScript 编译器在处理 .ts 文件中的代码时报告错误很相似。

比如,考虑下列组件:

content_copy@Component({
  selector: 'my-component',
  template: '{{person.addresss.street}}'
})
class MyComponent {
  person?: Person;
}

这会生成如下错误:

content_copymy.component.ts.MyComponent.html(1,1): : Property 'addresss' does not exist on type 'Person'. Did you mean 'address'?

错误信息中汇报的文件名 my.component.ts.MyComponent.html 是一个由模板编译器生成出的虚拟文件, 用于保存 MyComponent 类的模板内容。 编译器永远不会把这个文件写入磁盘。这个例子中,这里的行号和列号都是相对于 MyComponent@Component 注解中的模板字符串的。 如果组件使用 templateUrl 来代替 template,这些错误就会在 templateUrl 引用的 HTML 文件中汇报,而不是这个虚拟文件中。

错误的位置是从包含出错的插值表达式的那个文本节点开始的。 如果错误是一个属性绑定,比如 [value]="person.address.street" ,错误的位置就是那个包含错误的属性的位置。

这个验证过程使用 TypeScript 的类型检查器,这些选项也会提供给 TypeScript 编译器以控制类型验证的详细程度。 比如,如果指定了 strictTypeChecks,就会像上面的错误信息一样报告 my.component.ts.MyComponent.html(1,1): : Object is possibly 'undefined' 错误。

类型窄化

ngIf 指令中使用的表达式用来在 Angular 模板编译器中窄化联合类型,就像 TypeScript 中的 if 表达式一样。 比如,要在上述模板中消除 Object is possibly 'undefined' 错误,可以把它改成只在 person 的值初始化过的时候才生成这个插值表达式。

content_copy@Component({
  selector: 'my-component',
  template: '<span *ngIf="person"> {{person.addresss.street}} </span>'
})
class MyComponent {
  person?: Person;
}

使用 *ngIf 能让 TypeScript 编译器推断出这个绑定表达式中使用的 person 永远不会是 undefined

类似于的 ngIf 的自定义指令

那些行为与 *ngIf 类似的指令可以通过包含一个静态成员作为标记,来告诉模板编译器它们希望和 *ngIf 享受同等待遇。这个 *ngIf 的静态成员就是:

content_copypublic static ngIfUseIfTypeGuard: void;

它声明了 NgIf 指令的 ngIf 属性应该在用到它的模板中看做一个守卫,以表明只有当 ngIf 这个输入属性为 true 时,才应该生成那个模板。

非空类型断言操作符

使用 非空类型断言操作符可以在不方便使用 *ngIf 或 当组件中的某些约束可以确保这个绑定表达式在求值时永远不会为空时,防止出现 Object is possibly 'undefined' 错误。

在下列例子中,personaddress 属性总是一起出现的,如果 person 非空,则 address 也一定非空。没有一种简便的写法可以向 TypeScript 和模板编译器描述这种约束。但是这个例子中使用 address!.street 避免了报错。

content_copy@Component({  selector: 'my-component',  template: '<span *ngIf="person"> {{person.name}} lives on {{address!.street}} </span>'})class MyComponent {  person?: Person;  address?: Address;   setData(person: Person, address: Address) {    this.person = person;    this.address = address;  }}

应该保守点使用非空断言操作符,因为将来对组件的重构可能会破坏这个约束。

这个例子中,更建议在 *ngIf 中包含对 address 的检查,代码如下:

content_copy@Component({  selector: 'my-component',  template: '<span *ngIf="person && address"> {{person.name}} lives on {{address.street}} </span>'})class MyComponent {  person?: Person;  address?: Address;   setData(person: Person, address: Address) {    this.person = person;    this.address = address;  }}

使用 $any() 禁用类型检查

可以通过把绑定表达式包含在类型转换伪函数 $any() 中来禁用类型检查。 编译器会像在 TypeScript 中使用 <any>as any 进行类型转换一样对待它。

下面的例子中,通过把 person 转换成 any 类型,忽略了 Property addresss does not exist 错误。

content_copy@Component({
  selector: 'my-component',
  template: '{{$any(person).addresss.street}}'
})
class MyComponent {
  person?: Person;
}

小结

  • 什么是 AOT 编译器,以及它为什么如此重要。
  • 为何元数据必须使用 JavaScript 的一个子集来书写。
  • 这个子集是什么。
  • 定义元数据时的其它限制。
  • 宏函数和静态宏函数。
  • 与元数据有关的编译器错误。
  • 验证绑定表达式。
AngularJS

Angular 是一个开发平台。它能帮你更轻松的构建 Web 应用。Angular 集声明式模板、依赖注入、端到端工具和一些最佳实践于一身,为你解决开发方面的各种挑战。

AngularJS目录

1.快速上手 | quick start
2.语言服务
3.安全
4.环境准备与部署
5.Service Worker
6.保持最新
7.从 AngularJS 升级
8.服务端渲染
9.Visual Studio 2015 快速上手
10.风格指南
11.国际化
12.测试
13.路由与导航
14. 教程 | Tutorial
15.架构
16.组件与模板
17.表单
18.可观察对象与RxJS
19.引导启动
20.Angular 模块
21.依赖注入
22.HttpClient
23.词汇表
24.AngularJS 应用
25.AngularJS 模块
26.AngularJS 事件
27.AngularJS HTML DOM
28.AngularJS 过滤器
29.AngularJS 控制器
30.AngularJS 指令
31.AngularJS 表达式
32.AngularJS 简介
33.AngularJS 参考手册
34.AngularJS 实例
35.AngularJS 输入验证
36.AngularJS 表单
37.AngularJS SQL
38.AngularJS 表格
39.AngularJS Http
40.AngularJS 包含
41.AngularJS Bootstrap
42.AngularJS API
43.AngularJS ng-checked 指令
44.AngularJS ng-change 指令
45.AngularJS ng-blur 指令
46.AngularJS ng-bind-template 指令
47.AngularJS ng-bind-html 指令
48.AngularJS ng-bind 指令
49.AngularJS ng-app 指令
50.AngularJS Scope(作用域)
51.AngularJS ng-model 指令
52.AngularJS ng-dblclick 指令
53.AngularJS ng-cut 指令
54.AngularJS ng-csp 指令
55.AngularJS ng-copy 指令
56.AngularJS ng-controller 指令
57.AngularJS ng-cloak 指令
58.AngularJS ng-click 指令
59.AngularJS ng-class-odd 指令
60.AngularJS ng-class-even 指令
61.AngularJS ng-class 指令
62.AngularJS ng-keyup 指令
63.AngularJS ng-keypress 指令
64.AngularJS ng-keydown 指令
65.AngularJS ng-init 指令
66.AngularJS ng-include 指令
67.AngularJS ng-if 指令
68.AngularJS ng-href 指令
69.AngularJS ng-hide 指令
70.AngularJS ng-focus 指令
71.AngularJS ng-disabled 指令
72.AngularJS ng-non-bindable 指令
73.AngularJS ng-mouseup 指令
74.AngularJS ng-mouseover 指令
75.AngularJS ng-mousemove 指令
76.AngularJS ng-mouseleave 指令
77.AngularJS ng-mouseenter 指令
78.AngularJS ng-mousedown 指令
79.AngularJS ng-model-options 指令
80.AngularJS ng-model 指令
81.AngularJS ng-list 指令
82.AngularJS ng-style 指令
83.AngularJS ng-srcset 指令
84.AngularJS ng-src 指令
85.AngularJS ng-show 指令
86.AngularJS ng-selected 指令
87.AngularJS ng-repeat 指令
88.AngularJS ng-readonly 指令
89.AngularJS ng-paste 指令
90.AngularJS ng-options 指令
91.AngularJS ng-open 指令
92.AngularJS ng-value 指令
93.AngularJS ng-switch 指令
94.AngularJS ng-submit 指令
95.AngularJS 服务(Service)
96.AngularJS Select(选择框)
97.AngularJS 动画
98.AngularJS 依赖注入