บทเรียน 130 · Rails · Day 3

Hotwire — เว็บที่ขยับได้โดยไม่ต้องมี state ชุดที่สอง และ Turbo Drive

~22 นาที · ควรผ่าน บทเรียน 129 (Lab Vibe 2) และ บทเรียน 122 (state ใน client · สมุดเล่มที่สอง) มาก่อน

บทนี้เปิดบล็อก C — ทำให้เว็บขยับได้ทันใจแบบแอป React โดยไม่ต้องเก็บสำเนาข้อมูลไว้ในเบราว์เซอร์ · อ่านจบแล้วคุณจะอธิบายได้ว่า Hotwire คิดต่างจาก React ตรงไหน · รู้ว่าเวลากดลิงก์ในแอปของเราหน้าเว็บไม่ได้โหลดใหม่จริง ๆ มาตั้งแต่ Lab แรกแล้ว (และพิสูจน์เองได้ด้วย DevTools) · สั่งให้ลิงก์บางอันเลิกทำแบบนั้นได้ · และรู้จักผลข้างเคียงที่ทำให้โค้ด JavaScript แบบเดิม ๆ พัง — ซึ่งคือเหตุผลที่บท 134 ต้องมี Stimulus

🔁 ทวนก่อนเริ่ม — คำจากบทก่อน ๆ ที่บทนี้ใช้

🧰 ตรวจด้วย Ruby 4.0.9 · Rails 8.1.4 · turbo-rails 2.0.23 (Turbo 8.0.23) · Chrome 156 (สไลด์และ repo ของอาจารย์ใช้ Ruby 4.0.5–4.0.6 · Rails 8.1.3 — รุ่นแก้บั๊กในสายเดียวกัน เรื่องในบทนี้ไม่ต่างกัน)

🗂 โค้ดเริ่มจาก: แอปหลังจบ Lab บท 129 (มี navbar · การ์ด daisyUI · ปุ่มสลับธีม — ตามไม่ทันดูบท 129) · บทนี้ทดลองแล้วทิ้ง — จบบทให้ git restore . (บอกอีกทีท้ายภารกิจ)

ผลทุกก้อนรันจริงแล้วก็อปมา · พฤติกรรมในเบราว์เซอร์ตรวจด้วย Chrome จริง · ขนาดไฟล์ (byte) ของคุณจะต่างไปนิดหน่อย เพราะในหน้ามีข้อความที่เปลี่ยนตามเวลา เช่น "about 1 month ago" · และ rack-mini-profiler (ป้ายจับเวลาตอน development) ฝังรหัสสุ่มไว้ในหน้า ยิ่งเปิดบ่อยยิ่งยาวขึ้นนิดหน่อย

บล็อก C: ทำให้เว็บขยับได้ โดยไม่ต้องมีสมุดเล่มที่สอง

ตลอดบล็อก A และ B เราทำหน้าเว็บที่สวยและอ่านรู้เรื่องแล้ว · แต่มันยังเป็นเว็บแบบ "กดแล้วรอหน้าใหม่" · ถ้าอยากแก้หัวข้อบทความในหน้าเดิม หรือให้คอมเมนต์ใหม่เด้งขึ้นเองโดยไม่ต้องรีเฟรช — คนส่วนใหญ่จะนึกถึง React ทันที

บล็อก C จะทำของพวกนั้นด้วย Hotwire แทน · Hotwire คือชื่อรวมของเครื่องมือชุดหนึ่งจากทีมที่สร้าง Rails · สองตัวที่เราจะใช้คือ Turbo (เปลี่ยนหน้า/เปลี่ยนบางส่วนของหน้าโดยไม่ต้องเขียน JS) และ Stimulus (JS ชิ้นเล็ก ๆ ที่ HTML เป็นคนเรียก) · ทั้งคู่มากับ Rails ตั้งแต่ rails new ไม่ต้องติดตั้งเพิ่ม

บทเรื่องของจริงที่จะได้ในแอป
130 (บทนี้)แนวคิด + Turbo Driveไม่มีโค้ดใหม่ — พิสูจน์ว่ามันทำงานอยู่แล้ว
131Turbo Framesแก้หัวข้อบทความในหน้าเดิม
132Turbo Streamsเพิ่มคอมเมนต์แล้วหลายจุดในหน้าเปลี่ยนพร้อมกัน
133broadcastคอมเมนต์เด้งขึ้นทุกแท็บที่เปิดอยู่
134Stimulusตัวนับตัวอักษรในช่องคอมเมนต์
135Lab Vibe 3ตรวจรับงานทั้งบล็อก

คำถามเดียวที่อยู่เบื้องหลัง Hotwire

ทวนบท 122 ก่อน: แอป React ที่ fetch JSON มาเก็บใน useState จะมีข้อมูลสองชุด — ชุดจริงในฐานข้อมูล กับสำเนาในแท็บของผู้ใช้ · สองชุดนี้ต้องตรงกันตลอด ไม่งั้นจอโชว์ของเก่า (บั๊ก sync)

Hotwire ตั้งคำถามกลับว่า: "ถ้าเราไม่ยอมให้มีสำเนาชุดที่สองเลย เราจะทำให้หน้าเว็บขยับได้ถึงไหน" · สองฝั่งตอบไม่เหมือนกัน:

ฝั่ง SPA (React ที่ fetch JSON)ฝั่ง Hotwire
สำเนาข้อมูลในเบราว์เซอร์ยอมให้มี แล้วหาเครื่องมือมาช่วยดูแลมันไม่มี — ข้อมูลอยู่ที่ server ที่เดียว
อยากให้จอเปลี่ยนแก้สำเนาใน state → React วาดจอใหม่จากสำเนาไปขอ HTML ใหม่ จาก server → เอามาแทนเฉพาะส่วนที่ต้องเปลี่ยน
ของที่วิ่งผ่านเน็ตJSON (ข้อมูลดิบ)HTML (จัดหน้าเสร็จแล้ว)
โค้ดจัดหน้าตาอยู่ในเบราว์เซอร์ (JSX)อยู่ใน view ของ Rails (ERB) ที่เดียว

เครื่องมือที่ฝั่ง SPA ใช้ดูแลสำเนา มีชื่อที่คุณจะได้ยินบ่อย: Redux · Zustand (ที่เก็บ state กลางของทั้งแอป) และ React Query · SWR (ตัวช่วย fetch ที่จำว่าข้อมูลไหนเก่าแล้วและไปขอใหม่ให้เอง) · ถ้าเทียบกับที่เราเรียน: มันคือ useState + useEffect ฉบับอัปเกรด ที่ทำให้ไม่ลืม fetch ใหม่

เครื่องมือพวกนี้เก่งมาก · แต่สังเกตว่าทุกตัวเกิดมาแก้ปัญหาเดียวกัน คือทำให้สำเนาตรงกับของจริง · Hotwire เลือกตัดปัญหาที่ต้นทาง: ไม่มีสำเนา ก็ไม่มีอะไรต้องทำให้ตรง

🍳 เทียบให้เห็นภาพ — ส่งวัตถุดิบไปปรุงที่โต๊ะ vs ครัวจัดจานเสร็จแล้วเสิร์ฟ

ร้านแบบ SPA ส่งวัตถุดิบดิบ (JSON) ไปที่โต๊ะ · โต๊ะต้องมีเตาของตัวเอง (React) และสมุดจดของบนโต๊ะเล่มที่สอง · พอร้านใหญ่ขึ้น เจ้าของเลยจ้างผู้ช่วยประจำโต๊ะ (React Query · Redux) คอยวิ่งไปเช็คกับครัวว่าสมุดบนโต๊ะยังตรงไหม

ร้านแบบ Hotwire ไม่มีสมุดบนโต๊ะ · ลูกค้าอยากได้อะไร พนักงานไปบอกครัว ครัวเปิดสมุดเล่มเดียวของครัว จัดจานเสร็จ แล้วเปลี่ยนเฉพาะจานบนโต๊ะ · ไม่ต้องจ้างผู้ช่วยมาคอยเช็คสมุด เพราะไม่มีสมุดให้เช็ค

ที่ไม่เหมือนกัน: ร้าน Hotwire ต้องเดินไปครัวทุกครั้งที่จออยากเปลี่ยน (ต้องมีเน็ต และรอ server ตอบ) · งานที่ต้องตอบสนองทันทีโดยไม่ต้องถามครัว เช่น นับตัวอักษรขณะพิมพ์ ยังต้องมี JS เล็ก ๆ บนโต๊ะอยู่ดี (Stimulus · บท 134) · และโต๊ะอื่นจะไม่รู้เองว่าครัวเปลี่ยนอะไร จนกว่าครัวจะประกาศ (broadcast · บท 133)

🗣 ศัพท์ที่เพิ่งเรียนไป
Hotwire · HTML over the wire
แนวคิด "ส่ง HTML ที่จัดเสร็จแล้วผ่านเน็ต แทนการส่ง JSON" และชื่อชุดเครื่องมือ (Turbo + Stimulus) ของแนวคิดนี้ · ถาม senior ได้ว่า “หน้านี้ทำแบบ HTML over the wire ด้วย Turbo ได้ไหม หรือจำเป็นต้องเป็น SPA”
SPA (single-page application)
เว็บที่โหลดหน้าเดียวแล้วให้ JavaScript ในเบราว์เซอร์วาดจอเองทั้งหมดจาก JSON — แบบที่ React ทำ · พิมพ์บอก AI ได้ว่า “don't turn this page into an SPA; render HTML on the server and use Turbo”
state management library
เครื่องมือดูแลสำเนาข้อมูลฝั่ง client เช่น Redux · Zustand · React Query · SWR · ถาม senior ได้ว่า “ถ้าใช้ Hotwire แล้ว เรายังต้องมี state management library ไหม” (คำตอบปกติคือไม่ต้อง)

วัดจริง: HTML หนักกว่า JSON แค่ไหน

ข้อเสียที่ทุกคนถามคือ "ส่ง HTML ไม่เปลืองกว่าเหรอ" · ข้อนี้จริง แต่มากแค่ไหนต้องวัดเอง · แอปของเราตอบได้ทั้งสองแบบอยู่แล้ว (scaffold ทำ .json ไว้ให้ — บท 122) เลยวัดบทความชิ้นที่ 1 ได้เลย · เปิด bin/dev ทิ้งไว้ แล้วเปิดหน้าต่าง Terminal อีกอันในโฟลเดอร์แอปมารันคำสั่งนี้:

$ curl -s localhost:3000/posts/1.json
{"id":1,"title":"ต้มยำกุ้งน้ำข้น","description":"ต้มยำกุ้งแบบน้ำข้นใส่นมข้นจืด รสเปรี้ยวเผ็ดกลมกล่อม กุ้งแม่น้ำตัวใหญ่ หอมตะไคร้ ข่า ใบมะกรูด กินกับข้าวสวยร้อน ๆ ได้ทั้งถ้วย ร้านนี้เปิดมาสามสิบปีแล้ว คิวยาวทุกวันเสาร์อาทิตย์","category_id":1,"created_at":"2026-10-08T10:52:53.123+07:00","updated_at":"2026-11-07T10:52:53.123+07:00","url":"http://localhost:3000/posts/1.json"}

นี่คือ "วัตถุดิบดิบ" (วันเวลาใน created_at · updated_at ของคุณจะต่าง ตามวันที่คุณรัน seed) · ทีนี้นับขนาด (byte) ด้วย wc -c (นับจำนวน byte ที่ได้มา) และลองบีบด้วย gzip (วิธีบีบไฟล์ที่ server จริงมักเปิดไว้ตอนส่งผ่านเน็ต):

$ curl -s localhost:3000/posts/1.json | wc -c
     735
$ curl -s localhost:3000/posts/1 | wc -c
    6010
$ curl -s localhost:3000/posts/1.json | gzip | wc -c
     403
$ curl -s localhost:3000/posts/1 | gzip | wc -c
    2279

ทั้งหน้าใหญ่กว่า JSON ราว 8 เท่า · แต่ถ้าตัดมาเฉพาะการ์ดบทความ (<article> ที่มาจาก _post.html.erb) ซึ่งเป็นข้อมูลชุดเดียวกับ JSON:

$ curl -s localhost:3000/posts/1 | sed -n '/<article/,/<\/article>/p' | wc -c

(sed -n '/A/,/B/p' = พิมพ์เฉพาะบรรทัดตั้งแต่ที่เจอ A จนถึงที่เจอ B · ตรงนี้คือตัดเอาแค่ <article …> ถึง </article>)

คิดว่าการ์ดหนักกว่า JSON (735 byte) กี่เท่า — 1 เท่ากว่า ๆ · 4 เท่า · หรือ 8 เท่า?

$ curl -s localhost:3000/posts/1 | sed -n '/<article/,/<\/article>/p' | wc -c
     943
$ curl -s localhost:3000/posts/1 | sed -n '/<article/,/<\/article>/p' | gzip | wc -c
     514

แค่ราว 1.3 เท่า (บีบแล้วก็ราว 1.3 เท่า) · เพราะของที่หนักจริงคือตัวหนังสือไทย (ตัวละ 3 byte) ซึ่งต้องส่งเท่ากันทั้งสองแบบ · ส่วนที่ HTML เพิ่มมาคือชื่อแท็กกับชื่อคลาส เช่น class="card bg-base-100 shadow-sm"

สิ่งที่วัด (บทความชิ้นที่ 1)ไม่บีบบีบด้วย gzipเทียบกับ JSON
JSON /posts/1.json735403—
เฉพาะการ์ด <article>943514~1.3 เท่า
HTML ทั้งหน้า /posts/16,0102,279~8 เท่า (บีบแล้ว ~5.7 เท่า)

แปลว่าอะไร: HTML หนักกว่าจริง โดยเฉพาะตอนส่งทั้งหน้า · (ตอนรันบนเครื่องเรา Rails ยังแถมคอมเมนต์ <!-- BEGIN app/views/... --> ไว้บอกว่าแต่ละท่อนมาจากไฟล์ไหน · เว็บจริงไม่มี)

แต่ทั้งหน้าก็แค่ราว 2–6 KB (KB = พันกว่า byte) · เทียบกับไฟล์ที่หน้าเดียวกันใช้: tailwind-….css 64,279 byte · turbo.min-….js 105,579 byte · ไฟล์ใหญ่พวกนี้แหละที่ Turbo Drive ทำให้ไม่ต้องโหลดซ้ำ (สองหัวข้อถัดไป)

สิ่งที่ได้คืนมาจากการยอมส่ง HTML: ไม่ต้องมี state management library สักตัว · และโค้ดจัดหน้าตาอยู่ใน ERB ที่เดียว ไม่ต้องเขียนซ้ำทั้งฝั่ง Rails และฝั่ง JS

สไลด์หน้า 36 อ้างว่า "HTML หนักกว่า JSON ประมาณ 2–4 เท่า" · ของจริงขึ้นกับว่าเทียบกับอะไร: เฉพาะการ์ดต่างกันน้อย · ทั้งหน้าต่างกันหลายเท่าแม้บีบแล้ว · ถ้ามีคนเถียงเรื่องนี้ ให้วัดของจริงแบบที่เพิ่งทำ

Turbo มี 3 ชิ้น — แผนที่ของบล็อก C

Turbo แบ่งงานเป็นสามชิ้น ต่างกันที่ขนาดของส่วนที่เปลี่ยน · ตารางนี้คือแผนที่ของบท 130–133 · หลงเมื่อไหร่กลับมาดูตรงนี้

ชิ้นเปลี่ยนแค่ไหนในร้านอาหารเราต้องเขียนอะไรใช้ทำอะไรในแอปเรา
Turbo Drive (บทนี้)ทั้ง <body> เมื่อกดลิงก์หรือส่งฟอร์มเปลี่ยนจานบนโต๊ะเดิม ไม่รื้อโต๊ะจัดใหม่ไม่ต้องเขียนเลย ทำงานเองตั้งแต่ติดตั้งทุกลิงก์ในเว็บ ไม่กระพริบขาว
Turbo Frames (บท 131)เฉพาะกล่องที่ครอบไว้เปลี่ยนแค่ถ้วยใบเดียวบนโต๊ะครอบด้วย turbo_frame_tagแก้หัวข้อบทความในหน้าเดิม
Turbo Streams (บท 132–133)หลายจุดพร้อมกัน · ส่งจาก server มาเองได้ใบสั่งงานหลายข้อให้พนักงานเสิร์ฟไฟล์ .turbo_stream.erb · broadcastคอมเมนต์ใหม่เด้งขึ้นทุกแท็บ

กฎเลือกใช้ (สไลด์หน้า 37): ไล่จากหยาบไปละเอียด แล้วเลือกชิ้นที่หยาบที่สุดที่ยังทำงานได้ · ภาษาคน: เริ่มจากของที่ไม่ต้องเขียนอะไร (Drive) · ไม่พอค่อยครอบกล่อง (Frames) · ยังไม่พอค่อยสั่งหลายจุด (Streams) · ชิ้นที่ละเอียดกว่าทำได้มากกว่า แต่มีโค้ดให้ดูแลมากกว่า

Turbo Drive — ทำงานอยู่แล้วตั้งแต่ Lab แรก

ข่าวดี: เราใช้ Turbo Drive มาตลอดโดยไม่รู้ตัว · ทุกครั้งที่คุณกดลิงก์ในแอปตั้งแต่บท 120 หน้าเว็บไม่ได้โหลดใหม่ทั้งใบ · มันมาจากบรรทัดเดียวในไฟล์ JS หลักของแอป — เปิด app/javascript/application.js:

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"

พิสูจน์ด้วยตัวแปรใน JavaScript

ถ้าหน้าโหลดใหม่จริง เบราว์เซอร์จะทิ้งทุกอย่างของหน้าเก่า รวมถึงตัวแปร JS · ถ้าตัวแปรยังอยู่หลังกดลิงก์ = หน้าไม่ได้โหลดใหม่ · ผมลองใน Chrome: เปิด /posts → พิมพ์ window.marker = "ยังอยู่" ในแท็บ Console ของ DevTools (ช่องพิมพ์ JS ที่ใช้ในบท 129 · window. = ตัวแปรระดับทั้งหน้า) → กดหัวข้อ "นมเย็น" → กด F5 (รีเฟรช)

ตอนไหนURLwindow.markertitle ของแท็บเบราว์เซอร์สร้างหน้าใหม่ไหม
หลังตั้งค่า/posts'ยังอยู่'บทความทั้งหมด—
หลังกดลิงก์/posts/30'ยังอยู่' ✅นมเย็นไม่ — ใบเดิม
หลังกด F5/posts/30undefinedนมเย็นใช่ — ทิ้งของเก่าหมด

URL เปลี่ยน · หัวข้อแท็บเปลี่ยน · เนื้อหาเปลี่ยน — แต่ตัวแปรยังอยู่ · แปลว่าเบราว์เซอร์ไม่ได้เปิดหน้าใหม่ · Turbo แอบไปเอา HTML ของ /posts/30 มาแล้วสลับเนื้อหาให้ (ภารกิจท้ายบทให้คุณลองเองด้วย DevTools)

คุ้นไหม? บท 83 ทำการทดลองเดียวกันกับ <Link> ของ Next.js แล้วตัวแปรก็ยังอยู่ · ความรู้สึกเหมือนกัน คือกดแล้วไม่กระพริบ · ต่างกันที่ Next.js ต้องใช้ <Link> แทน <a> · ส่วน Turbo ดักลิงก์ <a> ธรรมดาทุกอันให้เอง (link_to ที่เราเขียนมาตลอดก็ได้ผลทันที)

request ที่ Turbo ยิง — server ไม่ต้องรู้ด้วยซ้ำ

ผมดักดู request ที่ Turbo ยิงตอนกดลิงก์นั้น · นอกจาก header (ข้อมูลกำกับ request) ที่เบราว์เซอร์ใส่ให้ทุก request อยู่แล้ว (เช่น User-Agent · Cookie) มีสองตัวนี้ที่ Turbo ใส่เอง:

accept: text/html, application/xhtml+xml
x-turbo-request-id: 1987ae28-93a8-4e4c-9672-007c2029345c

ฝั่ง server (log/development.log) เห็นเหมือนการเปิดหน้าธรรมดาทุกอย่าง:

Started GET "/posts/30" for ::1 at 2026-11-07 11:01:59 +0700
Processing by PostsController#show as HTML
Completed 200 OK in 14ms (Views: 8.4ms | ActiveRecord: 2.6ms (2 queries, 0 cached) | GC: 0.0ms)

as HTML · controller เดิม · view เดิม · ส่ง HTML ทั้งหน้าเหมือนเดิม — เราไม่ได้แก้ controller สักบรรทัด · งานทั้งหมดเกิดในเบราว์เซอร์ นี่คือเหตุผลที่ Turbo Drive "ฟรี"

6 ขั้นที่ Turbo Drive ทำตอนเรากดลิงก์

ลำดับตามสไลด์หน้า 38 · ข้างขวาคือสิ่งที่ผมเห็นจริงใน Chrome ตอนกดจาก /posts ไป /posts/30:

กดลิงก์ "นมเย็น" (href="/posts/30") │ ① ดักการกด ไม่ให้เบราว์เซอร์เปลี่ยนหน้าเอง → เบราว์เซอร์ไม่ได้สร้างหน้าใหม่ (ตัวแปร JS ยังอยู่) │ ② fetch "/posts/30" เบื้องหลัง → request ใหม่มีแค่ /posts/30 ตัวเดียว │ server ตอบ HTML ทั้งหน้า (มี <head> ด้วย) ▼ ③ เอา <body> ของหน้าใหม่มาแทน <body> เดิม → navbar · main · ทุกอย่างใน body เป็นชิ้นใหม่ ④ เทียบ <head> ใส่เพิ่มเฉพาะที่ต่าง → title เปลี่ยนเป็น "นมเย็น" · <link> CSS เป็นตัวเดิม │ ไม่ได้โหลด CSS/JS ซ้ำ ⑤ เปลี่ยน URL ด้วย History API → แถบที่อยู่เป็น /posts/30 · history.length +1 ⑥ เก็บภาพหน้าเดิมไว้ใน cache สำหรับปุ่ม back → กด back: หน้าเก่าโผล่ทันที ไม่มี request ใหม่ใน log

Turbo เรียกการเปลี่ยนหน้าหนึ่งครั้งว่า visit · กดลิงก์ = ขอ server ใหม่ (ขั้น ①–⑥) · กดปุ่ม back = เอาภาพจากขั้น ⑥ มาโชว์ (ถ้ามีภาพเก็บไว้ ไม่ต้องขอ server)

ขั้น ④ มีข้อยกเว้น: data-turbo-track="reload"

จำป้ายใน <head> จากบท 127 ได้ไหม: <link rel="stylesheet" href="/assets/application-8b441ae0.css" data-turbo-track="reload" /> · ป้ายนี้บอก Turbo ว่า "ถ้าหน้าใหม่ชี้ไปไฟล์ชื่ออื่น ให้โหลดใหม่ทั้งหน้า"

ผมลองแก้ app/assets/stylesheets/application.css (เติมคอมเมนต์บรรทัดเดียว) ระหว่างที่หน้าเปิดอยู่ แล้วกดลิงก์ "บทความ" · ผล: ชื่อไฟล์ใหม่เป็น application-a9162a1a.css (เลขของคุณจะต่าง ขึ้นกับสิ่งที่เติม) และ window.marker หายไป = Turbo ยอมโหลดใหม่ทั้งหน้า · นี่คือการทำงานร่วมกับ fingerprint ของบท 127: เลขรุ่นใหม่บนปกเมนู → ร้านรู้ว่าต้องจัดโต๊ะใหม่ทั้งโต๊ะ ผู้ใช้จะไม่ค้างอยู่กับ CSS เก่า

ฝั่ง JS ก็เหมือนกัน: <script type="importmap"> (สมุดรายชื่อของบท 127) ก็ติดป้ายนี้ · แก้ application.js เมื่อไหร่ การกดลิงก์ครั้งถัดไปจะโหลดใหม่ทั้งหน้าเหมือนกัน (ลองแล้ว) — จำไว้ใช้ในการทดลองข้างล่าง

🍽 เทียบให้เห็นภาพ — เปลี่ยนจาน ไม่รื้อโต๊ะ

เว็บแบบเก่า: ลูกค้าสั่งเมนูใหม่ทีไร พนักงานรื้อโต๊ะทั้งตัว ผ้าปู ช้อนส้อม แก้วน้ำ แล้วจัดใหม่หมด (หน้าจอกระพริบขาว · โหลด CSS/JS ใหม่) · Turbo Drive: ครัวยังเป็นเจ้าของสมุดสต็อกเล่มเดียวเหมือนเดิม · พนักงานแค่ยกจานเก่าออก วางจานใหม่ (<body>) · ผ้าปูกับช้อนส้อม (<head> · CSS · JS) อยู่ที่เดิม · ถ้าเปลี่ยนแค่ป้ายชื่อเมนู (title) ก็เปลี่ยนแค่ป้าย

ที่ไม่เหมือนกัน: โต๊ะจริงพนักงานเก็บเศษของบนโต๊ะทิ้งให้ด้วย · แต่ Turbo ไม่ได้เก็บของที่ JavaScript ทิ้งไว้ — ตัวแปร (อย่าง window.marker) และงานที่ JS ฝากไว้กับ "ตัวโต๊ะ" (ทั้งหน้า) ยังอยู่ครบ · ดีตรงที่จำค่าได้ · เสียตรงที่มันสะสม (หัวข้อ ★ ข้างล่าง) · และถ้าเมนูเล่มพิมพ์รุ่นใหม่ (ไฟล์ที่ติด data-turbo-track เปลี่ยนชื่อ) ร้านจะยอมรื้อโต๊ะจัดใหม่ทั้งโต๊ะ

🗣 ศัพท์ที่เพิ่งเรียนไป
Turbo Drive
ส่วนของ Turbo ที่ดักทุกลิงก์และฟอร์ม แล้วเปลี่ยนหน้าด้วยการสลับ <body> แทนการโหลดหน้าใหม่ · ถาม senior ได้ว่า “ลิงก์นี้ไปผ่าน Turbo Drive หรือเป็น full page load”
visit (application visit · restoration visit)
คำที่ Turbo เรียกการเปลี่ยนหน้าหนึ่งครั้ง · กดลิงก์ = application visit (ขอ server ใหม่) · กด back/forward = restoration visit (ใช้ภาพจาก cache ถ้ามี ไม่ต้องขอ server) · พิมพ์บอก AI ได้ว่า “this bug only happens on restoration visits (back button)”
full page load
เบราว์เซอร์ทิ้งหน้าเดิมแล้วโหลดใหม่ทั้งใบ — แบบที่เกิดตอนกด F5 · ตัวแปร JS หายหมด · ถาม senior ได้ว่า “ตรงนี้ควรบังคับ full page load ไหม”
data-turbo-track="reload"
ป้ายบน CSS/JS ใน <head> ให้ Turbo โหลดใหม่ทั้งหน้าถ้าไฟล์เปลี่ยนรุ่น (ชื่อไฟล์เปลี่ยน) · Rails ใส่ให้แล้วใน layout · ถาม AI ได้ว่า “after deploying new CSS, will Turbo reload users' pages because of data-turbo-track?”

สั่งให้มันหยุดยุ่ง: data-turbo="false" และ data-turbo-action="replace"

บางลิงก์ไม่อยากให้ Turbo จัดการ หรืออยากให้ทำต่างจากปกติ · ใส่ป้าย data-… บนลิงก์นั้นได้ · ใน ERB ใส่ผ่าน data: { … } ของ link_to (บท 103) · ลองแก้สองไฟล์นี้ (ทดลองแล้วจะ git restore ทิ้ง)

วิธีแก้: หาบรรทัด link_to เดิม แล้วเติม , data: { … } ต่อท้ายก่อน %> · ส่วนอื่นของบรรทัดเหมือนเดิมทุกตัว

app/views/shared/_navbar.html.erb — ลิงก์ "หมวด":

<%= link_to "หมวด", categories_path, class: "btn btn-ghost btn-sm", data: { turbo: false } %>

app/views/shared/_pagination.html.erb — ลิงก์ "ถัดไป":

<%= link_to "ถัดไป →", pagy.page_url(:next), class: "join-item btn btn-sm", data: { turbo_action: "replace" } %>

HTML ที่ได้ออกมา (curl -s localhost:3000/posts · สองบรรทัดนี้อยู่คนละที่ในหน้า ผมตัดมาวางคู่กัน):

<a class="btn btn-ghost btn-sm" data-turbo="false" href="/categories">หมวด</a>
<a class="join-item btn btn-sm" data-turbo-action="replace" href="/posts?page=2">ถัดไป →</a>

ผลใน Chrome (เริ่มที่หน้าหมวด /categories/1 แล้วกดตามลำดับ):

กดอะไรURL หลังกดhistory.lengthwindow.marker
(เริ่ม)/categories/12ยังอยู่
"บทความ" (ลิงก์ปกติ)/posts3 (+1)ยังอยู่
"ถัดไป" (replace)/posts?page=23 (ไม่เพิ่ม)ยังอยู่
"ถัดไป" (replace)/posts?page=33 (ไม่เพิ่ม)ยังอยู่
ปุ่ม back ของเบราว์เซอร์/categories/1 ← ข้ามหน้า 1–3 ไปเลย3ยังอยู่
"หมวด" (data-turbo="false")/categories3หายไป (โหลดใหม่ทั้งหน้า)

(history.length = จำนวนรายการในประวัติของแท็บ · ของผมเริ่มที่ 2 เพราะแท็บเปิดหน้าว่างไว้ก่อน ของคุณอาจเริ่มที่เลขอื่น — ดูที่การเพิ่ม)

★ ผลข้างเคียงที่ทุกคนต้องเจอ: DOMContentLoaded ยิงครั้งเดียว

ก่อนไปต่อ ต้องรู้จักสองคำของ JavaScript ฝั่งเบราว์เซอร์:

ชื่อ DOMContentLoaded มาจาก DOM = โครงของหน้าที่เบราว์เซอร์สร้างจาก HTML ให้ JS หยิบจับได้ (แท็กแต่ละตัวกลายเป็น element หนึ่งชิ้น) · แปลตรง ๆ คือ "สร้างโครงหน้าจาก HTML เสร็จแล้ว"

สมัยก่อนมี Turbo โค้ด JS แทบทุกตัวเริ่มด้วย "รอให้ HTML อ่านเสร็จ (DOMContentLoaded) แล้วค่อยไปหา element มาผูกงาน" — บทความสอน JS เก่า ๆ และโค้ดที่ AI เขียนให้ก็ยังเป็นแบบนี้บ่อย · เทียบกับ React คือคล้าย useEffect(() => { … }, []) ที่ทำครั้งเดียวหลังวาดจอ (บท 74)

ปัญหาคือ Turbo ไม่เคยทิ้งหน้าเดิม (เราเพิ่งพิสูจน์) · หน้าเว็บ "เกิด" แค่ครั้งเดียวตอนเปิดเว็บครั้งแรก · Turbo จึงมี event ของตัวเองชื่อ turbo:load ที่ยิงทั้งตอนเปิดครั้งแรกและทุกครั้งที่ Turbo เปลี่ยนหน้า · ลองดูว่าสองตัวนี้ยิงกี่ครั้ง

เพิ่มต่อท้าย app/javascript/application.js (ทดลองแล้วลบ):

// ทดลองบท 130 — ลบทิ้งเมื่อจบบท
document.addEventListener("DOMContentLoaded", () => {
  console.log("DOMContentLoaded ยิงแล้ว")
})
document.addEventListener("turbo:load", () => {
  console.log("turbo:load ยิงแล้ว", location.pathname)
})

บันทึกไฟล์ → เปิด localhost:3000/posts แล้วกด F5 หนึ่งครั้ง (ให้ได้ JS ตัวใหม่ตั้งแต่ต้น · ถ้าไม่กด การกดลิงก์ครั้งแรกจะโหลดใหม่ทั้งหน้าเพราะ data-turbo-track แล้วตัวเลขจะเพี้ยน) → เปิดแท็บ Console (F12 หรือ Cmd+Option+I บน Mac) → กดลิงก์บน navbar: หมวด → บทความ → หมวด

ใน Console จะมีกี่บรรทัด · DOMContentLoaded กี่บรรทัด · turbo:load กี่บรรทัด?

ผลจริงใน Chrome — 5 บรรทัด: DOMContentLoaded 1 · turbo:load 4 (เปิดครั้งแรก 1 + กดลิงก์ 3)

DOMContentLoaded ยิงแล้ว
turbo:load ยิงแล้ว /posts
turbo:load ยิงแล้ว /categories
turbo:load ยิงแล้ว /posts
turbo:load ยิงแล้ว /categories

ถ้าคุณกด F5 อีกที จะได้ DOMContentLoaded เพิ่มอีกบรรทัด — เพราะ F5 คือ full page load

โค้ด JS แบบดั้งเดิม — พังตั้งแต่หน้าที่สอง

ลบโค้ดเมื่อกี้ แล้วลองแบบที่สไลด์หน้า 40 ยกมา (ผมเปลี่ยน .menu เป็น main เพราะแอปเราไม่มีเมนูชื่อนั้น) · ทุกครั้งที่แก้ application.js ให้กด F5 ก่อนเริ่มลอง (เหตุผลเดียวกับข้างบน):

// ทดลองบท 130 — ลบทิ้งเมื่อจบบท
document.addEventListener("DOMContentLoaded", () => {
  document.querySelector("main").addEventListener("click", () => {
    console.log("คลิกใน main")
  })
})

ผลจริงใน Chrome (คลิกที่ว่าง ๆ ในหน้า ไม่โดนลิงก์):

ทำอะไรConsole ขึ้น
เปิด /posts · คลิกในหน้าคลิกใน main ✅
กด "หมวด" · คลิกในหน้า(ไม่มีอะไรขึ้น) ❌
กด "บทความ" กลับมา /posts · คลิกในหน้า(ไม่มีอะไรขึ้น) ❌ — แม้จะเป็นหน้าเดิมก็ตาม

ทำไม: DOMContentLoaded ยิงครั้งเดียว · ตอนนั้นมันผูกงานไว้กับ <main> ตัวแรก · พอ Turbo เปลี่ยนหน้า <body> ทั้งก้อนเป็นของใหม่ (ขั้น ③) → <main> ตัวใหม่ไม่มีใครผูกงานให้ · ไม่มี error สักบรรทัด ปุ่มแค่ "ตาย" เงียบ ๆ

ท่าแก้เฉพาะหน้า: turbo:load — ใช้ได้ แต่ระวังงานสะสม

เปลี่ยนคำว่า "DOMContentLoaded" ในโค้ดข้างบนเป็น "turbo:load" · ผลจริง: คลิกในหน้าแล้วขึ้น คลิกใน main ครบทุกหน้า หน้าละหนึ่งบรรทัด ✅ · เพราะทุกครั้งที่เปลี่ยนหน้า มันไปผูกงานให้ <main> ตัวใหม่

แต่ถ้างานที่ผูกไว้ฝากไว้กับของที่ไม่เคยถูกเปลี่ยน — เช่น document (ตัวแทนทั้งหน้า ซึ่ง Turbo ไม่เคยทิ้ง) — มันจะสะสม · งานแบบนี้มีจริงบ่อย เช่น "กด Esc แล้วปิดเมนู" หรือ "คลิกที่อื่นแล้วปิด dropdown" · ลองโค้ดนี้แทน:

// ทดลองบท 130 — ลบทิ้งเมื่อจบบท
let round = 0
document.addEventListener("turbo:load", () => {
  round = round + 1
  const myRound = round
  document.addEventListener("keydown", (event) => {
    console.log("กดปุ่ม", event.key, "— listener ตัวที่", myRound)
  })
})

ผลจริงใน Chrome — เปิด /posts กด k → กด "หมวด" → กด "บทความ" → กด k อีกครั้ง:

ทำอะไรConsole ขึ้น
เปิด /posts · กด kกดปุ่ม k — listener ตัวที่ 1
กด "หมวด" · กด "บทความ"(ไม่มีอะไรขึ้น)
กด k ครั้งเดียวกดปุ่ม k — listener ตัวที่ 1
กดปุ่ม k — listener ตัวที่ 2
กดปุ่ม k — listener ตัวที่ 3

กดครั้งเดียว ทำงาน 3 รอบ · เพราะทุก turbo:load ฝากงานใหม่ไว้กับ document ตัวเดิม และไม่มีใครถอดของเก่าออก · ถ้างานนั้นคือ "สลับเมนูเปิด/ปิด" กด 3 รอบ = เปิด-ปิด-เปิด · ยิ่งเปิดหลายหน้ายิ่งเพี้ยน และ refresh ทีเดียวอาการหายเป็นปลิดทิ้ง (เลยหาบั๊กยาก)

🔔 เทียบให้เห็นภาพ — กระดิ่งที่ผูกเพิ่มทุกครั้งที่เสิร์ฟจานใหม่

พนักงานคนหนึ่งมีนิสัย: ทุกครั้งที่วางจานใหม่ จะผูกกระดิ่งเรียกพนักงานเพิ่มอีกอันไว้ที่ขาโต๊ะ · โต๊ะไม่เคยถูกรื้อ (Turbo ไม่ทิ้ง document) → เสิร์ฟไป 3 จาน มีกระดิ่ง 3 อัน → ลูกค้ากดครั้งเดียว พนักงานวิ่งมา 3 คน · ส่วนกระดิ่งที่ติดไว้บนจาน (<main>) ถูกยกออกไปพร้อมจานเก่า จึงไม่สะสม

ที่ไม่เหมือนกัน: กระดิ่งจริงเรามองเห็นว่ามีกี่อัน · listener ใน JS มองไม่เห็น ไม่มี error ไม่มีคำเตือน เห็นแค่อาการ "ทำงานเกิน" · และในร้านจริงพนักงานคนต่อไปอาจเอะใจถอดกระดิ่งเก่าออก แต่โค้ดของเราไม่ถอดให้ จนกว่าเราจะเขียนสั่งเอง

ทางออกจริง: เปลี่ยนวิธีคิด

โค้ดทั้งหมดข้างบนเชื่อว่า "หน้าเว็บเกิดครั้งเดียวแล้วตาย" ซึ่งจริงในยุคที่ทุกลิงก์โหลดหน้าใหม่ · พอมี Turbo ความเชื่อนี้ไม่จริงอีกต่อไป · ทางออกที่สไลด์เสนอคือกลับทิศ:

ผู้ดูแลคนนั้นคือ Stimulus (บท 134) · และคุณใช้มันไปแล้ว: ปุ่ม 🌓 ใน navbar ของบท 129 เขียนว่า data-controller="theme" data-action="theme#toggle" — ปุ่มประกาศเองว่า "ฉันใช้ JS ชื่อ theme · ถ้าโดนคลิกให้เรียก toggle" · ปุ่มนี้จึงใช้ได้ทุกหน้าโดยไม่ต้องสนว่า Turbo เปลี่ยนหน้าไปกี่รอบ (ลองกดลิงก์ไปมาแล้วกด 🌓 ดู)

เทียบกับ React: คล้าย useEffect ที่มีทั้งงานตอนติดตั้ง และ return () => … ไว้เก็บกวาด (cleanup · บท 74) · ต่างกันที่ Stimulus ผูกกับการโผล่/หายของ element ใน HTML ไม่ใช่ของ component

🗣 ศัพท์ที่เพิ่งเรียนไป
event · event listener
event = เหตุการณ์ที่เบราว์เซอร์ประกาศ · listener = ฟังก์ชันที่ฝากไว้ด้วย addEventListener ให้ทำงานเมื่อเกิด event · ถาม senior ได้ว่า “listener ตัวนี้ผูกกับ element หรือกับ document ครับ”
DOMContentLoaded · turbo:load
DOMContentLoaded ยิงครั้งเดียวต่อการโหลดหน้าเต็ม · turbo:load ยิงตอนเปิดครั้งแรกและทุกครั้งที่ Turbo เปลี่ยนหน้า (รวมกด back) · พิมพ์บอก AI ได้ว่า “this app uses Turbo Drive; don't rely on DOMContentLoaded”
duplicate listeners · listener leak
listener ที่ถูกผูกซ้ำจนทำงานหลายรอบ หรือค้างอยู่ทั้งที่ไม่ใช้แล้ว · ถาม senior ได้ว่า “ปุ่มนี้ทำงานสองรอบหลังเปลี่ยนหน้า เป็น duplicate listener จาก turbo:load ไหม”
Stimulus (ชื่อไว้ก่อน)
เครื่องมือที่ให้ HTML ประกาศว่าต้องการ JS อะไร แล้วติดตั้ง/เก็บกวาดให้เองเมื่อ element โผล่/หาย · ละเอียดบท 134 · พิมพ์บอก AI ได้ว่า “write this as a Stimulus controller, not a turbo:load listener”

🧹 เก็บกวาดก่อนไปต่อ: ลบโค้ดทดลองทั้งหมดใน application.js · _navbar · _pagination ด้วย git restore . · แล้ว git status ต้องขึ้นว่า nothing to commit, working tree clean · บท 131 เริ่มจากสถานะนี้

ภารกิจ

🎯 ภารกิจ — จับ Turbo Drive คาหนังคาเขาด้วย DevTools (~10 นาที)

ทำในแอปของบท 129 (โค้ดสะอาด ไม่มีของทดลองค้าง) · ใช้ Chrome

  1. รัน bin/dev · เปิด http://localhost:3000/posts
  2. เปิด DevTools (คลิกขวาที่หน้า → Inspect · หรือ F12 · Mac กด Cmd+Option+I) → ไปแท็บ Network
  3. กด F5 หนึ่งครั้ง → ดูรายการ: ควรเห็นไฟล์ .css และ .js ที่มีเลขรุ่นต่อท้าย (เช่น tailwind-….css · turbo.min-….js)
  4. ล้างรายการ (ปุ่มรูปวงกลมขีดทับ 🚫 ด้านบนแท็บ Network) → กดหัวข้อบทความหนึ่งชิ้น → นับว่ามีแถวใหม่อะไรบ้าง
  5. ล้างอีกที → กด "หมวด" บน navbar → นับแถวใหม่
  6. ไปแท็บ Console พิมพ์ window.marker = "ยังอยู่" กด Enter → กดลิงก์ไปหน้าอื่น → พิมพ์ window.marker กด Enter

ก่อนกดเฉลย: ข้อ 4–5 คิดว่าจะเห็นไฟล์ .css/.js ไหม · และข้อ 6 ได้อะไร?

สิ่งที่ควรเห็น (ตรวจด้วย Chrome จริง — ผมนับจากรายการ resource ของหน้า ซึ่งเป็นรายการเดียวกับที่แท็บ Network โชว์):

ข้อแถวใหม่
3 (F5)CSS 2 ไฟล์ · JS 8 ไฟล์ (application · turbo · stimulus · controllers …) · icon 2 ไฟล์ · และของ rack-mini-profiler
4 (กดบทความ)หน้าที่กดหนึ่งแถว (เช่น 30 · ชนิด fetch) + แถว results ของ rack-mini-profiler (ผมเห็น 3 แถว) · ไม่มี .css / .js
5 (กดหมวด)แบบเดียวกัน: categories หนึ่งแถว (+ results) · ไม่มี .css / .js
6'ยังอยู่' — หน้าไม่ได้โหลดใหม่

แถว results มาจาก rack-mini-profiler (ป้ายเวลาเล็ก ๆ มุมซ้ายบนของหน้า ที่มากับ repo ของอาจารย์ · ใช้ใน development เท่านั้น) ไม่เกี่ยวกับ Turbo · ชนิดเป็น fetch คือหลักฐานว่า JS (Turbo) เป็นคนขอ ไม่ใช่เบราว์เซอร์เปิดหน้าเอง

ถ้าเจออาการนี้ แปลว่า…

  • กดลิงก์แล้ว .css/.js โผล่มาครบทุกครั้ง และ window.marker ได้ undefined — Turbo ไม่ได้ทำงาน · เปิด Console ดูว่ามีตัวแดงไหม · ผมลองลืมปิดวงเล็บใน application.js ได้ SyntaxError: missing ) after argument list แล้ว Turbo ไม่โหลดเลย ลิงก์ทุกอันกลายเป็นโหลดหน้าเต็ม (และปุ่ม 🌓 ก็พังด้วย) · แก้ไฟล์ให้ถูกหรือ git restore
  • เพิ่งแก้ CSS หรือ JS แล้วกดลิงก์ครั้งแรก หน้าโหลดใหม่ทั้งใบ — ไม่ใช่บั๊ก · คือ data-turbo-track="reload" เห็นว่าไฟล์เปลี่ยนรุ่น · ครั้งต่อไปจะกลับมาเป็นปกติ
  • มีลิงก์เดียวที่โหลดเต็มทุกครั้ง — ลิงก์นั้นอาจยังมี data-turbo="false" ค้างจากการทดลอง · git status ดูว่ามีไฟล์ค้างไหม

เห็นไม่ตรงกับตาราง หรือหาปุ่มใน DevTools ไม่เจอ ถามกลับมาได้เลย พร้อมแคปหน้าจอแท็บ Network

ทดสอบความเข้าใจ

Redux · React Query · SWR ต่างกันในรายละเอียด แต่สไลด์บอกว่าทุกตัว "เกิดมาแก้ปัญหาเดียวกัน" · ปัญหานั้นคืออะไร และ Hotwire แก้อย่างไร?

ตัวเลือกที่ 3 ถูก — เครื่องมือพวกนั้นช่วยดูแล "สมุดเล่มที่สอง" ให้ตรงกับครัว · Hotwire ตัดปัญหาที่ต้นทาง: อยากได้จอใหม่ก็ไปขอ HTML ใหม่จาก server ที่เป็นเจ้าของข้อมูลชุดเดียว

ตัวเลือกที่ 1 ผิดสองชั้น — HTML หนักกว่า JSON (เราวัดแล้ว) และ Hotwire ไม่ได้ขายความเล็ก · ตัวเลือกที่ 2 ผิด เพราะ Hotwire ให้ server วาด (ERB) ไม่ได้วาดใน JS · ตัวเลือกที่ 4 ผิด เพราะ server ไม่ได้ส่งผิด ปัญหาคือสำเนาที่เก็บไว้เก่า

จุดที่ต้องจำ: ไม่มีสำเนา = ไม่มีบั๊ก sync · ราคาที่จ่ายคือต้องถาม server ทุกครั้ง และ HTML หนักกว่า

ในแอปของเรา กดลิงก์จาก /posts ไป /posts/30 · ข้อไหนบอกสิ่งที่เกิดขึ้นได้ถูก?

ตัวเลือกที่ 2 ถูก — log ฝั่ง server เป็น Processing by PostsController#show as HTML เหมือนเปิดหน้าธรรมดา · Turbo เป็นคนเลือกเอาแค่ <body> กับส่วนของ <head> ที่ต่าง (เช่น title)

ตัวเลือกที่ 1 ผิด เพราะ server ส่งทั้งหน้า (มี <head>) และ CSS ไม่โหลดใหม่ · ตัวเลือกที่ 3 ผิด เพราะ request ขอ text/html ไม่มี JSON เลย · ตัวเลือกที่ 4 ผิด เพราะถ้าโหลดหน้าใหม่ทั้งใบ ตัวแปรต้องหาย (แบบตอนกด F5) — ที่ตัวแปรยังอยู่ก็เพราะไม่ได้โหลดใหม่

จุดที่ต้องจำ: Turbo Drive = เปลี่ยนจาน ไม่รื้อโต๊ะ · server ไม่ต้องรู้อะไรเลย

โค้ด document.addEventListener("DOMContentLoaded", …) ที่ผูก click ให้ <main> ทำงานหน้าแรก แต่กดลิงก์ไปหน้าอื่นแล้วคลิกไม่ได้ · เพราะอะไร?

ตัวเลือกที่ 1 ถูก — หน้าเว็บไม่ได้เกิดใหม่ DOMContentLoaded จึงไม่ยิงอีก · Turbo สลับ <body> ทั้งก้อน → <main> ที่เคยผูกไว้ถูกยกออกไปพร้อมจานเก่า

ตัวเลือกที่ 2 ผิด เพราะ addEventListener ใช้ได้ปกติ (ตัวอย่าง turbo:load ก็ใช้มัน) แค่ต้องรู้ว่าผูกเมื่อไหร่กับอะไร · ตัวเลือกที่ 3 ผิด เพราะ JS ไม่ได้ถูกโหลดใหม่เลยด้วยซ้ำ ปัญหาไม่ใช่โค้ดเก่า/ใหม่ · ตัวเลือกที่ 4 ผิด เพราะทุกหน้าที่ server ส่งมามี layout ครบ (มี <main>) — แค่เป็นตัวใหม่

จุดที่ต้องจำ: ใน Turbo หน้าเว็บ "เกิด" ครั้งเดียว · แต่ <body> เกิดใหม่ทุกครั้งที่เปลี่ยนหน้า

เพื่อนเขียน document.addEventListener("turbo:load", () => { document.addEventListener("keydown", (event) => { ปิดเมนู() }) }) แบบเดียวกับตัวอย่างในบท · เปิดเว็บ แล้วเปลี่ยนหน้าอีก 4 ครั้ง แล้วกด Esc ครั้งเดียว · "ปิดเมนู" ทำงานกี่รอบ?

ตัวเลือกที่ 4 ถูก — turbo:load ยิงตอนเปิดครั้งแรก 1 + เปลี่ยนหน้า 4 = 5 ครั้ง · ทุกครั้งฝาก listener ใหม่ไว้กับ document ตัวเดิม (ในบทเปลี่ยนหน้า 2 ครั้งได้ 3 ตัว แบบเดียวกัน)

ตัวเลือกที่ 1 ผิด เพราะไม่มีใครล้างให้ — นี่คือปัญหาทั้งหมด · ตัวเลือกที่ 2 ผิด เพราะ Turbo ไม่เคยทิ้ง document (เปลี่ยนแค่ <body>) · ตัวเลือกที่ 3 ลืมว่า turbo:load ยิงตอนเปิดเว็บครั้งแรกด้วย

จุดที่ต้องจำ: ผูกงานกับของที่อยู่ถาวร (document · window) ใน turbo:load = สะสม · ทางออกระยะยาวคือ Stimulus ที่ติดตั้งและเก็บกวาดให้ (บท 134)

ถ้าเปิดสไลด์ดูประกอบ

หน้าสไลด์เขียนของจริง (Turbo 8.0.23)ต้องสนใจไหม
36HTML หนักกว่า JSON ประมาณ 2–4 เท่า (gzip แล้วต่างกันน้อยมาก)วัดบทความชิ้นที่ 1: เฉพาะการ์ด ~1.3 เท่า · ทั้งหน้า ~8 เท่า (gzip แล้ว ~5.7 เท่า)นิดหน่อย — ตัวเลขขึ้นกับว่าเทียบกับอะไร วัดเองได้
38ขั้น 4: merge <head> เฉพาะที่ต่าง (CSS/JS ไม่โหลดซ้ำ)ถูก · แต่ถ้าไฟล์ที่ติด data-turbo-track="reload" เปลี่ยนชื่อ Turbo จะโหลดใหม่ทั้งหน้า (ลองแล้ว)สนใจ — อธิบายอาการ "แก้ CSS แล้วหน้าโหลดเต็มครั้งหนึ่ง"
38ตัวอย่าง <a href="/report.pdf" data-turbo="false"> เขียนเป็น HTML ตรง ๆใน ERB ของเราใช้ link_to …, data: { turbo: false } ได้ HTML เดียวกันไม่ต้อง — ความหมายเดียวกัน
40ใช้ turbo:load ผูกกับ .menu → "กด 10 หน้าแล้วกดเมนูครั้งเดียว toggle 10 รอบ"ลองแล้ว: ผูกกับ element ใน <body> ไม่สะสม (body เป็นของใหม่ทุกหน้า) · สะสมจริงเมื่อผูกกับของที่อยู่ถาวร เช่น document (เห็น 3 รอบหลังเปลี่ยน 2 หน้า)สนใจ — หลักการถูก แต่ตัวอย่าง .menu อาจไม่สะสมตามที่สไลด์บอก
38Turbo.session.drive = false ปิดทั้งแอปไม่ได้ลองในบทนี้ไม่ต้อง — สไลด์เองก็บอกว่าไม่แนะนำ

อ้างอิง