TypeScript এবং Web Storage

এই নিবন্ধে TypeScript এবং Web Storage ব্যাখ্যা করা হয়েছে।

আমরা বাস্তব উদাহরণসহ TypeScript এবং Web Storage ব্যাখ্যা করি।

YouTube Video

TypeScript এবং Web Storage

ব্রাউজারের Web Storage হলো স্ট্রিং-এর জন্য কী/ভ্যালু স্টোরেজ। এটি একটি সিঙ্ক্রোনাস API সহ হালকা-ওজনের, তবে খেয়াল রাখুন এটি কেবল স্ট্রিং সংরক্ষণ করতে পারে এবং স্টোরেজ কোটার বেশি হয়ে যাওয়া মতো এক্সসেপশন আপনাকেই সামলাতে হবে। TypeScript-এর সঙ্গে মিলিয়ে ব্যবহার করলে আপনি টাইপ সেফটি, নিরাপদ সিরিয়ালাইজেশন/ডিসিরিয়ালাইজেশন, কেন্দ্রীভূত কী ম্যানেজমেন্ট, এবং মেয়াদ (expiration) এবং ভার্সনিং যোগ করতে পারেন, ফলে প্রোডাকশন-রেডি একটি ডিজাইন পাওয়া যায়।

localStorage এবং sessionStorage

localStorage হলো পার্সিস্টেন্ট স্টোরেজ, যা ব্রাউজার বন্ধ করার পরও থাকে; অপরদিকে sessionStorage হলো প্রতি ট্যাব/উইন্ডোর সেশন স্টোরেজ, যা ট্যাব বন্ধ করলে মুছে যায়। দুটিই মানগুলোকে কী-ভ্যালু জোড়া (স্ট্রিং) হিসেবে সংরক্ষণ করে।

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"
  • এই কোডটি স্ট্রিং সংরক্ষণ ও পুনরুদ্ধারের একটি নমুনা। Web Storage কেবল স্ট্রিং সংরক্ষণ করতে পারে, তাই অবজেক্ট সংরক্ষণ করতে হলে JSON-এ রূপান্তর করতে হবে।

JSON পার্সিং ব্যবহার করে উদাহরণ

Web Storage-এ অবজেক্ট সংরক্ষণ ও পুনরুদ্ধার করতে 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" }
  • এই কোডটি একটি ন্যূনতম কার্যকর উদাহরণ। বাস্তব অ্যাপ্লিকেশনে পার্স ব্যর্থতা বা স্টোরেজ কোটার ঘাটতির মতো এক্সসেপশনগুলিও বিবেচনায় রাখতে হবে।

JSON পার্সিং-এর জন্য ব্যতিক্রম পরিচালনার উদাহরণ

এখানে আমরা রিড/রাইট নিরাপদ করতে এবং JSON.parse ব্যর্থতা ও 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}
  • এই ইউটিলিটিটি অ্যাপজুড়ে পুনর্ব্যবহার করতে পারেন। try/catch যোগ করতে চাইলে আরো র‍্যাপ করুন।

TTL (মেয়াদ) সহ সংরক্ষণের উদাহরণ

Web Storage-এ নিজস্ব TTL নেই, তাই মানে expiresAt যোগ করে এটি পরিচালনা করুন।

 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 ক্যাশে ও অটোসেভ (ড্রাফ্ট) এর জন্য কার্যকর এবং অসংগতিগুলো কমাতে পারে।

কেন্দ্রীভূত কী ব্যবস্থাপনা ও নেমস্পেসিং-এর উদাহরণ (সংঘর্ষ পরিহার)

কীগুলোকে prefix + version + name মানক করলে সংঘর্ষ কমে এবং মাইগ্রেশন সহজ হয়।

 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};
  • কীতে নেমস্পেস যুক্ত করলে পরে ভার্সন বদলানো বা ক্লিনআপ করা সহজ হয়।

কোটা অতিক্রম (QuotaExceededError) এবং ফলব্যাক কৌশলের উদাহরণ

setItem চালানোর সময় QuotaExceededError ঘটতে পারে—এটি মাথায় রেখে সেভ ব্যর্থ হলে কীভাবে ফলব্যাক হবে তার কৌশল ডিজাইন করুন। উদাহরণস্বরূপ, স্টোরেজ ধারণক্ষমতা ছাড়িয়ে গেলে পুরোনো ডেটা মুছে ফেলতে পারেন, অথবা অ্যাপের সামগ্রিক স্থিতিশীলতা বজায় রাখতে sessionStorage বা ইন-মেমরি ক্যাশে-এ ফলব্যাক করতে পারেন।

 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");
  • ফলব্যাকের গন্তব্যের ওপর নির্ভর করে ডেটা স্থায়িত্ব নিশ্চিত নাও হতে পারে। তাই আপনার ব্যবহারের ধরন অনুযায়ী উপযুক্ত স্টোরেজ গন্তব্য নির্বাচন করুন। যেমন, প্রাইভেট ব্রাউজিং মোডে বা স্টোরেজ সীমাবদ্ধতায় সাময়িকভাবে মেমরি বা sessionStorage ব্যবহার করে কার্যকারিতা বজায় রাখতে পারেন।

ক্রস-ট্যাব সিঙ্ক (storage ইভেন্ট) এবং একই ট্যাবের নোটিফিকেশনের উদাহরণ

window.addEventListener('storage', …) ব্যবহার করে আপনি অন্যান্য ট্যাব-এ ঘটে যাওয়া স্টোরেজ পরিবর্তন শনাক্ত করতে পারেন। তবে এই ইভেন্টটি একই ট্যাবের ভেতরে ট্রিগার হয় না। সে কারণে একই ট্যাবের ভেতরে পরিবর্তনের নোটিফিকেশন দিতে 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});
  • এই কোডের মাধ্যমে আপনি অন্যান্য ট্যাব এবং বর্তমান ট্যাব উভয়ের মধ্যে স্টোরেজ পরিবর্তন সমন্বয় করতে পারবেন।

টাইপ-সেফ রেজিস্ট্রি (প্রতি কী-র জন্য কড়া টাইপিং)

সেভ ও রিট্রিভের সময় ভুল এড়াতে TypeScript-এ কী-টু-টাইপ ম্যাপ সংজ্ঞায়িত করুন।

 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 型をキーに関連付けることで、ランタイムでの誤使用をコンパイル時に検出でき、安全性を高めることができます。 ^}

জটিল টাইপের জন্য সিরিয়ালাইজেশন/রিভাইভার (Date/Map ইত্যাদি)

DateMap-এর মতো অবজেক্ট সঠিকভাবে পুনরুদ্ধার করতে JSON.stringify-এর replacerreviver ব্যবহার করুন।

 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}
  • এই পদ্ধতিতে Date অবজেক্টগুলো সঠিকভাবে Date হিসেবেই পুনরুদ্ধার হয়। একইভাবে, Map এবং Set-ও চিহ্নিত করে পুনরুদ্ধার করা যায়।

ভার্সনিং ও মাইগ্রেশন কৌশলের উদাহরণ

ভবিষ্যতে স্টোরেজ ফরম্যাট বদলানোর সম্ভাবনা থাকলে, পে-লোডে একটি ভার্সন যুক্ত করুন এবং মাইগ্রেশনের ব্যবস্থা রাখুন।

 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}
  • ছোট ছোট মাইগ্রেশন স্তূপ করে আপনি পূর্ববর্তী সংস্করণের সাথে সামঞ্জস্যতা বজায় রাখতে পারেন।

SSR (server-side rendering) এ হ্যান্ডলিং-এর উদাহরণ

window-বিহীন পরিবেশে সরাসরি localStorage রেফারেন্স করলে ক্র্যাশ করবে, তাই environment guard ব্যবহার করুন।

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");
  • SSR সমর্থন জরুরি এমন কোডে, সবসময় typeof window পরীক্ষা করতে মনে রাখুন।

ব্যবহারিক টিপস

  • debounce/throttle দিয়ে লেখার ঘনত্ব সীমিত করুন (UI ক্রিয়াকলাপ থেকে আসা হঠাৎ স্পাইক কমাতে)।
  • namespace + version দিয়ে কী ম্যানেজ করুন, যেমন app:v1:...
  • নীতিগতভাবে সংবেদনশীল তথ্য (access token ইত্যাদি) সংরক্ষণ করবেন না। অত্যাবশ্যক হলে, স্বল্প মেয়াদ এবং সার্ভার ভ্যালিডেশন বা WebCrypto বিবেচনা করুন।
  • ধারণক্ষমতা ব্রাউজারভেদে (কয়েক MB) নির্ভর করে, তাই বড় ডেটা IndexedDB-তে সংরক্ষণ করুন।
  • একই ট্যাবের নোটিফিকেশনের জন্য CustomEvent এবং ক্রস-ট্যাবের জন্য storage ব্যবহার করুন।
  • SSR-এ সর্বদা typeof window পরীক্ষা করুন।

সমন্বিত 'type-safe store' ক্লাস

চলুন আমরা একটি জেনেরিক ক্লাসের উদাহরণ ইমপ্লিমেন্টেশন দেখি, যা এখন পর্যন্ত আলোচিত উপাদানগুলো—namespace, type safety, মেয়াদকাল (TTL), এবং exception handling—সমন্বিত করে। বাস্তব প্রোডাক্টে টেস্ট, লগিং, পুরোনো ডেটার LRU-ভিত্তিক মুছেপেলা, এনক্রিপশন ইত্যাদি যোগ করার কথা ভাবুন।

 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}
  • এই কোডটি একটি টাইপ-সেফ, ফিচার-সমৃদ্ধ কী-ভ্যালু স্টোরেজ গড়তে প্রয়োজনীয় কনফিগারেশন ও টাইপ ডেফিনিশন দেখায়।
 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; }
  • এই কোডটি TypedStorage ক্লাসের মূল অংশ, যা type-safe key-value স্টোরেজ প্রদান করে। এটি registry-এর ভিত্তিতে অনুমোদিত key ও তাদের type পরিচালনা করে, এবং প্রিফিক্সযুক্ত স্টোরেজ key তৈরি করে। অতিরিক্তভাবে, এটি localStorage ও ইন-মেমরি ফলব্যাক ব্যবহার করে, এবং একই ট্যাবের পরিবর্তন নোটিফিকেশনের জন্য একটি ইভেন্ট নাম সেট করার সুযোগ দেয়।
 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  }
  • get মেথডটি নির্দিষ্ট key-এর মান type-safe উপায়ে পুনরুদ্ধার করে এবং ঐচ্ছিকভাবে TTL (মেয়াদকাল) সহ মান হ্যান্ডেল করতে পারে।
 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  }
  • set মেথডটি নির্দিষ্ট key-এর অধীনে একটি মান সংরক্ষণ করে এবং এটি সফল হয়েছে কি না তা বোঝাতে একটি boolean রিটার্ন করে।
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  }
  • setWithTTL মেথডটি TTL (মেয়াদকাল) সহ একটি মান সংরক্ষণ করে।
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  }
  • remove মেথডটি নির্দিষ্ট key-এর মানকে স্টোরেজ এবং fallback—উভয় থেকেই মুছে দেয়।
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}
  • clear মেথডটি স্টোরেজ এবং fallback—উভয় জায়গায় সংরক্ষিত সব ডেটা মুছে দেয়।
 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
  • এই কোডটি TypedStorage ব্যবহার করার একটি উদাহরণ, যা type-safe key-value স্টোরে "theme" এবং "draft"-এর মতো মান সংরক্ষণ ও পুনরুদ্ধার করে, এবং TTL ও fallback-ও সমর্থন করে। এটি স্টোরেজ অপারেশন নিরাপদে সম্পাদনের জন্য একই ট্যাবের নোটিফিকেশন এবং ইন-মেমরি ফলব্যাক কনফিগার করে।

  • TypedStorage ক্লাসটি একটি ব্যবহারিক সূচনাবিন্দু। প্রয়োজনে LRU কৌশল, এনক্রিপশন, কমপ্রেশন, এবং IndexedDB-তে ফলব্যাক বাস্তবায়ন করুন।

সারসংক্ষেপ

TypeScript সহ Web Storage ব্যবহার করার সময় শক্তিশালী ডিজাইনের জন্য সবসময় চারটি বিষয় মনে রাখুন—টাইপ সেফটি, ব্যতিক্রম সহনশীলতা, নিরাপত্তা, এবং সিঙ্ক্রোনাইজেশন (একাধিক ট্যাব)। এ পর্যন্ত দেখা র‍্যাপার ও ইউটিলিটিগুলোই তার উদাহরণ। প্রয়োজনে IndexedDB-এর মতো অন্যান্য ব্রাউজার স্টোরেজেও মাইগ্রেট করতে পারেন।

আপনি আমাদের ইউটিউব চ্যানেলে ভিজ্যুয়াল স্টুডিও কোড ব্যবহার করে উপরের নিবন্ধটি অনুসরণ করতে পারেন। দয়া করে ইউটিউব চ্যানেলটিও দেখুন।

YouTube Video