iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
自我挑戰組

Angular 開發二三事系列 第 10 篇

Day10: 寫一個瀏覽器儲存空間封裝工具

  • 分享至 

  • xImage
  •  

有了 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'

StorageService

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;
    }
  }
}

實際應用

  1. 注入 StorageService。
  2. 宣告 product,有 session 就塞資料,沒有就空陣列。
  3. 在生命週期 ngOnInit 檢查是否有資料?沒有便呼叫 API。
  4. 在 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 {......}
  }
}

如此就大功告成啦!大家也來試試看吧。


上一篇
Day9: 全域 Loading
系列文
Angular 開發二三事 共 10 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言