Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เร่งความเร็ว Rails Apps ด้วย Hotwire: Server-Side Rendering โดยไม่ต้อง JavaScript

หากคุณมองหาวิธีเร่งการเปลี่ยนหน้าและการส่งฟอร์ม และแบ่งหน้าที่ซับซ้อนเป็น 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:

เร่งความเร็ว Rails Apps ด้วย Hotwire: Server-Side Rendering โดยไม่ต้อง JavaScript 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
...

หน้าสุดท้ายควรเป็นแบบนี้:

เร่งความเร็ว Rails Apps ด้วย Hotwire: Server-Side Rendering โดยไม่ต้อง JavaScript 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