[Angular 基礎] - service 服務
之前的筆記就列舉三個好了……沒想到 Angular 東西這么多(ー ー;)……全加感覺越來越湊字數了
-
[Angular 基礎] - 視圖封裝 & 局部引用 & 父子組件中內容傳遞
-
[Angular 基礎] - 生命周期函數
-
[Angular 基礎] - 自定義指令,深入學習 directive
Angular 的 service 如果后端出身的應該很熟悉,它是 Angular 自行管理,并使用 Dependency Injection 去實現的一個類。因此它比較合適使用的場景是,多個嵌套組件需要互相溝通,并需要傳遞值。
舉例說明:
|- a
| |- b
| | |- d
| |- c
| | |- e
這個情況下,a
如果需要和 d
與 e
進行溝通的話,那么
b
和c
也需要通過@Input
去獲取從a
傳來的值,并將其傳到d
和e
中去;b
和c
也需要通過@Output
去獲取從d
和e
傳來的事件,并將其傳到a
中去
這就是一個不可避免的溝通環節。
使用 service 就可以比較有效的解決這個問題
創建一個新的案例
這個案例相對比較簡單,就是按照上面的結構創建一個項目。在這個簡單的案例里,b
和 c
沒有任何作用,只是作為 a <--> d
和 a <--> e
之間的承接橋梁。在真實的項目中,b
和 c
的作用可能會包括一些數據處理、選擇渲染之類的。
項目結構如下:
? 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.ts5 directories, 17 files
a 的實現
這里主要還是傳值+綁定事件,具體內容在 [Angular 基礎] - 自定義事件 & 自定義屬性 里,這里就不多做贅述,直接放代碼了:
-
V 層
<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 層:
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 層
<div class=""><app-d [message]="message" (messageToB)="onRecieveMessage($event)"></app-d> </div>
-
VM 層
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 層
<input type="text" [value]="message" (input)="onChangeText($event)" />
-
VM 層
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:
? ng generate service services/message --skip-tests
CREATE src/app/services/message.service.ts (136 bytes)
此時結構如下:
實現如下:
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 的修改:
export class AppComponent {// 這里的 dependency injection 是由 angular 實現的constructor(private messageService: MessageService) {}
}
c 的實現
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
即可:
<app-e></app-e>
??:這里主要是 log 一下 service 中變化的值。因為 message
是一個 primitive,所以想要正確的獲取 message
的變化是要使用 Observable 的,目前暫時沒有涉及到這個部分,因此只是在 ngDoCheck
中輸出一下值,表示當前的變化已經被獲取了
e 的實現
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:
@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
,修改如下:
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:
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