🔁 ทวนก่อนเริ่ม — คำจากบทก่อน ๆ ที่บทนี้ใช้
- view · ERB (บท 103) — ไฟล์แม่แบบที่ Rails เติมข้อมูลแล้วได้ HTML ·
<%= %>= วางลงจาน ·<% %>= คำสั่งถึงคนจัด ไม่ลงจาน ·<%# %>= โน้ตของเชฟ @posts= ถาดหน้าครัว (บท 102–103) — controller วางของไว้ view หยิบไปจัดจาน- scaffold (บท 118) — คำสั่งที่สร้าง route · controller · view ครบ 7 action ให้ในทีเดียว · แอปของ Day 3 เริ่มจาก scaffold ดิบ ๆ
- useState · useEffect (บท 73–74) — useState = ความจำของ component · useEffect = งานที่ทำหลังวาดหน้าจอเสร็จ
- curl (บท 103 · 114) — โปรแกรมเปิดหน้าเว็บจากเทอร์มินัล ได้ HTML เป็นข้อความ เห็นสิ่งที่เบราว์เซอร์ได้รับจริงทุกตัวอักษร
🧰 ตรวจด้วย Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 18 (สไลด์และ repo ของอาจารย์ใช้ Ruby 4.0.5–4.0.6 · Rails 8.1.3 — รุ่นแก้บั๊กในสายเดียวกัน ใช้แทนกันได้)
🗂 โค้ดเริ่มต้น: ต่อจากบท 120 — แอป wongnok-blog-all-in-one branch my-day3 (แตกจาก day3-start ของอาจารย์) + ข้อมูลตัวอย่าง 30 บทความที่ commit ไว้แล้ว ·
ตามไม่ทันให้ทำบท 120 ก่อน · บทนี้แก้ไฟล์เพื่อทดลองเท่านั้น จบบทสั่ง git restore คืนทุกอย่าง
ผลทุกก้อนรันจริงแล้วก็อปมา · ตัดบรรทัดที่ไม่เกี่ยวเป็น … · วันเวลา · เวลา (ms) · รหัสยาว ๆ ในฟอร์มของคุณจะไม่ตรงกับในบท
บล็อก A ของ Day 3: ทั้งบล็อกคือเรื่องของ "คนจัดจาน"
Day 3 ช่วงเช้ามี 3 บล็อก · บล็อก A คือ view (คนจัดจาน) · บล็อก B คือ CSS · บล็อก C คือทำให้หน้าเว็บขยับได้ · บล็อก A ยาว 5 บท และจบด้วยหน้า index กับ show ที่อ่านรู้เรื่อง (บท 126)
- 122 (บทนี้) — ทำไม Rails ส่ง HTML ไม่ใช่ JSON · ERB 4 แบบ
- 123 — layout กับ partial (แม่พิมพ์ที่ใช้ซ้ำได้) · 124 — helper กับฟอร์ม · 125 — ช่องโหว่ที่ซ่อนใน view · 126 — Lab
งานเดียวกัน สองเส้นทาง
โจทย์: โชว์รายการบทความทั้งหมดบนหน้าเว็บ (แอปของเรามี 30 ชิ้น) · ฝั่ง FRONT + BACK ของคอร์สนี้สอนให้ทำแบบ ① · Rails ทำแบบ ② · อ่านจากบนลงล่าง = ข้อมูลเดินทางจากฐานข้อมูลไปถึงตาคน (ภาพจากสไลด์หน้า 10)
คำในภาพสองคำ: render = แปลงข้อมูลให้กลายเป็นหน้าตา (HTML) · DOM = หน้าเว็บที่เบราว์เซอร์อ่าน HTML แล้วสร้างเก็บไว้ในหน่วยความจำ ก่อนวาดออกจอ (ตัวที่เห็นในแท็บ Elements ของ DevTools) · บทนี้ให้คิดว่ามันคือ "จานที่วางอยู่บนโต๊ะแล้ว" พอ
ฝั่ง ① server ส่ง JSON ออกไป — ข้อมูลล้วน ๆ แบบที่ Go ส่งในบท 38 ยังไม่มีหน้าตา · แล้วโค้ด React ที่รันในเบราว์เซอร์ต้องรับมา เก็บไว้ แล้ววาดเอง
ฝั่ง ② server ทำทุกอย่างจนเสร็จ ส่ง HTML ที่พร้อมโชว์ออกไป · เบราว์เซอร์แค่เอาไปแสดง ·
ไม่มีใครต้อง "เรียก API" เพราะ controller กับ view อยู่ในแอปเดียวกัน ส่งของกันผ่าน @posts ได้เลย — ชั้น API ไม่ได้หายไปไหน แค่ไม่จำเป็นต้องมี
แบบ ② (Rails) เหมือนร้านอาหารทั่วไป: ครัวปรุงและจัดจานเสร็จ พนักงานยกจานพร้อมกินไปวางที่โต๊ะ · ลูกค้าแค่กิน
แบบ ① เหมือนร้านหมูกระทะ: ครัวส่งวัตถุดิบดิบ (JSON) ไปที่โต๊ะ · โต๊ะต้องมีเตาของตัวเอง (React) และสมุดจดว่าบนโต๊ะมีอะไรเหลือบ้าง (state) แล้วปรุงเอง
ที่ไม่เหมือนกัน: ในร้านหมูกระทะ ลูกค้าต้องปรุงเองจนเหนื่อย · แต่ในเว็บ "เตาบนโต๊ะ" คือโค้ด JavaScript ที่ทีม frontend เขียนไว้ให้ ลูกค้าไม่ได้ทำอะไรเพิ่ม · คนที่เหนื่อยจริงคือทีมที่ต้องดูแลสองครัว — ครัวหลัง (Go) กับเตาบนโต๊ะ (React)
Next.js รุ่นใหม่ render HTML บน server ได้ด้วย (Server Component — คือ component ที่ไม่มี 'use client' ที่เจอในบท 73) ·
ภาพ ① คือแบบที่ข้อมูลถูก fetch ในเบราว์เซอร์ ซึ่งเป็นแบบที่สไลด์เทียบ และเป็นแบบที่เจอบ่อยเวลา frontend กับ backend แยกทีมกัน
- API layer · JSON API
- ชั้นที่ server ส่งข้อมูลเป็น JSON ให้โปรแกรมอื่นมาเรียกใช้ (เช่น React · แอปมือถือ) · Go handler ในบท 38 คือ JSON API · ถาม senior ได้ว่า “หน้านี้ดึงข้อมูลผ่าน JSON API หรือ server render HTML มาเลย”
- server-rendered HTML · client-side rendering
- server-rendered = server สร้าง HTML ให้เสร็จแล้วส่ง (แบบ ②) · client-side rendering = เบราว์เซอร์รับ JSON แล้วสร้างหน้าจอเอง (แบบ ①) · พิมพ์บอก AI ได้ว่า “this is a Rails app with server-rendered ERB views, not a React SPA — don't add a JSON API for this page”
กล่องที่หายไป: "state ใน client" — ที่เกิดของบั๊ก "ทำไมหน้าจอไม่อัปเดต"
เทียบสองภาพข้างบน กล่องที่ ② ไม่มีคือ ★ state ใน client · ฟังดูเป็นแค่กล่องเดียว แต่สไลด์หน้า 10 บอกว่า บั๊กประเภท "ทำไมหน้าจอไม่อัปเดต" เกิดที่กล่องนี้เกือบทั้งหมด · มาดูว่าทำไม
นี่คือหน้ารายการแบบ ① ด้วยของที่คุณเพิ่งเรียน (ไฟล์ PostList.tsx · สมมติว่า Go รันอยู่ที่พอร์ต 8080 · แค่อ่าน ไม่ต้องพิมพ์ตาม):
'use client'
import { useEffect, useState } from 'react'
type Post = { id: number; title: string }
export default function PostList() {
const [posts, setPosts] = useState<Post[]>([])
useEffect(() => {
fetch('http://localhost:8080/posts')
.then((res) => res.json())
.then((data: Post[]) => setPosts(data))
}, [])
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
)
}
'use client'— บอก Next.js ว่า component นี้รันในเบราว์เซอร์ (บท 73) ·type Post— บอกว่าบทความหนึ่งชิ้นมีidกับtitle(บท 63–64)useState<Post[]>([])— สร้างความจำชื่อpostsเริ่มจากรายการว่าง ← นี่คือ ★ state ใน clientuseEffect(() => { … }, [])— หลังวาดจอครั้งแรก ให้ทำงานข้างใน ครั้งเดียว ([]= ไม่ต้องทำซ้ำ · บท 74)fetch(…)— คำสั่งของเบราว์เซอร์สำหรับขอข้อมูลจาก URL (คล้ายcurlแต่สั่งจากในโค้ด JavaScript) · ตรงนี้ขอ JSON จาก Go.then(…)— "ได้ของมาแล้วค่อยทำต่อ" (ต้องรอเพราะของเดินทางผ่านเน็ต) ·(res) => res.json()= ฟังก์ชันสั้น ๆ ที่รับคำตอบresแล้วแปลงข้อความ JSON เป็นข้อมูลที่ใช้ได้setPosts(data)— จดรายการบทความลงความจำ → React วาดจอใหม่posts.map(…)— วนทุกบทความ สร้าง<li>หนึ่งอันต่อหนึ่งชิ้น (key= ป้ายประจำแต่ละแถว ให้ React จำได้)
ตอนนี้ข้อมูลบทความมี สองชุด: ชุดจริงในฐานข้อมูล กับชุดที่ก็อปมาไว้ในหน่วยความจำของแท็บนี้ ·
ถ้าเพื่อนเปิดอีกแท็บแล้วแก้ชื่อบทความ — ฐานข้อมูลเปลี่ยนแล้ว แต่ posts ในแท็บของคุณไม่มีใครบอกมัน ·
useEffect ก็สั่งไว้ให้ fetch แค่ครั้งเดียว จอจึงโชว์ชื่อเก่าต่อไป
ทีมที่ใช้แบบ ① จึงต้องเขียนโค้ดเพิ่มเพื่อตามล้างชุดที่สอง: แก้เสร็จแล้ว fetch ใหม่ · ทุกกี่วินาทีไปถามใหม่ · หน้าไหนใช้ข้อมูลเดียวกันต้องบอกกัน — ลืมจุดเดียว = บั๊ก
ร้านหมูกระทะจดของบนโต๊ะไว้ในสมุดเล่มเล็ก "หมูสามชั้นเหลือ 2 จาน" · แต่ครัวเพิ่งประกาศว่าหมูสามชั้นหมดแล้ว ·
สมุดบนโต๊ะไม่รู้ ยังเขียนว่า 2 จาน ลูกค้าสั่งไปก็ได้คำตอบว่าไม่มี · ปัญหาไม่ได้อยู่ที่ครัวหรือโต๊ะ แต่อยู่ที่มีสมุดสองเล่มที่ต้องตรงกัน
ร้านแบบ ② (Rails) ไม่มีสมุดบนโต๊ะ · ทุกครั้งที่ลูกค้าขออะไร ครัวเปิดสมุดเล่มเดียวของครัวแล้วจัดจานใหม่
ที่ไม่เหมือนกัน: อย่าเข้าใจว่าหน้า Rails อัปเดตเองได้ · ผมเปิดหน้า /posts/1 สองแท็บใน Chrome จริง แก้ชื่อจากแท็บที่สอง —
แท็บแรกก็ยังโชว์ชื่อเก่าเหมือนกัน จนกดรีเฟรชถึงเห็นชื่อใหม่ ·
ที่ต่างคือแค่คลิกหรือรีเฟรชก็ได้ของล่าสุดจากครัวทันที ไม่มีสมุดเล่มที่สองให้ลืมล้าง ·
ทำให้แท็บแรกเด้งเองได้โดยไม่ต้องมีสมุดเล่มที่สอง คือเรื่องของบล็อก C (บท 133)
นี่คือประโยคหลักข้อแรกของ Day 3 (สไลด์หน้า 8): เว็บที่โต้ตอบได้ไม่จำเป็นต้องมี state สองชุด — ถ้า server เป็นเจ้าของข้อมูลชุดเดียว บั๊ก sync ก็ไม่มี · บล็อก C (บท 130) จะพากลับมาที่ประโยคนี้อีกครั้ง
- client state
- ข้อมูลที่ก็อปมาเก็บไว้ในเบราว์เซอร์ (เช่น ใน
useState) · สะดวก แต่ต้องคอยทำให้ตรงกับของจริงบน server · ถาม senior ได้ว่า “ข้อมูลหน้านี้เก็บเป็น client state ไหม แล้วอะไรเป็นตัวสั่งให้ fetch ใหม่” - single source of truth
- ข้อมูลมีเจ้าของเพียงที่เดียว ที่อื่นแค่ขอดู ไม่เก็บสำเนาของตัวเอง · ใน Rails เจ้าของคือฐานข้อมูลบน server · พิมพ์บอก AI ได้ว่า “keep the server as the single source of truth; don't keep a separate copy of posts in client state”
- stale data · sync bug
- stale = ข้อมูลค้าง (เก่าแล้วแต่ยังโชว์) · sync bug = บั๊กที่เกิดเพราะสองชุดไม่ตรงกัน · อาการคลาสสิก "แก้แล้วแต่จออีกหน้ายังเห็นของเก่า" · ถาม senior ได้ว่า “บั๊กนี้เป็น stale data ฝั่ง client หรือ server ส่งข้อมูลผิดมาจริง ๆ”
แฟร์ ๆ: ฝั่ง ① ไม่ได้ผิด มันแลกมาเพื่อของบางอย่าง
สไลด์หน้า 10 ย้ำไว้เองว่า "ไม่ใช่ว่าฝั่งซ้ายผิด" · ฝั่ง ① ยอมรับความยุ่งของสมุดเล่มที่สอง เพื่อแลกกับสองเรื่องนี้:
| ① Next.js + Go (ส่ง JSON) | ② Rails (ส่ง HTML) | |
|---|---|---|
| ได้ | ทีม frontend กับ backend ทำงานแยกกันได้ ตกลงกันแค่หน้าตา JSON · JSON ชุดเดียวใช้ได้หลายแบบ — เว็บ · แอปมือถือ · ระบบของพาร์ตเนอร์ | โค้ดน้อยกว่า ทีมเดียวทำได้ทั้งหน้า · ข้อมูลชุดเดียว ไม่มีบั๊ก sync ระหว่างสองชุด |
| เสีย | ต้องดูแลสองโปรแกรม · ต้องคอยให้ state ในเบราว์เซอร์ตรงกับของจริง | แอปมือถือหรือพาร์ตเนอร์อ่าน HTML ไม่สะดวก ต้องทำ JSON เพิ่ม · หน้าที่โต้ตอบซับซ้อนมาก ๆ ยังต้องพึ่ง JavaScript (เรื่องของบล็อก C) |
ช่อง "เสีย" ของ Rails มีทางแก้ที่ซ่อนอยู่ในแอปของคุณแล้ว — scaffold ทำ JSON ไว้ให้ด้วย · มาดูของจริงกัน
ดูของจริง: แอปเดียวกัน เสิร์ฟได้ทั้ง HTML และ JSON
เปิด server ในโฟลเดอร์แอป (bin/dev ตามบท 120) แล้วเปิดเทอร์มินัลอีกหน้าต่าง ลองขอหน้า /posts แบบที่เบราว์เซอร์ขอ ·
(-s = เงียบ ไม่ต้องโชว์แถบความคืบหน้า · บท 102) · ด้านล่างตัดมาเฉพาะต้นไฟล์กับบทความชิ้นแรก (ผลจริง · ทั้งหน้ายาวมาก):
$ curl -s localhost:3000/posts
<!-- BEGIN app/views/layouts/application.html.erb
--><!DOCTYPE html>
<html>
<head>
<title>Posts</title>
…
<div id="posts" class="min-w-full divide-y divide-gray-200 space-y-5">
<div class="flex flex-col sm:flex-row justify-between items-center pb-5 sm:pb-0">
<!-- BEGIN app/views/posts/_post.html.erb
--><div id="post_1" class="w-full sm:w-auto my-5 space-y-5">
<div>
<strong class="block font-medium mb-1">Title:</strong>
ต้มยำกุ้งน้ำข้น
</div>
…
- ได้ HTML พร้อมโชว์ — มีทั้งหัวหน้าเว็บ (
<head>) · กล่อง · ป้าย "Title:" · ชื่อบทความ "ต้มยำกุ้งน้ำข้น" ถูกวางลงจานแล้ว class="min-w-full divide-y …"— คลาสของ Tailwind (บท 72) ที่ scaffold ใส่มาให้ · ตอนนี้ไม่ต้องสนใจ เรื่องหน้าตาอยู่บล็อก B<!-- BEGIN … -->— โน้ตที่ Rails แปะให้ตอนพัฒนา บอกว่าส่วนนี้มาจากไฟล์ไหน (บท 103) ·_post.html.erbคือแม่พิมพ์ของบทความหนึ่งชิ้น (เจอละเอียดในบท 123)
ทีนี้ขอ URL เดิม แต่เติม .json ท้าย · ขอบทความชิ้นเดียว (/posts/1.json) จะได้อ่านง่าย ·
ท่อน | python3 -m json.tool --no-ensure-ascii แค่จัดบรรทัดให้สวยและโชว์ภาษาไทยตรง ๆ (ไม่ต้องจำ):
$ curl -s localhost:3000/posts/1.json | python3 -m json.tool --no-ensure-ascii
{
"id": 1,
"title": "ต้มยำกุ้งน้ำข้น",
"description": "ต้มยำกุ้งแบบน้ำข้นใส่นมข้นจืด รสเปรี้ยวเผ็ดกลมกล่อม กุ้งแม่น้ำตัวใหญ่ หอมตะไคร้ ข่า ใบมะกรูด กินกับข้าวสวยร้อน ๆ ได้ทั้งถ้วย ร้านนี้เปิดมาสามสิบปีแล้ว คิวยาวทุกวันเสาร์อาทิตย์",
"category_id": 1,
"created_at": "2026-10-08T03:38:59.900Z",
"updated_at": "2026-11-07T03:38:59.900Z",
"url": "http://localhost:3000/posts/1.json"
}
บทความชิ้นเดียวกัน แต่คราวนี้เป็นวัตถุดิบดิบ — ไม่มีกล่อง ไม่มีป้าย ไม่มีสี · เหมือน JSON ที่ Go ส่งในบท 38 ทุกอย่าง ·
(วันที่ created_at ของคุณจะต่าง เพราะข้อมูลตัวอย่างนับย้อนจากวันที่คุณรัน seed) ·
ช่อง url ไม่ได้อยู่ในตาราง — แม่แบบ JSON ของ scaffold เติมที่อยู่ของบทความชิ้นนั้นให้
ทีนี้สั่ง curl -s localhost:3000/posts แล้วตามด้วย curl -s localhost:3000/posts.json (รายการทั้งหมดแบบ JSON) ·
ใน log ของ server (log/development.log) เห็นชัดว่าเป็น action เดียวกัน แค่เลือกแม่แบบคนละไฟล์ (ผลจริง · ตัดบรรทัดที่ไม่เกี่ยวออก):
Started GET "/posts" for ::1 at 2026-11-07 11:22:43 +0700
Processing by PostsController#index as */*
Rendering layout layouts/application.html.erb
Rendering posts/index.html.erb within layouts/application
…
Started GET "/posts.json" for ::1 at 2026-11-07 11:22:44 +0700
Processing by PostsController#index as JSON
Rendering posts/index.json.jbuilder
…
for ::1— ใบสั่งมาจากเครื่องตัวเอง (::1คือ localhost แบบเขียนอีกแบบ) · วันเวลาของคุณจะต่างPostsController#index— action เดียวกันทั้งสองครั้ง (โค้ดข้างในมีแค่@posts = Post.all)as */*/as JSON— Rails ดูว่าลูกค้าขอแบบไหน (เรียกว่า format) · ท้าย URL เป็น.json= ขอ JSON ·*/*= curl บอกว่า "รับได้ทุกแบบ" Rails เลยให้ HTML · (ถ้าเปิด/postsด้วยเบราว์เซอร์ บรรทัดนี้จะเป็นas HTML— ผมเช็คกับ Chrome จริงแล้ว)Rendering posts/index.json.jbuilderกับposts/index.html.erb— แม่แบบคนละไฟล์ · HTML มี layout (จานรองมาตรฐาน) ครอบด้วย · JSON ไม่มี
ลองวัดขนาดหน้ารายการ 30 บทความ (-o /dev/null = ไม่ต้องโชว์เนื้อ · -w '%{size_download}' = บอกขนาดที่ได้มาเป็นไบต์) — ผลจริงในเครื่องที่ใช้ตรวจ:
$ curl -s -o /dev/null -w '%{size_download}\n' localhost:3000/posts
53937
$ curl -s -o /dev/null -w '%{size_download}\n' localhost:3000/posts.json
13566
HTML ใหญ่กว่าราว 4 เท่า เพราะมีของหน้าตาติดมาด้วย (ชื่อ class ยาว ๆ · ปุ่ม Show/Edit/Destroy · โน้ต BEGIN/END ของโหมดพัฒนา) · ตัวเลข HTML ของคุณจะต่างเล็กน้อย และโตขึ้นนิดหน่อยทุกครั้งที่สั่งซ้ำ (เครื่องมือวัดความเร็ว rack-mini-profiler ที่ติดมากับแอปแปะรหัสเพิ่มท้ายหน้า) · ตัวเลขนี้คือหน้า scaffold ในเครื่องพัฒนา ไม่ใช่กฎตายตัว · ประเด็นคือ Rails ไม่ได้ "ทำ API ไม่ได้" — ถ้าวันหนึ่งต้องมีแอปมือถือ JSON ก็อยู่ตรงนี้แล้ว แค่ไม่บังคับให้หน้าเว็บต้องผ่านมัน
ชื่อไฟล์บอกทุกอย่าง: index.html.erb กับ index.json.jbuilder
ดูโฟลเดอร์ view ของบทความ (ผลจริง · ในเทอร์มินัลของคุณชื่ออาจเรียงเป็นหลายคอลัมน์ แต่เป็นไฟล์ชุดเดียวกัน):
$ ls app/views/posts
_form.html.erb
_post.html.erb
_post.json.jbuilder
edit.html.erb
index.html.erb
index.json.jbuilder
new.html.erb
show.html.erb
show.json.jbuilder
ไฟล์มาเป็นคู่ — index มีทั้ง .html.erb และ .json.jbuilder · show ก็เหมือนกัน ·
ชื่อไฟล์แบ่งเป็น 3 ท่อน อ่านจากขวาไปซ้าย (สไลด์หน้า 11):
ไฟล์ index.json.jbuilder สั้นมาก — มีบรรทัดเดียว:
# app/views/posts/index.json.jbuilder
json.array! @posts, partial: "posts/post", as: :post
json.array! @posts— "สร้าง JSON เป็นรายการ จากบทความทุกชิ้นบนถาด@posts"partial: "posts/post", as: :post— "แต่ละชิ้นให้จัดตามแม่พิมพ์_post.json.jbuilder" (แม่พิมพ์แบบนี้คือ partial — ละเอียดในบท 123)
บทนี้แค่ให้รู้ว่ามันอยู่ตรงนี้ · Day 3 ทั้งวันเราทำงานกับไฟล์ .html.erb
- format ·
respond_to - "ลูกค้าขอผลแบบไหน" — HTML · JSON ฯลฯ · ดูจากท้าย URL (
.json) หรือหัวใบสั่ง (header — ข้อมูลกำกับที่เบราว์เซอร์แนบมากับ request บอกว่า "รับ HTML นะ") · action ไหนต้องทำต่างกันตามแบบจะเขียนrespond_to do |format|(เปิดดูได้ในเมธอดcreateของapp/controllers/posts_controller.rb) · ถาม senior ได้ว่า “action นี้ต้องตอบ JSON ด้วยไหม หรือลบ .json.jbuilder ทิ้งได้” - template handler · jbuilder
- ท่อนท้ายสุดของชื่อไฟล์ บอกว่าใช้เครื่องมืออะไรแปลงแม่แบบ ·
erbสำหรับ HTML ·jbuilder(gem ที่มากับ Rails) สำหรับสร้าง JSON · พิมพ์บอก AI ได้ว่า “add the category name to the JSON in app/views/posts/_post.json.jbuilder”
ERB 4 แบบ — ทวนบท 103 แล้วลองบนหน้าจริง
บท 103 รู้จักไปแล้ว 3 แท็ก · สไลด์หน้า 11 นับเป็น 4 แบบ เพราะแยก "วนลูป" ออกมาเป็นอีกแบบ (จริง ๆ คือสองแท็กแรกทำงานร่วมกัน):
| แบบ | หน้าตา | Ruby ทำงานไหม | ผลไปถึงเบราว์เซอร์ไหม |
|---|---|---|---|
| ① พิมพ์ผล | <%= @post.title %> | ✅ | ✅ ผลลัพธ์ถูกวางลงตรงนั้น |
| ② รันอย่างเดียว | <% if … %> … <% end %> | ✅ | ❌ ตัวมันไม่ไป (แต่ HTML ที่มันคุมไปได้) |
| ③ วนลูป | <% @posts.each do |post| %> … <% end %> | ✅ | ❌ ตัวมันไม่ไป · ของข้างในถูกพิมพ์ซ้ำทุกรอบ |
| ④ คอมเมนต์ | <%# โน้ต %> | ❌ | ❌ หายตั้งแต่ในครัว |
มาลองบนหน้า /posts ของแอปจริง · เปิดไฟล์ app/views/posts/index.html.erb หาบรรทัดที่ 3 <div class="w-full"> ·
แล้วแทรก 9 บรรทัดนี้ต่อจากมัน (โค้ดทดลอง · จบบทจะลบทิ้ง):
<%# ทดลองบท 122: โน้ตของเชฟ ลูกค้าไม่เห็น %>
<!-- ทดลองบท 122: โน้ต HTML ลูกค้าเปิดดูได้ -->
<p>มีบทความทั้งหมด <%= @posts.size %> ชิ้น</p>
<p>บทความแรก: <%= @posts.first.title %></p>
<ul>
<% @posts.first(3).each do |post| %>
<li><%= post.title %></li>
<% end %>
</ul>
- บรรทัด
<%# … %>— แบบ ④ โน้ตของ ERB - บรรทัด
<!-- … -->— ไม่ใช่ ERB · เป็นคอมเมนต์ของ HTML ธรรมดา (ข้อความที่เบราว์เซอร์ไม่โชว์บนจอ) · ใส่ไว้เทียบกัน @posts.size— นับว่ามีกี่ชิ้น (บท 113) · แบบ ① จึงโชว์ตัวเลข@posts.first.title— ชื่อของชิ้นแรก ·first= ชิ้นแรกเรียงตามเลขที่@posts.first(3).each do |post|— แบบ ③ ·first(3)= เอา 3 ชิ้นแรก แล้ววนทีละชิ้น
เซฟแล้วไม่ต้องรีสตาร์ต server (โหมดพัฒนาอ่านไฟล์ view ใหม่ทุกครั้งเอง) · curl ดูเฉพาะช่วงที่แทรก (ผลจริง · ตัดส่วนอื่นออก):
$ curl -s localhost:3000/posts
…
<!-- BEGIN app/views/posts/index.html.erb
-->
<div class="w-full">
<!-- ทดลองบท 122: โน้ต HTML ลูกค้าเปิดดูได้ -->
<p>มีบทความทั้งหมด 30 ชิ้น</p>
<p>บทความแรก: ต้มยำกุ้งน้ำข้น</p>
<ul>
<li>ต้มยำกุ้งน้ำข้น</li>
<li>ก๋วยเตี๋ยวเรืออยุธยา</li>
<li>ข้าวเหนียวมะม่วง</li>
</ul>
…
- โน้ต
<%# %>หายไปทั้งบรรทัด — ไม่ถูกส่งออกจากครัวเลย - โน้ต
<!-- -->ยังอยู่ — ถูกส่งไปถึงเบราว์เซอร์ · ผมเปิดใน Chrome จริง: บนจอไม่โชว์ แต่ใครกด "ดูซอร์สของหน้า" (View Page Source) ก็อ่านได้ <%= @posts.size %>กลายเป็น30·<%= @posts.first.title %>กลายเป็นชื่อบทความ- บรรทัด
<% … each … %>กับ<% end %>หายไป · แต่<li>ข้างในถูกพิมพ์ 3 รอบ
ไฟล์ .html.erb คือแม่แบบจานที่ติดไว้ในครัว · <%= %> = "เติมของในช่องนี้แล้ววางลงจาน" ·
<% %> = งานที่คนจัดจานทำในครัวเงียบ ๆ เช่น "ทำซ้ำ 3 ที่" — ตัวคำสั่งไม่ขึ้นจาน ·
<%# %> = โน้ตดินสอบนแม่แบบ ไม่ออกจากครัว
ส่วน <!-- --> คือกระดาษโน้ตที่แปะติดใต้จาน — ลูกค้ามองจากด้านบนไม่เห็น แต่ใครพลิกจานดูก็อ่านได้
ที่ไม่เหมือนกัน: คนจัดจานจริงเห็นช่องว่างก็ทักว่า "ช่องนี้ลืมใส่อะไรหรือเปล่า" · แต่ ERB ไม่ทักอะไรเลย — ใช้แท็กผิดแบบ มันทำตามตัวอักษรเป๊ะ ๆ แล้วเสิร์ฟจานนั้นออกไปเงียบ ๆ ไม่มี error · สามหัวข้อถัดไปคือกับดักจากเรื่องนี้
- output tag
<%= %>· execution tag<% %> - แท็กที่รันแล้วพิมพ์ผล กับแท็กที่รันอย่างเดียว · พิมพ์บอก AI ได้ว่า “the title is blank in the HTML — check whether I used an execution tag instead of an output tag”
- ERB comment
<%# %>· HTML comment<!-- --> - คอมเมนต์ของ ERB หายตั้งแต่บน server · คอมเมนต์ของ HTML ถูกส่งไปถึงเบราว์เซอร์และใครก็เปิดซอร์สอ่านได้ · ถาม senior ได้ว่า “ทีมเราห้ามใส่ HTML comment ใน view ไหม หรือให้ใช้ ERB comment อย่างเดียว”
กับดัก ①: ลืม = ตัวเดียว — หน้าว่าง ไม่มี error
สไลด์หน้า 11 บอกว่านี่คือบั๊กที่มือใหม่เจอบ่อยที่สุด · บท 103 เห็นมาแล้วกับข้อมูลจำลอง · คราวนี้ลองกับฐานข้อมูลจริง แล้วดูให้ลึกกว่าเดิม
<%# แก้บรรทัดเดียวในโค้ดทดลอง: ลบ = ออกจาก "บทความแรก" %>
<p>บทความแรก: <% @posts.first.title %></p>
เซฟแล้ว curl ใหม่ · บรรทัดนี้ได้ HTML หน้าตาแบบไหน? แล้ว Rails ยังไปหยิบบทความแรกจากฐานข้อมูลไหม ทั้งที่ไม่ได้โชว์?
ผลจริง — ชื่อหายไป เหลือแค่ป้าย · หน้าเว็บตอบ 200 OK ปกติ ไม่มี error:
<p>มีบทความทั้งหมด 30 ชิ้น</p>
<p>บทความแรก: </p>
และ log บอกว่ายังไปหยิบจริง (ผลจริง · ตัดมาสองบรรทัด):
Post Load (0.8ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT 1 /*action='index',application='WongnokBlogAllInOne',controller='posts'*/
↳ app/views/posts/index.html.erb:8
ORDER BY "posts"."id" ASC LIMIT 1= "เรียงตามเลขที่ เอาแผ่นแรกแผ่นเดียว" — นี่คือ@posts.first/*action='index',…*/ท้าย SQL = ป้ายที่ Rails แปะบอกว่า query นี้มาจาก controller/action ไหน ·(0.8ms)= เวลาที่ใช้ ของคุณจะต่าง↳ …index.html.erb:8= บรรทัดที่สั่ง · ในไฟล์คือบรรทัด 7 แต่ log นับเกิน 1 เพราะโน้ต BEGIN (เรื่องเดียวกับบท 114)
นี่คือความหมายของประโยคในสไลด์ว่า "มันรันจริง แค่ไม่พิมพ์ผลออกมา" · Ruby ไปตู้ หยิบแฟ้ม อ่านชื่อ — แล้วทิ้ง เพราะแท็กไม่มี =
ถ้าหน้าเว็บ "ว่างตรงที่ควรมีข้อมูล" แต่ไม่มี error — เช็ค = ก่อนอย่างอื่น · เปิดไฟล์ view แล้วไล่ดูทุกแท็กที่ควรโชว์ของ ว่าขึ้นต้นด้วย <%= ครบไหม
กับดัก ②: ใส่ = เกินที่บรรทัด each
ทิศกลับกันก็พัง · ลองคืน = ให้บรรทัด "บทความแรก" แล้วเผลอใส่ = ที่บรรทัด each แทน:
<%= @posts.first(3).each do |post| %>
ผลจริงจาก curl (ตัดมาเฉพาะ <ul> · บรรทัดยาวมากจนต้องเลื่อนดู):
<ul>
<li>ต้มยำกุ้งน้ำข้น</li>
<li>ก๋วยเตี๋ยวเรืออยุธยา</li>
<li>ข้าวเหนียวมะม่วง</li>
[#<Post id: 1, title: "ต้มยำกุ้งน้ำข้น", description: "ต้มยำกุ้งแบบน้ำข้นใส่นมข้นจืด รสเปรี้ยวเผ็ดกลมกล่อ...", category_id: 1, created_at: "2026-10-08 03:38:59.900313000 +0000", updated_at: "2026-11-07 03:38:59.900694000 +0000">, #<Post id: 2, … >] </ul>
- รายการ 3 ชิ้นยังโผล่ครบ · แต่ต่อท้ายด้วยก้อนข้อความยาวเหยียด ที่ขึ้นต้นด้วย
[#<Post id: 1, … - ทำไม:
eachวนครบแล้วคืนรายการเดิมทั้งก้อนกลับมา ·<%=แปลว่า "เอาผลไปวางลงจาน" มันจึงวางรายการบทความทั้งก้อนลงไป ในรูปที่ Ruby ใช้โชว์ข้อมูลใน console <·"— คือ<กับ"ที่ Rails แปลงให้ไม่ทำงานเป็น HTML (เรื่องกัน XSS · บท 103 และลงลึกในบท 125)
ผมเปิดหน้านี้ใน Chrome จริง: ใต้รายการมีข้อความ [#<Post id: 1, title: "ต้มยำกุ้งน้ำข้น", … โชว์อยู่บนจอให้ทุกคนเห็น ·
(วันที่ในข้อความของคุณจะต่าง)
เห็นข้อความแปลก ๆ หน้าตา [#<Post id: …>] โผล่บนหน้าเว็บ = มี <%= อยู่หน้า each · บรรทัด each · if · end ใช้ <% (ไม่มี =) —
ผมลอง <%= if … %> แล้วได้หน้า error ActionView::SyntaxErrorInTemplate เลย ·
ข้อยกเว้นเดียวที่จะเจอ: helper ที่สร้าง HTML ครอบเนื้อหาข้างใน เช่น <%= form_with … do |f| %> (บท 124) ตัวนั้นต้องมี =
กับดัก ③: <!-- --> ไม่ได้หยุด Ruby
มือใหม่ชอบ "ปิดบรรทัดชั่วคราว" ด้วยคอมเมนต์ HTML เพราะคุ้นจากตอนเขียน HTML ล้วน ๆ · ลองครอบบรรทัด "บทความแรก" (ที่มี = ครบแล้ว) ด้วย <!-- -->:
<!-- <p>บทความแรก: <%= @posts.first.title %></p> -->
ผลจริง:
<!-- <p>บทความแรก: ต้มยำกุ้งน้ำข้น</p> -->
ชื่อบทความยังถูกเติมเข้าไปในคอมเมนต์ · และ log ยังมีบรรทัด Post Load … LIMIT 1 ครบ ·
ERB ไม่รู้จัก <!-- --> — สำหรับมันนั่นคือ HTML ธรรมดา ก้อนหนึ่ง มันเติมช่อง <%= %> ข้างในตามปกติ แล้วเบราว์เซอร์ค่อยซ่อน ·
ถ้าในนั้นเป็นข้อมูลที่ไม่ควรให้ใครเห็น ก็หลุดไปถึงเบราว์เซอร์แล้ว
วิธีที่ถูก: เติม # หลัง <% ให้กลายเป็นคอมเมนต์ของ ERB:
<p>บทความแรก: <%#= @posts.first.title %></p>
ผลจริง: ได้ <p>บทความแรก: </p> และใน log ไม่มีบรรทัด LIMIT 1 แล้ว — Ruby ไม่ได้ทำงานเลย ·
แอปของคุณใช้ท่านี้อยู่แล้ว: เปิด app/views/layouts/application.html.erb จะเจอบรรทัด <%#= tag.link rel: "manifest", … %> ที่ Rails ปิดไว้ให้
เทียบกับ JSX: ทิศกลับกัน
สไลด์หน้า 11 สรุปไว้ประโยคเดียว: JSX คือ JavaScript ที่เขียน HTML ได้ · ERB คือ HTML ที่เขียน Ruby ได้ · วางงานเดียวกันข้างกันจะเห็นชัด:
JSX (ไฟล์ .tsx) — markup อยู่ในภาษา
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
ERB (ไฟล์ .html.erb) — ภาษาอยู่ใน markup
<ul>
<% @posts.each do |post| %>
<li><%= post.title %></li>
<% end %>
</ul>
- ไฟล์ทั้งไฟล์เป็นภาษาอะไร —
.tsxคือ TypeScript ที่มีก้อน HTML แทรก ·.html.erbคือ HTML ที่มีช่อง Ruby แทรก (ส่วนที่ไม่อยู่ในแท็ก<% %>ส่งออกไปตามตัวอักษร) - ช่องสำหรับโชว์ค่า — JSX ใช้
{ }· ERB ใช้<%= %> - วนลูป — JSX ใช้
mapที่คืนรายการ<li>ออกมา · ERB ใช้eachที่ "พิมพ์ทุกรอบ" แล้วผลของeachเองต้องทิ้ง (เพราะอย่างนี้ใส่=ที่ each จึงพัง — กับดัก ②) - รันที่ไหน — JSX ของ client component รันในเบราว์เซอร์ และวาดใหม่ทุกครั้งที่ state เปลี่ยน · ERB รันบน server ครั้งเดียวต่อหนึ่งใบสั่ง ได้ HTML แล้วจบ
ภารกิจ
🎯 ภารกิจ — กล่องสรุปบนหน้า /posts แล้วจงใจทำพังสองแบบ (~10 นาที)
ทำในแอป wongnok-blog-all-in-one (สถานะจากบท 120) · ถ้ายังมีโค้ดทดลองของบทนี้ค้างอยู่ สั่ง git restore app/views/posts/index.html.erb ก่อน · เปิด server ด้วย bin/dev
- ใน
app/views/posts/index.html.erbแทรกต่อจาก<div class="w-full">:<%# กล่องสรุป (ภารกิจบท 122) %> <!-- กล่องสรุป เริ่มตรงนี้ --> <p>มีบทความทั้งหมด <%= @posts.size %> ชิ้น</p> <ol> <% @posts.first(5).each do |post| %> <li><%= post.title %></li> <% end %> </ol> - เปิด
http://localhost:3000/postsในเบราว์เซอร์ · แล้วดูซอร์สของหน้า (Mac: ⌥ Option+⌘ Cmd+U · Windows: Ctrl+U) หรือcurl -s localhost:3000/posts· จดว่าคอมเมนต์อันไหนอยู่ในซอร์ส อันไหนไม่อยู่ - พังแบบที่ 1: ลบ
=ออกจาก<%= post.title %>→ รีเฟรช → ดูทั้งบนจอและในซอร์ส - คืน
=· พังแบบที่ 2: ลบบรรทัด<% end %>ทิ้ง → รีเฟรช - เก็บกวาด:
git restore app/views/posts/index.html.erbแล้วgit statusต้องไม่เหลือไฟล์ที่แก้
ก่อนกดเฉลย: ขั้น 3 จะได้ error ไหม? และขั้น 4 ต่างจากขั้น 3 ยังไง?
ขั้น 2 — ในซอร์สมี <!-- กล่องสรุป เริ่มตรงนี้ --> แต่ไม่มีบรรทัด "ภารกิจบท 122" ·
บนจอเห็นประโยค "มีบทความทั้งหมด 30 ชิ้น" กับชื่อบทความ 5 บรรทัด (ไม่มีเลขนำหน้า — อธิบายในขั้น 3) · ในซอร์สส่วนนี้หน้าตาแบบนี้ (ผลจริง):
<p>มีบทความทั้งหมด 30 ชิ้น</p>
<ol>
<li>ต้มยำกุ้งน้ำข้น</li>
<li>ก๋วยเตี๋ยวเรืออยุธยา</li>
<li>ข้าวเหนียวมะม่วง</li>
<li>หมูปิ้งข้าวเหนียว</li>
<li>ชาไทยเย็น</li>
</ol>
ขั้น 3 — ไม่มี error หน้าโหลดปกติ · บนจอรายการหายไปเฉย ๆ เหลือแค่บรรทัด "มีบทความทั้งหมด 30 ชิ้น" ·
แต่ในซอร์สมี <li></li> ว่าง ๆ ห้าอัน (ผลจริง) — กับดัก ① แบบในลูป ·
(ที่บนจอไม่เห็นแม้แต่เลข 1–5 เพราะ Tailwind ของแอปปิดเลขหน้ารายการไว้ และ <li> ที่ว่างก็ไม่มีความสูง — ผมเช็คใน Chrome จริง: รายการสูง 0 พิกเซล)
ขั้น 4 — พังทั้งหน้า ได้หน้า error สีแดงของ Rails หัวข้อ ActionView::SyntaxErrorInTemplate in PostsController#index ·
ต่างจากขั้น 3 เพราะลืม end = Ruby อ่านโค้ดไม่ออก (เปิดบล็อกแล้วไม่ปิด) · ส่วนลืม = = Ruby อ่านออกและรันได้ แค่ไม่พิมพ์
ถ้าเจออาการนี้ แปลว่า…
- log เขียนว่า
ActionView::SyntaxErrorInTemplate (Encountered a syntax error while rendering template located at: app/views/posts/index.html.erb)— มีdoหรือifที่ไม่มี<% end %>คู่กัน · นับคู่ให้ครบ (เลขบรรทัดใน error มักชี้ไปท้ายไฟล์ ไม่ใช่จุดที่ลืม) - มีข้อความ
[#<Post id: 1, …โผล่ใต้รายการ — คุณพิมพ์<%= @posts.first(5).each …(มี=เกิน) · กลับไปดูกับดัก ② - ไม่เห็นกล่องสรุปเลย ทั้งที่เซฟแล้ว — แก้ผิดไฟล์ (เช่น
show.html.erb) หรือเปิดผิดหน้า (/posts/1) · กล่องอยู่ที่/postsเท่านั้น - ชื่อบทความไม่ตรงกับเฉลย — ไม่เป็นไร ถ้าข้อมูลของคุณไม่ได้มาจาก seed ของบท 120 · ที่ต้องตรงคือจำนวนและอาการ
ทำแล้วผลไม่ตรง ลองถาม Claude Code (บท 86) โดยแปะโค้ดที่แทรกกับผล curl ช่วงนั้นไปด้วย — ได้คำตอบตรงกว่าเล่าอาการเฉย ๆ
ทดสอบความเข้าใจ
หน้ารายการแบบ ① (React fetch ใน useEffect แล้ว setPosts) เปิดค้างไว้ · เพื่อนแก้ชื่อบทความจากอีกแท็บสำเร็จ แต่จอนี้ยังโชว์ชื่อเก่า · สาเหตุหลักคืออะไร?
ตัวเลือกที่ 2 ถูก — ข้อมูลมีสองชุด: ในฐานข้อมูล (เปลี่ยนแล้ว) กับใน useState ของแท็บนี้ (ยังเป็นของเก่า) · useEffect ที่มี [] fetch แค่ครั้งเดียว ไม่มีอะไรไปบอกให้ fetch ใหม่
ตัวเลือกที่ 1 ผิด เพราะเพื่อนแก้สำเร็จแล้ว ชื่อใหม่อยู่ในฐานข้อมูลแล้ว · ตัวเลือกที่ 3 ผิด เพราะไม่มีข้อมูลใหม่กำลัง "เดินทาง" มาเลย แท็บนี้ไม่ได้ขอใหม่ · ตัวเลือกที่ 4 ผิด เพราะ CSS เป็นเรื่องหน้าตา ไม่เกี่ยวกับข้อความชื่อบทความ
จุดที่ต้องจำ: มีสำเนาข้อมูลสองชุด = ต้องคอยทำให้ตรงกัน · ลืมจุดเดียว = บั๊ก "ทำไมหน้าจอไม่อัปเดต"
ใน index.html.erb เขียนว่า <p>บทความแรก: <% @posts.first.title %></p> · เปิดหน้าแล้วเกิดอะไรขึ้น?
ตัวเลือกที่ 3 ถูก — ผลจริงในกับดัก ①: ป้ายยังอยู่ ชื่อหาย · log มี Post Load … LIMIT 1 แปลว่า Ruby รันจริง แค่ไม่พิมพ์
ตัวเลือกที่ 1 ผิด เพราะโค้ดถูกไวยากรณ์ทุกตัว ไม่มีอะไรให้ error ·
ตัวเลือกที่ 2 ผิด เพราะ ERB ไม่เดาใจ ทำตามแท็กเป๊ะ ๆ ·
ตัวเลือกที่ 4 ผิด เพราะสิ่งที่หายคือแค่ผลของแท็ก ส่วน <p>บทความแรก: เป็น HTML ธรรมดานอกแท็ก จึงถูกส่งออกไป
จุดที่ต้องจำ: จะโชว์ค่า ต้องมี = · ไม่มี = = รันแล้วทิ้ง เงียบ ๆ ไม่มี error
แอป scaffold มีทั้ง index.html.erb และ index.json.jbuilder · เปิด /posts.json แล้ว Rails ใช้อะไรสร้างคำตอบ?
ตัวเลือกที่ 4 ถูก — log จริง: Processing by PostsController#index as JSON แล้ว Rendering posts/index.json.jbuilder · action เดียวกัน เลือกแม่แบบตาม format
ตัวเลือกที่ 1 ผิด เพราะ Rails ไม่แปลง HTML เป็น JSON · ไฟล์ .html.erb ใช้เฉพาะเมื่อขอ HTML ·
ตัวเลือกที่ 2 ผิด เพราะ _post.html.erb สร้าง HTML · ฝั่ง JSON ใช้ _post.json.jbuilder ·
ตัวเลือกที่ 3 ผิด เพราะ index ของ scaffold มีแค่ @posts = Post.all ไม่ได้เขียน JSON เอง
จุดที่ต้องจำ: ชื่อไฟล์ = action . ส่งออกเป็นอะไร . ประมวลผลด้วยอะไร · Rails เลือกไฟล์จาก format ที่ลูกค้าขอ
ข้อไหนอธิบายความต่างของ <%# โน้ต %> กับ <!-- โน้ต --> ในไฟล์ .html.erb ได้ถูกต้อง?
ตัวเลือกที่ 1 ถูก — ผลจริงจาก curl: บรรทัด <%# ไม่มีในซอร์สเลย · บรรทัด <!-- อยู่ในซอร์ส และ <%= %> ที่อยู่ข้างในถูกเติมค่าตามปกติ (กับดัก ③)
ตัวเลือกที่ 2 ผิด เพราะ ERB ไม่รู้จัก <!-- มันส่งออกไปเหมือน HTML ทั่วไป ·
ตัวเลือกที่ 3 สลับสองแบบกัน ·
ตัวเลือกที่ 4 ผิดครึ่งหนึ่ง: <!-- ถูกส่งและซ่อนจริง แต่ <%# ไม่เคยออกจาก server
จุดที่ต้องจำ: จะปิดโค้ด Ruby ใน view ชั่วคราว ใช้ <%# (เช่น <%#=) ไม่ใช่ <!-- -->
ถ้าเปิดสไลด์ดูประกอบ
| หน้า | สไลด์เขียน | ของจริง (Rails 8.1.4 · แอปของ Day 3) | ต้องสนใจไหม |
|---|---|---|---|
| 11 | <% if @post.published? %> | บทความของเราไม่มีช่อง published · พิมพ์ตามใน show.html.erb ได้หน้า error undefined method 'published?' for an instance of Post | สนใจ ถ้าจะพิมพ์ตาม — เป็นแค่ตัวอย่างรูปแบบ if |
| 10 | "โชว์รายการโพสต์ 20 อัน" | แอปของเรามี 30 บทความ | ไม่ต้อง — ภาพเดียวกัน |
| 10 | Next.js + Go = fetch ใน browser → state ใน client | Next.js render บน server ได้ด้วย (Server Component) · ภาพนี้คือแบบ client fetch | ไม่ต้อง — แค่รู้ว่าไม่ใช่ Next.js ทุกแบบ |
| 11 | "ชื่อไฟล์บอกทุกอย่าง" ยกตัวอย่างแค่ index.html.erb | scaffold สร้าง index.json.jbuilder ไว้ข้างกันด้วย · /posts.json ใช้ได้ทันที | ไม่ต้อง — แต่ดีที่รู้ว่ามันอยู่ตรงนั้น |
อ้างอิง
- Rails Guides — Action View Overview § ERB —
อ่านแค่หัวข้อ ERB (สั้น) · ยืนยัน
<% %>·<%= %>และคอมเมนต์<%# %>· หัวข้อถัดไป § Jbuilder อ่านแค่ย่อหน้าแรกพอรู้ว่าไฟล์.json.jbuilderคืออะไร - Rails API — ActionView::Base § ERB —
ดูตัวอย่างวนลูปย่อหน้าแรก (
eachใช้<% %>· ชื่อใช้<%= %>) กับตัวอย่างWRONGถัดมาที่บอกว่าputsใช้พิมพ์ใน ERB ไม่ได้ - Rails Guides — Using Rails for API-only Applications § Why Use Rails for JSON APIs? — ฝั่งตรงข้ามของบทนี้: ถ้าทีมเลือกแบบ ① Rails ก็เป็น JSON API ล้วน ๆ ได้ · อ่านแค่หัวข้อนี้กับหัวข้อก่อนหน้า (What is an API Application?)
- React — Writing Markup with JSX — ฝั่ง JSX ที่เทียบกัน · อ่านถึงหัวข้อ The Rules of JSX จะเห็นว่า JSX คือ JavaScript ที่เขียน markup ได้
- React — You Might Not Need an Effect — สำหรับคนที่อยากรู้ว่าทีม React เองก็ระวังการเก็บสำเนาข้อมูลไว้ใน state · อ่านแค่ย่อหน้าแรกกับหัวข้อ How to remove unnecessary Effects
- MDN — HTML Comments — ยืนยันว่าคอมเมนต์ HTML เป็นส่วนหนึ่งของเอกสารที่ส่งไปถึงเบราว์เซอร์ แค่ไม่ถูกแสดง · อ่านส่วน Syntax