ข้ามไปยังเนื้อหาหลัก
ProgrammerThailand

AI & Vibe Coding

Workshop: Vibe Code แอป Todo ด้วย HTML/JS ใน 30 นาที

Workshop: Vibe Code แอป Todo ด้วย HTML/JS ใน 30 นาที

อ่านเรื่องแนวคิดมาเยอะแล้ว บทนี้มาลงมือจริงกัน เราจะ vibe code แอป Todo แบบไฟล์เดียวด้วย HTML และ JavaScript ล้วน ไม่มี framework ไม่มี build tool ใช้เวลาประมาณ 30 นาที ผมจะเล่ากระบวนการ prompt ทีละรอบว่าสั่งอะไร ได้อะไรกลับมา และตรงไหนที่ต้องหยุดอ่านแล้วแก้เอง เพราะแม้แต่แอปเล็กขนาดนี้ AI ก็ยังทิ้งปัญหาไว้ให้เจอ เมื่อจบ workshop คุณจะได้แอปที่ใช้งานได้จริง เก็บข้อมูลไว้ใน localStorage และได้เห็นจังหวะการทำงานกับ AI ที่นำไปใช้กับงานใหญ่กว่านี้ได้

เตรียมตัวก่อนเริ่ม (นาทีที่ 0-3)

สิ่งที่ต้องมี: editor, เบราว์เซอร์ และผู้ช่วย AI ตัวไหนก็ได้ที่คุณถนัด สร้างโฟลเดอร์ใหม่แล้วเริ่ม Git ไว้เลย เพื่อให้ย้อนกลับได้ทุกรอบ

mkdir todo-vibe && cd todo-vibe
git init

ก่อนพิมพ์ prompt แรก ให้เขียนขอบเขตไว้สั้น ๆ ว่าเราต้องการอะไรและไม่ต้องการอะไร การกำหนดว่า "ไม่ทำอะไร" สำคัญพอ ๆ กัน เพราะ AI ชอบแถมฟีเจอร์

  • ต้องมี: เพิ่มงาน ติ๊กว่าเสร็จ ลบงาน กรองดู ทั้งหมด/ค้าง/เสร็จ ข้อมูลไม่หายเมื่อรีเฟรช
  • ไม่ต้องมี: ระบบสมาชิก backend drag and drop ธีมมืด (ไว้รอบหน้า)

รอบที่ 1: โครงที่ใช้งานได้ (นาทีที่ 3-10)

สร้างแอป Todo เป็นไฟล์ index.html ไฟล์เดียว ใช้ HTML, CSS และ JavaScript ล้วน
ไม่ใช้ framework หรือ library ภายนอก
ฟีเจอร์รอบนี้มีแค่:
- ช่องกรอก + ปุ่มเพิ่มงาน (กด Enter ก็เพิ่มได้)
- รายการงาน แต่ละงานมี checkbox ว่าเสร็จ และปุ่มลบ
- เก็บข้อมูลใน localStorage ให้รีเฟรชแล้วไม่หาย
ข้อกำหนด:
- ห้ามใช้ innerHTML กับข้อความที่ผู้ใช้พิมพ์ ให้ใช้ textContent
- แยก state (array ของงาน) ออกจากการ render
- ส่วนติดต่อผู้ใช้เป็นภาษาไทย

ข้อกำหนดเรื่อง innerHTML ใส่ไว้ตั้งแต่ต้นเพราะถ้าไม่บอก AI มักเขียนแบบ template string ต่อ HTML ซึ่งถ้าผู้ใช้พิมพ์ <img src=x onerror=alert(1)> เป็นชื่องาน สคริปต์จะทำงานทันที แม้เป็นแอปส่วนตัวก็ควรฝึกนิสัยนี้

ได้โค้ดกลับมาแล้ว อย่าเพิ่งเชื่อ ให้เปิดในเบราว์เซอร์แล้วลองตามนี้: เพิ่มงาน 3 งาน ติ๊ก 1 งาน ลบ 1 งาน รีเฟรช แล้วดูว่าสถานะยังอยู่ครบ ถ้าผ่านก็ commit

git add index.html
git commit -m "feat: basic todo with add, toggle, delete and localStorage"

รอบที่ 2: ตัวกรองและตัวนับ (นาทีที่ 10-17)

เพิ่มฟีเจอร์ในไฟล์ index.html เดิม โดยไม่เปลี่ยนโครงสร้างข้อมูลใน localStorage:
- ปุ่มกรอง: ทั้งหมด / ค้างอยู่ / เสร็จแล้ว (ปุ่มที่เลือกอยู่ให้เห็นต่างจากปุ่มอื่น)
- ข้อความแสดงจำนวนงานที่ค้างอยู่ เช่น "เหลือ 3 งาน"
- ปุ่ม "ล้างงานที่เสร็จแล้ว"
แก้เฉพาะส่วนที่จำเป็น และบอกว่าแก้ตรงไหนบ้าง

ประโยค "โดยไม่เปลี่ยนโครงสร้างข้อมูลใน localStorage" สำคัญ เพราะถ้า AI เปลี่ยนชื่อ key หรือรูปแบบข้อมูล งานที่ผู้ใช้บันทึกไว้แล้วจะหายหรือโหลดไม่ขึ้น อ่าน git diff ก่อนรับ ถ้ามีการเปลี่ยน key ให้สั่งแก้กลับ

โค้ดผลลัพธ์ (ฉบับที่ผ่านการแก้แล้ว)

หลังจากผ่านสองรอบและแก้จุดต่าง ๆ ในหัวข้อถัดไป ส่วน JavaScript หลักมีหน้าตาประมาณนี้ (ตัด CSS ออกเพื่อความกระชับ):

<form id="add-form">
  <input id="new-todo" maxlength="200" placeholder="ต้องทำอะไร?" autocomplete="off" required>
  <button type="submit">เพิ่ม</button>
</form>
<div id="filters">
  <button data-filter="all">ทั้งหมด</button>
  <button data-filter="active">ค้างอยู่</button>
  <button data-filter="done">เสร็จแล้ว</button>
</div>
<ul id="list"></ul>
<p><span id="count"></span> <button id="clear-done">ล้างงานที่เสร็จแล้ว</button></p>
const STORAGE_KEY = 'todo-vibe.v1';
let todos = load();
let filter = 'all';

function load() {
  try {
    const data = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
    return Array.isArray(data) ? data.filter(t => t && typeof t.text === 'string') : [];
  } catch (e) {
    return []; // ข้อมูลเสีย หรือ localStorage ใช้ไม่ได้ (เช่น บางโหมดของเบราว์เซอร์)
  }
}

function save() {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
  } catch (e) {
    alert('บันทึกไม่สำเร็จ พื้นที่เก็บข้อมูลอาจเต็มหรือถูกปิดไว้');
  }
}

function render() {
  const list = document.getElementById('list');
  list.replaceChildren();
  const visible = todos.filter(t =>
    filter === 'all' ? true : filter === 'done' ? t.done : !t.done);

  for (const todo of visible) {
    const li = document.createElement('li');
    const box = document.createElement('input');
    box.type = 'checkbox';
    box.checked = todo.done;
    box.addEventListener('change', () => { todo.done = box.checked; update(); });

    const label = document.createElement('span');
    label.textContent = todo.text; // ปลอดภัยจาก XSS

    const del = document.createElement('button');
    del.textContent = 'ลบ';
    del.addEventListener('click', () => {
      todos = todos.filter(t => t.id !== todo.id);
      update();
    });

    li.append(box, label, del);
    list.append(li);
  }

  const left = todos.filter(t => !t.done).length;
  document.getElementById('count').textContent = `เหลือ ${left} งาน`;
  document.querySelectorAll('#filters button').forEach(b =>
    b.setAttribute('aria-pressed', String(b.dataset.filter === filter)));
}

function update() {
  save();
  render();
}

document.getElementById('add-form').addEventListener('submit', (e) => {
  e.preventDefault();
  const input = document.getElementById('new-todo');
  const text = input.value.trim();
  if (text === '') return;
  todos.push({ id: Date.now().toString(36) + Math.random().toString(36).slice(2), text, done: false });
  input.value = '';
  update();
});

document.getElementById('filters').addEventListener('click', (e) => {
  const btn = e.target.closest('button[data-filter]');
  if (!btn) return;
  filter = btn.dataset.filter;
  render();
});

document.getElementById('clear-done').addEventListener('click', () => {
  todos = todos.filter(t => !t.done);
  update();
});

render();

นำทั้งสองส่วนไปไว้ในไฟล์ index.html โดยใส่ JavaScript ในแท็ก script ท้าย body แล้วเปิดในเบราว์เซอร์ได้ทันที ไม่ต้องมี server

จุดที่ต้องแก้เอง (นาทีที่ 17-27)

นี่คือส่วนที่สำคัญที่สุดของ workshop ต่อไปนี้คือปัญหาแบบที่มักเจอในโค้ดรอบแรกจาก AI และเป็นเหตุผลที่โค้ดด้านบนหน้าตาเป็นแบบนั้น:

  • ใช้ index ของ array เป็นตัวระบุงาน โค้ดรอบแรกมักลบงานด้วย todos.splice(index, 1) ซึ่งพังทันทีเมื่อเปิดตัวกรอง เพราะ index ของรายการที่แสดงไม่ตรงกับ index ใน array จริง ผลคือกดลบงานหนึ่งแต่อีกงานหายไป แก้โดยให้ทุกงานมี id และอ้างอิงด้วย id เสมอ
  • JSON.parse ไม่มี try/catch ถ้าข้อมูลใน localStorage เสีย แอปจะขาวทั้งหน้า ทดสอบได้โดยเปิด DevTools แล้วแก้ค่าใน localStorage ให้เป็นข้อความมั่ว ๆ แล้วรีเฟรช
  • เพิ่มงานว่างได้ กดเพิ่มทั้งที่ช่องมีแต่ช่องว่าง ต้อง trim() ก่อนตรวจ
  • ใช้ innerHTML แม้สั่งห้ามแล้ว บางครั้ง AI ทำตามในจุดหนึ่งแต่ลืมอีกจุด เช่น ส่วนแสดงจำนวนงาน ให้ค้นคำว่า innerHTML ในไฟล์ทุกครั้ง
  • ไม่มีขีดจำกัดความยาว ใส่ maxlength ไว้ที่ input เพื่อกันข้อความยาวผิดปกติทำให้หน้าตาพัง
  • การเข้าถึง (accessibility) ปุ่มกรองไม่บอกสถานะให้ screen reader รู้ เราเพิ่ม aria-pressed เข้าไปเอง และใช้ <form> เพื่อให้ Enter ทำงานโดยไม่ต้องดักคีย์เอง

วิธีแก้แต่ละจุด เลือกได้สองทาง คือแก้เองถ้าเป็นจุดเล็กและเราเข้าใจชัดแล้ว หรือสั่ง AI โดยระบุปัญหาให้ชัด เช่น "เมื่อเปิดตัวกรอง 'เสร็จแล้ว' แล้วกดลบงานแรก งานที่หายไปเป็นคนละงาน สาเหตุน่าจะมาจากการใช้ index ให้เปลี่ยนไปใช้ id แทน แก้เฉพาะส่วนนี้" อย่าสั่งแค่ "มันมีบั๊ก แก้ให้หน่อย" แก้เสร็จแต่ละจุดให้ทดสอบซ้ำแล้ว commit แยก

ทดสอบรอบสุดท้ายและสิ่งที่ได้เรียนรู้ (นาทีที่ 27-30)

Checklist ทดสอบด้วยมือก่อนถือว่าเสร็จ:

  • เพิ่ม ติ๊ก ลบ ล้างงานที่เสร็จ แล้วรีเฟรช สถานะต้องถูกต้องทุกครั้ง
  • เปิดตัวกรองแต่ละแบบแล้วลบงาน ต้องลบถูกงาน
  • เพิ่มงานชื่อ <b>ทดสอบ</b> ต้องแสดงเป็นข้อความธรรมดา ไม่เป็นตัวหนา
  • พิมพ์แต่ช่องว่างแล้วกดเพิ่ม ต้องไม่มีงานใหม่
  • ทำให้ข้อมูลใน localStorage เสีย แล้วรีเฟรช แอปต้องยังเปิดได้

บทเรียนจาก workshop นี้ใช้ได้กับงานใหญ่เช่นกัน: เขียนขอบเขตก่อน สั่งทีละฟีเจอร์ ใส่ข้อกำหนดด้านความปลอดภัยไว้ใน prompt ตั้งแต่แรก ทดสอบด้วยมือจริงทุกรอบ อ่าน diff และ commit ทุกครั้งที่ผ่าน

สรุป

  • กำหนดขอบเขตว่าจะทำและไม่ทำอะไรก่อนพิมพ์ prompt แรก แล้วแบ่งงานเป็นรอบเล็ก ๆ
  • ใส่ข้อกำหนดที่สำคัญไว้ใน prompt ตั้งแต่ต้น เช่น ห้ามใช้ innerHTML กับข้อมูลผู้ใช้ และห้ามเปลี่ยนรูปแบบข้อมูลที่บันทึกไว้
  • โค้ดรอบแรกจาก AI มักทำงานได้ในกรณีปกติ แต่พลาดเรื่อง id, error handling, input ว่าง และ accessibility
  • ทดสอบด้วยมือตาม checklist ทุกรอบ อ่าน diff และ commit เมื่อผ่าน

ลองทำ

ต่อยอดแอปนี้ด้วยฟีเจอร์ "ดับเบิลคลิกเพื่อแก้ไขชื่องาน" โดยใช้กระบวนการเดียวกัน: เขียนข้อกำหนดก่อน (เช่น กด Enter เพื่อบันทึก กด Escape เพื่อยกเลิก ถ้าแก้เป็นค่าว่างให้ถือว่ายกเลิก) สั่ง AI โดยระบุว่าห้ามเปลี่ยนรูปแบบข้อมูลใน localStorage ทดสอบทุกกรณีรวมทั้งการแก้ขณะเปิดตัวกรอง และจดว่าในรอบนี้คุณต้องแก้อะไรเองบ้าง

ติดปัญหาตอนทำตาม? ถามในชุมชน

ความคิดเห็น (0)

ยังไม่มีความคิดเห็น — เริ่มคุยเป็นคนแรก ถามสิ่งที่สงสัย หรือแชร์ประสบการณ์ของคุณ