Angular进阶知识回顾

一.注解

1.什么是装饰器(注解)

  • 装饰器(注解)就是一个函数,但它是一个返回函数的函数
  • 如果注解需要传递参数,则在声明注解的时候获取参数并使用即可
  • 它是TypeScript的特性,而不是Angular的特性

2.自定义无参数注解

  • 定义用于添加表情符号的注解类@Emoji

    export function Emoji() {// target表示目标对象的类,key表示对应的属性名return (target:object, key:string) => {let val = target[key];const getter = () =>{return val;}const setter = (value:string) => {val = `?${value}?`;}// JavaScript的定义:将target类中的key属性重新定义Object.defineProperty(target, key, {get: getter,//替换读属性值的方法set: setter,//替换写属性值的方法enumerable: true,configurable: true});}
    }
    
  • 使用装饰器

    export class TestComponent {@Emoji()result: string = 'Test'; //此时target是TestComponent, key是result// 装饰之后的结果是 ?Test?
    }
    

3.定义有参数注解

  • 定义用于在调用方法前弹出对话框的注解类@Confirmable

    export function Confirmable(message: string) {// descriptor为属性描述符[PropertyDescripter]// 此描述符就相当于上述方法Object.defineProperty的第三个参数return (target:object, key:string, descriptor: PropertyDescripter) => {const original = descriptor.value;// 通过value属性获得操作的函数descriptor.value = (...args: any) => { // 获取到方法参数值const allow = windows.confirm(message);//弹出提示框if(allow) {const result = original.apply(this, args);//如果点确定了就通过apply方法传递参数并调用函数return result;}return null;}return descriptor;}
    }
    
  • 使用装饰器

    export class TestComponent {@Confirmable('您确定要执行吗?') // 点击确认就会执行console.log,点击取消则不会执行方法内容handleClick() {console.log('点击执行');}
    }
    

二.指令

1.Angular中的指令类型

  • 组件:其实就是带模板的指令
  • 结构型指令:改变宿主文档结构
  • 属性型指令:改变宿主行为

2.内建指令

  • 结构型指令
    • ngIf
    • ngFor
    • ngSwich
  • 属性型指令
    • ngClass
    • ngStyle
    • ngModel

3.自定义属性型指令

  • 定义指令

    @Directive({selector: '[appGridItem]'//添加[]表示需要依附在宿主上使用
    })
    export class AppGridItemDirective {}
    
  • 使用

    
    <div appGridItem><img src="" alt="" appGridItemImage/><span appGridItemTitle>span>
    div>
    

三.指令的样式和事件绑定

指令没有模板,指令要寄宿在一个元素之上-宿主(Host)

1.相关注解

  • @HostBinding绑定宿主的属性或者样式
  • @HostListener绑定宿主的事件
  • 组件的样式中也可使用:host这样一个伪类选择器

2.实例代码

  • 上述实例使用注解实现

    @Directive({selector: '[appGridItem]'//添加[]表示需要依附在宿主上使用
    })
    export class AppGridItemDirective implements OnInit{// 通过@HostBinding注解可以绑定宿主的style中的display属性值@HostBinding('style.display') display = 'grid';//此时宿主的display属性值为grid // @HostBinding配合@Input()使用,可以在定义组件时使用直接对style.height赋值// @HostBinding('style.height') @Input() height = '3px'; //赋值修改成4px// 通过入参的ElementRef可以获得当前DOM类型的ElementRefconstructor(private elr: ElementRef, private renderer2: Renderer2) {} // 指令执行ngOnInit时,宿主已经加载完成了ngOnInit() {// 对宿主的display属性设置值// this.rd2.setStyle(this.elr.nativeElement, 'display', 'grid');}// 使用@HostListener绑定点击事件@HostListener('click', ['$event.target']) //参数1:事件名称;参数2:事件所携带的数据handleClick(ev) {console.log(ev);//打印了宿主的元素}
    }
    
  • 如果在组件的scss文件中使用:host伪类, 该伪类作用的元素就是当前组件

    :host {background: #000; //当前组件的背景是#000
    }
    

四.组件嵌套与投影组件

1.组件嵌套

  • 组件嵌套是不可避免的
    • 过渡嵌套会陷入复杂和冗余
  • 组件本身和外界的交互
    • 通过@Input和@Output
  • 避免组件嵌套导致冗余数据和事件传递
    • 内容投影
    • 路由
    • 指令
    • 服务

2.投影组件

  • ng-content是什么

    • 通过标签可以设置动态内容,即父组件调用使用了ng-content的子组件时,通过ng-content标签的select元素可以定义保留的内部内容是什么
  • 表现形式

  • 适合场景

    • 动态内容
    • 容器组件
  • 实例代码1(select是标签)

    • 父组件的html模板

      <app-test><span><div>This is Divdiv>span><div><span>This is spanspan><img src="./test.png">div>
      app-test>
      
    • 子组件(AppTestComponent)的html模板

      <ng-content select="span">ng-content>
      
    • 显示内容

      • 上述含义表示子组件只保留父组件定义的span中的元素,即只会显示This is Div
  • 实例代码2(select是样式类)

    • 父组件的html模板

      <app-test><span><div>This is Divdiv>span><div class="special"><span>This is spanspan><img src="./test.png">div>
      app-test>
      
    • 子组件的html模板

      <ng-content select=".special">ng-content>
      
    • 显示内容

      • 上述含义表示子组件只保留父组件定义的special类标记的元素,即只会显示This is span和test.png图片
  • 实例代码3(select是指定指令)

    • 父组件的html模板

      <app-test><span><div>This is Divdiv>span><div appDirective class="special"><span>This is spanspan><img src="./test.png">div>
      app-test>
      
    • 子组件的html模板

      <ng-content select="[appDirective]">ng-content>
      
    • 显示内容

      • 上述含义表示子组件只保留父组件定义的appDirective指令标记的元素,即只会显示This is span和test.png图片
  • 实例代码4(使用多个ng-content选择内容)

    • 父组件的html模板

      <app-test><span><div>This is Divdiv>span><div appDirective class="special"><span>This is spanspan><img src="./test.png">div>
      app-test>
      
    • 子组件的html模板

      <ng-content select="span">ng-content>
      <ng-content select="[appDirective]">ng-content>
      
    • 显示内容

      • 由于使用多个ng-content分别选择不同内容显示,则都会显示
  • 通过组件投影的方式可以把逻辑提到父组件中进行处理,此时可以减少中间一层的@Input和@Output

五.路由

1.路由初步

  • 路由是什么
    • 路由(导航)本质上是切换视图的一种机制
  • 路由的导航URL是否真实存在
    • Angular的路由借鉴了大家熟知的浏览器URL变化导致页面切换的机制
    • Angular是单页程序,路由显示的路径不过是一种保存路由状态的机制,这个路径在web服务器上不存在
  • 路由实现
    • 定义插座,用于定义下方插入的路由组件
    • 路由的好处在于代码的隔离
  • 本地启动build后的服务
    • 使用npm install -g http-server安装HttpServer
    • 使用prod模式打包并进入到dist目录 ng build --prod && cd dist
    • 在httpServer上启动http-server .会把打包后的项目启动在8080上
    • 访问浏览器的8080端口可以访问到项目,通过路由的方式可以进入到指定组件,但如果在指定路由中刷新页面,就会发现项目404。【因为刷新的时候会认为路由是API,会发送Get请求到服务器,服务器找到这个API发现不存在,就会返回404。解决这个问题:可以将404重定向到index.html,这样刷新就不会有问题了。】

2.路由定义

  • 定义路由数组【更详细的放前面,更宽泛(如**任意匹配)的放下面】

    • 路径
    • 组件
    • 子路由
  • 导入RouterModule

    • forRoot 【对于根模块来说的是RouterModule.forRoot()
    • forChild【对于功能模块(子模块)来说是RouterModule.forChild()
  • 实例代码

    const routes: Routes = [{ path: '', redirectTo: 'home', pathMatch: 'full' },{path: 'home',component: HomeComponent,children: [{path: '',redirectTo: 'hot',pathMatch: 'full'},{path: ':tabLink',component: HomeDetailComponent}]},{path: 'recommend',loadChildren: './recommend/recommend.module#RecommendModule'//懒加载}
    ];
    @NgModule({imports: [RouterModule.forRoot(routes, {enableTracing: true})],//enableTracing表示是否允许debug跟踪exports: [RouterModule]
    })
    export class AppRoutingModule{}
    

3.子路由

1.子路由的写法

  • 在父组件中添加定义子路由的插座
  • 在路由表里定义Routes对象指明children子路由的path和component

2.路径参数

  • 配置
    • {path:':tabLink', component:HomeDetailComponent}
  • 激活方式 【其中tab.link传递给的值就是tabLink的值】
    • ..."
    • this.router.navigate(['home',tab.link])
  • URL
    • http://localhost:4200/home/sports
  • 读取【route是ActivatedRoute类型】
    • this.route.paramsMap.subscribe(params=>{...})

3.路径对象参数

  • 配置
    • {path: ':tabLink', component:HomeDetailComponent}
  • 激活
    • ...
    • this.router.navigate(['home',tab.link,{name:'val1'}]);
  • URL
    • http://localhost:4200/home/sports;name=val1
    • 上述链接实际上传递params有两个:一个是tabLink值为sports;另一个是name值为val1
    • 可以通过以下方式从params中获取tabLink或name对应的value值
  • 读取【route是ActivatedRoute类型】
    • this.route.paramsMap.subscribe(params=>{...});

4.查询参数

  • 配置
    • {path:'home', component: HomeContainerComponent}
  • 激活
    • ...
    • this.router.navigate(['home'],{queryParams:{name:'val1'}});
  • URL
    • http://localhost:4200/home?name=val1
  • 读取【route是ActivatedRoute类型】
    • this.route.queryParamsMap.subscribe(params=>{ this.name = params.get('key'); ...;});

四.管道

1.管道的概念

  • 管道的作用就是在视图上提供便利的值变化的方法
  • 如在页面上将Data对象变到两天前,将1234.23变成$1,234.23

2.Angular内嵌的常用管道

  • AsyncPipe:用来处理异步的管道
  • DecimalPipe:处理数字的管道
  • I18nSelectPipe:国际化的管道
  • LowerCasePipe:把字母变小写的管道
  • TitleCasePipe:把每个单词首字母大写的管道
  • CurrencyPipe:货币处理的管道
  • JsonPipe:调试使用,可以将对象转成Json字符串的管道
  • PercentPipe:格式化成百分数的管道
  • UpperCasePipe:将字母变大写的管道
  • DatePipe:日期处理的管道
  • I18nPluralPipe:处理国际化中复数的管道
  • KeyValuePipe:处理字典对象的管道
  • SlicePipe:字符串、数组等取某几位的管道

3.实现使用管道

  • ts文件内容

    export class TestComponent {obj = {productId: 2,productName: 'JackProduct',model: 's',type: 'smart'}date = new Date();price = 123.32;data = [1,2,3,4,5];
    }
    
  • 在模板中使用管道处理

    <p>{{ obj | json }}
    p>
    
    <p>{{ date | date:'MM-dd' }}
    p>
    
    <p>{{ price | currency }}
    p>
    
    
    
    <p>{{ data | slice:1:3 }}
    p>
    
     
    

4.自定义管道

  • 定义Pipe:用于自定义输出时间的转换格式

    @Pipe({name: 'appAgo'})
    export class AgoPipe implements Pipe {transform(value: any):any {if(value) {// 通过+将Date类型对象转成时间戳const seconds = Math.floor((+new Date() - +new Date(value))/1000);//转成秒// 如果小于30秒,则输出 刚刚if(seconds < 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= interval[unitName];// 从最大时间往小时间做舍尾除法,获得在多长时间之前counter = Math.floor(seconds/unitValue);if(counter > 0) {return `${counter} ${unitName} 前`}}}// 如果都不满足则直接返回值return value;}}
    }
    
  • 使用管道,会根据上述逻辑进行相应输出

    <p>{{ value | appAgo }}
    p>
    

五.依赖注入

1.依赖注入的过程及使用

  • 提供服务
    • @Injectable()标记在服务中可以注入别的依赖
  • 模块中声明
    • providers数组中声明
    • 或者import对应模块
  • 在组建中使用
    • 构造函数中直接声明,Angular框架帮你完成注入

2.Angular自定义注入的方式【通常不用自己定义】

//注意Angular提供的依赖注入都是单例的
//自己定义池子
const injector = Injector.create({providers: [{provide: Product,//使用useFactory方式注入useFactory: ()=>{return new Product('小米手机',11);},deps: []},{provide: PurchaseOrder,useClass: PurchaseOrder,//useClass表示直接提供一个PurchaseOrder实例,deps:[ Product ]//deps属性表示PurchaseOrder中依赖的服务}//还有useExsiting表示使用其他地方创建好的对象实例//useValue表示直接使用一个指定值//...]
})
//通过injector.get(Product)或injector.get(PurchaseOrder)的方式来获取
//或通过构造参数constructor(@Inject(Product)private product: Product)注入即可

3.Angular6服务注册新特性

  • Angular6以前注入服务都是在AppModule中的providers中注入

  • Angular6以后可以在定义服务的时候使用providedIn:XXX的方式自动注入

    @Injectable({providedIn: 'root'//表示注入到根//providedIn: HomeModule表示注入到Module
    }) 
    

六.脏值检测

1.脏值检测概要

  • 什么是脏值检测
    • 当数据改变时更新视图(DOM)
  • 什么时候会触发脏值检测
    • 浏览器事件(如click,mouseover,keyup等)
    • setTimeout()和setInterval()
    • Http请求
  • 如何进行检测
    • 检查两个状态:当前状态和新状态

2.组件的生命周期

在这里插入图片描述

  • 每一个属性会经历两次脏值检测,第一次是已经将值赋给属性了,第二次是检测属性是否赋值成功,如果没变化放行,如果有变化则会成为死循环了

  • 注意不能在AfterViewChecked和AfterViewInit函数中更新属性值(console会报错),因为Angular是通过脏值检测机制更新DOM的,如果在AfterViewChecked和AfterViewInit中更新属性值,想把属性值同步到页面中就需要再做一次脏值检测,两次属性值不同则脏治检测不通过

  • 如果需要在AfterViewChecked和AfterViewInit中更新属性,需要使用NgZone对象(依赖注入),NgZone是浏览器的js运行时划分出n个区域,每个区域面向自己程序相互不干扰。可以借助NgZone对象使得属性的改变运行在Angular程序区域之外,此时脏值检测就检测不到此属性的改变(绕过去~)。

    //constructor(private ngZone: NgZone){}方式注入
    this.ngZone.runOutsideAngualr(()=>{setInterval(()=>{//通过异步的方式避开第二次脏值检测,此时第一次第二次脏值检测都会通过this._title = "HelloJack"; },1000)
    });
    
  • 如果想做倒计时功能等实时更新属性值的功能,可以通过ViewChild获取到Dom元素,并通过innerHTML的更改实时更改内部显示的内容,就可以做到倒计时的效果

3.脏值检测的OnPush策略

  • 非OnPush(Default)策略的检测:只要组件树中任意一个节点的数据发生变化,都会跑一边整个树,会导致性能消耗
  • OnPush策略:执行此策略时只对组件中有@Input注解的属性进行检测,如果属性发生改变则触发脏值检测,而且只会检测又脏值发生改变的节点和子树

4.OnPush策略实际代码应用

  • 组件默认都是Default策略,即任意一个节点数据的变化都会遍历整个树

  • 通过在组件的@Component中添加changeDetection值为ChangeDetectionStrategy.OnPush设置为OnPush策略

    @Component({selector: xxx,templateUrl: xxx,styleUrls: [xxx],changeDetection:ChangeDetectionStrategy.OnPush
    })
    
  • 设定了OnPush策略的组件就会只看@Input属性的变化,只有@Input属性修饰的属性变换才会触发脏值检测,且只会触发此分支的,否则则不理【即笨组件】

5.OnPush策略修饰带来的问题

  • 路由参数发生组件改变,不会销毁组件,而是重用组件,所以ngOnInit只会走一遍
  • 如果将路由参数改变的组件变成OnPush策略后,由于没有@Input属性,如果在ngOnInit中的代码逻辑即会被执行但不发生变更检测即不会反映到页面上(如果写了获取页面数据变化的逻辑即不会在页面显示)
  • 解决上述问题:
    • 需要通过依赖的方式导入ChangeDetectorRef对象constructor(private cd: ChangeDetectorRef)
    • 通过this.cd.markForCheck();方法通知框架进行变化检查,如果属性发生了变化则需要在页面上也进行显示


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部