🔁 ทวนก่อนเริ่ม — คำจากบทก่อน ๆ ที่บทนี้ใช้
- layout (บท 103 · 123) — "จานรองมาตรฐาน" ไฟล์
app/views/layouts/application.html.erbที่ห่อทุกหน้า · ส่วน<head>ของมันคือที่ที่บทนี้จะแกะดู curl(บท 99–105) — ขอหน้าเว็บจาก server แล้วโชว์ HTML ดิบ ๆ ที่เบราว์เซอร์ได้รับจริง ·curl -sI= ขอดูแค่ header ไม่เอาเนื้อ (header = "หัวจดหมาย" ข้อมูลกำกับที่ server แนบมาก่อนตัวไฟล์ เช่น ไฟล์นี้ชนิดอะไร ใหญ่เท่าไหร่ — บท 56)- Tailwind (บท 72) — เขียนคลาสสั้น ๆ อย่าง
text-3xlใน HTML แล้วเครื่องมือสร้างไฟล์ CSS ให้ · ในแอปนี้ไฟล์ที่สร้างได้คือapp/assets/builds/tailwind.css(สร้างโดยbin/devหรือbin/rails tailwindcss:build· ละเอียดในบท 128) npm install·node_modules·npm run build(บท 70) — ฝั่ง Next.js ต้องโหลด library มากองในเครื่องก่อน และมีขั้น build ก่อนขึ้นเครื่องจริง · deploy (บท 109) = เอาโค้ดขึ้นเครื่อง server จริง (production) ให้คนอื่นใช้import … from "…"(บท 63–71) — ดึงโค้ดจากไฟล์อื่นหรือ library อื่นมาใช้ใน JavaScript/TypeScript
🧰 ตรวจด้วย Ruby 4.0.9 · Rails 8.1.4 · propshaft 1.3.2 · importmap-rails 2.2.3 · tailwindcss-rails 4.7.0 (Tailwind 4.3.3) (สไลด์และ repo ของอาจารย์ใช้ Ruby 4.0.5–4.0.6 · Rails 8.1.3 — รุ่นแก้บั๊กในสายเดียวกัน เรื่องในบทนี้ไม่ต่างกัน)
🗂 โค้ดเริ่มจาก: แอปหลังจบ Lab บท 126 (ตามไม่ทันดูบท 126) · บทนี้ทดลองแล้วทิ้ง —
จบบทให้ git restore ไฟล์ที่แก้ + bin/rails assets:clobber + bin/rails tailwindcss:build (บอกอีกทีท้ายภารกิจ)
ผลทุกก้อนรันจริงแล้วก็อปมา · ตัดบรรทัดที่ไม่เกี่ยวเป็น … ·
เลขท้ายชื่อไฟล์ (-8b441ae0) คำนวณจากเนื้อไฟล์ — ถ้าไฟล์ของคุณเหมือนของเราทุกตัวอักษร เลขจะตรงกัน ถ้าต่างแม้ตัวเดียว เลขจะต่าง (เดี๋ยวรู้ว่าทำไม)
asset คืออะไร — ของในร้านที่ไม่ได้ทำใหม่ทุกจาน
หน้าเว็บหนึ่งหน้าไม่ได้มีแค่ HTML · HTML บอกว่ามีอะไรบนหน้า แต่หน้าตามาจากไฟล์ CSS · การขยับได้มาจากไฟล์ JavaScript ·
และยังมีรูป ฟอนต์ ไอคอนอีก
ไฟล์พวกนี้ที่ไม่ใช่ HTML เรียกรวม ๆ ว่า asset · ต่างจาก HTML ตรงที่ทุกคนได้ไฟล์เดียวกันเป๊ะ ไม่ต้องทำใหม่ต่อคำขอ
เทียบกับร้านอาหาร: HTML คือจานอาหารที่ครัวทำใหม่ทุกออร์เดอร์ (บทความไม่เหมือนกันแต่ละหน้า) · ส่วน asset คือของตกแต่งร้านและเมนูเล่ม — พิมพ์ครั้งเดียว ทุกโต๊ะใช้เล่มเดียวกัน
ในแอปของเรา asset อยู่ตรงนี้:
| ไฟล์/โฟลเดอร์ | คืออะไร |
|---|---|
app/assets/stylesheets/application.css | CSS ที่เราเขียนเอง (ตอนนี้มีแต่คอมเมนต์ ยังว่าง) |
app/assets/builds/tailwind.css | CSS ที่ Tailwind สร้างให้ จากคลาสใน view · ไม่ได้เก็บใน git · ห้ามแก้มือ (สร้างใหม่ทับทุกรอบ) |
app/javascript/ | JavaScript ของเรา (application.js และโฟลเดอร์ controllers/ ของ Stimulus — บท 134) |
app/assets/images/ | รูปภาพ (ตอนนี้ว่าง) |
แล้วเบราว์เซอร์รู้ได้ยังไงว่าต้องไปเอาไฟล์พวกนี้? เพราะ layout เขียนบอกไว้ใน <head> ·
ดูสองบรรทัดนี้ใน app/views/layouts/application.html.erb:
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
stylesheet_link_tag :app— helper ที่เขียนแท็ก<link rel="stylesheet">ให้ ทุกไฟล์ CSS ในแอป (:app= "เอาของแอปทั้งหมด") · ของเรามีสองไฟล์ จึงได้สองบรรทัด"data-turbo-track": "reload"— ป้ายให้ Turbo คอยดูว่าชื่อไฟล์นี้เปลี่ยนไหม ถ้าเปลี่ยนให้โหลดใหม่ทั้งหน้า (Turbo คือตัวที่ทำให้เปลี่ยนหน้าโดยไม่โหลดใหม่ทั้งหน้า — ละเอียดบท 130) · ตอนนี้แค่รู้ว่ามีjavascript_importmap_tags— helper ที่เขียนส่วน JavaScript ทั้งหมดให้ (หัวข้อ importmap ท้ายบท)
เปิด server (bin/dev แบบบท 120) แล้วดูว่าเบราว์เซอร์ได้อะไรจริง — ตัดเอาเฉพาะช่วงนั้นของ <head>:
$ curl -s localhost:3000/posts | sed -n '/rel="stylesheet"/,/type="module"/p'
<link rel="stylesheet" href="/assets/application-8b441ae0.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/tailwind-798e8e3d.css" data-turbo-track="reload" />
<script type="importmap" data-turbo-track="reload">{
"imports": {
"application": "/assets/application-bfcdf840.js",
"@hotwired/turbo-rails": "/assets/turbo.min-9fd88cd5.js",
…
}
}</script>
<link rel="modulepreload" href="/assets/application-bfcdf840.js">
…
<script type="module">import "application"</script>
sed -n '/A/,/B/p' แปลว่า "พิมพ์ตั้งแต่บรรทัดที่มี A ถึงบรรทัดที่มี B" — แค่ตัดให้สั้น ไม่ต้องจำ ·
สังเกตบรรทัดแรก: ไฟล์ของเราชื่อ application.css แต่ในหน้าเว็บกลายเป็น application-8b441ae0.css ·
เลขต่อท้ายนี้คือพระเอกของบท · แต่ก่อนจะรู้ว่ามันมาทำไม ต้องรู้จัก cache ก่อน
ส่วน <script type="importmap"> · modulepreload · <script type="module"> คือส่วนของ JavaScript ·
ตอนนี้ข้ามไปก่อน เดี๋ยวแกะทีละบรรทัดในหัวข้อ importmap · … คือบรรทัดที่เราตัดออกให้สั้น
cache ของเบราว์เซอร์ — ลูกค้าประจำที่จำเมนูเล่มเก่า
เบราว์เซอร์ฉลาดพอจะไม่โหลดไฟล์เดิมซ้ำ ๆ · ไฟล์ไหนเคยโหลดแล้ว มันจะเก็บสำเนาไว้ในเครื่อง ครั้งหน้าใช้สำเนาเลย ไม่ต้องรอเน็ต ·
ที่เก็บสำเนานี้เรียกว่า cache (อ่านว่า "แคช")
เก็บไว้นานแค่ไหน — server เป็นคนบอก ผ่าน header (หัวจดหมาย) ที่แนบมากับไฟล์
ไอเดียเดียวกับ includes ใน Day 2 (บท 114):
ของที่หยิบมาแล้ว อย่าเดินไปขอซ้ำ · ต่างกันแค่ includes ลดการเดินไปตู้เก็บแฟ้ม ส่วน cache ลดการเดินไปขอไฟล์จาก server
เราเปิดหน้าเว็บด้วย Chrome จริง แล้ววัดว่าไฟล์ CSS แต่ละไฟล์ต้องโหลดผ่านเน็ตกี่ byte (0 = ไม่ได้โหลดเลย หยิบจากสำเนา):
| เปิดหน้า | application-8b441ae0.css | tailwind-798e8e3d.css |
|---|---|---|
ครั้งแรก /posts | 791 | 34,446 |
ต่อมา /posts/30 (หน้าอื่น แต่ใช้ CSS ชุดเดียวกัน) | 0 | 0 |
หน้าที่สองไม่โหลด CSS ผ่านเน็ตเลยสักไบต์ · นี่คือสิ่งที่ทำให้เว็บเปิดหน้าถัดไปเร็ว
(ตัวเลขรวม header ด้วย จึงมากกว่าขนาดไฟล์นิดหน่อย · ของคุณดูได้ในแท็บ Network ของ DevTools แบบบท 56 คอลัมน์ Size —
ไฟล์ที่หยิบจากสำเนาจะขึ้นเป็นคำว่า memory cache หรือ disk cache แทนตัวเลข · ถ้าติ๊ก Disable cache ไว้ จะโหลดใหม่ทุกครั้ง)
แต่ข้อดีนี้มีด้านมืด: ถ้าเราแก้ไฟล์ CSS แล้วชื่อไฟล์ยังเหมือนเดิม เบราว์เซอร์จะคิดว่า "อันนี้มีแล้ว" แล้วใช้สำเนาเก่าต่อไป
ลูกค้าประจำมาร้านบ่อยจนจำเมนูได้ขึ้นใจ เลยไม่หยิบเมนูเล่มบนโต๊ะมาเปิดอีก — สั่งจากความจำเลย เร็วดี · วันหนึ่งร้านเปลี่ยนราคาและเพิ่มเมนูใหม่ แต่หน้าปกเมนูเหมือนเดิมทุกอย่าง · ลูกค้าประจำก็ยังสั่งจากความจำ ไม่รู้เลยว่ามีของใหม่
ที่ไม่เหมือนกัน: ลูกค้าคนจริงอาจสังเกตป้ายหน้าร้านหรือถามพนักงานเอง · เบราว์เซอร์ไม่ถามเลย ถ้า server เคยบอกว่า "เก็บไว้ได้หนึ่งปี" มันก็เชื่อตามนั้นหนึ่งปีเต็ม ไม่สนว่าเนื้อในเปลี่ยนไปแล้ว
- asset
- ไฟล์ที่ไม่ใช่ HTML ที่หน้าเว็บต้องใช้: CSS · JavaScript · รูป · ฟอนต์ · ถาม senior ได้ว่า “asset ของโปรเจกต์นี้เสิร์ฟจาก Rails เองหรือผ่าน CDN” (CDN = บริการเก็บไฟล์ไว้หลายประเทศให้โหลดใกล้ ๆ)
- browser cache
- ที่เบราว์เซอร์เก็บสำเนาไฟล์ที่เคยโหลดไว้ เพื่อไม่ต้องโหลดซ้ำ · พิมพ์บอก AI ได้ว่า “after deploy, users still see the old CSS — is it a browser cache problem?”
Cache-Controlheader- บรรทัดในหัวจดหมายที่ server บอกเบราว์เซอร์ว่าเก็บสำเนาได้นานแค่ไหน เช่น
max-age=31536000= เก็บได้ 31,536,000 วินาที (หนึ่งปี) · ถาม senior ได้ว่า “ไฟล์ asset ของเราตั้ง Cache-Control ไว้เท่าไหร่”
สามปัญหาที่ asset pipeline มีไว้แก้
asset pipeline คือชื่อรวมของ "ขั้นตอนที่พาไฟล์ asset จากโฟลเดอร์ของเราไปถึงเบราว์เซอร์" · สไลด์หน้า 24 บอกว่ามันเกิดมาเพื่อแก้สามปัญหาที่ทุกเว็บเจอ ไม่ว่าเขียนด้วยภาษาอะไร:
| ปัญหา | ภาษาคน | วิธีแก้เดิม | ตอนนี้ยังสำคัญไหม |
|---|---|---|---|
| 1 · cache ตามชื่อ | แก้ CSS แล้ว deploy แต่ผู้ใช้ยังเห็นของเก่าเป็นวัน (หัวข้อที่แล้ว) | fingerprinting | สำคัญเสมอ แก้ทางอื่นไม่ได้ |
| 2 · ไฟล์เยอะ = ช้า | สมัยก่อนโหลดได้ทีละไม่กี่ไฟล์พร้อมกัน 50 ไฟล์ต้องต่อคิวนาน | bundling = มัดหลายไฟล์เป็นก้อนเดียว | น้อยลงมาก |
| 3 · ไฟล์ใหญ่เกินจำเป็น | ช่องว่าง คอมเมนต์ ชื่อตัวแปรยาว ๆ เบราว์เซอร์ไม่ได้ใช้ | minify = บีบเอาช่องว่าง/คอมเมนต์ออก · compress = บีบอัดแบบไฟล์ zip | น้อยลงมาก |
ทำไมข้อ 2–3 สำคัญน้อยลง (แค่รู้ไว้ ไม่ต้องลงลึก):
- HTTP/2 — "ภาษาที่เบราว์เซอร์คุยกับ server" รุ่นใหม่ ส่งหลายไฟล์พร้อมกันบนสายเดียวได้ ไฟล์เยอะจึงไม่ต้องต่อคิวเหมือนเมื่อก่อน
- gzip · brotli — วิธีบีบอัดข้อมูลที่ server (หรือตัวกลางหน้า server) ทำให้อัตโนมัติตอนส่ง ไม่ต้องบีบไว้ล่วงหน้าเอง
- ES module — การเขียน
import … fromที่เบราว์เซอร์ทุกตัวเข้าใจเองแล้ว ไม่ต้องมีเครื่องมือมัดรวมให้ก่อน (หัวข้อ importmap)
แต่ข้อ 1 ไม่มีเทคโนโลยีไหนมาช่วย เพราะมันเป็นเรื่องของชื่อ · สไลด์สรุปว่านี่คือเหตุผลที่ Rails 8 ยอมทิ้งเครื่องมือมัดรวมไฟล์ แต่ยังเก็บ fingerprinting ไว้
fingerprinting — เปลี่ยนเนื้อใน ชื่อเปลี่ยนตาม
ทางแก้ข้อ 1 ง่ายจนน่าตกใจ: ถ้าเนื้อไฟล์เปลี่ยน ก็เปลี่ยนชื่อไฟล์ด้วย · ชื่อใหม่ที่เบราว์เซอร์ไม่เคยเห็น = ต้องโหลดใหม่แน่นอน
เลขท้ายชื่อคำนวณจากเนื้อไฟล์ทุกตัวอักษร เรียกว่า hash — คิดว่าเป็น "ลายนิ้วมือของเนื้อไฟล์" ก็ได้:
เนื้อเดิมได้เลขเดิมทุกครั้ง · แก้แม้ตัวเดียวได้เลขใหม่ทันที
/* เปิด /posts แล้ว /posts/30 ไปแล้ว (สำเนาของ CSS ทั้งสองไฟล์อยู่ในเครื่อง) */
/* จากนั้นเพิ่มบรรทัดนี้ใน app/assets/stylesheets/application.css แล้วเปิด /posts อีกครั้ง */
body { background: #fffbeb; }
รอบนี้เบราว์เซอร์ต้องโหลดไฟล์ CSS ไหนผ่านเน็ตใหม่ — application อย่างเดียว · tailwind อย่างเดียว · หรือทั้งคู่?
ผลจริงจาก Chrome (ต่อจากตารางในหัวข้อ cache) — โหลดใหม่แค่ application:
| เปิดหน้า | ไฟล์ application | ไฟล์ tailwind |
|---|---|---|
/posts หลังแก้ CSS | application-ab636f88.css · 822 byte | tailwind-798e8e3d.css · 0 |
ไฟล์ที่ชื่อเปลี่ยนถูกโหลดใหม่ทันที · ไฟล์ที่ชื่อเดิม (34 KB ของ Tailwind) ยังหยิบจากสำเนาได้ ไม่เสียเน็ตเลย ·
และพื้นหลังเป็นสีครีมจริง (Chrome รายงานสี body เป็น rgb(255, 251, 235) = #fffbeb)
นี่คือข้อดีที่ซ่อนอยู่: แก้ไฟล์ไหน ผู้ใช้โหลดใหม่แค่ไฟล์นั้น ที่เหลือยังเร็วเหมือนเดิม
ทีนี้ดูด้วยตาตัวเองผ่าน curl · เปิด app/assets/stylesheets/application.css ไปล่างสุด (ต่อจากบรรทัด */)
เว้นบรรทัดว่างหนึ่งบรรทัด แล้วเพิ่มบรรทัดนี้ (ทำให้พื้นหลังเป็นสีครีม):
*/
body { background: #fffbeb; }
ทำไมต้องเว้นบรรทัดให้ตรง? เพราะเลขคำนวณจากทุกตัวอักษร รวมบรรทัดว่าง · เราลองไม่เว้นบรรทัด ได้ application-6b521159.css แทน ·
ถ้าเลขของคุณไม่ตรงกับในบท ไม่ผิด — ที่ต้องเห็นคือเลขเปลี่ยน
บันทึกไฟล์ แล้วขอหน้าเว็บใหม่ — ไม่ต้อง restart server ไม่ต้องสั่ง build อะไร:
$ curl -s localhost:3000/posts | grep stylesheet
<link rel="stylesheet" href="/assets/application-ab636f88.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/tailwind-798e8e3d.css" data-turbo-track="reload" />
application-8b441ae0กลายเป็นapplication-ab636f88— เพราะเนื้อเปลี่ยนtailwind-798e8e3dเหมือนเดิม — เราไม่ได้แตะไฟล์นั้น เลขจึงไม่เปลี่ยน- ไม่ต้อง build เพราะในเครื่องเรา (development) Rails คำนวณลายนิ้วมือใหม่ทุกครั้งที่มีคนขอหน้า
แล้วชื่อเก่ายังขอได้ไหม? และชื่อใหม่ server แนบหัวจดหมายอะไรมา?
$ curl -sI localhost:3000/assets/application-8b441ae0.css
HTTP/1.1 404 Not Found
…
$ curl -sI localhost:3000/assets/application-ab636f88.css
HTTP/1.1 200 OK
content-type: text/css; charset=utf-8
vary: Accept-Encoding
etag: "ab636f88"
cache-control: public, max-age=31536000, immutable
set-cookie: __profilin=p%3Dt; path=/; httponly; samesite=lax
content-length: 522
404 Not Found— ในเครื่องเรา Rails เสิร์ฟเฉพาะไฟล์ "ฉบับปัจจุบัน" ชื่อเก่าจึงหาไม่เจอแล้วcache-control: public, max-age=31536000, immutable— "เก็บไว้ได้หนึ่งปี และไฟล์ชื่อนี้ไม่มีวันเปลี่ยนเนื้อ (immutable) ไม่ต้องมาถามซ้ำ" · กล้าสั่งแบบนี้ได้เพราะถ้าเนื้อเปลี่ยน ชื่อก็เปลี่ยนไปแล้วcontent-type: text/css= "นี่คือไฟล์ CSS" ·content-length: 522= ขนาด 522 byte- บรรทัดที่เหลือ (
vary·etag·set-cookie) ไม่เกี่ยวกับบทนี้ ข้ามได้
ก่อนไปต่อ ลบบรรทัด body { … } ที่เพิ่งเพิ่มออก (หรือ git restore app/assets/stylesheets/application.css) ·
เนื้อไฟล์กลับเป็นเหมือนตอนแรกทุกตัวอักษร แล้วเลขจะเป็นอะไร? เก็บไว้ทายในภารกิจท้ายบท
ร้านตั้งกฎว่า แก้เมนูเมื่อไหร่ต้องพิมพ์เลขรุ่นใหม่บนปก เช่น "เมนู-8b441ae0" → "เมนู-ab636f88" · ลูกค้าประจำเห็นเลขบนปกไม่ตรงกับที่จำไว้ ก็รู้ทันทีว่าต้องหยิบเล่มใหม่มาอ่าน · ส่วนป้ายตกแต่งผนังที่ไม่ได้แก้ เลขเดิม ลูกค้าก็ไม่ต้องดูซ้ำ
ที่ไม่เหมือนกัน: เลขรุ่นในร้านจริงคนตั้งเองตามใจ (รุ่น 1, 2, 3) · ในเว็บเลขมาจากเนื้อไฟล์ ล้วน ๆ — แก้แล้วแก้กลับ เลขก็กลับเป็นเลขเดิม · และเมนูเล่มเก่าในร้านต้องมีคนเก็บทิ้ง แต่ไฟล์ชื่อเก่าไม่ต้องมีใครไปลบจากเครื่องผู้ใช้ — แค่ไม่มีหน้าไหนเรียกถึงมันอีก
สไลด์หน้า 25 ให้จำประโยคนี้ไว้: "ไม่ต้องไปลบของเก่า — ให้ key เปลี่ยน แล้วของเก่าจะหมดความหมายไปเอง" · key ในที่นี้คือชื่อไฟล์ · หลักการเดียวกันนี้จะกลับมาอีกครั้งตอนเรียนเรื่อง cache ของหน้าเว็บใน Day 3 ครึ่งหลัง (ยังไม่มาในชุดนี้)
📝 สังเกตว่าแม้ในเครื่องเรา (development) ไฟล์ก็ได้ "เก็บได้หนึ่งปี" และ Chrome ก็เก็บสำเนาจริง (ตาราง 0 byte ข้างบน) · ไม่มีปัญหาเพราะชื่อเปลี่ยนเองทุกครั้งที่แก้ไฟล์ — แก้แล้ว refresh ก็เห็นของใหม่
- fingerprinting · digest
- ต่อเลขที่คำนวณจากเนื้อไฟล์ไว้ท้ายชื่อ (
application-8b441ae0.css) · เลขนั้นเรียกว่า digest หรือ hash · ถาม senior ได้ว่า “ไฟล์นี้ผ่าน fingerprinting หรือยัง ทำไมชื่อไม่มี digest” - cache busting
- ชื่อเรียกเทคนิค "บังคับให้เบราว์เซอร์ทิ้งสำเนาเก่า" ซึ่ง fingerprinting เป็นวิธีที่นิยมที่สุด · พิมพ์บอก AI ได้ว่า “use digest-based cache busting, don't add ?v=123 by hand”
immutable- คำใน
Cache-Controlที่บอกว่า "ไฟล์ชื่อนี้ไม่มีวันเปลี่ยนเนื้อ ไม่ต้องถามซ้ำ" · ใช้ได้ปลอดภัยก็ต่อเมื่อชื่อมี digest · ถาม senior ได้ว่า “ไฟล์ที่ไม่มี digest ห้ามตั้ง immutable ใช่ไหม” - key-based cache expiration
- ชื่อทางการของประโยค "ให้ key เปลี่ยน แทนการลบของเก่า" · เจอเต็ม ๆ ใน Day 3 ครึ่งหลัง · ถาม senior ได้ว่า “ตรงนี้ใช้ key-based expiration ได้ไหม แทนการสั่งลบ cache”
Propshaft — พนักงานที่ติดเลขรุ่นแล้ววางบนชั้น
คนที่คำนวณลายนิ้วมือ · เสิร์ฟไฟล์ที่ /assets/… · และบอก stylesheet_link_tag ว่าตอนนี้ไฟล์ชื่ออะไร คือ gem ชื่อ Propshaft ·
เป็นค่าเริ่มต้นของ Rails 8 และทำแค่นั้นจริง ๆ — ไม่มัดรวมไฟล์ ไม่แปลงภาษา ไม่บีบไฟล์
รุ่นก่อนหน้าคือ Sprockets ที่ทำทุกอย่างเอง (สไลด์หน้า 26):
| Sprockets (ของเดิม) | Propshaft (Rails 8 ค่าเริ่มต้น) | |
|---|---|---|
| ทำอะไร | fingerprint + มัดรวม + บีบ + แปลงภาษา (เช่น Sass → CSS · Sass คือภาษาเขียน CSS แบบย่อที่ต้องแปลงก่อนใช้) | fingerprint + เสิร์ฟไฟล์ เท่านั้น |
| ภาษาของตัวเอง | มี เช่นบรรทัด //= require ในหัวไฟล์ = "เอาไฟล์นี้มารวมด้วย" | ไม่มี · CSS/JS ธรรมดา |
| ข้อเสีย | ทำเยอะ จึงช้าและหาบั๊กยาก | ต้องการมัดรวมหรือแปลงภาษา ต้องใช้เครื่องมืออื่นช่วย |
เหตุผลเบื้องหลัง (สไลด์): Sprockets เกิดตอนที่เบราว์เซอร์ทำอะไรเองไม่ค่อยได้ เครื่องมือจึงต้องทำแทนหมด · ทุกวันนี้เบราว์เซอร์ทำเองได้เกือบหมดแล้ว (หัวข้อสามปัญหา) เครื่องมือที่ทำเยอะเลยกลายเป็นภาระแทน
เทียบกับ Next.js (บท 70): งานมัดรวม · บีบ · แปลง TypeScript ทั้งหมดซ่อนอยู่ใน npm run build ที่เครื่องมือของ Next.js ทำให้ ·
Rails 8 แยกงานออกเป็นชิ้นเล็ก ๆ แล้วให้ Propshaft ถือแค่ชิ้นที่เล็กที่สุด
ในเครื่องเรา vs ตอนขึ้นเครื่องจริง
ในเครื่องเรา (development) Propshaft คำนวณสด ๆ ทุกครั้งที่มีคนขอ — ที่เห็นไปเมื่อกี้ ·
แต่บนเครื่องจริง (production) ไม่มีใครอยากให้คำนวณซ้ำทุกคำขอ · จึงมีคำสั่งทำทีเดียวตอน deploy:
bin/rails assets:precompile = "ทำไฟล์ asset ทุกไฟล์ให้มีชื่อพร้อม digest แล้ววางไว้ใน public/assets/"
ลองรันตามในเครื่องได้ · แต่รันแล้วต้องเก็บกวาดตามกล่อง ⚠️ ข้างล่างทันที ไม่งั้นแก้ CSS ต่อจากนี้จะไม่เห็นผล
$ bin/rails assets:precompile
≈ tailwindcss v4.3.3
/*! 🌼 daisyUI 5.7.50 */
Done in 81ms
Warning: You are precompiling assets in development. Rails will not serve any changed assets until you delete public/assets/.manifest.json
- บรรทัดก่อน
Warning— tailwindcss-rails แอบสั่ง build Tailwind ให้ก่อน เพื่อให้ได้tailwind.cssฉบับล่าสุด (เวลา ms ของคุณจะต่าง) Warning: …— "คุณกำลัง precompile ในเครื่อง development · Rails จะไม่เสิร์ฟไฟล์ที่แก้ใหม่จนกว่าคุณจะลบpublic/assets/.manifest.json" · จำไว้ เดี๋ยวเจอกับดัก
$ ls public/assets
action_cable-5212cfee.js
…
application-8b441ae0.css
application-bfcdf840.js
controllers
…
tailwind-798e8e3d.css
trix-4bf79781.js
trix-65afdb1d.css
turbo-736fb387.js
turbo.min-5eb167c2.js.map
turbo.min-9fd88cd5.js
ได้ไฟล์ของเราพร้อมชื่อชุดเดียวกับที่เห็นในหน้าเว็บ · มีไฟล์ของ gem อื่นติดมาด้วย (trix action_cable ฯลฯ) เพราะ Propshaft คัดลอกทุกไฟล์ในโฟลเดอร์ที่มันดูแล ไม่ได้ดูว่าใช้จริงไหม ·
และมีไฟล์ซ่อนหนึ่งไฟล์ (ชื่อขึ้นต้นด้วยจุด) คือ ตารางแมปชื่อ:
$ head -c 260 public/assets/.manifest.json
{"tailwind.css":{"digested_path":"tailwind-798e8e3d.css","integrity":null},"application.css":{"digested_path":"application-8b441ae0.css","integrity":null},"application.js":{"digested_path":"application-bfcdf840.js","integrity":null},"controllers/hello_controll
(head -c 260 = เอาแค่ 260 ตัวอักษรแรก ไฟล์จริงยาวกว่านี้ จึงขาดกลางคำ · "integrity":null ไม่ต้องสนใจ)
อ่านว่า "application.css → ตอนนี้ชื่อ application-8b441ae0.css" ·
บนเครื่องจริง stylesheet_link_tag เปิดตารางนี้ดูว่าจะเขียนชื่ออะไรลงใน HTML · และ config/environments/production.rb ของแอปนี้ตั้งหัวจดหมายของไฟล์ใน public/ ไว้ว่า:
config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }
1.year.to_i = จำนวนวินาทีในหนึ่งปี · คือ "เก็บสำเนาได้หนึ่งปี" แบบเดียวกับที่เห็นในเครื่องเรา
⚠️ กับดัก: precompile ในเครื่องแล้วลืมลบ — เราลองแล้ว: precompile → แก้ CSS → ปิดแล้วเปิด server ใหม่ ·
หน้าเว็บยังชี้ไฟล์ชื่อเดิม และสียังเป็นสีเก่า ทั้งที่ไฟล์ในโฟลเดอร์เปลี่ยนแล้ว — เพราะ Rails เห็นตาราง .manifest.json แล้วใช้ตารางนั้นแทนการคำนวณสด
ทางแก้คือ bin/rails assets:clobber (ลบ public/assets ทิ้งทั้งโฟลเดอร์) ·
แต่มันลบ app/assets/builds/tailwind.css ไปด้วย → หน้าเว็บจะไม่มีสไตล์ Tailwind เลย จนกว่าจะสั่ง bin/rails tailwindcss:build (หรือปิดแล้วเปิด bin/dev ใหม่):
$ bin/rails assets:clobber
$ ls public/assets app/assets/builds
ls: public/assets: No such file or directory
app/assets/builds:
tailwind
$ bin/rails tailwindcss:build
≈ tailwindcss v4.3.3
/*! 🌼 daisyUI 5.7.50 */
Done in 90ms
$ ls app/assets/builds
tailwind
tailwind.css
assets:clobber ทำงานเงียบ ๆ ไม่พิมพ์อะไรเลย · (tailwind ที่เหลืออยู่คือโฟลเดอร์ว่าง ไม่ต้องสนใจ) · public/assets ไม่ได้เก็บใน git อยู่แล้ว ลบได้ไม่ต้องกลัว
Propshaft คือพนักงานหน้าร้านที่รับเมนูเล่มที่พิมพ์เสร็จแล้ว ติดเลขรุ่นที่ปก วางบนชั้น แล้วจดในสมุดว่า "เมนูอาหาร → ตอนนี้คือเล่ม 8b441ae0" ·
เขาไม่แก้เนื้อในเมนู ไม่เย็บหลายเล่มรวมกัน — ใครอยากได้เมนูเล่มรวม ต้องให้โรงพิมพ์ข้างนอกทำมาก่อน ·
assets:precompile คือการติดเลขและวางบนชั้นให้ครบทุกเล่มก่อนร้านเปิด แทนที่จะทำตอนลูกค้าขอ
ที่ไม่เหมือนกัน: พนักงานคนจริงเห็นเมนูเล่มใหม่วางอยู่ก็คงเปลี่ยนสมุดเอง ·
Rails ไม่ทำแบบนั้น — ถ้ามีสมุด (.manifest.json) อยู่แล้ว มันเชื่อสมุดตอนเปิด server จนกว่าคุณจะทิ้งสมุดเอง (กับดักข้างบน)
- Propshaft · Sprockets
- gem จัดการ asset ของ Rails · Propshaft (Rails 8) ทำแค่ fingerprint + เสิร์ฟ · Sprockets (รุ่นเก่า) ทำทุกอย่าง · โปรเจกต์เก่ามักยังใช้ Sprockets · ถาม senior ได้ว่า “โปรเจกต์นี้ใช้ Propshaft หรือ Sprockets” — ดูได้จาก Gemfile
assets:precompile- คำสั่งเตรียม asset ทุกไฟล์ (ชื่อ + digest) ไว้ใน
public/assets/ตอน deploy · พิมพ์บอก AI ได้ว่า “CSS works locally but not in production — check that assets:precompile ran during deploy” - manifest
- ตารางแมป "ชื่อเดิม → ชื่อที่มี digest" (
public/assets/.manifest.json) · ถาม senior ได้ว่า “หลัง precompile ในเครื่องแล้วหน้าไม่อัปเดต ต้อง clobber ใช่ไหม” assets:clobber- ลบไฟล์ที่ precompile ไว้ทิ้งทั้งหมด (และ
tailwind.cssที่ build ไว้ด้วย) · ใช้ตอนเจอของเก่าค้าง · พิมพ์บอก AI ได้ว่า “I ran assets:clobber, now rebuild Tailwind before starting the server”
importmap — JavaScript โดยไม่มี node_modules
ตอนทำ Next.js (บท 70) สิ่งแรกที่ทำคือ npm install แล้วได้โฟลเดอร์ node_modules ก้อนโต ·
ก่อนขึ้นเครื่องจริงต้อง npm run build ให้เครื่องมือมัด JavaScript ทั้งหมดเป็นก้อน · ในโปรเจกต์ Rails ของเรา:
$ ls node_modules package.json
ls: node_modules: No such file or directory
ls: package.json: No such file or directory
ไม่มีทั้งคู่ · แต่ไฟล์ 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"
import "…" แบบไม่มี from = "โหลดไฟล์นั้นมารันเลย" ไม่ได้หยิบตัวแปรอะไรออกมาใช้ ·
"@hotwired/turbo-rails" ไม่ใช่ที่อยู่ไฟล์ — มันเป็นแค่ชื่อเล่น · เบราว์เซอร์เปล่า ๆ ไม่รู้ว่าชื่อนี้คือไฟล์ไหน ·
ใน Next.js เครื่องมือ build ไปหาใน node_modules ให้ · ใน Rails 8 ใช้วิธีที่เบราว์เซอร์เข้าใจเอง ชื่อ import map =
"สมุดรายชื่อที่บอกเบราว์เซอร์ว่า ชื่อเล่นนี้ → ไฟล์ที่อยู่ตรงไหน"
สมุดนี้เราเขียนที่ config/importmap.rb (ไฟล์จริงของแอป):
# Pin npm packages by running ./bin/importmap
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "application"— จดชื่อapplication→ ไฟล์application.js(ไม่มีto:= ชื่อไฟล์เหมือนชื่อเล่น)pin "@hotwired/turbo-rails", to: "turbo.min.js"— ชื่อเล่น@hotwired/turbo-rails→ ไฟล์turbo.min.js(.min= ฉบับที่บีบช่องว่างออกแล้ว)- สองบรรทัด
@hotwired/stimulus…— แบบเดียวกัน แต่เป็นของ Stimulus (ตัวช่วยเขียน JS เล็ก ๆ ผูกกับ HTML — บท 134) pin_all_from "app/javascript/controllers", under: "controllers"— จดทุกไฟล์ในโฟลเดอร์นั้นทีเดียว โดยขึ้นต้นชื่อเล่นด้วยcontrollers/(ใช้ในบท 134)
ดูสมุดฉบับที่ Rails แปลงแล้ว (มีชื่อพร้อม digest) ด้วยคำสั่งอ่านอย่างเดียว:
$ bin/importmap json
{
"imports": {
"application": "/assets/application-bfcdf840.js",
"@hotwired/turbo-rails": "/assets/turbo.min-9fd88cd5.js",
"@hotwired/stimulus": "/assets/stimulus.min-4b1e420e.js",
"@hotwired/stimulus-loading": "/assets/stimulus-loading-1fc53fe7.js",
"controllers/application": "/assets/controllers/application-3affb389.js",
"controllers/hello_controller": "/assets/controllers/hello_controller-708796bd.js",
"controllers": "/assets/controllers/index-ee64e1f1.js"
}
}
นี่คือก้อนเดียวกับที่อยู่ใน <script type="importmap"> ของหน้าเว็บ (หัวข้อแรก) · ไฟล์ JS ก็ได้ fingerprint เหมือน CSS เพราะ Propshaft คนเดิมเป็นคนเสิร์ฟ ·
ทีนี้ไล่ตามลำดับที่เบราว์เซอร์ทำ:
แล้ว turbo.min.js อยู่ไหน ในเมื่อโปรเจกต์ไม่มี node_modules? ถาม Propshaft ว่ามันหาเจอที่ไหน:
$ bin/rails runner 'puts Rails.application.assets.load_path.find("turbo.min.js").path'
…/gems/turbo-rails-2.0.23/app/assets/javascripts/turbo.min.js
อยู่ใน gem turbo-rails ที่ลงมากับ Gemfile — library JS ที่ Rails ใช้ประจำมากับ gem เลย ไม่ต้องผ่าน npm ·
(bin/rails runner '…' = รันโค้ด Ruby หนึ่งบรรทัดในแอปโดยไม่ต้องเปิด console ·
load_path = รายชื่อโฟลเดอร์ที่ Propshaft ไล่หาไฟล์ ซึ่งรวมโฟลเดอร์ใน gem ด้วย · ส่วนหน้า path ของคุณจะต่างตามเครื่อง)
อยากเพิ่ม library ใหม่ทำยังไง
สไลด์หน้า 27 ยกตัวอย่าง bin/importmap pin sortablejs (library ลากวางรายการ) ·
คำสั่งนี้ไปถามบริการ CDN ชื่อ jspm.io แล้วดาวน์โหลดไฟล์ JS มาเก็บใน vendor/javascript/ (โฟลเดอร์เก็บโค้ดของคนอื่นที่เอามาใช้) พร้อมเพิ่มบรรทัด pin ลงใน config/importmap.rb ·
บทนี้ไม่ได้รันคำสั่งนี้ (ไม่ได้ใช้ library นั้น และไม่อยากโหลดของจากเน็ตเข้าโปรเจกต์โดยไม่จำเป็น) — ถ้าวันหนึ่งต้องใช้ ให้เปิดดูไฟล์ที่ได้มาก่อน commit
| ได้อะไร | เสียอะไร |
|---|---|
ไม่มี node_modules ไม่มี package.json ไม่มีขั้น build ของ JS | ไม่มี tree-shaking (การตัดส่วนของ library ที่เราไม่ได้ใช้ทิ้ง) — ได้ library มาทั้งก้อน |
| แก้ไฟล์ JS แล้ว refresh เห็นเลย | เขียน TypeScript / JSX ตรง ๆ ไม่ได้ (ต้องมีตัวแปลงเป็น JS ก่อน แบบที่ tsc ทำในบท 63) |
| ไม่ต้องกังวล library แปลก ๆ ที่ npm ลากเข้ามาเป็นร้อยตัว | library รุ่นเก่าที่ไม่ได้เขียนแบบ import/export ใช้ไม่ได้ |
| deploy เร็วขึ้น (สไลด์อ้าง) | ถ้า JS เยอะมาก ๆ จะมีไฟล์เล็ก ๆ เต็มไปหมด |
Next.js เหมือนร้านที่สั่งวัตถุดิบมากองเต็มโกดัง (node_modules) แล้วให้ครัวกลางปรุงรวมเป็นชุดสำเร็จก่อนเปิดร้าน (npm run build) ·
Rails 8 แค่แปะสมุดรายชื่อไว้หน้าร้าน: "ขอ turbo → ไปหยิบที่ชั้น A เล่ม 9fd88cd5" · ลูกค้า (เบราว์เซอร์) เปิดสมุดแล้วเดินไปหยิบเอง
ที่ไม่เหมือนกัน: สมุดรายชื่อไม่ได้ทำให้ของเบาลง — ถ้าชั้นนั้นมีหนังสือหนาพันหน้า ลูกค้าก็ต้องแบกไปทั้งเล่ม แม้จะอ่านแค่หน้าเดียว (ไม่มี tree-shaking) · และสมุดใช้ได้เฉพาะของที่ "หยิบได้เลย" ถ้าต้องปรุงก่อน (TypeScript · JSX) ต้องมีครัวแยกมาช่วย
- import map ·
pin - ตารางที่บอกเบราว์เซอร์ว่าชื่อใน
import "…"คือไฟล์ไหน ·pin= เพิ่มหนึ่งบรรทัดในตารางนั้น · พิมพ์บอก AI ได้ว่า “this app uses importmap-rails, not npm — pin the library instead of npm install” (ไม่บอก AI มักสั่งnpm installมา) - bundler · bundling
- เครื่องมือที่มัด JS หลายไฟล์ (และ library) รวมเป็นก้อนเดียว เช่น esbuild · Vite · ที่ Next.js ใช้ตอน build · ถาม senior ได้ว่า “แอปนี้ JS เยอะพอจะต้องมี bundler หรือยัง”
- tree-shaking
- การที่ bundler ตัดส่วนของ library ที่โค้ดเราไม่ได้เรียกทิ้ง ให้ไฟล์เล็กลง · importmap ไม่มีสิ่งนี้ · ถาม senior ได้ว่า “library ตัวนี้ใหญ่แค่ไหน ถ้าไม่มี tree-shaking คุ้มไหม”
- ES module
- มาตรฐานการเขียน
import/exportที่เบราว์เซอร์เข้าใจเอง ·<script type="module">คือการบอกว่าไฟล์นี้เขียนแบบนั้น · ถาม senior ได้ว่า “library นี้มีเวอร์ชัน ES module ไหม ใช้กับ importmap ได้หรือเปล่า”
ทางเลือกทั้งหมด — เลือกจากสองคำถาม
สไลด์หน้า 28 รวมเครื่องมือที่ Rails มีให้เลือก · ไม่ต้องจำชื่อ gem และไม่ต้องรู้จักชื่อเครื่องมือในวงเล็บ — จำเกณฑ์เลือก:
| gem | ดูแล | ทำอะไร | เหมาะกับ |
|---|---|---|---|
| importmap-rails ✅ | JS | ไม่มีขั้น build · เบราว์เซอร์ import เอง | แอปที่ JS ไม่เยอะ · ค่าเริ่มต้น Rails 8 |
| jsbundling-rails | JS | เรียก bundler (esbuild · rollup · Bun · Webpack) มามัด JS ให้ | ต้องใช้ TypeScript หรือ library ที่ต้องมัดรวม |
| cssbundling-rails | CSS | เรียกเครื่องมือ CSS (Tailwind · Bootstrap · Sass) ผ่าน yarn จึงต้องมี Node | framework CSS ที่ต้อง build |
| tailwindcss-rails ✅ | CSS | Tailwind ฉบับไม่ต้องมี Node เลย | ใช้ Tailwind อย่างเดียว (บท 128) |
| propshaft ✅ | ทั้งคู่ | fingerprint + เสิร์ฟ ไม่มัดรวม | เป็นฐานของทุกตัวข้างบน |
| vite_rails | ทั้งคู่ | ยก Vite มาทั้งชุด (แก้โค้ดแล้วหน้าเปลี่ยนทันทีแบบ npm run dev) | ทีมที่คุ้น Vite และ JS เยอะมาก |
✅ = แอปของเราใช้ตัวนี้ (ดูได้จาก Gemfile) · ไม่มี Node ในโปรเจกต์เลยสักตัว · เวลาต้องเลือก ถามตัวเองสองข้อ:
- JS เยอะแค่ไหน? — แค่ปุ่มเปิด/ปิด ตัวนับอักษร (Stimulus · บท 134) → importmap พอ · ทั้งหน้าเป็นแอปแบบ React → ต้อง bundler
- ยอมมีขั้น build ไหม? — มี build = ได้ TypeScript · tree-shaking แต่ต้องดูแล Node และรอ build ทุกครั้ง
🎯 ภารกิจ: เปลี่ยนสีพื้นหลัง แล้วดูชื่อไฟล์เปลี่ยนตาม (~10 นาที)
- เปิด server ไว้ (
bin/dev) · อีกหน้าต่าง terminal รันcurl -s localhost:3000/posts | grep stylesheet· จดเลขท้ายapplication-…ไว้ - เพิ่มบรรทัด
body { background: #fef3c7; }ท้ายไฟล์app/assets/stylesheets/application.css(เว้นบรรทัดว่างหนึ่งบรรทัดก่อน แบบเดียวกับข้างบน) แล้วบันทึก - รันคำสั่ง
curlเดิมอีกรอบ · เลขเปลี่ยนไหม? เลขของtailwind-…เปลี่ยนไหม? · เปิดhttp://localhost:3000/postsในเบราว์เซอร์ดูสีด้วย - แก้เฉพาะรหัสสีในบรรทัดเดิมเป็น
#dcfce7แล้วcurlอีกรอบ git restore app/assets/stylesheets/application.cssแล้วcurlอีกรอบ · ทายก่อนรันว่าจะได้เลขอะไร
ควรเห็น: ข้อ 3 เลขของ application เปลี่ยน แต่ tailwind เลขเดิม · พื้นหลังเป็นสีเหลืองอ่อน · ข้อ 5 ได้เลขเดียวกับที่จดไว้ในข้อ 1
ถ้าเจออาการนี้ แปลว่า…
- แก้ไฟล์แล้วเลขไม่เปลี่ยนเลย → มี
public/assets/.manifest.jsonค้างจากการ precompile (กับดักในหัวข้อ Propshaft) · สั่งbin/rails assets:clobberแล้วbin/rails tailwindcss:buildแล้วเปิด server ใหม่ - หน้าเว็บโล่ง ไม่มีสไตล์เลย · บรรทัด
tailwind-…หายไปจากcurl→tailwind.cssถูกลบ (เช่นหลังassets:clobber) · สั่งbin/rails tailwindcss:build - เลข
tailwind-…ของคุณไม่ตรงกับในบท → ปกติ · Tailwind อ่านไฟล์ในโปรเจกต์เพื่อหาคลาส (บท 128) ไฟล์ต่างกันนิดเดียวเนื้อ CSS ก็ต่าง เลขก็ต่าง · เราเจอเองตอนมีไฟล์ทดลองค้างในโฟลเดอร์โปรเจกต์
ผลจริงตามลำดับ (ข้อ 1 · 3 · 4 · 5) · เลขของข้อ 3–4 จะตรงกับของเราถ้าเว้นบรรทัดแบบเดียวกัน ถ้าไม่ตรงไม่ผิด ขอแค่มันเปลี่ยน:
$ curl -s localhost:3000/posts | grep stylesheet
<link rel="stylesheet" href="/assets/application-8b441ae0.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/tailwind-798e8e3d.css" data-turbo-track="reload" />
$ curl -s localhost:3000/posts | grep stylesheet # #fef3c7
<link rel="stylesheet" href="/assets/application-6c48751d.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/tailwind-798e8e3d.css" data-turbo-track="reload" />
$ curl -s localhost:3000/posts | grep stylesheet # #dcfce7
<link rel="stylesheet" href="/assets/application-3cafeaa9.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/tailwind-798e8e3d.css" data-turbo-track="reload" />
$ curl -s localhost:3000/posts | grep stylesheet # หลัง git restore
<link rel="stylesheet" href="/assets/application-8b441ae0.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/tailwind-798e8e3d.css" data-turbo-track="reload" />
ทุกครั้งที่เนื้อ application.css เปลี่ยน ชื่อเปลี่ยนตาม · tailwind-798e8e3d นิ่งตลอดเพราะไม่ได้แตะ ·
และ git restore คืนเนื้อเดิม → ได้ 8b441ae0 กลับมา (ถ้า application.css ของคุณเหมือนในแอปกลางทุกตัวอักษร เลขของคุณจะตรงกับนี้ด้วย)
ก่อนไปบท 128: ถ้าลอง assets:precompile ตามหัวข้อ Propshaft ไว้ ให้เก็บกวาดให้ครบ:
$ git restore app/assets/stylesheets/application.css
$ bin/rails assets:clobber
$ bin/rails tailwindcss:build
git status ควรไม่เหลืออะไรที่แก้ · โค้ดกลับมาเป็นสถานะเดียวกับตอนจบบท 126
แบบฝึกหัด
สไลด์หน้า 33 ถามว่า ถ้าเราไม่ทำ fingerprinting เลย แต่ตั้งหัวจดหมายบอกเบราว์เซอร์ว่า "ห้ามเก็บสำเนาไฟล์ CSS" แทน จะเกิดอะไรขึ้น?
ตัวเลือกที่ 2 ถูก — ห้ามเก็บสำเนา = ไม่มีของเก่าค้าง แต่ก็เสียข้อดีของ cache ไปทั้งหมด · ตาราง Chrome ในบทนี้ หน้าที่สองโหลด CSS 0 byte เพราะมีสำเนา · ถ้าห้ามเก็บ ทุกหน้าต้องโหลดใหม่ (34 KB ของ Tailwind ทุกหน้า) server ก็เหนื่อยขึ้น · fingerprinting ได้ทั้งสองอย่าง: เก็บได้นานหนึ่งปี และ ได้ของใหม่ทันทีที่แก้
ตัวเลือกที่ 1 ผิดเพราะสั่งห้ามเก็บแล้ว จะไม่มีสำเนาเก่าให้จำ · ตัวเลือกที่ 3 ผิดเพราะการต่อเลขคือ fingerprinting ที่โจทย์บอกว่าไม่ทำ · ตัวเลือกที่ 4 ผิดเพราะเราวัดแล้วว่าเบราว์เซอร์เก็บสำเนาจริง (0 byte ในหน้าที่สอง)
จุดที่ต้องจำ: ปิด cache = ได้ของใหม่แต่ช้า · cache ยาวโดยไม่มี digest = เร็วแต่ได้ของเก่า · fingerprinting = เร็วและใหม่
ข้อไหนบอกหน้าที่ของ Propshaft ได้ตรงที่สุด?
ตัวเลือกที่ 3 ถูก — Propshaft ทำแค่ fingerprint + เสิร์ฟ (+ ทำตาราง manifest ให้ stylesheet_link_tag รู้ชื่อ) ไม่ทำอย่างอื่น
ตัวเลือกที่ 1 คืองานของ tailwindcss-rails (บท 128) · ตัวเลือกที่ 2 คืองานของ importmap · ตัวเลือกที่ 4 คืองานของ bundler (jsbundling-rails · Vite) หรือ Sprockets สมัยก่อน — Propshaft ตั้งใจไม่ทำ
จุดที่ต้องจำ: Propshaft = เลขรุ่น + เสิร์ฟ · Tailwind = สร้าง CSS · importmap = สมุดชื่อเล่นของ JS
เพื่อนลอง bin/rails assets:precompile ในเครื่องเมื่อวาน · วันนี้แก้ application.css แล้วเปิด server ใหม่ · สีไม่เปลี่ยน และชื่อไฟล์ใน HTML ก็ยังเป็นชื่อเดิม · เกิดจากอะไร?
ตัวเลือกที่ 1 ถูก — คำเตือนตอน precompile บอกไว้ตรง ๆ ว่า "Rails will not serve any changed assets until you delete public/assets/.manifest.json" · เราลองแล้วได้อาการนี้เป๊ะ · แก้ด้วย assets:clobber แล้วอย่าลืม tailwindcss:build
ตัวเลือกที่ 2 ผิดเพราะชื่อใน HTMLยังเป็นชื่อเดิม — ปัญหาอยู่ฝั่ง server ไม่ใช่เบราว์เซอร์ (ถ้าชื่อเปลี่ยน เบราว์เซอร์โหลดใหม่เองอยู่แล้ว) · ตัวเลือกที่ 3 ผิดเพราะ importmap ดูแลแค่ JS ไม่เกี่ยวกับ CSS · ตัวเลือกที่ 4 ผิดเพราะ digest มาจากเนื้อไฟล์ล้วน ๆ (แก้แล้วแก้กลับได้เลขเดิม)
จุดที่ต้องจำ: ชื่อใน HTML ไม่เปลี่ยน = ดูฝั่ง server ก่อน · precompile ในเครื่องแล้วต้อง clobber
โปรเจกต์ไม่มี node_modules แต่ app/javascript/application.js เขียน import "@hotwired/turbo-rails" แล้วทำงานได้ · เพราะอะไร?
ตัวเลือกที่ 4 ถูก — <script type="importmap"> แมปชื่อเล่นไปที่ไฟล์ · ไฟล์นั้นจริง ๆ อยู่ใน gem turbo-rails และ Propshaft เป็นคนเสิร์ฟ (พร้อม digest)
ตัวเลือกที่ 1 คือสิ่งที่ bundler ทำ — importmap ไม่มัดรวม (ไฟล์ส่งแยกกัน เห็นได้จาก modulepreload 7 บรรทัด) ·
ตัวเลือกที่ 2 ผิดเพราะไฟล์มาจาก server ของเราเอง ที่ /assets/ · npm ไม่เกี่ยวเลย ·
ตัวเลือกที่ 3 ผิดเพราะ JS รันในเบราว์เซอร์ Ruby ไม่ได้แตะบรรทัดนั้น
จุดที่ต้องจำ: importmap = สมุดชื่อเล่น → ไฟล์ · เบราว์เซอร์อ่านสมุดแล้วโหลดเอง ไม่มีขั้น build
ติดตรงไหน ลองถามผู้สอนหรือ AI ด้วยประโยคในกล่องศัพท์ได้เลย · หรือแปะผล curl -sI ของไฟล์ที่สงสัยแล้วถามว่า "ทำไมไฟล์นี้ cache แบบนี้"
ถ้าเปิดสไลด์ดูประกอบ
| หน้า | สไลด์เขียน | ของจริง (Rails 8.1.4 · propshaft 1.3.2) | ต้องสนใจไหม |
|---|---|---|---|
| 26 | เรียกใน view ด้วย stylesheet_link_tag "application" | layout จริงใช้ stylesheet_link_tag :app = ได้ทั้ง application และ tailwind | สนใจ — อย่าแก้ตามสไลด์ ไม่งั้น Tailwind หายทั้งเว็บ |
| 26 | ไฟล์วางที่ app/assets/stylesheets/ · app/assets/images/ | มี app/assets/builds/ (Tailwind สร้าง ห้ามแก้มือ) และ app/javascript/ ด้วย | นิดหน่อย — แก้ CSS ของเราที่ stylesheets/ เท่านั้น |
| 25 | ชื่อตัวอย่าง app-9a8b7c.css | ชื่อจริงเต็ม + 8 ตัว เช่น application-8b441ae0.css | ไม่ต้อง — หลักการเดียวกัน |
| 27 | ตัวอย่าง importmap มี turbo-c3d4.js สองบรรทัด | จริงมี 7 ชื่อ · turbo คือ turbo.min-9fd88cd5.js · มี modulepreload ตามมาอีก 7 บรรทัด | ไม่ต้อง |
| 27 | bin/importmap pin sortablejs | ดาวน์โหลดไฟล์จาก jspm.io มาไว้ vendor/javascript/ · บทนี้ไม่ได้รัน | สนใจตอนจะเพิ่ม library — ตรวจไฟล์ที่ได้ก่อน commit |
อ้างอิง
- Rails Guides — Asset Pipeline § Fingerprinting: Versioning with Digest-Based URLs —
อ่านถึงหัวข้อ Manifest Files แล้วหยุด · ยืนยันเรื่องชื่อ + digest และตาราง
.manifest.jsonที่เราเปิดดู - Rails Guides — Asset Pipeline § Sprockets vs. Propshaft — อ่านแค่ Load Order กับ Precompilation · ที่มาของตาราง Sprockets/Propshaft ในบทนี้
- Rails Guides — Working with JavaScript in Rails § Import Maps — อ่านหัวข้อ Import Maps และ Choosing Between Import Maps and a JavaScript Bundler · เกณฑ์เลือกเดียวกับสองคำถามท้ายบท
- github.com/rails/propshaft —
อ่าน README ส่วนแรกกับหัวข้อ Usage · บอกตรง ๆ ว่า Propshaft ไม่ทำอะไรและทำแค่สี่อย่าง (รวม digest และ manifest) · Usage ยืนยันว่า precompile คัดลอกทุกไฟล์ใน load path ไป
public/assets(ที่มาของ trix ที่เราไม่ได้ใช้) - github.com/rails/importmap-rails —
อ่าน README ส่วน Usage กับ Using npm packages via JavaScript CDNs · อธิบายว่า
bin/importmap pinโหลดไฟล์มาไว้vendor/javascript - MDN — HTTP caching § Cache Busting — อ่านหัวข้อ Cache Busting และย้อนไปอ่าน What's lost by no-store · เรื่องเดียวกับ fingerprinting เล่าจากฝั่งเบราว์เซอร์ และตรงกับ quiz ข้อ 1
- MDN — <script type="importmap"> — ดูตัวอย่างแรกของหน้าพอ · ยืนยันว่า import map เป็นของมาตรฐานเบราว์เซอร์ ไม่ใช่ของ Rails