目录
angular 架构概览
基本涉及常见重要的知识点:
- 模块
- 路由
- 组件
- 模板
- 服务
- 指令
- 管道
不同的类型,文件名通常会都按照一定的规范来命名,以便直接看出该文件的角色。
当然,文件命名只是给开发人员来方便维护、辨别,对于 Angular 来说,这些都是一份份的 ts 文件代码,所以,都需要在相对应的文件中加上一些装饰器比如:
@Directive,@Pipe,@Component,@NgModel 等这些,才能够让 Angular 识别出该文件的角色、用途。
基本上,用 Angular 做一个简单的前端项目,就是跟上面这些打交道,理清它们各自的用途及用法,还有之间的联系,基本上,就可以上手进行一些开发了。
当然,像在 Service 服务中,还会有异步编程、HttpClient 网络编程的相关知识点;
在 Component 组件中,也还会有表单、动画相关的编程知识点,这些都是需要进一步去深入学习研究,但从总体架构上来看,就是要先了解以上这些知识点了。
模块:
angular用
@NgModel
声明的
ts 文件
,表明
该文件
作为
模块角色
,来
管理
其他角色
。
其他角色包括:
组件、指令、管道、服务等等
,这些角色必须在模块文件中声明了,才能够被该模块内的其他角色所使用,而且同一个组件、指令、管道不允许同时在多个模块中进行声明,只能通过模块 exports 给其他模块使用。
Angular 里的模块,不仅可以在项目结构上集中管理同一个模块的代码文件,还可以为模块内的代码提供一个运行的上下文。
意思就是说,不同模块在运行期间互不影响,就好像各自运行在各自的沙箱容器中一样。举个简单的例子,在不同模块中声明相同的变量名,或相同的 css 的类选择器,它们之间并不会起冲突。
当然,模块之间可以有交互,模块可以依赖于另一模块,模块内的可以共享资源等等,所以,NgModel 中
有许多需要配置的声明项
,比如:
- declarations:声明属于本模块内的组件、指令、管道
- providers:声明属于本模块内的服务
- imports:声明本模块所引用的其他模块,通常是 imports 其他模块在 exports 中声明的项
- exports:声明本模块对外公开的组件、指令、管道等,在这里公开的项才可以被其他模块所使用
- bootstrap:只有根模块才需要配置,用来设置应用主视图,Angular 应用启动后,这里就是入口
- 还有其他一些可选配置,比如应用主题,或者动态的组件声明等等
在 Angular 中,大多数的模式就是,一个根模块管理着很多功能模块,然后,每个模块管理自己模块内部所使用到的组件、指令、管道、服务、或者需要依赖于其他模块,如果该模块内部的这些角色,有些可以供其他模块使用,那么就需要对外暴露。
路由:
一个项目这么多模块,Angular 并不会一开始就把
所有模块
都加载
,而是
惰性加载
,
按需加载
。
那么,
什么时候会去加载呢?
就是
等某个模块内部的组件被使用的时候会加载
,而组件是
什么时候会被使用的呢?
有两个时机,
一是组件被直接调用;二是触发了路由去加载;
路由通常的配置方式是用一个 @NgModel 声明的模块,但只用其中两项配置:imports 和 exports,imports 用来导入当前模块所有组件与 url 的映射表,而 exports 用来将这些映射表信息暴露,以供相对应的模块去引入使用。
当然,你不想抽离路由配置,直接将其配置在对应模块的 imports 内也可以,抽离的话,相对独立,可维护。
区别于
传统的前端网页
的
跳转方式
,Angular 项目是
一个单页应用
,
所谓的单页应用
就是说
只有一个页面
,所有
页面的跳转
,其实是将
当前页面的显示内容进行替换
,页面仍旧只有一个,并不会打开新的页面。
而
页面的跳转
,通常有以下
几种场景
:
- 用户输入 url 进行跳转
- 用户点击交互按钮进行跳转
- 用户操作前进或后退进行跳转
这些场景,
路由的工作机制
都能够很好的
支持
。
如果网页很简单,只有一个首页,并不存在页面跳转场景,那么可以不用配置路由,只需要在 index.html 中配置根视图,以及在根模块的 bootstrap 中配置根视图组件即可。
但如果
项目划分
成了
多个功能模块
,那么应该交由
每个模块
管理自己的
路由表
,而后
选择一个上层模块
,来
统一关联各个模块路由
,有两种方式:
一是在上层模块的 imports 内按照一定顺序来导入各个功能模块;
但这种方式想要按照路由层级来查看路由表就比较麻烦,需要到各个模块内部去查看或者借助一些工具。
另一种方式是,在上层模块的路由表中使用 loadChildren 加载各个功能模块,然后各个功能模块默认路由都显示成空视图,各自内部再通过配置 children 的路由表方式来管理各个模块内部自己的路由表。
目录
在 Angular 中,最常接触的应该就是组件了。
我是这么理解的,
组件
可以是你
在界面上看到的任何东西
,可以是一个页面,可以是页面上的一个按钮。
而对于浏览器解析并呈现前端页面时,Html、CSS、JavaScript 这三分文件通常都是需要的,而 Angular 是使用了 TypeScript,所以一个组件,其实就
包括
了:
Html
,
CSS
,
TypeScript
。
在
Angular 中
,可以说,是
以组件为单位来组成页面
的,组件是核心,因为 Angular 提供的功能基本都是用来为组件服务的。
以上,是我的理解。
但要注意,官网教程中,很多地方的组件描述,更多时候是倾向于表示 TypeScript 的那份文件,因为对于组件来说,TypeScript 可以说是它的核心,CSS 只是样式文件,Html 更类似于模板存在。
所以这里将组件和模板放在一起讲,因为就像开头那张图一样,组件是
一份 TypeScript 文件
,在该文件中,定义了这个组件的
模板(template)来源
和
CSS 样式来源
。
模板提供了该组件的
呈现结构
,而 TypeScript 里定义了组件的
数据来源及交互行为
,它们两一起
组织成一个视图
呈现给用户。
既然,这份 TypeScript 的组件文件和模板文件需要共同合作,那么它们之间就少不了交互,所以就涉及到很多所谓的
模板语法
,也就是所谓的
组件和模板之间的交互方式
。
比如,当要往模板中嵌入 TypeScript 中的变量数据时,可以使用
{
{value}}
这种语法形式,同样的,还有模板中标签的属性绑定,事件回调注册的交互方式的语法。
总之,
Angular 支持双向数据绑定,是一种以数据驱动的思想来让页面进行交互刷新的方式
,区别于
传统的前端模式
。在以往,如果
需要动态的更新 DOM 上的信息时
,需要先获取到相对应的元素实例对象,然后
调用相应的 DOM API 来操纵 DOM
;
而使用 Angular 的话,可以直接在模板的相应元素中,
将某个属性与 TypeScript 文件中某个变量直接进行绑定,后续这个变量值变化时,Angular 会自动去更新相应 DOM 的属性,也就是说,原本那些操纵 DOM 的代码,Angular 帮我们做了,我们不用再自己去处理了
。
另外,注意,以上出现的 TypeScript 的描述,你可以理解成官网中的组件,我之所以不想用组件的方式来进行描述,是因为,我觉得,组件是一个整体,它本身就包括了 TypeScript 文件和模板文件,所以
官网中说的组件和模板的交互
,我觉得,换成
组件中的 TypeScript 文件与模板文件的交互更为适合。
当然,这只是我目前阶段的理解。
服务:
服务是一个广义上的概念,通常用来
处理那些跟 UI 交互无关的事情
,比如网络请求的工作等。
所以它也是为组件服务,而且 Angular 有一套依赖注入机制,也就是说,组件只需要告诉 Angular,它需要哪些服务,至于这些服务的实例是什么时候创建,交给谁去管理等这些组件内部都不用自己去处理了。
Angular 会自动创建相关的服务实例,然后在组件适当的时候,将这个实例注入给组件去使用。
在 Angular 中,你可以借助它依赖注入的机制,来让 Angular 帮你去做这些依赖的对象的实例管理的事,如果需要一个全局的单例服务,那么可以将该服务声明成 root 即全局可用;如果需要一个模块内的单例,那么可以在该模块的 providers 中声明该服务;如果需要一个组件自己的实例对象,那么可以在组件的元数据块的 providers 中配置该服务。
总之,就是,跟 UI 交互无关的工作,可以抽到服务中去处理,而该服务实例的管理,交给 Angular 就可以了,组件只需要
告诉 Angular 它需要哪种形式的服务
即可。
那么,
组件是怎么告诉 Angular 的呢?
在 Angular 中,
直接在组件的构造函数的参数中
,
声明某个服务类型的参数即可
。
指令:
指令也是为组件服务的,但是,是在组件的模板文件中来使用。
因为
组件的模板
,
其实就是一份 HTML 文件,基于 HTML 的标签之上,加上一些 Angular 的模板语法,而 Angular 在将这份 HTML 文件代码交给浏览器解析之前,会先自行解析一遍,去将模板中不属于 HTML 的那些语法解析出相应的行为。
而
指令
分为
结构型指令
和
属性型指令
,它们的
区别
,其实就在于,
一个是改变 DOM 的结构
,
一个是改变 DOM 元素的样式
。
所以说,
指令的目的,其实就是简化一些操纵 DOM 的工作
,比如你需要让某些按钮都具有统一的行为和样式,当被点击时先做什么,再做什么。
实现这个,你当然可以在 TypeScript 中去书写这些逻辑,但要应用到每个按钮上,就比较繁琐。
这个时候,就可以将这些工作都封装到指令内部,然后在每个按钮标签上加上该指令,Angular 在解析模板时,发现了这个指令,就会为每个按钮都加上这么一段程序逻辑。
我个人觉得,指令的功能,让我们处理一些相同的行为,可以更好的去封装,减少冗余和繁琐。
当然,上面举的场景,也可以自己封装个按钮组件,然后在其他模板中,不使用原生按钮,而使用封装后的按钮组件,也可以达到目的。
所以,组件其实也是指令的一种,但组件的实现方式会比较重,有时候,只需要封装一些简单的行为逻辑,就可以直接借助指令的方式封装。
指令的原理
也很简单,
在模板中某个元素标签上,添加上某个指令后,解析到这个指令时,会进入这个指令的相关工作,而指令内部,会获取到一个当前指令挂载的元素标签对象,既然都拿到这个对象了,那么,在指令内部想对这个元素做什么,都可以了。
指令还有另一个通途,通常用来扩展原有的功能,因为可能项目中,在模板里使用的组件或者 HTML 元素的标签因为种种原生无权或不方便进行修改,而又想在其基础上扩展一些功能,此时就可以利用指令来实现。
管道:
管道
同样是为组件服务,也同样是在组件的模板文件中来使用。
它的用途,在于,
将数据按照一定的规则进行转换
,比如 Object 对象,转换成 json 格式数据,再比如,long 型的时间,转换成具体的时间日期等等。
Angular 中已经内置了一些管道,也可以自定义管道。
angular项目结构
在利用 Angular Cli 工具生成脚手架时,默认就已经生成了很多配置项,而且此时,项目已经是可以运行的,因为也自动生成了一个根模块和根视图,默认页面是 Angular 的欢迎界面。
angular.json
这是 Angular-CLI 的配置文件,而 Angular-CLI 是自动化的工程构建工具,也就是利用这个工具,可以帮助我们完成很多工作,比如创建项目、创建文件、构建、打包等等。
原本的 HTML、CSS、JavaScript 的前端开发模式,并没有工程的概念,只要用浏览器打开 HTML 文件就能够运行。而 Angular 引入了 TypeScript,Scss 等浏览器并不无法识别的语言,自然,要让浏览器运行 Angular 项目之前,需要进行一次编译,一次转换。
这些工作就可以借助 Angular-CLI 来进行。另外,创建一个模块,创建一个组件,也都可以通过 Angular-CLI 来。
那么,在创建这些文件或者说,打包编译这些项目文件时,该按照怎样的规则,就是参照 angular.json 这份配置文件。
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", // 默认的配置项,比如默认配置了 ng g component 生成组件时应该生成哪些文件等等
"version": 1,
"newProjectRoot": "projects",
"projects": {
"daView": { // 项目的配置
"root": "",
"sourceRoot": "src", // 源代码路基
"projectType": "application", // 项目的类型,是应用还是三方库(library)
"prefix": "app", // 利用命令生成 component 和 directive 的前缀
"schematics": {}, // 替换掉第一行的 schema.json 中的一些默认配置项,不如创建组件时,不要生成spec文件
"architect": { // 执行一些构造工作时的配置
"build": { // 执行 ng build 时的一些配置项
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/daView", // 编译后的文件输出的位置
"index": "src/index.html", // 构建所需的模板 Index.html
"main": "src/main.ts", // 构建所需的文件
"polyfills": "src/polyfills.ts", // 构建所需的文件
"tsConfig": "src/tsconfig.app.json", // 对 typescript 编译的配置文件
"assets": [ // 构建所需的资源
"src/favicon.ico",
"src/assets"
],
"styles": [ // 构建所需的样式文件,可以是 scss
"src/styles.css"
],
"scripts": [] // 构建所需的三方库,比如 jQuery
},
"configurations": {/*...*/}
},
"serve": {/*...*/}, // 执行 ng serve 时的一些配置项
"extract-i18n": {/*...*/},
"test": {/*...*/},
"lint": {/*...*/}
}
}
},
"daView-e2e": {/*...*/},
"defaultProject": "daView"
}
所以,利用 Angular-CLI 生成的初始项目中,有许多基本的文件,这些文件,基本也都在 angular.json 中被配置使用了,每个配置文件基本都有各自的用途。
比如,tslint 用来配置 lint 检查,tsconfig 用来配置 TypeScript 的编译配置,其他那些 html,css,ts,js 文件基本都是 Angular 项目运行所需的基础文件。
package.json
对于一个工程项目来说,依赖的三方库管理工具也很重要,在 Angular 项目中,是使用 npm 来进行三方库的管理,对应的配置文件就是 package.json。
在这份配置文件中,配置了项目所需要的三方库,npm 会自动去将这些三方库下载到
node_modules
目录中。然后,再去将一些需要一起打包的三方库在 angular.json 中进行配置。
app/src 源码
以上就是利用 Angular-CLI 创建项目生成的初始架构中各个文件的大概用途,下面讲讲 Angular 项目的大概运行流程。
在 src 中的
index.html
文件就是单页应用的页面文件,里面的 body 标签内,自动加入了一行根视图的组件:
<app-root></app-root>
就是根组件 AppComponent (自动生成的)的组件标签,当 Angular 在 HTML 文件中发现有组件标签时,就会去加载该组件所属的模块,并去解析组件的模板文件,将其嵌入到 HTML 文件的组件标签
看一下自动生成的根模块的部分内容:
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
//app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'daView';
}
app.module.ts
文件用 @NgModule 表示该文件角色是模块,并在内部配置了它的组件 AppComponent,这样 AppComponent 组件就只属于该模块了,并能够在该模块内的其他组件中被使用。
另外,由于该模块是根模块,所以还需要配置 bootstrap,设置应用的根视图,这个配置需要和
index.html
里的 body 标签内的根视图组件是同一个组件,否则运行时就会报错了。
当项目中模块多了的时候,各模块之间基本是通过路由或者组件来进行相互关联。
比如,我们新创建个 Home 模块,然后在根模块中创建个 app-routing 路由配置文件:
//app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: 'home', loadChildren: './home/home.module#HomeModule'
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
然后在 app.module.ts 的 imports 中将该路由配置导入,这样当路由到 home 时,会去加载 home 模块,然后看看 home 模块的路由配置:
//home-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {HomeComponent} from './home.component';
import {HomeCenterComponent} from './component/home-center.component';
const routes: Routes = [
{
path: '',
children: [
{
path: '', component: HomeCenterComponent
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule { }
home 模块的默认视图为空,但交由其子视图来控制,所以,当导航到 home 时,home 模块会去加载它内部的 HomeCenterComponent 组件。
以上,是当项目中有多模块时,我的处理方式。
当按照这种方式来实现时,对于了解一个 Angular,就有一定的规律可循了:
- 先找根视图组件,然后确认根视图组件中的 router-outlet 标签的区域,因为这个区域展示的就是由根模块路由导航到的新的组件内容;
- 去根模块的配置中找到根模块的路由配置表,来查看第一个层级的路由分别对应哪些模块;
- 去这些相应的模块中,查看它们各自内部的路由配置表,来确定各自模块的默认视图组件是哪个,下一个层级的各个路由所对应的视图组件;
- 这样,一个页面的组件层次结构就能够很快的理清。