在 Vue 生態系中,VueUse 提供許多 Composables,讓我們可以快速使用瀏覽器功能,例如監聽網路狀態、保存使用者設定,或偵測元素是否進入可視範圍。
其實 Angular 也有類似的工具,就是 Signality。
Signality 是一套以 Angular Signal 為核心的工具函式庫,將常見的 Browser API 與 DOM Observer 封裝起來,減少自行處理事件監聽、狀態更新與生命週期清理的程式碼。
今天挑選四個比較實用的功能:
Signal 資料保存到瀏覽器。使用者操作系統時,可能會遇到 Wi-Fi 中斷或網路不穩的情況,導致資料無法正常送出。
一般可以透過 navigator.onLine 取得連線狀態,再監聽 online、offline 事件。如果要搭配 Angular Signal,還需要自行更新狀態,並在 Component 銷毀時移除事件監聽。
network.component.ts
import { Component, signal, OnDestroy } from '@angular/core';
@Component({
selector: 'app-network',
template: `
@if (!isOnline()) {
<p>目前沒有網路連線,請檢查網路狀態</p>
}
`
})
export class NetworkComponent implements OnDestroy {
readonly isOnline = signal(navigator.onLine);
private readonly updateStatus = () => {
this.isOnline.set(navigator.onLine);
};
constructor() {
window.addEventListener('online', this.updateStatus);
window.addEventListener('offline', this.updateStatus);
}
ngOnDestroy() {
window.removeEventListener('online', this.updateStatus);
window.removeEventListener('offline', this.updateStatus);
}
}
改用 Signality 的 network() 後,就不需要自行處理事件監聽與清理:
network.component.ts
import { Component } from '@angular/core';
import { network } from '@signality/core';
@Component({
selector: 'app-network',
template: `
@if (!net.isOnline()) {
<p>目前沒有網路連線,請檢查網路狀態</p>
}
`
})
export class NetworkComponent {
readonly net = network();
}
network() 會回傳多個 Signal,其中 isOnline() 代表瀏覽器目前的連線狀態。當網路中斷或恢復時,畫面就會跟著更新。
除了判斷是否連線,還可以取得其他網路資訊:
net.effectiveType(); // 連線類型,例如 3g、4g
net.downlink(); // 預估下載速度 Mbps
net.rtt(); // 預估往返延遲 ms
net.saveData(); // 是否啟用節省流量
不過,isOnline() 不代表後端 API 一定能正常連線,只能反映瀏覽器回報的連線狀態。
另外,effectiveType()、downlink() 等資訊依賴 Network Information API,目前主要由 Chromium 系列瀏覽器支援。可以透過 net.isSupported() 判斷是否支援,再決定是否使用這些資訊。
開發網站時,經常需要保存一些資料,例如深淺色主題、購物車內容,或使用者還沒送出的表單。
通常會使用 localStorage 或 sessionStorage:
localStorage.setItem('name', 'Antonio');
const name = localStorage.getItem('name');
不過,Storage API 只能儲存字串。如果要保存物件,還得自行處理 JSON.stringify() 與 JSON.parse(),而且資料更新後也不會自動通知 Angular Signal。
Signality 提供的 storage() 會回傳 WritableSignal,可以直接使用 set()、update() 更新資料,並同步保存到瀏覽器 Storage。
例如,使用者正在填寫表單,希望重新整理頁面後,還能保留尚未送出的內容。
draft.component.ts
import { Component } from '@angular/core';
import { storage } from '@signality/core';
@Component({
selector: 'app-draft',
template: `
<textarea
[value]="draft()"
(input)="draft.set($any($event.target).value)"
placeholder="請輸入內容..."
></textarea>
<button (click)="draft.set('')">
清空草稿
</button>
`
})
export class DraftComponent {
readonly draft = storage('form-draft', '', {
type: 'session'
});
}
storage() 的第一個參數是 Storage Key,第二個參數是預設值。這裡使用 type: 'session',將資料保存在 sessionStorage,所以即使重新整理頁面,草稿也不會消失。
如果希望關閉分頁後仍然保留資料,可以改用預設的 localStorage:
readonly draft = storage('form-draft', '');
兩者的差異如下:
localStorage |
sessionStorage |
|
|---|---|---|
| 保存時間 | 持續保存,直到被清除 | 分頁工作階段結束時清除 |
| 重新整理 | 保留 | 保留 |
| 不同分頁 | 同源分頁可共用 | 通常各分頁獨立 |
storage() 也能保存物件或陣列,不需要自行處理 JSON 轉換:
readonly settings = storage('settings', {
theme: 'light',
fontSize: 16
});
changeTheme() {
this.settings.update(value => ({
...value,
theme: 'dark'
}));
}
這裡透過 update() 取得目前的設定,再使用展開運算子建立新物件,只修改 theme,其他屬性則保留原本的值。
要注意,Storage 裡的資料可以透過 DevTools 查看或修改,因此不適合保存密碼或其他敏感資訊。
製作圖片延遲載入、無限捲動或進場動畫時,我們經常需要知道某個元素是否進入畫面。
瀏覽器原生提供 IntersectionObserver,可以觀察元素與可視範圍的交集狀態。Signality 則提供 intersectionObserver(),讓我們可以搭配 Angular 的 viewChild() 使用。
以圖片延遲載入為例,HTML 本身就提供了 loading="lazy":
<img
src="/images/photo.jpg"
loading="lazy"
alt="風景照片"
/>
設定後,瀏覽器會自行決定什麼時候下載圖片,不一定要等到圖片真正出現在畫面上。
如果希望更精確地控制載入時機,例如當圖片距離可視範圍 200px 時才開始載入,就可以使用 intersectionObserver()。另外,也可以先顯示 Placeholder,避免圖片載入前出現空白區塊。
lazy-image.component.ts
import {
Component,
ElementRef,
signal,
viewChild
} from '@angular/core';
import { intersectionObserver } from '@signality/core';
@Component({
selector: 'app-lazy-image',
template: `
<div class="spacer">往下捲動</div>
<div #container class="image-container">
@if (shouldLoad()) {
<img
src="/images/photo.jpg"
width="800"
height="500"
alt="風景照片"
[class.loaded]="isLoaded()"
(load)="isLoaded.set(true)"
/>
}
</div>
`,
styles: [`
.spacer {
height: 100vh;
}
.image-container {
width: 100%;
aspect-ratio: 8 / 5;
background: #e5e7eb;
overflow: hidden;
}
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.3s ease;
}
img.loaded {
opacity: 1;
}
`]
})
export class LazyImageComponent {
readonly container = viewChild<ElementRef>('container');
readonly shouldLoad = signal(false);
readonly isLoaded = signal(false);
readonly observer = intersectionObserver(
this.container,
([entry]) => {
if (entry?.isIntersecting) {
this.shouldLoad.set(true);
}
},
{
rootMargin: '200px 0px',
threshold: 0
}
);
}
這裡先透過 viewChild() 取得 #container,再交給 intersectionObserver() 觀察。
第二個參數是 Callback,當元素與觀察範圍的交集狀態符合觸發條件時,就會收到 entries 陣列,每筆資料都是一個 IntersectionObserverEntry。因為目前只觀察一個元素,所以使用 [entry] 直接取得第一筆結果。
另外,這次設定了兩個參數:
rootMargin: '200px 0px':將上下觀察範圍向外延伸 200px,讓圖片在進入畫面前就能觸發載入。threshold: 0:以元素是否進入或離開觀察範圍作為觸發條件,再透過 isIntersecting 判斷目前是否相交。當 entry.isIntersecting 為 true,就將 shouldLoad 設為 true,讓 Angular 透過 @if 建立 <img> 元素,瀏覽器便能開始下載圖片。
圖片還沒載入時,.image-container 會先顯示灰色背景作為 Placeholder。等圖片下載完成並觸發 (load) 事件後,isLoaded 就會變成 true,透過 CSS 的 opacity 讓圖片淡入。
這裡沒有再加上 loading="lazy",因為我們已經透過 IntersectionObserver 控制圖片建立時機。如果兩者一起使用,圖片建立後仍可能被瀏覽器延後下載,反而無法明確控制開始載入的時間。
另外,shouldLoad 一旦變成 true 就不會再恢復 false,避免使用者捲動離開後移除已載入的圖片。Signality 也會在 Component 銷毀時自動停止觀察,不需要手動呼叫 disconnect()。
Angular Service 或 Signal 可以在同一個 Application 中共用狀態,但如果使用者同時開啟多個分頁,每個分頁都會建立自己的 Angular Application,狀態不會自動同步。
例如,使用者在其中一個分頁修改了訂單資料,希望其他分頁也能收到更新通知,就可以使用瀏覽器提供的 BroadcastChannel API。
BroadcastChannel 可以讓同源分頁透過指定頻道互相傳遞訊息,不需要透過後端轉送。不過,實際能否通訊也會受到瀏覽器儲存分區的限制。
Signality 提供 broadcastChannel(),讓我們可以透過 Signal 取得其他分頁發送的訊息。
order-notification.component.ts
import { Component } from '@angular/core';
import { broadcastChannel } from '@signality/core';
type OrderEvent = {
type: 'updated';
orderId: string;
};
@Component({
selector: 'app-order-notification',
template: `
<button (click)="simulateUpdate()">
模擬更新訂單
</button>
@if (channel.data(); as event) {
<p>
訂單 {{ event.orderId }} 已在其他分頁更新
</p>
}
`
})
export class OrderNotificationComponent {
readonly channel =
broadcastChannel<OrderEvent>('order-events');
simulateUpdate() {
this.channel.post({
type: 'updated',
orderId: 'ORD-001'
});
}
}
broadcastChannel<OrderEvent>('order-events') 建立一個名為 order-events 的通訊頻道,並指定傳遞的訊息型別。
範例使用兩個功能:
channel.post():向同一頻道的其他分頁發送訊息。channel.data():透過 Signal 取得最後一次收到的訊息。可以在瀏覽器開啟兩個相同網址的分頁,接著在其中一個分頁點擊「模擬更新訂單」,另一個分頁就會收到訊息,並顯示訂單更新提示。
這裡只是模擬訂單更新事件。實際系統通常會在資料成功更新後才呼叫 post(),其他分頁收到通知後,再決定是否重新取得最新資料。
需要注意,BroadcastChannel 不會將訊息傳回發送者自己的 Channel 物件,因此發送訊息的分頁不會收到自己的更新通知。
另外,BroadcastChannel 不會保存先前發送的訊息。如果分頁重新整理或稍後才開啟,就不會收到過去的通知。這類資料仍然需要從後端重新取得。
除了訂單更新,BroadcastChannel 也可以用於跨分頁登出、通知已讀狀態同步等情境,特別適合處理同源分頁之間的即時事件通知。
今天介紹了 Signality 的四個功能,包含網路狀態、瀏覽器資料保存、元素觀察與跨分頁通訊,這些都是平常開發時可能遇到的需求。
Signality 將常見的 Browser API 與 Angular Signal 整合,讓我們能直接使用響應式資料,也減少自行管理事件監聽與生命週期的程式碼。不過,實際使用時仍要留意瀏覽器相容性,以及各個 API 本身的限制。
除了這四個功能,Signality 還提供 Clipboard、ResizeObserver、PageVisibility 等工具。有興趣可以到 Signality 官方文件 看看其他功能,再依照專案需求決定是否使用。
network() 回傳的連線狀態與其他網路資訊。storage() 回傳 WritableSignal 與 type 選項。post() 發送與 data() 取得訊息。