# angular

# 工程总览

# 工程创建

npm i @angular/cli
ng new project_name   ##创建名为project——name的项目
ng new project_name --skip-install --style css --routing false  ##只生成文件不下载依赖
ng serve   ##启动服务
ng build   ##打包
ng lint ##扫描代码
ng g c component/component_name  ## 创建新的组件
ng g m Home --routing##创建路由模块
ng add ng-zorro-antd ##ui库

# 项目目录

--e2e  测试目录
--src  源码目录
----assets  资源目录(存放图片,音频等)
----environments  环境目录
----app  主要逻辑代码目录
--editorconfig  编码风格设置
--angular.json  项目的定义文件
--browserslist  浏览器兼容设置(css)
--karma.conf.js  测试文件
--tsconfig.*  ts配置文件
--tslint.json  静态代码扫描
--polyfills.ts  浏览器兼容设置(js)

# 项目结构

项目是由一个个模块构成的。每个模块包括该模块内容与相对应的路由模块。模块内容包括一个个组件、指令、管道、服务。

模块分为根模块,普通模块,共享模块。

--根模块---组件一、指令一、管道一、服务一
|	   |--组件二、指令二、管道二、服务二
|-根路由模块
|-模块一---组件一、指令一、管道一、服务一
|	   |--组件二、指令二、管道二、服务二
|-负责模块一的路由模块
|-模块二---组件一、指令一、管道一、服务一
|	   |--组件二、指令二、管道二、服务二
|-负责模块二的路由模块
|-共享模块---公用组件一、公用指令一、公用管道一
	     |--公用组件二、公用指令二、公用管道二

以下是模块与组件的关系,组件是最小渲染单元。

//在app.module.ts中,配置所有组件
@NgModule({
  declarations: [//本模块中含有的组件
    AppComponent
  ],
  imports: [//依赖的其他模块
    BrowserModule
  ],
  providers: [],//对内需要暴露的服务
  bootstrap: [AppComponent]//启动的根组件,入口组件
})

而一个项目通常是由多个模板组成的。

# 脏值检测

# 定义

脏值检测是angular的一个基本机制,他保证了当angular的数据变化时可以更新视图。

# 触发脏值检测的条件
  • 浏览器事件,例如click,mouseover,keyup

  • setTimeout和setInterval

  • HTTP请求

# 如何进行脏值检测

他将组件按照嵌套关系生成以可组件树,递归的查询每棵树上的绑定数据的值,如果有变化则更新视图。

在有父组件和子组件的生命周期中

子组<------更新输入型属性
      |---OnInit,DoCheck,OnChanges
      |   更新dom--------->父组件
      |---脏值检测(完成后会引发下面的周期)
      |---AfterViewCheck,AfterViewInit

脏值检测的时候,angular会从跟组件开始循环遍历,查看所有绑定数据的属性值有没有变化,且进行两次。

这也指明了我们不能在AfterViewCheck,AfterViewInit中对绑定的数据进行更改,因为如果在此更改后,脏值检测完成后,触发AfterViewCheck事件更改了值,紧接着触发第二次脏值检测,检测到值不一样了会抛出异常。

# 默认策略与onpush策略

以上说的为默认政策,默认政策时,脏值检测当出发的时候会递归的查询所有组件,有时候可能会影响性能。onpush政策可以使当触发脏值检测的时候,只检查本组件的input变量有没有变化而忽略其他的改变。而input变量有来自于组件外,所以,将组件设置为onpush政策使得该组件变成了一个笨组件(木偶组件),只用于单纯的展示。

要设置成onpush策略我们要在组件逻辑ts中加入如下:

@Component({
	changeDetection:ChangeDetectionStrategy.onpush
})

但需要主义的使,路由的参数变化在默认策略之下,不会销毁这个组件而是重用这个组件,所以ngOnInit(我们默认数据的获取写在这里)只走一遍,这在默认模式下面没有问题,但在onpush模式下会被忽略,从而导致在改变路由参数之后,再改回来的时候,数据会无法获取到。

解决方法,我们将private cd:ChangeDetectorRef引入到类中,通过再函数末尾使用this.cd.markForCheck()来告诉组件此处发生了变化,需要更新数据。

# angular基础知识

# 条件渲染

*ngFor:循环渲染

<ul>
	<li *ngFor="let menu of menus;
		let i=index;  //索引
		let first=first;  //是否为第一个元素
        let last=last;   //是否为最后一个元素
        let odd=odd;   //是否为第奇数个
        let even=even;   //是否为第偶数个
        trackBy:trackElement;   //提升性能,后面借一个表达式或者函数
    " >
        <a herf="#">{{menu.title}}</a>
	</li>
</ul>

*ngIf:判断渲染

<div *ngIf="条件">
    条件为真的内容
</div>

<div *ngIf="条件" else elseContent>
    条件为真的内容
</div>
<ng-template #elseContent>
    条件为假的内容
</ng-template>

# 事件绑定

<a
   herf="#"
   (click)='function(){}'
   >
</a>

# 样式绑定

  • 第一种:当后面的表达式成立,.active样式生效。适用于单个样式

    <a
       herf="#"
       [class.active]="selectedIndex===i"
       >
    </a>
    
  • 第二种:自由度拓展性最强的绑定方法

    <a
       [ngClass]="{'active':true,'first':false}"
    ></a>
    
  • 第三种:嵌入式样式,会覆盖其他样式。常用于动态改变某个css属性。

    <a
    	[ngStyle]="{'color':somecolor,'font-size':somesize}"   
    ></a>
    
  • 第四种:js操作dom去绑定(不推荐使用)

  • 第五种:js使用rd2去绑定

    (private elr:ElementRef,private rd2:Renderer2)=>{
    	this.rd2.setStyle(this.elr.nativeElement,'display','grid')
    }
    

eg:在组件中有一个伪类:host,他的属性定义会应用到他的宿主(也就是他所在的组件本身),而不是模板中的元素。

# 依赖注入

当我们定义一个类之后,想要应用这个类中的方法,就比如通过new一个他的实例然后使用。但是如果该类被标记了注入服务,那么我们在声明他的时候就不用在通过new去创建实例,而是可以直接声明类型后去使用。此时该类也变成了单利模式。

@Injectable()//声明可注入
class Product{
    constructor(private name:string){}
}
@Injectable()
class PurchaseOrder{
    private product:Product;
    constructor(private product:Product){}
}

ngOnInit(){
    const injector=Injector.create({
        //在声明自己函数后,在此将自己的函数创建实例,供别人直接使用
        providers:[//依赖池:在此声明所有需要供用的函数
            {
                provide:Product,//标识符
                useFactory:()=>{
                	return newProduct('haha')
            	}
            },{
                provide:PurchaseOrder,
                useClass:Product,//去new的东西
                deps:[Product]本类中依赖的类
            }
        ]
    })
    console.log(this.injector.get(Product))//可以通过js方式获取该类
}


//此后如果我想用Product类,可以直接通过private product:Product的方式,而不用new了。

在angular中提供了更为简单的方式,不用我们自己通过Injector.create自行构造的,再将类暴露出去之后,可以直接通过在模块中设置providers属性中设置。

@NgModule({
	declarations:[],
	providers:[
		PurchaseOrder,//如果无需工厂设置可以直接只写一个类名
		{
			provide:Product,
			useFactory:()=>{
				return newProduct('haha')
			}
		}
	],
	imports:[]
})

除了在模块中自己设置angular6以后的版本中还有一种更简单的设置方法,直接在服务创建的时候自行声明

@Injectable({
    providedIn: 'root' //root代表跟模块,也可以指定模块
})
export class Product{
    constructor(private name:string){}
}

如果我们要提供的不是一个具体的类,而是一个值,我们可以这么写

const injector=Injector.create({
        providers:[
            {
                provide:'baseUrl',
                useValue:'http://localhost'
            }
        ]
})
this.injector.get('baseUrl')

但是在大型项目中,使用字符串做名字是很危险的,极有可能造成命名重复,所以我们创建一个token来确保他的唯一性

const token=new InjectionToken<string>('baseUrl')
const injector=Injector.create({
        providers:[
            {
                provide:token,
                useValue:'http://localhost'
            }
        ]
})
this.injector.get(token)
//此后如果我想用这个字符串,可以这样声明:
//@Inject(token) private baseurl:string

# 组件

# 组件的封装

  • 通过ng g c component_name生成新的组件

  • 在新生成的组件中创建index.ts并暴露此组件

  • 在使用的组件中引入组件

  • 组件可以使用其ts文件中selector作为名字在其他地方直接使用

  • 结构目录如下

    ---component
    	---index.ts
    	---scrollable
    		---index.ts
    		---scrollable.component.css
    		---scrollable.component.html
    		---scrollable.component.ts
    		---scrollable.component.spec.ts
    	---app.component.css
    	---app.component.html
    	---app.component.ts
    	---app.component.spec.ts
    	---app.module.ts
    
    

# 组件的通讯

# 子组件--->父组件 事件绑定

在子组件中

export class ChildComponent {
    index:-1;
  	@Output() tabSelected=new EventEmitter();
  	handleSelect(index){
        this.tabSelected.emit(index)
  		 	
  	}
}

在父组件中

<child_component (tabSelect)="handleTabSelected($event)"></child_component>

export class AppComponent {
  	handleTabSelected(index){
		console.log(index)
	}
}

# 父组件--->子组件 属性绑定

在父组件中

<!--定义变量-->
export class AppComponent {
  menus=[1,2,3]
}
<!--将menus变量传给子组件-->
<child_component [data]='menus'></child_component>

在子组件中

<!--接收变量-->
export class ChildComponent {
  @Input() data=[]
}
<!--使用变量-->
<ul>
    <li *ngFor="let menu of data;">
		{{menu}}
    </li>
</ul>

# 组件的生命周期

组件在使用前需要继承相关接口

export class ScrollableTabComponent implements OnInit {//需要先继承
  constructor() { }
  ngOnInit() {
  }
}

# 组件的投影

<!--父组件中-->
<child_component>
	<p>
        hello
    </p>
</child_component>
<!--在子组件中,被嵌套的内容会显示在此标签的位置,select选择指定内容显示-->
<ng-content select="样式/html标签/指令"></ng-content>  

# 组件类中引用模板的元素

<!--在模板中,用#做一个唯一标识,一下分别时去一个普通元素,一个自定义元素,多个普通元素-->
<div #helloDiv>
    hello
</div>
<app-image-slider></app-image-slider>
<img #img *ngFor"let a of lists" [src]="a.src"/>
<!--在组件类中使用@ViewChild选择器选择。ElementRef是html的一个包装类-->
export class AppComponent{
	@ViewChild('helloDiv') helloDivRef:ElementRef;
	@ViewChild('ImageSliderComponent') imageSlider:ImageSliderComponent;
	@ViewChildren("img") imgs:QueryList<ElementRef>
    
    handleable(){
		console.log(helloDivRef.nativeElement) //元素本身
	}
}

# 组件的双向绑定

双向绑定最常见的作用就是,在我们更新input输入内容的同时,input的内容会不断更新在另外一处地方。

在此之前我们已经可以达到双向绑定了,通过事件绑定+属性绑定来实现。

<input type="text" [value]="username" (input)="username=$event.target.value">

在angular中还有更简单的方法。[(ngModel)]=“变量”,这其实就是一个语法糖。

首先需要引入FormsModule模块。

然后可以简写为

<input type="text" [(ngModel)]="username"> 

# 模块

# 服务

# 管道

# 内建管道
  • json管道:a|json 转化为json格式

  • data管道:a| data: slice:1:3 切割第一位到第三位

  • date管道:a| date:'MM-dd' 将日期转化为月-日的格式

  • currency管道:a|currency 'CNY' :'symbol':'4.0-2' 使用¥前缀,小数点前4位小数点后两位保留数

  • async管道:异步管道,例子如下

    
    
# 自定义管道

通过ng-pipe在vscode中创建模板

//设置倒计时管道
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'appAgo' })
export class AgoPipe implements PipeTransform {
  transform(value: any): any {
    if (value) {
      const seconds = Math.floor((+new Date() - +new Date(value)) / 1000);
      if (seconds < 29) {
        // 小于 30 秒
        return '刚刚';
      }
      const intervals = {
        : 3600 * 24 * 365,
        : 3600 * 24 * 30,
        : 3600 * 24 * 7,
        : 3600 * 24,
        小时: 3600,
        分钟: 60,
        : 1
      };
      let counter = 0;
      for (const unitName in intervals) {
        if (intervals.hasOwnProperty(unitName)) {
          const unitValue = intervals[unitName];
          counter = Math.floor(seconds / unitValue);
          if (counter > 0) {
            return counter + ' ' + unitName + '前';
          }
        }
      }
    }
    return value;
  }
}

# 指令

指令分为三种:

  • 组件:特殊的指令,带模板的指令
  • 结构型指令(内建指令):改变dom结构。例如ngIf、ngFor、ngSwitch
  • 属性型指令(内建指令):改变宿主行为。例如ngClass、ngStyle、ngModel

除了使用内建指令,我们还可以自己创建一个指令

//创建一个属性指令
import { Directive, HostBinding } from '@angular/core';

/**
 * 指令可以理解为没有模版的组件,它需要一个宿主元素。
 * 推荐使用方括号 [] 指定 Selector,使它变成一个属性。
 */
@Directive({
  selector: '[appGridItem]'
})
export class GridItemDirective implements OnInit{
    contructors(private elr: ElementRef, private renderer: Renderer2){}
    ngOnInit():void {//注意修改属性的方法要放到oninit生命周期里执行 ,而不能放到contructor里面。
        this.rd2.setStyle(this.elr.nativeElement,'display','grid')
    }
}

如果单纯使用指令绑定样式和事件,angular提供了更简单的方法。

由于指令没有模板,所以他要寄宿在一个元素之上(宿主),@HostBinding 可以绑定宿主的属性或者样式,@HostListener可以绑定宿主的事件。

export class GridItemDirective {
	@HostBinding('style.display') display = 'grid';
	@HostBinding('style.place-items') align = 'center';
/*
使用hostbinding绑定后,使得宿主的style.display与display变量相关联,同时变化。
*/
	@HostListener('click',['$event.target'])
   	handleClick(ev){console.log(ev)}
/*
使用hostlistener绑定后,第一个参数是事件类型,第二个参数是数组,写入数据依赖,当宿主触发了对一个事件后,会触发指令中的函数方法
*/
}

# 路由

# 基本形式

通过ng-router-appmodule创建模板

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { RecommendContainerComponent } from './components';

const routes: Routes = [
    {path: '', redirectTo: 'home',pathMatch: 'full'},
    {path: 'home',component: RecommendContainerComponent1},
    {path: '**', component: RecommendContainerComponent2},
];
//''路径表示默认,**表示不识别的路径
@NgModule({
  imports: [RouterModule.forChild(routes)],//在此处导入模块
  exports: [RouterModule]
})
export class RecommendRoutingModule {}


//应用
//<router-outlet></router-outlet>
# 路由的嵌套

当然路由也可以嵌套,加入home下面还有子路由,我们可以再子路由中设置

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeContainerComponent, HomeDetailComponent } from './components';

const routes: Routes = [
  {
    path: 'home',
    component: HomeContainerComponent,
    children: [
      {
        /**
         * 路由节点可以没有 component
         * 一般用于重定向到一个默认子路由
         */
        path: '',
        redirectTo: 'hot',
        pathMatch: 'full'
      },
      {
        /**
         * 路径参数,看起来是 URL 的一部分
         */
        path: ':tabLink',
        component: HomeDetailComponent
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class HomeRoutingModule {}

# 路径参数
拼接在url上面的,叫做路径参数
配置
{path:':tabLink',component:component}
激活
<a [routerLink]="['/home','/spots']"></a>
this.router.navigate(['home','/spots'])
url
http://loaclhost:4200/home/sports
取读
this.route.paramsMap.subcribe(params=>{})
# 路径对象参数
形似`name=val1`的参数

配置

{path:':tabLink',component:component}

激活

<a [routerLink]="['/home','/spots',{name:'val1'}]"></a>

this.router.navigate(['home','/spots',{name:'val1'}])

url

http://loaclhost:4200/home/sports;name=val1

取读

this.route.paramsMap.subcribe(params=>{})
# 路径查询参数
形似`?name=val1`的参数

配置

{path:':tabLink',component:component}

激活

<a [routerLink]="['/home']" [queryParms]={name:'val1'}></a>

this.router.navigate(['home'],{queryParms:{name:'val1'}})

url

http://loaclhost:4200/home/sports;name=val1

取读

this.route.queryParamsMap.subcribe(params=>{})
# 点击触发样式

我们可以通过routerLinkActive属性在标签点击触发路由的时候添加类名

<a 
   [routerLink]="['/home']" 
   [queryParms]={name:'val1'}
   routerLinkActive="active"
></a>

# 网络接口对接

# HttpClient
  • 在根模块导入HttpClientModule
  • 在构造中注入HttpClient
  • 订阅后发送请求
//在根模块引入后,在其他模块中也可以使用
export class HomeService{
    constructor(private http:HttpClient){}
    getData(){
        //通过泛型的形式,将他返回的数据进行格式转换,此处可以是string,也可以是其他自行定义的类型
        return this.http.get<string>(url,{
            params:{icode:123}
        })
    }
}
//http是一种异步操作
//这个时候getData返回的数据类型是
//在使用他的时候为
export class Home{
    constructor(private serve:HomeService){}
    let _data='';
    ngOnInit(){
        this.serve.subscribe(tabs=>{
         	this._data=tabs;   
        })
    }
}
# http拦截器 httpInterceptor

在http获取到数据之前,我们先将其进行一步处理,然后再交给httpclient去处理。在请求和访问的时候都可以进行拦截。

在请求中,例如要为很多http请求统一加入一个请求参数而无需一个一个加。

通过ng-http-interceptor在vscode中生成模板

import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpInterceptor,
  HttpHandler,
  HttpRequest
} from '@angular/common/http';
import { environment } from 'src/environments/environment';

@Injectable()
export class ParamInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // 对请求消息进行处理
    const modifiedReq = req.clone({
      //将原来的req克隆下来,并为他加入查询参数
      setParams: { icode: environment.icode }
    });
    //交给下一个拦截器去处理,因为在angular中可能有多个拦截器
    return next.handle(modifiedReq);
  }
}

完成设置后,我们要在根模块将它引入

@NgModule({
  declarations: [AppComponent],
  imports: [],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: NotificationInterceptor,
      multi: true
    }
  ],
  bootstrap: []
})

同理,我们也可以拦截请求到的数据,也就是respond的数据。

例如我们要设置,当请求成功的时候弹出消息。

import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpInterceptor,
  HttpHandler,
  HttpRequest,
  HttpResponse
} from '@angular/common/http';
import { tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class NotificationInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // 对响应消息进行处理
    return next.handle(req).pipe(
      tap((event: HttpEvent<any>) => {
        if (
          event instanceof HttpResponse &&
          event.status >= 200 &&
          event.status < 300
        ) {
          console.log('[此处假装弹出消息] 请求成功!');
        }
      })
    );
  }
}

完成后同样在根模块引入。

# rxjs 响应式编程类库
# 基础概念

rx要把事件或者数据看成一个流,随着事件流中的元素的变化随之做出相应的动作。

流的种类:无线、有限、单个、空

流的状态:next(得到流的数据之后的处理)、error(发生错误的处理)、complete(无论成功与否,最后都要执行的处理)

此外,所有的流都是异步的。

rx中流的类型用observable类型来表示。

rx最强大的地方之一,就是他拥有方便的操作符。

# 操作符

在路由参数接受的时候,我们运用的就是observable类型,如下例子

paramData;
ngOnInit(){
	this.route.paramMap.subscribe(params=>{
    	this.paramData=params.get('tabLink')
	})
}

使用在模板中

<div *ngIf="paramData==='home'">
    <p>
        hello
    </p>
</div>

上面的代码中,我们获取到params参数中,获取到tabLink属性,我们也可以添加管道,通过操作符改造。

paramData;
ngOnInit(){
    this.route.paramMap
    .pipe(
		filter(params=>params.has('tabLink')),
    	map(params=>params.get('tabLink'))
	)
    .subscribe(tabLink=>{
    	this.paramData=tabLink;
	})
}

应用在模板中同上。

但是在angular中,为了更好的兼容rx,我们可以直接将一个变量声明为流

//当变量作为流的时候,我们习惯在命名上加上一个$用于区分
paramData$:Observable<string>;
ngOnInit(){
    this.paramData=this.route.paramMap
    .pipe(
		filter(params=>params.has('tabLink')),
    	map(params=>params.get('tabLink'))
	)
}

使用在模板中时,由于这是一部操作,所以在一开始的时候paramData和home的数据类型不同,所以我们通过异步管道实现它。

<div *ngIf="(paramData | async)==='home'">
    <p>
        hello
    </p>
</div>