이 글은 여러분에게 Angular에 대한 심층적인 이해를 제공하고 가장 포괄적인 Angular 초보자 가이드를 공유할 것입니다. 이 글이 모든 사람에게 도움이 되기를 바랍니다.
Angular는 TypeScript를 기반으로 Google에서 개발한 오픈 소스 웹 프런트 엔드 프레임워크입니다. [관련 튜토리얼 추천: "angular tutorial"]
and React Vue에 비해 Angular는 중견기업 및 대기업 수준의 프로젝트에 더 적합합니다.
ng new 새 프로젝트 만들기
ng 봉사 시작 프로젝트
ng 빌드 패키징 프로젝트
ng 생성 모듈/컴포넌트/서비스 생성
main.ts => ts => app.comComponent.ts => index.html => app.comComponent.html
|-- project |-- .editorconfig // 用于在不同编辑器中统一代码风格 |-- .gitignore // git中的忽略文件列表 |-- README.md // markdown格式的说明文件 |-- angular.json // angular的配置文件 |-- browserslist // 用于配置浏览器兼容性的文件 |-- karma.conf.js // 自动化测试框架Karma的配置文件 |-- package-lock.json // 依赖包版本锁定文件 |-- package.json // npm的包定义文件 |-- tsconfig.app.json // 用于app项目的ts配置文件 |-- tsconfig.json // 整个工作区的ts配置文件 |-- tsconfig.spec.json // 用于测试的ts配置文件 |-- tslint.json // ts的代码静态扫描配置 |-- e2e // 自动化集成测试目录 |-- src // 源代码目录 |-- src // 源代码目录 |-- favicon.ico // 收藏图标 |-- index.html // 单页应用到宿主HTML |-- main.ts // 入口 ts 文件 |-- polyfills.ts // 用于不同浏览器的兼容脚本加载 |-- styles.css // 整个项目的全局css |-- test.ts // 测试入口 |-- app // 工程源码目录 |-- assets // 资源目录 |-- environments // 环境配置 |-- environments.prod.ts // 生产环境 |-- environments.ts // 开发环境
AppModule 정의 이 루트 모듈은 Angular에 애플리케이션을 어셈블하는 방법을 알려줍니다.
@NgModule은 Angular에게 애플리케이션을 컴파일하고 시작하는 방법을 알려주는 메타데이터 개체를 허용합니다.
디자인 의도
메타데이터
일반적으로 사용되는 모듈에는 코어 모듈, 일반 모듈, 폼 모듈, 네트워크 모듈 등이 있습니다.
프로젝트가 상대적으로 작기 때문에 커스텀 모듈을 사용할 필요는 없습니다
그러나 프로젝트가 매우 큰 경우 때로는 모든 구성 요소를 루트 모듈에 마운트하는 것이 적절하지 않습니다
그래서 커스텀 모듈을 사용하여 프로젝트를 구성할 수 있으며, 경로의 지연 로딩은 사용자 정의 모듈을 통해 달성할 수 있습니다
다른 모듈을 가져올 때 모듈 사용 목적을 알아야 합니다.
모든 필수 모듈에서 가져오기 필요
CommonModule
: 기본 제공 바인딩, *ngIf, *ngFor 등의 명령어는 기본적으로 모든 모듈에서 import가 필요합니다CommonModule
: 提供绑定、*ngIf 和 *ngFor 等基础指令,基本上每个模块都需要导入它FormsModule / ReactiveFormsModule
: 表单模块需要在每个需要的模块导入只在根模块导入一次的
HttpClientModule / BrowerAnimationsModule NoopAnimationsModule
FormsModule / ReactiveFormsModule
: 모든 필수 모듈에서 양식 모듈을 import해야 합니다HttpClientModule / BrowerAnimationsModule NoopAnimationsModule
서비스만 제공하는 모듈
🎜🎜🎜🎜Angular 컴포넌트🎜🎜🎜🎜🎜NgModule
才能被其他组件或应用使用@NgModule
元数据的 declarations
字段中引用@Component({ selector: 'app-xxx', templateUrl: 'XXX', styleUrls: ['XXX'], encapsulation:ViewEncapsulation.Emulated // 不写则默认该值,表示该组件样式只作用于组件本身,不影响全局样式,在 head 中生成单独的 style 标签 })
数据绑定 {{data}}
属性绑定 [id]="id"
,其中[class.样式类名]=“判断表达式”
是在应用单个class
样式时的常用技巧
事件绑定 (keyup)="keyUpFn($event)"
样式绑定可以用 :host
这样一个伪类选择器,绑定的样式作用于组件本身
双向数据绑定 [(ngModel)]
// 注意引入:FormsModule import { FormsModule } from '@angular/forms'; <input type="text" [(ngModel)]="inputValue"/> {{inputValue}} // 其实是一个语法糖 [ngModel]="username" (ngModelChange)="username = $event"
脏值检测:当数据改变时更新视图(DOM)
如何进行检测:检测两个状态值(当前状态和新状态)
何时触发脏值检测:浏览器事件(click
、mouseover
、keyup
等)、setTimeout()
或setInterval()
、HTTP请求
Angular 有两种变更检测策略:Default
和 OnPush
可以通过在@Component
元数据中设置changeDetection: ChangeDetectionStrategy.OnPush
进行切换
Default
:
优点:每一次有异步事件发生,Angular 都会触发变更检测,从根组件开始遍历其子组件,对每一个组件都进行变更检测,对dom进行更新。
缺点:有很多组件状态没有发生变化,无需进行变更检测。如果应用程序中组件越多,性能问题会越来越明显。
OnPush
:
优点:组件的变更检测完全依赖于组件的输入(@Input
),只要输入值不变就不会触发变更检测,也不会对其子组件进行变更检测,在组件很多的时候会有明显的性能提升。
缺点:必须保证输入(@Input
)是不可变的(可以用Immutable.js
解决),每一次输入变化都必须是新的引用。
父组件给子组件传值 @input
父组件不仅可以给子组件传递简单的数据,还可把自己的方法以及整个父组件传给子组件。
// 父组件调用子组件的时候传入数据 <app-header [msg]="msg"></app-header> // 子组件引入 Input 模块 import { Component, OnInit ,Input } from '@angular/core'; // 子组件中 @Input 装饰器接收父组件传过来的数据 export class HeaderComponent implements OnInit { @Input() msg:string constructor() { } ngOnInit() { } } // 子组件中使用父组件的数据 <h2>这是头部组件--{{msg}}</h2>
**子组件触发父组件的方法 @Output **
// 子组件引入 Output 和 EventEmitter import { Component,OnInit,Input,Output,EventEmitter} from '@angular/core'; // 子组件中实例化 EventEmitter // 用 EventEmitter 和 @Output 装饰器配合使用 <string> 指定类型变量 @Output() private outer=new EventEmitter<string>(); // 子组件通过 EventEmitter 对象 outer 实例广播数据 sendParent(){ this.outer.emit('msg from child') } // 父组件调用子组件的时候,定义接收事件,outer 就是子组件的 EventEmitter 对象 outer <app-header (outer)="runParent($event)"></app-header> // 父组件接收到数据会调用自己的 runParent, 这个时候就能拿到子组件的数据 // 接收子组件传递过来的数据 runParent(msg:string){ alert(msg); }
父组件通过 ViewChild 主动调用子组件DOM和方法
// 给子组件定义一个名称 <app-footer #footerChild></app-footer> // 引入 ViewChild import { Component, OnInit ,ViewChild} from '@angular/core'; // ViewChild 和子组件关联起来 @ViewChild('footerChild') footer; // 调用子组件 run(){ this.footer.footerRun(); }
由于组件过度嵌套会导致数据冗余和事件传递,因此引入投影组件的概念
投影组件 ng-content
作为一个容器组件使用
主要用于组件动态内容的渲染,而这些内容没有复杂的业务逻辑,也不需要重用,只是一小部分 HTML 片段
使用 ng-content
指令将父组件模板中的任意片段投影到它的子组件上
组件里面的 ng-content
部分可以被组件外部包裹的元素替代
// 表现形式: <ng-content select="样式类/HTML标签/指令"></ng-content> <ng-content select="[appGridItem]"></ng-content>
select
表明包含 appGridItem
的指令的元素才能投影穿透过来
指令可以理解为没有模版的组件,它需要一个宿主元素(Host)
推荐使用方括号 [] 指定 Selector,使它变成一个属性
@Directive({ selector: '[appGridItem]' })
NgClass
ngClass
是自由度和拓展性最强的样式绑定方式
<div [ngClass]="{'red': true, 'blue': false}"> 这是一个 div </div>
NgStyle
ngStyle
由于是嵌入式样式,因此可能会覆盖掉其他样式,需谨慎
<div>你好 ngStyle</div>
NgModel
// 注意引入:FormsModule import { FormsModule } from '@angular/forms'; <input type="text" [(ngModel)]="inputValue"/> {{inputValue}}
ngIf
ngIf 根据表达式是否成立,决定是否展示 DOM 标签
<p> 3">这是 ngIF 判断是否显示</p>
ngIf else
<div>这是 ngIF 内容</div> <ng-template> <h2>这是 else 内容</h2> </ng-template> // 结构性指令都依赖于 ng-template,*ngIf 实际上就是 ng-template 指令的 [ngIf] 属性。
ngFor
<ul> <li *ngFor="let item of list;let i = index;"> {{item}} --{{i}} </li> </ul>
ngSwitch
<ul [ngSwitch]="score"> <li *ngSwitchCase="1">已支付</li> <li *ngSwitchCase="2">已确认</li> <li *ngSwitchCase="3">已发货</li> <li *ngSwitchDefault>已失效</li> </ul>
@HostBinding
绑定宿主的属性或者样式
@HostBinding('style.display') display = "grid"; // 用样式绑定代替rd2的 this.setStyle('display','grid');
@HostListener
绑定宿主的事件
@HostListener('click',['$event.target']) // 第一个参数是事件名,第二个是事件携带参数
生命周期函数通俗的讲就是组件创建、组件更新、组件销毁的时候会触发的一系列的方法
当 Angular 使用构造函数新建一个组件或指令后,就会按下面规定的顺序在特定时刻调用生命周期钩子
constructor :构造函数永远首先被调用,一般用于变量初始化以及类实例化
ngOnChanges :被绑定的输入属性变化时被调用,首次调用一定在 ngOnInit 之前。输入属性发生变化是触发,但组件内部改变输入属性是不会触发的。注意:如果组件没有输入,或者使用它时没有提供任何输入,那么框架就不会调用 ngOnChanges
ngOnInit :组件初始化时被调用,在第一轮 ngOnChanges 完成之后调用,只调用一次。使用 ngOnInit 可以在构造函数之后马上执行复杂的初始化逻辑,同时在 Angular 设置完输入属性之后,可以很安全的对该组件进行构建
ngDoCheck :脏值检测时调用,在变更检测周期中 ngOnChanges 和 ngOnInit 之后
ngAfterContentInit :内容投影ng-content完成时调用,只在第一次 ngDoCheck 之后调用
ngAfterContentChecked: 每次完成被投影组件内容的变更检测之后调用(多次)
ngAfterViewInit :组件视图及子视图初始化完成时调用,只在第一次 ngAfterContentChecked 调用一次
ngAfterViewChecked: 检测组件视图及子视图变化之后调用(多次)
ngOnDestroy 当组件销毁时调用,可以反订阅可观察对象和分离事件处理器,以防内存泄漏
路由(导航)本质上是切换视图的一种机制,路由的导航URL并不真实存在
Angular 的路由借鉴了浏览器URL变化导致页面切换的机制
Angular 是单页程序,路由显示的路径不过是一种保存路由状态的机制,这个路径在 web 服务器上不存在
/** * 在功能模块中定义子路由后,只要导入该模块,等同于在根路由中直接定义 * 也就是说在 AppModule 中导入 HomeModule 的时候, * 由于 HomeModule 中导入了 HomeRouting Module * 在 HomeRoutingModule 中定义的路由会合并到根路由表 * 相当于直接在根模块中定义下面的数组。 * const routes = [{ * path: 'home', * component: HomeContainerComponent * }] */ const routes: Routes = [ {path: 'home', component: HomeComponent}, {path: 'news', component: NewsComponent}, {path: 'newscontent/:id', component: NewscontentComponent}, // 配置动态路由 { path: '', redirectTo: '/home', // 重定向 pathMatch: 'full' }, //匹配不到路由的时候加载的组件 或者跳转的路由 { path: '**', /*任意的路由*/ // component:HomeComponent redirectTo:'home' } ] @NgModule({ /** * 根路由使用 `RouterModule.forRoot(routes)` 形式。 * 而功能模块中的路由模块使用 `outerModule.forChild(routes)` 形式。 * 启用路由的 debug 跟踪模式,需要在根模块中设置 `enableTracing: true` */ imports: [RouterModule.forRoot(routes, { enableTracing: true })], exports: [RouterModule] }) export class AppRoutingModule { }
找到 app.component.html
根组件模板,配置 router-outlet
通过模版属性访问路由,即路由链接 routerLink
<h1> <a [routerLink]="['/home']">首页</a> <a [routerLink]="['/home',tab.link]">首页</a><!-- 路径参数 --> <a [routerLink]="['/home',tab.link,{name:'val1'}]">首页</a> <!-- 路径对象参数 --> <a [routerLink]="['/home']" [queryParams]="{name:'val1'}">首页</a> <!-- 查询参数 --> </h1> <router-outlet></router-outlet> <!-- 路由插座,占位标签 --> <!-- 路由显示的内容是插入到 router-outlet 的同级的下方节点 而不是在 router-outlet 中包含 --> <!-- 当事件处理或者达到某个条件时,可以使用手动跳转 this.router.navigate(['home']); this.router.navigate(['home',tab.link]); this.router.navigate(['home',tab.link,{name:'val1'}]); this.router.navigate(['home'],{queryParams:{name:'val1'}}); -->
控制路由激活状态的样式 routerLinkActive
<h1> <a routerLink="/home" routerLinkActive="active">首页</a> <a routerLink="/news" routerLinkActive="active">新闻</a> </h1> <h1> <a [routerLink]="[ '/home' ]" routerLinkActive="active">首页</a> <a [routerLink]="[ '/news' ]" routerLinkActive="active">新闻</a> </h1> .active{ color:red; }
路径参数读取
this.route.paramsMap.subscribe(params => {...})
查询参数读取
this.route.queryParamsMap.subscribe(params => {...})
路由传递一个参数及其接收方法:
传递参数:path:’info/:id’
接收参数:
constructor(private routerInfo: ActivatedRoute){} ngOnInit(){ this.routerInfo.snapshot.params['id'] }
路由传递多个参数及其接收方法:
传递:[queryParams]=‘{id:1,name:‘crm’}’
接收参数:
constructor(private routerInfo: ActivatedRoute){} ngOnInit(){ this.routerInfo.snapshot.params['id'] this.routerInfo.snapshot.params['name'] }
懒加载子模块,子模块需要配置路由设置启动子模块 loadChildren
const routes: Routes = [ {path:'user',loadChildren:'./module/user/user.module#UserModule' }, {path:'product',loadChildren:'./module/product/product.module#ProductModule'}, {path:'article',loadChildren:'./module/article/article.module#ArticleModule'}, {path:'**',redirectTo:'user'} ]; // 上面好像会报错 Error find module // 配置懒加载 const routes: Routes = [ {path:'user',loadChildren:()=>import('./module/user/user.module').then(mod=>mod.UserModule)}, {path:'article',loadChildren:()=>import('./module/article/article.module').then(mod=>mod.ArticleModule)}, {path:'product',loadChildren:()=>import('./module/product/product.module').then(mod=>mod.ProductModule)}, {path:'**',redirectTo:'user'} ];
组件不应该直接获取或保存数据,应该聚焦于展示数据,而把数据访问的职责委托给某个服务
获取数据和视图展示应该相分离,获取数据的方法应该放在服务中
类似 VueX,全局的共享数据(通用数据)及非父子组件传值、共享数据放在服务中
组件之间相互调用各组件里定义的方法
多个组件都用的方法(例如数据缓存的方法)放在服务(service)里
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class HeroService { aa = 'abc'; constructor(){ } ngOnInit(){ } } import { HeroService } from '../../../services/hero/hero.service'; export class AComponent implements OnInit{ constructor(private heroService : HeroService) {} //实例化 ngOnInit(){ console.log(this.heroService.aa) } }
在 Angular 中,要把一个类定义为服务,就要用 @Injectable()
装饰器来提供元数据,以便让 Angular 把它作为依赖注入到组件中。
同样,也要使用 @Injectable ()
装饰器来表明一个组件或其它类(比如另一个服务、管道或 NgModule
)拥有一个依赖。
@Injectable ()
装饰器把这个服务类标记为依赖注入系统的参与者之一,它是每个 Angular 服务定义中的基本要素。
在未配置好 Angular 的依赖注入器时,Angular 实际上无法将它注入到任何位置。
@Injectable ()
装饰器具有一个名叫 providedIn
的元数据选项,providedIn
设置为 'root'
,即根组件中,那么该服务就可以在整个应用程序中使用了。
providedIn
提供这些值:‘root'
、'platform'
、'any'
、null
对于要用到的任何服务,必须至少注册一个提供者。
服务可以在自己的元数据中把自己注册为提供者,可以让自己随处可用,也可以为特定的模块或组件注册提供者。
要注册提供者,就要在服务的 @Injectable ()
装饰器中提供它的元数据,或者在 @NgModule ()
或 @Component ()
的元数据中。
在组件中提供服务时,还可以使用 viewProdivers
,viewProviders
对子组件树不可见
可以使用不同层级的提供者来配置注入器,也表示该服务的作用范围
Angular 创建服务默认采用的方式:在服务本身的 @Injectable () 装饰器中
该服务只在某服务中使用:在 NgModule 的 @NgModule () 装饰器中
该服务在某组件中使用:在组件的 @Component () 装饰器中
在项目中,有人提供服务,有人消耗服务,而依赖注入的机制提供了中间的接口,并替消费者创建并初始化处理
消费者只需要知道拿到的是完整可用的服务就好,至于这个服务内部的实现,甚至是它又依赖了怎样的其他服务,都不需要关注。
Angular 通过 service
共享状态,而这些管理状态和数据的服务便是通过依赖注入的方式进行处理的
Angular 的 service
的本质就是依赖注入,将service
作为一个Injector
注入到component
中
归根到底,很多时候我们创建服务,是为了维护公用的状态和数据,通过依赖注入的方式来规定哪些组件可共享
正是因为 Angular 提供的这种依赖注入机制,才能在构造函数中直接声明实例化
constructor(private heroService : HeroService) {} // 依赖注入
先看一下 Angular 中 TS 单文件的注入
// 首先写 @injectable 我们需要注入的东西,比如说 product @Injectable() class Product { constructor( private name: string, private color: string, private price: number, ) { } } class PurchaseOrder { constructor(private product: Product){ } } export class HomeGrandComponent implements OnInit { constructor() { } ngOnInit() { // 构造一个 injector 用 create 方法 里面 providers 数组中写我们需要构造的东西 const injector = Injector.create({ providers: [ { provide: Product, // 构造 Product 在 useFactory 中就会把上面定义的 product 注入到这里 useFactory: () => { return new Product('大米手机', '黑色', 2999); }, deps: [] }, { provide: PurchaseOrder, deps: [Product] }, { provide: token, useValue: { baseUrl: 'http://local.dev' } } ] }); console.log('injector获取product', injector.get(PurchaseOrder).getProduct); console.log(injector.get(token)); }
再看一下Angular 中 module 模块的注入
// .service.ts 中 @Injectable () 依赖注入 @Injectable() export class HomeService { imageSliders: ImageSlider[] = [ { imgUrl:'', link: '', caption: '' } ] getBanners() { return this.imageSliders; } } // 使用模块对应的.module.ts 中 @NgModule({ declarations: [ HomeDetailComponent, ], providers:[HomeService], // 在 providers 直接写对应服务,直接将服务注入模块 imports: [SharedModule, HomeRoutingModule] })
不管是在组件内还是在模块内,我们使用
providers
的时候,就是进行了一次依赖注入的注册和初始化其实模块类(
NgModule
)也和组件一样,在依赖注入中是一个注入器,作为容器提供依赖注入的接口
NgModule
使我们不需要在一个组件中注入另一个组件,通过模块类(NgModule
)可以进行获取和共享
Angular
管道是编写可以在 HTML 组件中声明的显示值转换的方法
管道将数据作为输入并将其转换为所需的输出
管道其实就是过滤器,用来转换数据然后显示给用户
管道将整数、字符串、数组和日期作为输入,用 |
分隔,然后根据需要转换格式,并在浏览器中显示出来
在插值表达式中,可以定义管道并根据情况使用
在 Angular
应用程序中可以使用许多类型的管道
String
-> String
Number
-> String
Object
-> String
Tools
使用方法
<div>{{ 'Angular' | uppercase }}</div> <!-- Output: ANGULAR --> <div>{{ data | date:'yyyy-MM-dd' }}</div> <!-- Output: 2022-05-17 --> <div>{{ { name: 'ccc' } | json }}</div> <!-- Output: { "name": "ccc" } --> <!-- 管道可以接收任意数量的参数,使用方式是在管道名称后面添加: 和参数值 若需要传递多个参数则参数之间用冒号隔开 --> <!-- 可以将多个管道连接在一起,组成管道链对数据进行处理 --> <div>{{ 'ccc' | slice:0:1 | uppercase }}</div>
管道本质上就是个类,在这个类里面去实现 PipeTransfrom
接口的 transform
这个方法
@Pipe
装饰器定义 Pipe
的 metadata
信息,如 Pipe
的名称 - 即 name
属性PipeTransform
接口中定义的 transform
方法// 引入PipeTransform是为了继承transform方法 import { Pipe, PipeTransform } form '@angular/core'; // name属性值惯用小驼峰写法, name的值为html中 | 后面的名称 @Pipe({ name: 'sexReform' }) export class SexReformPipe implements PipeTransform { transform(value: string, args?: any): string { // value的值为html中 | 前面传入的值, args为名称后传入的参数 switch(value){ case 'male': return '男'; case 'female': return '女'; default: return '雌雄同体'; } } } // demo.component.ts export Class DemoComponent { sexValue = 'female'; } // demo.component.html <span>{{ sexValue | sexReform }}</span> // 浏览器输出 女 // 管道可以链式使用,还可以传参 <span> {{date | date: 'fullDate' | uppercase}} </span> // 每一个自定义管道都需要实现 PipeTransform 接口,这个接口非常简单,只需要实现 transform 方法即可。 // transform()方法参数格式 - transform(value: string, args1: any, args2?: any): // value为传入的值(即为需要用此管道处理的值, | 前面的值); // args 为传入的参数(?:代表可选); // html 中使用管道格式 - {{ 数据 | 管道名 : 参数1 : 参数2 }} // 与 component 一样,pipe 需要先在 declarations 数组中声明后使用
ngAfterViewInit(){ var boxDom:any=document.getElementById('box'); boxDom.style.color='red'; }
ElementRef
是对视图中某个原生元素的包装类
因为 DOM 元素不是 Angular 中的类,所以需要一个包装类以便在 Angular 中使用和标识其类型
ElementRef
的背后是一个可渲染的具体元素。在浏览器中,它通常是一个 DOM 元素
class ElementRef<T> { constructor(nativeElement: T) nativeElement: T //背后的原生元素,如果不支持直接访问原生元素,则为 null(比如:在 Web Worker 环境下运行此应用的时候)。 }
当需要直接访问 DOM 时,请把本 API 作为最后选择 。优先使用 Angular 提供的模板和数据绑定机制
如果依赖直接访问 DOM 的方式,就可能在应用和渲染层之间产生紧耦合。这将导致无法分开两者,也就无法将应用发布到 Web Worker 中
使用模板和数据绑定机制,使用 @viewChild
// 模版中给 DOM 起一个引用名字,以便可以在组件类或模版中进行引用 <div #myattr></div> // 引入 ViewChild import { ViewChild,ElementRef } from '@angular/core'; // 用 ViewChild 绑定 DOM @ViewChild('myattr') myattr: ElementRef; // 在 ngAfterViewInit 生命周期函数里可以很安全的获取 ViewChild 引用的 DOM ngAfterViewInit(){ let attrEl = this.myattr.nativeElement; }
父组件中可以通过 ViewChild
调用子组件的方法
// 给子组件定义一个名称 <app-footer #footerChild></app-footer> // 引入 ViewChild import { Component, OnInit ,ViewChild} from '@angular/core'; // ViewChild 和子组件关联起来 // 如果想引用模版中的 Angular 组件,ViewChild 中可以使用引用名,也可以使用组件类型 @ViewChild('footerChild') footer; // @ViewChild('imageSlider', { static: true }) // static指定是动态还是静态,在*ngFor或者*ngIf中是动态,否则即为静态,动态为 true // 调用子组件 run(){ this.footer.footerRun(); }
引用多个模版元素,可以用@ViewChildren
,在ViewChildren
中可以使用引用名
或者使用 Angular 组件/指令的类型,声明类型为 QueryList>
<img #img *ngFor="let slider of sliders" [src]="slider.imgUrl" [alt]="slider.capiton" > // 使用 ViewChildren 引用获取 @ViewChildren(’img‘); // 使用类型引用获取 imgs: QueryList<ElementRef>;
Renderer2
是 Angular 提供的操作 element
的抽象类,使用该类提供的方法,能够实现在不直接接触 DOM 的情况下操作页面上的元素。
Renderer2
的常用方法:
addClass
/removeClass
在 directive
的宿主元素添加或删除 class
import { Directive, Renderer2, ElementRef, OnInit } from '@angular/core'; @Directive({ selector: '[testRenderer2]' }) export class TestRenderer2Directive implements OnInit { constructor(private renderer: Renderer2, private el: ElementRef) {} // 实例化 ngOnInit() { this.renderer.addClass(this.el.nativeElement, 'test-renderer2'); // this.renderer.removeClass(this.el.nativeElement, 'old-class'); } }
createElement
/appendChild
/createText
创建 DIV 元素,插入文本内容,并将其挂载到宿主元素上import { Directive, Renderer2, ElementRef, OnInit } from '@angular/core'; constructor(private renderer: Renderer2, private el: ElementRef) {} ngOnInit() { const div = this.renderer.createElement('div'); const text = this.renderer.createText('Hello world!'); this.renderer.appendChild(div, text); this.renderer.appendChild(this.el.nativeElement, div); }
setAttribute
/removeAttribute
在宿主元素上添加或删除 attribute
import { Directive, Renderer2, ElementRef, OnInit } from '@angular/core'; constructor(private renderer: Renderer2, private el: ElementRef) {} ngOnInit() { this.renderer.setAttribute(this.el.nativeElement, 'aria-hidden', 'true'); }
setStyle
/removeStyle
在宿主元素上添加 inline-style
import { Directive, Renderer2, ElementRef, OnInit } from '@angular/core'; constructor(private renderer: Renderer2, private el: ElementRef) {} ngOnInit() { this.renderer.setStyle( this.el.nativeElement, 'border-left', '2px dashed olive' ); }
移除 inline-style
:
constructor(private renderer: Renderer2, private el: ElementRef) {} ngOnInit() { this.renderer.removeStyle(this.el.nativeElement, 'border-left'); }
setProperty
设置宿主元素的 property
的值constructor(private renderer: Renderer2, private el: ElementRef) {} ngOnInit() { this.renderer.setProperty(this.el.nativeElement, 'alt', 'Cute alligator'); }
直接操作DOM,
Angular
不推荐。尽量采用@viewChild
和renderer2
组合,Angular
推荐使用constructor(private rd2: Renderer2) {}
依赖注入,
import { Component, OnInit, Renderer2, ViewChild, } from '@angular/core'; import { AboxItemComponent } from './abox-item/abox-item.component'; @Component({ selector: 'app-abox', templateUrl: './abox.component.html', styleUrls: ['./abox.component.less'], }) export class AboxComponent implements OnInit { private container; activeIndex: number; @ViewChild('containers') containers: any; constructor(private rd2: Renderer2) {} ngOnInit(): void {} ngAfterViewInit(): void { this.container = this.containers.nativeElement; this.initCarouselWidth(); } initCarouselWidth() { this.rd2.setStyle(this.container, 'width', '100px'); } }
需导入 HttpClientModule
,只在根模块中导入,并且整个应用只需导入一次,不用在其他模块导入
在构造函数中注入HttpClient
,get/post
方法对应HTTP方法,这些方法是泛型的,可以直接把返回的JSON转换成对应类型。若是不规范的请求,使用request
方法
返回的值是 Observable
,必须订阅才会发送请求,否则不会发送
get 请求数据
// 在 app.module.ts 中引入 HttpClientModule 并注入 import {HttpClientModule} from '@angular/common/http'; imports: [ BrowserModule, HttpClientModule ] // 在用到的地方引入 HttpClient 并在构造函数声明 import {HttpClient} from "@angular/common/http"; constructor(private http: HttpClient,private cd: ChangeDetectorRef) { } // 依赖注入 // get 请求数据 var api = "http://baidu.com/api/productlist"; this.http.get(api).subscribe(response => { console.log(response); this.cd.markForCheck(); // 如果改变了脏值检测的变更原则 changeDetection: ChangeDetectionStrategy.OnPush // 则需要使用 this.cd.markForCheck() 手动提醒 Angular 这里需要进行脏值检测 });
post 提交数据
// 在 app.module.ts 中引入 HttpClientModule 并注入 import {HttpClientModule} from '@angular/common/http'; imports: [ BrowserModule, HttpClientModule ] // 在用到的地方引入 HttpClient 、HttpHeaders 并在构造函数声明 HttpClient import {HttpClient,HttpHeaders} from "@angular/common/http"; constructor(private http:HttpClient) { } // 实例化 // post 提交数据 const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; var api = "http://127.0.0.1:4200/doLogin"; this.http.post(api,{username:'瑞萌萌',age:'22'},httpOptions).subscribe(response => { console.log(response); });
Jsonp请求数据
// 在 app.module.ts 中引入 HttpClientModule、HttpClientJsonpModule 并注入 import {HttpClientModule,HttpClientJsonpModule} from'@angular/common/http'; imports: [ BrowserModule, HttpClientModule, HttpClientJsonpModule ] // 在用到的地方引入 HttpClient 并在构造函数声明 import {HttpClient} from "@angular/common/http"; constructor(private http:HttpClient) { } // 实例化 // jsonp 请求数据 var api = "http://baidu.com/api/productlist"; this.http.jsonp(api,'callback').subscribe(response => { console.log(response); });
Angular 拦截器是 Angular 应用中全局捕获和修改 HTTP 请求和响应的方式,例如携带 Token
和捕获 Error
前提是只能拦截使用 HttpClientModule
发出的请求,如果使用 axios
则拦截不到
创建拦截器
// 使用命令 ng g interceptor name,在这里创建拦截器 ng g interceptor LanJieQi // cli 生成拦截器是没有简写方式的 import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable() export class LanJieQiInterceptor implements HttpInterceptor { constructor() {} // 默认的 intercept() 方法只是单纯的将请求转发给下一个拦截器(如果有),并最终返回 HTTP 响应体的 Observable // request: HttpRequest<unknown> 表示请求对象,包含了请求相关的所有信息,unknown指定请求体body的类型 // next: HttpHandler 请求对象修改完成,将修改后的请求对象通过next中的handle方法传回真正发送请求的方法中 intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { // next 对象表示拦截器链表中的下一个拦截器(在应用中可以设置多个拦截器) return next.handle(request); } }
注入拦截器
// 在 @NgModule 模块中注入拦截器 // 拦截器也是一个由 Angular 依赖注入 (DI) 系统管理的服务,也必须先提供这个拦截器类,才能使用它 // 由于拦截器是 HttpClient 服务的依赖,所以必须在提供 HttpClient 的同一个(或其各级父注入器)注入器中提供这些拦截器 @NgModule({ imports: [ HttpClientModule // others... ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: LanJieQiInterceptor, // multi: true 表明 HTTP_INTERCEPTORS 是一个多重提供者的令牌,表示这个令牌可以注入多个拦截器 multi: true }, ], bootstrap: [AppComponent] }) export class AppModule { }
请求头拦截
@Injectable()export class LanJieQiInterceptor implements HttpInterceptor { constructor() {} intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<httpevent>> { // 为了统一设置请求头,需要修改请求 // 但 HttpRequest 和 HttpResponse 实例的属性却是只读(readonly)的 // 所以修改前需要先 clone 一份,修改这个克隆体后再把它传给 next.handle() let req = request.clone({ setHeaders:{ token:"123456" // 在请求头中增加 token:123456 } // setHeaders 和 headers: request.headers.set('token', '123456') 一致 }) return next.handle(req)// 将修改后的请求返回给应用 }}</httpevent></unknown>
响应捕获
@Injectable() export class LanJieQiInterceptor implements HttpInterceptor { constructor() {} intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { // 为了统一设置请求头,需要修改请求 // 但 HttpRequest 和 HttpResponse 实例的属性却是只读(readonly)的 // 所以修改前需要先 clone 一份,修改这个克隆体后再把它传给 next.handle() let req = request.clone({ setHeaders:{ token:"123456" // 在请求头中增加 token:123456 } // setHeaders 和 headers: request.headers.set('token', '123456') 一致 }) return next.handle(req)// 将修改后的请求返回给应用 } }
如果有多个拦截器,请求顺序是按照配置顺序执行,响应拦截则是相反的顺序
如果提供拦截器的顺序是先 A再 B再 C,那么请求阶段的执行顺序就是 A->B->C,而响应阶段的执行顺序则是 C->B->A
模板驱动表单在往应用中添加简单的表单时非常有用,但是不像响应式表单那么容易扩展
如果有非常基本的表单需求和简单到能用模板管理的逻辑,就使用模板驱动表单
响应式表单和模板驱动表单共享了一些底层构造块:
FormControl
实例用于追踪单个表单控件的值和验证状态
FormGroup
用于追踪一个表单控件组的值和状态
FormArray
用于追踪表单控件数组的值和状态,有长度属性,通常用来代表一个可以增长的字段集合
ControlValueAccessor
用于在 Angular 的FormControl
实例和原生 DOM 元素之间创建一个桥梁
FormControl
和 FormGroup
是 angular 中两个最基本的表单对象
FormControl
代表单一的输入字段,它是 Angular 表单中最小单员,它封装了这些字段的值和状态,比如是否有效、是否脏(被修改过)或是否有错误等
FormGroup
可以为一组 FormControl
提供总包接口(wrapper interface),来管理多个 FormControl
当我们试图从 FormGroup
中获取 value 时,会收到一个 “键值对” 结构的对象
它能让我们从表单中一次性获取全部的值而无需逐一遍历 FormControl
,使用起来相当顺手
FormGroup
和 FormControl
都继承自同一个祖先 AbstractControltractControl
(这是 FormControl
,FormGroup
和 FormArray
的基类)
首先加载 FormsModule
// 先在 NgModule 中导入了 FormsModule 表单库 // FormsModule 为我们提供了一些模板驱动的指令,例如:ngModel、NgForm import { FormsModule } from '@angular/forms'; @NgModule({ declarations: [ FormsDemoApp, DemoFormSku, // ... our declarations here ], imports: [ BrowserModule, FormsModule, ], bootstrap: [ FormsDemoApp ] }) class FormsDemoAppModule {}
接下来创建一个模版表单
<div> <h2>基础表单:商品名称</h2> <form #f="ngForm" (ngSubmit)="onSubmit(f.value)"> <div class="sku"> <label for="skuInput">商品名称:</label> <input type="text" id="skuInput" placeholder="商品名称" name="sku" //使用form时必须定义,可以理解为当前控件的名字 ngModel /> </div> <button>提交</button> </form> </div>
我们导入了 FormsModule
,因此可以在视图中使用 NgForm
了
当这些指令在视图中可用时,它就会被附加到任何能匹配其 selector 的节点上
NgForm
做了一件便利但隐晦的工作:它的选择器包含 form 标签(而不用显式添加 ngForm
属性)
这意味着当导入 FormsModule
时候,NgForm
就会被自动附加到视图中所有的标签上
NgForm
提供了两个重要的功能:
ngForm
的 FormGroup
对象ngSubmit
)<form #f="ngForm" (ngSubmit)="onSubmit(f.value)" > <!-- 这里使用了 #f=“ngForm”,#v=thing 的意思是我们希望在当前视图中创建一个局部变量 这里为视图中的 ngForm 创建了一个别名,并绑定到变量 #f 这个 ngForm 是由 NgForm 指令导出的 ngForm 的类型的对象是 FormGroup 类型的 这意味着可以在视图中把变量 f 当作 FormGroup 使用,而这也正是我们在输出事件 (ngSubmit) 中的使用方法 在表单中绑定 ngSubmit 事件 (ngSubmit)=“onSubmit (f.value)“ (ngSubmit) 来自 NgForm 指令 onSubmit() 将会在组件类中进行定义 f 就是 FormGroup ,而 .value 会以键值对的形式返回 FormGroup 中所有控件的值 总结:当提交表单时,将会以该表单的值作为参数,调用组件实例上的 `onSubmit` 方法 -->
NgModel
会创建一个新的 FormControl
对象,把它自动添加到父 FormGroup
上(这里也就是 form 表单对象)
并把这个 FormControl
对象绑定到一个 DOM 上
也就是说,它会在视图中的 input
标签和 FormControl
对象之间建立关联
这种关联是通过 name
属性建立的,在本例中是 "name"
使用 ngForm
构建 FormControl
和 FormGroup
很方便,但是无法提供定制化选项,因此引入响应式表单
响应式表单提供了一种模型驱动的方式来处理表单输入,其中的值会随时间而变化
使用响应式表单时,通过编写 TypeScript 代码而不是 HTML 代码来创建一个底层的数据模型
在这个模型定义好以后,使用一些特定的指令将模板上的 HTML 元素与底层的数据模型连接在一起
FormBuilder
是一个名副其实的表单构建助手(可以把他看作一个 “工厂” 对象)
在先前的例子中添加一个 FormBuilder
,然后在组件定义类中使用 FormGroup
// 先在 NgModule 中导入了 ReactiveFormsModule 表单库 import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [ FormsModule, ReactiveFormsModule ] }) // 使用 formGroup 和 formControl 指令来构建这个组件,需要导入相应的类 import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; // 在组件类上注入一个从 FormBuilder 类创建的对象实例,并把它赋值给 fb 变量(来自构造函数) export class DemoFormSkuBuilder { myForm: FormGroup; // myForm 是 FormGroup 类型 constructor(fb: FormBuilder) { // FormBuilder 中的 group 方法用于创建一个新的 FormGroup // group 方法的参数是代表组内各个 FormControl 的键值对 this.myForm = fb.group({ // 调用 fb.group () 来创建 FormGroup // 设置一个名为 sku 的控件,控件的默认值为 "123456" 'sku': ['123456'] }); } onSubmit(value: string): void { console.log('submit value:', value); } }
在视图表单中使用自定义的 FormGroup
<h2 class="ui header">Demo Form: Sku with Builder</h2> <!-- 当导入 FormsModule 时,ngForm 就会自动创建它自己的 FormGroup 但这里不希望使用外部的 FormGroup,而是使用 FormBuilder 创建这个 myForm 实例变量 Angular提供了 formGroup 指令,能让我们使用现有的 FormGroup NgForm 不会应用到带 formGroup 属性的节点上 这里我们告诉Angular,想用 myForm 作为这个表单的 FormGroup --> <form [formGroup]="myForm" <label for="skuInput"> SKU </label> <input type="text" id="skuInput" placeholder="SKU" [formControl]="myForm.controls['sku']"> <!-- 将 FormControl 绑定到 input 标签上 : ngModel 会创建一个新的 FormControl 对象并附加到父 FormGroup 中 但在例子中,我们已经用 FormBuilder 创建了自己的 FormControl 要将现有的 FormControl 绑定到 input 上,可以用 formControl 指令 将 input 标签上的 formControl 指令指向 myForm.controls 上现有的 FormControl 控件 sku -->
记住以下两点:
- 如果想隐式创建新的 FormGroup 和 FormControl,使用:ngForm、ngModel
- 如果要绑定一个现有的 FormGroup 和 FormControl,使用:formGroup、formControl
用户输入的数据格式并不总是正确的,如果有人输入错误的数据格式,我们希望给他反馈并阻止他提交表单
因此,我们要用到验证器,由 validators
模块提供
Validators.required
是最简单的验证,表明指定的字段是必填项,否则就认为 FormControl
是无效的
如果 FormGroup
中有一个 FormControl
是无效的, 那整个 FormGroup
都是无效的
要为 FormControl
对象分配一个验证器 ,可以直接把它作为第二个参数传给 FormControl
的构造函数
const control = new FormControl('name', Validators.required); // 在组件定义类中使用 FormBuilder constructor(fb: FormBuilder) { this.myForm = fb.group({ 'name': ['',Validators.required] }); this.name = this.myForm.controls['name']; }
在视图中检查验证器的状态,并据此采取行动
template:`<div> <h2>商品表单:商品名称</h2> <form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm)"> <div> <label for="nameInput">商品名称:</label> <input type="text" id="nameInput" placeholder="请输入名称" [formControl]="myForm.controls['name']" /> <div style="color:red" *ngIf="!name.valid"> 名称无效 </div> <div style="color:red" *ngIf="name.hasError('textinvalid')"> 名称不是以“123”开头 </div> <div *ngIf="name.dirty"> 数据已变动 </div> </div> <div> <label for="codeInput">商品料号:</label> <input type="text" id="codeInput" placeholder="请输入料号" [formControl]="myForm.controls['code']" /> <div style="color:red" *ngIf="myForm.controls.code.hasError('required')" > 该项必填 </div> <div style="color:red" *ngIf="myForm.controls.code.hasError('pattern')" > 只可输入数字和英文 </div> </div> <div style="color:green" *ngIf="myForm.isvalid"> 表单无效 </div> <div style="color:green" *ngIf="myForm.valid"> 表单有效 </div> <button type="submit">提交</button> </form> </div>` export class NonInWarehouseComponent implements OnInit { myForm: FormGroup; name: AbstractControl; constructor(fb: FormBuilder) { this.myForm = fb.group({ name: ['牛奶', Validators.compose([Validators.required, textValidator])], code: ['', [Validators.required, Validators.pattern('^[A-Za-z0-9]*$')]], }); this.name = this.myForm.controls.name; } ngOnInit() { const nameControl = new FormControl('nate'); console.log('nameControl', nameControl); } onSubmit(a: any) { console.log('a', a); } }
内置校验器
Angular 提供了几个内置校验器,下面是比较常用的校验器:
Validators.required
- 表单控件值非空Validators.email
- 表单控件值的格式是 emailValidators.minLength()
- 表单控件值的最小长度Validators.maxLength()
- 表单控件值的最大长度Validators.pattern()
- 表单控件的值需匹配 pattern 对应的模式(正则表达式)自定义验证器
假设我们的 name 有特殊的验证需求,比如 name 必须以 123 作为开始
当输入值(控件的值 control.value
)不是以 123 作为开始时,验证器会返回错误代码 invalidSku
// angular 源代码中实现 Validators.required export class Validators { // 接收一个 AbstractControl 对象作为输入 static required(control: AbstractControl): ValidationErrors | null; } // 当验证器失败时,会返回一个 String Map<string,any> 对象,他的键是” 错误代码 “,它的值是 true export declare type ValidationErrors = { [key: string]: any; }; // 自定义验证器 function textValidator( controls: FormControl // 因为FormControl继承于 AbstractControl 所以也可以写成FormControl对象 ): { [s: string]: boolean; } { if (!controls.value.match(/^123/)) { return { textinvalid: true }; } }
给 FormControl
分配验证器,但是 name 已经有一个验证器了,如何在同一个字段上添加多个验证器
用 Validators.compose
来实现
Validators.compose
把两个验证器包装在一起,我们可以将其赋值给 FormControl
只有当两个验证器都合法时,FormControl
才是合法的
Validators.compose([Validators.required, textValidator]) // 不用compose [Validators.required, textValidator] // 保留 compose 是为了向以前历史版本进行兼容,不用 compose 也可实现
要实现 Angular 动态表单,主要使用 formArray
方法,formArray
生成的实例是一个数组,在这个数组中可以动态的放入 formGroup
和 formControl
,这样便形成了动态表单。
export class ReativeFormsComponent implements OnInit { ngOnInit() { this.addContact() } //动态表单 personMess: FormGroup = new FormGroup({ //生成动态表单数组 contacts: new FormArray([]) }) //获取数组对象 get contacts(){ return this.personMess.get('contacts') as FormArray } //增加一个表单组 addContact(){ let myContact = new FormGroup({ name: new FormControl(), phone: new FormControl() }) this.contacts.push(myContact) } //删除一个表单组 deleteContact(i:number){ this.contacts.removeAt(i) } //提交表单 OnSubmit() { console.log(this.personMess.value) } }
<form [formGroup]="personMess" (submit)="OnSubmit()"> <div formArrayName="contacts"> <!-- 注意:这里遍历的时contacts.controls --> <div *ngFor="let contact of contacts.controls;let i =index" [formGroupName]="i"> <input type="text" formControlName="name"> <input type="text" formControlName="phone"> <button (click)="deleteContact(i)">删除信息</button> </div> </div> <button (click)="addContact()">添加信息</button><br> <input type="submit"> </form>
CDK 是 Component Dev kit
的简称,是 Angular Material 团队在开发 Library 时发现组件有很多相似的地方,最后进行了抽取,提炼出了公共的逻辑,这部分即是 CDK
官方用了一个很形象的比喻:如果组件库是火箭飞船,那么 CDK 就是发动机零件盒
更多编程相关知识,请访问:编程入门!!
위 내용은 Angular에 대해 자세히 알아보기(초보자 가이드)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!