0

[Vibe Coding Thực Chiến #09] "Đổi Máu" Cho Frontend: Thay Thế LocalStorage Bằng Dữ Liệu Thật Qua API & MCP!

Tác giả: Lee
Series: Vibe Coding Thực Chiến Cùng EzyPlatform (#09)
Hạ tầng thực nghiệm: VPS Ubuntu 22.04 LTS (157.66.47.xxx) | Domain Admin: https://1576647xxx-admin.interdata.io.vn
Bộ công cụ: VS Code + Extension youngmonkeys.ezyarticle + Claude Code CLI / Codex + GraphQL API


Ở bài viết số #08, chúng ta đã trang bị cho AI "vũ khí tối thượng" Model Context Protocol (MCP). Thay vì để AI phỏng đoán mù quáng, Claude Code đã tự kết nối vào VPS qua giao thức MCP, tự đọc cấu trúc bảng ez_products và ez_orders từ database EzyPlatform chỉ trong 320ms với độ chính xác 100%.

Giờ đây, chúng ta đang đứng trước bước ngoặt lớn nhất của toàn bộ series:

"Làm thế nào để vứt bỏ dữ liệu giả lập (Mock State) trong LocalStorage từ Bài #07, và đấu nối 100% giỏ hàng sang API GraphQL thật của máy chủ VPS mà không làm vỡ một pixel giao diện nào?"

Nhiều anh em dev thường rất ngán giai đoạn "đổi máu" (Refactoring) này. Chỉ cần sơ sẩy một chút là giao diện crash trắng xóa, badge giỏ hàng đứng hình, hoặc đơn hàng gửi lên máy chủ bị lỗi 500 do mismatch kiểu dữ liệu.

Nhưng vì chúng ta đã áp dụng chuẩn mực tư duy "Mock State First" ở Bài #07 và đã khóa chặt schema bằng MCP ở Bài #08, quá trình "thay ruột" này sẽ diễn ra cực kỳ êm ái chỉ trong 20 phút. Hãy cùng mình bắt tay vào thực hiện nhé!


1. Triết lý "Đổi Máu Không Vỡ Giao Diện" (Data Source Swap via Adapter)

Tại sao giao diện của chúng ta lại không bị xáo trộn khi chuyển từ LocalStorage sang API mạng?

Bí quyết nằm ở mô hình Adapter Pattern siêu nhẹ: Chúng ta tách bạch hoàn toàn giữa Tầng Hiển Thị (UI View) và Tầng Dữ Liệu (Store / ApiService).

Kiến Trúc Đổi Máu Không Vỡ Giao Diện Cho Frontend Nguyên lý Adapter: Tầng UI View (Drawer, Badge, Modal) hoàn toàn giữ nguyên, chỉ tráo nguồn dữ liệu từ LocalStorage sang GraphQL VPS.

3 ưu điểm vượt trội của cách tiếp cận này:

  1. Không phải viết lại một dòng HTML hay CSS nào: Các component như Header Badge, Cart Drawer hay Checkout Modal chỉ quan tâm đến sự kiện ezy:cart-updated. Khi dữ liệu bên dưới đổi từ LocalStorage sang API mạng, giao diện vẫn tự động render mượt mà.
  2. Dễ dàng Rollback khi có sự cố: Nếu máy chủ VPS gặp trục trặc, hệ thống có thể lập tức fallback về LocalStorage tạm thời mà khách hàng không hề bị mất giỏ hàng đang chọn.
  3. Context của AI cực kỳ tập trung: Khi yêu cầu AI refactor, bạn chỉ cần đưa cho nó file foot.html chứa logic ApiService. AI không cần đọc lại toàn bộ CSS hay HTML cấu trúc của trang.

2. Chuẩn mực Production UX: Optimistic UI & Cơ chế Rollback

Khi làm việc với LocalStorage, tốc độ đọc ghi là 0ms. Nhưng khi chuyển sang gọi API mạng lên VPS 157.66.47.xxx, mỗi request sẽ mất từ 100ms - 300ms tùy vào đường truyền.

Nếu bắt người dùng phải nhìn một cái loading spinner xoay tròn mỗi khi bấm nút "+ Thêm vào giỏ", trải nghiệm sẽ cực kỳ đơ giật. Do đó, chúng ta áp dụng cơ chế Optimistic UI (Cập nhật lạc quan):

Cơ chế Optimistic UI và Rollback khi gọi API máy chủ Luồng 4 bước: Tương tác ➔ Cập nhật UI ngay tức thì (0ms) ➔ Gửi API ngầm lên VPS ➔ Xác nhận mã đơn thật hoặc Rollback an toàn nếu mất mạng.

Nguyên lý hoạt động:

  • Khách bấm nút: UI cập nhật số lượng bánh và tổng tiền ngay lập tức (0ms). Trải nghiệm mượt như ứng dụng bản địa (Native App).
  • Chạy ngầm (Background Fetch): Trình duyệt gửi request GraphQL createOrder lên VPS.
  • Nếu thành công: Cập nhật mã đơn chính thức do máy chủ sinh ra (#ORD-2026-LIVE-XXXX).
  • Nếu mất mạng: Tự động khôi phục (Rollback) state về trước đó và hiển thị Toast thông báo lịch sự cho người dùng.

3. Thực chiến 4 bước "thay máu" cho Tiệm Bánh Le Petit Bakery

Bây giờ, hãy mở VS Code và mở file pages/bakery-shop/foot.html để cùng tiến hành nhé!

Bước 1: Nạp danh sách bánh ngọt động qua GraphQL Query getBakeryProducts

Thay vì khai báo mảng sản phẩm cứng trong mã nguồn HTML, chúng ta tạo hàm fetch động từ database EzyPlatform:

// pages/bakery-shop/foot.html
const ApiService = {
  endpoint: 'https://1576647xxx-admin.interdata.io.vn/graphql',

  async fetchProducts() {
    const query = `
      query GetProducts {
        products(category: "french-pastry") {
          id
          title
          price
          currency
          slug
        }
      }
    `;

    try {
      const res = await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query })
      });
      const json = await res.json();
      return json.data.products;
    } catch (err) {
      console.warn("Lỗi nạp sản phẩm từ VPS, dùng fallback cache:", err);
      return [];
    }
  }
};

Khi trang vừa mở, lưới sản phẩm tự động hiển thị dữ liệu thật từ bảng ez_products trên VPS với thời gian phản hồi chỉ 142ms:

Giao diện danh sách sản phẩm động từ database VPS Danh sách bánh ngọt hiển thị trực tiếp từ database VPS 157.66.47.xxx kèm hiệu ứng Skeleton Loading khi nạp trang.


Bước 2: Nâng cấp CartStore — Gửi Mutation createOrder lên máy chủ VPS

Khi khách hoàn tất thông tin và bấm "Tiến Hành Đặt Hàng", chúng ta gửi toàn bộ giỏ hàng lên VPS qua GraphQL Mutation:

// Gửi đơn hàng lên máy chủ EzyPlatform
async submitOrder(customerInfo, cartItems, totalAmount) {
  const mutation = `
    mutation CreateBakeryOrder($input: CreateOrderInput!) {
      createOrder(input: $input) {
        orderCode
        status
        createdAt
      }
    }
  `;

  const variables = {
    input: {
      customerName: customerInfo.name,
      customerPhone: customerInfo.phone,
      customerAddress: customerInfo.address,
      items: cartItems.map(item => ({
        productId: item.id,
        quantity: item.qty,
        price: item.price
      })),
      totalAmount: totalAmount
    }
  };

  const res = await fetch(this.endpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query: mutation, variables })
  });

  const json = await res.json();
  if (json.errors) throw new Error(json.errors[0].message);
  return json.data.createOrder;
}

Bước 3: Soi Chrome DevTools Network Tab kiểm tra kết nối VPS

Hãy bấm F12, chuyển sang tab Network và lọc theo từ khóa graphql:

DevTools Network Tab hiển thị Mutation gửi lên VPS DevTools Network ghi nhận request POST /graphql trả về mã 200 OK với mã đơn hàng máy chủ sinh ra #ORD-2026-LIVE-9041.

Máy chủ VPS 157.66.47.xxx phản hồi trong tích tắc (185ms), trả về mã đơn hàng hợp lệ #ORD-2026-LIVE-9041 cùng trạng thái CONFIRMED!


Bước 4: Kiểm tra đơn hàng nổ trực tiếp trên EzyPlatform Admin Portal

Bây giờ, hãy mở trình duyệt truy cập vào trang quản trị EzyPlatform (https://1576647xxx-admin.interdata.io.vn/orders):

Admin Dashboard EzyPlatform ghi nhận đơn hàng mới Đơn hàng #ORD-2026-LIVE-9041 từ khách hàng Nguyen Van A hiển thị sắc nét trong bảng quản trị EzyPlatform.

Đơn hàng vừa bấm từ trang frontend đã lập tức xuất hiện ngay trên hệ thống quản trị của chủ shop! Dữ liệu đã được ghi bền vững vào cơ sở dữ liệu VPS. Chúng ta đã chính thức hoàn thành việc chuyển đổi sang một sản phẩm production thực thụ! 🎉


4. 3 bẫy lỗi kinh điển khi "đổi máu" sang API & Cách né 😡

Trong quá trình đưa ứng dụng từ LocalStorage lên API thật, đây là 3 lỗi khiến nhiều anh em "toát mồ hôi hột":

Bẫy lỗi #1: Quên cấu hình CORS Preflight khiến request bị chặn đứng 😣

  • Hiện tượng: Mọi request POST từ trình duyệt đều báo đỏ lòm: Access to fetch at ... has been blocked by CORS policy: Response to preflight request doesn't pass access control check.
  • Nguyên nhân: Tên miền client gọi API khác origin với admin portal mà chưa khai báo trong EzyPlatform.
  • Cách né (Recommend 👍): Vào trang Settings > Regular trên EzyPlatform Admin, thêm domain hoặc subdomain vào mục Allowed CORS Origins và bật cờ Allow Credentials.

Bẫy lỗi #2: Không bọc Try/Catch khiến trang đơ cứng khi rớt mạng 😡

  • Hiện tượng: Khách hàng đang đặt bánh ở nơi sóng yếu (hoặc rớt Wi-Fi), nút "Đặt hàng" quay vô tận, không báo lỗi, bấm lại không được.
  • Nguyên nhân: AI viết hàm fetch nhưng không xử lý catch cho TypeError: Failed to fetch.
  • Cách né (Bí kíp 💡): Luôn bắt lỗi mạng và kích hoạt cơ chế Rollback an toàn kèm Toast thông báo:

Thông báo Toast và cơ chế Rollback khi rớt mạng Giao diện xử lý lỗi lịch sự: Thông báo mất mạng nhưng giữ nguyên dữ liệu giỏ hàng an toàn cho người dùng.

Bẫy lỗi #3: Mismatch kiểu dữ liệu số giữa Frontend và GraphQL Schema ⚠️

  • Hiện tượng: Gửi payload lên máy chủ nhận lại lỗi: Variable "$input" got invalid value "95000" at "price"; Float cannot represent non-numeric value.
  • Nguyên nhân: Giá trị lấy từ input.value trong DOM là chuỗi ký tự (string), chưa được ép kiểu sang số thực (parseFloat hoặc Number).
  • Cách né (Lưu ý ⚠️): Nhờ việc đã chạy Introspection ở Bài #08, ta biết trường price bắt buộc là Float. Trước khi gửi lên API, luôn ép kiểu tường minh: price: parseFloat(item.price).

5. Cheatsheet & Code Mẫu ApiStore Module Đầy Đủ (Copy & Chạy Ngay) 📋

Dưới đây là toàn bộ mã nguồn module ApiCartStore tối ưu hóa, tích hợp đầy đủ Optimistic UI và Error Rollback để bạn nhúng thẳng vào file foot.html:

// File: pages/bakery-shop/foot.html
const ApiCartStore = {
  endpoint: 'https://1576647xxx-admin.interdata.io.vn/graphql',
  state: { items: [], totalCount: 0, totalAmount: 0 },
  backupState: null,

  // 1. Khởi tạo và đồng bộ State
  init() {
    this.renderBadge();
  },

  // 2. Thêm bánh với Optimistic UI
  addItem(product) {
    this.backupState = JSON.parse(JSON.stringify(this.state));
    const exist = this.state.items.find(i => i.id === product.id);
    if (exist) {
      exist.qty += 1;
    } else {
      this.state.items.push({ ...product, qty: 1 });
    }
    this.recalculate();
    this.renderBadge();
    window.dispatchEvent(new CustomEvent('ezy:cart-updated', { detail: this.state }));
  },

  recalculate() {
    this.state.totalCount = this.state.items.reduce((s, i) => s + i.qty, 0);
    this.state.totalAmount = this.state.items.reduce((s, i) => s + (i.price * i.qty), 0);
  },

  renderBadge() {
    const badge = document.getElementById('header-badge');
    if (badge) badge.innerText = this.state.totalCount;
  },

  // 3. Gửi đơn hàng lên máy chủ VPS
  async checkout(customerInfo) {
    this.backupState = JSON.parse(JSON.stringify(this.state));

    const mutation = `
      mutation CreateOrder($input: CreateOrderInput!) {
        createOrder(input: $input) {
          orderCode
          status
        }
      }
    `;

    const payload = {
      query: mutation,
      variables: {
        input: {
          customerName: customerInfo.name,
          customerPhone: customerInfo.phone,
          customerAddress: customerInfo.address,
          items: this.state.items.map(i => ({
            productId: Number(i.id),
            quantity: Number(i.qty),
            price: parseFloat(i.price)
          })),
          totalAmount: parseFloat(this.state.totalAmount)
        }
      }
    };

    try {
      const res = await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
      });
      const data = await res.json();
      if (data.errors) throw new Error(data.errors[0].message);

      // Thành công: Xóa giỏ hàng
      const order = data.data.createOrder;
      this.state = { items: [], totalCount: 0, totalAmount: 0 };
      this.renderBadge();
      return { success: true, orderCode: order.orderCode };
    } catch (err) {
      // Thất bại: Rollback về trạng thái cũ
      this.state = this.backupState;
      this.renderBadge();
      throw err;
    }
  }
};

document.addEventListener('DOMContentLoaded', () => ApiCartStore.init());

6. Tổng kết & Kỳ tiếp theo

Quá trình "đổi máu" cho Frontend là minh chứng rõ ràng nhất cho sức mạnh của phương pháp Vibe Coding có kỷ luật:

  • Bắt đầu bằng Mock State First (Bài #07) để chốt UX trong 15 phút.
  • Dùng Model Context Protocol (Bài #08) để khóa chặt Schema máy chủ trong 3 giây.
  • Dùng Adapter Pattern (Bài #09) để đấu nối dữ liệu thật trong 20 phút mà không vỡ một đường nét giao diện nào!

🎯 3 điểm cốt lõi cần nhớ từ bài viết này:

  1. Tách biệt UI và Data Source: Dùng Adapter Pattern giúp bạn đổi nguồn dữ liệu từ LocalStorage sang API mà không cần sửa giao diện.
  2. Optimistic UI là chìa khóa của sự mượt mà: Cập nhật UI ngay lập tức trong 0ms trước khi request mạng phản hồi.
  3. Luôn có cơ chế Rollback: Bọc khối try...catch cẩn thận để giữ nguyên giỏ hàng khi người dùng rớt mạng.

Ở bài viết tiếp theo, chúng ta sẽ mở rộng năng lực tự động hóa sau khi đơn hàng được tạo:

👉 [Vibe Coding Thực Chiến #10] Tự Động Hóa Sau Bán Hàng: Tích Hợp Webhook Bắn Đơn Sang Telegram & Gửi Email Tức Thì!

Chúng ta sẽ cấu hình EzyPlatform Webhook để ngay khi khách bấm đặt bánh, máy chủ VPS sẽ lập tức "ting ting" thông báo đơn hàng vào nhóm Telegram của bạn và tự động gửi email xác nhận hóa đơn cho khách hàng!


💬 Góc thảo luận: Khi chuyển đổi từ dữ liệu mock sang API thật, anh em thường gặp vấn đề gì "đau đầu" nhất? Anh em có thói quen làm Optimistic UI cho các thao tác giỏ hàng không? Hãy cùng chia sẻ kinh nghiệm bên dưới nhé!

Nếu thấy bài viết này hữu ích, đừng quên bấm Upvote 👍 và Bookmark (Clip) 🔖 để đón đọc tiếp Bài #10 nhé. Chúc anh em Vibe Coding thật thăng hoa!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.