二、Angular 基础知识

前言

  • 基于 Angular 10
  • Angular 官网:https://v10.angular.cn/docs
  • 需要基础:HTML、CSS、Javascript、TypeScript
  • 环境:Node.js 12.11.0
  • Angular CLI: 10.1.7
  • 查看angular 的版本: ng verision
  • Node 和 angular 版本对应表:https://blog.youkuaiyun.com/weixin_44596902/article/details/119342708
  • angular 默认端口号:4200
  • 运行 anlular 项目:ng serve --open
  • 生成的项目的文件的说明:https://v10.angular.cn/guide/file-structure

Angular CLI 常用的命令

命令说明
ng new <name> [options]创建并初始化一个新的angular项目
ng generate <type> [options] 可简写成 ng g <type> [options]创建命令根据type选项的种类生成或创建响应的文件(type 类型见下表)
  • type 种类
支持的种类示例说明
componentng g component new-component创建组件
directiveng g directive new-directive创建指令
pipeng g pipe new-pipe创建管道
serviceng g service new-service创建服务类
classng g class new-class创建类
interfaceng g interface new-interface创建接口
enumng g enum new-enum创建枚举类
moduleng g module new-module创建模块

Angular 项目的启动过程

Angular 项目的启动过程分为以下几步。

  1. 当在终端执行 ng serve 命令时,Angular CLI 会根据 angular.json 文件中的 main 元素找到项目的入口文件 main.ts。
  2. main.ts 文件加载 AppModule 根模块 (app.module.ts 文件 )。
  3. AppModule 根模块引导 AppComponent 根组件(app.component.ts 文件)。
  4. AppComponent 根组件完成自身的初始化工作,如完成标签<app-root> 的初始化工作在

上述步骤完成后,当打开浏览器并浏览到“http:/ocalhost:4200”时,会出现以下情况。
5. 在默认情况下,浏览器会打开文件 index.html。
6. index.html 文件中加载了 标签,会显示项目内容。

一、 组件

  • 组件模板包含用于在浏览器中显示组件的标记代码,组件通过 @
    Component() 装饰器把组件类和模板关联在一起。
  • 组件样式不同于传统样式,它仅对当前组件有效。

(一) 组件类的构成

  • Angular 中用 @Component() 装饰器声明组件类,@Component() 装饰器会指出紧随其后的类是组件类,并告知 Angular 如何处理这个组件类。该装饰器包含多个属性,这些属性及其值称为元数据。元数据告诉 Angular 到哪里获取它需要的主要信息,以创建和展示这个组件类及其视图。Angular 会根据元数据的值来渲染组件并执行组件的逻辑。具体来说,@Component() 装饰器把一个模板、样式和该组件类关联起来,该组件类及其模板、样式共同描述了一个视图。

  • 除了包含或指向模板和样式之外,@Component() 装饰器的元数据还会针对如何在 HTML 中引用该组件类,以及该组件类需要哪些服务等情况进行配置。

    @Component({
        selector: 'app-root',
        templateUrl: 'app.component.html',
        styleUrls: ['./app.component.scss'],
    })
    
  • selector: 是一个CSS 选择器,它会告诉 Angular,一旦在模板 HTML 中找到了这个选择器对应的标签,就创建并插入 App 组件的一个实例的视图。如项目的根 HTML 文件中包含了<app-root></app-root> 标签,当代码运行到此处时,Angular 就会在这个标签中插入一个 Ap-pComponent 实例的视图。

  • templateUrl: App组件的HTML模板文件,引用当前目录下的app.component.html文件。
    这个模板文件定义了 App 组件的视图。

  • styleUrls : App 组件的 CSS 文件,引用当前目录下的 app.component.css 文件。这个文件定义了App 组件模板的样式。

@Component() 装饰器的其他元数据配置选项

元数据配置选项说明
animations当前组件的动画列表
changeDetection指定使用的变化监测策略
encapsulation当前组件使用的样式封装策略
entryComponents一组应该和当前组件一起编译的组件
exportAs给指令分配一个变量,使其可以在模板中使用
inputs指定组件的输入属性
interpolation当前组件模板中使用的自定义插值标记,默认是{{}}
moduleld包含该组件模板的 ID,被用于解析模板和样式的相对路径
outputs指定组件的输出属性,显示其他组件可以订阅的输出事件的类属性名称列表
providers指定该组件及其所有子组件可用的服务依赖注入
queries设置需要被注入组件的查询
viewProviders指定该组件及其所有子组件可用的服

(二) 组件模板的种类

  • 内联:

    @Component({
        selector: 'app-root',
        template: `<h2>hello</h2>`,
        styles: ['h2 {border:1px solid #000; padding: 10px}'],
    })
    
  • 外部:

    @Component({
        selector: 'app-root',
        templateUrl: 'app.component.html',
        styleUrls: ['./app.component.scss'],
    })
    

(三) 单向数据绑定

[1]. 插值语法

  • 在模板视图中显示组件类的属性,可以通过插值绑定属性名。插值的语法就是把属性名写在双花括号里,如{{message}}
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<div>{{message}}</div>',
})
export class DemoComponent {
  message = '一条消息';
}

[2]. 属性绑定

  • 数据绑定方向是从组件类到视图。属性绑定是通过方括号“0”将组件类中的属性单向绑定到绑定目标,绑定目标可以是 DOM 属性、HTML 特性等。
  • 在 Angular 中,HTML 特性的唯一作用是初始化元素和指令的状态。
1). DOM 属性绑定
  • DOM 属性绑定指通过方括号"[]”将模板视图中的 DOM 属性与组件类中的属性进行绑定

    [DOM 属性]="组件类中的属性"
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<img [src]="imgSrc" />',
})
export class DemoComponent {
  imgSrc = './icon.png';
}
2). THML 特性绑定
  • HTML 特性绑定的语法类似于 DOM 属性绑定,但其方括号之间不是元素的 DOM 属性,而由前缀 attr、点“.”和 HTML 特性名称组成的字符串。
    [attr.HTML 特性名称]
    
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<table>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
    <tr>
      <td [attr.colSpan]="colSpan">一整行</td>
    </tr>
  </table>`,
})
export class DemoComponent {
  colSpan = 3;
}
3). class 样式绑定
  • Class 样式绑定用于设置视图元素的 class 属性。可以使用 Class 样式绑定在视图元素的class 属性中添加和删除CSS 样式名称。Class 样式绑定的语法类似于 HTML 特性绑定,它以前缀class 开头,后跟一个点“.”,然后是该 CSS 样式的名称。

    [class.CSS 样式名称]
    
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<p>angular 版本不一样,下面的效果可能不同</p>
    <p class="my-font" [class]="'my-class'">my-font 样式不被覆盖</p>
    <p class="my-font" [attr.class]="'my-class'">my-font 样式被覆盖</p>
    <p class="my-font" [class.my-class]="showMyClass">my-font 样式不被覆盖</p>
    <p class="my-font" [class.my-class]="!showMyClass">my-font 样式不被覆盖</p>`,
  styles: ['.my-class {color:red;}', '.my-font {font-size:20px}'],
})
export class AppComponent {
  showMyClass = true;
}
4). style 样式绑定
  • 通过 Style 样式绑定,可以设置视图中元素的内联样式。Style 样式绑定的语法与 Class 样式绑定类似,方括号中的值是由 style 前缀、点“.”和 CSS 样式名称组成的。
    [style.CSS样式名称]
    
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<p>angular 版本不一样,下面的效果可能不同</p>
    <p style="color:blue;" [style.color]="myColor">红色字体</p>
    <p [style.font-size]="myColor">字体16px</p>
    <p [style.font-size.em]="1.5">字体1.5em</p>`,
})
export class AppComponent {
  myColor = 'red';
  myFontSize = '16px';
}

[3]. 事件绑定

  • 事件绑定用来监听视图中的事件,如按键、鼠标移动、单击和触屏等

    (目标事件)="模板声明"
    
  • 圆括号之间的目标事件是触发事件的名称,模板声明是关于目标事件发生时该怎么做的说明。通常,事件绑定是对组件类中方法的调用,它通常会修改绑定到模板的实例变量,从而导致UI 发生更改。

  • Anqular 提供了一个模板变量 $event,引用的是DOM 事件对象。在事件绑定中,可通过模板语句传递 $event 模板变量给类方法。$event 模板变量包含有关事件的所有信息,也包括任何潜在的更新值。事件对象的类型取决于目标事件。如果目标事件是原生 DOM元素事件,那么$event模板变量就是 DOM 事件对象,它有如 target 和 target.value 这样的属性。

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `<img [src]="imgSrc" /> <button (click)="changeImg()">切换图片</button>`,
    })
    export class AppComponent {
      imgSrc = '1.png';
      changeImg() {
        this.imgSrc = '2.png';
      }
    }
    

(四) 双向数据绑定

  • 双向数据绑定为 Web 应用程序提供了一种在组件类及其模板之间共享数据的方式。 Angular提供了 NgModel 内置指令实现将双向数据绑定添加到 HTML 表单元素。使用 NgModel 内置指令有专门的绑定语法。

    [(NgModel)] ="组件类变量”
    

    下边两个代码是一样的:

    <input [value]="username" (input)="username = $event.target.value" />
    <input [(ngModel)]="username" />
    
  • 注意: NgModel内置指令来自Angular 中的 FormsModule (表单模块),使用之前必须手动将其导入主模块中。

  1. 修改 src/app/app.module.ts 文件,添加 FormsModule 指令
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    // 引入 FormsModule
    import { FormsModule } from '@angular/forms';
    
    import { AppComponent } from './app.component';
    
    @NgModule({
      declarations: [AppComponent],
      imports: [
        BrowserModule,
        FormsModule, // 导入
      ],
      providers: [],
      bootstrap: [AppComponent],
    })
    export class AppModule {}
    
  2. 修改 src/app/app.component.ts 文件
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <p>前景色:<input [(ngModel)]="fg" /></p>
    <p>背景色:<input [(ngModel)]="bg" /></p>
    <div [ngStyle]="{ color: fg, 'background-color': bg }">
      可在文本框中颜色以修改这里的样子
    </div>
  `,
})
export class AppComponent {
  fg = '#fff';
  bg = '#000';
}

(五) 组件的生命周期

  • 组件的生命周期通常被划分为 8 个不同的阶段,表按照组件生命周期的 8个阶段,从先到后地展示了相应的每个生命周期接口的详细信息。
接口方法描述
OnChangesngOnChanges()输入或输出绑定值更改时调用,每次变化时都会调用
OnlnitngOnlnit()在第一次 ngOnChanges()之后,初始化指令1组件时调用,仅调用一次在每个变更检测周期中,紧跟
DoCheckngDoCheck()在每个变更检测周期中,紧跟在 ngOnChanges() 和 ngOninit() 后面调用
AfterContentlnitngAfterContentlnit()组件内容初始化后,第一次 ngDoCheck() 之后调用只调用一次
AfterContentCheckedngAfterContentChecked()ngAfterContentlnit() 和 ngDoCheck()之后调用
AfterViewInitngAfterViewlnit()在组件的视图初始化之后调用,仅调用一次
AfterViewCheckedngAfterViewChecked()每次检查组件的视图后调用
OnDestroyngOnDestroy()在指令/组件被销毁之前调用

(六) 父子组件交互

  • 组件是构成 Angular 的基础和核心。通俗来说,组件用来包装特定的功能,Web 应用程序的有序运行依赖于组件之间的协调工作。组件本身就类似容器,它可以包含其他组件。因此,可以把父组件拆分成若千个小一点的子组件。拆分成子组件至少有下面这些好处。
    • 子组件能重复使用,特别有助于UI的布局。
    • 子组件打包为特定的单一功能,维护起来也方便。
    • 子组件功能单一,方便对其进行测试。

[1]. 父组件传递数据给子组件 Input 装饰器

  • 组件类中以 @lnput() 装饰器声明的类属性称为输入型属性,父组件通过输入型属性绑定把数据从父组件传递到子组件。
  1. src/app 文件夹下新建子组件 son-cpn/son-cpn.component.ts

    • 注意:自己手动创建的文件需要在src/app/app.module.ts 中引入的组件,并声明,才能被使用

      import { BrowserModule } from '@angular/platform-browser';
      import { NgModule } from '@angular/core';
      import { AppComponent } from './app.component';
      // 引入子组件
      import { SonCpnComponent } from './son-cpn/son-cpn.component';
      
      @NgModule({
        declarations: [
          AppComponent,
          SonCpnComponent, // 声明子组件
        ],
        imports: [BrowserModule],
        providers: [],
        bootstrap: [AppComponent],
      })
      export class AppModule {}
      
    • 也可以使用如下命令生成, 生成的文件中,son-cpn.component.htmlson-cpn.component.cssson-cpn.component.spec.ts 暂时用不到,使用命令创建组件不需要再src/app/app.module.ts 中引入和声明,因为系统已经自动添加过了

      ng g c son-cpn
      
  2. 修改 src/app/son-cpn/son-cpn.component.ts 文件内容如下

    // Input 需要手动添加
    import { Component, OnInit, Input } from '@angular/core';
    
    @Component({
      selector: 'app-son-cpn', // 子组件的标签
      template: `<p>{{ title }}</p>
        <p>父组件传递的消息:{{ msg }}</p>
        <p>父组件传递的内容:{{ content }}</p>`,
    })
    export class SonCpnComponent implements OnInit {
      @Input() msg: string; // 添加的输入型属性
      @Input() content: string; // 添加的输入型属性
    
      title: string = '我是一个子组件';
    
      constructor() {}
    
      ngOnInit(): void {}
    }
    
    
  3. 修改 src/app/app.component.ts 文件内容如下,父组件通过绑定属性的方式传递类变量给子组件

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      // app-son-cpn 来自子组件 Component 声明中的 selector
      // 父组件通过绑定属性的方式传递类变量给子组件
      template: `<app-son-cpn [msg]="message" [content]="other"></app-son-cpn>`,
    })
    export class AppComponent {
      message = '父组件传递给子组件一个消息';
      other = '父组件传递给子组件一个内容';
    }
    

[2]. 子组件传递数据给父组件 Output 装饰器

  • 组件类中以@Output()装饰器声明的类属性称为输出型属性。子组件暴露一个 EventEmitter对象,当事件发生时,子组件利用该对象的 emits()方法对外发射事件。父组件绑定到这个事件并在事件发生时做出回应。
  1. src/app 文件夹下新建子组件 son-cpn/son-cpn.component.ts

    • 注意:自己手动创建的文件需要在src/app/app.module.ts 中引入的组件,并声明,才能被使用

      import { BrowserModule } from '@angular/platform-browser';
      import { NgModule } from '@angular/core';
      import { AppComponent } from './app.component';
      // 引入子组件
      import { SonCpnComponent } from './son-cpn/son-cpn.component';
      
      @NgModule({
        declarations: [
          AppComponent,
          SonCpnComponent, // 声明子组件
        ],
        imports: [BrowserModule],
        providers: [],
        bootstrap: [AppComponent],
      })
      export class AppModule {}
      
    • 也可以使用如下命令生成, 生成的文件中,son-cpn.component.htmlson-cpn.component.cssson-cpn.component.spec.ts 暂时用不到,使用命令创建组件不需要再src/app/app.module.ts 中引入和声明,因为系统已经自动添加过了

      ng g c son-cpn
      
  2. 修改 src/app/son-cpn/son-cpn.component.ts 文件内容如下

    // Output ,EventEmitter需要手动添加
    import { Component, OnInit, Output, EventEmitter } from '@angular/core';
    
    @Component({
      selector: 'app-son-cpn', // 子组件的标签
      template: `<p>{{ title }}</p>
        <button (click)="senMsg()">向父组件发送信息</button>`
    })
    export class SonCpnComponent implements OnInit {
      // 添加的输出型属性,属性名为 sendBtn 对应父组件中的方法名
      @Output() sendBtn = new EventEmitter();
      title: string = '我是一个子组件';
      msg = '子组件向父组件发送一个信息';
    
      constructor() {}
    
      ngOnInit(): void {}
    
      senMsg() {
        this.sendBtn.emit(this.msg);
      }
    }
    
  3. 修改 src/app/app.component.ts 文件内容如下,父组件通过绑定属性的方式传递类变量给子组件

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      // app-son-cpn 来自子组件 Component 声明中的 selector
      // sendBtn 一定要与子组件中 Output 声明的变量一致
      template: `
        <app-son-cpn (sendBtn)="getSonMsg($event)"></app-son-cpn>
        <p>子组件发送给父组件的信息:{{ sonMsg }}</p>
      `,
    })
    export class AppComponent {
      sonMsg = '';
      getSonMsg(msg) {
        this.sonMsg = msg;
      }
    }
    

二、 模板

(一) 模板引用

[1]. 模板引用变量

  • 模板引用变量通常是对模板中 DOM 元素的引用,还可以引用指令、组件等。Anqular 使用井号“#”声明模板引用变量。

  • 模板引用变量 #phone 在<input>中声明了一个phone 变量,然后在<button>元素的事件绑定方法中引用它。

    import { Component } from '@angular/core';
    // phone指的是<input>。按钮的单击事件将把<input>的值传给<button>的callPhone()方法
    // 有两种表达方式:# + 名字, ref- + 名字
    @Component({
      selector: 'app-root',
      template: `<input #phone placeholder="电话号码" />
        <input type="number" ref-phone2 placeholder="电话号码" />
        <!-- phone.constructor.name : HTMLInputElement  -->
        模板引用变量可以在模板中使用: {{ phone.constructor.name }}
        <button (click)="callPhone(phone.value)">电话1</button>
        <button (click)="callPhone(phone2.value)">电话2</button>`,
    })
    export class AppComponent {
      callPhone(value) {
        console.log(value);
      }
    }
    

[2]. @ViewChild() 装饰器

  • @ViewChild()装饰器是由 Angular 提供的属性装饰器,用来从模板视图中获取匹配的元素,返回匹配的一个或首个元素。使用 @ViewChild()装饰器时需注意:查询视图元素的工作在组件的生命周期 AfterViewinit 开始时完成,因此在 ngAfterViewinit()方法中能正确获取查询的元素。

    @ViewChild(selector, {read: ElementRef, static: false})  selector; 
    
  • @ViewChild的参数说明

    • selector:用于查询指令的类型和名字
    • read:read参数是可选的,因为 Angular 会根据 DOM 元素的类型推断出该引用类型。 一般Angular会推断出一些比较简单的类型如: ElementRefTemplateRef
      一些引用类型如 ViewContainerRef 就不可以被 Angular 推断出来,所以必须在 read 参数中显式声明
    • static: 一个标志符,告诉检测器是否容许从动态模板中获取满足条件的元素或者指令
      • True表示在执行变更检测之前解析查询结果,false表示在执行变更检测之后解析。默认为false。
  1. 新建 src/app/son-cpn/son-cpn.component.ts ,注意:自己手动创建的文件需要在src/app/app.module.ts 中引入的组件,并声明,才能被使用,具体的操作步骤可以看 (六)父子组件交互内容

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-son-cpn',
      template: `<p>{{ title }}</p>`,
    })
    export class SonCpnComponent {
      title = '我是子组件';
    }
    
  2. 修改 src/app/app.component.ts 文件内容如下

    import {
      Component,
      AfterViewInit,
      ViewChild,
      AfterContentInit,
      OnInit,
      ElementRef,
      AfterViewChecked,
    } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `<h1>父组件</h1>
        <app-son-cpn #sonCpn></app-son-cpn>
        <app-son-cpn #sonCpn2></app-son-cpn>
        <p #title>Hi {{ name }}</p>
        <p #title2>Hello {{ name }}</p> `,
    })
    export class AppComponent
      implements OnInit, AfterContentInit, AfterViewInit, AfterViewChecked
    {
      name: string = 'Murphy';
      // static 默认为 false
      @ViewChild('sonCpn') sonCpn: ElementRef;
      @ViewChild('sonCpn2', { static: true }) sonCpn2: ElementRef;
      @ViewChild('title') ctitle: ElementRef;
      @ViewChild('title2', { static: true }) ctitle2: ElementRef;
      //方法1
      ngOnInit() {
        console.log('子组件 ngOnInit:' + this.getTitleValue(this.sonCpn)); // undefined
        console.log('子组件2 ngOnInit:' + this.getTitleValue(this.sonCpn2)); // SonCpnComponent 能获取到title “我是子组件”
        console.log('元素 ngOnInit:' + this.getTitleValue(this.ctitle)); // ElementRef 获取不到innerHTML
        console.log('元素2 ngOnInit:' + this.getTitleValue(this.ctitle2)); // undefined
        console.log('*******************1************************');
      }
      //方法2
      ngAfterContentInit() {
        console.log('子组件 ngAfterContentInit:' + this.getTitleValue(this.sonCpn)); // SonCpnComponent 获取不到title
        console.log('子组件2 ngAfterContentInit:' + this.getTitleValue(this.sonCpn2)); // SonCpnComponent 能获取到title “我是子组件”
        console.log('元素 ngAfterContentInit:' + this.getTitleValue(this.ctitle)); //  ElementRef  获取不到innerHTML
        console.log('元素2 ngAfterContentInit:' + this.getTitleValue(this.ctitle2)); //  ElementRef 获取不到innerHTML
        console.log('********************2***********************');
      }
    
      //方法3
      ngAfterViewInit() {
        console.log('子组件 ngAfterViewInit:' + this.getTitleValue(this.sonCpn));// SonCpnComponent 能获取到title “我是子组件”
        console.log('子组件2 ngAfterViewInit:' + this.getTitleValue(this.sonCpn2));// SonCpnComponent 能获取到title “我是子组件”
        console.log('元素 ngAfterViewInit:' + this.getTitleValue(this.ctitle));//  ElementRef 能获取到innerHTML “Hi Murphy”
        console.log('元素2 ngAfterViewInit:' + this.getTitleValue(this.ctitle2));//  ElementRef 能获取到innerHTML “Hello Murphy”
        console.log('********************3***********************');
      }
    
      //方法4
      ngAfterViewChecked() {
        console.log('子组件 ngAfterviewchecked:' + this.getTitleValue(this.sonCpn));// SonCpnComponent 能获取到title “我是子组件”
        console.log('子组件2 ngAfterviewchecked:' + this.getTitleValue(this.sonCpn2));// SonCpnComponent 能获取到title “我是子组件”
        console.log('元素 ngAfterViewchecked:' + this.getTitleValue(this.ctitle));//  ElementRef 能获取到innerHTML  “Hi Murphy”
        console.log('元素2 ngAfterViewchecked:' + this.getTitleValue(this.ctitle2));//  ElementRef 能获取到innerHTML “Hello Murphy”
        console.log('*******************4************************');
      }
    
      //如果传入的元素不为空,则输出该元素的文本内容
      getTitleValue(v) {
        console.log(v);
    
        if (!v) return '';
        return v && v.nativeElement ? v.nativeElement.innerHTML : v.title;
      }
    }
    
    

[3]. @ViewChildren() 装饰器

  • @ViewChildren()装饰器也是 Angular 提供的属性装饰器,与 @ViewChild()装饰器一样,用来从模板视图中获取匹配的元素,不同的是返回匹配的所有元素。
  • 返回的结果是一个 QueryList 集合。(获得一个视图集合,它是一个类数组的形式, 可以使用 .toArray() 方法转化为一个数组)
  1. 新建 src/app/son-cpn/son-cpn.component.ts ,注意:自己手动创建的文件需要在src/app/app.module.ts 中引入的组件,并声明,才能被使用,具体的操作步骤可以看 (六)父子组件交互内容

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-son-cpn',
      template: `<p>{{ title }}</p>`,
    })
    export class SonCpnComponent {
      title = '我是子组件';
    }
    
  2. 修改 src/app/app.component.ts 文件内容如下

    import {
      Component,
      AfterViewInit,
      ElementRef,
      ViewChildren,
    } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `<h1>父组件</h1>
        <app-son-cpn #sonCpn></app-son-cpn>
        <app-son-cpn #sonCpn2></app-son-cpn>
        <p #title>Hi {{ name }}</p>
        <p #title2>Hello {{ name }}</p> `,
    })
    export class AppComponent implements AfterViewInit {
      name: string = 'Murphy';
      // static 默认为 false
      @ViewChildren('sonCpn, sonCpn2, title, title2') ctitles: ElementRef[];
    
      //方法3
      ngAfterViewInit() {
        console.log(this.ctitles[0]); // undefined
        // 只能遍历 ctitles, 可以使用 this.ctitles.toArray() 转化为数组
        this.ctitles.forEach((childComponent) => {
          this.getTitleValue(childComponent);
        });
      }
    
      //如果传入的元素不为空,则输出该元素的文本内容
      getTitleValue(v) {
        console.log(v);
    
        if (!v) return '';
        console.log(v && v.nativeElement ? v.nativeElement.innerHTML : v.title);
      }
    }
    

三、 指令

(一) 结构型指令

[1]. NgIf 指令

  • 用户可以通过将 Nglf 指令应用在 HTML 元素上来在 DOM 中添加或删除 DOM 元素。Nglf 指的模板表达式接收一个布尔值,如果值为true,DOM 元素里面的内容将显示在视图 DOM 中;如果值为 false,DOM 元素里面的内容将不显示(从视图 DOM 中去除 )。
    <div *nglf="模板表达式">
    
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <p *ngIf="showName">显示名字</p>
    <p *ngIf="!showName">不显示名字</p>
    <button (click)="toggle()" type="button">切换</button>
  `,
})
export class AppComponent {
  showName = true;
  toggle() {
    this.showName = !this.showName;
  }
}

[2]. NgFor 指令

  • NgFor 是一个迭代指令,它通过将可迭代的每个子项作为模板的上下文来重复渲染模板 。赋值给*ngFor 的字符串不是模板表达式,而是由 Angular 解释的一种小型语言。

    <div *ngFor='let item of items'>ffitem.namel</div>
    
  • NgFor 指令的内置变量

    内置变量描述
    itemngFor ="let item of items"中,item 代表迭代的每个子项
    index每个模板上下文的当前循环迭代的索引
    last布尔值,指示当前项是否是迭代中的最后一项
    even布尔值,指示当前索引是否是偶数索引
    odd布尔值,指示当前索引是否是奇数索引
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <ul>
      <li
        *ngFor="
          let name of names;
          let index = index;
          let last = last;
          let even = even;
          let odd = odd
        "
      >
        {{ index }}: {{ name }}- {{ even ? '是偶数索引' : '' }} -
        {{ odd ? '是奇数索引' : '' }} -
        {{ last ? '是最后一个' : '' }}
      </li>
    </ul>
  `,
})
export class AppComponent {
  names = ['张三', '李四', '王五'];
}

[3]. NgSwitch 指令

  • NgSwitch 指令类似于 JavaScript 的 switch 语句。它根据切换条件显示几个可能的元素中的一个。Angular 只会将选定的元素放入 DOM。
  • NgSwitch 指令实际上是3 个协作指令的集合: NgSwitchNgSwitchCase 和 NgSwitchDefault。
<container-element [ngSwitch]="switch_expression">
	<some-element *ngSwitchCase="match expression 1">...</some-element>
	...
	<some-element *ngSwitchDefault>...</some-element>
</container-element>
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="addDay()">增加天数:{{ day }}</button>
    <ul [ngSwitch]="day">
      <li *ngSwitchCase="1">周一</li>
      <li *ngSwitchCase="2">周二</li>
      <li *ngSwitchDefault>不知道</li>
    </ul>
  `,
})
export class AppComponent {
  day = 1;

  addDay() {
    this.day++;
  }
}

[4]. ng-container 分组元素

  • Angular 的 ng-container 是一个分组元素,但它不会影响样式或 DOM布局,因为 Angular不会把它放进 DOM 中。ng-container 是一个由 Angular 解析器负责识别处理的语法元素。它不是一个指令、组件、类或接口,更像是 JavaScript 的 if 块中的花括号。当没有合适的宿主元素时用户可以使用 ng-container 对元素进行分组。
  • <ng-container> 的使用场景之一是,在需要遍历或if判断时,它可以起到一个载体的作用。
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <ul>
      <!-- 不会引入其他元素 -->
      <ng-container *ngFor="let day of days">
        <li>{{ day }}</li>
      </ng-container>
    </ul>
  `,
})
export class AppComponent {
  days = ['monday', 'tuesday'];
}

(二) 属性型指令

[1]. NgClass 指令

  • NgClass 指令的作用是添加和删除一组样式。NgClass 指令的语法支持 3 种格式的表达式。
    • 使用空格分隔的字符串:[ngClass]="is-info is-item has-border"
    • 字符串数组:[ngClass]="['is-info', 'is-item', 'has-border']"
    • 对象:[ngClass]="{'is-info': true,'is-item': true]"
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <p [ngClass]="infoClass">信息</p>
    <p [ngClass]="successClass">成功</p>
    <p [ngClass]="other">其他</p>
  `,
  styles: [
    '.base:{font-size:16px;}',
    '.info {color: #999}',
    '.success {color: green}',
    '.other {color: #000}',
  ],
})
export class AppComponent {
  infoClass = 'base info';
  successClass = ['base', 'success'];
  other = { base: true, other: true };
}

[2]. NgStyle 指令

  • NgStyle 指令的作用是添加和删除一组 Style 样式。NgStyle 指令接收一个键/值(Key/Value)对的对象表达式,键/值对的键是一个样式属性。

  • 用户可以在键上添加一个前缀简化写法

    [ngStyle]="{font-size.px: 15}"
    // 等同于
    [ngStyle]="{font-size: 15px}"
    
  • 使用 NgStyle 指令需要注意以下几点。

    • NgStyle 指令接收键/值对作为输入表达式,前提条件是键必须是有效的属性名称。
    • NgStyle 指令可以绑定组件类中的属性
    • NgStyle 指令将会重写当前元素的内置样式
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <p [ngStyle]="myStyle">一行文字</p>
    <!-- style 中与ngStyle 相同的样式会被覆盖掉,ngStyle 没有的保留 -->
    <p style="font-size: 16px;font-weight: bold;" [ngStyle]="myStyle">一行文字</p>
  `,
})
export class AppComponent {
  myStyle = {
    'font-size': '15px',
    'border-top.px': 5,
    color: '#666',
  };
}

[3]. NgContent 指令

  • NgContent 指令的作用是帮助开发者创建可重用和可组合的组件。NgContent 指令的格式<ng-content></ng-content> 。我们可以将<ng-content> 理解为动态内容的占位符,在解析模板时,Angular将其替换为动态内容。在 Angular 中,这一操作被称为“内容投影”,意思是将内容从父组件投影到子组件中,子组件中包含NgContent 指令。
  • NgContent 指令类似于插值表达式,二者的不同之处在于插值表达式中的值来自组件类,而NgContent 指令的值来自组件的执行上下文。
  • 我们可以在组件内放入许多不同的东西,包括 HTML 标签甚至其他组件。
  1. 新建一个组件 src/app/add-button/add-button.component.ts,手动和通过命令创建可以参考(六) 父子组件交互 有详细的说明

    import { Component, OnInit, Output, EventEmitter } from '@angular/core';
    
    @Component({
      selector: 'app-add-button',
      template: `<button (click)="senMsg()">
        <!-- 预留一个占位符,用来显示按钮文本 -->
        <ng-content></ng-content>
      </button>`,
    })
    export class AddButtonComponent implements OnInit {
      @Output() sendBtn = new EventEmitter();
      msg = '公用button组件发送的消息';
    
      constructor() {}
    
      ngOnInit(): void {}
    
      senMsg() {
        this.sendBtn.emit(this.msg);
      }
    }
    
  2. 将新建的组件引入到 src/app/app.module.ts 文件中

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { AppComponent } from './app.component';
    
    // 引入组件
    import { AddButtonComponent } from './add-button/add-button.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        AddButtonComponent, // 声明组件
      ],
      imports: [BrowserModule],
      providers: [],
      bootstrap: [AppComponent],
    })
    export class AppModule {}
    
  3. 修改 src/app/app.component.ts 文件

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <app-add-button (sendBtn)="getMsg($event)"> 添加明细 </app-add-button>
    <app-add-button (sendBtn)="getMsg($event)">
      <span>插入HTML</span>
    </app-add-button>
    <app-add-button (sendBtn)="getMsg($event)">
      <app-add-button>组件插入组件</app-add-button>
    </app-add-button>
  `,
})
export class AppComponent {
  getMsg(msg) {
    console.log(msg);
  }
}

[4]. 使用 @ContentChildren() 装饰器获取 NgContent 中的子组件

  • @ContentChildren() 装饰器与内容子节点有关,它用于操作投影进来的内容。@ContentChildren() 装饰器与 @ViewChild() 装饰器的区别是,@ViewChild() 装饰器是与模板视图本身的子节点有关的,它用于操作模板自身的视图内容。

  • @ContentChild()@ContentChildren() 都是参数装饰器,分别用于从内容 DOM 中获取子元素或指令的查询对象(Query)和查询列表对象(QueryList )。每当添加或删除子元素/组件时Query 或 QueryList 都会更新。子元素或指令的查询在组件的生命周期 AfterContentlnit 开始时完成,因此在 ngAfterContentlnit() 方法中,就能正确获取查询的元素

  1. 新建一个子组件 src/app/son-cpn/son-cpn.component.ts,手动和通过命令创建可以参考(六) 父子组件交互 有详细的说明

    import { Component, Input } from '@angular/core';
    
    @Component({
      selector: 'app-son-cpn', // 子组件的标签
      template: `
        <h4>{{ tab.title }}</h4>
        <p>{{ tab.content }}</p>
      `,
    })
    export class SonCpnComponent {
      // 输入属性
      @Input() tab: TabInterFace;
    
      constructor() {}
    
      printTitle() {
        console.log(this.tab.title);
      }
    }
    
    // 定义一个 Tab 接口
    interface TabInterFace {
      title: string;
      content: string;
    }
    
  2. 新建一个父组件 src/app/parent-cpn/parent-cpn.component.ts,手动和通过命令创建可以参考(六) 父子组件交互 有详细的说明

    import {
      Component,
      AfterContentInit,
      ContentChildren,
      QueryList,
    } from '@angular/core';
    
    // 引入子组件
    import { SonCpnComponent } from './../son-cpn/son-cpn.component';
    
    @Component({
      selector: 'app-parent-cpn',
      template: `
        <p>
          <!-- 用来显示父组件(app.component.ts)投影进来的内容 -->
          <ng-content></ng-content>
        </p>
      `,
    })
    export class ParentCpnComponent implements AfterContentInit {
      constructor() {}
    
      // 使用@ContentChildren() 装饰器获取包含子组件 SonCpnComponent 的列表的 QueryList
      @ContentChildren(SonCpnComponent) tabList: QueryList<SonCpnComponent>;
    
      ngAfterContentInit(): void {
        // 调用子组件的方法
        this.tabList.toArray()[0].printTitle();
      }
    }
    
  3. src/app/app.module.ts文件中引入子组件,如果是通过 ng g c 命令创建的忽略这步,因为会自动添加

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { AppComponent } from './app.component';
    
    // 引入组件
    import { SonCpnComponent } from './son-cpn/son-cpn.component';
    import { ParentCpnComponent } from './parent-cpn/parent-cpn.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        SonCpnComponent, // 声明组件
        ParentCpnComponent, // 声明组件
      ],
      imports: [BrowserModule],
      providers: [],
      bootstrap: [AppComponent],
    })
    export class AppModule {}
    
  4. 修改src/app/app.component.ts文件

    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
        <app-parent-cpn>
          <app-son-cpn *ngFor="let tab of tabs" [tab]="tab"></app-son-cpn>
        </app-parent-cpn>
      `,
    })
    export class AppComponent implements OnInit {
      tabs = [];
    
      ngOnInit(): void {
        this.tabs = [
          {title: '第一个tab的标题', content: '第一个tab内容'},
          {title: '第二个tab的标题', content: '第二个tab内容'},
          {title: '第三个tab的标题', content: '第三个tab内容'}
        ]
      }
    }
    

(三) 创建指令

[1]. 概述

  • 指令和组件都是 Angular 对象,它们都对应于模板视图中的元素,并且可以修改生成的视图界面。

  • 指令和组件一样,都有对应的 Angular 类,类都有构造函数。指令类用 @Directive() 装饰器声明。 @Directive() 装饰器和 @Component() 装饰器类似,它接收的也是一个元数据。表列出了 @Directive() 装饰器的常见元数据配置选项。

    元数据配置选项说明
    selector对应的是 HTML 标签的属性名称,名称格式如[appSizer]
    inputs列举指令的一组可供数据绑定的输入属性
    outputs列举指令的一组可供事件绑定的输出属性
    host使用一组键/值对,把类的属性映射到宿主元素的绑定属性和事件
  • 指令和组件并不完全相同: 如组件需要视图,而指令不需要;指令没有模板,指令将行为添加到现有 DOM 元素上。

  • 创建指令的命令:

    ng generate directive 指令名称
    #简写
    ng g d 指令名称
    
  • 指令是 DOM 元素(如属性)上的标记,它告诉 Angular 将指定的行为附加到现有DOM 元素。这意味着我们需要一种方法来访问应用该指令的 DOM 元素,以及修改 DOM 元素的方法。Angular 为我们提供了两个非常有用的对象: ElementRef 对象和 Renderer2 对象。

    • ElementRef 对象: 可以通过 ElementRef 对象的 nativeElement 属性直接访问应用该指的 DOM 元素。
    • Renderer2 对象: Renderer2 对象可实现自定义渲染器,它提供了许多辅助方法,如可以通过该对象修改 DOM 元素的样式。
  • 我们可以将两者通过构造函数注入指令类中,并使每个DOM 元素成为私有实例变量。

    constructor(private element: ElementRef, private renderer: Renderer2){}
    

[2]. 在指令中访问 DOM 属性

  1. 使用命令创建指令文件 src/app/log.directive.ts,创建指令文件时,会自动在 src/app/app.module.ts 文件中引入,并在 declarations 中添加

    ng g d log
    
  2. 修改 src/app/log.directive.ts 内容如下:

    import { Directive, OnInit, Input, ElementRef, Renderer2 } from '@angular/core';
    
    @Directive({
      selector: '[log]', // HTML 标签的属性名称
      inputs: ['size'], // 绑定指令的输入属性
    })
    export class LogDirective implements OnInit {
      // 如果省略了 Directive 中的 inputs , 可以使用@Input() 装饰器进行声明
      // @Input() size:string
    
      size: string; // 与元数据中的输入属性对应
      constructor(private element: ElementRef, private renderer: Renderer2) {}
    
      ngOnInit(): void {
        this.renderer.setStyle(this.element.nativeElement, 'font-size', this.size);
      }
    }
    
  3. 修改 src/app/app.component.ts 内容如下:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: ` <button log size="25px">点击</button> `,
    })
    export class AppComponent {}
    

[3]. 在指令中监听事件

  • @Directive() 装饰器中的 inputs 元数据用于绑定属性,它还有一个host 元数据,用于绑定事件。
  • @Directive() 装饰器中的 host元数据是用来监听宿主元素对象的,host元数据接收一组键/值对数据。当Key为属性时,对应的是宿主元素的 DOM 属性,Value 对应的是具体的 DOM 属性的值; 当 Key 为事件名时,对应的就是监听宿主元素上的 DOM事件,Value 对应的是处理的方法。@Directive() 装饰器通过 host 元数据将指令的触发事件与指令类中的方法进行绑定。
  1. 使用命令创建指令文件 src/app/log.directive.ts,创建指令文件时,会自动在 src/app/app.module.ts 文件中引入,并在 declarations 中添加

    ng g d log
    
  2. 修改 src/app/log.directive.ts 内容如下:

    import { Directive, ElementRef, Renderer2 } from '@angular/core';
    
    @Directive({
      selector: '[log]', // HTML 标签的属性名称
      host: {
        // 绑定事件
        '(click)': 'onClick($event)',
      },
    })
    export class LogDirective {
      count = 0;
      textContent = '点击';
      constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
    
      onClick(event: Event) {
        console.log('click', event);
        this.count++;
        console.log('counts', this.count);
    
        // 修改按钮的文本
        this.renderer.setProperty(
          this.elementRef.nativeElement,
          'textContent',
          this.textContent + this.count
        );
    
        // 修改按钮的背景色
        this.renderer.setStyle(
          this.elementRef.nativeElement,
          'background-color',
          'yellow'
        );
      }
    }
    
  3. 修改 src/app/app.component.ts 内容如下:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: ` <button log>点击</button> `,
    })
    export class AppComponent {}
    

[4]. 在指令中使用 @HostBinding() 装饰器绑定 DOM 属性

  • 前面介绍了如何使用 ElementRef对象的nativeElement属性来访问宿主元素,以及如何使用 Renderer2对象操作其 DOM属性。这么做需要额外注入底层的 ElementRef对象和Renderer2对象。其实,Anqular 提供了一种简单的绑定 DOM 属性的实现方式,我们可以使用@HostBinding()装饰器绑定宿主元素上的 DOM 属性,从而处理 DOM 属性的值。
  • @HostBinding()装饰器仅接收一个元数据属性,hostPropertyName 表示绑定到宿主元素上的 DOM 属性;
    @HostBinding("hostPropertyName')
    
  1. 使用命令创建指令文件 src/app/log.directive.ts,创建指令文件时,会自动在 src/app/app.module.ts 文件中引入,并在 declarations 中添加

    ng g d log
    
  2. 修改 src/app/log.directive.ts 内容如下:使用该指令的DOM元素将会改变字体大小和背景颜色

    import { Directive, HostBinding, OnInit } from '@angular/core';
    
    @Directive({
      // 注意: 这个名字修改过
      selector: '[log]', // HTML标签的属性名称
      inputs: ['size'], // 绑定指令的输入属性
    })
    export class LogDirective implements OnInit {
      size: string; // 与元数据中的输入属性对应
    
      @HostBinding('style.font-size') // 绑定宿主元素的字体样式属性
      fontSize: string;
    
      @HostBinding('style.background-color') // 绑定宿主元素的背景样式属性
      backgroundColor: string;
    
      ngOnInit(): void {
        this.fontSize = this.size;
        this.backgroundColor = 'yellow';
      }
    }
    
  3. 修改 src/app/app.component.ts 内容如下:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppComponent } from './app.component';
    import { SonCpnComponent } from './son-cpn/son-cpn.component';
    import { LogDirective } from './log.directive';
    
    @NgModule({
      declarations: [AppComponent, SonCpnComponent, LogDirective],
      imports: [BrowserModule],
      providers: [],
      bootstrap: [AppComponent],
    })
    export class AppModule {}
    

[5]. 在指令中使用 @HostListener() 装饰器绑监听DOM事件

  • Angular也提供了一种简单的绑定事件的实现方式:使用 @HostListener()装饰器绑定宿主元素上的事件。

  • @HostListener()装饰器接收两个元数据属性,第一个元数据属性 eventName 是要监听的事件名;第二个元数据属性是选项参数,它是当该事件发生时传给组件类方法的一组选项参数(如果有多个参数,参数间以逗号分隔)。

    @HostListener('eventName',['$event'])
    
  1. 使用命令创建指令文件 src/app/log.directive.ts,创建指令文件时,会自动在 src/app/app.module.ts 文件中引入,并在 declarations 中添加

    ng g d log
    
  2. 修改 src/app/log.directive.ts 内容如下:使用该指令的DOM元素将会改变字体大小和背景颜色

    import { Directive, HostBinding, HostListener } from '@angular/core';
    
    @Directive({
      // 注意: 这个名字修改过
      selector: '[log]', // HTML标签的属性名称
    })
    export class LogDirective {
      count = 0;
      @HostBinding('textContent') // 绑定宿主元素的文本属性
      textContent = '点击';
    
      @HostListener('click', ['$event']) // 监听宿主元素的单击事件
      onClick(event: Event) {
        this.count++;
        this.textContent = '点击' + this.count;
      }
    }
    
  3. 修改 src/app/app.component.ts 内容如下:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `<button log>点击</button> `,
    })
    export class AppComponent {}
    

四、 模块

模块是包含一个或多个功能的软件组件或程序的一部分。一个或多个独立开发的模块组成一个完整的 Web 应用程序。企业级软件可能包含几个不同的模块,并且每个模块都提供唯一且独立的业务操作。模块之间没有父子关系,只能相互之间引用。模块是组织 Web 应用程序和使用外部程序库的最佳途径之一。

(一) 模块

[1]. 概述

  • 由 Angular 开发的 Web 应用程序是模块化的,它拥有自己的模块化系统,称作 NgModule 类。一个 NgModule 类就是一个容器,用于存放一些内聚的代码块,这些代码块专注于某个应用领域、某个工作流或一组紧密相关的功能。它可以包含一些组件、服务或其他代码文件,其作用域由包含它们的 NaModule 类定义。它还可以导入其他一些模块中的功能,并导出一些指定的功能供其他 NgModule 类使用。
  • Angular模块系统是将代码捆绑成可重用模块,Angular 系统代码本身使用此模块系统进行模块化。许多第三方软件为 Angular 模块提供了额外的功能,用户可以轻松地将这些模块包含在Web 应用程序中。
  • Angular 模块是带有 @NgModule()装饰器声明的类,Angular 模块的主要作用是管理指令、管道、组件。

[2]. 根模块

  • 每个由 Angular 开发的 Web 应用程序都至少有一个NgModule 类,也就是根模块,默认命名为 AppModule,它位于一个名叫 app.module.ts 的文件中。引导这个根模块就可以启动由Angular 开发的 Web 应用程序。
  • 由 Angular 开发的 Web 应用程序是通过引导根模块 AppModule 来启动的,引导过程还会创建 bootstrap 数组中列出的组件,并把它们逐个插入浏览器的 DOM 中。每个被引导的组件都是它自己的组件树的根组件。插入一个被引导的组件通常会触发一系列组件的创建并形成组件树。虽然也可以在主页面中放置多个组件,但是大多数 Web 应用程序只有一个组件树,并且只从一个根组件开始引导。这个根组件默认为 AppComponent,并且位于根模块的 bootstrap 数组中。
  • NgModule 类是一个带有 @NgModule() 装饰器的类,也称为 Angular 模块。 NgModule 类把组件、指令和管道打包成内聚的功能块,每个功能块聚焦于一个特定区域、业务领域、工作流或通用工具。 模块还可以把服务加到 Web 应用程序中。这些服务可能是内部开发的(如用户自己写的),或者来自外部(如 Angular 的路由和 HTTP 客户端 )。
  • @NgModue()装饰器是一个函数,它接收一个元数据对象,该元数据对象的属性用来描述模块,其中最重要的属性如下。
    • declarations 属性:属于该模块的组件、指令或管道被定义在这个属性中,属性列表中的内容一般都是用户自己创建的。
    • exports 属性:导出某些类,以便其他的模块可以使用它们。
    • imports 属性:导入其他模块,导入的模块都是使用 @NgModule()装饰器声明的,如 Angular 内置模块 BrowserModule 或第三方的 NaModule 类。
    • providers 属性:把提供 Web 应用程序级服务的提供商(Provider)定义在这个属性中提供商负责创建对应的服务,以便 Web 应用程序中的任何组件都能使用它。
    • bootstrap属性:Web应用程序的主视图,称为根组件。只有根模块才应该设置bootstrap属性。根据下面的用例,对照上面的描述来理解这些元数据对象的属性。
  1. src/app/app.module.ts文件如下,该文件是创建angualr 项目时自动生成的
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppComponent } from './app.component';
    // 自定义的组件
    import { SonCpnComponent } from './son-cpn/son-cpn.component';
    // 自定义的指令
    import { LogDirective } from './log.directive';
    
    @NgModule({
      declarations: [AppComponent, SonCpnComponent, LogDirective],
      imports: [BrowserModule],
      providers: [],
      bootstrap: [AppComponent],
    })
    export class AppModule {}
    

[3]. 特性模块

  • Angular 中除了根模块外,其他模块从技术角度来说都是特性模块。特性模块是用来对代码进行组织的模块。随着 Web 应用程序功能数量的增长,我们可能需要组织与特定 Web 应用程序有关的代码,这需要在不同特性之间划出清晰的边界。使用特性模块,可以把与特定的功能或特性有关的代码从其他代码中分离出来,为 Web 应用程序勾勒出清晰的边界,这有助于开发者之间、团队之间的协作,有助于分离各个指令,并帮助开发者管理根模块的大小。
  • 特性模块提供了聚焦于特定 Web 应用程序需求的一组功能,如用户工作流、路由或表单等。虽然用户也可以用根模块实现所有功能,但是特性模块可以把 Web 应用程序划分成一些聚焦的功能区。特性模块通过它提供的服务、共享的组件、指令、管道来与根模块和其他模块合作。
  • 特性模块具有以下特征。·
    • 与根模块一样,特性模块必须在 declarations 属性列表中声明所需的所有组件、指令和管道。
    • 特性模块不需要导入 BrowserModule 内置模块,一般导入 CommonModule,该模块包含Angular 的通用指令,如nglf、ngFor、ngClass 等。
    • 特性模块也不需要配置 bootstrap 属性。
  • 根模块是初始化时自动生成的,特性模块可以使用如下命令创建。
    ng g module name #创建特性块
    ng g module name --routing #创建带路由的特性模块
    

(二) 常用内置模块

  • 无论是根模块还是特性模块,其实都可以引用这些内置模块。换句话说,内置模块根据需要都可以导入 @NgModule() 装饰器的 imports 属性中。
内置模块导入包的路径内置模块介绍
BrowserModule@angular/platform-browser默认导入,这是在浏览器中运行该 Web 应用程序所必需的
CommonModule@angular/common包含内置指令,如 Nglf和 NgFor 等
FormsModule@angular/forms当要构建模板驱动表单时采用它包含 NgModel
ReactiveFormsModule@angular/forms当要构建响应式表单时采用
RouterModule@angular/router要使用路由功能,并且要用到 RouterLink对象的 forRoot()方法和 forChild()方法
HttpClientModule@angular/common/http当需要访问 HTTP 服务时采用

(三) 模块业务分类

  • 随着项目功能数量的增长,用户创建的组件、指令和管道越来越多,由于它们必须要被导入根模块中,因此根模块会越来越大,代码开始变得混乱,难以阅读。
  • 常规业务角度的分类原则:Angular 模块从业务上可以分为根模块(AppModule)、核心模块(CoreModule)、共享模块(SharedModule)和其他特性模块。

[1]. 理解核心模块

  • 核心模块的定位是应该仅包含服务,并且仅被根模块 AppModule 导入。从技术角度分析核心模块,它遵循下面的准则。
    • 核心模块中包含使用 Web 应用程序启动时加载的单例服务(全局中仅存在一个实例的服务 )。
    • 核心模块是仅在根模块 AppModule 中导入一次,而在其他模块中不再导入的模块。
    • 核心模块的 @NgModule()装饰器中的 declarations 属性列表和 exports 属性列表均保持为空。
  • 关于 Angular 中的单例服务是这么定义的:把该服务包含在根模块 AppModule 或某个只会被根模块 AppModule 导入的模块中。而核心模块的定义就是仅被根模块 AppModule 导入,因此在核心模块中定义的服务就是单例服务。
  • 在开发实践中,有些全局性的类服务也需要放置在核心模块中。如有一个用户模块(User-Module),其中包含注册服务(SinUpService)、登录服务(SianlnService)、认证服务(SocialAuthService)和查询个人信息服务(UserProfileService)之类的服务。如果在核心模块中导入UserModule,那么 UserModule 的所有服务都将在整个 Web 应用程序范围内可用。根据上面的准则,UserModule 不应该具有声明(declarations)或导出(exports),而应该只有服务提供者( providers ).
  • 综上所述,核心模块又可以称为核心服务模块。

[2]. 防止重复导入核心模块

  • 只有根模块 AppModule 才能导入核心模块。如果一个其他特性模块也导入了它,该 Web 应用程序就会为服务生成多个实例。 要想防止其他特性模块重复导入核心模块,可以在该核心模块中添加如下函数。

    constructor (@Optional() @SkipSelf()parentModule: CoreModule){
    	if (parentModule){
    		throw new Error( 'CoreModule已加载过了,它仅可以被导入AppModule');
    	}
    }
    
  • 上述代码中的 CoreModule 可替换为具体的核心模块。该构造函数要求 Angular 把核心模块注入它自己。如果 Angular 在当前注入器(Injector)中查找核心模块,这次注入就会导致死循环。@SkipSelf()装饰器的意思是,在注入器树中层次高于自己的注入器中查找核心模块。

  • 正常情况下,该核心模块是第一次被导入根模块 AppModule 中并加载,找不到任何已经注册过的核心模块实例。默认情况下,当注入器找不到服务时,会抛出一个错误。但 @Optional() 装饰器表示找不到服务也无所谓。于是注入器会返回 null,parentModule 参数也就被赋成了空值,构造函数中的 if()方法就不会执行。 如果在根模块 AppModule 中找到了实例,那么 parentModule参数为 true,接着就会抛出一个错误信息。

[3]. 理解共享模块

  • 创建共享模块的目的是更好地组织和梳理代码。用户可以把常用的指令、管道和组件放进共享模块中,然后在 Web 应用程序中其他需要这些的地方导入共享模块。从技术角度分析,共享模块遵循下面的准则。
    • 把在 Web 应用程序中各处重复使用的组件、指令和管道集中放进一个共享模块。此共享模块应完全由声明组成,并且其中大多数被重新导出,以供其他模块共享。
    • 共享模块可能会重新导出 Widget小部件(可以理解为简单的组件、指令和管道的组合),如 CommonModule、FormsModue 和其他的 UI 模块。
    • 共享模块不应该具有 providers。它的任何导入或再导出模块都不应具有 providers。
    • 共享模块仅被需要的特性模块导入,包括在 Web 应用程序启动时加载的模块和以后加载的模块。
  • 如有一个 UIModule 模块,其中包含 ButtonComponent组件、NavComponent组件、SlideshowComponent 组件、HighlightLinkDirective 指令和 CtaPipe 管道。根据上面的准则UIModule 模块中包含的组件、指令和管道需要再次导出,然后在需要使用它的特性模块中导入UIModue 模块,就可以使用其中的一个或者全部 的 Widget 小部件。简单地说,共享模块里仅包含 Widget 小部件,在被特性模块导入后,可以直接在特性模块中使用这些 Widget 小部件。

五、管道

(一) 内置管道

[1]. async 管道

async 管道即异步管道,它会订阅一个可观察对象或 Promise 对象,并返回它发出的最近一个值。当新值到来时,async 管道就会把组件标记为需要进行变更检测。当组件被销毁时async 管道就会自动取消订阅,以消除潜在的内存泄漏隐患。

import { Component } from '@angular/core';
import { interval, Observable, of } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    <p>当前日期:{{ currentTime$ }}</p>
    <p>当前日期2:{{ currentTime$ | async }}</p>
    <p>当前日期3:{{ currentTime$ | async | date : 'yyyy-MM-dd HH:MM:ss' }}</p>
  `,
  styles: []
})
export class AppComponent {
  currentTime$: Observable<number>;

  constructor() {
    // 每秒触发一下订阅
    interval(1000).subscribe(() => {
      this.currentTime$ = of(Date.now());
    });
  }
}

在这里插入图片描述

[2]. currency 管道

currency 管道负责把数值转换成金额,currency 管道相对其他管道来说比较灵活,它可以根据配置项进行灵活的格式化。currency 管道的使用语法如下。

{{ 数值表达式 | currency [ : currencyCode [ : display [ : digitsInfo[ : locale ]]]]}}
配置选项类型说明
currencyCode(可选)string货币代码,如 JPY 表示日元,USD 表示美元(默认)
display ( 可选)string
boolean
货币指示器的格式(可选,默认值是 symbol),有效值如下
code:显示货币代码(如 USD)
symbol(默认):显示货币符号(如$)
symbol-narrow:使用区域的窄化符号。如加拿大元的符号是CA$,而其窄化符号是 $
String :使用指定的字符串值代替货币代码或符号。空字符串将会去掉货币代码或符号
digitsInfo ( 可选)string数字展现的选项,通过格式为 {a}.{b}-{c}的字符串指定
a:小数点前的最小位数,默认为 1
b:小数点后的最小位数,默认为 0
c:小数点后的最大位数,默认为 3
locale(可选)string要使用的本地化格式代码;如果未提供,默认为 en-US
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <!-- $0.26 -->
    <p>{{ a | currency }}</p>
    <!-- $123.00 -->
    <p>{{ b | currency }}</p>
    <!-- $45.60 -->
    <p>{{ c | currency }}</p>
    <hr />
    <!-- ¥0 -->
    <p>{{ a | currency : 'JPY' }}</p>
    <!-- ¥123 -->
    <p>{{ b | currency : 'JPY' }}</p>
    <!-- ¥46 -->
    <p>{{ c | currency : 'JPY' }}</p>
    <hr />
    <!-- ¥0.26 -->
    <p>{{ a | currency : 'JPY' : 'symbol' : '1.1-2' }}</p>
    <!-- ¥123.0 -->
    <p>{{ b | currency : 'JPY' : 'symbol' : '1.1-2' }}</p>
    <!-- ¥45.6 -->
    <p>{{ c | currency : 'JPY' : 'symbol' : '1.1-2' }}</p>
    <hr />
    <!-- ¥0.26 -->
    <p>{{ a | currency : '¥' }}</p>
    <!-- RMB123.00 -->
    <p>{{ b | currency : 'RMB' }}</p>
    <!-- CA$45.60 -->
    <p>{{ c | currency : 'CAD' }}</p>
    <!-- CAD45.60 -->
    <p>{{ c | currency : 'CAD' : 'code' }}</p>
    <!-- $0,045.60 -->
    <p>{{ c | currency : 'CAD' : 'symbol-narrow' : '4.2-2' }}</p>
  `,
  styles: []
})
export class AppComponent {
  a = 0.258;
  b = 123;
  c = 45.6;
}

[3]. date 管道

具体的可以查看官方文档: https://v10.angular.cn/api/common/DatePipe

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<p>
    {{ currentTime | date : 'yyyy-MM-dd HH:MM:ss' }}
  </p>`,
  styles: []
})
export class AppComponent {
  currentTime = Date.now();
}

[4]. i18nSelect 管道

i18nSelect 管道类似一个通用选择器,显示匹配当前值的字符串。i18nSelect 管道的使用场景:如在数据中存在性别字段,它的值一般为 0和1,但是期望在页面上显示中文的性别,这时可以考虑使用i18nSelect 管道,请参阅下面的示例。

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<p>
    {{ female | i18nSelect : dicMap }}
  </p>`,
  styles: []
})
export class AppComponent {
  female = '0';
  dicMap = { '0': '女', '1': '男' };
}

(二) 自定义管道

  • 实现自定义管道的步骤可以分为 3 步。
    • (1)自定义一个管道类,该类需要实现 PipeTransform 接口,并实现接口中的 transform0方法。
    • (2)用@Pipe() 装饰器声明该类,并且通过装饰器中的元数据 name 属性定义管道的名字管道名一般推荐小写字符串形式。
    • (3)注册自定义管道类。将管道类导入NgModule 类中的 declarations 数组中。
  • @Pipe() 装饰器中除了 name 属性外,还有 pure 属性,它表示该管道是否是纯管道:pure 属性值等于 true 时,表示为纯管道,意思是当 transform() 方法中的参数发生变化时,管道才执行方法中的逻辑;反之,则为非纯管道。pure 属性为可选项,默认值为 true,Angular 中的内置管道都属于纯管道。
  • Angular 提供了Angular CLI命令“ng generate pipe 管道类”生成自定义管道。
  1. 新建一个项目

    ng new demo-pipe -s -t --style=css --routing=false --minimal
    
  2. 使用命令新建 orderby 管道文件(位置:src/app/orderby.pipe.ts)

    #简写 ng g p orderby
    ng generate pipe orderby
    
  3. 如果使用命令创建的管道,管道将在 src/app/app.module.ts 文件被自动引入

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppComponent } from './app.component';
    // 引入管道
    import { OrderbyPipe } from './orderby.pipe';
    
    @NgModule({
      declarations: [
        AppComponent,
        OrderbyPipe // 声明管道
      ],
      imports: [BrowserModule],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule {}
    
  4. 修改 orderby 管道文件(位置:src/app/orderby.pipe.ts)

    import { Pipe, PipeTransform } from '@angular/core';
    
    @Pipe({
      name: 'orderby'
    })
    export class OrderbyPipe implements PipeTransform {
      transform(value: Array<unknown>, ...args: unknown[]): Array<unknown> {
        if (args.length === 0 || args[0] === 'asc') {
          return value.sort();
        } else if (args[0] === 'desc') {
          return value.sort().reverse();
        }
    
        return value;
      }
    }
    
  5. 修改 src/app/app.component.ts 组件

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `<p>{{ fruits | orderby }}</p>
        <p>{{ fruits | orderby : 'desc' }}</p>`,
      styles: []
    })
    export class AppComponent {
      fruits = ['apple', 'tomato', 'banana'];
    }
    
  6. 页面显示
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值