หากคุณมองหาวิธีเร่งการเปลี่ยนหน้าและการส่งฟอร์ม และแบ่งหน้าที่ซับซ้อนเป็น components โดยไม่เขียน JavaScript code บทความนี้จะช่วยนำ Rails สู่ระดับต่อไปด้วย Hotwire บทความนี้จะสอนการใช้ tool สำหรับ server-side rendering
Hotwire คืออะไร?
Hotwire ให้วิธีสร้าง modern web applications โดยไม่เขียน JavaScript โดยส่ง HTML แทน JSON over the wire ทำให้หน้าโหลดเร็วขึ้น มัน maintain rendering บน server-side และอนุญาตให้ development experience เรียบง่ายและ productive มากขึ้น ตามที่ Rails เหมือนจะ incorporate เข้า technologies ของมันเสมอ โดยไม่犧牲 speed หรือ responsiveness ที่เกี่ยวกับ traditional single-page application (SPA)
Core ของ Hotwire คือ Turbo gem ชุดเทคนิค complementary ที่เร่ง navigation ของหน้าและการส่งฟอร์ม แบ่งหน้าที่ซับซ้อนเป็น components และส่ง partial updates ของหน้าผ่าน WebSocket (ประกอบด้วย ActionCable, channels และ streaming data)
ทำไมควรใช้?
หากคุณ struggle กับ JavaScript และอยากได้ประโยชน์จาก better user experience ผ่านความรู้สึก navigate ระหว่างหน้าเร็วขึ้น Hotwire เป็น alternative สำหรับคุณ
Hotwire ทำงานยังไง? Hotwire ใช้ server-side rendering (SSR) แก้ปัญหาบางอย่างของ SPAs ขณะรักษาข้อดีหลัก SSR กลับกระบวนการ rendering นำส่วนหนึ่งของ SPA rendering effort ไปที่ server คล้าย traditional loading SSR ให้ users loading application มีประสิทธิภาพมากขึ้น เนื่องจากบางส่วน rendering ทำบน server นอกจาก performance อาจ improve ยังช่วย deal กับ SEO problems เช่น indexing
ใช้ง่ายไหม?
ใช้ง่ายมาก แค่ต้องการ default package ของ Rails project (Ruby, RoR, ActionCable, WebSocket), Turbo gem download JS dependencies ทั้งหมด และ Redis เก็บ temporary data ขณะ browse WebSocket
ไม่ต้องเรียนภาษาอื่น (JS) เพื่อได้ speed ของ single-page web application เพราะ Turbo complemented โดย Turbo Drive, Frames, Streams และ Native Drive เร่ง links และ forms ลดความต้องการ reload networks ขณะ Frames แบ่ง networks เป็น independent contexts ที่โหลดง่ายขึ้น
ใช้กับ Rails ยังไง?
ส่วนนี้จะแสดงตัวอย่างทีละขั้นตอน
สำหรับตัวอย่างนี้ ต้องการ:
- Ruby
- Ruby on Rails
- Redis
- SQLite (default database)
- Gem Hotwire
- TurboRails
- StimulusJS
- WebSocket
- ActionCable
จะสร้างโปรเจกต์อะไร?
เราจะสร้าง social media project
เปิด terminal เริ่มด้วย rails project ใหม่:
rails new social-media
เข้า project:
cd social-media
เพิ่ม Hotwire
เพิ่ม Hotwire gem ในโปรเจกต์:
bundle add hotwire-rails
หรือเปิด Gemfile เพิ่ม:
gem "hotwire-rails"
ถ้าใช้ option ที่สอง ต้องรัน bundle ตอนนี้
bundle install
จากนั้น install:
rails hotwire:install
Initial settings
ตอนนี้เวลาดีสำหรับ analyze initial settings ที่ Hotwire ทำ ไปที่ Gemfile ควรเป็นแบบนี้:
gem 'redis', '~> 4.0'
ทำไมต้อง Redis?
Redis gem ถูกเพิ่มเพราะ ActionCable ต้องการมันเก็บ temporary data ขณะ browse WebSocket แต่แค่ติดตั้ง Redis ไม่พอ ต้องเช็คว่า config ถูกต้อง ไปที่ config/cable.yml ควรเป็นแบบนี้:
development:
adapter: redis
url: redis://localhost:6379/1
แน่ใจว่า Redis รันตอน start application (redis-server)
JS dependencies
เช็ค dependencies ใน package.json:
dependencies: {
@hotwired/turbo-rails: ^7.0.0-beta.5,
@rails/actioncable: ^6.0.0,
@rails/activestorage: ^6.0.0,
@rails/ujs: ^6.0.0,
@rails/webpacker: 4.3.0,
stimulus: ^2.0.0
}
Generate Model ของเรา
หลังเช็คไฟล์ทั้งหมด เราจะ generate views, controllers, models และ migrations สำหรับ table posts ด้วย columns body และ likes รันใน terminal:
rails g scaffold posts body:text likes:integer
สร้าง Database ของเรา
ตอนนี้มีทุกอย่าง generated แล้ว ต้องส่ง changes ไป database รันใน terminal:
rails db:create db:migrate
ถ้าทุกอย่างดี เราสามารถรัน server (rails server) เช็คว่าทุกอย่าง OK ไปที่ page posts ซึ่งจะเป็น https://localhost:3000/posts:
printscreen of the posts directory
Listing the posts
ตอนนี้ list posts สร้างไฟล์ app/views/posts/_post.html.erb เพิ่มโค้ด:
<div style="background: lightgrey; width: 300px; padding: 10px;">
<%= post.body %>
<br>
<%= link_to :edit, edit_post_path(post) %>
<%= button_to "likes (#{post.likes || 0})", post_path(post, like: true), method: :put %>
</div>
<br>
Validations
ต้อง validate body field (not null) และบอก broadcast 显示 tweet บนหน้าเดียวกับ tweet แรกหลังสร้าง แก้ไฟล์ app/models/post.rb ใส่โค้ด:
class Post < ApplicationRecord
validates_presence_of :body
after_create_commit { broadcast_prepend_to :posts }
end
Ordering
ต้อง order posts เปิด controller (app/controllers/posts_controller.rb)
...
def index
@posts = Post.all.order(created_at: :desc)
@post = Post.new
end
...
Wrapping up
แก้ index (app/views/posts/index.html.erb) แสดง new post page และ list ของ posts ทั้งหมด
<%= turbo_stream_from :posts %>
<%= turbo_frame_tag :post_form do %>
<%= render 'posts/form', post: @post %>
<% end %>
<%= turbo_frame_tag :posts do %>
<%= render @posts %>
<% end %>
Redirecting to the index
สุดท้าย แก้ create method ใน controller เพื่อไม่ redirect ไป show post page เก็บทุกอย่างหน้าเดียว
...
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.html { redirect_to posts_path }
format.json { render :show, status: :created, location: @post }
else
format.turbo_stream { render turbo_stream: turbo_stream.replace(@post, partial: 'posts/form', locals: { post: @post }) }
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
...
หน้าสุดท้ายควรเป็นแบบนี้:
printscreen of the final page
Checking the logs
เมื่อสร้าง post เช็ค terminal logs ควรเป็นแบบนี้:
[ActionCable] Broadcasting to posts: "<turbo-stream action=\"prepend\" target=\"posts\"><template><div style=\"background: lightgrey; width: 300px; padding: 10px;\">\n first post\n <br>\n <a href=\"/posts/1/edit\">edit</a>\n <form class=\"button_to\" method=\"post\" action=\"/posts/4?like=true\"><input type=\"hidden\" name=\"_method\" value=\"put\" /><input type=\"submit\" value=\"likes (0)\" /><input type=\"hidden\" name=\"authenticity_token\" value=\"<token>==\" /></form>\n</div>\n<br>\n</template></turbo-stream>"
Thoughts
หมายถึง ActionCable ทำงานกับ turbo stream ด้วย super fast performance ตามที่เห็น: Completed 302 Found in 18ms
Deploying
ต้อง executable อื่นรันควบคู่ Rails server ไหม?
ไม่ เพราะ ActionCable ทำงานดีกับ popular servers เช่น Unicorn, Puma, Passenger
Heroku รองรับ Hotwire ไหม?
ใช่ Heroku รองรับ base ของ Hotwire (websockets) หากมีคำถามเพิ่ม เช็ค Heroku's official documentation
ในกรณีคุณ wonder ทำไมเราใช้ Redis มีอีกเหตุผลหนึ่ง
ถ้า Heroku ใช้โดยไม่มี Redis messages จะไม่ส่งไปทุกคนเมื่อ app scale ไปมากกว่า single dyno เนื่องจาก stateless nature ของ current app clients connected to first dyno ไม่ได้ messages ส่งโดย clients connected to second dyno Redis แก้ปัญหานี้โดยเก็บ message state ใน global storage
Setup Redis บน Heroku
เพื่อ establish Redis CLI session กับ remote Redis instance ใช้ heroku redis:cli หากไม่ระบุ instance instance ที่ REDIS_URL ใช้ default หากมีมากกว่า 1 instance ระบุ instance ที่ต้องการ connect
มันทำงานบน application server ยังไง?
ต้อง setup แต่ละ dyno ใช้ Redis pub-sub system ทุก dynos subscribe ไป channel เดียวกัน (default) และรอ messages เมื่อแต่ละ server ได้ message สามารถ publish ไป connected clients เราจะ cover subscribe ใน thread แยก เพราะ subscribe เป็น blocking function รอ message จะหยุด execution flow นอกจากนี้ ต้องการ Redis connection ที่สอง เพราะ once subscribe command ทำบน connection connection นั้นสามารถ unsubscribe หรือ receive messages เท่านั้น เรียนรู้เพิ่ม scaling ที่นี่
Security
ปัจจุบัน application exposed และ vulnerable หลาย attacks โปรด setup WSS และ sanitize input เรียนรู้เพิ่ม WebSocket Security ที่นี่
สรุป
เป็นไปได้ที่จะใช้ Hotwire ผ่าน new application ตามที่แสดง แต่เป็นไปได้ update จาก Turbolinks
บทความนี้ควรช่วยเข้าใจว่า "magic framework" ทำงานยังไง ในมัน คุณได้พัฒนาในทางปฏิบัติ small simple application แสดง requests ผ่าน aggregated turbo stream พร้อม manipulation และ navigation via WebSocket