Hotwire เป็นหัวข้อที่น่าสนใจมากในช่วงนี้สำหรับนักพัฒนา Rails ทุกคน หากคุณทำงานกับ Rails แล้ว มีโอกาสสูงที่คุณเคยได้ยินเกี่ยวกับมันมาก่อนแล้ว
Hotwire คืออะไร?
Hotwire ไม่ใช่ไลบรารีเพียงตัวเดียว แต่มันเป็นแนวทางใหม่ในการสร้างเว็บและแอปพลิเคชันมือถือโดยการส่ง HTML ผ่านสาย (over the wire) มันประกอบด้วย Turbo, Stimulus และ Strada (ซึ่งจะวางตัวออกมาภายในปีนี้) เราจะพูดถึงแต่ละส่วนอย่างละเอียดในส่วนถัดไป
หมายเหตุ: แม้ Hotwire จะเชื่อมโยงกับ Rails อย่างแน่นอน แต่มันเป็นภาษาอิสระ (language-agnostic) จึงสามารถทำงานได้ดีกับแอปพลิเคชันอื่นๆ เช่นกัน ผมเคยใช้ Stimulus ในโปรดักชันบนแอปที่ไม่ใช่ Rails และเว็บไซต์สแตติกหลายแห่ง คุณสามารถใช้ Turbo โดยไม่ต้องมี Rails ได้เช่นกัน
แต่ตอนนี้มากลับมาที่โลกของ Rails ก่อน
ทำไมควรใช้ Hotwire ในแอป Rails ของคุณ?
เมื่อไหร่จึงควรใช้ Hotwire? คำตอบคือทุกที่ที่คุณต้องการเพิ่มการโต้ตอบให้กับแอปพลิเคชัน ตัวอย่างเช่น หากคุณต้องการ:
- แสดง/ซ่อนเนื้อหาแบบมีเงื่อนไขตามการโต้ตอบของผู้ใช้ (เช่น ฟอร์มที่อยู่ที่รายการจังหวัดเปลี่ยนตามประเทศที่เลือก)
- อัปเดตเนื้อหาแบบเรียลไทม์ (เช่น ฟีดแบบ Twitter ที่โพสต์ใหม่ถูกเพิ่มเข้าไปในหน้าโดยอัตโนมัติ)
- โหลดส่วนต่างๆ ของหน้าแบบ Lazy Loading (เช่น ภายใน Accordion คุณสามารถโหลดเฉพาะหัวข้อและทำเครื่องหมายรายละเอียดให้โหลดภายหลังเพื่อเพิ่มความเร็ว)
องค์ประกอบของ Hotwire
ตามที่กล่าวไว้ Hotwire เป็นชุดรวมของเทคนิคใหม่ (และบางอย่างเก่า) สำหรับสร้างเว็บแอป
Turbo
HTML เป็นหัวใจของ Turbo Turbo ให้เทคนิคหลายอย่างในการจัดการข้อมูล HTML ที่ส่งมาผ่านสายและแสดงผลบนแอปพลิเคชันของคุณโดยไม่ต้องโหลดหน้าใหม่ทั้งหน้า Turbo ประกอบด้วย:
Turbo Drive
หากเคยใช้ Turbolinks มาก่อน คุณจะคุ้นเคยกับ Turbo Drive ทันที ตัวมันคือโค้ด JavaScript ที่ intercept เหตุการณ์บนแอป โหลด HTML แบบอะซิงโครนัส และแทนที่ส่วนของ HTML markup
Turbo Frames
Turbo Frames แยกส่วนของ markup ออกเป็นส่วนต่างๆ ที่สามารถโหลดได้อย่างอิสระ
เช่น ถ้าคุณมีบล็อกแอป เนื้อหาบทความและความคิดเห็นเป็นสองส่วนที่เกี่ยวข้องแต่อิสระ คุณสามารถแยกพวกมันออก เพื่อให้การนำทางทำงานได้แยกกัน หรือแม้แต่โหลดแบบอะซิงโครนัสด้วย Turbo Frames
Turbo Streams
Turbo Streams ให้ utilities เพื่อนำข้อมูลเรียลไทม์เข้าสู่แอปได้ง่าย ตัวอย่างเช่น คุณสร้างฟีดข่าวแบบ Twitter ต้องการดึง Tweet ใหม่เข้า Feed ทันทีที่โพสต์โดยไม่ต้องโหลดหน้าใหม่ Turbo Streams ทำให้คุณทำได้โดยไม่ต้องเขียน JS แค่บรรทัดเดียว
Turbo Native
Turbo Native ให้คุณสร้าง Native wrapper รอบเว็บแอป การนำทางและการโต้ตอบจะรู้สึกเหมือน Native โดยคุณไม่ต้องเขียนหน้าจอทั้งหมดใหม่แบบ Native
คุณยังคงส่งมอบส่วนที่เหลือผ่านเว็บ ซึ่งช่วยให้คุณโฟกัสที่ส่วนที่โต้ตอบได้จริงและทำให้ถูกต้อง
Stimulus
Stimulus เป็นเฟรมเวิร์ก JavaScript สำหรับเขียน Controller ที่โต้ตอบกับ HTML ของคุณ
สมมติเราต้องเพิ่ม attribute อย่าง data-controller, data-action, และ data-target ให้กับ element บนหน้า เราจะเขียน Stimulus controller ที่เข้าถึง element ที่รับเหตุการณ์ตาม attribute เหล่านั้น ตัวอย่าง:
ง่ายมากที่จะเข้าใจว่ามันทำอะไรโดยไม่ต้องอ่าน Stimulus controller ที่เกี่ยวข้อง
นี่คือ Controller ที่ไปกับ HTML ด้านบน:
นี่คือหัวใจของ Stimulus: ทำให้สิ่งต่างๆ เรียบง่ายและนำกลับมาใช้ซ้ำได้
ตอนนี้ หากคุณต้องการปุ่ม Copy to Clipboard บนหน้าอื่น คุณแค่ reuse controller นั้น เพิ่ม attribute data-* บน markup เพื่อให้ทุกอย่างทำงาน
Strada
น่าเสียดายที่เรายังไม่รู้มากเกี่ยวกับ Strada แต่จะช่วยให้เว็บแอปสื่อสาร (และอาจจะทำการกระทำ) กับแอป Native โดยใช้ HTML bridge attributes
วิธีใช้ Hotwire ในแอป Ruby on Rails ของคุณ
ผมไม่ต้องการใช้เวลามากเกินไปในการอธิบายการติดตั้ง Hotwire หรือ Use case พื้นฐาน ทีม Hotwire ทำงานดีเยี่ยมแล้วใน Screencast ของพวกเขา สำหรับคำแนะนำเต็มรูปแบบ โปรดดูการติดตั้ง turbo-rails และการติดตั้ง Stimulus
มาดู Use case ที่พบบ่อยของ Hotwire กันเลย
Endless Scroll (เลื่อนไม่รู้จบ)
ด้วย Turbo Frames เราสามารถทำหน้าที่มี Pagination อัตโนมัติเมื่อผู้ใช้เลื่อนลงได้ง่ายๆ ต้องทำ 2 อย่าง:
- Render แต่ละ "หน้า" ภายใน Frame ของตัวมันเอง โดยผนวกหมายเลขหน้ากับ Frame ID (เช่น
turbo_frame_tag "posts_#{@posts.current_page}") - ใช้ Frame แบบ
lazyสำหรับหน้าถัดไป เพื่อไม่ให้โหลดอัตโนมัติจนกว่าจะมองเห็น
หมายเหตุ: ตัวอย่างนี้ใช้เมธอดจาก Kaminari แต่คุณสามารถปรับให้ใช้กับ Pagination library อื่นได้
เราไม่ต้องทำอะไรพิเศษใน Controller เมธอด index ธรรมดาทำงานได้:
เทคนิคคือเราใช้ Nested frames โดย Frame ของหน้าถัดไปอยู่ภายใน Frame ของหน้าก่อนหน้า ดังนั้นเมื่อหน้าแรกโหลด Frame ของหน้าที่ 2 จะอยู่ท้ายสุด เมื่อผู้ใช้เลื่อนถึง Frame นั้น จะถูกแทนที่ด้วยเนื้อหาหน้าที่ 2 Frame แบบ lazy ของหน้าที่ 3 จะ render อยู่ท้ายสุด
Dynamic Forms (ฟอร์มไดนามิก)
คุณสามารถทำ Dynamic Forms ด้วย Hotwire ได้ง่ายโดยไม่ต้องเขียน Logic Toggle field ฝั่ง Front end เรื่องนี้ซับซ้อนกว่า Endless Scroll เพราะรวม Turbo Stream และ Stimulus ด้วยกัน
เริ่มจากฟอร์มก่อน:
ฟอร์มง่ายมาก — แสดง Select kind ที่มีตัวเลือก News และ Blog เราต้องการเปลี่ยนค่า Category ที่มีอยู่ตาม Kind ที่เลือก (สมมติ categories_for_kind(@post.kind) คืนค่ารายการ Category สำหรับ Kind นั้น)
ดูใกล้ๆ จะเห็นว่าเราเพิ่ม Data attributes ให้ฟอร์ม data-target จะเชื่อม Element ฟอร์มกับ Target form ของ RefreshFormController Stimulus Controller และ data-action ค่า change->refresh-form#refreshForm จะเรียกเมธอด refreshForm บน Controller ที่เชื่อมทุกครั้งที่ Select kind เปลี่ยน
มาดู Stimulus Controller ของเรา:
ทุกครั้งที่เรียก refreshForm เราแค่ส่ง Request PUT ไปที่ URL ของ Controller (ตั้งค่าผ่าน data-refresh-form-url บน Element เดียวที่มี data-controller="refresh-form") ส่วนสำคัญคือ responseKind ตั้งเป็น turbo-stream ไลบรารี @rails/request เข้าใจ Response นี้และทำตาม Instruction ใน Stream
เหลือแค่ Return Stream ที่ถูกต้องจาก refresh_form ให้ Turbo เข้าใจและอัปเดตฟอร์ม
แค่อัปเดต Attribute บน Post และระบุว่าต้องการตอบกลับในรูปแบบ turbo_stream (ดังนั้นมันจะหา refresh_form.turbo_stream.erb)
ขั้นตอนนี้เรา Reuse form Partial ของเรา ห่ออยู่ใน turbo_stream พร้อม Action replace
และแค่นี้คุณก็ได้ Dynamic Form ที่ทำงานแล้ว ผมรู้ว่าดูซับซ้อน แต่ refresh Stimulus Controller เป็น Shared part ที่คุณสามารถนำไปใช้กับ Dynamic Form ทั้งหมดโดยเพียงแค่เพิ่ม data-* attributes ที่ถูกต้อง กล่าวคือ ตอนนี้คุณได้ Server-side Dynamic Form Refresh โดยไม่ต้องเขียน JS ใหม่สำหรับฟอร์มอื่นๆ เยี่ยมมากใช่ไหม?
Append Content to Pages Without Reloading (เพิ่มเนื้อหาโดยไม่โหลดใหม่)
Use case ถัดไปที่ Hotwire ทำได้ง่ายคือ Streaming HTML ผ่าน WebSocket และอัปเดตหน้าด้วยเนื้อหาใหม่เมื่อมาถึง ตัวอย่างดีคือส่วนความคิดเห็นของ GitHub คุณสามารถทำได้ง่ายด้วย Turbo Streams
มี 2 ส่วน:
แรก เรา Embed Turbo Stream Listener บนหน้า Listing ที่เปิด WebSocket Connection ไป Server และรอ Event
ต่อมา เรา Update Model ให้ Broadcast Comment ใหม่ไปยัง Stream
คุณไม่ต้องทำอะไรเพิ่ม Turbo จะ Render Partial app/views/comments/_comment.html.erb สำหรับ Comment ใหม่แต่ละอันและส่งผ่าน WebSocket มันจะถูกดูดโดย Turbo JS และ Prepend ไปที่ Target ID comments
มาก้าวหนึ่ง เพิ่ม Indicator ให้ Comment ใหม่ทั้งหมดด้วย Stimulus Controller เล็กๆ
ก่อน แก้ไข Broadcast และ Partial comment ให้รวม Controller แบบมีเงื่อนไข
Stimulus Controller นี้เพิ่ม Class Highlight พิเศษตอน Connect 3 วินาที แล้วลบออก
หมายเหตุ: คุณต้องอัปเดต CSS Highlight ตามการมีอยู่ของ Class นั้นด้วย
Controller นี้เสร็จแล้ว คุณสามารถ Reuse ได้กับอะไรก็ตามที่ต้องการ Highlight Class คุณแม้แต่ปรับให้รับ Duration และ Class Name จาก Data Attributes ถ้าต้องการความยืดหยุ่น
นี่คือความดีของ Hotwire — พามันไปไกล และคุณไม่ต้องมาคีย์ JS เอง ตอนที่ต้องเขียน JS จริง Stimulus ให้เครื่องมือสร้าง Controller เล็กๆ แบบ Generic ที่ Reuse ได้
สรุปและแหล่งข้อมูลเพิ่มเติม
ชุมชน Rails ตื่นเต้นมากกับการมาของ Hotwire และแน่นอนว่าน่าสนใจ
ในบทความนี้ เราดูองค์ประกอบหลักของ Hotwire และวิธีใช้ในแอป Rails เราได้สัมผัสวิธีนำชีวิตให้แอปด้วย Turbo และ Stimulus
Screecast แนะนำ Hotwire อย่างเป็นทางการ และเอกสาร Turbo เป็นสถานที่ดีที่จะดูว่า Hotwire และ Turbo ทำอะไรให้คุณได้บ้าง
สำหรับการใช้งานขั้นสูง แนะนำให้ไปที่ GitHub Repo ของ turbo-rails น่าเสียดายที่เอกสารค่อนข้างน้อย แต่ถ้าคุณไม่กล้าลงมืออ่านโค้ดและ Inline Comments ใน:
Turbo::FramesHelperสำหรับ Turbo FramesTurbo::Broadcastableสำหรับ Broadcast ไป Turbo Streams จากโค้ดTurbo::Streams::TagBuilderสำหรับ Broadcast ไป Turbo Streams ใน Controller Actions แบบ Inline
Happy Coding!
P.S. หากต้องการอ่านบทความ Ruby Magic ทันทีที่เผยแพร่ สมัคร Newsletter Ruby Magic ของเราและจะไม่พลาดบทความเด็ดๆ ไป!
Sapan Diwakar
ผู้เขียน אורח Sapan Diwakar เป็น Full-stack Developer เขียนเกี่ยวกับความสนใจบนบล็อกของตัวเอง และเป็นแฟนตัวแข็งของการทำให้สิ่งต่างๆ เรียบง่าย ไม่ว่าจะเป็นชีวิตหรือโค้ด ตอนไม่ได้ทำงานกับเทคโนโลยี เขาชอบใช้เวลาในสวน ป่าเดินป่า และเล่นกีฬากลางแจ้ง
บทความทั้งหมดโดย Sapan Diwakar