สัปดาห์ที่ 2

ทบทวน HTML, CSS, JavaScript & ES2022 (Arrow Fn, Async/Await, Modules)

CLO1 CLO4
📖 ทฤษฎี

HTML Semantic Elements

HTML5 นำเสนอ Semantic Elements ซึ่งเป็น Tags ที่มีความหมายในตัวเอง ช่วยให้ browser, Search Engine และ Screen Reader เข้าใจโครงสร้างหน้าเว็บได้ดีขึ้น แทนที่จะใช้ <div> สำหรับทุกอย่าง ให้เลือก Tag ที่ตรงกับความหมาย:

  • <header> — ส่วนหัวของหน้าหรือ section (Logo, Navigation, Hero)
  • <main> — เนื้อหาหลักของหน้า ใช้ได้เพียง 1 อันต่อหน้า
  • <section> — ส่วนของเนื้อหาที่มีหัวข้อรวมกัน เช่น Hero, Services, Contact
  • <article> — เนื้อหาที่เป็นอิสระ อ่านได้โดยไม่ต้องอิงบริบทรอบข้าง เช่น บทความ Blog
  • <aside> — เนื้อหาเสริมข้างเคียง เช่น Sidebar, Related Links
  • <footer> — ส่วนท้ายของหน้าหรือ section (ลิขสิทธิ์, ลิงก์ Social)
  • <nav> — เมนูนำทางหลัก ห่อรายการลิงก์ที่ใช้นำทางหน้า

ใน BookEasy (โปรเจกต์หลักของรายวิชา) หน้า Landing Page จะใช้โครงสร้าง: <header> (ชื่อแอป + nav) → <main> ที่มี <section> ย่อย (Hero, Services, CTA) → <footer> (copyright)

เคล็ดลับ

ใช้ Semantic Elements แทน <div> ทั่วไปเสมอเมื่อมีความหมายที่ตรงกัน เช่น ใช้ <nav> แทน <div class="nav"> ช่วยให้ Screen Reader อ่านหน้าเว็บได้ถูกต้องและ SEO ดีขึ้น

CSS Flexbox & Grid เบื้องต้น

Flexbox เหมาะสำหรับ Layout 1 มิติ (แถวหรือคอลัมน์) เช่น Navigation bar, รายการ Card ที่ต้องการตัดบรรทัดอัตโนมัติ:

  • display: flex — เปิดใช้ Flexbox บน container
  • flex-direction: row | column — ทิศทางการวาง items
  • justify-content: flex-start | center | space-between — จัดตำแหน่งในแนวแกนหลัก
  • align-items: flex-start | center | stretch — จัดตำแหน่งในแนวตั้งฉาก
  • flex-wrap: wrap — อนุญาตให้ items ตัดบรรทัดใหม่เมื่อเต็มแถว
  • gap — ระยะห่างระหว่าง items โดยไม่ต้องใช้ margin

CSS Grid เหมาะสำหรับ Layout 2 มิติ (แถวและคอลัมน์พร้อมกัน) เช่น Layout หน้าหลักที่มี Sidebar + Content หรือ Services grid 3 คอลัมน์:

  • display: grid — เปิดใช้ Grid
  • grid-template-columns: repeat(3, 1fr) — 3 คอลัมน์ขนาดเท่ากัน
  • grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) — ปรับจำนวนคอลัมน์อัตโนมัติ
  • gap — ระยะห่างระหว่าง grid cells
  • grid-column: span 2 — ขยาย item ให้ครอบ 2 คอลัมน์

ใช้ @media (max-width: 768px) เปลี่ยน Layout เป็น 1 คอลัมน์บน Mobile เพื่อให้หน้าเว็บ Responsive รองรับทุกขนาดหน้าจอ

ES2022 ที่ใช้บ่อยใน Stack นี้

JavaScript มาตรฐานสมัยใหม่ (ES2015+ หรือที่เรียกรวมว่า ESNext/ES2022) มีฟีเจอร์ที่ทำให้ เขียน code ได้กระชับและอ่านง่ายขึ้นมาก ทุก Stack ที่ใช้ในรายวิชา (React, Hono, Supabase) ล้วนพึ่งพาฟีเจอร์เหล่านี้:

  • Arrow Functions — ไวยากรณ์กระชับสำหรับฟังก์ชัน const add = (a, b) => a + b ต่างจาก function ปกติตรงที่ไม่มี this binding ของตัวเอง จึงเหมาะมากสำหรับ callback ใน array methods และ event handlers
  • Destructuring — แตกค่าออกจาก Object หรือ Array ด้วย const { name, price } = service (object) และ const [first, ...rest] = services (array) ทำให้ไม่ต้องเข้าถึง property ซ้ำๆ ด้วย dot notation
  • Spread & Rest Operators (...) — คัดลอกและรวม Object/Array โดยไม่ mutate ต้นฉบับ ใช้มากใน React state updates: const updated = { ...service, price: 500 }
  • Optional Chaining (?.) — เข้าถึง nested properties อย่างปลอดภัย โดยไม่เกิด TypeError หาก property ใด undefined: user?.profile?.phone
  • Nullish Coalescing (??) — ให้ค่า default เมื่อ null หรือ undefined: phone ?? 'ไม่ระบุ' ต่างจาก || ตรงที่ 0 และ '' ไม่ถูกนับว่าเป็น falsy
  • Template Literals — String ที่ฝัง expression ได้ด้วย backtick: `บริการ ${name} ราคา ${price} บาท` รองรับ multiline โดยไม่ต้องต่อ string ด้วย +
หมายเหตุ

ฟีเจอร์ทั้งหมดนี้ใช้ได้ใน browser ทุกตัวที่ทันสมัย (Chrome, Firefox, Safari, Edge) และ Node.js v14+ รวมถึง Cloudflare Workers ไม่ต้องใช้ Babel transpile แล้วในปัจจุบัน

async/await และ Fetch API

JavaScript เป็น Single-threaded ใช้ Asynchronous เพื่อทำงานที่ใช้เวลา เช่น ดึงข้อมูลจาก API โดยไม่บล็อก UI Promises เป็นกลไกพื้นฐาน และ async/await เป็น syntax ที่ทำให้เขียนและอ่าน Promise ได้ง่ายขึ้น:

  • async function — ประกาศฟังก์ชันที่ทำงาน asynchronous ฟังก์ชัน async จะ return Promise เสมอ
  • await — หยุดรอผล Promise ก่อนทำบรรทัดถัดไป ใช้ได้เฉพาะภายใน async function หรือ top-level module
  • try / catch — จัดการ Error ที่เกิดจาก await หาก fetch ล้มเหลวหรือ server คืน error ต้องจับด้วย catch

Fetch API เป็น built-in ใน browser สำหรับส่ง HTTP request ทำงานด้วย Promises และใช้ร่วมกับ async/await ได้ตรงๆ: const response = await fetch('/api/services') จากนั้นแปลงเป็น JSON ด้วย await response.json()

ข้อควรระวัง

fetch จะไม่ throw Error เมื่อ server ตอบ 4xx หรือ 5xx ต้องตรวจสอบ response.ok หรือ response.status ด้วยตัวเอง แล้ว throw new Error(...) เพื่อให้ catch จัดการได้ถูกต้อง

ES Modules: import / export

ES Modules คือระบบ module มาตรฐานของ JavaScript ที่ช่วยแบ่งโค้ดออกเป็นไฟล์ย่อย แต่ละไฟล์มี scope ของตัวเอง ไม่มีการปนกันของตัวแปร React, Hono และทุกเครื่องมือใน stack นี้ล้วนใช้ระบบ module นี้:

  • Named exportexport const BASE_URL = '...' และ export function getServices() {} import ด้วย import { getServices, BASE_URL } from './services.js'
  • Default exportexport default function App() {} import ด้วย import App from './App.js' (ตั้งชื่อเองได้)
  • ใช้ใน browser — เพิ่ม type="module" บน script tag: <script type="module" src="main.js"></script> browser จะโหลด dependency ทั้งหมดอัตโนมัติ

ข้อสำคัญ: ES Modules ต้องรันผ่าน HTTP server เท่านั้น ไม่สามารถเปิดไฟล์ด้วย file:// โดยตรงได้เพราะ CORS policy ของ browser ในสัปดาห์นี้ใช้ python3 -m http.server 8080 แทน Vite (Vite จะเริ่มใช้จริงตั้งแต่สัปดาห์ที่ 3)

💻 โค้ดตัวอย่าง
modern.js JavaScript
// snippet 1: ES2022 Modern JavaScript
// Arrow functions
const add = (a, b) => a + b;

// Destructuring
const { name, price } = service;
const [first, ...rest] = services;

// Spread operator
const updated = { ...service, price: 500 };

// Optional chaining & nullish coalescing
const phone = user?.profile?.phone ?? 'ไม่ระบุ';

// Template literals
const msg = `บริการ ${name} ราคา ${price} บาท`;
fetch-services.js JavaScript
// snippet 2: Async/Await + Fetch API
async function fetchServices() {
  try {
    const response = await fetch('/api/services');
    if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('โหลดข้อมูลล้มเหลว:', error);
    return [];
  }
}
services.js / main.js JavaScript
// snippet 3: ES Modules
// services.js — export
export const BASE_URL = 'http://localhost:8787';

export async function getServices() {
  const res = await fetch(`${BASE_URL}/api/services`);
  return res.json();
}

// main.js — import
import { getServices, BASE_URL } from './services.js';

const services = await getServices();
console.log(services);

🧪 ปฏิบัติการ Lab 1 — BookEasy: หน้า Landing Page (Static)

สร้างหน้า Landing Page ของ BookEasy ด้วย HTML/CSS บริสุทธิ์ และเพิ่ม JavaScript เบื้องต้น พร้อมทดสอบ ES Modules ยังไม่มี React หรือ Vite ในสัปดาห์นี้

หมายเหตุ

ยังไม่ใช้ Vite ในสัปดาห์นี้ ใช้ python3 -m http.server 8080 เพื่อรัน ES Modules แล้วเปิดเบราว์เซอร์ที่ http://localhost:8080

1
สร้าง project folder และไฟล์ HTML

สร้าง Landing Page ของ BookEasy ด้วย HTML และ CSS บริสุทธิ์ โดยยังไม่มี JavaScript ในขั้นตอนนี้

  • สร้าง folder bookeasy-static/
  • สร้าง bookeasy-static/index.html ที่มี <header> (ชื่อแอป + nav), <main> ที่ประกอบด้วย Hero section, Services section (การ์ด dummy 3 ใบ), และ <footer>
  • สร้าง bookeasy-static/style.css สำหรับ layout ด้วย Flexbox/Grid ครอบคลุม Header, Services grid, Footer
  • เชื่อม CSS เข้ากับ HTML ด้วย <link rel="stylesheet" href="style.css">
เกณฑ์การผ่าน

เปิด http://localhost:8080 แล้วเห็นหน้า Landing Page ที่มีครบ: Header, Hero, Services (3 การ์ด), Footer ใช้ Semantic HTML5 Elements ถูกต้อง และ CSS Grid/Flexbox จัดหน้าเว็บได้อย่างมีระเบียบ

คำแนะนำ

เริ่มจาก HTML skeleton ก่อน (ไม่มี CSS) แล้วค่อยเพิ่ม CSS ทีละส่วน ใช้ grid-template-columns: repeat(3, 1fr) สำหรับ Services grid และ display: flex; justify-content: space-between สำหรับ Header

2
เพิ่ม JavaScript เบื้องต้น

เพิ่ม JavaScript ให้หน้าเว็บแสดงข้อมูล Services แบบ Dynamic โดยดึงจาก mock array แทนการ hardcode ใน HTML

  • สร้าง bookeasy-static/main.js
  • เพิ่มฟังก์ชัน async function loadServices() ที่ใช้ hardcode array แทน fetch เช่น const services = [{ name: 'นวดไทย', price: 350 }, ...]
  • ใช้ document.querySelector และ innerHTML หรือ appendChild เพิ่ม card ลงใน DOM
  • เรียกใช้ loadServices() เมื่อหน้าโหลดเสร็จ
เกณฑ์การผ่าน

หน้า Services แสดง card จาก JavaScript array ไม่ใช่ HTML hardcode — ลบข้อมูลใน array แล้ว Reload หน้าต้องไม่เห็น card เลย และเพิ่มข้อมูลใหม่ใน array แล้ว Reload ต้องเห็น card เพิ่มขึ้น

คำแนะนำ

ใช้ Arrow Function และ Template Literal ที่เรียนมา: services.forEach(s => { container.innerHTML += `<div class="card">${s.name}</div>` }) หรือจะใช้ map().join('') ก็ได้ ใส่ await ไว้เพื่อเตรียม refactor เป็น fetch จริงในภายหลัง

3
ทดสอบ ES Modules

แยกโค้ดออกเป็น module และใช้ ES Modules import / export เพื่อฝึกโครงสร้างที่ใช้จริงในโปรเจกต์ React/Hono

  • สร้าง bookeasy-static/services.js สำหรับ mock data และ helper functions
  • ใน services.js ประกาศ export const MOCK_SERVICES = [...] และ export function renderCards(data) {}
  • แก้ main.js ให้ import จาก services.js: import { MOCK_SERVICES, renderCards } from './services.js'
  • แก้ tag <script> ใน HTML เป็น <script type="module" src="main.js"></script>
เกณฑ์การผ่าน

หน้าเว็บยังแสดงผล Services card ได้ปกติหลังเปลี่ยนเป็น module และในแท็บ Network ของ DevTools เห็น browser โหลดทั้ง main.js และ services.js แยกกัน ไม่มี Error ใน Console

คำแนะนำ

หาก Console แสดง CORS error ให้ตรวจสอบว่ารันผ่าน python3 -m http.server 8080 แล้ว ไม่ใช่เปิดไฟล์โดยตรงด้วย file:// และตรวจสอบว่า import path ใช้ ./services.js (มี ./ นำหน้าและนามสกุล .js ครบ)