Entities in Angular
这里讨论的是Angular顶层设计中的一些实体
- Modules--模块,帮助你将依赖组织到一个具体的单元中
- Components--组件,包含你的应用的大部分结构和逻辑的元素(element)
- Directives--是一个对象,具有修改元素(DOM)或者改变其行为的能力
- Pipes--在数据被渲染之前格式化的函数
- Services--包含类似于数据处理和帮助工具(函数)的可重用对象
Modules
模块是用来存储相关联的易重用和发布的实体的容器。Angular本身就是由一些模块构成的,任何你使用的外部库都会被包装成模块的形式。模块包括ES2015中加入的JS模块概念和Angular模块。JS模块是语言结构,它提供了一种将代码分成不同的文件以按需加载的方式。自己写的TS文件都是JS模块,因为它们引入了一些值并导出了一些值。Angular模块是一种逻辑结构,它是用来组织相似的实体并且让Angular决定需要什么依赖并且加载哪些模块。
一个模块可以通过定义一个类,并且加上@NgModule
注解来声明。注解中包含的是元数据(metadata)。declarations
数组包含主要模块需要的所有组件和directives,providers
数组包含应用需要的所有服务。imports
数组包含这个模块依赖的其它模块(这里是检查某些模块是否被加载的第一个地方)。在渲染之前,Angular需要知道渲染什么组件,它会查看bootstrap
数组,通常这个数组只包含一个组件
Components
一个组件是一个处理元素(encapsulated element),用来维护如何渲染输出的内部逻辑。在HTML中,一个select可以被认为是一个组件。使用Angular,我们创建自己的组件。创建组件应该遵循的原则包括:
- Encapsulation -- 保持组件逻辑独立
- Isolation -- 保持组件内部细节隐藏
- Reusablity -- 最小代价下组件可重用
- Event-based -- 在组件的生命周期可以注册事件
- Customizable -- 可以个性化组件(style)
- Declarative -- 可以声明式(标签)使用组件
组件之间基本的交流是从父组件向下传递数据,通常通过绑定和复制,或者事件。一般做法是将组件树通过有意义的接口来组合起来
Directives
directives是“教给”HTML标签新技能的强有力的工具。它可以给予一个普通元素以额外的能力。假如你不希望让用户在一个表单上突然点击完成按钮,那么你可以使用它来保证用户正在使用这个表单并且还没有完成表单的内容
NgFor
可以遍历数组中的元素,NgClass
可以改变元素的class内容,NgIf
可以判断条件。NgIf
在计算完赋给它的值后,如果为真则渲染元素,否则会将其移除DOM
Directives有三类,attribute directives,structural directives和components。组件已经在上文讨论过了,它赋予了标签元素新的能力。因为它是唯一有模板的directive,所以最好还是把它看成独有的类型。attribute directives是改变元素的外观或行为的,例如NgClass
,有很多内置的此类directive。structural directive根据一些条件来修改DOM树。它们可以增加或者移除DOM元素(NgIf
和NgFor
)
Angular提供的主要的默认directives包括
- NgClass -- 有条件应用在元素上的class
- NgStyle -- 有条件应用一系列style到元素上
- NgIf -- 有条件在DOM树上插入或者移除元素
- NgFor -- 遍历集合元素
- NgSwitch -- 有条件从一系列选项中展示一个
Pipes
你经常需要用不同的格式来展示相同的数据,使用管道,我们可以在渲染过程中在不改变值得情况下转化数据。Angular提供了很多默认的管道,覆盖很多普通用例。管道只是改变了数据的渲染方式,而不改变属性,它创建了输出的拷贝,然后修改它,展示修改后的值。可以自定义管道
Services
服务是可重用的JS函数。Angular提供了很多开箱即用的服务,很多第三方模块也会暴露服务
服务是数据处理的最理想的地方,而组件controller不是。Angular遵循单一职责原则,保持单个实体专注单一任务
Angular是如何开始渲染app的
Angular CLI会生成一个相对简单的app。ng new app-name
,Angular有一个app启动机制来开始渲染
CLI使用webpack来构建,它会编译所有的JS文件并且在index.html
底部加上script
标签,Angular启动后,它会加载App的模块,读取加载所需要的所有依赖,在base app中,Browser
模块会在开始加载进应用。然后Angular渲染App组件,这是你应用的根元素,当App组件渲染时,它的所有子组件也会在组件树上被渲染,在这个过程中,它也会绑定数据并且设置事件监听器。这个过程完成后,整个应用渲染完成,供用户使用。应用的生命周期从用户开始使用记起,在用户的导航下,屏幕中的组件被移除、新组件被添加并渲染,这个过程一直进行直到应用停止
编译器的类型
Angular提供了两类编译器,JiT(Just-in Time)和Ahead-of-Time(AoT)。这两者的主要区别是编译器的工具和时间,这会改变你应用的行为方式和应用如何服务用户。JiT编译,应用的编译过程在所有的asset被加载之后才在浏览器中开始,所以在加载页面和看到内容中间可能会有一个过渡。AoT编译,在将内容发送给浏览器之前就会进行编译,那么一旦应用asset开始加载用户在没有任何loading信息的情况下就能看到需要展示的内容。另外一个区别是,JiT中,应用在运行前必须加载编译器库,然而AoT可以将这个过程放在发送过程中,loading体验更好。通过使用AoT,因为应用在服务之前就被编译,所以有很多优化点。另外它提供了服务端渲染的功能,对于一些用户指定的数据进行提前渲染也是有用的
在生产环境中要保证使用AoT编译器
依赖注入
追踪和共享(tracking and sharing)对象。DI是一种维护对象的模式,它通过一个registry来维护所有可用对象并且提供一个你可以获取对象的服务。它跟JS中import和export的区别是什么?对Angular来说,保持对于应用的哪一部分需要哪个服务的了解是必要的,然而JS不需要关心依赖之间是如何关联的。这个信息对于如果更好的组织依赖是很重要的。并且通过Angular来注入依赖可用处理所有第三方依赖
DI系统中的几个关键点。第一是injector
,Angular提供了这个服务来请求和注册依赖,injector通常在幕后工作,偶尔会直接使用。大多数时间,你通过在属性上声明类型注解来调用injector。第二是providers
,它负责创建对象的实例,injector直到可用的provider,并且基于名字来调用provider的工厂方法来返回需求的对象。任何通过NgModule
中的providers
注册的东西都会被注入到你的应用中,你可以在任何地方使用注入(constructor(private @Inject(HttpClient) http) {}
)。providers不必暴露根模块,它只需要在特定组件或者组件树中可见即可
change detection
简单来说,change detection是保持数据和渲染后的view内容一致的机制。改变总是从model向view的,Angular使用了一个单向的从父到子的改变传播机制。那么当父元素改变,其子元素会检查并且做出相应的改变。从上一次进程运行开始,只要数据值发生改变,Angular就会运行change detection。JS没有任何相关机制来通知对象的任何改变。这个过程经过了很多的优化。要做这件事情,Angular创建了特殊的类,即change detector
,当它渲染组件时,这个类负责追踪组件数据的状态和运行change detection过程中任何数据发生的变化。当一个组件中的值发生了变化,它会更新组件和其潜在的子组件。因为Angular的应用是组件树,Angular可以判断那个组件会被影响从而限制改变范围
Angular有两种方式来触发change。默认模式会遍历整个树查找每个变化,OnPush模式会告诉Angular哪些组件只关心从它们父组件传入的数据变化,那么Angular会在这个过程中得知其父组件没有变化后不再检查子组件
change detection可以通过事件、接收HTTP响应、timer/interval来触发。理解它的最好方式是,在任何时间点有异步行为发生时,change detection过程开始查看哪些东西会被改变,因为同步调用在渲染过程中已经被处理了
组件树的数据从上到下传递,而事件将数据从下到上传递
模板表达式和绑定
一个组件总有一个模板。Angular允许逻辑和自定义直接放进模板,这使得模板更加声明化(declarative)。模板本身是普通的HTML,一个模板可以使用模板逻辑中controller中存储的值。模板的几个概念包括
- Interpolation -- 页面中展示的内容
- Attribute and property bindings -- 将controller中的数据和元素的属性联系起来
- Event binding -- 向元素添加事件监听器
- Directives -- 修改元素的行为或者给元素添加额外的结构
- Pipes -- 展示内容之前格式化
在模板中我们能看到模板表达式,很像普通的JS表达式,所有的值和controller挂钩。这里的表达式跟JS比有一些不同
- 不能访问全局变量,例如
console
- 不能给变量赋值(除了在事件中)
- 不能使用
new ++ -- | &
等操作符 - 提供了新的 | 操作符来进行管道操作和 ?. 来允许访问null属性
模板表达式在三个地方使用,interpolation,property bindings,event bindings
Interpolation
它处理的是在页面中作为字符串展示和绑定的结果值。这个绑定通过定义一个表达式,计算表达式,将结果替换到标签内容中。<p>{{user.name}}</p>
,Interpolation总是使用{{value}}
的语法来将数据绑定到模板中(这个一个叫做mustache的模板引擎的语法)
Property bindings
它允许你将某个值绑定到元素的属性上来修改它的一些行为。包括class/disabled/href/textContent
等,它允许你绑定自定义的组件属性(叫做inputs
),例如当你想将一个URL绑定到img
元素中,可以<img [src]="user.img" />
。事实上,interpolation就是绑定textContent
属性的缩写。中括号中的是属性名,引号中的是表达式,这里的表达式总是会被计算。这里作者自己总是使用property bindings的风格,即<p [textContent]="user.name" />
。这个方式中括号的property总是驼峰命名形式。使用[]
语法是将数据绑定在元素的property上而不是attribute,这里的区别是properties是DOM元素的属性,它有很多原生的属性,而attribute则相当于直接把数据绑定到了properties本身
特殊的property bindings
对于设置class和style有很多特殊的property bindings。这两个属性包含多个内容,Angular对此有特殊的语法。一个元素的class属性是一个DOMTokenList
,要给数组,你可以通过[class]="getClass()"
来设置class的字符串,但是对于有既定class的元素来说并不好用。通常你只会控制单个class,你可以使用[class.className]
语法,意思是将某个className通过一个函数控制来决定是否添加进class属性。类似的,style属性是CSSStyleDeclaration
对象,是一个特殊的对象,持有所有CSS属性,使用[style.styleName]
来设置任何CSS的值
Attribute bindings
一些元素属性不能直接被绑定,因为一些HTML元素存在还没有被当作properties的attribute。例如aria
。此时可以使用Angular提供的方法。aria
属性用来表示关于元素的辅助设备信息,例如aria-required
,它将input标记为需要提交的。而这个属性在一些情况下并不需要,如果你使用上面的方式会有parse error。应该使用[attr.aria-required]="isRequired()"
这种特殊的绑定语法
Event bindings
将模板中的数据绑定回组件。JS的内置事件(event)就是做这件事的。你可以创建自己的事件
事件绑定的语法使用括号()
来绑定一个已知事件,你将事件名放在括号内(即不包含on
的其余字符),例如<form (submit)="save()">...</form>
。上下文很重要,因为事件只绑定在当前的组件上,但是你可以触发事件,这些事件会向上冒泡到父组件(如果父组件也监听了这个事件)。组件和directives可以发送它们自己的事件,你可以监听这些事件
双向绑定,它使用了property binding和event binding,Angular称其为banana in a box([()])。这种方式允许你同步模板和controller同时绑定的值。它首先做了普通的属性绑定,并且设置了一个事件绑定,在form元素中你只能使用NgModel
,但是在很多属性中都能使用双向绑定。通常会限制使用双向绑定