我学会 Angular 了

1,992 阅读45分钟

最近浅学了一下 Angular, 总体感觉是一个比较重量型的框架,但是其中提出的一些概念绝对能让人耳目一新。本文是我结合 B 站视频入门 Angular 的一些随笔,有点乱,写下来以作纪念。

1. Angular 应用程序

请首先记住!使用 Angular 创建的应用程序都是单页面应用程序 SPA

1.1 使用 Angular 构建应用程序主要在于其下面一些优点:

IMG_2099.jpg

IMG_2098.jpg

1.2 Angular 的应用程序则是由一个个的组件堆叠构成的:

IMG_2101.jpg

1.3 Angular 中的组件可以看成是有 模板、类和元信息构成的。

IMG_2102.jpg

或者更加详细的表示为:

IMG_2111.jpg

1.4 Angular 中组件和模块的关系:

IMG_2103.jpg

模块又可分为 根模块、特性模块和共享模块:

IMG_2108.jpg

Angular 中的模块和 ES6 中的模块对比:前者是应用层面上组织后者是代码层面上的组织

IMG_2109.jpg

Angular 中常见的核心模块:

IMG_2116.jpg

1.5 学习 Angular 的基本知识点有:

  • 组件
  • 模板、数据绑定、指令
  • 服务和依赖注入
  • http 和 Observable 对象
  • 导航和路由
  • 模块
  • 脚手架

2. 从创建一个新的 Angular 应用程序开始

2.1 创建 Angular 应用程序的步骤为:

  • 创建根目录
  • 创建项目和组件的配置文件
  • 初始化项目
  • 创建根模块
  • 创建入口文件
  • 创建根 html

2.2 创建一个 Angular 组件

启动 Angular 应用首先需要创建一个入口组件 App Component,创建这个根组件的过程为:

  • 创建一个 ES6 Class
  • 使用特定的装饰器修饰这个类
  • 在装饰器中配置元信息
  • 导入组件需要环境

如下所示:

IMG_2112.jpg

上述代码各个部分内容作用详解:

IMG_2114.jpg

2.3 补充一些装饰器的相关知识:

装饰器(Decorator)是一种设计模式,用于在不修改原有对象结构的情况下,动态地给对象添加额外的功能。在JavaScript中,装饰器是一种特殊类型的声明,它允许我们修改类的行为。

在ES6(ECMAScript 2015)中,装饰器是一种实验性特性,它允许我们以声明方式添加类的功能。装饰器允许我们在类、方法、属性或参数上添加额外的逻辑,而不需要改变原有的代码。

以下是一些基本的装饰器概念:

  1. 类装饰器:用于类本身,可以用于修改类的行为。
  2. 方法装饰器:用于类的某个方法,可以修改方法的行为。
  3. 访问器装饰器:用于类的getter和setter,可以修改访问器的行为。
  4. 属性装饰器:用于类的属性,可以修改属性的行为。

在ES6中,装饰器的语法如下:

// 类装饰器示例
function myClassDecorator(constructor) {
  console.log('类装饰器被调用');
  return class extends constructor {
    constructor(...args) {
      super(...args);
      console.log('子类构造函数');
    }
  };
}

@myClassDecorator
class MyClass {
  constructor() {
    console.log('MyClass 构造函数');
  }
}

// 方法装饰器示例
function myMethodDecorator(target, propertyKey, descriptor) {
  console.log('方法装饰器被调用');
  const originalMethod = descriptor.value;
  descriptor.value = function(...args) {
    console.log('方法装饰器 - 前置操作');
    originalMethod.apply(this, args);
    console.log('方法装饰器 - 后置操作');
  };
}

class MyClass {
  @myMethodDecorator
  myMethod() {
    console.log('MyMethod 执行');
  }
}

// 访问器装饰器示例
function myAccessorDecorator(target, propertyKey) {
  console.log('访问器装饰器被调用');
  const originalGetter = target[propertyKey].get;
  const originalSetter = target[propertyKey].set;

  target[propertyKey].get = function() {
    console.log('访问器装饰器 - getter 前置操作');
    return originalGetter.call(this);
  };

  target[propertyKey].set = function(value) {
    console.log('访问器装饰器 - setter 前置操作');
    originalSetter.call(this, value);
  };
}

class MyClass {
  constructor() {
    this._value = 0;
  }

  @myAccessorDecorator
  get value() {
    return this._value;
  }

  set value(v) {
    this._value = v;
  }
}

// 属性装饰器示例
function myPropertyDecorator(target, propertyKey) {
  console.log('属性装饰器被调用');
}

class MyClass {
  @myPropertyDecorator
  myProperty;
}

总而言之,装饰器提供了一种强大的方式,可以在不修改原有代码的情况下增强类和方法的功能,使得代码更加模块化和可重用。

2.4 将根组件放入根模块中作为启动组件

在根模块 AppModule 中的元信息中添加 declarations 和 bootstrap 数组,然后放入 AppComponent, 这样就完成了启动组件的设置。

IMG_2120.jpg

根组件会被在入口 index.html 中被使用:

IMG_2121.jpg

标签 pm-root 正好对应的就是 AppComponent 的 selector 元信息。

这里简单捋一捋,按理来说 html 中才没有什么 pm-root 标签才对,之所以能被渲染出来,在于自定义的解析逻辑。而提供这个自定义解析 html 中标签的编译环境/功能则是由引用 AppComponent 的 AppModule 提供的。所以至少可以得出以下结论:

  • html 是自定义标签的使用者。
  • component 是自定义标签的定义者。
  • module 是自定义标签的解析者。

现在有一个大概的印象了吗?

下图展示的是父组件如何调用子组件的过程,符合上述结论。

image.png

2.5 Module 提供了 Component 的编译环境

如上图所示,在 app.component.ts 中的 template 中使用 pm-products 标签的环境是谁提供的?

上面只说了 App Module 提供了 pm-root 的编译环境(因为在 AppModule 中的元信息中添加 declarations 和 bootstrap 数组,然后放入了 AppComponent)

其实,让 html 中的 pm-product 组件生效还需要做同样的工作:

image.png

也就是在 declarations 元信息中添加 ProductListComponent 组件。

3. Angular 中的视图层 -- 模板

3.0 模板的构建

熟悉 Vue 的朋友都知道,Vue 中的指令是抄袭 Angular 的。Angular 中的指令如 *ngIf 或者 *ngFor 是作用于模板 html 提供强大、灵活的模板构建功能。

<div class='table-responsive'>
    <table class='table' *ngIf='products && products.length'>
        <thead>
        </thead>
        <tbody>
        </tbody>
    </table>
</div>
<tr *ngFor='let product of products'>
    <td></td>
    <td>{{ product.productName}}</td>
    <td>{{ product.productCode }}</td>
    <td>{{ product.releaseDate }}</td>
    <td>{{ product.price }}</td>
    <td>{{ product.starRating }}</td>
</tr>

不过,使用条件或者循环指令,需要引入编译环境 BrowserModule, 而在上面已经引入了。

那么模板究竟是什么呢?刚才已经说过了,模板中可以使用自定义的标签名,如 pm-root, 而模板本身就是被 component 定义出来的,定义模板的方式有以下三种

IMG_2124.jpg

上图中的 pageTitle 实际上是 class 中的属性,Angular 中一个组件的 class 和 template 就是通过这种方式进行通信的。

for..in 和 for..of 的区别是什么?前者变量 Object 对象上的 key 后者遍历 Object 对象提供的 iterator 接口。

3.1 class 和 template 的通信

IMG_2134.jpg

简单来说数据从 class 到 template 的方式为属性传递:[]=''

IMG_2136.jpg

而从 template 到 class 的方式为事件函数回调:()=''

IMG_2138.jpg

在 template 中也可以写一些行内样式,如下所示,有点奇怪。

IMG_2137.jpg

对于表单组件,和 Vue 相同, Angular 中有双向绑定的概念:[()]=''

IMG_2141.jpg

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

@Component({
  selector: 'app-root',
  template: `
    <input [(ngModel)]="name" placeholder="Enter name">
    <p>Hello {{ name }}!</p>
  `
})
export class AppComponent {
  name: string = ''; // 初始化name变量
}

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

@Component({ selector: 'app-root', template: <input [(ngModel)]="name" placeholder="Enter name"> <p>Hello {{ name }}!</p> }) export class AppComponent { name: string = ''; // 初始化name变量 }需要注意的是,使用双向绑定需要提供此指令的编译环境,如下所示:

image.png

最后将上面知识融合起来做一个点击按钮展示/隐藏图片的效果:

  1. Angular组件的HTML模板 (app.component.html)
<button (click)="toggleImage()">Toggle Image</button> <!-- 绑定点击事件 -->
<img *ngIf="showImage" [src]="imagePath" alt="Descriptive Text" /> <!-- 使用ngIf指令 -->
  1. Angular组件的TypeScript类 (app.component.ts)
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  // 初始化showImage变量为false,表示图片默认不显示
  showImage: boolean = false;
  imagePath: string = "path/to/your/image.jpg";

  // 定义一个方法来切换showImage变量的值
  toggleImage(): void {
    this.showImage = !this.showImage;
  }
}

总结一下 class 和 template 的信息传递:

IMG_2146.jpg

3.2 三元表达式和管道

这两个特性都是为了方便模板的内容服务的。

IMG_2140.jpg

Angular 中的管道有内置的和自定义的,作用是在数据展示之前对其进行格式化。常见的内置管道有:

  • data
  • number decimal percent currency
  • json slice

既然叫管道,那就能多个连接起来使用。并且管道还可以通过冒号传递参数,如下所示:

IMG_2145.jpg

4. 再看组件

可以通过下面的几个方面增强 Angular 组件:

  • 定义类型和接口
  • 使用隔离的样式
  • 定义生命周期函数
  • 使用自定义管道
  • 调用子组件,或进行组件间的嵌套

4.1 组件的接口

参考下面这张图来定义接口:

IMG_2154.jpg

4.2 样式隔离

在组件的元信息中可以定义 css 样式文件,并且是自动隔离的,这是 Angular 的一大优势。

image.png

当然,对于简单的样式可以直接写在元信息中:

IMG_2157.jpg

4.3 组件的生命周期

IMG_2159.jpg

使用 hook 的三个步骤:从 @angular/core 引入对应的 hook 名称;在 class 上声明要 implement 这个 hook 接口;在 class 中实现这个接口。

4.4 使用自定义的管道

下图展示的是我们可以通过管道的调用方法来推断管道的构造方式:

IMG_2164.jpg

我们甚至还可以在其它指令中使用管道:

<tr *ngFor='let product of products l productFilter: listFilter'>

需要注意的是,使用自定义管道需要事先声明,如下图所示:

IMG_2165.jpg

通过脚手架可以方便的创建一个自定义管道。使用Angular CLI的ng generate pipe命令。下面将介绍如何创建一个管道,该管道可以根据用户指定的分隔符来转换字符串。

首先,使用Angular CLI创建一个新的管道:

ng generate pipe split-string

这将在你的Angular项目中创建一个名为split-string的新管道文件。

  1. 更新管道的TypeScript文件 (split-string.pipe.ts)
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'splitString'
})
export class SplitStringPipe implements PipeTransform {
  // 实现 PipeTransform 接口的 transform 方法
  transform(value: string, delimiter: string): string[] {
    // 使用正则表达式来处理分隔符,确保分隔符可以是特殊字符
    const regex = new RegExp(`\\${delimiter}`, 'g');
    return value.split(regex).filter(part => part !== '');
  }
}
  1. 在模块中声明管道

确保在你的Angular模块(通常是app.module.ts)中导入并声明了你的管道:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { SplitStringPipe } from './split-string.pipe'; // 引入管道
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
    SplitStringPipe // 声明管道
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
  1. 使用管道在你的组件模板中

在你的组件模板中,你可以这样使用split-string管道:

<!-- 使用管道并指定分隔符 -->
<p>{{ '这里是一些,需要被分割的,字符串' | splitString:',' }}</p>

在这个例子中,splitString管道接受一个参数(分隔符),它将根据这个分隔符来分割字符串。管道的transform方法接收两个参数:value是要转换的字符串,delimiter是分隔符。该方法返回一个字符串数组。

很多开发者不建议使用管道的方式对数据进行排序和过滤,他们认为这件事应该由组件自身完成,而不是依靠管道,下面的代码就是通过 getter/setter 实现的一个过滤功能。

import { Component, OnInit } from '@angular/core';
import { IProduct } from "./product";

@Component({
  selector: 'pm-products',
  templateUrl: "./product-list.component.html",
  styleUrls: ["./product-list.component.css"]
})
export class ProductListComponent implements OnInit {
  pageTitle: string = 'Product List';
  imageWidth: number = 50;
  imageMargin: number = 2;
  showImage: boolean = false;
  listFilter: string;
  products: IProduct[] = [];
  filteredProducts: IProduct[] = [];

  get listFilter(): string {
    return this._listFilter;
  }

  set listFilter(value: string) {
    this._listFilter = value;
    this.filteredProducts = this._listFilter ? this.performFilter(this._listFilter) : this.products;
  }

  private _listFilter: string = '';

  ngOnInit() {
    // Assume that this method fetches products and assigns them to this.products
  }

  performFilter(filterBy: string): IProduct[] {
    // Assume this method performs the actual filtering based on the filterBy string
    // For now, it's just a placeholder for the actual implementation
    return this.products.filter(product =>
      product.name.toLowerCase().includes(filterBy.toLowerCase())
    );
  }
}

4.5 使用子组件

使用子组件,或者完成组件之间的嵌套涉及信息传递等诸多方面,见第 5 小节。

5. 制作一个内嵌组件

关于内嵌组件需要掌握的知识点有:

  • 创建一个子组件
  • 使用子组件
  • 使用修饰符 @input 传递参数
  • 使用修饰符 @output 传递消息

5.1 内嵌组件的组织关系

image.png

5.2 使用生命周期函数 ngOnChanges 感知子组件输入值变化

在Angular中,ngOnChanges是一个Angular组件的生命周期钩子,它会在Angular检测到数据绑定输入属性的值发生变化时被调用。这个生命周期钩子对于响应由父组件传递的属性值变化非常有用。

下面是一个简单的Angular组件示例,其中包含了ngOnChanges生命周期钩子的使用:

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `<h2>{{ name }}</h2>`
})
export class ChildComponent implements OnChanges {
  @Input() name: string;

  ngOnChanges(changes: SimpleChanges) {
    for (let propName in changes) {
      let change = changes[propName];
      let curVal  = JSON.stringify(change.currentValue);
      let prevVal = JSON.stringify(change.previousValue);

      console.log(`${propName}: currentValue = ${curVal}, previousValue = ${prevVal}`);
    }
  }
}

在这个示例中,ChildComponent是一个简单的Angular组件,它接受一个名为name的输入属性。当name属性的值发生变化时,ngOnChanges生命周期钩子会被触发。

ngOnChanges方法接受一个SimpleChanges对象作为参数,该对象包含了所有发生变化的输入属性的信息。在ngOnChanges方法内部,我们可以通过遍历changes对象来检查每个属性的变化,并获取变化前后的值。在这个示例中,我们简单地将变化前后的值打印到控制台。

需要注意的是,ngOnChanges只会在组件的输入属性值发生变化时被调用,并且它会在每个检测周期中多次调用(如果输入属性持续发生变化)。此外,ngOnChanges会在ngOnInit之前被调用,因此它特别适用于在组件初始化之前需要对输入属性进行预处理或验证的场景。

image.png

5.3 在 root module 中声明的组件在应用的任何 html 中都可以使用

import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { ProductListComponent } from './product-list.component';
import { ConvertToSnacesPipe } from './convert-to-snaces.pipe'; // 注意这里可能有拼写错误,应为 ConvertToSnakeCasePipe
import { StarComponent } from './shared/star.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule // 导入FormsModule以支持表单功能
  ],
  declarations: [
    AppComponent, // 声明根组件
    ProductListComponent, // 声明产品列表组件
    // ConvertToSnacesPipe, // 这里可能有拼写错误,应为 ConvertToSnakeCasePipe
    ConvertToSnakeCasePipe, // 假设更正为正确的转换为蛇形命名的管道
    StarComponent // 声明共享的Star组件
  ],
  bootstrap: [AppComponent] // 指定要自举的根组件
})
export class AppModule { }

5.4 使用 @input 指令完成父组件向子组件传参

image.png

image.png

image.png

使用 []='' 向子组件传递参数,并使用 @input 修饰的属性进行接受。这样在父组件输入改变的时候,子组件就会自动更新了;而子组件更新的时候又会调用 ngOnchanges 钩子函数,引起 starWidth 被动更新,并同时修改样式。最终这些更新全部显示到页面上。

image.png

5.5 使用 @output 指令完成子组件向父组件传递消息

子组件中的内容发生了变化,如果想要将变化的内容传递给父组件,就需要使用到 @ouput 修饰符。

image.png

父组件向子组件传递消息走的是 “属性” ,而子组件向父组件传递消息走的是 “事件” ,这主要还是由于 “单向渲染” 的根本特性决定的

image.png

大概的过程是这样的:

  • 发送消息方:子组件中会创建一个 notify 属性专门用来传递消息。这个 notify 属性的值是一个 EventEmitter 类型的对象;然后使用修饰符 @Output 修饰这个 notify。接下来,在子组件中创建一个普通方法作为 html 中元素的回调函数,如上图中的 onClick. 当 onClick 被执行的时候会调用实例上的 notify 对象上的 emit 方法广播消息。
  • 接受消息方:注意这个时候接受消息的是父组件,记为 container. 在 container 调用子组件的时候会订阅 notify 广播的消息。订阅的形式就是将 notify 和 container 的方法绑定起来。注意上图中的 (notify)='onNotify($event)'(click)='onClick($event)' 没有任何区别。一旦绑定成功,那么子组件广播的消息就可以被父组件中的名为 onNotify 的方法处理了。
  • 总结一下,父传子用的是 props, 子传父用的是 event Emitter, 或者自定义事件,而发送自定义事件用的是自定义的事件发生器。

6. 服务和依赖注入

Angular 中的 Service 是一个独立于组件的,提供可复用数据和逻辑的,封装了外部交互的 ES6 中的类。它是一个具有专注目的的类。 用于具有以下特点的功能: 独立于任何特定组件、在组件之间提供共享数据或逻辑、封装外部交互。

我们从以下几点来了解 Service:

  • 服务是如何工作的
  • 如何创建一个服务
  • 创建的服务注册适用范围
  • 组件中注入服务

下图展示依赖注入的过程:

image.png

看起来,在一个 Component 实例化的时候会自动的从构造参数处获取 Service 实例。

这个过程由于非常契合编程中注入的概念,因此有一个固定的名称:依赖注入

所谓注入,指的就是一种编程模式,当一个类在实例化的时候从构造参数接受了另外一个类实例,并使用这个外部实例上的方法完成功能而不是构建自己的方法完成这个功能。这个引用外部类实例的过程就是“注入”的过程。

6.1 创建一个服务

下面我们来一起制作一个自定义 Service:

image.png image.png image.png

我们可以在 Service 构建从后端请求数据的方法 getProducts,然后在组件中使用 Service 注入实例上的 getProducts 方法以获取想要的数据。

image.png

构建一个 Service 就是构建一个简单的 ES6 中的类。然后使用装饰器 @Injectable 修饰即可。下面的问题在于怎么用?参考 Components 的用法,不难想到,我们需要在 root Module 中声明这个 Service 的引用。

image.png

你可以选择将 Service 注入到全局或者某个特定组件上,它们的区别在于:

全局服务注入(Root Injector)

  1. 注册方式:服务在根注入器(Root Injector)中注册,通常是在应用的主模块(AppModule)中。
  2. 可用性:注册为全局服务后,该服务在整个应用中都可用,可以注入到任何组件、指令或其他服务中。
  3. 推荐场景:对于大多数场景,如果服务需要在多个组件之间共享数据或逻辑,推荐使用全局服务注入。
  4. 示例
    // app.module.ts
    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { AppComponent } from './app.component';
    import { MyService } from './my.service';
    
    @NgModule({
      declarations: [AppComponent],
      imports: [BrowserModule],
      providers: [MyService], // 注册为全局服务
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    

组件服务注入(Component Injector)

  1. 注册方式:服务在特定组件的注入器中注册,通常是在组件的providers数组中。
  2. 可用性:注册为组件服务后,该服务仅在该组件及其子组件(嵌套组件)中可用。
  3. 隔离性:如果一个服务仅被一个组件使用,或者需要隔离服务的状态,可以注册为组件服务。
  4. 多实例:在组件注入器中,可以为不同的组件提供服务的多个实例。
  5. 示例
    // product-list.component.ts
    import { Component } from '@angular/core';
    import { MyService } from './my.service';
    
    @Component({
      selector: 'app-product-list',
      template: `...`,
      providers: [MyService] // 注册为组件服务
    })
    export class ProductListComponent {
      constructor(private myService: MyService) {
        // 使用 myService
      }
    }
    
  • 全局服务注入适用于需要跨多个组件共享的服务,它简化了依赖管理,并且可以减少服务实例的数量。
  • 组件服务注入适用于特定组件专用的服务,或者需要隔离状态的场景。它提供了更高的灵活性,允许为不同的组件创建服务的多个实例。

在实际开发中,应该根据服务的用途和需求来选择合适的注入方式。全局服务注入是更常见的做法,而组件服务注入则适用于特定的用例。

6.2 全局和部分组件注册方式

注意,在构建 Service 的时候就已经通过 providedIn 这个配置属性声明了适用范围:

image.png

如下如所示,你可以使用第一种和第三种方式完成全局注入,或者使用第二种方式完成特定组件注入:

image.png

目前第三种方式已经被第一种方式替代了,因为第一种可以被 Tree Shaking, 于性能有利。 也就是说从形式上来看,如果要注入到全局则在其元数据中即可声明,而如果只注入到特定组件则需要在该组件的元数据中声明

6.3 更多参考信息

在Angular中,服务(Service)是一种单例模式的对象,它被设计用来为多个组件提供共享的数据或功能。服务可以在应用程序的任何地方被注入和使用,它们通常用于封装业务逻辑、数据获取、数据共享、工具函数等。

服务的创建和注入
  1. 创建服务: 首先,你需要使用Angular CLI的ng generate service命令来创建一个新的服务文件。

    ng generate service myService
    

    这将创建一个名为my-service.service.ts的新文件,并自动包含服务的基础结构。

  2. 定义服务: 在服务文件中,你可以定义方法和属性,这些方法和属性可以被应用程序的其他部分调用。

    import { Injectable } from '@angular/core';
    
    @Injectable({
      providedIn: 'root' // 这表示服务是单例的,由根模块提供
    })
    export class MyService {
      constructor() {}
    
      getData() {
        // 返回一些数据
        return 'Some data';
      }
    }
    
  3. 注入服务: 服务可以通过构造函数注入到组件、其他服务或任何Angular类中。Angular的依赖注入系统会自动处理服务的实例化。

    在组件中注入服务:

    import { Component } from '@angular/core';
    import { MyService } from './my-service.service';
    
    @Component({
      selector: 'app-my-component',
      templateUrl: './my-component.component.html'
    })
    export class MyComponent {
      constructor(private myService: MyService) {}
    
      ngOnInit() {
        const data = this.myService.getData();
        console.log(data);
      }
    }
    
服务的提供方式
  • providedIn: 'root':这是默认的提供方式,表示服务由根模块(AppModule)提供,因此它是单例的,整个应用程序中只有一个实例。

  • providedIn: 'any':这种方式允许服务在多个地方被提供,可能会导致多个实例。

  • providedIn: 'platform':这种方式用于提供平台级别的服务,通常用于Web Workers。

  • 手动提供:你也可以在模块的providers数组中手动提供服务,这允许你控制服务的作用域和生命周期。

服务的作用域

服务的作用域取决于它们是如何被提供的:

  • Singleton(单例):如果服务在根模块中提供(providedIn: 'root'),它将作为单例在整个应用程序中共享。

  • Transient(暂时):如果服务在组件或特定模块中提供,它将为每个组件或模块创建一个新的实例。

服务的生命周期

服务没有像组件那样的生命周期钩子(如ngOnInit),但它们可以通过构造函数进行初始化。

服务的测试

服务是纯JavaScript类,因此它们很容易进行单元测试。你可以直接实例化服务并测试其方法。

服务在Angular中扮演着非常重要的角色,它们帮助我们保持组件的简洁和关注点分离,同时提供可重用性和状态管理。通过依赖注入,Angular使得服务的创建和使用变得简单和高效。

6.4 service 的使用

如下图所示,Service 的注入依赖于 Typescript 规定的类型。也就是说 ProductService 这个类型决定了 constructor 的入参 productiveService 是否会被赋值。这里二次确认一点内容:如果服务已经在组件的上级声明过则 Service 无需在 Component 的 metadata 中进行声明!

image.png

然后,请注意如果你的 service 是用来获取数据的,那么需要在正确的 hook 中使用!如下所示,是一个获取网络数据的服务,那么调用它的合适时机就是组件的 ngOnInit 钩子函数中:

image.png

7. 详解在 Angular 中使用 Service 请求网路数据

与此相关的两个概念:ObservablesReactive Extensions

image.png

与网络请求相关的知识点有:

  • 可观察对象和 RxJS
  • 发送 http 请求
  • 处理错误或者异常
  • 订阅可观察对象

7.1 Observables 和 Reactive Extensions

image.png

operator: image.png

chain:

image.png

example:

image.png

7.2 observable 对比 promise

在JavaScript和异步编程中,PromiseObservable 都是处理异步数据流的方式,但它们之间存在一些关键的差异:

Promise

  1. 单一值: Promise 提供单一的未来值。一旦 Promise 被解决(resolved)或拒绝(rejected),它的状态就固定了,不能再次改变。
  2. 非懒加载: Promise 是非懒加载的,意味着一旦 Promise 被创建,它就会立即开始执行异步操作,直到它被解决或拒绝。
  3. 不可取消: 标准的 Promise 不支持取消操作。一旦创建,就必须等待它完成,无论结果如何。
  4. 简单性: Promise 相对简单,适合处理单一的异步操作,例如一个API调用。

Observable

  1. 多个值: Observable 可以随着时间发出多个值,直到完成(complete)或错误(error)。
  2. 懒加载: Observable 是懒加载的,这意味着订阅(subscribe)操作会触发异步操作的执行,而不是在创建 Observable 时。
  3. 可取消: Observable 支持取消订阅,允许在不需要更多值时停止监听。
  4. 操作符: Observable 支持多种操作符,如 mapfilterreduce 等,这些操作符允许对数据流进行转换和处理。
  5. 响应式编程: Observable 是响应式编程的核心构件,它允许创建复杂的异步数据流处理逻辑。

对比

  • 值的数量: Promise 一次只能处理一个值,而 Observable 可以处理多个值。
  • 执行时机: Promise 立即执行,Observable 则在订阅时执行。
  • 取消支持: Observable 可以取消订阅,而 Promise 不能。
  • 操作性: Observable 提供了丰富的操作符来处理数据流,Promise 则没有。
  • 适用场景: Promise 适合简单的异步操作,Observable 更适合复杂的异步数据流,如用户输入、实时数据更新等。

结论

选择 Promise 还是 Observable 取决于你的具体需求。如果你需要处理一个简单的异步操作,Promise 可能是更好的选择。如果你需要处理一个可以发出多个值的异步数据流,或者需要更复杂的数据处理逻辑,那么 Observable 将是一个更强大的工具。在现代JavaScript开发中,Observable 通过库如RxJS(Reactive Extensions for JavaScript)得到了广泛的应用,它为异步编程提供了强大的响应式编程模型。

image.png

Promise 对象的特点在于:提供的是未来的数据,是瞬发的,不可取消的。而与之对应的 Observable 则有着恰好相反的特点:可以同时对多个值进行处理,需要时机才能触发,是可以被取消的。

7.3 一个可以用来请求数据的 Service

image.png

不难发现,上述代码实际上是自定义的 Service 在使用内置的 Service. 也就是说 Service 是可以嵌套使用的;并且在使用 HttpClient 这个 Service 的时候一定要先在 imports 中引用,即便它是内置的:

image.png

当使用 this.http.get() 之后,根据上面的对比情况,这个时候请求还没有发出去,只有在其后继续使用 subscribe 才会让网络请求发送出去。这一点和 Promise 是不一样的!

7.4 接口类型推断

在Angular中,当你使用HttpClientget方法发送HTTP请求时,你可以指定返回数据的类型。这通常通过泛型参数来实现,从而确保返回的数据能够被正确地类型化和处理。

使用泛型指定返回类型

Angular的HttpClient模块提供了一种类型安全的方式来处理HTTP请求和响应。当你调用get<T>()方法时,<T>是一个泛型参数,它允许你指定期望的响应类型。泛型参数T可以是任何类型,包括基本类型、接口、类或别名。

示例:使用接口定义类型

假设你有一个JSON响应,如下所示:

{
  "id": 1,
  "name": "Angular Service",
  "description": "Service component for Angular applications"
}

你可以定义一个接口来匹配这个JSON结构:

interface Service {
  id: number;
  name: string;
  description: string;
}

然后,在你的组件或服务中,使用HttpClientget方法,并指定泛型参数为你的接口:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Service } from './service.interface'; // 引入接口

@Injectable({
  providedIn: 'root'
})
export class MyService {
  constructor(private http: HttpClient) {}

  getService(id: number): Observable<Service> {
    return this.http.get<Service>(`https://api.example.com/services/${id}`);
  }
}

在这个例子中,getService方法返回一个Observable<Service>,这意味着当你订阅这个Observable时,你将得到一个Service类型的数据。

自动类型推断

在某些情况下,你可以使用Angular的自动类型推断功能来简化代码。如果你在调用get方法时不指定泛型参数,Angular将尝试根据响应的JSON结构推断类型。

示例:自动类型推断
getService(id: number): Observable<any> {
  return this.http.get(`https://api.example.com/services/${id}`);
}

尽管这种方法更简单,但它牺牲了类型安全性。如果响应数据的结构发生变化,你可能不会立即在编译时得到警告,这可能导致运行时错误。

使用类型别名

除了接口,你也可以使用类型别名来定义响应类型:

type ServiceType = {
  id: number;
  name: string;
  description: string;
};

// 在get方法中使用类型别名
getService(id: number): Observable<ServiceType> {
  return this.http.get<ServiceType>(`https://api.example.com/services/${id}`);
}

通过使用泛型,Angular的HttpClient提供了一种类型安全的方式来处理HTTP请求的响应。这不仅提高了代码的可读性和可维护性,还有助于在开发过程中捕捉潜在的错误。建议尽可能使用接口或类型别名来明确指定响应类型,以充分利用TypeScript的类型系统。

7.5 在 Angular 中调试接口数据的方法

image.png

基于 HttpClient service 构建获取产品列表的自定义 service:

image.png

7.6 订阅一个 Observable 对象

image.png

核心代码是:

getProducts(): Observable<IProduct[]> {  
  return this.http.get<IProduct[]>(this.productUrl).pipe(  
    tap(data => console.log('All: ' + JSON.stringify(data))),  
    catchError(this.handleError)  
  );  
}

从上图不难看出来,所谓的 Observable 本质上只是一个配置对象,这个对象有三个属性:nextFn errorFn errorFn completeFn 对应的是 try catch finally.

订阅对象 Observable 的两种构建形式,使用和不使用箭头函数:

image.png

注意上面的代码中,传入了 next 和 error 作为 subscribe 函数的配置项,分别处理订阅成功和失败的两种情况。 next 和 error 可以写成箭头函数或者简写的普通函数的形式。

8. Angular 中的路由

与 Angular 路由相关的知识点有:

  • Angular 中的路由是如何工作的。
  • 如何配置路由。
  • 如何通过代码 / action 来触发路由跳转。
  • 合理布置视图

安装脚手架

npm install -g @angular/cli

使用脚手架在当前项目中快速的创建一个 component

ng g c products/product-detail

这样就会创建一个包裹 component 的文件夹。但是如果你不想要文件夹的话可以使用命令参数 --flat.

8.1 如何处理 undefined

现在有一个显而易见的问题。如果我们的 component 的 html 中使用到的数据是通过网络请求回来的,那么在使用的时候就会有一些风险: product.name 可能会报错,因为 product 可以是 undefined. 这个时候有两种选择:

  • 使用 product?.name 代替 product.name
  • *ngIf='product' 这样 product 没有值的时候组件不渲染,也不会报错。

8.2 元信息中 selector 存在的必要性

@Component 中,我们通过配置 selector 指定了这个组件在 html 中的 tagName, 但这并不是必要的行为。如果我们开始使用路由,这个组件可能本身就是根组件,没有其它组件会去调用它。这个时候无需指定 selector 的值。这个时候,此组件被称为 Basic Component. 但是仍然需要在 root module 中进行声明:

image.png

8.3 Angular 项目都是 SPA

这一点很重要。在学习路由之前必须要搞清楚这一点。

8.4 使用路由的流程

  • 为每一个组件都配置一个路由
  • 定义触发路由 option 以及触发路由的 action
  • 触发 action 跳转到预置的路由上去
  • 显示该路由对应的视图组件

app.module.ts 中设置路由,使用的是 RouterModule 内置对象:

import { RouterModule as ngRouterModule } from '@angular/router';
@NgModule({
  declarations: [],
  imports: [
    ngRouterModule.forRoot(
      [{ path: 'hello123', component: HelloComponent }], 
      { enableTracing: false, useHash: true }),
  ]
})

app.module.ts 中配置路由的第一步:

image.png

配置路由的时候下面是一些常见的组合:

image.png

详细解释上图中的五种路由配置:

  1. { path: 'products', component: ProductListComponent }

    • 解释:当用户访问/products路径时,会加载并显示ProductListComponent组件。这通常用于展示产品列表。
  2. { path: 'products/:id', component: ProductDetailComponent }

    • 解释:这是一个参数化路由。:id是一个路由参数,它允许你传递一个动态值(例如产品的ID)到路由中。当用户访问类似/products/123的路径时,123就会作为id参数传递给ProductDetailComponent,在这个组件内部你可以通过Angular的路由服务来获取这个参数,并据此加载相应的产品详情。
  3. { path: 'welcome', component: WelcomeComponent }

    • 解释:当用户访问/welcome路径时,会加载并显示WelcomeComponent组件。这通常用于展示一个欢迎页面或者首页。
  4. redirectTo: 'welcome', pathMatch: 'full'

    • 解释:这是一个重定向路由。当用户访问的路径与这个规则匹配时(通常是空路径/或者未定义的路径),他们会被重定向到/welcome路径。pathMatch: 'full'意味着只有当路径完全匹配时(即路径为空或者与配置的路径完全一致),才会触发重定向。这通常用于将用户引导到一个默认页面,如首页或欢迎页面。

    注意:这个路由配置通常与其他路由一起使用,作为path: ''的一个属性,例如:

    { path: '', redirectTo: 'welcome', pathMatch: 'full' }
    
  5. { path: '**', component: PageNotFoundComponent }

    • 解释:这是一个通配符路由。**代表任何路径,当没有其他路由规则匹配时,这个规则会被触发。它通常用于显示一个404错误页面,告知用户他们请求的页面不存在。在这个配置中,当用户访问的路径不匹配前面定义的任何路由时,会加载并显示PageNotFoundComponent组件。

上图所示的 5 种配置既全面又重要,一定要作为基础掌握好。

除此之外,还有一点需要额外留心,那就是众多路由规则它可能是相互有重叠的,并且 Angular 中是从前到后的优先级进行匹配的,因此同一个数组中的路由规则必须遵守先具体宽泛的准则。

8.5 触发路由跳转的方式

image.png

  • 通过 a 标签进行跳转

image.png

注意,在进行路由跳转的时候,重要的不是 a 标签而是 [routerLink]="'/welcome'",事实上,将其写在 button 标签上仍然可以奏效。

  • 指定路由对应组件的渲染位置(路由占位符)

image.png

从上面的图上可以得知接受路由组件渲染的是 <router-outlet></router-outlet> 这个组件,渲染完成之后这个占位符就会被移除了。在一个 Angular 项目中不局限有一个 router-outlet, 我们可以通过命名的方式创建多个路由占位符。

  • 配置路由的步骤

image.png

  • 触发路由的方式

image.png

  • 渲染路由组件的方式

image.png

8.6 通过子组件和通过路由渲染一个组件的对比

在Angular框架中,组件(Components)是构建用户界面的基石。根据它们的用途和特性,组件可以分为嵌套组件(Nest-able components)和路由组件(Routed components)。以下是这两种组件类型的对比:

嵌套组件(Nest-able components)

  1. 选择器(Selector): 嵌套组件定义了一个选择器,这是一个CSS选择器,用于在模板中引用组件。
  2. 嵌套性(Nesting): 嵌套组件可以作为另一个组件的子组件,形成组件树的一部分。
  3. 无路由(No route): 嵌套组件不与路由直接关联。它们的存在和渲染由其父组件控制。
  4. 示例:
    @Component({
      selector: 'app-child',
      template: `<p>I am a child component</p>`
    })
    export class ChildComponent {}
    
    @Component({
      selector: 'app-parent',
      template: `<app-child></app-child>`
    })
    export class ParentComponent {}
    

路由组件(Routed components)

  1. 无选择器(No selector): 路由组件通常不定义选择器,因为它们不是通过模板中的标签来引用的。
  2. 配置路由(Configure routes): 路由组件通过路由配置与特定的路径关联,它们是由路由器根据URL激活和显示的。
  3. 绑定动作(Tie routes to actions): 路由组件与用户的动作(如点击链接或输入URL)绑定,当相应的路由被激活时,路由器会加载和渲染这些组件。
  4. 示例:
    const routes: Routes = [
      { path: 'parent', component: ParentComponent },
      { path: 'child', component: ChildComponent }
    ];
    
    // 在RouterModule.forRoot(routes)中配置路由表
    

对比

  • 选择器: 嵌套组件需要定义选择器以便在模板中使用,而路由组件不需要。
  • 嵌套性: 嵌套组件可以被其他组件包含,形成嵌套结构;路由组件则通过路由配置嵌套。
  • 路由: 嵌套组件不直接与路由关联,路由组件则与特定的路由路径绑定。
  • 激活方式: 嵌套组件由其父组件控制渲染,路由组件由路由器根据URL激活。
  • 用途: 嵌套组件适用于构建组件层次结构和重用UI片段,路由组件适用于应用的页面导航和页面级别的UI。

结论

嵌套组件和路由组件在Angular中扮演着不同的角色。嵌套组件是构建用户界面的基础,允许开发者创建可重用的UI片段。路由组件则处理应用的路由和导航,允许开发者定义应用的不同页面和视图。理解这两种组件的区别对于构建大型、模块化的Angular应用至关重要。

image.png

8.7 如果想要通过 code 进行路由跳转而不是 clickable 元素?

如果我想在某个 button 的 onclick 中除了跳转还要再做一些别的事情,这个时候就不能简单的给这个 button 元素上设置 [routerLink]='/back' 了。这个时候必须将跳转逻辑融到 onclick 的回调函数中去。也就是说,我们需要使用代码进行路由跳转,而不是通过点击某个元素的方式。

image.png

也就是说,如果我们需要进行路由跳转就必须使用名为 Router 的 service. 如果要使用的话切记得注入此依赖

8.8 路由参数

image.png

被调用的路由组件中可以通过下面的这种方式接收到传递的路由参数:

image.png

上图代码中的 ActivatedRoute 是一个服务(Service)。我们使用这个注入的服务上的方法就可以拿到传递的路由参数。

一般来说,我们会在 ngOnInit 生命周期函数中获取这个路由参数。

image.png

下面是其详细的注入过程:

  1. 定义依赖:在组件的构造函数中,通过构造函数注入的方式声明了对ActivatedRoute的依赖,如你所示代码中的private route: ActivatedRoute
  2. 实例化组件:当Angular需要创建一个组件实例时(比如因为用户导航到了一个新的路由),它会查看该组件的构造函数,并识别出所有需要注入的依赖。
  3. 解析依赖:Angular的DI系统会查找已经注册的提供者(providers)来解析这些依赖。对于ActivatedRoute,它通常是在Angular的路由模块中作为提供者注册的。
  4. 创建服务实例:如果这是该服务在该注入器层次结构中的第一次请求,Angular会创建一个新的ActivatedRoute实例(或者如果它已经在该层次结构中被创建过,则返回现有的实例,但这种情况对于ActivatedRoute来说并不适用,因为它是与特定路由关联的)。然而,值得注意的是,对于ActivatedRoute,每个激活的路由都会有一个新的实例。
  5. 注入服务实例:最后,Angular会将新创建的(或现有的)ActivatedRoute服务实例注入到组件的构造函数中,赋值给route参数。
  6. 组件初始化:一旦所有依赖都被注入,组件的构造函数就会被调用,之后是组件的初始化生命周期钩子(如ngOnInit)。

8.9 路由守卫

为什么有的路由不能跳?因为要有权限,否则是很不安全的。

下图展示的是 4 种 Angular 中的路由守卫:

image.png

实现一个路由守卫得基本想法是:自定义一个 service 实现 canActive 接口方法

image.png

然后在我们的 app.module.ts 中注入自定义的 service:

image.png

注意注入的位置,是在路由配置对象的 canActivate 字段中,而不是在 imports 中!!

我们通过 Angular 的脚手架来创建一个路由守卫:ng g g products/product-detail

image.png

运行之后就会创建一个路由守卫了,并且自动注入到了全局中:

image.png

在模板的基础上我们可以方便的写出自己的路由守卫:

import { Injectable } from '@angular/core';  
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from "@angular/router";  
import { Observable } from 'rxjs';  
  
@Injectable({  
  providedIn: 'root'  
})  
export class ProductDetailGuard implements CanActivate {  
  constructor(private router: Router) { }  
  
  canActivate(  
    next: ActivatedRouteSnapshot,  
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {  
    let id = +next.url[1].path;  
    if (isNaN(id) || id < 1) {  
      alert("Invalid product Id");  
      this.router.navigate(['/products']);  
      return false;  
    }  
    return true;  
  }  
}

创建一个简单路由守卫的步骤如下:

  1. 构建守卫服务:首先,需要创建一个新的服务,这个服务将作为路由守卫。
  2. 实现守卫类型:守卫服务需要实现 CanActivate 接口,这是一个定义了 canActivate() 方法的接口。
  3. 创建 canActivate() 方法:在守卫服务中实现 canActivate() 方法,该方法将决定是否允许激活路由。
  4. 注册守卫服务提供者:将守卫服务注册到Angular的依赖注入系统中,以便可以在路由配置中使用。
  5. 使用 providedIn 属性:在服务装饰器中使用 providedIn 属性指定服务应该在哪里提供,通常是在模块级别。
  6. 将守卫添加到所需路由:在路由配置中,通过 canActivate 属性将守卫服务添加到特定的路由上,以控制对该路由的访问。

路由的高级用法: 在Angular中,路由器(Router)是一个强大的组件,它提供了多种高级用法来增强应用的导航和性能。以下是一些Angular路由器的高级用法:

  1. 必需参数、可选参数和查询参数
  • 必需参数:在路由配置中,使用冒号 : 来定义必需的路径参数,例如 user/:userId。这表示URL中必须包含 userId 参数。
  • 可选参数:可选参数使用方括号 [] 来定义,例如 user/:userId?。这意味着 userId 是可选的。
  • 查询参数:查询参数是在URL的 ? 后面定义的,例如 ?search=query。它们通常用于过滤或排序数据。
  1. 预加载与路由解析器(Prefetching with route resolvers)
  • 路由解析器:使用 Resolve 守卫和 Route 服务,可以在路由激活之前预先加载数据。这可以减少页面加载时间,提供更快的用户体验。
  • 预加载策略:Angular还提供了预加载模块,可以自动预加载尚未激活但在路由配置中定义的懒加载模块。
  1. 子路由和辅助路由出口(Child and secondary router outlets)
  • 子路由:子路由允许你在组件模板中定义多个路由出口,用于嵌套路由。这使得可以创建复杂的组件层次结构。
  • 辅助路由出口:使用 router-outletname 属性,可以在组件模板中定义多个路由出口。这允许同时激活多个路由。
  1. 路由守卫(Router guards)
  • 路由守卫:路由守卫是RxJS可观察对象,用于保护路由,控制访问权限。它们可以在路由激活之前或之后执行。
  • CanActivate:决定是否允许激活路由。
  • CanDeactivate:决定是否允许离开当前路由。
  • CanLoad:决定是否允许加载懒加载模块。
  1. 懒加载(Lazy loading)
  • 懒加载模块:懒加载是一种性能优化技术,可以将应用拆分为多个模块,只在需要时加载特定的模块。
  • 路由配置:在路由配置中使用 loadChildren 函数来定义懒加载模块的加载方式。

Angular的路由器提供了多种高级用法,包括参数处理、预加载、子路由、路由守卫和懒加载。这些高级用法使得开发者可以构建灵活、高性能的单页应用(SPA)。通过合理使用这些高级特性,可以提高应用的用户体验和响应速度。

9. Angular Module 和 Component 基础知识

9.1 Module 部分

我们可以画出如下所示的 Angular Module 之间的关系图。

image.png

与 Angular Module 相关的知识点有:

  • Angular 中的 module 是什么。
  • module 的元信息指的是什么。
  • 创建一个 Feature module。
  • 创建一个 Shared module。
  • 回顾 aap.moudle.ts 的构成。

image.png

module 具有将资源联系在一起的功能。 image.png

module 提供了解释 html 模板的独立环境。

module 的目的是为了更好的组织代码。 image.png

Bootstrap Array Truths(自举数组规则)

  1. 自举数组规则 #1:每个Angular应用必须至少自举一个组件,即根应用组件。这意味着应用的启动点是一个特定的组件。
  2. 自举数组规则 #2:自举数组应该只用于根应用模块(通常是 AppModule)。这意味着只有根模块负责启动组件。

Declarations Array Truths(声明数组规则)

  1. 声明数组规则 #1:我们创建的每个组件、指令和管道都必须属于且仅属于一个Angular模块。这确保了代码的组织和封装性。
  2. 声明数组规则 #2:声明数组中只能声明组件、指令和管道。这是Angular模块系统的基本要求。
  3. 声明数组规则 #3:永远不要重新声明属于另一个模块的组件、指令或管道。这避免了潜在的冲突和错误。
  4. 声明数组规则 #4:所有声明的组件、指令和管道默认是私有的。它们只能被同一模块中声明的其他组件、指令和管道访问。
  5. 声明数组规则 #5:Angular模块为其组件模板提供了模板解析环境。这意味着模块负责解析和提供组件所需的依赖。

从 module 中导出资源:

image.png

Exports Array Truths(导出数组规则)

  1. 导出数组规则 #1:如果其他组件需要使用某个组件、指令或管道,应该将其导出。这允许其他模块通过导入来使用这些声明。
  2. 导出数组规则 #2:可以通过重新导出(Re-export)模块来间接导出其组件、指令和管道。这意味着可以导出一个模块中的所有内容,而不需要单独导出每个组件。
  3. 导出数组规则 #3:我们可以在没有先导入的情况下重新导出某个内容。这提供了一种灵活的方式来组织和共享模块中的声明。
  4. 导出数组规则 #4:永远不要导出服务(Service)。服务应该只在创建它们的模块中提供,以避免多实例问题,确保服务的单例模式。

module 导入资源

image.png

Imports Array Truths(导入数组规则)

  1. 导入数组规则 #1:导入一个模块会使得该模块中导出的所有组件、指令和管道在当前模块中可用。这意味着通过导入,可以访问和使用其他模块提供的声明。
  2. 导入数组规则 #2:只导入当前模块需要的模块。这有助于保持模块的简洁性,避免不必要的依赖。
  3. 导入数组规则 #3:导入一个模块并不会提供对该模块所导入的其他模块的访问权限。换句话说,如果模块A导入了模块B,那么只有模块A内部可以访问模块B提供的导出内容,其他模块需要直接导入模块B才能访问。

暴露资源之间的关系:

image.png 上面这幅图中,ProductListComponent 可以使用 StarComponent 但是不可以使用 ngModal.

image.png 上面这幅图中,ProductListComponent 不仅可以使用 StarComponent 也可以使用 ngModal.

providers 数组: 不再推荐在 providers 中注入 service

image.png

Providers Array Truths(提供者数组规则)

  1. 提供者数组规则 #1:添加到提供者数组中的任何服务提供者都会在应用的根级别注册。这意味着这些服务会作为单例在整个应用中提供,无论在哪里注入,都会得到同一个实例。
  2. 提供者数组规则 #2:不要在共享模块(Shared Module)的提供者数组中添加服务。共享模块通常用于存放可以在多个特性模块中重用的组件、指令和管道,而服务的提供应该更加集中和谨慎,以避免不必要的依赖和复杂性。
  3. 提供者数组规则 #3(隐含):考虑构建一个核心模块(CoreModule),用于存放和提供应用的核心服务,然后在应用模块(AppModule)中导入这个核心模块一次。这样可以实现服务的集中管理和单例模式,同时保持模块的清晰和可维护性。

9.2 组件模块化

组件模块化之前:

image.png

模块化第一步:完成一个 feature module

image.png

使用脚手架完成新建模块的工作:ng g m products/product 当然你可以使用 --flat 除此之外,你可以使用 -m app 来指定新建 module 被哪个 module 所使用。

创建完成之后,名为 ProductModule 的模块就会自动被引用:

image.png

模板 module 的内容如下所示:

image.png

如果我们不是在 app.module.ts中引用 RouterModule 那么我们使用的是名为 forChild 的方法而不是之前的 forRoot 方法:

image.png

模块化第二步:完成一个 shared module 对于那些要被多个组件引用的组件,我们一般使用 shared module 包裹之。

架构如下所示:

image.png

使用 Angular 的脚手架完成 shared module 的创建:ng g m shared/shared

创建出来的模板 shared module 内容如下:

image.png

上述 shared module 架构的代码为:

image.png

shared module 有一个特点,那就是会用到 exports 数组。

9.3 对比 shared module 和 feature module

在Angular中,SharedModuleFeatureModule是两种不同类型的模块,它们在代码层面和应用结构上有明显的区别。

SharedModule

SharedModule通常包含了一些在多个其他模块中都会用到的组件、指令、管道和服务。这个模块的目的是为了重用代码,减少冗余。在Angular应用中,SharedModule本身通常不会被导入到AppModule中,而是会被其他特性模块(Feature Modules)或者其他需要共享资源的模块导入。

下面是一个简单的SharedModule示例:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

// 导入共享的组件、指令、管道等
import { SharedComponent } from './shared.component';

@NgModule({
  declarations: [
    SharedComponent
    // ... 其他共享的组件、指令、管道等
  ],
  imports: [
    CommonModule
    // ... 可能需要导入的其他模块
  ],
  exports: [
    SharedComponent
    // ... 其他需要导出的组件、指令、管道等
  ]
})
export class SharedModule { }

FeatureModule

FeatureModule是专门为应用的一个特定功能或特性而创建的模块。这些模块通常包含了一组相关的组件、服务等,用于实现应用的某个具体功能。与SharedModule不同,FeatureModule通常不会被其他模块导入,而是直接被AppModule或其他顶级模块导入。

下面是一个简单的FeatureModule示例:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';

// 导入特性模块的组件、服务等
import { FeatureComponent } from './feature.component';
import { FeatureService } from './feature.service';

@NgModule({
  declarations: [
    FeatureComponent
    // ... 其他属于这个特性模块的组件、指令等
  ],
  imports: [
    CommonModule,
    RouterModule.forChild([
      // ... 路由配置
    ])
    // ... 可能需要导入的其他模块,比如SharedModule等
  ],
  providers: [
    FeatureService
    // ... 特性模块的服务等
  ]
})
export class FeatureModule { }

代码层面的主要区别

  1. 目的SharedModule的目的是为了重用代码,而FeatureModule的目的是为了封装和组织特定功能的代码。
  2. 导入和导出SharedModule通常会导出其包含的组件、指令和管道等,以便其他模块可以使用它们。而FeatureModule通常不会导出其内部的组件、服务等,因为它们主要供该特性模块内部使用。
  3. 服务提供FeatureModule可能会在其providers数组中提供服务,这些服务通常只在该特性模块内部使用。而SharedModule则更侧重于提供可重用的UI元素,而不是服务。
  4. 路由配置FeatureModule通常会有自己的路由配置,定义了这个特性模块内部的路由结构。而SharedModule则不包含路由配置,因为它提供的是通用的UI元素,不涉及具体的路由结构。

9.4 回顾 app.module.ts 的架构

image.png

我们的目的就是通过模块化将其架构变成下面的模样:

image.png

9.5 路由模块化

image.png

基本过程就是把 RouterModule imports 进来,然后使用其上面的 forRoot 方法加工之后,再通过 exports 导出。

这样,app.module.ts 中的一部分逻辑继续可以简化出来,这样我们使用名为 AppRoutingModulemodule 将一部分逻辑抽象了出来,抽象之后的代码如下所示:

image.png

这里注意,imports 数组中的各个 module 之间的顺序是不能颠倒的。因为他们是按照序列号从小到大的顺序排列的。

9.6 子模块路由模块化

路由的模块化不仅体现在 app.moudle.ts 中,还可以体现在其它子模块中。如下图所示,我们对子模块中的路由进行了剥离,这个过程和在根组件中是完全相同的,如下图所示:

image.png

剥离了路由之后的子模块的代码简化如下所示:

image.png

9.7 模块的元信息

所谓模块的元信息,指的就是:Bootstrap Declarations Exports Imports Providers

  1. Bootstrap(开始) : 指的是应用启动时初始化的组件。至少会有一个根组件,它是应用的入口点。
  2. Declarations(声明) : 指明了属于该模块的组件、指令和管道。这些声明是模块的本地定义,只能在该模块内部使用。
  3. Exports(导出) : 定义了其他模块可以导入并使用的组件、指令和管道。通过导出,可以使得这些声明在其他模块中可用。
  4. Imports(导入) : 指明了当前模块依赖的其他模块。通过导入其他模块,可以访问那些模块的声明和导出。
  5. Providers(上下文) : 列出了要在模块中提供的服务。这些服务可以是单例,也可以是依赖注入系统提供的其他任何类型的对象。

10. Angular 脚手架 cli

脚手架上提供的常用命令:

image.png

脚手架的作用:

image.png

安装脚手架:npm install -g @angular/cli

查看脚手架相关信息:ng v

image.png

创建一个崭新的 Angular 项目:ng new hello-world

启动 Angular 项目:ng server -o 其中 -o 的作用就是打开默认浏览器。

使用 ng generate 来为项目添砖加瓦。

image.png

ng g c welcome 运行之后的结果为:

image.png

使用 ng test 或者 ng e2e 进行测试。其中 e2e 很有意思。

使用 ng build 或者 ng build --prod 进行打包。

11. 更多

其他课程:

  • Angular: First Look
  • Angular cli
  • Angular reactive forms
  • Angular routing
  • Angular component communication
  • Angular fundamentals
  • unit testing in angular