Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm chủ Ruby on Rails: View Pattern & Anti-pattern Phổ biến

Chào mừng quay lại phần ba của series Ruby on Rails Patterns and Anti-Patterns. Trong bài trước, chúng ta đã bao quát pattern và anti-pattern nói chung cũng như liên quan đến Rails Models. Trong bài này, chúng ta sẽ đi qua một số pattern và anti-pattern liên quan đến Rails views.

Rails views đôi khi hoạt động hoàn hảo và nhanh, và đôi khi lại có đủ loại vấn đề. Nếu bạn muốn tăng confidence khi handle view hoặc chỉ muốn học thêm về chủ đề, bài blog này dành cho bạn. Hãy đi thẳng vào vấn đề.

Như bạn có thể biết, Rails framework tuân theo convention over configuration. Vì Rails lớn về Model-View-Controller (MVC) pattern, motto tự nhiên áp dụng cho View code. Điều này bao gồm markup (ERB hoặc Slim files), JavaScript và CSS files. Lúc đầu, bạn có thể nghĩ View layer khá straightforward và dễ dàng, nhưng hãy nhớ rằng ngày nay, có sự mix của công nghệ sống trong View layer.

Chúng ta dùng JavaScript, HTML, và CSS trong view. Ba thứ này có thể dẫn đến confusion và disorganization code — dẫn đến implementation không có nhiều ý nghĩa trong long run. May mắn thay, hôm nay chúng ta sẽ đi qua một số vấn đề phổ biến và giải pháp với Rails View layer.

Powerlifting Views (View Gánh Team)

Đây là lỗi không xảy ra thường, nhưng khi xảy ra, nó là "sore eyes" (đau mắt). Đôi khi, mọi người 경향 để domain logic hoặc query trực tiếp vào View. Điều này làm View layer làm heavy-lifting hay powerlifting. Thú vị là Rails thực sự cho phép điều này dễ dàng xảy ra. Không có 'safety net' khi nói đến việc này, bạn được phép làm gì muốn trong View layer.

Theo định nghĩa, View layer của MVC pattern nên chứa presentation logic. Nó không nên bị phiền toái bởi domain logic hoặc query data. Trong Rails, bạn có ERB files (Embedded Ruby) cho phép viết Ruby code sau đó evaluated thành HTML. Nếu xét ví dụ website liệt kê bài hát ở trang index, view logic sẽ ở app/views/songs/index.html.erb.

Để minh họa "powerlifting" nghĩa là gì và điều không nên làm, hãy xem ví dụ sau:

# app/views/songs/index.html.erb <div class="songs"> <% Song.where(published: true).order(:title) do |song| %> <section id="song_<%= song.id %>"> <span><%= song.title %></span> <span><%= song.description %></span> <a href="<%= song.download_url %>">Download</a> </section> <% end %> </div>

Một anti-pattern khổng lồ ở đây là fetch songs ngay trong markup. Trách nhiệm fetch data nên delegate cho controller hoặc service được gọi từ controller. Tôi đôi khi thấy người chuẩn bị data trong controller và sau đó fetch thêm data trong view. Đây là bad design và làm website chậm hơn vì bạn stress database với query nhiều hơn.

Điều bạn nên làm thay vào đó là expose instance variable @songs từ controller action và gọi nó trong markup, như sau:

class SongsController < ApplicationController ... def index @songs = Song.all.where(published: true).order(:title) end ... end
# app/views/songs/index.html.erb <div class="songs"> <% @songs.each do |song| %> <section id="song_<%= song.id %>"> <span><%= song.title %></span> <span><%= song.description %></span> <a href="<%= song.download_url %>">Download</a> </section> <% end %> </div>

Các ví dụ này còn xa perfect. Nếu bạn muốn controller code readable hơn và tránh SQL Pasta, tôi urge bạn check blog post trước. Cũng, bỏ logic ra khỏi View layer tăng cơ hội người khác sẽ cố gắng build solution dựa trên nó.

Tận dụng Những Gì Rails Cho Bạn

Chúng ta sẽ giữ ngắn ở đây. Ruby on Rails as a framework đi kèm nhiều helper tiện lợi, đặc biệt bên trong view. Những helper nhỏ xíu này cho phép bạn build View layer nhanh và không tốn công. Là beginner Rails, bạn có thể tempted viết full HTML trong ERb files như sau:

# app/views/songs/new.html.erb <form action="/songs" method="post"> <div class="field"> <label for="song_title">Title</label> <input type="text" name="song[title]" id="song_title"> </div> <div class="field"> <label for="song_description">Description</label> <textarea name="song[description]" id="song_description"></textarea> </div> <div class="field"> <label for="song_download_url">Download URL</label> <textarea name="song[download_url]" id="song_download_url"></textarea> </div> <input type="submit" name="commit" value="Create Song"> </form>

Với HTML này, bạn sẽ get form đẹp cho bài hát mới như screenshot dưới:

Nhưng với Rails, bạn không cần và không nên viết plain HTML như vậy vì Rails có lưng bạn ngay đó. Bạn có thể dùng view helper form_with sẽ generate HTML cho bạn. form_with được giới thiệu trong Rails 5.1 và nó thay thế form_tag và form_for có thể familiar với một số người. Hãy xem form_with cứu chúng ta khỏi viết code thừa như thế nào:

<%= form_with(model: song, local: true) do |form| %> <div class="field"> <%= form.label :title %> <%= form.text_field :title %> </div> <div class="field"> <%= form.label :description %> <%= form.text_area :description %> </div> <div class="field"> <%= form.label :download_url do %> Download URL <% end %> <%= form.text_area :download_url %> </div> <%= form.submit %> <% end %>

Ngoài generate HTML, form_with cũng generate authenticity token ngăn CSRF attack. Vậy hầu hết case, bạn better off dùng designated helper vì chúng có thể play well với Rails framework. Nếu bạn thử submit plain HTML form, nó sẽ fail vì không có valid authenticity token submitted với request.

Ngoài form_with, label, text_area, và submit helper, còn một đống view helper khác out-of-the-box với Rails. Chúng ở đó để làm cuộc sống bạn dễ dàng và bạn nên biết chúng tốt hơn. Một "all-stars" chắc chắn là link_to:

<%= link_to "Songs", songs_path %>

Sẽ generate HTML sau:

<a href="/songs">Songs</a>

Tôi sẽ không đi sâu vào chi tiết mỗi helper, vì bài sẽ quá dài và đi qua tất cả không phải chủ đề hôm nay. Tôi suggest bạn đọc Rails Action View helpers guide và pick cái bạn cần cho website.

Tái sử dụng và Tổ chức View Code

Hãy tưởng tượng perfect web application. Trong perfect use-case, không có if-else statement, chỉ pure code lấy data từ controller và đặt giữa HTML tag. Loại app đó có thể tồn tại trong hackathon và giấc mơ, nhưng real-world app có đống branch và condition khi render view.

Làm sao khi logic show phần trang phức tạp? Đi đâu từ đó? Câu trả lời chung là perhaps reach cho modern JavaScript library/framework và build thứ phức tạp. Nhưng, vì bài này về Rails Views, hãy xem option chúng ta có bên trong.

After-Market (Custom) Helpers

Giả sử bạn muốn show call-to-action (CTA) button dưới bài hát. Nhưng có catch — một Song có thể có download URL hoặc, vì lý do gì, nó missing. Chúng ta có thể tempted code như sau:

app/views/songs/show.html.erb ... <div class="song-cta"> <% if @song.download_url %> <%= link_to "Download", download_url %> <% else %> <%= link_to "Subscribe to artists updates", artist_updates_path(@song.artist) %> <% end %> </div> ...

Nếu nhìn ví dụ trên như isolated presentational logic, nó không quá tệ, phải không? Nhưng, nếu có nhiều conditional render này, code kém readable hơn. Nó cũng tăng cơ hội cái gì đó, ở đâu đó không render đúng, đặc biệt nếu có nhiều condition hơn.

Một cách fight những này là extract chúng vào helper riêng. May mắn thay, Rails cho cách dễ dàng viết custom helper. Trong app/helpers chúng ta có thể tạo SongsHelper, như sau:

module SongsHelper def song_cta_link content_tag(:div, class: 'song-cta') do if @song.download_url link_to "Download", @song.download_url else link_to "Subscribe to artists updates", artist_updates_path(@song.artist) end end end end

Nếu mở show page của bài hát, chúng ta vẫn get cùng kết quả. Tuy nhiên, chúng ta có thể làm ví dụ này tốt hơn. Trong ví dụ trên, chúng ta dùng instance variable @song. Điều này có thể không available nếu chúng ta quyết định dùng helper ở nơi @song là nil. Vậy để cut off external dependency dạng instance variable, chúng ta có thể pass argument vào helper như sau:

module SongsHelper def song_cta_link(song) content_tag(:div, class: 'song-cta') do if song.download_url link_to "Download", song.download_url else link_to "Subscribe to artists updates", artist_updates_path(song.artist) end end end end

Sau đó, trong view, chúng ta gọi helper như dưới:

app/views/songs/show.html.erb ... <%= song_cta_link(@song) %> ...

Với đó, chúng ta nên get cùng kết quả trong view như trước. Điều tốt của helper: bạn có thể viết test cho chúng đảm bảo không regression về chúng trong tương lai. Con: chúng được define globally và bạn phải đảm bảo helper name unique across app.

Nếu bạn không fan viết Rails custom helper, bạn luôn có thể opt-in View Model pattern với Draper gem. Hoặc tự roll View Model pattern, không quá phức tạp. Nếu mới bắt đầu web app, tôi suggest bắt đầu chậm bằng custom helper và nếu đau đớn, chuyển sang giải pháp khác.

DRY up Your Views (Khô ráo View của bạn)

Điều tôi thực sự thích khi bắt đầu Rails là khả năng dễ dàng DRY up markup đến mức gần như không tin nổi. Rails cho bạn tạo partials — reusable code pieces bạn có thể include ở đâu cũng được. Ví dụ, nếu bạn render songs ở nhiều nơi, và có cùng code across nhiều file, thì tạo song partial hợp lý.

Giả sử bạn show song như dưới:

# app/views/songs/show.html.erb <p id="notice"><%= notice %></p> <p> <strong>Title:</strong> <%= @song.title %> </p> <p> <strong>Description:</strong> <%= @song.description %> </p> <%= song_cta_link %> <%= link_to 'Edit', edit_song_path(@song) %> | <%= link_to 'Back', songs_path %>

Nhưng, bạn cũng muốn show nó trên trang khác với cùng markup. Khi đó bạn tạo file mới với underscore prefix như app/views/songs/_song.html.erb.

# app/views/songs/_song.html.erb <p> <strong>Title:</strong> <%= @song.title %> </p> <p> <strong>Description:</strong> <%= @song.description %> </p> <%= song_cta_link(@song) %>

Và ở đâu bạn muốn include song partial, bạn làm sau:

... <%= render "song" %> ...

Rails sẽ auto-lookup xem _song partial có tồn tại không và render nó. Tương tự ví dụ custom helper, tốt nhất là bỏ instance variable @song trong partial.

 # app/views/songs/_song.html.erb <p> <strong>Title:</strong> <%= song.title %> </p> <p> <strong>Description:</strong> <%= song.description %> </p> <%= song_cta_link(song) %>

Sau đó, chúng ta cần pass song variable vào partial, làm nó reusable hơn và phù hợp include ở nơi khác.

... <%= render "song", song: @song %> ...

Tư duy Cuối Cùng

Đó là tất cả cho bài này. Để tóm tắt, chúng ta đã đi qua một vài pattern và anti-pattern bạn có thể gặp trong Rails View realm. Đây là vài takeaway:

  • Tránh logic phức tạp trong UI (đừng làm View làm nhiều powerlifting)
  • Học những gì Rails cho bạn out-of-the-box về View helpers.
  • Cấu trúc và tái sử dụng code với custom helpers và partials
  • Không phụ thuộc instance variable quá nhiều.

Trong bài sau, chúng ta sẽ cover Rails Controller patterns and anti-patterns nơi mọi thứ có thể khá messy. Stay tuned cho đó.

Cho đến lần sau, cheers!

P.S. Nếu bạn muốn đọc bài Ruby Magic ngay khi ra mắt, hãy subscribe newsletter Ruby Magic và không bao giờ bỏ lỡ bài nào!