Angular Signal API

RxJS 是一個使用可觀察序列編寫非同步和基於事件的程式的函式庫。

它提供了一種核心型別,即 Observable、一些周邊型別(Observer、Scheduler、Subjects)和類似於 Array 方法(map、filter、reduce、every 等)的運算子,以便將非同步事件作為集合進行處理。

什麼是HttpClient

HttpClient 是Angular 內建專門處理AJAX的模組,負責處理用API 溝通取得資料的複雜流程。

匯入HttpClient

Angular 18 引進了一種觸發偵測變更的新方法。檢測更改完全由 Zone Js 處理。現在,偵測變化由框架本身直接觸發。
Angular 18 的新增功能
Angular 19版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
+ import { provideHttpClient} from '@angular/common/http';
import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
+ provideHttpClient(),
provideRouter(routes),
provideClientHydration(withEventReplay())
]
};

Subscribe

推薦把它們用於事件處理、非同步程式設計以及處理多個值等場景。

接收可觀察物件通知的處理器要實現 Observer 介面

  • next:用來處理每個送達值。在開始執行後可能執行零次或多次。
  • error:用來處理錯誤通知。錯誤會中斷這個可觀察物件實例的執行過程。
  • complete:用來處理執行完畢(complete)通知。當執行完畢後,這些值就會繼續傳給下一個處理器。
1

Angular Signal API

「當我們真的有改變需要渲染的資料時,才進行渲染」,我們也可以稱他為一種「回應變化」的處理方式,也就是每次渲染,都一定會有一個主動的變化,也是我們常常講的「reactidve programming」的一種應用。
Angular 借用了 solid.js 提出的 signal 概念,我們可以把它想像成是一種資料的「訊號」,只有當這個「訊號」有發送的時候,我們才進行回應 (也就是畫面渲染),如此一來就可以提高整體應用程式的效率!

定義Signal

Signal Html

html

1
2
3
4
5
6
<div style="font-size: 80px;">{{count()}}</div>
<div class="btn_group">
<a (click)="set()" class="button set_button">設置</a>
<a (click)="add()" class="button add_button">+1</a>
<a (click)="minus()" class="button minus_button">-1</a>
</div>

Signal TS 設置更新邏輯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
count = signal(1)
// 設置初始數
set() {
this.count.set(1)
}
// Add增加
add() {
this.count.update(c => c + 1)
}
// Minus減少
minus() {
this.count.update(c => c - 1)
}
}

計算computed Singal

computed() 會回傳一個 readonly signal,且在呼叫時需要傳入一個 callback function,在此 function 內的 signal 發生變化時,這個 callback function 就會自動被呼叫,因此我們可以在 signal 有變化時,才去根據來源 signal 產生新結果,

引入 computed, signal

1
2
3
import { Component,computed, signal } from '@angular/core';
...
counter = signal(0);

TS

定義count=>signal(值)
computed(() => { return … })

1
2
3
4
5
6
7
8
9
10
11
12

import { Component,computed, signal } from '@angular/core';

export class HomeComponent {

count = signal(2)
countPlus = computed(() => { return this.count() * this.count() })

add() {
this.count.update(() => this.count() * this.count())
}
}

Html

1
2
3
4
5
6
7
8
9
10
11
<div class="container">
<div class="flex_center">
<p class="flex_center">compouted works!</p></div>
<div class="flex_center">
<div class="flex_center">{{ count() }}</div>
<div class="flex_center">{{ countPlus() }}</div>
</div>
<div class="flex_center" >
<a class="button add_button" (click)="add()">+1</a>
</div>
</div>

Angular 環境變數

嵌套路由

1
<router-outlet></router-outlet>

this.router.navigate([‘users’]); 切換至頁面

專案內 src 新增environment 資料夾內environment.ts 與 environment.development.ts
environment.ts檔案內新增

1
2
3
4
5
export const environment = {
production:false,
webSit:'網址',
weatherApiKey: '密碼',
};

environment.development.ts檔案內新增

1
2
3
4
5
export const environment = {
production: true
webSit:'網址',
weatherApiKey: '密碼',
};

在頁面中使用

src/app/app.component.ts 內

  • 引入environment
  • 引入environment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
+ import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'vite_angular19_todo_project';

+ ngOnInit(): void {
+ console.log(environment.production ? 'Production' : '開發中')
+ }
}

環境變數github

Angular 子傳父「傳值」

子元件藉由@Output裝飾器定義屬性,該屬性為EventEmitter實體,可以設定要傳送的資料型別,透過事件繫結(Event Binding)通知父元件有事件發生。

子元件

1在子元件類中匯入 Output 和 EventEmitter
1
import { Output, EventEmitter } from '@angular/core';
2@Output() newPageTitle = new EventEmitter();
1
2
3
4
5
6
export class NavbarComponent implements OnInit{
@Output() newPageTitle = new EventEmitter<string>();
sendActionList(path: string) {
+ this.newPageTitle.emit(path);
}
}
舉例 ex:Navbar
TS
在子元件類中匯入 Output 和 EventEmitter
NgFor 必須載入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
+ import { Component, Input, OnInit,EventEmitter, Output } from '@angular/core';
+ import { NgFor } from '@angular/common';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { navListType } from '../../Type/nav';
@Component({
selector: 'app-navbar',
imports: [
+ NgFor,
CommonModule,
RouterLink
],
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss',
})

export class NavbarComponent implements OnInit{
// 子傳父
+ @Input() sendNavLists!: navListType[];
+ @Input() sendPageTitle!: string;
+ @Input() sendIsActive!: boolean;
// 父傳子 @Output() 欄位 = new EventEmitter<屬性>();
+ @Output() newPageTitle = new EventEmitter<string>();
//點擊頁面按鈕函式
sendActionList(path: string) {
//this.欄位.emit()
+ this.newPageTitle.emit(path);
}
ngOnInit(): void {

}
}
HTML
*ngFor 陣列方式迴圈
[ngClass] 動態Class
[routerLink] 動態Class
(click) 點擊函式
1
2
3
4
5
6
7
8
 <ul  class="nav">
<li *ngFor="let item of sendNavLists ; let i = index"
[ngClass]="sendPageTitle === item.title ? 'activeNav' : ''">
<a [routerLink]="item.path"
(click)="sendActionList(item.title)"
class="cursor-pointer">{{item.title}}</a>
</li>
</ul>
父元件
Html
(newPageTitle)="actionList($event)" => @Output() 藉由newPageTitle 通訊
1
2
3
4
5
6
7
8
9
10
<app-navbar
[sendNavLists]="navLists"
[sendPageTitle]="pageTitle"
[sendIsActive]="isActive"
+ (newPageTitle)="actionList($event)"
></app-navbar>

<main class="main">
<router-outlet />
</main>

TS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { environment } from '../environments/environment';
import { NavbarComponent } from "./components/navbar/navbar.component";
import { navListType } from "./Type/nav";

@Component({
selector: 'app-root',
imports: [
RouterOutlet,
FormsModule,
NavbarComponent,
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})

export class AppComponent {
title = "我的網站";
navLists: navListType[] = [
{ title: 'Home' ,path:'/'},
{ title: 'Users', path: 'user' },
{title:'Login',path:'login'}
]
pageTitle: string = 'Home';
isActive: boolean = false;
+ actionList(path: string) {
+ this.pageTitle = path;
+ }
ngOnInit(): void {
console.log(environment.production ? 'Production' : '開發中')
}
}

Angular 父傳子「傳值」

組件通訊 Component Communication with @Input

有時應用程式開發需要您將資料傳送到元件。這些資料可用於自訂元件或將資訊從父元件傳送到子元件。
Angular 使用一個稱為@Input輸入的概念。這與其他框架中的 props 類似。若要建立輸入屬性,請使用@Input 裝飾器。

在本活動中,您將學習如何使用@Input 裝飾器向組件發送訊息。

Angular 父傳子「傳值」

子元件Ts

  • 引入 Input, OnInit => 註解1
  • 引入 NgFor
  • 引入 RouterLink Navbar 因為有使用RouterLink 必須引入
  • @Component() => 註解2
  • export class NavbarComponent 改為 export class NavbarComponent implements OnInit
  • export class NavbarComponent implements OnInit 內
    @Input() Object 陣列 ex.sendNavLists!: navListType[]
    @Input() string 字串 ex.sendPageTitle!: string
    @Input() boolean布林值 ex.sendIsActive!: boolean
  • ngOnInit
    用來初始化頁面內容,顯示數據綁定、設置 directive 和輸入屬性
    在第一次 ngOnChanges 完成後呼叫,只執行一次

註解1:Input 與 OnInit 顧名思義就是進入與輸出,是使用在父子層傳遞資料使用

註解2:@Component() 裝飾器告訴 Angular 這個 class 是一個 Component,而這個裝飾器裡有三個屬性:
  • selector – 我們在其他 Component 的 HTML 檔中如何用標籤選中這個 Component 並渲染。
  • templateUrl - 要到哪裡去找 HTML 檔案,預設是同個資料夾中的 "元件名稱.component.html"
  • styleUrls - 要到哪裡去找專屬這個 Component 的 CSS 檔案,預設是同個資料夾中的 "元件名稱.
  • component.css"
  • imports- 引入其他的依賴(Dependency)

子元件 Html

  • *ngFor
    是Angular 的一個結構性指令,用於在HTML模板中迭代陣列或可迭代對象的元素。它允許我們將數據動態地呈現在HTML 中,並以迭代方式生成元素。
  • [ngClass]
    動態Class狀態
  • [routerLink]
    routerLink 是Angular 的路由機制實作的Directive指示 routerLink 說明參考

子元件 寫法

@Input() 參數欄位名稱!: 屬性;
@Input() 參數欄位名稱: 屬性 |undefined;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
+ import { Component, Input, OnInit,Output,EventEmitter } from '@angular/core';
+ import { NgFor } from '@angular/common';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
interface navListType{
title: string,
path: string,
}

@Component({
selector: 'app-navbar',
imports: [
+ NgFor,
+ CommonModule,
+ RouterLink
],
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss',
})
// 實作初始化
+ export class NavbarComponent implements OnInit{
+ @Input() sendNavLists!: navListType[];
+ @Input() sendPageTitle!: string;
+ @Input() sendIsActive!: boolean;

@Output() newPageTitle = new EventEmitter<string>();
// 點擊頁面按鈕函式
actionList(titlePage: string) {
this.newPageTitle.emit(titlePag);
}
+ ngOnInit(): void {}
}

子元件 Html

1
2
3
4
5
6
7
8
<ul  class="nav">
<li *ngFor="let item of sendNavLists ; let i = index"
[ngClass]="sendPageTitle === item.title ? 'activeNav' : ''">
<a [routerLink]="item.path"
(click)="actionList(item.title)"
class="cursor-pointer">{{item.title}}</a>
</li>
</ul>

子元件 Scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  .nav{
list-style-type: none;
display: flex;
margin:0px auto auto auto;
padding-inline-start: 0px;
background-color: #00bcd4;

width: 100%;
max-width: 100%;
display: flex;
justify-content: center;
height: 50px;

li{
list-style-type: none;
display: flex;
margin: auto 10px;
a{
cursor: pointer;
text-decoration: none;
color:white;
}
}
}

.activeNav{
border-bottom: 1px solid white;
}

父元件

  • 載入NavbarComponent
父元件 TS
=> 載入子元件 => 以下藉由子元件@input 欄位來傳值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { environment } from '../environments/environment';
+ import { NavbarComponent } from "./components/navbar/navbar.component";
interface navListType{
title: string,
path: string,
}
@Component({
selector: 'app-root',
imports: [
RouterOutlet,
FormsModule,
+ NavbarComponent,
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
// NavBar 數據
+ navLists: navListType[] = [
+ { title: 'Home' ,path:'/'},
+ { title: 'Users', path: 'user' },
+ {title:'Login',path:'login'}
+ ]
+ pageTitle: string = 'Home';
+ isActive: boolean = false;
ngOnInit(): void {
console.log(environment.production ? 'Production' : '開發中')
}
}
父元件 Html

以下藉由子元件@input 欄位來傳值

  • [sendNavLists]
    是Angular 父對子傳值中的一個
  • [sendPageTitle]
    是Angular 的一個結構性指令,用於在HTML模板中迭代陣列或可迭代對象的元素。它允許我們將數據動態地呈現在HTML 中,並以迭代方式生成元素。
  • [sendIsActive]
    是Angular 的一個結構性指令,用於在HTML模板中迭代陣列或可迭代對象的元素。它允許我們將數據動態地呈現在HTML 中,並以迭代方式生成元素。
1
2
3
4
5
6
7
8
9
10
<app-navbar
[sendNavLists]="navLists"
[sendPageTitle]="pageTitle"
[sendIsActive]="isActive"
></app-navbar>

<main class="main">
<router-outlet />
</main>

navLists功能

Angular Directive(指示)

Angular Directive(指示)

Angular中的Directive 直接翻譯是指令、指示,
我個人比較喜歡指示、指引的翻譯,代表Angular看到這個特別的詞,要去做對應的事情或動作。
指令比較像在Terminal上做的輸入。

Angular中的Directive分成以下三種:

  • 元件型 Component Directive
  • 屬性型 Attribute Directive
  • 結構型 Structure Directive

元件型指示 Component Directive

什麼是元件型指示?
指的就是元件啦!

ex: < app-root >、< app-component >
含有樣板的指示,以標籤(tag)形式呈現

屬性型 Attribute Directive

屬性、樣式
屬性型指令有以下三種:

  • ngStyle
  • ngClass
  • ngModel
ngStyle

app.component.html 檔案內
[ngStyle]="{'font-size': 26 + counter + 'px'}"

1
2
3
4
<div class="container">
<h1 [ngStyle]="{'font-size': 26 + counter + 'px'}">{{counter}}</h1>
<input type="button" value="計數器+按了會變大" (click)="count()">
</div>

app.component.ts中設定屬性變數
(click)="count()"

1
2
3
4
5
6
7
8
9
import { Component } from '@angular/core';

export class AppComponent {
counter = 0;
count(){
this.counter++;
}
}

另一種寫法

ngStyle app.component.html 檔案內 [ngStyle]="getStyle()"
1
2
3
4
<div class="container">
<h1 [ngStyle]="getStyle()">{{counter}}</h1>
<input type="button" value="計數器+按了會變大" (click)="count()">
</div>
app.component.ts中設定屬性變數 (click)="count()"
1
2
3
4
5
6
7
8
9
10
11
12
import { Component } from '@angular/core';

export class AppComponent {
counter = 0;
count(){
this.counter++;
}
getStyle(){
return {('font-size': 26 + this.counter) + 'px'};
}
}

[style.font-size] 綁定style
[style.color]綁定style
[ngClass]綁定class
app.component.html 檔案內
1
2
3
4
5
<div class="container">
<h1 [style.font-size]="(26+counter)+'px'" [style.color]='"green"'>{{counter}}</h1>
<input type="button" value="計數器+按了會變大" (click)="count()">
<p [ngClass]="{highlight: counter % 2 == 0}">偶數時會有螢光背景</p>
</div>

css

1
2
3
.highlight{
background: yellow;
}

也可以改成簡短一點

1
<p [class.highlight]="counter % 2 == 0">偶數時會有螢光背景</p>

全部

1
2
3
4
5
<div class="container">
<h1 [style.font-size]="(26+counter)+'px'" [style.color]='"green"'>{{counter}}</h1>
<input type="button" value="計數器+按了會變大" (click)="count()">
<p [class.highlight]="counter % 2 == 0">偶數時會有螢光背景</p>
</div>

結構型 Structure Directive:會影響到程式流的指令

  • ngIf
  • ngSwitch
  • ngFor
ngIf
符合條件時會動態新增DOM、不符條件時動態移除(是移除而非隱藏) 若該元素被移除,若元素裡面有其他的tag或directive 也會一併被移除。 斬草除根。 app.component.html 檔案內=>HTML
1
<p *ngIf="counter % 2 == 0">偶數時整個DOM會被移除</p>
ngSwitch
app.component.html 檔案內=>HTML
1
2
3
4
5
6
7
8
9
10
<div class="container">
<h1 [style.font-size]="(26+counter)+'px'" [style.color]='"green"'>{{counter}}</h1>
<input type="button" value="計數器+按了會變大" (click)="count()">

<div [ngSwitch]="counter % 3">
<div *ngSwitchCase="1"><p>3N+1</p></div>
<div *ngSwitchCase="2"><p>3N+2</p></div>
<div *ngSwitchDefault><p>Default 三的倍數</p></div>
</div>
</div>
ngFor
*ngFor="let item of list" app.component.ts中設定屬性變數 Ts
1
2
3
4
5
6
7
8
9
10
export class AppComponent {
data = [
{SID: 'S001', name: '王大明', score: 80, 'image-url': 'https://picsum.photos/id/10/200/300', 'self-intro': '<div>大家好,我是王大明。</div>'},
{SID: 'S002', name: '林一二', score: 99, 'image-url': 'https://picsum.photos/id/20/200/300', 'self-intro': '<div>大家好,我是林一二<br>請各位多多指教。</div>'},
{SID: 'S003', name: '黃阿道', score: 54, 'image-url': 'https://picsum.photos/id/30/200/300', 'self-intro': '<div>大家好,我是黃阿道<br>我成績不太好<br>請大家多包涵。</div>'},
];


set = new Set([1, 1, 2, 3, 4, 5, 5, 5]);
}
app.component.html 檔案內=>HTML
1
2
3
4
5
6
7
8
9
10
11
12
<div class="container" *ngFor="let item of set;">
<p>{{item}}</p>
</div>
<div class="container d-flex">
<div class="student border border-dark m-5" id="student0" *ngFor="let item of data">
<p>學號: {{item.SID}}</p>
<p>姓名: {{item.name}}</p>
<img [src]="item['image-url']" alt="大頭照">
<p>分數: {{item.score}}</p>
<p class="self-intro" [innerHTML]="item['self-intro']"></p>
</div>
</div>

參考資料

Angular Binding (綁定)

內嵌繫結

app.component.ts中設定屬性變數

1
2
3
4
5
export class AppComponent {
title = 'My Website';
link = '前往Google';
url = 'https://google.com';
}

app.component.html 檔案內使用雙括號將變數宣染
{{}} 繃定

1
2
3
4
<div class="container my-5">
<h1>{{title}}</h1>
<a href="{{url}}">{{link}}</a>
</div>
app.component.ts中設定屬性變數
1
2
3
4
export class AppComponent {
a = 10;
b = 50;
}
app.component.html 檔案內直接在樣板做運算 {{算數}} 繃定

1
<p>a+b: {{a + b}}</p>

屬性繫結 property binding

[property] = ‘statement’
app.component.html 檔案內
a標籤 [routerLink] 必須引入RouterLink 在 from ‘@angular/router’
[欄位參數]繫結

1
2
3
4
<div class="container my-5">
<h1>標題</h1>
<a [routerLink]="欄位參數">Google連結</a>
</div>

Class動態樣式 [ngClass] 綁定方是等於布林值是或否 ? ‘active’ : ‘’”
綁定自定義的attribute?
app.component.html 檔案內
[attr.data-title]繫結

1
<h1 [attr.data-title]=title>標題</h1>

事件繫結 event binding

app.component.html 檔案內
(click)事件繫結函式

1
2
3
4
<div class="container my-5">
<input type="button" value="更換標題" (click)="changeTitle()">
<h1>{{title}}</h1>
</div>

app.component.ts中設定屬性變數
點擊函式改變title

1
2
3
4
5
6
export class AppComponent {
title = 'My Website';
changeTitle(){
this.title = '更換後的標題';
}
}

傳入事件參數$event

app.component.html 檔案內
點擊函式傳入參數$event

1
2
3
4
5
6
<div class="container my-5">
<input type="button"
value="更換標題"
(click)="changeTest($event)">
<h1>{{title}}</h1>
</div>

app.component.ts中設定屬性變數

1
2
3
4
5
6
7
export class AppComponent {
title = 'My Website';
changeTest($event: any) {
console.log($event)
this.title = 'Lara換後的標題';
}
}

雙向繫結 two-way binding

[(ngModel)] = ‘property’

雙向繫結能同時做到屬性繫結()以及事件繫結[],所以符號是[()]用他來繫結某個property
app.component.html 檔案內
表單[()] 綁定

1
2
3
<input type="text" value="" placeholder="請輸入點什麼吧" [(ngModel)]="text">
<p>您的輸入: <span>{{text}}</span></p>
<p>字數: <span>{{text.length}}</span></p>

app.component.ts中設定屬性變數

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component } from '@angular/core';
+ import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [
+ FormsModule
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
+ text="text"
}

Angular Pipe 管道元件

  • AsyncPipe:從非同步原語中解開一個值。
  • CurrencyPipe:將數字轉換為貨幣字串,並根據確定群組大小和分隔符號、小數點字元和其他特定於區域設定的配置的區域設定規則進行格式化。
  • DatePipe:根據區域設定規則格式化日期值。
  • DecimalPipe:根據數字選項和區域設定規則格式化值。區域設定決定群組大小和分隔符號、小數點字元以及其他特定於區域設定的配置。
  • JsonPipe:將值轉換為 JSON 格式的表示形式。對於調試很有用。
  • KeyValuePipe:將物件或映射轉換為鍵值對的陣列。
  • LowerCasePipe:將文字轉換為全部小寫。
  • PercentPipe:將數字轉換為百分比字串,並根據確定群組大小和分隔符號、小數點字元和其他特定於語言環境的配置的語言環境規則進行格式化。
  • SlicePipe:建立一個包含元素子集(切片)的新陣列或字串。
  • TitleCasePipe:將文字轉換為標題大小寫。將每個單字的首字母大寫,並將其餘部分轉換為小寫。單字由任何空白字元(如空格、製表符或換行符)分隔。
  • UpperCasePipe:將文字轉換為全部大寫。

AsyncPip從非同步原語中解開一個值。

非同步管道訂閱 ObservablePromise 並傳回其發出的最新值。當發出新值時,非同步管道會標記要檢查變更的元件。當組件被銷毀時,非同步管道會自動取消訂閱以避免潛在的記憶體洩漏。當表達式的參考發生變化時,非同步管道會自動取消訂閱舊的 Observable 或 Promise,並訂閱新的。
This example binds a Promise to the view. Clicking the Resolve button resolves the promise.
此範例將 Promise 綁定到視圖。點擊“解決”按鈕即可解決該承諾。
參考AsyncPipe官網

引入AsyncPipe

1
{{ obj_expression | async }}

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { AsyncPipe } from '@angular/common';
@Component({
selector: 'async-promise-pipe',
template: `<div>
<code>promise|async</code>:
<button (click)="clicked()">{{ arrived ? 'Reset' : 'Resolve' }}</button>
<span>Wait for it... {{ greeting | async }}</span>
</div>`,
})
export class AsyncPromisePipeComponent {
greeting: Promise<string> | null = null;
arrived: boolean = false;

private resolve: Function | null = null;

constructor() {
this.reset();
}

reset() {
this.arrived = false;
this.greeting = new Promise<string>((resolve, reject) => {
this.resolve = resolve;
});
}

clicked() {
if (this.arrived) {
this.reset();
} else {
this.resolve!('hi there!');
this.arrived = true;
}
}
}

也可以將非同步與 Observable 一起使用。下面的範例將時間 Observable 綁定到視圖。 Observable 使用目前時間不斷更新視圖。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { AsyncPipe } from '@angular/common';
import { Observable, Observer } from 'rxjs';

@Component({
selector: 'async-observable-pipe',
template: '<div><code>observable|async</code>: Time: {{ time | async }}</div>',
})

export class AsyncObservablePipeComponent {
time = new Observable<string>((observer: Observer<string>) => {
setInterval(() => observer.next(new Date().toString()), 1000);
});
}

大寫 DatePipe:uppercase,小寫 LowerCasePipe:lowercase,首字大寫 TitleCasePipe:titlecase

大寫 引入UpperCasePipe import { UpperCasePipe } from '@angular/common';
1
2
3
// html
<p>{{'Hello World' | uppercase}}</p>

HELLO WORLD

小寫
引入LowerCasePipe
import { LowerCasePipe } from ‘@angular/common’;

1
2
3
// html
<p>{{'Hello World' | lowercase}}</p>

hello world
首字大寫
引入TitleCasePipe
import { TitleCasePipe } from ‘@angular/common’;

1
<p>{{ 'some string' | titlecase }}</p>

日期 DatePipe

根據區域設定規則格式化日期值。

參考DatePipePipe官網

引入UpperCasePipe
import { DatePipe } from ‘@angular/common’;

1
{{ value_expression | date [ : format [ : timezone [ : locale ] ] ] }}

ts檔案中新增以下設定

1
2
3
4
5
6
7
8
9
10
11
+ import { DatePipe} from '@angular/common';

@Component({
selector: 'app-test-list',
+ imports: [DatePipe],
templateUrl: './test-list.component.html',
styleUrl: './test-list.component.scss'
})
export class TestListComponent implements OnInit {
now :number= Date.now();
}
| date
html
1
<td>{{now  | date  }} </td> 
顯示:Apr 4, 2025
| date | uppercase
1
<p>{{now | date | uppercase }}</p> 
顯示:APR 4, 2025
| date: 'short':'+0000'
1
<p> {{now | date: 'short':'+0000' }}</p> 
顯示:4/4/25, 6:35 AM
| date:'yyyy-MM-dd HH:mm:ss'
1
<p> {{now |  date:'yyyy-MM-dd HH:mm:ss' }}</p> 
顯示:2025-04-04 06:35:34
| date: 'short'
1
<p> {{now | date: 'short' }}</p> 
顯示:4/4/25, 2:35 PM
| date: 'medium'
1
<p> {{now | date: 'medium' }}</p> 
顯示:Apr 4, 2025, 2:35:34 PM
| date: 'long'
1
<p> {{now | date: 'long' }}</p> 
顯示:April 4, 2025 at 2:35:34 PM GMT+8
| date: 'full'
1
<p> {{now | date: 'full' }}</p> 
顯示:Friday, April 4, 2025 at 2:35:34 PM GMT+08:00

貨幣 (CurrencyPipe)

將數字轉換為貨幣字串,並根據確定群組大小和分隔符號、小數點字元和其他特定於區域設定的配置的區域設定規則進行格式化。
CurrencyPipe公式

1
{{ value_expression | currency [ : currencyCode [ : display [ : digitsInfo [ : locale ] ] ] ] }}
引入UpperCasePipe import { CurrencyPipe } from '@angular/common';
1
2
3
4
5
6
7
8
9
10
11
+ import {  CurrencyPip} from '@angular/common';

@Component({
selector: 'app-test-list',
+ imports: [ CurrencyPip],
templateUrl: './test-list.component.html',
styleUrl: './test-list.component.scss'
})
export class TestListComponent implements OnInit {
}

| currency
html
1
<p> {{50000 | currency}}</p>
顯示:$50,000.00
| currency:'TWD':true
html
1
<p> {{50000 | currency:'TWD':true}}</p> 
顯示:NT$50,000.00
| currency:'TWD':'TWD'
html
1
<p> {{50000 | TWD50,000.00}}</p> 
顯示:TWD50,000.00
| currency:'TWD':'TWD':'4.1-3'
digitsInfo:數字顯示方式 (String)
  • minIntegerDigits:小數點前的最小整數位數。默認值為1
  • minFractionDigits:小數點後的最少位數。默認值為2
  • maxFractionDigits:小數點後的最大位數。默認值為2
html
1
<p>{{987.1234 | currency:'TWD':'TWD':'4.1-3'}}</p>
顯示:TWD0,987.123
1
2
3
4
<p>{{ 0.259 | currency: 'CAD' }}</p>
<p>{{ 1.3495 | currency: 'CAD' }}</p>
<p> {{ 0.259 | currency: 'CAD' : 'code' }}</p>
<p> {{ 1.3495| currency: 'CAD' : 'symbol' : '4.2-2' }}</p>

SlicePip

建立一個包含元素子集(切片)的新陣列或字串。
參考官網SlicePip資料

引入SlicePipe
import { SlicePipe } from ‘@angular/common’;

1
2
3
4
5
6
7
8
9
10
+ import {SlicePipe} from '@angular/common';
@Component({
selector: 'app-test-list',
+ imports: [ SlicePipe],
templateUrl: './test-list.component.html',
styleUrl: './test-list.component.scss'
})
export class TestListComponent implements OnInit {
collection: string[] = ['a', 'b', 'c', 'd'];
}

使用公式

1
{{ value_expression | slice : start [ : end ] }}

html

1
2
3
<ul>
<li *ngFor="let i of collection | slice: 1 : 3">{{ i }}</li>
</ul>

顯示:

  • b
  • c

DecimalPipe

根據數字選項和區域設定規則格式化值。區域設定決定群組大小和分隔符號、小數點字元以及其他特定於區域設定的配置

參考官網DecimalPipe資料

1
2
3
4
5
6
7
8
9
10
+ import {DecimalPipe} from '@angular/common';
@Component({
selector: 'app-test-list',
+ imports: [DecimalPipe],
templateUrl: './test-list.component.html',
styleUrl: './test-list.component.scss'
})
export class TestListComponent implements OnInit {
testNumber:number=3.14159;
}

html

1
<p>{{ testNumber| number  }}</p>

顯示:3.141

小數點建議定義參數後 .toFixed(5)

1
2
3
testNumber:number=3.14159;

{{ testNumber.toFixed(5) }}

自定義 Pipe

  • 新增pipe 功能ts名稱
    • 引入:Pipe, PipeTransform
    • @Pipe({名稱:功能})
    • export class FormatTaiwanYearPipe implements PipeTransform
    • transform(date: number | any) {.... return ....}
ˊ

範例:民國年

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'TaiwanYear'
})

export class FormatTaiwanYearPipe implements PipeTransform {
transform(date: number | any) {
let d = new Date(date),
year = d.getFullYear() - 1911,
month = (d.getMonth()+1 < 10 ? '0'+(d.getMonth()+1) : d.getMonth()+1) ,
day = (d.getDate()< 10 ?'0'+d.getDate():d.getDate()),
hour=(d.getHours()<10?'0'+d.getHours():d.getHours()) ,
minutes = (d.getMinutes()<10?'0'+d.getMinutes():d.getMinutes())

return [year, month, day].join('-')+' '+ [hour,minutes].join(':');
}
}

頁面使用

  • 引入類名稱
1
2
3
4
5
6
7
8
9
import { FormatTaiwanYearPipe } from '存放位址';

@Component({
imports: [FormatTaiwanYearPipe],
})

export class TestListComponent implements OnInit {
nowTimeStamp:number =Date.now()
}
1
2
3
4
<p >
{{ nowTimeStamp | TaiwanYear }}
</p>

github

Angular 元件執行順序 & 生命週期

ng cli產出的元件,底下都會有以下兩個方法(Method)

  • constructor Typescript的構建物件函式
  • ngOnInit Angular元件初始化時執行
執行結果
  • constructor 構建一個物件首先執行的函式,在這裡引入物件所需用到的服務
  • ngOnInit 生命週期 元件初始化,進入此元件做的事情,此時畫面尚未生成完畢
  • ngAfterViewInit 生命週期 當畫面(View)渲染完畢才做的事 例如抓取頁面上的某個節點,若在畫面出現之前執行的話會抓不到元素而變成undefined
  • ngOnDestroy 生命週期 離開、結束此元件時做的事情 刷新頁面或離開頁面都不會觸發效果 因為是這兩件事情主導權不在Angular手上,而是由瀏覽器銷毀 在未來提及Rouing切換頁面元件時可以看到效果

以人類的生命週期來對應的話,分別是

  • 受精階段: constructor 建構胚胎卵,注入所需要的養分服務,以幫助未來成長
  • 胎兒階段: ngOnInit 元件在媽媽的肚子裡面長好了
  • 嬰兒階段: ngAfterViewInit 畫面生出來了,我們肉眼看的見四肢了
  • 死亡階段: destroy 生命離開肉體軀殼,被大自然回收了

Vue3 錄音與播放套件 js-audio-recorder

安装js-audio-recorder

1
npm install js-audio-recorder --save

獲取數據

1
2
3
4
5
// 獲取錄音數據 buffer
recorder.getRecordAnalyseData()

// 獲取播放數據 buffer
recorder.getPlayAnalyseData()

錄音方法

1
2
3
4
5
6
7
8
9
10
11
// 錄音
recorder.start()

// 暫停
recorder.pause()

// 繼續
recorder.resume()

// 結束
recorder.stop()

播放

1
2
3
4
5
6
7
8
9
10
11
// 播放
recorder.play()

// 暫停
recorder.pausePlay()

// 繼續
recorder.resumePlay()

// 結束
recorder.stopPlay()

下载方法

1
2
3
4
5
// 下载 pcm 文件
recorder.downloadPCM()

// 下载 wam 文件
recorder.downloadWAV()

銷毀

1
2
// 銷毀
recorder.destroy()

參數配置

1
2
3
4
5
6
7
8
9
10
11
 let query = {
// 取樣位數,支援 8 或 16,預設是 16
sampleBits: 16,
// 取樣率,支援11025、16000、22050、24000、44100、48000,依瀏覽器預設值
sampleRate: 48000,
// 聲道,支援1或2,預設是1
numChannels: 1,
// 是否記錄邊,預設是false
compiling: false
}
new Recorder(query)

常用属性 attrs duration

1
2
// 時長
recorder.duration

文件大小 fileSize

1
recorder.fileSize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
<template>
<div class="BaseRecorder">
<div class="BaseRecorder-record">
<i @click="startRecorder()" title="開始錄音" class="fa-solid fa-microphone"></i>
<i @click="resumeRecorder()" title="继续錄音" class="fa-solid fa-microphone-lines"></i>
<i @click="pauseRecorder()" title="暂停錄音" class="fa-solid fa-microphone-lines-slash"></i>
<i @click="stopRecorder()" title="停止錄音" class="fa-solid fa-microphone-slash"></i>
</div>
<div class="BaseRecorder-play">
<i @click="playRecorder()" class="fa-solid fa-volume-high"></i>
<i @click="pausePlayRecorder()" class="fa-solid fa-volume-xmark"></i>

<button @click="resumePlayRecorder()">恢复錄音播放</button>
<button @click="stopPlayRecorder()">停止錄音播放</button>
</div>
<div class="BaseRecorder-download">
<i @click="downPCM()" title="下载PCM" class="fa-solid fa-cloud-arrow-down"></i>
<i @click="downWAV()" title="下载WAV" class="fa-solid fa-cloud-arrow-down"></i>

</div>
<div class="BaseRecorder-destroy">
<button @click="destroyRecorder()">销毁錄音</button>

</div>
<div class="BaseRecorder-wave">
<canvas ref="record"></canvas>
<canvas ref="play"></canvas>
</div>
</div>

</template>

<script setup lang="ts">
import Recorder from 'js-audio-recorder'
import { ref, computed,onMounted} from 'vue'
const recorder = ref<any>(null)
//波浪图-录音
const drawRecordId = ref<any>(null)
//波浪图-播放
const drawPlayId = ref<any>(null)
const init = () => {
let query = {
// 取樣位數,支援 8 或 16,預設是 16
sampleBits: 16,
// 取樣率,支援11025、16000、22050、24000、44100、48000,依瀏覽器預設值
sampleRate: 48000,
// 聲道,支援1或2,預設是1
numChannels: 1,
// 是否記錄邊,預設是false
compiling: false
}
recorder.value = new Recorder(query)
// console.log('recorder.value',recorder.value)
}

//開始錄音
const startRecorder =() =>{
recorder.value.start()
.then(
() => {
drawRecord()
},
error => {
// 出错了
console.log(`${error.name} : ${error.message}`)
}
)
}
//ref="record" =>this.$refs.record
const record = ref<any>()
//繪製記錄
const drawRecord = () => {
//**方法通知瀏覽器我們想要產生動畫,並且要求瀏覽器在下次重繪畫面前呼叫特定函數更新動畫。這個方法接受一個引數作為下次重繪前調用的回呼函數。
let query = {
canvas: record.value,
dataArray: recorder.value.getRecordAnalyseData(),
bgcolor: 'rgb(50 65 150)',
lineWidth: 2,
lineColor: 'rgb(255, 255, 255)'
}
drawRecordId.value = requestAnimationFrame(drawRecord)
drawWave(query)
console.log('query', query)
console.log('drawRecordId.value', drawRecordId.value)
}
//畫波
const drawWave = ({
canvas,
dataArray,
bgcolor = 'rgb(200, 200, 200)',
lineWidth = 2,
lineColor = 'rgb(0, 0, 0)'
}) => {
if (!canvas) return
const ctx = record.value.getContext('2d')
const bufferLength = dataArray.length
// 一個點佔多少位置,每個bufferLength個點要控制
const sliceWidth = canvas.width / bufferLength
// 受傷點的x軸位
let x = 0

// 填充背景色
ctx.fillStyle = bgcolor
ctx.fillRect(0, 0, canvas.width, canvas.height)

// 設定,設計顏色
ctx.lineWidth = lineWidth
ctx.strokeStyle = lineColor

ctx.beginPath()

for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128
const y = (v * canvas.height) / 2

if (i === 0) {
// 第一个点
ctx.moveTo(x, y)
} else {
// 剩余的点
ctx.lineTo(x, y)
}
// 依次平移,绘制所有点
x += sliceWidth
}

// 最后一个点
ctx.lineTo(canvas.width, canvas.height / 2)
ctx.stroke()
}
// 继续錄音
const resumeRecorder = () => {
recorder.value.resume();
}
//暂停錄音
const pauseRecorder = () => {
recorder.value.pause();
//cancelAnimationFrame:是一種JavaScript實作動畫的技巧,它解決了傳統計時器(setTimeout和setInterval)在處理動畫時的一些問題,特別是與瀏覽器重繪
drawRecordId.value && cancelAnimationFrame(drawRecordId.value)
drawRecordId.value = null
}
// 停止錄音
const stopRecorder = () => {
recorder.value.stop();
drawRecordId.value && cancelAnimationFrame(drawRecordId.value)
drawRecordId.value = null
}
//录音播放
const playRecorder = () => {
recorder.value.play()
drawPlay()
}
/**
* 绘制波浪图-播放
* */
const play = ref<any>(null)
//
const drawPlay = () => {
drawPlayId.value = requestAnimationFrame(drawPlay)
drawWave({
canvas: play,
dataArray: recorder.value.getPlayAnalyseData()
})
console.log('recorder.value.getPlayAnalyseData()', recorder.value.getPlayAnalyseData())
}
//暂停录音播放
const pausePlayRecorder = () => {
recorder.value.pausePlay()
}
//恢复录音播放
const resumePlayRecorder = () => {
recorder.value.resumePlay()
drawPlay() // 绘制波浪图
}
//
const stopPlayRecorder = () => {
recorder.value.stopPlay();

}

const downPCM = () => {
recorder.value.downloadPCM('新文件');
console.log(recorder.value.downloadPCM('新文件'))
}
const downWAV = () => {
recorder.value.downloadWAV('新文件');
console.log(recorder.value.downloadWAV('新文件'))
}
const destroyRecorder = () => {
recorder.value.destroy().then(function () {
drawRecordId.value && cancelAnimationFrame(drawRecordId.value)
drawRecordId.value = null

drawPlayId.value && cancelAnimationFrame(drawPlayId.value)
drawPlayId.value = null
recorder.value = null
})
}


onMounted(() => {
init();
})
</script>

參考
前端工程師
API串接常見問題 - CORS - 概念篇 (1)
API串接常見問題 - CORS - 概念篇 (2)

使用 MediaStream 錄製 API

MediaStream 錄製 API