Client-Side Interactions & State Management
UI คือภาพสะท้อนของข้อมูล — เราไม่ลบแถวออกจาก HTML แต่ลบ item ออกจาก array แล้วสั่ง render ใหม่
HTML vs UI vs DOM
| คืออะไร | สถานะ | |
|---|---|---|
| HTML | โครงสร้าง — บอกว่ามีเนื้อหาอะไรบ้าง (heading, paragraph, button) | static |
| UI | ประสบการณ์ — ผลลัพธ์ทางสายตาหลังใส่ CSS, JS และหลักการออกแบบ | สิ่งที่ผู้ใช้เห็นและสัมผัส |
| DOM | ตัวแทนที่ "มีชีวิต" — เบราว์เซอร์ parse HTML เป็นโครงสร้างต้นไม้ของ object ในหน่วยความจำ | dynamic |
- HTML เป็น "โครงกระดูก" ของ UI — เว็บไม่มี HTML ไม่ได้
- DOM เป็น programming interface (API) ให้ JavaScript เข้าไปเปลี่ยนโครงสร้าง สไตล์ และเนื้อหาได้
- ลำดับใน HTML (อะไรมาก่อน อะไรจัดกลุ่มกัน) คือหัวใจของ accessibility
DOM Tree
เบราว์เซอร์อ่าน HTML แล้วสร้าง DOM ขึ้นมาในหน่วยความจำ จากนั้นจึงเริ่ม "ฟัง" เหตุการณ์
<html lang="en">
<head><title>Accessing DOM</title></head>
<body>
<script>
const heading = document.createElement("h1");
const headingText = document.createTextNode("Big Head!");
heading.appendChild(headingText);
document.body.appendChild(heading);
</script>
</body>
</html>
Event-Driven Architecture (EDA)
ส่วนประกอบของระบบสื่อสารกันด้วยการ "ผลิต" และ "ตอบสนอง" ต่อ event
ตัวอย่างเชิงธุรกิจ
ลูกค้าสั่งซื้อ → เกิด event Order Placed → service ชำระเงิน, จัดการสต็อก และส่งอีเมล ไม่ต้องคอยถามระบบสั่งซื้อตลอดเวลา แต่ตอบสนองเองเมื่อ event เกิด
| บทบาท | คืออะไร | ตัวอย่าง |
|---|---|---|
| Producer (ตัวกระตุ้น) | element ที่เป็นจุดเริ่มของ action | ปุ่ม "Buy Now", ช่องค้นหา |
| Event (ข้อความ) | object ที่บรรจุข้อมูลว่าเกิดอะไรขึ้น | กดปุ่มไหน ค่าปัจจุบันของ dropdown คืออะไร |
| Consumer (ตัวจัดการ) | ฟังก์ชัน JS ที่รัน business logic | เพิ่มสินค้าเข้า array ของตะกร้า |
Event Loop
- JavaScript เป็น single-threaded — ทำงานได้ทีละอย่าง
- ระหว่างรอผู้ใช้คลิกหรือรอไฟล์โหลด มันไม่ค้าง แต่เอา event ไปเข้า queue
- จุดที่ข้อสอบชอบถาม: ถ้าเอางานหนัก (เช่นประมวลผลสินค้า 10,000 ชิ้น) มารันบน main thread ตรง ๆ UI จะ ค้าง (hang)
- แก้ด้วย logic แบบ asynchronous
Event Delegation
Business logic: ทำให้แอป scale ได้ — catalog โตขึ้นแต่ memory footprint เท่าเดิม
document.getElementById('catalog').addEventListener('click', function(event) {
const btn = event.target.closest('.add-to-cart');
if (!btn) return; // คลิกโดนที่ว่าง ไม่ต้องทำอะไร
const id = btn.dataset.id; // อ่าน data-id ของปุ่มที่ถูกคลิก
addToCart(id);
});
Debouncing
ช่องค้นหาที่กรองทุกการกดแป้นอาจยิง 10 requests ใน 2 วินาที — แก้ด้วยการรอให้ผู้ใช้หยุดพิมพ์ 300msก่อนค่อยทำงาน
let timer;
searchBox.addEventListener('input', function(e) {
clearTimeout(timer); // ยกเลิกนัดเดิม
timer = setTimeout(function() {
filterProducts(e.target.value); // ทำจริงเมื่อเงียบครบ 300ms
}, 300);
});
State เป็น Single Source of Truth
- UI คือภาพสะท้อนของ state — ไม่ลบแถวใน HTML แต่ลบ item ออกจาก array แล้ว render ใหม่ ถ้าข้อมูลถูก UI จะถูกตาม
- แยก logic ออกจาก trigger —
filterProducts()ไม่ควรสนใจว่าถูกเรียกจาก input box หรือ dropdown ทำให้นำกลับมาใช้ซ้ำและเทสต์ง่าย - เผื่อ edge case — ถ้าค้นแล้วไม่เจอ ต้องมี state "No results found" ไม่ใช่ปล่อยหน้าว่าง
Logic table จากสไลด์
| User Action | DOM Event | Business Logic | UI Change |
|---|---|---|---|
| คลิก "Price: High to Low" | change | sort allProducts ตามราคา | catalog เรียงใหม่ทันที |
| พิมพ์ "Nike" ในช่องค้นหา | input | filter array หา "Nike" | สินค้าที่ไม่ตรงหายไป |
toLowerCase() ทั้งสองฝั่ง)localStorage / sessionStorage / cookies
| Feature | localStorage | sessionStorage | Cookies |
|---|---|---|---|
| Persistence | จนกว่าจะลบเอง | จนกว่าจะปิดแท็บ | ตามที่ตั้งค่า |
| Scope | ใช้ร่วมทุกแท็บของ origin เดียวกัน | เฉพาะแท็บที่สร้าง | คุมด้วย Domain และ Path |
| Capacity | ~5–10MB | ~5MB | ~4KB |
| Data Types | String | String | String |
| API | ง่าย | ง่าย | ปานกลาง |
localStorage.setItem('theme', 'dark'); // เพิ่ม/ทับค่า
const theme = localStorage.getItem('theme'); // อ่านค่า
localStorage.removeItem('theme'); // ลบเฉพาะ key
localStorage.clear(); // ล้างทั้ง origin
- Same-Origin Policy — แยกตาม protocol + domain + port เช่น
http://site.comอ่านข้อมูลของhttps://site.comไม่ได้ - Synchronous — ถ้าข้อมูลใหญ่มากจะบล็อก main thread
Serialization Bridge
localStorage เก็บได้แค่ string จึงต้องแปลงไปกลับ
localStorage.setItem('cart', JSON.stringify(cartArray)); // Saving : object → string
const cart = JSON.parse(localStorage.getItem('cart')); // Loading : string → object
| สถานะข้อมูล | อยู่ที่ไหน | ลักษณะ |
|---|---|---|
| In-Memory (Live) | ตัวแปร JS ระหว่างแอปทำงาน | เร็ว แต่ volatile — refresh แล้วหาย |
| On-Disk (Stored) | localStorage | อยู่ข้ามการ refresh แต่เป็น string ล้วน |
State Rehydration — ลำดับตอนเปิดหน้า
- Check — มี key
shopping_cartใน localStorage ไหม - Validate — ข้อมูลที่ได้เป็น array ที่ใช้ได้จริงไหม (เผื่อข้อมูลเสีย)
- Sync — ตั้งค่าตัวแปร global
cartStateให้เท่ากับข้อมูลที่บันทึกไว้ - Render — เรียกฟังก์ชันอัปเดต UI ทันที ให้ผู้ใช้เห็นของในตะกร้าตั้งแต่หน้าโหลดเสร็จ
cartState แล้วสั่ง refresh ทุกส่วน ไม่ใช่เขียนฟังก์ชันอัปเดตแยก 3 ตัวความเสี่ยง XSS ของ localStorage
เพราะ JavaScript ทุกตัวที่รันบนหน้านั้นเข้าถึง localStorage ได้ทั้งหมด ถ้าเว็บมีช่องโหว่ XSS (Cross-Site Scripting) สคริปต์ของผู้โจมตีจะอ่าน token ไปได้ทันที
ทางลด: sanitize ข้อมูลก่อนเอาไปแสดงผลใน DOM, ใช้ cookie แบบ HttpOnly สำหรับ token ที่อ่อนไหว
และตั้งอายุ token ให้สั้น
แนวข้อสอบอัตนัย
โจทย์: เว็บร้านค้าเก็บ JWT ไว้ใน localStorage และมีช่องรีวิวสินค้าที่แสดงข้อความผู้ใช้ด้วย innerHTML
จงอธิบายปัญหา สาเหตุ ผลกระทบ และวิธีแก้
- ปัญหา — ผู้โจมตีโพสต์รีวิวที่มี
<script>ซึ่งจะถูกรันในเบราว์เซอร์ของผู้ใช้คนอื่น แล้วขโมย token จาก localStorage - สาเหตุทางเทคนิค — สองอย่างประกอบกัน: (ก) ข้อมูลจากผู้ใช้ถูกเขียนเข้า DOM ผ่าน
innerHTMLโดยไม่ sanitize ทำให้เกิด XSS (ข) token ถูกเก็บใน localStorage ซึ่ง JavaScript ทุกตัวบนหน้าอ่านได้ - ผลกระทบ — ผู้โจมตีสวมสิทธิ์ผู้ใช้ (session hijacking) สั่งซื้อหรือดูข้อมูลส่วนตัวได้ กระทบทุกคนที่เปิดหน้ารีวิวนั้น
- วิธีแก้เชิงสถาปัตยกรรม — แสดงข้อความผู้ใช้ด้วย
textContentแทนinnerHTMLและ sanitize/escape ข้อมูลทั้งฝั่ง server ก่อนบันทึกและฝั่ง client ก่อนแสดง, ย้าย token ไปเก็บใน cookie แบบHttpOnly; Secure; SameSite, ตั้งอายุ token สั้นและมี refresh token