angular复习笔记2-架构总览

angular架构总览

一个完整的Angular应用主要由6个重要部分构成,分别是:组件、模板、指令、服务、依赖注入和路由。这些组成部分各司其职,而又紧密协作,它们的关系如图所示。

与用户直接交互的是模板视图,模板视图并不是独立的模块,它是组成组件的要素之一。另一个要素是组件类,用以维护组件的数据模型及功能逻辑。

路由的功能是控制组件的创建和销毁,从而驱使应用界面跳转切换。指令与模板相互关联,最重要的作用是增强模板特性,间接扩展了模板的语法。

服务是封装若干功能逻辑的单元,这个功能逻辑可以通过依赖注入机制引入到组件内部,作为组件功能的扩展。

在Angular的应用接收用户指令、加工处理后输出相应视图的过程中,组件始终处于这个交互的出入口,这正是Angular基于组件设计的体现。组件承载着Angular的核心功能,所以接下来的内容将从组件开始,逐步揭开Angular框架的神秘面纱。

组件Component

angular应用基于组件设计,每个组件也并不是孤立存在的,组件与组件之间通常会构成一个树状的结构,父子组件之间存在着双向的数据流动。要理解数据是怎样流动的,首先要了解组件之间的调用方式。简单地说,组件的外在形态就是自定义标签,所以组件的调用实际体现在模板标签里的引用上。

组件之间可以通过模板来形成关系,组件可以通过@Input和@Output来对外暴露接口。在模板里面“[property]”表示属性绑定,数据从父组件流向子组件,“(event)”表示事件绑定,数据从子组件流向父组件。在angular模板里面可以直接引用组件类中的字段,组件类和模板之间的数据交互称为“数据绑定”。前面说的属性绑定和事件绑定也是数据绑定的范畴,数据在组件间的流动需要一种机制来推动实现,需要一种驱动力,这个驱动力就是angular的变化监测机制。angular是一个响应式的框架,每次数据变动几乎都能实时处理,并更新到视图(模板),那么Angular是如何感知数据对象发生了变动呢?ES5提供了getter/setter语言接口来捕获对象变动,然而Angular并没有采用。Angular是在适当的时机中检验对象的值是否被改动的,这个适当的时机并不是指固定的某个频率,而通常是在用户操作事件(如单击)或者setTimeout、XHR回调等这些异步事件触发之后。Angular捕获这些异步事件的工作是通过Zone.js库实现的(关于Zones的内容在后面会展开描述)。每个组件背后都维护着一个独立的变化监测器,这个变化监测器记录着所属组件的数据变更状态。由于应用是以组件树的形式组织的,因此每个应用也都对应着一棵变化监测树。当Zones捕获到某异步事件后,它会通知Angular执行变化监测操作,每次变化监测操作都始于根组件,并以深度优先的原则向叶子组件遍历执行。angular5提供了不依赖Zones的写法,后面会有详细的描述。Angular强大的数据变化监测机制使得开发者不必关心数据何时变动,结合数据绑定实现模板视图实时更新。变化监测机制提供了数据自动更新功能,若此时需要手动捕获变化事件做一些额外处理,可以吗?答案是肯定的。Angular还提供了完善的生命周期钩子给开发者调用,如ngOnChanges可以满足刚提到的捕获变化事件的要求,ngOnDestroy可以在组件销毁前做一些清理工作,等等。

模板Template

模板的语法基于HTML,Angular为模板定义了一套强大的语法,数据绑定是模板最基本的功能。除了上述提到的属性绑定和事件绑定,插值也是很常见的数据绑定的方法,向这样:{{item}},这个在vue里面也是一样的用法。

插值的变量上下文是组件类本身,它是一种单向的数据传递,从组件类流向模板。上面提到的三种数据绑定都是单向的流动的。但是在某些场景需要双向的数据绑定,例如表单,angular结合了属性绑定和事件绑定,提供了如下的语法来支持双向绑定:

<input [(ngModel)]="item"></input>

[()]是实现双向绑定的语法糖。ngModel是辅助实现双向绑定的内置指令。上述代码执行后,input控件和item之间就形成了双向的数据关联,input的值发生改变时,会自动赋值到item上,item的值发生改变时也会更新input的值。

数据绑定解决了数据的传递和展示,而针对数据的格式化显示,angular提供了一种称作管道的功能来解决。管道使用竖线“|”来表示:

<span>{{item | phone}}</span>

如上可将item的值显示为phone的格式。比如item的值是13844443333,使用管道操作符可以将item的显示方式转换成138-4444-3333.

指令Directive

指令与模板关系密切,指令可以与DOM进行灵活交互,它或者改变样式,或者改变布局。指令的范畴很广,实际上组件也是指令的一种。组件与一般指令的区别在于:组件带有单独的模板,即DOM元素;而一般的指令作用在已有的DOM元素上。一般的指令分为两种:结构指令和属性指令。

结构指令能够添加、删除、修改DOM从而改变布局,例如ngIf:

<button *ngIf="canEdit">编辑<button>

当canEdit的值为true时,可以点击按钮,如果为false时,该按钮会从DOM中删除,不显示。

注意:*号很重要,不能丢掉,它是结构型指令的重要组成部分。

属性指令用来改变元素的外观或行为,使用起来跟普通的HTML元素属性非常相似,例如ngStyle指令,用于动态计算样式值。示例代码如下:

<span [ngStyle]="setStyle()">{{contact.name}}</span>

<span>标签的样式由setStyle()方法计算得出。setStyle()是组件类的函数成员,返回一个计算好的样式对象:

 

 关于指令的用法会在后面进行详细的描述,指令的强大之处就在于可以自定义指令,能够最大限度的实现UI层面的逻辑。

服务Service

服务是封装单一功能的单元,类似于工具库,常被引用到组件内部,作为组件的功能扩展。那么服务包含什么?它可以是一个简单的字符串或JSON数据,也可以是一个函数,甚至是一个类,几乎所有的对象都可以封装成服务。
封装成独立模块的服务可以被许多不同的组件重复使用,这就是服务的设计原则。HTTP是angular常用的服务,它封装了一系列异步接口,但与一般的接口不同,它对外暴露的是Reactive programing规范的接口,基于RXJS实现,严格贯彻响应式编程的思想。所以会在后面详细介绍HTTP的同时,也会详细的介绍RXJS这个响应式编程的框架。

依赖注入Dependency Injecting

依赖注入一直都是angular的卖点所在,当一个组件需要某个服务时,只需要在组件的构造函数中声明对服务的依赖,框架会在恰当的时候根据构造函数依赖将服务注入到组件中,而开发者无需关心这些依赖是如何被实例化的。一个简单的依赖注入的例子如下:

@Component({
  selector: 'app-document',
  templateUrl: './document.component.html',
  styleUrls: ['./document.component.scss'],
  providers: [PermissionService]
})
export class DocumentComponent implements OnInit {

  constructor(private permissionService: PermissionService) { }

  ngOnInit() {
  }

}

@Component装饰器中的providers是关键,框架会给DocumentComponent组件创建一个注入器对象并新建PermissionService的一个实例将该实例存储到注入器中。组件在需要PermissionService时,会根据TypeScript的类型匹配从注入器中拿到该类型对象的实例,无须在重复显式的实例化。需要注意的是,服务的每一次注入(也就是使用providers声明),该服务都会被创建出新的实例,组件的所有子组件均默认继承父组件的注入器对象,复用该注入器里存储的服务实例。这种机制可保证服务以单例模式运行,除非某个子组件再次注入(即通过providers声明)。这种灵活的方式让你有更多的选择来定义一个服务的生命周期:

①当一个服务可以全局单例的方式运行时,可以将服务注册到根组件或者如下代码所示这样:

@Injectable({
  providedIn: 'root'
})
export class AccountService {
constructor(){}
}

@Injectable()装饰器中的providedIn的值是”root“,这样,这个服务就可以全局单例的方式运行了。

②当一个服务需要在不同的组件上存储不同的数据和逻辑时,这意味着你不需要也不能在多个组件中共享这个组件,那你就在每个组件的@Component()装饰器中使用providers属性来注入这个服务,那这个服务就会在这个组件和这个组件的子组件上面形成单例(子组件会共享父组件的依赖注入器),而不会在别的不相干的组件上共享该服务了。框架会根据@Component()装饰器的providers属性来为组件初始化一个注入器。

路由Router

angular作为一个单页应用(SPA)框架,路由是必不可少的组成部分之一。在Angular中,路由的作用是建立URL路径和组件之间的对应关系,根据不同的URL路径匹配出相应的组件并渲染。假设通讯录应用需要添加一个通话记录页面,简单的路由配置如下:

注意该配置的第一项path的值为空,这表示默认路由。上述配置的作用如下:·

访问http://www.abc.com/时,页面渲染ContactListComponent组件。·

访问http://www.abc.com/record时,页面渲染RecordListComponent组件。

 组件树的节点会不断发生变化,如下图所示:

原来的组件树中多了一个路由指令(标签名为<router-outlet></router-outlet>),上图的模板语法为:

路由指令router-outlet起着类似于“插座”的作用,根据当前的URL路径匹配插入对应的组件节点,实现了主体内容(页面)的刷新,这就是Angular路由最基本的功能。路由指令还支持多重嵌套,实现子路由功能。假设通讯录应用的通话记录页面需要新增标签页切换功能,用来切换显示全部来电及未接来电,可以修改路由配置如下:

 

上面配置中的record条目新增了一个children的配置项,用于设置子路由的信息。当URL改变为http://www.abc.com/record时,显示的是AllRecordsComponent组件视图,通话记录组件子路由功能如下图所示。

 

 路由还支持路径参数,如http://www.abc.com/list/123,其中123为联系人ID,从而实现类似于RESTful风格的URL形式。另外,在同一层节点上还可以放置多个路由指令,实现从属路由功能。关于更多、更强大的路由功能将会在后面的章节中详细介绍。

应用模块Module

在上面章节中,读者了解了Angular应用中的六个主要组成部分,那么这些不同的组成部分是如何组织起来,构成一个完整的功能单元甚至是完整的应用呢?Angular引入了模块机制,对某些特定的功能特性进行封装,可能包含若干组件、指令、服务等,甚至拥有独立的路由配置,其关系如下图所示。

每个angular应用中最少会有一个模块,这个模块就是根模块(默认名称是AppModule)。除了这个根模块外,你可以根据自身的业务特点将不同的组件、服务、路由和指令等等都封装到一个模块中,一般不建议将应用的所有逻辑都封装到根模块中。在angular中,除了根模块外,其他的模块类型有:封装某个完整功能的特性模块(FeatureModule)、封装一些公共构件的共享模块(SharedModule),以及存放应用级别核心构件的核心模块(CoreModule)。

  • 路由:特性模块也可以自带路由配置,当特性模块导入到根模块后,特性模块的路由配置会自动与根模块里的路由配置合并。·
  • 组件和指令:在默认情况下,模块内的组件和指令是私有的。也就是说,特性模块A被导入到根模块后,根模块依然不能使用特性模块A里的组件和指令,除非特性模块A里显式暴露了某些组件或指令,这些暴露的组件或指令相当于模块的API。
  • 服务:服务的处理则有些特殊,通过依赖注入机制,服务同样可以注入到模块里,但跟组件里的依赖注入的作用域并不相同。注入到组件里的服务只能使用在该组件及其子组件上,而注入到模块里的服务在整个应用里均能使用,因为所有模块都共享着同一个应用级别的根注入器。这种机制似乎有点违背模块的封装性,到底这种设计是否合适,当有命名冲突时又是怎么解决的,这些疑问会在后续章节一一解答。

Angular已经封装了不少常用的特性模块,如:

  • ApplicationModule——封装一些与启动相关的工具。
  • CommonModule——封装一些常用的内置指令和内置管道等。
  • BrowserModule——封装在浏览器平台运行时的一些工具库,同时将Common-Module和ApplicationModule打包导出,所以通常在使用时引入BrowserModule就可以了。
  • FormsModule和ReactiveFormsModule——封装与表单相关的组件指令等。
  • RouterModule——封装与路由相关的组件指令等。
  • HttpModule——封装与网络请求相关的服务等。

上述已提及,Angular通过引导运行根模块来启动应用,引导方式有两种:动态引导和静态引导。要理解两者的区别,先来看看Angular应用的启动过程——Angular应用在运行前,都需要经过编译器对模块、组件等进行编译,编译完成后才开始启动应用并渲染界面。

动态引导和静态引导的区别就在于编译的时机不同,动态引导是将所有代码加载到浏览器后,在浏览器中进行编译,即JiT编译;而静态引导是将编译过程前置到开发时的工程打包阶段,加载到浏览器的将是编译后的代码,称为AoT编译。

假设根模块为AppModule,动态引导的代码如下:

动态引导从platformBrowserDynamic函数启动,该函数是从'@angular/platform-browser-dynamic'文件模块(关于Angular文件模块将在下一节中讲述)导入的。动态引导启动的模块AppModule即是我们编写的模块。再来看看静态引导的示例代码:

静态引导从platformBrowser函数启动,这个函数是从@angular/platform-browser文件模块导入的,跟动态引导的不是同一个。静态引导启动的是AppModuleNgFactory模块,这是AppModule经过编译处理后生成的模块(app.module文件编译后生成app.module.ngfactory文件)。由于省去了浏览器编译这个步骤,因此应用启动的速度也会更快。

AngularCLI包含的构建工具已经非常好地支持AoT编译,在开发中只需要按照JiT方式进行代码编写,构建时打开AoT编译选项,如ngbuild--aot,即可完成如文件编译及静态引导等这些AoT处理过程。JiT编译开发流程简单明了,但性能欠佳,仅适合在开发阶段使用;而AoT编译性能提升明显,推荐使用。

构建工具在不断迭代升级中,在未来的迭代版本中将会支持在开发阶段实施AoT编译。

源码结构介绍

Angular是基于TypeScript编写的,TypeScript又是ES6的超集,而ES6给开发者带来的一个新特性是文件级别的模块功能。利用这个特性,整个Angular项目的源码是基于ES6模块来组织的。注意这里的模块和上一节提到的模块并不是同一个概念,上节提到的是应用级别的模块,是以功能特性为划分依据的;而本节的模块是语言级别的,是以物理文件或文件夹为划分依据的。GitHub源码结构如下图所示(angular/packages)。

在实际的npm包使用中,使用@angular作为命名空间来引入Angular模块,如@angular/common。源码在发布至npm前会经过构建,把packages文件夹下的模块打包成@angular前缀。

在上图中,用方框标记的为常用的一级模块(一级文件夹),下面对主要的模块进行介绍。

·packages/core:存放核心代码,如变化监测机制、依赖注入机制、渲染等,核心功能的实现、装饰器(@Component、@Directive等)也会存放到这个模块中。

·packages/common:存放一些常用的内置指令和内置管道等。

·packages/forms:存放与表单相关的内置组件及内置指令等。

·packages/http:存放与网络请求相关的服务等。在Angular4.3以上版本中已有网络请求工具的替代方案,存放在packages/common/http里,在后面的章节会有详细的解释。

·packages/router:存放与路由相关的组件和指令等。

·packages/platform-<x>:存放的是与引导启动相关的工具。Angular支持在多个平台下运行,不同的平台都有对应的启动工具,这些启动工具会被封装到不同的模块里,如服务端渲染这个场景的启动工具存放在packages/platform-server下,浏览器的启动工具则存放在packages/platform-browser下。

这些语言级别的模块和应用级别的模块非常相似,实际上它们是有关联的,如CommonModule模块本身就存放在packages/common里,当开发者需要引用packages/common里的诸多指令或者组件时,只需引入CommonModule即可。CommonModule的作用是打包packages/common下零散的组件指令并作为该模块的API暴露出来,方便开发者一次性引入。关于其他模块,有兴趣的读者可以到Angular的GitHub库中查阅。

 

posted @ 2019-05-17 16:01  wall-ee  阅读(430)  评论(0编辑  收藏  举报