[Vibe Coding Thực Chiến #07] Xây Dựng Tính Năng Frontend: Giỏ Hàng, Đặt Hàng Và Quản Lý State Bằng LocalStorage!
Tác giả: Lee
Series: Vibe Coding Thực Chiến Cùng EzyPlatform (#07)
Hạ tầng thực nghiệm: VPS Ubuntu 22.04 LTS (157.66.47.xxx) | Domain:https://1576647xxx-admin.interdata.io.vn
Bộ công cụ: VS Code + Extensionyoungmonkeys.ezyarticle+ Claude Code CLI / ChatGPT Codex
Ở bài viết số #06, chúng ta đã cùng nhau giải quyết triệt để bài toán nhận diện thương hiệu: thiết lập Brand Brief khóa chặt bảng màu, xây dựng Responsive đa thiết bị, xử lý tính năng Dark Mode FOUC-Free (chống chớp trắng màn hình) và tái sử dụng Header/Footer bằng Page Fragments.
Thế nhưng, một website đẹp mắt nhưng chỉ toàn "ảnh tĩnh" và không bấm được nút nào thì chưa thể gọi là một ứng dụng phần mềm thực thụ.
Đến lúc này, 80% anh em dev khi Vibe Coding cùng AI sẽ lập tức mắc một sai lầm kinh điển:
"Vội vàng bắt AI tạo bảng MySQL, thiết kế database schema, rồi hì hục viết API Spring Boot / Node.js... Để rồi cả buổi chiều vật lộn với lỗi CORS, mismatch kiểu dữ liệu Request Body, trong khi giao diện người dùng thì vẫn đơ cứng như khúc gỗ!" 😣
Tại sao phải tự làm khổ mình như vậy? Trong bài viết này, mình sẽ cùng bạn làm chủ tư duy "Mock State First" — hoàn thiện 100% trải nghiệm mua sắm, giỏ hàng phản ứng thời gian thực và tạo đơn hàng giả lập bằng Vanilla JS + LocalStorage chỉ trong 15 phút trước khi đụng vào một dòng code Backend nào!
1. Triết lý "Mock State First": Đừng vội viết Backend khi UX chưa thông!
Rất nhiều bạn hỏi mình: "Tại sao không bảo AI viết luôn API lưu database cho nhanh, mắc mớ gì phải lưu tạm vào LocalStorage?"
Hãy nhìn vào sự đối lập giữa hai cách tiếp cận:
So sánh: Cái bẫy Backend-First làm bạn sa lầy vào hạ tầng, trong khi Mock State First giúp chốt nhanh trải nghiệm người dùng sau 15 phút.
3 lý do vì sao "Mock State First" là chân lý khi Vibe Coding:
- Kiểm chứng nhanh trải nghiệm người dùng (UX Validation): Khách bấm nút "Thêm vào giỏ" thì badge có nảy số không? Giỏ hàng trượt ra có mượt không? Tăng giảm số lượng bánh có tự đổi tổng tiền không? Tất cả những tương tác vi mô (micro-interactions) này chỉ cần vài chục dòng JavaScript là test được ngay trên VPS thật.
- Không làm loãng Context Window của AI: Khi bạn chỉ yêu cầu AI tập trung vào logic thao tác DOM và quản lý Client State, AI sẽ nhả ra code cực kỳ sạch, không bị lẫn lộn giữa SQL, ORM entity hay cấu hình CORS máy chủ.
- Khóa chặt Data Schema trước khi thiết kế Database: Khi giỏ hàng chạy trơn tru trên
localStorage, bạn đã định hình chính xác từng trường dữ liệu củaOrderItem(id,name,price,quantity,subtotal). Sang Bài #08, khi cắm MCP Server vào database thật, bạn chỉ việc ánh xạ 1:1 mà không bao giờ sợ bị lệch kiểu dữ liệu!
2. Kiến trúc Unidirectional Data Flow: Quản lý giỏ hàng chuẩn chỉ
Để giỏ hàng hoạt động mượt mà mà không cần cài đặt React, Redux hay bất kỳ thư viện nặng nề nào, chúng ta áp dụng mô hình Luồng dữ liệu một chiều (Unidirectional Data Flow):
Kiến trúc 4 bước: Sự kiện click ➔ Cart Store xử lý ➔ Ghi vào LocalStorage & Broadcast Event ➔ Tự động cập nhật Header Badge & Drawer.
Cấu trúc State chuẩn lưu trong localStorage:
{
"items": [
{ "id": "cake-01", "name": "Bánh Mousse Socola Bỉ", "price": 95000, "qty": 1, "image": "/media/mousse.png" },
{ "id": "cake-02", "name": "Bánh Croissant Bơ Pháp", "price": 45000, "qty": 2, "image": "/media/croissant.png" }
],
"totalCount": 3,
"totalAmount": 185000,
"currency": "VND",
"updatedAt": 1728086400000
}
Bất cứ khi nào giỏ hàng có sự biến động (thêm bánh, bớt bánh, xóa sạch), hệ thống sẽ bắn một sự kiện toàn cục:
window.dispatchEvent(new CustomEvent('ezy:cart-updated', { detail: cartState }));
Nhờ cơ chế này, dù Header nằm ở một Page Fragment riêng và Thân trang nằm ở file content.html, cả hai đều tự động lắng nghe và nhảy số đồng bộ 100%!
3. Thực chiến 3 bước: Dựng tiệm bánh "Le Petit Bakery" trên VPS
Bây giờ, hãy mở VS Code và bắt tay vào việc nhé!
Bước 1: Tạo trang danh sách sản phẩm pages/bakery-shop/
Mở file pages/bakery-shop/meta.json để đăng ký trang trên EzyPlatform:
{
"id": 0,
"slug": "bakery-shop",
"title": "Le Petit Bakery - Tiệm Bánh Nghệ Nhân Chuẩn Pháp",
"pageType": "PAGE",
"contentType": "HTML",
"status": "PUBLISHED",
"metadata": {
"layout": "empty"
}
}
Trong file pages/bakery-shop/content.html, chúng ta tạo lưới sản phẩm và nút mở giỏ hàng:
Danh sách bánh ngọt chuẩn Tailwind CSS Dark Mode hiển thị trực tiếp trên VPS 157.66.47.xxx.
Mỗi nút "Thêm vào giỏ" được gắn trực tiếp các data-attributes để JavaScript dễ dàng bóc tách dữ liệu:
<button
onclick="CartStore.addItem({ id: 'cake-01', name: 'Bánh Mousse Socola Bỉ', price: 95000 })"
class="px-3 py-1.5 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-xs font-bold text-white shadow-md shadow-indigo-600/30 transition">
+ Thêm vào giỏ
</button>
Bước 2: Xây dựng Slide-out Cart Drawer trong foot.html
Khi khách bấm vào nút "Giỏ hàng" trên Header, một ngăn kéo (Drawer) chứa danh sách các món bánh đã chọn sẽ trượt êm từ mép phải màn hình:
Drawer giỏ hàng cho phép tăng giảm số lượng (+ / -) và tự động tính lại tổng tiền tức thì.
Toàn bộ logic tính toán và hiển thị drawer được đặt gói gọn trong file foot.html:
// pages/bakery-shop/foot.html
const CartStore = {
key: 'ezy_cart_state',
get() {
try {
const data = localStorage.getItem(this.key);
return data ? JSON.parse(data) : { items: [], totalCount: 0, totalAmount: 0 };
} catch (e) {
console.warn("Lỗi đọc LocalStorage, khôi phục giỏ hàng rỗng:", e);
return { items: [], totalCount: 0, totalAmount: 0 };
}
},
save(state) {
state.totalCount = state.items.reduce((sum, item) => sum + item.qty, 0);
state.totalAmount = state.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
localStorage.setItem(this.key, JSON.stringify(state));
window.dispatchEvent(new CustomEvent('ezy:cart-updated', { detail: state }));
},
addItem(product) {
const state = this.get();
const existing = state.items.find(i => i.id === product.id);
if (existing) {
existing.qty += 1;
} else {
state.items.push({ ...product, qty: 1 });
}
this.save(state);
},
updateQty(id, delta) {
const state = this.get();
const item = state.items.find(i => i.id === id);
if (!item) return;
item.qty += delta;
if (item.qty <= 0) {
state.items = state.items.filter(i => i.id !== id);
}
this.save(state);
},
clear() {
this.save({ items: [], totalCount: 0, totalAmount: 0 });
}
};
Bước 3: Kiểm tra tính bền vững bằng Chrome DevTools
Bây giờ, hãy mở Chrome DevTools (bấm F12 hoặc chuột phải chọn Inspect), chuyển sang tab Application > Local Storage:
Dữ liệu ezy_cart_state được lưu trữ an toàn, có cấu trúc chặt chẽ trên domain https://1576647xxx-admin.interdata.io.vn.
Bất kể bạn F5 trang web hàng trăm lần, hay mở tab mới, hay chuyển qua lại giữa các trang con trong EzyPlatform, số lượng giỏ hàng vẫn còn nguyên vẹn 100%!
Bước 4: Form Đặt Hàng & Giả Lập Tạo Mã Đơn Hàng (Mock Order Generator)
Sau khi khách ưng ý với giỏ hàng và bấm "Tiến Hành Đặt Hàng", hệ thống hiển thị Form điền thông tin người nhận (Họ tên, SĐT, Địa chỉ).
Khi bấm nút xác nhận, script sẽ:
- Validate form: Kiểm tra không được để trống số điện thoại và tên.
- Sinh mã đơn hàng ngẫu nhiên theo định dạng chuẩn:
#EZY-2026-XXXX. - Lưu đơn hàng vào
localStorage.last_order. - Gọi
CartStore.clear()để dọn sạch giỏ hàng. - Mở modal thông báo thành công rực rỡ:
Đơn hàng giả lập được tạo thành công kèm mã đơn #EZY-2026-8942 và tổng tiền 185.000 ₫.
Toàn bộ chu trình mua hàng khép kín đã hoàn thiện và chạy mượt mà ngay trên VPS thật chỉ sau chưa đầy 15 phút! 🎉
4. 3 bẫy lỗi kinh điển khi dùng AI làm Frontend State & Cách né 😡
Trong quá trình đồng hành cùng anh em dev, đây là 3 lỗi phổ biến nhất khiến tính năng giỏ hàng bị gãy:
Bẫy lỗi #1: State bị lệch pha (Desynchronization) giữa các trang 😣
- Hiện tượng: Ở trang danh sách bánh thì badge giỏ hàng hiện số 2, nhưng khi bấm chuyển sang trang Giới thiệu thì badge lại về số 0.
- Nguyên nhân: Chỉ lưu state vào một biến JavaScript toàn cục (
let cart = []) trong RAM của trang đó, hoặc không đồng bộ sự kiện khi chuyển trang. - Cách né (Recommend 👍): Luôn kết hợp lưu
localStoragevà lắng nghe sự kiệnDOMContentLoadedtrên mọi trang để đọc lại dữ liệu khởi tạo.
Bẫy lỗi #2: Trắng màn hình vì lỗi JSON.parse dữ liệu hỏng 😡
- Hiện tượng: Người dùng lỡ tay bấm F5 hoặc dữ liệu cũ bị sửa tay, trang web lập tức crash trắng xóa với lỗi đỏ lòm:
Uncaught SyntaxError: Unexpected token in JSON at position 0. - Nguyên nhân: AI viết câu lệnh ngây thơ:
const cart = JSON.parse(localStorage.getItem('cart'));mà không có khốitry...catchbọc ngoài. Nếu giá trị trả về lànullhoặc chuỗi rác, hàm parse sẽ ném lỗi crash toàn bộ script. - Cách né (Bí kíp 💡): Luôn bọc trong khối
try...catchan toàn như đoạn mãCartStore.get()ở trên: nếu có bất kỳ lỗi cú pháp nào, hàm tự động trả về{ items: [], totalCount: 0 }.
Bẫy lỗi #3: AI nhồi code JS vào giữa file content.html làm vỡ SSR Thymeleaf ⚠️
- Hiện tượng: AI tiện tay viết thẻ
<script>ngay giữa thân thẻ<section>trong filecontent.html. Khi Thymeleaf Engine compile trang trên server VPS, các biến template hoặc sự kiện click bị xung đột khiến script chạy trước khi DOM nạp xong. - Cách né (Lưu ý ⚠️): Tuân thủ nghiêm ngặt Quy chuẩn 4 file EzyArticle: Toàn bộ mã nguồn JavaScript xử lý tương tác bắt buộc phải đặt trong file
foot.html.
5. Cheatsheet & Code Mẫu Cart Store Dùng Ngay (Copy & Bookmark) 📋
Dưới đây là đoạn mã hoàn chỉnh của module CartStore siêu nhẹ (dưới 60 dòng, 0 dependencies) để bạn copy vào file foot.html trong dự án của mình:
// File: foot.html — CartStore Module Chuẩn Mực
const CartStore = {
key: 'ezy_cart_state',
get() {
try {
const data = localStorage.getItem(this.key);
return data ? JSON.parse(data) : { items: [], totalCount: 0, totalAmount: 0 };
} catch (e) {
return { items: [], totalCount: 0, totalAmount: 0 };
}
},
save(state) {
state.totalCount = state.items.reduce((sum, item) => sum + item.qty, 0);
state.totalAmount = state.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
localStorage.setItem(this.key, JSON.stringify(state));
window.dispatchEvent(new CustomEvent('ezy:cart-updated', { detail: state }));
},
addItem(product) {
const state = this.get();
const item = state.items.find(i => i.id === product.id);
if (item) { item.qty += 1; } else { state.items.push({ ...product, qty: 1 }); }
this.save(state);
},
updateQty(id, delta) {
const state = this.get();
const item = state.items.find(i => i.id === id);
if (!item) return;
item.qty += delta;
if (item.qty <= 0) { state.items = state.items.filter(i => i.id !== id); }
this.save(state);
},
clear() {
this.save({ items: [], totalCount: 0, totalAmount: 0 });
}
};
// Lắng nghe sự kiện để cập nhật Badge toàn cục
window.addEventListener('ezy:cart-updated', (e) => {
const badge = document.getElementById('header-badge');
if (badge) badge.innerText = e.detail.totalCount;
});
6. Tổng kết & Kỳ tiếp theo
Tư duy "Mock State First" không phải là đi đường tắt tạm bợ, mà là nghệ thuật phân lập ranh giới (Separation of Concerns) trong kỹ nghệ phần mềm. Bằng cách chốt chặt luồng UX và cấu trúc dữ liệu phía Client trước, bạn tiết kiệm được hàng chục giờ loay hoay với backend mà vẫn mang lại cảm giác "vibe" cực đã khi sản phẩm vận hành mượt mà ngay trước mắt!
🎯 3 điểm cốt lõi cần nhớ từ bài viết này:
- Mock State First: Luôn hoàn thiện luồng tương tác và trải nghiệm người dùng trên Client trước khi viết API backend.
- LocalStorage là bệ phóng hoàn hảo: Giúp lưu trữ giỏ hàng bền vững, chống mất dữ liệu khi F5 và định hình chuẩn xác schema đơn hàng.
- Quản lý dữ liệu bằng CustomEvent: Giúp Header, Drawer và Checkout tự động đồng bộ mà không cần cài thêm framework nặng nề.
Ở bài viết tiếp theo, chúng ta sẽ bước lên đỉnh cao công nghệ của Vibe Coding hiện đại:
👉 [Vibe Coding Thực Chiến #08] Vũ Khí Tối Thượng MCP (Model Context Protocol): Cho AI Cắm Trực Tiếp Vào Database & API Máy Chủ!
Chúng ta sẽ cấu hình MCP Server trên EzyPlatform, cho phép Claude hoặc Codex tự kết nối vào VPS, tự đọc bảng orders, products trên database thật và thay thế LocalStorage bằng cơ sở dữ liệu production mà không cần bạn phải viết một dòng SQL thủ công nào!
💬 Góc thảo luận: Anh em theo trường phái làm mockup state phía frontend trước để test nhanh UX, hay thích dựng xong DB/API backend rồi mới quay lại đắp giao diện? Trải nghiệm quản lý state lớn nhất của anh em khi dùng AI là gì? Hãy chia sẻ bên dưới nhé!
Nếu thấy bài viết này hữu ích và giúp bạn giải phóng tốc độ làm app, đừng quên bấm Upvote 👍 và Bookmark (Clip) 🔖 bài viết nhé. Hẹn gặp lại anh em ở Bài #08!
All rights reserved