บทเรียน 56 · Frontend · Basic HTML & CSS

เว็บทำงานยังไง — HTML, CSS, JS แบ่งหน้าที่กันคนละอย่าง

~20 นาที · ไม่ต้องมีพื้นฐาน HTML/CSS มาก่อน · ถ้าผ่าน บทเรียน 37 (HTTP server ตัวแรก) มาแล้ว บทนี้จะง่ายกว่าที่คิดมาก

นี่คือบทแรกของฝั่ง frontend ทั้งหมด และข่าวดีคือคุณเรียนครึ่งหนึ่งของเรื่องนี้ไปแล้ว
9 วันที่ผ่านมาคุณเขียนโปรแกรมที่ รับ HTTP request แล้ว ตอบ JSON กลับไป
วันนี้เราแค่ย้ายไปยืนอีกฝั่งของสายเดียวกัน — ฝั่งของคนที่รับของกลับมาแล้วต้องแปลงเป็นภาพบนจอ
บทนี้ตอบสามคำถาม: เบราว์เซอร์ขออะไรและได้อะไรกลับมา · HTML / CSS / JavaScript แบ่งงานกันยังไง · และไฟล์ HTML ที่เล็กที่สุดที่ยังรันได้จริงหน้าตาเป็นยังไง
ทุกกล่องที่มีคำกำกับข้างใต้ว่า "ผลลัพธ์จริง" ในบทนี้ คือหน้าเว็บที่เบราว์เซอร์ของคุณกำลังเรนเดอร์อยู่ตอนนี้จริง ๆ ไม่ใช่ภาพหน้าจอ

🧰 ของที่ใช้ตรวจในบทนี้ (รันวันที่ 27 สิงหาคม 2026)

Chrome (เอนจิน Blink)          # ใช้วัด document.compatMode / characterSet
python3 -m http.server 8756    # static file server ที่ใช้ยิง curl ใส่

ผลรันทุกอันในบทนี้ก็อปมาจาก terminal และจาก DevTools ของจริง คุณทำซ้ำได้ด้วยคำสั่งเดียว: python3 -m http.server 8756 ในโฟลเดอร์ที่มีไฟล์ HTML

1 · คุณอยู่ตรงไหนของสายนี้แล้ว

ใน บทเรียน 37 คุณเขียนโปรแกรม Go ที่นั่งรอ request แล้วตอบกลับด้วย Content-Type: application/json กับ body ที่เป็นตัวอักษร JSON
เว็บเซิร์ฟเวอร์ที่ส่งหน้าเว็บทำเรื่องเดียวกันเป๊ะ ต่างกันแค่สองจุด: Content-Type เป็น text/html และตัวอักษรใน body เป็น HTML แทน JSON

นี่คือของจริง — ผมยิง curl -i ใส่ static file server แล้วก็อปมาทั้งดุ้น:

$ curl -i http://localhost:8756/resume.html

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.14.2
Content-type: text/html
Content-Length: 260

<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Joey's Resume</title>
</head>
<body>
  <h1>Resume</h1>
  <p>Hello, My name is Joey Wasin</p>
</body>
</html>

สังเกตให้ดี: ทั้งก้อนนี้คือ "ตัวอักษร" ไม่มีอะไรเป็นรูปภาพ ไม่มีอะไรเป็นหน้าจอ
สิ่งที่วิ่งอยู่ในสายคือ byte เรียงกัน 260 ตัว — เหมือนตอนที่ server Go ของคุณส่ง {"status":"ok"} ออกไป
คนที่เปลี่ยน byte พวกนี้ให้กลายเป็นภาพคือ เบราว์เซอร์ ไม่ใช่ server

🧰 เทียบให้เห็นภาพ — สั่งเฟอร์นิเจอร์ IKEA

คุณสั่งชั้นวางหนังสือออนไลน์ สิ่งที่รถส่งของเอามาให้คือกล่องแบน ๆ กับกระดาษคู่มือ ไม่ใช่ชั้นวางหนังสือ
คนที่ทำให้มันกลายเป็นชั้นวางหนังสือจริง ๆ คือคุณ ที่ห้องคุณ

server ส่ง คู่มือ (ไฟล์ HTML ที่เป็นตัวอักษร) · เบราว์เซอร์คือคนประกอบ
หน้าเว็บสวย ๆ ที่คุณเห็น ไม่เคยเดินทางผ่านสายเน็ตเลยสักครั้ง มันถูกประกอบขึ้นในเครื่องคุณทุกครั้งที่เปิด

ที่ไม่เหมือนกัน: คู่มือ IKEA ถูกออกแบบให้ประกอบออกมาได้ชิ้นเดียวเป๊ะ ๆ แต่ HTML ชุดเดียวกันประกอบออกมาไม่เหมือนกันได้ — จอมือถือกับจอคอม เบราว์เซอร์คนละยี่ห้อ คนที่ขยายตัวอักษรไว้ 150% ได้หน้าตาต่างกันหมด และนั่นคือเรื่องปกติ ไม่ใช่บั๊ก

2 · หนึ่งหน้าเว็บ ไม่ใช่หนึ่ง request

จุดนี้คือจุดที่คนมาจากฝั่ง backend เข้าใจผิดบ่อยที่สุด เพราะ endpoint ที่คุณเขียนมาทั้งหมด คือ "หนึ่ง request หนึ่ง response" จบในตัว
หน้าเว็บไม่ใช่แบบนั้น — ไฟล์ HTML ที่ได้มาเป็นแค่ใบสั่งของ พออ่านเจอบรรทัดที่ชี้ไปหาไฟล์อื่น เบราว์เซอร์จะยิง request เพิ่มเองอัตโนมัติ โดยที่คุณไม่ได้สั่ง

<!-- page.html -->
<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8" />
  <title>Joey's Resume</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <h1>Resume</h1>
  <img src="logo.svg" alt="logo" />
  <script src="app.js"></script>
</body>
</html>

เปิดหน้านี้หนึ่งครั้ง เบราว์เซอร์ยิง HTTP request ไปที่ server กี่ครั้ง?

4 ครั้ง — นี่คือรายการจริงจากแท็บ Network ของ DevTools:

GET http://localhost:8756/page.html  → 200 OK
GET http://localhost:8756/style.css  → 200 OK
GET http://localhost:8756/logo.svg   → 200 OK
GET http://localhost:8756/app.js     → 200 OK

คุณพิมพ์ URL ไปครั้งเดียว แต่ได้ request 4 ครั้ง
request แรกคือของที่คุณสั่ง · อีก 3 ครั้งเบราว์เซอร์สั่งเพิ่มเองหลังอ่าน HTML จบ เพราะเจอ href กับ src ที่ชี้ไปหาไฟล์อื่น

ผลตามมาที่ต้องรู้ไว้ตั้งแต่วันนี้: ทุกไฟล์ที่คุณแนบเข้าไปในหน้า คือ request หนึ่งครั้งเสมอ หน้าเว็บที่แนบไฟล์ 60 ไฟล์ = 61 request ต่อคนเข้า 1 คน — และนั่นคือเหตุผลที่มีอาชีพชื่อ "performance engineer" อยู่บนโลก

🗣 ศัพท์ที่เพิ่งเรียนไป
Content-Type
header ที่บอกว่า body ที่ส่งมาเป็นของชนิดไหน — application/json ที่คุณใช้มาตลอด กับ text/html ของหน้าเว็บ ต่างกันแค่บรรทัดนี้บรรทัดเดียว · ถาม senior ได้ว่า “endpoint นี้ตอบ Content-Type อะไร” ก่อนถามว่า “ตอบอะไรกลับมา” เพราะคำตอบแรกบอกได้เลยว่าฝั่งรับต้องเอาไปทำอะไรต่อ
subresource (ไฟล์ลูก)
ไฟล์ที่เบราว์เซอร์โหลดตามมาเองเพราะ HTML ชี้ไปหา — CSS, รูป, ฟอนต์, สคริปต์ · พิมพ์ถาม AI ได้ว่า “list every subresource this page will request on first load” จะได้รายการที่ตรงกับแท็บ Network มากกว่าการนั่งไล่อ่านเอง
static file server
server ที่ไม่ได้คิดอะไรเลย มีหน้าที่หยิบไฟล์จากดิสก์ส่งออกไปตาม path ที่ขอมา · ตรงข้ามกับ API server ที่คุณเขียนมา ซึ่งประกอบ response ขึ้นใหม่ทุกครั้ง · ประโยคที่ใช้ได้จริง: “ตรงนี้ต้องการแค่ static file server ไม่ต้องมี backend”

3 · สามภาษา สามหน้าที่ — และมันแยกกันได้จริง

สไลด์สรุปไว้สั้นมากว่า HTML + CSS + JavaScript = Web แล้วให้ภาพมาสามภาพ: ตึกที่กำลังก่อสร้าง (โครงเหล็ก ยังไม่มีผนัง) · ห้องนั่งเล่นที่ตกแต่งเสร็จแล้ว · และสวิตช์ไฟบนผนัง
สามภาพนี้ไม่ใช่ภาพประกอบสวย ๆ มันคือคำนิยามทั้งหมดของสามภาษา:

ภาษาตอบคำถามว่าถ้าไม่มีมัน
HTML
Structure / Content
หน้านี้มีอะไรอยู่บ้าง และอะไรอยู่ในอะไร ไม่มีหน้าเว็บ — ไม่มีอะไรให้แต่งตั้งแต่แรก
CSS
Styling / Interior
ของพวกนั้นหน้าตาเป็นยังไง วางตรงไหน สีอะไร ยังอ่านได้ครบทุกตัวอักษร แค่หน้าตาเป็นค่าเริ่มต้นของเบราว์เซอร์
JavaScript
Function / Interaction
พอผู้ใช้ทำอะไรแล้วเกิดอะไรขึ้น หน้ายังอ่านได้ แค่กดแล้วไม่มีอะไรเปลี่ยน
🧰 เทียบให้เห็นภาพ — สร้างบ้านหนึ่งหลัง

HTML คือโครงสร้าง — เสา คาน พื้น ผนัง ช่องประตู บ้านที่มีแค่โครงยังเข้าไปเดินได้ ยังรู้ว่าห้องไหนเป็นห้องไหน แค่มันดิบ ๆ
CSS คือการตกแต่งภายใน — สีผนัง พื้นไม้ โคมไฟ ตำแหน่งโซฟา
JavaScript คือสวิตช์ไฟกับรีโมทแอร์ — ของที่ "ทำอะไรบางอย่าง" ตอนมีคนไปกด

ลำดับนี้สำคัญ: ตกแต่งภายในบ้านที่ยังไม่มีผนังไม่ได้ · ติดสวิตช์ไฟบนอากาศไม่ได้
ทุกอย่างเกาะอยู่บน HTML — นี่คือเหตุผลที่คอร์สนี้สอน HTML ก่อน

ที่ไม่เหมือนกัน: บ้านจริงถ้าไม่ทาสี มันก็ยังเป็นบ้านที่อยู่ได้ แต่หน้าเว็บที่ไม่มี CSS ยังใช้งานได้ 100% — คลิกลิงก์ได้ กรอกฟอร์มได้ ส่งข้อมูลได้ครบ โปรแกรมอ่านหน้าจอของคนตาบอดอ่านมันได้เหมือนเดิมทุกตัวอักษร ความแตกต่างระหว่าง "ไม่มี CSS" กับ "ไม่มีผนัง" ห่างกันเยอะกว่าที่การเปรียบเทียบนี้ทำให้รู้สึกมาก

พูดเฉย ๆ ไม่พิสูจน์อะไร ลองดูของจริง — นี่คือ HTML ล้วน ๆ ไม่มี CSS เลยแม้แต่บรรทัดเดียว:

<h1>Joey Wasin</h1>
<p>Software Engineer ที่ ODDS</p>
<ul>
  <li>Go</li>
  <li>HTML</li>
</ul>
<a href="https://example.com">ดูผลงาน</a>

ขี้เหร่ แต่ครบ — หัวข้อตัวใหญ่ ลิสต์มีจุดนำหน้า ลิงก์เป็นสีน้ำเงินขีดเส้นใต้และกดได้
ของพวกนี้ไม่ได้มาจากไหน มันคือค่าเริ่มต้นที่เบราว์เซอร์แถมมาให้ทุกแท็ก
ทีนี้เติม CSS 6 บรรทัดลงไป โดยไม่แตะ HTML แม้แต่ตัวเดียว:

<style>
  h1 { color: #7c4dff; font-size: 22px; }
  p  { color: #666; }
  ul { list-style: none; padding: 0; }
  li { display: inline-block; background: #eee;
       padding: 4px 10px; border-radius: 99px; }
  a  { color: #0a7; text-decoration: none; }
</style>

<h1>Joey Wasin</h1>
<p>Software Engineer ที่ ODDS</p>
<ul>
  <li>Go</li>
  <li>HTML</li>
</ul>
<a href="https://example.com">ดูผลงาน</a>
✅ สิ่งที่เพิ่งพิสูจน์ไป

สองกล่องข้างบน HTML เหมือนกันทุกตัวอักษร ต่างกันแค่มี <style> เพิ่มมาก้อนเดียว
นี่คือคำว่า "แยกหน้าที่กัน" ของจริง — ไม่ใช่คำพูดสวย ๆ ในสไลด์
และมันแปลว่าคุณเปลี่ยนหน้าตาเว็บทั้งเว็บได้โดยไม่แตะเนื้อหาเลย ซึ่งเป็นเหตุผลที่ปุ่ม "โหมดมืด" มีอยู่ได้

🗣 ศัพท์ที่เพิ่งเรียนไป
markup language
ภาษาที่ไม่ได้ใช้ "สั่งให้ทำ" แต่ใช้ "ติดป้ายว่านี่คืออะไร" — HTML ไม่มี if ไม่มี loop เพราะมันไม่ใช่ภาษาโปรแกรม · ถ้ามีคนบอกว่า “เขียนโปรแกรมด้วย HTML” ให้รู้ว่าเขาใช้คำผิด และเป็นสัญญาณว่าอาจสื่อสารกันคนละเรื่องอยู่
render / rendering engine
ขั้นตอนที่เบราว์เซอร์เอา HTML + CSS มาคำนวณว่าอะไรควรอยู่พิกัดไหน สีอะไร แล้ววาดลงจอ · ตัวที่ทำงานนี้ใน Chrome ชื่อ Blink ใน Safari ชื่อ WebKit · พิมพ์ถาม AI ได้ว่า “why does this render differently in Safari and Chrome” คำว่า render ทำให้คำถามแคบลงกว่าคำว่า “ทำไมมันเพี้ยน”
default styles (user-agent stylesheet)
CSS ที่เบราว์เซอร์แถมมาให้ทุกแท็กตั้งแต่ก่อนคุณเขียนอะไร — ที่มาของ <h1> ตัวใหญ่ และลิงก์สีน้ำเงิน · ถาม senior ได้ว่า “อันนี้เป็นสไตล์ที่เราเขียน หรือเป็น user-agent stylesheet” เวลาเจอสไตล์ที่หาต้นตอในโปรเจกต์ไม่เจอ

4 · ไฟล์ HTML ที่เล็กที่สุดที่ยังใช้งานได้จริง

สไลด์ให้โครงนี้มา และมันคือโครงที่คุณจะพิมพ์ซ้ำไปตลอดชีวิตการทำงาน เซฟเป็นไฟล์ชื่อ resume.html แล้วดับเบิลคลิกได้เลย ไม่ต้องมี server ไม่ต้อง build อะไรทั้งนั้น:

<!DOCTYPE html>
<html lang="th">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Joey's Resume</title>
  </head>
  <body>
    <h1>Resume</h1>
    <p>Hello, My name is Joey Wasin</p>
  </body>
</html>
บรรทัดมีไว้ทำอะไรลืมแล้วเกิดอะไร
<!DOCTYPE html> บอกเบราว์เซอร์ว่า "อ่านไฟล์นี้ด้วยกฎยุคปัจจุบัน" เบราว์เซอร์สลับไปโหมดเลียนแบบของเก่า (ดูกล่องทายผลข้างล่าง)
<html lang="th"> รากของทั้งเอกสาร · lang บอกภาษาของเนื้อหา โปรแกรมอ่านหน้าจอออกเสียงผิดภาษา · เบราว์เซอร์เสนอแปลหน้าผิด ๆ
<head> ที่เก็บของที่ไม่ได้แสดงบนหน้า — ชื่อแท็บ, การเชื่อม CSS, ข้อมูลกำกับไฟล์ เบราว์เซอร์สร้าง <head> ให้เองแล้วเก็บ meta/title เข้าไปให้ หน้าไม่พัง — แต่พังทันทีถ้าลำดับผิด เอา <h1> ไปวางก่อน <title> เมื่อไหร่ ของที่เหลือตกไปอยู่ใน <body> หมด (ตรวจกับ Chrome แล้ว)
<meta charset="UTF-8"> บอกว่าตัวอักษรในไฟล์นี้เข้ารหัสแบบไหน ภาษาไทยกลายเป็นตัวขยะทั้งหน้า — มีผลรันจริงอยู่ข้างล่าง
<meta name="viewport" …> บอกมือถือว่า "ใช้ความกว้างจอจริง อย่าย่อหน้าเว็บให้เล็กจิ๋ว" เปิดบนมือถือแล้วตัวหนังสือเล็กเท่ามด ต้องซูมเอง
<title> ชื่อที่ขึ้นบนแท็บเบราว์เซอร์และในผลค้นหา Google แท็บโชว์ชื่อไฟล์หรือ URL ดิบ ๆ แทน
<body> ทุกอย่างที่มองเห็นบนหน้าอยู่ในนี้ เบราว์เซอร์สร้างให้เองเหมือนกัน · แต่ถ้าไม่มีอะไรอยู่ข้างในเลย ก็ไม่มีอะไรให้ดู

ส่วนที่มองเห็นจริง ของไฟล์นั้นมีแค่สองบรรทัดใน <body> ที่เหลือทั้งหมดเป็นงานหลังบ้าน:

<h1>Resume</h1>
<p>Hello, My name is Joey Wasin</p>

ทำไมกล่องข้างบนถึงมีแค่ส่วนใน <body>? เพราะกล่อง "ผลลัพธ์จริง" ในเว็บนี้คือหน้าเว็บซ้อนอยู่ในหน้าเว็บ (<iframe>) มันมี <!DOCTYPE> · <html> · <head> ของตัวเองอยู่แล้ว
อยากเห็นไฟล์เต็ม ๆ ทำงานจริงต้องเซฟเป็นไฟล์แล้วเปิดเอง ซึ่งคือข้อ 1 ของภารกิจท้ายบท

⚠️ ลืม <meta charset="UTF-8"> แล้วเป็นแบบนี้จริง ๆ

ผมเซฟไฟล์เดียวกันสองเวอร์ชัน (เป็น UTF-8 ทั้งคู่) ต่างกันแค่มี/ไม่มีบรรทัด meta charset แล้วเสิร์ฟผ่าน server ที่ไม่ได้บอก charset มาใน header — ผลที่ได้จาก Chrome:

# มี <meta charset="UTF-8">
สวัสดีครับ ผมชื่อโจอี้
document.characterSet = UTF-8

# ไม่มี
สวัสดีครับ ผมชื่อโจอี้
document.characterSet = windows-1252

ไฟล์ไม่ได้พัง byte ในไฟล์เหมือนเดิมเป๊ะทุกตัว — เบราว์เซอร์แค่เดาผิดว่าจะแปล byte พวกนั้นเป็นตัวอักษรอะไร
มันเดาเป็น windows-1252 ซึ่งเป็นการเดาแบบยุค 90 ที่ยังติดมาถึงทุกวันนี้
บรรทัดเดียวนี้คือความต่างระหว่าง "เว็บภาษาไทย" กับ "เว็บที่อ่านไม่ออก"

<!-- ไฟล์ ก. -->
<!DOCTYPE html>
<html lang="th"><head><meta charset="UTF-8">
<style>body { font-size: 30px }</style></head>
<body><table><tr><td id="c">ในตาราง</td></tr></table></body></html>

<!-- ไฟล์ ข. — เหมือนกันทุกตัวอักษร แต่ลบบรรทัด <!DOCTYPE html> ออก -->

อ่านค่า font-size ที่คำนวณได้จริงของ <td> ทั้งสองไฟล์ ได้เท่าไหร่?

# ไฟล์ ก. (มี doctype)
compatMode = CSS1Compat   td font-size = 30px

# ไฟล์ ข. (ไม่มี doctype)
compatMode = BackCompat   td font-size = 16px

ไฟล์ ข. ไม่ได้ error อะไรเลย ไม่มีคำเตือน ไม่มีขีดแดง หน้ายังขึ้นปกติ
แต่ <td> ของมันไม่รับ font-size ที่สืบทอดมาจาก body — เพราะเบราว์เซอร์สลับไปใช้กฎของยุคก่อนปี 2000 ที่ตารางไม่สืบทอดฟอนต์

สิ่งที่ผมทดสอบแล้วพบว่า "ไม่ต่าง" ก็มี: ผมลองวัด offsetWidth ของกล่องที่มี width:200px; padding:20px; border:5px ในทั้งสองโหมด — ได้ 250px เท่ากันทั้งคู่ ทั้งที่ตำราเก่าหลายเล่มบอกว่าโหมดเก่าจะได้ 200
Chrome ปี 2026 ปิดช่องว่างตรงนั้นไปแล้ว แต่ยังไม่ได้ปิดทุกช่อง อย่างที่เห็นในเรื่องตาราง

บทเรียนที่แท้จริงจึงไม่ใช่ "ลืม doctype แล้วพังยังไง" — เพราะรายการนั้นเปลี่ยนทุกปี แต่คือ: ไม่มี doctype = คุณเข้าไปอยู่ในโหมดที่ไม่มีใครทดสอบอีกแล้ว พิมพ์บรรทัดเดียวแล้วออกมาซะ

🗣 ศัพท์ที่เพิ่งเรียนไป
element · tag · attribute
<p class="x">สวัสดี</p> ทั้งก้อนคือ element · <p> กับ </p> คือ tag เปิดและปิด · class="x" คือ attribute · ใช้คำว่า element เวลาพูดถึง “ของชิ้นนั้น” และ tag เวลาพูดถึง “ตัวหนังสือที่พิมพ์” แยกสองคำนี้ให้ออกแล้วอ่านเอกสาร MDN ง่ายขึ้นทันที
doctype
บรรทัดแรกสุดของไฟล์ที่ประกาศว่าเอกสารนี้เป็น HTML ยุคปัจจุบัน · ไม่ใช่ tag ไม่ใช่ element เป็นคำประกาศเฉย ๆ · ประโยคที่ใช้ได้จริง: “หน้านี้หลุด standards mode รึเปล่า เช็ค doctype ให้หน่อย”
standards mode / quirks mode
สองโหมดการตีความ CSS ของเบราว์เซอร์ · เช็คได้ทันทีด้วยการพิมพ์ document.compatMode ใน Console — ได้ CSS1Compat คือปกติ ได้ BackCompat คือหลุดไปโหมดเก่า · พิมพ์ถาม AI ได้ว่า “document.compatMode returns BackCompat — what could cause that”

5 · เอา CSS มาต่อกับ HTML — มีสามทาง ใช้จริงทางเดียว

CSS ไม่ได้ลอยไปเกาะ HTML เอง คุณต้องบอกให้เบราว์เซอร์รู้ ว่ามีสไตล์อยู่ตรงไหน มีสามวิธี และควรรู้จักทั้งสามเพื่ออ่านโค้ดคนอื่นออก ถึงแม้จะใช้จริงแค่วิธีเดียว

วิธีเขียนยังไงใช้เมื่อไหร่
1. ไฟล์แยก
external
<link rel="stylesheet" href="style.css"> ใน <head> ค่าเริ่มต้นที่ควรใช้เสมอ — ไฟล์เดียวคุมได้ทุกหน้า และเบราว์เซอร์แคชไว้ใช้ซ้ำได้
2. ฝังในไฟล์
internal
<style> … </style> ใน <head> ไฟล์เดียวจบ เช่นตัวอย่างสอน · หน้าเดียวโดด ๆ · อีเมล HTML
3. ติดที่ตัว element
inline
<p style="color: red"> เกือบไม่ควรใช้ · แก้แล้วต้องไล่หาทีละจุด และไปทับสไตล์อื่นแบบเงียบ ๆ
🧰 เทียบให้เห็นภาพ — กฎการแต่งกายในออฟฟิศ

ไฟล์แยก = ประกาศติดบอร์ดกลางที่ทุกชั้นอ่านร่วมกัน แก้แผ่นเดียวเปลี่ยนทั้งบริษัท
ฝังในไฟล์ = ประกาศติดหน้าห้องประชุมห้องเดียว ใช้ได้เฉพาะห้องนั้น
inline = เดินไปกระซิบบอกพนักงานทีละคน ว่าวันนี้ใส่เสื้อสีอะไร

วิธีที่สามได้ผลทันทีและชัดเจนที่สุด — จนกระทั่งวันที่ต้องเปลี่ยนกฎ แล้วคุณจำไม่ได้ว่ากระซิบใครไปบ้าง

ที่ไม่เหมือนกัน: ในออฟฟิศจริง คำสั่งจากปากหัวหน้ากับประกาศบนบอร์ดขัดกันเมื่อไหร่ ยังเถียงกันได้ว่าอันไหนใหญ่กว่า
แต่ใน CSS มีกฎตายตัวว่าใครชนะ ไม่มีการเถียง และเกือบทุกครั้ง style="" คือฝ่ายที่ชนะประกาศบนบอร์ด (มีทางเดียวที่บอร์ดชนะกลับได้ — เดี๋ยวบอกท้ายหัวข้อนี้) — ซึ่งฟังดูดี จนกระทั่งคุณเป็นคนที่พยายามแก้บอร์ดแล้วหน้าไม่ยอมเปลี่ยน

วิธีที่ 1 ต้องมีไฟล์จริงสองไฟล์ กล่องตัวอย่างในบทนี้ทำให้ดูไม่ได้ (มันเป็นไฟล์เดียวโดด ๆ ต่อไฟล์ภายนอกไม่ได้) — ไปลองเองในภารกิจข้อ 3
ส่วนวิธีที่ 2 กับ 3 ดูได้เลย นี่คือทั้งสองวิธีอยู่ในหน้าเดียวกัน:

<style>
  .box { border: 2px solid #7c4dff; padding: 10px;
         border-radius: 6px; color: #7c4dff; }
</style>

<p class="box">ผมได้สไตล์จากบล็อก style ข้างบน</p>
<p class="box" style="color: crimson; border-color: crimson">
  ผมมี class เดียวกัน แต่ style ที่ติดตัวผมมาชนะ
</p>

ย่อหน้าสองอันมี class="box" เหมือนกัน แต่ได้สีคนละสี
นี่คือครั้งแรกที่คุณเห็น "กฎสองข้อชนกันแล้วต้องมีคนแพ้" ในโลก CSS ซึ่งเป็นเรื่องที่บทหลัง ๆ จะลงรายละเอียดทั้งบท
ตอนนี้จำแค่ว่า: สไตล์ที่ติดกับตัว element ชนะสไตล์ที่เขียนไว้ที่อื่น (มีข้อยกเว้นอยู่หนึ่งอย่างชื่อ !important ซึ่งจะเจอในบทหลัง) และนั่นคือเหตุผลที่มันเป็นวิธีที่คนเกลียดที่สุด — มันแก้ทับของคนอื่นได้โดยที่คนอื่นไม่รู้ตัว

🛠️ ภารกิจ — resume ของคุณเอง แล้วพิสูจน์ด้วยสายตาตัวเอง

เป้าหมายไม่ใช่ "ทำหน้าเว็บให้สวย" — เป้าหมายคือทำให้ทั้งสามอย่างในบทนี้เกิดขึ้นกับมือ ทั้งอันที่ทำงาน และอันที่พัง ถ้าข้ามข้อ 4 กับ 5 ไป ภารกิจนี้จะไม่เหลืออะไรเลย

  1. ไฟล์แรก: สร้างโฟลเดอร์ใหม่ → ไฟล์ resume.html → พิมพ์โครงขั้นต่ำจากหัวข้อ 4 ด้วยมือ ห้ามก็อป (พิมพ์เองรอบเดียวจำได้นานกว่าอ่านสิบรอบ) → ใส่ชื่อจริงของคุณใน <h1> และแนะนำตัวหนึ่งบรรทัดใน <p> เป็นภาษาไทย → ดับเบิลคลิกไฟล์เพื่อเปิดในเบราว์เซอร์
  2. ยืนยันว่าอยู่ในโหมดที่ถูก: กด F12 เปิด DevTools → แท็บ Console → พิมพ์ document.compatMode → ต้องได้ "CSS1Compat"
    ถ้าได้ "BackCompat" แปลว่าบรรทัด <!DOCTYPE html> ของคุณหาย หรือมีอะไรอยู่ข้างหน้ามัน
  3. แยก CSS ออกเป็นไฟล์: สร้าง style.css ข้าง ๆ กัน ใส่กฎอะไรก็ได้สองสามข้อ (เช่น h1 { color: teal }) → เชื่อมด้วย <link rel="stylesheet" href="style.css"> ใน <head> → รีเฟรช สีต้องเปลี่ยน
    เปิดแท็บ Network ของ DevTools แล้วรีเฟรชอีกครั้ง — ต้องเห็น 2 รายการ คือ resume.html กับ style.css (นี่คือหัวข้อ 2 เกิดขึ้นตรงหน้าคุณ)
  4. ทำให้ภาษาไทยพังโดยตั้งใจ: ลบบรรทัด <meta charset="UTF-8"> ออก → รีเฟรช
    ถ้าเปิดจากไฟล์ตรง ๆ (file://) แล้วมันยังไม่พัง ถือว่าปกติ — เบราว์เซอร์เดาถูกพอดี ให้ลองใหม่ผ่าน server ด้วย python3 -m http.server 8000 แล้วเปิด http://localhost:8000/resume.html
    จดไว้ว่าคุณเห็นอะไร แล้วใส่บรรทัดนั้นกลับ
  5. ทำให้ doctype พังโดยตั้งใจ: ลบ <!DOCTYPE html> ออก → รีเฟรช → พิมพ์ document.compatMode ใน Console อีกครั้ง
    สังเกตว่าหน้ายังดูปกติทุกอย่าง — นี่คือประเด็นทั้งหมด: บั๊กประเภทนี้ ไม่ประกาศตัว มันรอ
    แล้วใส่กลับ
  6. เขียนสรุปสามบรรทัด ไว้ท้ายไฟล์ในคอมเมนต์ <!-- … -->: ข้อ 4 ทำให้เกิดอะไร · ข้อ 5 ทำให้เกิดอะไร · และข้อไหน "น่ากลัวกว่า" ในความเห็นคุณ พร้อมเหตุผลหนึ่งประโยค

ข้อ 5 น่ากลัวกว่ามาก

ข้อ 4 (ลืม charset) ตะโกนใส่หน้าคุณทันที — เห็นตัวขยะเต็มจอ แก้ภายในสิบวินาที ไม่มีทางหลุดขึ้น production

ข้อ 5 (ลืม doctype) เงียบสนิท หน้าดูปกติทุกอย่าง คุณส่งงาน ผ่านรีวิว ขึ้น production แล้วสามเดือนต่อมามีคนบอกว่า "ตารางในหน้านั้นฟอนต์แปลก ๆ นะ" แล้วทีมนั่งหาสามชั่วโมง
บั๊กที่แสดงอาการทันทีคือบั๊กที่ถูกที่สุดในชีวิต — บั๊กที่รอ คือบั๊กที่แพงที่สุด

6 · เช็คความเข้าใจ

1. server ส่งไฟล์ HTML ก้อนหนึ่งกลับมาที่เบราว์เซอร์ — ตอนที่ byte ชุดนั้นวิ่งอยู่ในสายเน็ต มันคืออะไร?

ผลจริงจาก curl -i ในหัวข้อ 1 ยืนยันข้อนี้: response body คือตัวอักษร 260 ตัว ที่คุณอ่านออกด้วยตาเปล่า และ Content-Type: text/html แค่บอกว่า "ตัวอักษรพวกนี้ควรถูกตีความแบบ HTML"
ตัวเลือกที่ 1 คือความเข้าใจผิดที่พบบ่อยที่สุด — ถ้าจริง เว็บจะเปลี่ยนขนาดตามจอไม่ได้เลย และ Ctrl+F หาคำในหน้าก็ทำไม่ได้
ตัวเลือกที่ 2 ผิดตรงคำว่า "พิกัด" — server ไม่รู้เลยว่าจอคุณกว้างเท่าไหร่ การคำนวณพิกัดเกิดในเครื่องคุณล้วน ๆ
ตัวเลือกที่ 4 ใกล้เคียงแต่ผิดชนิด: HTML ไม่ใช่ชุดคำสั่ง มันคือการติดป้ายว่า "อันนี้คือหัวข้อ อันนี้คือย่อหน้า" ส่วนจะวาดยังไงเบราว์เซอร์ตัดสินเอง
จุดที่ต้องจำ: server ส่ง "ความหมาย" ไม่ได้ส่ง "ภาพ" — ทุกครั้งที่หน้าเว็บแสดงผลเพี้ยนเฉพาะบางเครื่อง ให้เริ่มหาที่ฝั่งเบราว์เซอร์ก่อนเสมอ

2. หน้าเว็บหน้าหนึ่งอ้างถึงไฟล์ CSS 1 ไฟล์ รูป 3 รูป และสคริปต์ 2 ไฟล์ — เปิดหน้านี้หนึ่งครั้งเกิด HTTP request กี่ครั้ง?

นับตรง ๆ: 1 (HTML) + 1 (CSS) + 3 (รูป) + 2 (สคริปต์) = 7
ในหัวข้อ 2 ผมรันของจริงกับหน้าที่อ้าง 3 ไฟล์ แล้วได้ 4 รายการในแท็บ Network ตรงตามสูตรนี้
ตัวเลือกที่ 1 กับ 4 คิดว่ามีการ "มัดรวม" ซึ่งไม่มีในโปรโตคอล — HTTP รู้จักแค่ "ขอหนึ่งอย่าง ได้หนึ่งอย่าง" เหมือน endpoint ที่คุณเขียนมาทั้งหมด
ตัวเลือกที่ 3 ลืมนับตัวหน้าเอง ซึ่งก็เป็น request หนึ่งครั้งเต็ม ๆ ไม่ต่างจากไฟล์อื่น
จุดที่ต้องจำ: ทุกไฟล์ที่คุณเพิ่มเข้าไปในหน้า = อีกหนึ่ง request ต่อผู้ใช้หนึ่งคนเสมอ ตัวเลขนี้คือสิ่งแรกที่ต้องดูเวลามีคนบอกว่า "เว็บช้า"

3. เพื่อนส่งไฟล์ HTML มาให้ช่วยดู หน้าแสดงผลปกติทุกอย่าง แต่พิมพ์ document.compatMode ใน Console แล้วได้ "BackCompat" — แปลว่าอะไร?

ผลรันจริงในหัวข้อ 4: ไฟล์เดียวกันสองเวอร์ชัน ต่างกันแค่บรรทัด doctype → ได้ CSS1Compat กับ BackCompat ตามลำดับ และ <td> ในเวอร์ชันหลังไม่รับ font-size ที่สืบทอดมา (16px แทน 30px)
ตัวเลือกที่ 1 ผิดชนิดของปัญหา — CSS ที่พังจะถูกข้ามเป็นรายบรรทัด ไม่เกี่ยวกับโหมดของเอกสาร
ตัวเลือกที่ 2 ผิดตรงที่คิดว่าโหมดขึ้นกับวิธีเปิด — โหมดมาจากเนื้อไฟล์อย่างเดียว เปิดยังไงก็ได้ค่าเดิม
ตัวเลือกที่ 3 กลับหัวกลับหาง: เบราว์เซอร์ใหม่ต่างหากที่ยังต้องรองรับโหมดเก่านี้ เพื่อไม่ให้เว็บอายุ 25 ปีพังหมดโลก
จุดที่ต้องจำ: "หน้าดูปกติ" ไม่ใช่หลักฐานว่าไม่มีปัญหา — document.compatMode คือการเช็คหนึ่งวินาทีที่ควรทำทุกครั้งที่รับไฟล์ HTML แปลกหน้ามาดูแล

4. ทีมขอให้เปลี่ยนสีปุ่มทั้งเว็บ 40 หน้า จากน้ำเงินเป็นเขียว — วิธีแนบ CSS แบบไหนที่ทำให้งานนี้เป็นฝันร้าย?

inline คือการ "กระซิบทีละคน" ตามการเปรียบเทียบในหัวข้อ 5 — ปุ่ม 300 ตัวคือ 300 จุดที่ต้องแก้ และถ้าพลาดไปตัวเดียวจะไม่มีอะไรเตือนคุณเลย
ซ้ำร้ายกว่านั้น: ต่อให้คุณไปเพิ่มกฎสีเขียวในไฟล์กลางเรียบร้อย ปุ่มที่มี style="" ก็ยังเป็นสีน้ำเงินอยู่ดี เพราะมันชนะกฎกลาง (เห็นได้จากกล่องผลลัพธ์จริงในหัวข้อ 5 ที่ย่อหน้าสองอันมี class เดียวกันแต่คนละสี)
ตัวเลือกที่ 2 กลับข้าง — ไฟล์แยกคือกรณีที่ง่ายที่สุด แก้ที่เดียวจบทั้ง 40 หน้า
ตัวเลือกที่ 3 อ้างเรื่องแคชผิดทาง: <style> ฝังอยู่ในตัวหน้า HTML มันจึงถูกส่งใหม่พร้อมหน้าทุกครั้ง ไฟล์แยกต่างหากที่ถูกแคช (และแก้ได้ด้วยการเปลี่ยนชื่อไฟล์ ไม่ใช่ปัญหาที่แก้ไม่ได้)
จุดที่ต้องจำ: เลือกวิธีแนบ CSS ด้วยคำถามว่า "วันที่ต้องแก้ ต้องไปแก้กี่ที่" ไม่ใช่ "วันนี้พิมพ์อันไหนเร็วกว่า"

💬 ติดตรงไหนถามได้ — โดยเฉพาะคำถามประเภท "ทำไมโค้ดผมเหมือนในบทเป๊ะแต่ผลไม่เหมือน" ซึ่งเป็นคำถามที่ดีที่สุดของบทนี้ เพราะคำตอบมักอยู่ที่ charset หรือ path ของไฟล์ ไม่ได้อยู่ที่ตัวโค้ด
ถ้าอยากถาม AI ให้ตรงจุด แปะโค้ดพร้อมประโยคนี้: "this is plain HTML/CSS, no framework — tell me what the browser does with it line by line"

อ้างอิง