Hotwire โดยไม่ต้องใช้ Action Cable: ส่ง Turbo Streams ใน Rails ผ่าน Hibira
· Hibira
ทำไมเราสร้าง hibira-rails: ใช้ broadcasts_to และ turbo_stream_from เหมือนเดิม ส่วนการเชื่อมต่อ WebSocket ให้ Hibira ดูแล เมธอดเดียวที่เปลี่ยน สตรีมที่ลงลายเซ็นยังปลอดภัยอย่างไร ข้อความที่ใหญ่เกินจะเป็นอย่างไร และการลองทำห้านาที

สิ่งที่มีเสน่ห์ที่สุดของการทำอัปเดตแบบเรียลไทม์ใน Rails คือโค้ดบรรทัดเดียวนี้
class Message < ApplicationRecord
broadcasts_to :room
endใส่ <%= turbo_stream_from @room %> ไว้ในหน้าอีกบรรทัด ทุกครั้งที่มีคนโพสต์ข้อความ เบราว์เซอร์ทุกตัวที่เปิดห้องแชตนั้นอยู่จะมีบรรทัดใหม่เพิ่มขึ้นพร้อมกัน โดยไม่ต้องเขียน JavaScript เลยสักบรรทัด นี่คือจุดที่ Hotwire ทำให้คนหลงรัก Rails
ปัญหาจะโผล่มาตอนขึ้น production เพราะเบื้องหลังบรรทัดนั้นคือ Action Cable การเชื่อมต่อ WebSocket อยู่ใน Puma ของคุณเอง การกระจายข้อความข้ามเครื่องต้องมี Redis หรือ Solid Cable แพลตฟอร์มโฮสติ้งจำกัดจำนวนการเชื่อมต่อ และเมื่อผู้ใช้มากขึ้น คุณก็ต้องแยก cable server ออกไปขยายต่างหาก
บทความนี้เล่าเรื่องที่เราสร้าง hibira-rails ว่าทำไมเราอยากทำ มันเปลี่ยนแค่จุดเดียวตรงไหน และวิธีเชื่อมแอป Rails ที่มีอยู่แล้วภายในห้านาที
เบื้องหลังบรรทัดเดียวนั้น คือเซิร์ฟเวอร์ทั้งชุด
Action Cable ไม่ได้ใช้ยาก ที่ยากคือการดูแลมัน ผู้ใช้แต่ละคนที่เปิดหน้าไว้จะครองการเชื่อมต่อระยะยาวหนึ่งเส้นบนเซิร์ฟเวอร์แอปของคุณ ปกติแทบไม่รู้สึก แต่ตอนทราฟฟิกพุ่ง ตอนรีสตาร์ตเพื่อ deploy หรือตอนเปลี่ยนเครื่อง การเชื่อมต่อเหล่านี้จะหลุดพร้อมกันและเชื่อมกลับพร้อมกัน ขณะที่โปรเซส Rails ยังต้องรับคำขอปกติไปด้วย
เราเริ่มทำ Hibira เพื่อให้ทีมไม่ต้องดูแลชั้นนี้เอง Hibira เป็นบริการพุชเรียลไทม์แบบโฮสต์ที่รองรับโปรโตคอล Pusher Channels ไลบรารีสำเร็จรูปอย่าง pusher-js จึงเชื่อมต่อได้ทันที แต่นักพัฒนา Rails ไม่ได้ใช้ pusher-js พวกเขาใช้ broadcasts_to กับ turbo_stream_from ถ้าการเปลี่ยนบริการต้องเขียน model และ view ใหม่ทั้งหมด นั่นไม่ใช่ "การเชื่อมต่อ" แต่เป็น "การเขียนใหม่"
เราจึงตั้งเป้าหมายไว้ข้อเดียว: ไม่แก้ model ไม่แก้ view ติดตั้งเสร็จแล้ว บรรทัดเดิมนั้นวิ่งผ่าน Hibira ทันที
เปลี่ยนแค่จุดเดียว
ถ้าอ่านซอร์สโค้ดของ turbo-rails จะพบว่า ไม่ว่าคุณจะใช้ broadcasts_to, broadcast_append_to, broadcasts_refreshes หรือ broadcast_replace_later_to ที่รันใน background job สุดท้ายทั้งหมดจะไปจบที่เมธอดเดียวกันคือ Turbo::StreamsChannel.broadcast_stream_to ซึ่งส่ง HTML ของ Turbo Stream ที่เรนเดอร์แล้วให้ Action Cable
นี่เป็นจุดแทรกที่สะอาดมาก hibira-rails เขียนทับแค่เมธอดนี้ ให้ส่ง HTML ชุดเดิมไปยังช่องบน Hibira แทน ฝั่งหน้าเว็บก็เช่นกัน เดิม turbo_stream_from สร้าง <turbo-cable-stream-source> ตอนนี้จะสร้าง <hibira-turbo-stream-source> แทน แล้ว JavaScript ชิ้นเล็กๆ จะสมัครรับช่องและส่งสิ่งที่ได้รับให้ Turbo ตัว Turbo เองไม่รู้เลยว่าข้อความมาจากไหน
ก่อน
หลัง
จากนี้โปรเซส Rails ทำแค่สิ่งที่ถนัด คือรับคำขอ เรนเดอร์ HTML และเผยแพร่ครั้งเดียว ส่วนการเชื่อมต่อ WebSocket การเชื่อมต่อใหม่ และการกระจายข้อความ เป็นหน้าที่ของ Hibira
กฎความปลอดภัยเหมือนเดิม: ไม่มีลายเซ็น ก็สมัครรับไม่ได้
สิ่งที่น่ากลัวที่สุดเวลาเปลี่ยนชั้นการรับส่งข้อมูล คือการเผลอเปลี่ยนสิทธิ์ไปด้วย กฎของ Action Cable เรียบง่าย turbo_stream_from ใส่ชื่อสตรีมที่ลงลายเซ็นไว้ในหน้า และเซิร์ฟเวอร์รับเฉพาะการสมัครรับที่ลายเซ็นถูกต้อง เฉพาะคนที่เห็นหน้านั้นได้เท่านั้นที่จะได้ลายเซ็น
เราคงกฎนี้ไว้ทุกประการ Turbo stream แต่ละตัวจับคู่กับช่องส่วนตัวหนึ่งช่องบน Hibira โดยชื่อช่องสร้างจากค่าแฮชของชื่อสตรีม เมื่อเบราว์เซอร์จะสมัครรับ ต้องนำลายเซ็นจากหน้าเว็บกลับไปแลกการอนุญาตกับแอป Rails ของคุณ ถ้าลายเซ็นไม่ถูก หรือช่องไม่ตรงกับสตรีม ก็จะถูกปฏิเสธ
app secret อยู่บนเซิร์ฟเวอร์ของคุณเท่านั้น เบราว์เซอร์ได้แค่ app key สาธารณะ
ลองทำจริง: เชื่อมแอปที่มีอยู่ภายในห้านาที
เราสาธิตด้วยโปรเจกต์ Rails 8.1 ใหม่เอี่ยม และตั้งใจสร้างด้วย --skip-action-cable เพื่อแสดงว่าทั้งหมดนี้ไม่ต้องมีเซิร์ฟเวอร์ Action Cable
ติดตั้ง gem แล้วรันตัวติดตั้ง:
bundle add hibira-rails
bin/rails generate hibira:installตัวติดตั้งจะสร้าง config/initializers/hibira.rb ปักหมุด pusher-js ด้วย importmap (โปรเจกต์ jsbundling จะติดตั้งด้วยตัวจัดการแพ็กเกจของคุณ) และเพิ่ม import "hibira/turbo" หนึ่งบรรทัดใน app/javascript/application.js
จากนั้นนำค่าสามค่าจากหน้าแอปในแดชบอร์ด Hibira ใส่ในตัวแปรสภาพแวดล้อม หรือใต้ hibira: ใน bin/rails credentials:edit:
HIBIRA_APP_ID=your-app-id
HIBIRA_APP_KEY=your-app-key
HIBIRA_APP_SECRET=your-app-secretตรงนี้มีสองเรื่องที่มักทำให้ติด อย่างแรก secret จะแสดงเพียงครั้งเดียวหลังสร้างแอป จึงต้องบันทึกไว้ทันที ส่วน app ID และ key ดูได้เสมอที่ ภาพรวม → ข้อมูลรับรอง ในหน้าแอป อย่างที่สอง แพ็กเกจ Free ใช้ * เป็น origin ที่อนุญาตไม่ได้ ต้องเพิ่มทุก origin ที่หน้าเว็บเปิดอยู่ เช่น http://localhost:3000 ระหว่างพัฒนาและโดเมนจริง ไม่อย่างนั้นเบราว์เซอร์จะถูกปฏิเสธตอนเชื่อมต่อ
แล้วตรวจสอบ:
$ bin/rails hibira:check
ok App 1 on ws.hibira.io:443
ok Credentials accepted by the REST API
ok Turbo Streams go through Hibira
info Push notifications off (set HIBIRA_PUSH_INSTANCE_ID and HIBIRA_PUSH_SECRET_KEY)model และ view เป็นแบบเดียวกับในคู่มือ Turbo ไม่ได้แก้แม้แต่ตัวอักษรเดียว เปิดสองหน้าต่าง ส่งข้อความจากหน้าต่างหนึ่ง อีกหน้าต่างจะแสดงขึ้นมาโดยไม่ต้องรีเฟรช:

เปลี่ยนหน้าก็เหมือนกัน เมื่อ Turbo สลับหน้า การสมัครรับของหน้าเก่าจะถูกเก็บไว้ครู่หนึ่ง ถ้าหน้าใหม่สมัครรับสตรีมเดียวกันก็ใช้ต่อได้เลย ไม่ต้องตัดแล้วเชื่อมต่อใหม่
เทสต์ไม่พัง
เมื่อการกระจายข้อความถูกส่งไปยังบริการภายนอก สิ่งที่พังง่ายที่สุดคือเทสต์ รันเทสต์ทีไรก็เรียกเครือข่ายทุกที หรือ assert_turbo_stream_broadcasts ที่มากับ Turbo ใช้ไม่ได้ขึ้นมาเฉยๆ
ค่าเริ่มต้นจึงเป็น turbo = :auto คือวิ่งผ่าน Hibira เมื่อตั้งคีย์ไว้ แต่ในสภาพแวดล้อม test จะยังทำงานแบบ Action Cable การตรวจสอบการกระจายข้อความที่มีอยู่ยังผ่านตามเดิม และ CI ก็ไม่ออกไปเรียกภายนอก ถ้าต้องการบังคับเปิดหรือปิด ให้ตั้งเป็น true หรือ false
เมื่อข้อความใหญ่เกินไป
นี่คือส่วนที่เราต้องคิดให้รอบคอบที่สุด Turbo Stream ส่ง HTML ที่เรนเดอร์แล้ว การ์ดสินค้าหนึ่งใบอาจแค่ไม่กี่ร้อยไบต์ แต่ตารางทั้งตารางอาจหลาย KB และทุกแพ็กเกจมีขนาดสูงสุดต่ออีเวนต์ แพ็กเกจ Free อยู่ที่ 4 KB
เมื่อเกินขนาด Hibira จะปฏิเสธอีเวนต์นั้น ถ้าไม่จัดการอะไรเลย หน้าจอของผู้ใช้คนอื่นจะหยุดอัปเดตไปเงียบๆ ซึ่งเป็นผลลัพธ์ที่แย่ที่สุด
วิธีของเราคือ เมื่อเจอการกระจายข้อความที่ใหญ่เกิน จะส่ง page refresh ของ Turbo 8 แทนโดยอัตโนมัติ ซึ่งมีขนาดแค่ไม่กี่สิบไบต์ และหน้าที่ได้รับจะดึงเนื้อหาล่าสุดเอง หน้าจอจะไม่ได้แค่เพิ่มบรรทัดเดียวแบบ append แต่จะถูกต้องเสมอ
ถ้าหน้าของคุณเป็นรายการยาวอยู่แล้ว เราแนะนำให้ใช้ broadcasts_refreshes ของ Turbo 8 คู่กับ turbo_refreshes_with method: :morph ไปเลย การกระจายข้อความทุกครั้งจะเล็ก หน้าอัปเดตด้วย morph และตำแหน่งการเลื่อนไม่กระโดด ถ้าไม่อยากได้ refresh อัตโนมัติ ก็ตั้งให้บันทึก log แล้วทิ้ง หรือให้โยน exception ได้
ไม่ใช่แค่ Turbo
ในเมื่อเชื่อม Rails กับ Hibira แล้ว เราจึงทำงานที่พบบ่อยอีกสองสามอย่างไว้ด้วย:
- ช่องส่วนตัวและช่อง presence ของคุณเอง: ตั้งค่า
authorize_channelแล้วใช้current_userข้างในเพื่อกำหนดว่าใครสมัครรับช่องไหนได้ ถ้าเป็นช่อง presence ก็คืนข้อมูลผู้ใช้ มันสืบทอดจากApplicationControllerของคุณ สถานะการล็อกอินและการป้องกัน CSRF จึงทำงานตามปกติ - Webhook: รับ webhook ของ Hibira ที่
/hibira/webhooksพร้อมตรวจลายเซ็นให้ ในช่วงผ่อนผันระหว่างหมุนเวียน secret ก็ยังรับ webhook ที่เซ็นด้วย secret เก่า จึงไม่พลาดอีเวนต์ก่อน deploy ใหม่ - การแจ้งเตือนแบบพุช:
Hibira.pushส่งพุชไปยัง iOS, Android และเบราว์เซอร์ และ/hibira/push/tokenออกโทเค็นผู้ใช้ให้ SDK
สิ่งที่รองรับ
- Rails 7.0 ถึง 8.1, Ruby 3.1 ขึ้นไป, turbo-rails 1.5 ถึง 2 ทุกชุดผสมรันใน CI
- ใช้ได้ทั้ง importmap และ jsbundling (ตรวจสอบ assets:precompile กับ Propshaft แล้ว)
- ใช้ได้กับโปรเจกต์ที่สร้างด้วย
--skip-action-cable
hibira-rails เผยแพร่บน RubyGems แล้ว และซอร์สโค้ดอยู่บน GitHub ภายใต้สัญญาอนุญาต MIT ถ้าคุณมีแอป Hotwire ที่พึ่ง Action Cable อยู่ ลองใช้ดู แพ็กเกจฟรีของ Hibira ไม่ต้องใช้บัตรเครดิต และเมื่อติดตั้งแล้ว broadcasts_to บรรทัดนั้นก็ไม่ต้องให้เซิร์ฟเวอร์ของคุณแบกอีกต่อไป
เรากำลังทำการเชื่อมต่อแบบเดียวกันให้เฟรมเวิร์กและเอนจินอื่นๆ ตัวต่อไปคืออะไร เราจะเปิดเผยในบทความหน้า โปรดติดตาม