[Angular 基础] - service 服务

[Angular 基础] - service 服务

之前的笔记就列举三个好了......没想到 Angular 东西这么多(ー ー;)......全加感觉越来越凑字数了


Angular 的 service 如果后端出身的应该很熟悉,它是 Angular 自行管理,并使用 Dependency Injection 去实现的一个类。因此它比较合适使用的场景是,多个嵌套组件需要互相沟通,并需要传递值。

举例说明:

bash 复制代码
|- a
|  |- b
|  |  |- d
|  |- c
|  |  |- e

这个情况下,a 如果需要和 de 进行沟通的话,那么

  • bc 也需要通过 @Input 去获取从 a 传来的值,并将其传到 de 中去;
  • bc 也需要通过 @Output 去获取从 de 传来的事件,并将其传到 a 中去

这就是一个不可避免的沟通环节。

使用 service 就可以比较有效的解决这个问题

创建一个新的案例

这个案例相对比较简单,就是按照上面的结构创建一个项目。在这个简单的案例里,bc 没有任何作用,只是作为 a <--> da <--> e 之间的承接桥梁。在真实的项目中,bc 的作用可能会包括一些数据处理、选择渲染之类的。

项目结构如下:

bash 复制代码
❯ tree src/app/
src/app/
├── app.component.css
├── app.component.html
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── b
│   ├── b.component.css
│   ├── b.component.html
│   ├── b.component.ts
│   └── d
│       ├── d.component.css
│       ├── d.component.html
│       └── d.component.ts
└── c
    ├── c.component.css
    ├── c.component.html
    ├── c.component.ts
    └── e
        ├── e.component.css
        ├── e.component.html
        └── e.component.ts

5 directories, 17 files

a 的实现

这里主要还是传值+绑定事件,具体内容在 [Angular 基础] - 自定义事件 & 自定义属性 里,这里就不多做赘述,直接放代码了:

  • V 层

    html 复制代码
    <div class="container">
      <div class="row">
        <div class="col-xs-12 col-md-8 col-md-offset-2">
          <app-b [message]="aToD" (messageFromB)="onRecieveMessageFromB"></app-b>
          <app-c [message]="aToE"></app-c>
        </div>
      </div>
    </div>
  • VM 层:

    typescript 复制代码
    import { Component, EventEmitter, OnInit, Output } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css'],
    })
    export class AppComponent {
      aToD = 'message from a to d';
      aToE = 'message from a to e';
    
      @Output() messageFromB = new EventEmitter<string>();
    
      onRecieveMessageFromB($event: string): void {
        this.aToD = $event;
        console.log('message from b to a: ', $event);
      }
    }

b 的实现

实现基本和 a 一致,这里也就放代码了:

  • V 层

    html 复制代码
    <div class="">
      <app-d [message]="message" (messageToB)="onRecieveMessage($event)"></app-d>
    </div>
  • VM 层

    typescript 复制代码
    import {
      Component,
      EventEmitter,
      Input,
      OnInit,
      Output,
    } from '@angular/core';
    
    @Component({
      selector: 'app-b',
      templateUrl: './b.component.html',
      styleUrl: './b.component.css',
    })
    export class BComponent implements OnInit {
      @Input() message: string;
      @Output() messageToA = new EventEmitter<string>();
    
      ngOnInit(): void {}
    
      onRecieveMessage($event: string): void {
        this.message = $event;
        this.messageToA.emit(this.message);
        console.log('message from b to a: ', this.message);
      }
    }

d 的实现

  • V 层

    html 复制代码
    <input type="text" [value]="message" (input)="onChangeText($event)" />
  • VM 层

    typescript 复制代码
    import {
      Component,
      EventEmitter,
      Input,
      OnInit,
      Output,
    } from '@angular/core';
    
    @Component({
      selector: 'app-d',
      templateUrl: './d.component.html',
      styleUrl: './d.component.css',
    })
    export class DComponent implements OnInit {
      @Input() message: string;
      @Output() messageToB = new EventEmitter<string>();
    
      ngOnInit(): void {}
    
      onChangeText($event: Event): void {
        this.message = ($event.target as HTMLInputElement).value;
        this.messageToB.emit(this.message);
        console.log('message from d to b: ', this.message);
      }
    }

最后实现效果如下:

如果说 React 只是将 onChangeHandler 一个个向子组件里传递,做 props drilling,那么 Angular 除了要在 HTML Template 中传值之外,还需要在组件中实现 @Input@Output 去接受从父组件中传下来的值,并且将事件送到父组件中,对比起来操作更加的麻烦

使用 service 代替

这里使用 service 代替上下传递 @Input@Outpu 进行实现

创建 service

这里依旧使用 cli 去创建 service:

bash 复制代码
❯ ng generate service services/message --skip-tests
CREATE src/app/services/message.service.ts (136 bytes)

此时结构如下:

实现如下:

typescript 复制代码
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MessageService {
  passedMessage = 'message from a to e';

  constructor() {}

  updateMessage(msg: string) {
    this.passedMessage = msg;
  }
}

具体实现会在下一个 section 说明

调用 service

调用方式是在构造函数中让 Angular 自动使用 dependency injection 实现

a 的修改:
typescript 复制代码
export class AppComponent {
  // 这里的 dependency injection 是由 angular 实现的
  constructor(private messageService: MessageService) {}
}
c 的实现
typescript 复制代码
import { Component, DoCheck, Input } from '@angular/core';
import { MessageService } from '../services/message.service';

@Component({
  selector: 'app-c',
  templateUrl: './c.component.html',
  styleUrl: './c.component.css',
})
export class CComponent implements DoCheck {
  message: string;

  constructor(private messageService: MessageService) {
    this.message = this.messageService.passedMessage;
  }

  ngDoCheck(): void {
    console.log(this.messageService.passedMessage);
  }
}

HTML Template 中只需要渲染一个 e 即可:

html 复制代码
<app-e></app-e>

⚠️:这里主要是 log 一下 service 中变化的值。因为 message 是一个 primitive,所以想要正确的获取 message 的变化是要使用 Observable 的,目前暂时没有涉及到这个部分,因此只是在 ngDoCheck 中输出一下值,表示当前的变化已经被获取了

e 的实现
typescript 复制代码
import { Component, Input } from '@angular/core';
import { MessageService } from '../../services/message.service';

@Component({
  selector: 'app-e',
  templateUrl: './e.component.html',
  styleUrl: './e.component.css',
})
export class EComponent {
  message: string;

  constructor(private messageService: MessageService) {
    this.message = this.messageService.passedMessage;
  }

  onChangeText($event: Event): void {
    this.messageService.updateMessage((<HTMLInputElement>$event.target).value);
  }
}

最终效果:

可以看到,对比 a <--> b <--> d 的沟通, a <--> c <--> e 中使用 service 更加的简洁

深入了解 service

Injectable

这个 decorator 在新版的 Angular 是推荐每个 service 都放上,现在默认使用 cli 就会自动带上 Injectable

providedIn 则是挂载的范围,默认情况下挂载的范围是全局。换言之所有的 component 都共享一个 singleton。如果将 providedIn 删除的话,那么 Angular 就可以创建多个 instance

多个 instance & providers

这里首先需要将 Injectable 中的 providedIn 去掉,只保留 @Injectable 这个 decorator 或者去除都行------新版 Angular 是推荐保留 decorator 的

随后需要修改 @Component decorator,这里是修改 B/C 两个组件中的 decorator:

typescript 复制代码
@Component({
  selector: 'app-b',
  templateUrl: './b.component.html',
  styleUrl: './b.component.css',
  providers: [MessageService],
})

这样当前 component 及其后代 component 都会共享同一个 service:

⚠️:这里页面显示的(d/e 从 MessageService 中接受的信息)与 log 中是一致的

如果修改 d/e decorator 中的 providers 的话,d/e 二者也会有自己的 service instance:

⚠️:这里页面显示的(d/e 从 MessageService 中接受的信息)与 log 中是不一致的

这是因为 providers 是 Angular 接受参数用来配置 Dependency Injection 的地方,提供值就会新建一个新的 instance。因此如果想要组件内共享同一个 service 的话,就需要在最近祖先节点修改对应的 providers

👀:传的信息内容我通过 Faker 的随机 lorem 生成,所以每个 service 会不一样

service 注入 service

我这里的实现是两个 service 都会有 @Injectable 这个装饰器,这样的实现会方便一些。MessageService 的实现基本不变,需要修改的就是在构造函数内,通过依赖注入绑定一个 LoggingService,修改如下:

typescript 复制代码
import { Injectable } from '@angular/core';
import { faker } from '@faker-js/faker';
import { LoggingService } from './logging.service';

@Injectable()
export class MessageService {
  passedMessage = faker.lorem.sentence();

  constructor(private loggingService: LoggingService) {
    this.loggingService.logMessage(
      'MessageService constructor created message to ' + this.passedMessage
    );
  }

  updateMessage(msg: string) {
    this.passedMessage = msg;
    this.loggingService.logMessage('MessageService updated message to ' + msg);
  }
}

LoggingService 则是一个实现了输出信息的 service:

typescript 复制代码
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class LoggingService {
  constructor() {}

  logMessage(msg: string) {
    console.log(`${msg} received at ${new Date().toLocaleTimeString()}`);
  }
}

这样每次当 MessageService 被实例化和变动的时候,都会调用一次输出日志方法:

services 的应用场景

根据案例可以看出来,它可以实现以下几个功能:

  • 数据共享

    不用使用 @Input 进行不同层级的数据传递

  • 状态管理

    这个作用和 React 的 Context 有点相似,在层级内控制状态,并且通过状态进行数据和组件的对应渲染

  • API 交互

    HTTP 请求的抽象实现,比如说实现一个 API 层级的 CRUD 封装,这样所有的组件都可以较为方便的调用

  • 业务逻辑实现

    也是属于功能的一种抽象,如果某些功能不是特定属于几个组件内,那么就可以将其抽离出来进行共享

  • util

    也是属于功能的一种抽象,如果某些功能不是特定属于几个组件内,那么就可以将其抽离出来进行共享

    其中一个例子就是上面实现的 logging util

相关推荐
前端小巷子1 分钟前
web从输入网址到页面加载完成
前端·面试·浏览器
江城开朗的豌豆2 分钟前
Vue路由动态生成秘籍:让你的链接'活'起来!
前端·javascript·vue.js
晓得迷路了3 分钟前
栗子前端技术周刊第 88 期 - Apache ECharts 6.0 beta、Deno 2.4、Astro 5.11...
前端·javascript·echarts
江城开朗的豌豆8 分钟前
在写vue公用组件的时候,怎么提高可配置性
前端·javascript·vue.js
江城开朗的豌豆8 分钟前
Vue路由跳转的N种姿势,总有一种适合你!
前端·javascript·vue.js
江城开朗的豌豆9 分钟前
Vue路由玩法大揭秘:三种路由模式你Pick谁?
前端·javascript·vue.js
江城开朗的豌豆10 分钟前
Vue路由守卫全攻略:给页面访问装上'安检门'
前端·javascript·vue.js
小磊哥er17 分钟前
【前端工程化】前端组件模版构建那些事
前端
前端 贾公子17 分钟前
monorepo + Turborepo --- 开发应用程序
java·前端·javascript
江城开朗的豌豆22 分钟前
Vue路由传参避坑指南:params和query的那些猫腻
前端·javascript·vue.js