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()使用,可以在定义组件时使用<div [appGridItem]="'4px'">直接对style.height赋值
        // @HostBinding('style.height') @Input() height = '3px'; //赋值修改成4px
        
        // 通过入参的ElementRef可以获得当前DOM类型的ElementRef
        constructor(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的子组件时,通过ng-content标签的select元素可以定义保留的内部内容是什么
  • 表现形式

    • <ng-content select="样式类/HTML标签/指令"></ng-content>
  • 适合场景

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

    • 父组件的html模板

      <app-test>
      	<span>
          	<div>
                  This is Div
              </div>
          </span>
          <div>
              <span>This is span</span>
              <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 Div
              </div>
          </span>
          <div class="special">
              <span>This is span</span>
              <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 Div
              </div>
          </span>
          <div appDirective class="special">
              <span>This is span</span>
              <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 Div
              </div>
          </span>
          <div appDirective class="special">
              <span>This is span</span>
              <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服务器上不存在
  • 路由实现
    • <router-outlet></router-outlet>定义插座,用于定义下方插入的路由组件
    • 路由的好处在于代码的隔离
  • 本地启动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.子路由的写法

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

2.路径参数

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

3.路径对象参数

  • 配置
    • {path: ':tabLink', component:HomeDetailComponent}
  • 激活
    • <a [routerLink]="['/home',tab.link,{name:'val1'}]">...</a>
    • 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}
  • 激活
    • <a [routerLink]="['/home']" [queryParams]={name:'val1'}>...</a>
    • 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>
    <!-- 输出结果是标准型的json: {"productId": 2,"productName": 'JackProduct',"model": 's',"type": 'smart'} -->
    <p>
        {{ date | date:'MM-dd' }}
    </p>
    <!-- 通过:定义格式,上述输出结果是07-09,也可以加yy等等 -->
    <p>
        {{ price | currency }}
    </p>
    <!-- 上述输出结果是$123.32 -->
    <!-- 如果要是想显示¥的话,需要在app.module中使用provider定义如下 -->
    <!--
    providers:[{
    	provider: LOCALE_ID,
    	useValue: 'zh-Hans'
    }]
    且需要在AppModule的constructor()构造函数中通过registerLocaleData(localZh, 'zh')的方式将本地导入中国
    使用currency:'CNY'就可以显示¥123.32
    也可以通过currency:'CNY':'symbol':'4.0-2'表示小数点左侧至少4位,右侧0-2位显示是¥0,123.32
    -->
    <p>
        {{ data | slice:1:3 }}
    </p>
    <!-- 还可以通过slice实现分片,获得索引值1(包含)-3(不包含)的 -->
    <!-- 输出结果: 2,3 --> 
    

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();方法通知框架进行变化检查,如果属性发生了变化则需要在页面上也进行显示
发布了258 篇原创文章 · 获赞 332 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq_34829447/article/details/95040073