TypeScript dan Web Storage

TypeScript dan Web Storage

Artikel ini menerangkan TypeScript dan Web Storage.

Kami menerangkan TypeScript dan Web Storage, termasuk contoh praktikal.

YouTube Video

TypeScript dan Web Storage

Web Storage pelayar ialah storan kunci/nilai untuk rentetan. Ia ringan dengan API segerak, tetapi perlu diingat bahawa ia hanya boleh menyimpan rentetan sahaja, dan anda mesti menangani pengecualian seperti melebihi kuota storan. Apabila digabungkan dengan TypeScript, anda boleh menambah keselamatan jenis (type safety), penyirian/penyahsirian yang selamat, pengurusan kunci berpusat, serta luput (expiration) dan versi (versioning), menghasilkan reka bentuk yang sedia untuk produksi.

localStorage dan sessionStorage

localStorage ialah storan berterusan yang kekal selepas menutup pelayar, manakala sessionStorage ialah storan sesi per-tab/tetingkap yang dipadam apabila tab ditutup. Kedua-duanya menyimpan nilai sebagai pasangan kunci-nilai (rentetan).

1// Simple usage example: store and retrieve a string
2// This example uses localStorage to persist a username.
3localStorage.setItem('username', 'alice');
4console.log('saved username:', localStorage.getItem('username')); // "alice"
5
6// session storage example
7sessionStorage.setItem('session-id', 'xyz-123');
8console.log('session id:', sessionStorage.getItem('session-id')); // "xyz-123"
  • Kod ini ialah contoh untuk menyimpan dan mendapatkan semula rentetan. Oleh kerana Web Storage hanya boleh menyimpan rentetan, objek mesti ditukarkan kepada JSON untuk disimpan.

Contoh menggunakan penghuraian JSON

Untuk menyimpan dan memulihkan objek dalam Web Storage, gunakan JSON.stringify / JSON.parse.

1// Simple write & read with JSON
2// Store a small object as JSON and read it back.
3const user = { id: 1, name: "Alice" };
4localStorage.setItem("app:user", JSON.stringify(user));
5
6const raw = localStorage.getItem("app:user");
7const parsed = raw ? (JSON.parse(raw) as { id: number; name: string }) : null;
8
9console.log("user:", parsed); // user: { id: 1, name: "Alice" }
  • Kod ini ialah contoh berfungsi minimum. Dalam aplikasi sebenar, anda juga mesti mengambil kira pengecualian seperti kegagalan penghuraian (parse) dan kehabisan kuota storan.

Contoh pengendalian pengecualian untuk penghuraian JSON

Di sini, kami menyediakan pembungkus (wrapper) untuk menjadikan operasi baca dan tulis lebih selamat serta mengendalikan kegagalan JSON.parse dan pengecualian setItem.

 1// Safe JSON helpers
 2// These helpers handle parse errors and memory/storage issues separately.
 3export function safeParseJson<T>(raw: string | null): T | null {
 4  if (raw == null) return null;
 5
 6  try {
 7    return JSON.parse(raw) as T;
 8  } catch (error: any) {
 9    if (error instanceof SyntaxError) {
10      console.error("JSON parse error:", error.message);
11      return null;
12    }
13
14    console.error("Unexpected JSON error:", error);
15    return null;
16  }
17}
18
19// Safe setter for JSON values
20export function setJson(storage: Storage, key: string, value: unknown): void {
21  try {
22    const json = JSON.stringify(value);
23    storage.setItem(key, json);
24  } catch (error: any) {
25    if (error?.name === "QuotaExceededError") {
26      console.error("Storage quota exceeded while saving JSON:", error.message);
27    } else if (error instanceof TypeError) {
28      console.error("JSON serialization failed:", error.message);
29    } else {
30      console.error("Unexpected error while setting JSON:", error);
31    }
32  }
33}
34
35// Safe getter for JSON values
36export function getJson<T>(storage: Storage, key: string, fallback?: T): T | null {
37  const parsed = safeParseJson<T>(storage.getItem(key));
38  return parsed ?? (fallback ?? null);
39}
  • Anda boleh guna semula utiliti ini di seluruh aplikasi. Buat pembalut lanjut apabila anda mahu menambah try/catch.

Contoh penyimpanan dengan TTL (luput)

Oleh kerana Web Storage sendiri tiada TTL, uruskannya dengan menambah expiresAt pada nilai.

 1// TTL wrapper
 2// Store { value, expiresAt } and automatically expire items on read.
 3type WithTTL<T> = { value: T; expiresAt: number | null };
 4
 5export function setWithTTL<T>(storage: Storage, key: string, value: T, ttlMs?: number) {
 6  const payload: WithTTL<T> = { value, expiresAt: ttlMs ? Date.now() + ttlMs : null };
 7  setJson(storage, key, payload);
 8}
 9
10export function getWithTTL<T>(storage: Storage, key: string): T | null {
11  const payload = getJson<WithTTL<T>>(storage, key);
12  if (!payload) return null;
13  if (payload.expiresAt && Date.now() > payload.expiresAt) {
14    storage.removeItem(key);
15    return null;
16  }
17  return payload.value;
18}
  • TTL berkesan untuk cache dan auto-simpan (draf) dan boleh mengurangkan ketidakselarasan.

Contoh pengurusan kunci berpusat dan namespacing (mengelakkan perlanggaran)

Menyeragamkan kunci sebagai awalan + versi + nama mengurangkan perlanggaran dan memudahkan migrasi.

 1// Key factory with namespacing and versioning
 2// Create namespaced keys like "myapp:v1:theme" to avoid collisions.
 3const APP = "myapp";
 4const V = "v1";
 5
 6const ns = (k: string) => `${APP}:${V}:${k}`;
 7
 8const Keys = {
 9  theme: ns("theme"),
10  user: ns("user"),
11  cart: ns("cart"),
12  draft: ns("draft"),
13};
  • Dengan memasukkan ruang nama (namespace) pada kunci, lebih mudah untuk menukar versi atau melakukan pembersihan kemudian.

Contoh kuota melebihi (QuotaExceededError) dan strategi sandaran

Pertimbangkan bahawa QuotaExceededError mungkin berlaku ketika menjalankan setItem, dan reka strategi sandaran (fallback) apabila penyimpanan data gagal. Contohnya, apabila anda melebihi kapasiti storan, anda boleh memadam data lama atau beralih kepada sessionStorage ataupun cache dalam memori untuk mengekalkan kestabilan aplikasi secara keseluruhan.

 1// Quota-safe set with fallback to in-memory storage
 2// Return true if stored, false otherwise.
 3export function trySetJson(storage: Storage, key: string, value: unknown, fallback?: Map<string, string>): boolean {
 4  try {
 5    storage.setItem(key, JSON.stringify(value));
 6    return true;
 7  } catch (err) {
 8    console.warn("Failed to set item:", key, err);
 9    if (fallback) {
10      try {
11        fallback.set(key, JSON.stringify(value));
12        return true;
13      } catch {
14        return false;
15      }
16    }
17    return false;
18  }
19}
20
21// Example fallback usage
22const inMemoryFallback = new Map<string, string>();
23const ok = trySetJson(localStorage, Keys.cart, { items: [] }, inMemoryFallback);
24if (!ok) console.log("Saved to fallback map instead");
  • Bergantung pada destinasi sandaran (fallback), kekalan data mungkin tidak terjamin. Oleh itu, pilih destinasi storan yang sesuai mengikut kes penggunaan anda. Sebagai contoh, dalam mod pelayaran peribadi atau di bawah had storan, anda boleh mengekalkan fungsi dengan menggunakan memori atau sessionStorage buat sementara waktu.

Contoh penyegerakan rentas tab (acara storage) dan pemberitahuan dalam tab yang sama

Dengan menggunakan window.addEventListener('storage', …), anda boleh mengesan perubahan storan yang berlaku pada tab lain. Walau bagaimanapun, acara ini tidak dicetuskan dalam tab yang sama. Oleh itu, untuk pemberitahuan perubahan dalam tab yang sama, terbitkan acara anda sendiri menggunakan CustomEvent.

 1// Cross-tab and same-tab notification helpers
 2// storage event fires on other tabs; use CustomEvent for same-tab listeners.
 3const SAME_TAB_EVENT = "storage:changed";
 4
 5function notifyChanged(key: string) {
 6  window.dispatchEvent(new CustomEvent(SAME_TAB_EVENT, { detail: { key } }));
 7}
 8
 9function setJsonWithNotify(storage: Storage, key: string, value: unknown) {
10  setJson(storage, key, value);
11  notifyChanged(key);
12}
13
14// Listeners
15window.addEventListener("storage", (e) => {
16  if (e.key === Keys.theme) {
17    const theme = safeParseJson<string>(e.newValue);
18    console.log("Theme changed in another tab:", theme);
19  }
20});
21
22window.addEventListener(SAME_TAB_EVENT, (e: Event) => {
23  const detail = (e as CustomEvent).detail as { key: string };
24  console.log("Changed in this tab:", detail.key);
25});
  • Dengan kod ini, anda boleh menyegerakkan perubahan storan merentasi tab lain dan tab semasa.

Daftar selamat jenis (pengtaipan ketat bagi setiap kunci)

Tentukan peta kunci-ke-jenis dalam TypeScript untuk mencegah kesilapan semasa penyimpanan dan pengambilan semula.

 1// Typed registry that enforces types per key
 2// Registry maps keys to their allowed types.
 3type Registry = {
 4  [k in typeof Keys.theme]: "light" | "dark";
 5} & {
 6  [k in typeof Keys.user]: { id: number; name: string };
 7};
 8
 9type KeyOf<R> = Extract<keyof R, string>;
10
11export const TypedStore = {
12  get<K extends KeyOf<Registry>>(key: K, storage: Storage = localStorage): Registry[K] | null {
13    return getJson<Registry[K]>(storage, key);
14  },
15  set<K extends KeyOf<Registry>>(key: K, value: Registry[K], storage: Storage = localStorage): void {
16    setJson(storage, key, value);
17  },
18  remove<K extends KeyOf<Registry>>(key: K, storage: Storage = localStorage): void {
19    storage.removeItem(key);
20  },
21};
  • {^ i18n_speak 型をキーに関連付けることで、ランタイムでの誤使用をコンパイル時に検出でき、安全性を高めることができます。 ^}

Pensirian/pemulih (reviver) untuk jenis kompleks (Date/Map, dsb.)

Untuk memulihkan objek seperti Date dan Map dengan betul, manfaatkan replacer dan reviver bagi JSON.stringify.

 1// Serializing Dates with replacer and reviver
 2// Custom replacer marks Date objects for correct revival.
 3function replacer(_k: string, v: unknown) {
 4  if (v instanceof Date) return { __type: "Date", value: v.toISOString() };
 5  return v;
 6}
 7
 8function reviver(_k: string, v: any) {
 9  if (v && v.__type === "Date") return new Date(v.value);
10  return v;
11}
12
13function setJsonWithDates(storage: Storage, key: string, value: unknown) {
14  storage.setItem(key, JSON.stringify(value, replacer));
15}
16
17function getJsonWithDates<T>(storage: Storage, key: string): T | null {
18  const raw = storage.getItem(key);
19  if (!raw) return null;
20  try { return JSON.parse(raw, reviver) as T; } catch { return null; }
21}
  • Dengan kaedah ini, objek Date dipulihkan dengan betul sebagai Date. Begitu juga, Map dan Set juga boleh ditanda dan dipulihkan.

Contoh pengversian dan strategi migrasi

Jika anda mungkin mengubah format storan pada masa hadapan, sertakan versi dalam payload dan sediakan migrasi.

 1// Versioned payload pattern for migrations
 2// Keep { v, data } and migrate on read if necessary.
 3type VersionedPayload<T> = { v: number; data: T };
 4
 5function migrateUserV1toV2(u1: { id: number; name: string }) {
 6  return { id: u1.id, profile: { displayName: u1.name } };
 7}
 8
 9function readUserAnyVersion(): { id: number; profile: { displayName: string } } | null {
10  const raw = localStorage.getItem(Keys.user);
11  if (!raw) return null;
12  try {
13    const obj = JSON.parse(raw) as VersionedPayload<any>;
14    if (obj.v === 2) {
15      return obj.data;
16    } else if (obj.v === 1) {
17      const migrated = migrateUserV1toV2(obj.data);
18      localStorage.setItem(Keys.user, JSON.stringify({ v: 2, data: migrated }));
19      return migrated;
20    }
21    return null;
22  } catch (err) {
23    console.error("migration parse error", err);
24    return null;
25  }
26}
  • Dengan menyusun migrasi kecil-kecilan, anda boleh mengekalkan keserasian ke belakang.

Contoh pengendalian dalam SSR (renderan sisi pelayan)

Dalam persekitaran tanpa window, merujuk terus kepada localStorage akan menyebabkan ranap, jadi gunakan pengawal persekitaran.

1// Guard for SSR
2// Return a Storage-compatible object or null when not in browser.
3export const isBrowser = (): boolean => typeof window !== "undefined" && typeof window.localStorage !== "undefined";
4
5export const safeLocalStorage = (): Storage | null => (isBrowser() ? window.localStorage : null);
6
7// Usage
8const ls = safeLocalStorage();
9if (ls) setJson(ls, Keys.theme, "dark");
  • Untuk kod yang mesti menyokong SSR, sentiasa ingat untuk menyemak typeof window.

Petua praktikal

  • Hadkan kekerapan penulisan dengan debounce/throttle (untuk mengurangkan ledakan akibat tindakan UI).
  • Urus kunci dengan ruang nama + versi, cth., app:v1:....
  • Jangan simpan maklumat sensitif (token akses, dsb.) sebagai amalan. Jika terpaksa, pertimbangkan jangka hayat pendek beserta pengesahan pelayan atau WebCrypto.
  • Kapasiti bergantung pada pelayar (beberapa MB), jadi simpan data besar dalam IndexedDB.
  • Guna CustomEvent untuk pemberitahuan dalam tab yang sama dan storage untuk rentas tab.
  • Dalam SSR, sentiasa semak typeof window.

Kelas 'type-safe store' yang disatukan

Mari kita lihat contoh pelaksanaan bagi kelas generik yang mengintegrasikan elemen yang telah kita bincangkan setakat ini, termasuk ruang nama, keselamatan jenis, tempoh luput (TTL), dan pengendalian pengecualian. Dalam produk sebenar, pertimbangkan untuk menambah ujian, pembalakan (logging), pemadaman data lama berasaskan LRU, penyulitan, dan lain-lain.

 1// Comprehensive TypedStorage store integrating many patterns shown above.
 2// - type-safe registry per key
 3// - prefix (namespace + version)
 4// - trySet with fallback
 5// - same-tab notify
 6// - TTL optional getter/setter
 7type Jsonifiable = string | number | boolean | null | Jsonifiable[] | { [k: string]: Jsonifiable };
 8
 9interface StoreOptions {
10  storage?: Storage | null;   // default: auto-detected localStorage or null
11  prefix?: string;            // e.g., "myapp:v1"
12  sameTabEvent?: string | null;
13  fallback?: Map<string, string>; // in-memory fallback
14}
  • Kod ini menunjukkan konfigurasi dan definisi jenis untuk membina storan kunci-nilai yang selamat jenis (type-safe) dan kaya ciri.
 1export class TypedStorage<Reg extends Record<string, Jsonifiable | object>> {
 2  private storage: Storage | null;
 3  private prefix: string;
 4  private sameTabEvent: string | null;
 5  private fallback?: Map<string, string>;
 6
 7  constructor(private registry: Reg, opts: StoreOptions = {}) {
 8    this.storage = opts.storage ?? (typeof window !== "undefined" ? window.localStorage : null);
 9    this.prefix = (opts.prefix ?? "app:v1") + ":";
10    this.sameTabEvent = opts.sameTabEvent ?? "storage:changed";
11    this.fallback = opts.fallback;
12  }
13
14  private k(key: keyof Reg & string) { return this.prefix + key; }
  • Kod ini ialah teras kepada kelas TypedStorage yang menyediakan storan kunci-nilai yang selamat jenis. Ia mengurus kunci yang dibenarkan dan jenisnya berdasarkan registry, serta menjana kunci storan dengan awalan. Selain itu, ia menggunakan localStorage dan sandaran dalam memori, serta membolehkan anda menetapkan nama acara untuk pemberitahuan perubahan dalam tab yang sama.
 1  // Basic get with optional TTL-aware retrieval
 2  get<K extends keyof Reg & string>(key: K): Reg[K] | null {
 3    const fullKey = this.k(key);
 4    try {
 5      const raw = this.storage ? this.storage.getItem(fullKey) : this.fallback?.get(fullKey) ?? null;
 6      if (!raw) return null;
 7      // Check if value is TTL-wrapped
 8      const maybe = safeParseJson<{ value: Reg[K]; expiresAt?: number }>(raw);
 9      if (maybe && typeof maybe.expiresAt === "number") {
10        if (maybe.expiresAt && Date.now() > maybe.expiresAt) {
11          this.remove(key);
12          return null;
13        }
14        return maybe.value;
15      }
16      return safeParseJson<Reg[K]>(raw);
17    } catch (err) {
18      console.error("TypedStorage.get error", err);
19      return null;
20    }
21  }
  • Kaedah get mendapatkan nilai bagi kunci yang dinyatakan dengan cara yang selamat jenis dan secara pilihan boleh mengendalikan nilai yang mempunyai TTL (tempoh luput).
 1  // Basic set; returns success boolean
 2  set<K extends keyof Reg & string>(key: K, value: Reg[K]): boolean {
 3    const fullKey = this.k(key);
 4    const payload = JSON.stringify(value);
 5    try {
 6      if (this.storage) this.storage.setItem(fullKey, payload);
 7      else this.fallback?.set(fullKey, payload);
 8      if (this.sameTabEvent) window.dispatchEvent(new CustomEvent(this.sameTabEvent, { detail: { key: fullKey } }));
 9      return true;
10    } catch (err) {
11      console.warn("TypedStorage.set primary failed, trying fallback", err);
12      try {
13        if (this.fallback) {
14          this.fallback.set(fullKey, payload);
15          return true;
16        }
17        return false;
18      } catch (e) {
19        console.error("TypedStorage.set fallback failed", e);
20        return false;
21      }
22    }
23  }
  • Kaedah set menyimpan nilai di bawah kunci yang dinyatakan dan mengembalikan nilai boolean yang menunjukkan sama ada ia berjaya.
1  // Set with TTL convenience
2  setWithTTL<K extends keyof Reg & string>(key: K, value: Reg[K], ttlMs?: number): boolean {
3    const payload = { value, expiresAt: ttlMs ? Date.now() + ttlMs : null };
4    return this.set(key, payload as unknown as Reg[K]);
5  }
  • Kaedah setWithTTL menyimpan nilai dengan TTL (tempoh luput).
1  remove<K extends keyof Reg & string>(key: K) {
2    const fullKey = this.k(key);
3    try {
4      if (this.storage) this.storage.removeItem(fullKey);
5      this.fallback?.delete(fullKey);
6    } catch (err) { console.warn("TypedStorage.remove error", err); }
7  }
  • Kaedah remove memadam nilai bagi kunci yang dinyatakan daripada kedua-dua storan dan storan sandaran.
1  clear() {
2    try {
3      if (this.storage) this.storage.clear();
4      this.fallback?.clear();
5    } catch (err) { console.warn("TypedStorage.clear error", err); }
6  }
7}
  • Kaedah clear memadam semua data yang disimpan dalam kedua-dua storan dan storan sandaran.
 1// Usage example
 2type MyReg = {
 3  theme: "light" | "dark";
 4  user: { id: number; name: string };
 5  draft: string;
 6};
 7
 8const memFallback = new Map<string, string>();
 9const store = new TypedStorage<MyReg>({} as MyReg, {
10  prefix: "myapp:v1",
11  sameTabEvent: "storage:changed",
12  fallback: memFallback
13});
14store.set("theme", "dark");
15console.log(store.get("theme")); // "dark"
16store.setWithTTL("draft", "in-progress...", 1000 * 60 * 60); // keep 1 hour
  • Kod ini ialah contoh yang menggunakan TypedStorage, yang menyimpan dan mendapatkan semula nilai seperti "theme" dan "draft" dalam storan kunci-nilai yang selamat jenis, serta menyokong TTL dan storan sandaran. Ia mengkonfigurasikan pemberitahuan dalam tab yang sama dan sandaran dalam memori untuk melaksanakan operasi storan dengan selamat.

  • Kelas TypedStorage ialah titik permulaan yang praktikal. Laksanakan strategi LRU, penyulitan, pemampatan, dan sandaran ke IndexedDB mengikut keperluan.

Ringkasan

Apabila menggunakan Web Storage dengan TypeScript, sentiasa ingat empat perkara—keselamatan jenis, daya tahan terhadap pengecualian, keselamatan, dan penyegerakan (berbilang tab)—untuk reka bentuk yang teguh. Pembalut dan utiliti yang kita lihat setakat ini ialah contoh-contohnya. Anda juga boleh berpindah ke storan pelayar lain seperti IndexedDB mengikut keperluan.

Anda boleh mengikuti artikel di atas menggunakan Visual Studio Code di saluran YouTube kami. Sila lihat juga saluran YouTube kami.

YouTube Video