有了 AI 輔助開發後,對寫鐵人賽好迷惘喔。
是 AI 懂,不是我懂。
我很喜歡「封包」的作法,就好像收納時,會在大抽屜放許多小格子,讓文具分門別類放好,有需要再拿出來,而不是散亂一團。
因此這次就將瀏覽器儲存空間的:localStorage / sessionStorage / Cookie,裝起來吧!
先來了解不懂的地方:
SameSite=Lax:SameSite 是用來控制瀏覽器在跨站(Cross-Site)請求時是否發送 Cookie 的安全性屬性,主要用來防範跨站請求偽造(CSRF)攻擊。預設為 Lax。PLATFORM_ID:Angular 的 Inject,用來辨識當前執行的程式碼在哪一端,例如:瀏覽器端 browser、伺服器端 server。isPlatformBrowser:PLATFORM_ID的搭檔,回傳 true 表示程式碼正在瀏覽器執行。DOCUMENT:Angular 封裝的「全域 DOM document」物件。在實務上直接禁止存取全域 document,因此透過此 DOCUMENT 能維持程式碼的可測試性,也能避免 SSR 執行時出現 document is not defined ERROR。encodeURIComponent: 是 JavaScript 原生的 URL 編碼函式,將字串轉換為有效的 UTF-8 URL 格式。可見以下例子:encodeURIComponent('user_name'); // 'user_name'
encodeURIComponent('用戶 1'); // '%E7%94%A8%E6%88%B6%201'
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
export const STORAGE_KEYS = {
ACCESS_TOKEN: 'access_token',
REFRESH_TOKEN: 'refresh_token',
} as const;
export type StorageType = 'SESSION' | 'LOCAL' | 'COOKIE';
@Injectable({
providedIn: 'root',
})
export class StorageService {
private readonly platformId = inject(PLATFORM_ID);
private readonly document = inject(DOCUMENT);
private readonly isBrowser = isPlatformBrowser(this.platformId);
store<T>(key: string, value: T, type: StorageType = 'SESSION'): void {
if (!this.isBrowser) return;
const strValue = JSON.stringify(value);
switch (type) {
case 'SESSION':
sessionStorage.setItem(key, strValue);
break;
case 'LOCAL':
localStorage.setItem(key, strValue);
break;
case 'COOKIE': {
// 移除 expires 設定,使 Cookie 預設在關閉瀏覽器時自動刪除
this.document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(strValue)}; path=/; SameSite=Lax;`;
break;
}
}
}
get<T>(key: string, type: StorageType = 'SESSION'): T | null {
if (!this.isBrowser || !key) return null;
let value: string | null = null;
switch (type) {
case 'SESSION':
value = sessionStorage.getItem(key);
break;
case 'LOCAL':
value = localStorage.getItem(key);
break;
case 'COOKIE': {
const match = this.document.cookie.match(
new RegExp(
`(?:^|; )${encodeURIComponent(key)}=([^;]*)`,
),
);
value = match ? decodeURIComponent(match[1]) : null;
break;
}
}
if (!value) return null;
try {
return JSON.parse(value) as T;
} catch {
return value as unknown as T;
}
}
remove(key: string, type: StorageType = 'SESSION'): void {
if (!this.isBrowser) return;
switch (type) {
case 'SESSION':
sessionStorage.removeItem(key);
break;
case 'LOCAL':
localStorage.removeItem(key);
break;
case 'COOKIE':
this.document.cookie = `${encodeURIComponent(key)}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
break;
}
}
}
StorageService。product,有 session 就塞資料,沒有就空陣列。ngOnInit 檢查是否有資料?沒有便呼叫 API。getProduct() 成功取得資料時,要記得塞進 session → this.storageService.store('PRDUCT', this.proposals(), 'SESSION');。import { Component, OnInit } from '@angular/core';
import { StorageService, ApiService } from '@/app/core';
@Component({
selector: 'app-home-view',
imports: [],
templateUrl: './home.html',
styleUrl: './home.scss',
})
export default class HomeViewComponent implements OnInit {
private readonly storageService = inject(StorageService);
private readonly apiService = inject(ApiService);
readonly product = signal<ProductItem[]>(
this.storageService.get<ProductItem[]>('PRODUCT', 'SESSION') ?? [],
);
// Angular 生命週期
ngOnInit(): void {
if (this.product().length === 0) {
void this.getProduct();
}
}
async getProduct(): Promise<void> {
try {
const res = await firstValueFrom(this.apiService.getProduct());
this.product.update((current) => [...current, ...res.data])
this.storageService.store('PRDUCT', this.proposals(), 'SESSION');
} catch {......}
}
}
如此就大功告成啦!大家也來試試看吧。