ยินดีต้อนรับสู่ตอนที่ 3 ของซีรีส์ Ruby on Rails Patterns and Anti-Patterns ในโพสต์ก่อนหน้า เราครอบคลุม Patterns และ Anti-Patterns ทั่วไป รวมถึงที่เกี่ยวกับ Rails Models ในโพสต์นี้ เราจะผ่าน Patterns และ Anti-Patterns บางอย่างที่เกี่ยวข้องกับ Rails Views
Rails Views บางครั้งทำงานได้สมบูรณ์และรวดเร็ว และบางครั้งก็มี Issues ทุกประเภท หากคุณต้องการเพิ่ม Confidence ในการจัดการ Views ของคุณ หรือคุณแค่อยากเรียนรู้เพิ่มเติมเกี่ยวกับหัวข้อนี้ โพสต์นี้เหมาะสำหรับคุณ มาเจาะลึกทันที
ตามที่คุณน่าจะรู้ Rails Framework ติดตาม Convention over Configuration และเนื่องจาก Rails ใหญ่โตบน Model-View-Controller (MVC) Pattern คำขวัญนี้จึงใช้ได้กับ View Code เช่นกัน นี่รวมถึง Markup (ERB หรือ Slim Files), JavaScript และ CSS Files ตรง מבטแรก คุณอาจคิดว่า View Layer ค่อนข้าง Straightforward และ Easy แต่จำไว้ว่า วันนี้มี Mix ของ Technologies อยู่ใน View Layer
เราใช้ JavaScript, HTML และ CSS ใน View สามอย่างนี้อาจนำไปสู่ Confusion และ Disorganization ของ Code — นำไปสู่ Implementation ที่ไม่ทำ Sense มากใน Long Run โชคดีที่วันนี้เราจะผ่าน Common Problems และ Solutions กับ Rails View Layer
Powerlifting Views (Views ที่ยกน้ำหนักหนัก)
นี่คือ Mistake ที่ไม่เกิดบ่อยนัก แต่เมื่อเกิด มันเป็น Eyesore บางครั้ง คนมักใส่ Domain Logic หรือ Querying ตรงใน View นี่ทำให้ View Layer ทำ Heavy-lifting หรือ Powerlifting สิ่งน่าสนใจคือ Rails จริงๆ แล้ว Allow ให้เกิดขึ้นง่าย ไม่มี 'Safety Net' เรื่องนี้ คุณได้รับอนุญาตให้ทำอะไรก็ได้ใน View Layer
โดย Definition View Layer ของ MVC Pattern ควรมี Presentation Logic เท่านั้น ไม่ควรยุ่งกับ Domain Logic หรือ Querying Data ใน Rails คุณได้ ERB Files (Embedded Ruby) ที่ให้คุณเขียน Ruby Code ที่จะ Evaluate กลายเป็น HTML หากเราพิจารณาตัวอย่าง Website ที่ List เพลงใน Index Page View Logic จะอยู่ใน app/views/songs/index.html.erb
เพื่อ Illustrate ว่า "Powerlifting" หมายถึงอะไร และอะไรที่ไม่ควรทำ มาดูตัวอย่างต่อไปนี้:
# 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>
Anti-pattern ใหญ่ที่นี่คือการ Fetch เพลงตรงใน Markup Responsibility ของการ Fetch Data ควร Delegate ให้ Controller หรือ Service ที่ถูกเรียกจาก Controller ฉันบางครั้งเห็นคน Prepare Data บางส่วนใน Controller และภายหลัง Fetch Data เพิ่มใน Views นี่เป็น Bad Design และทำให้ Website ช้าลง เพราะคุณ Stress Database ด้วย Queries บ่อยขึ้น
สิ่งที่คุณควรทำคือ Expose Instance Variable @songs จาก Controller Action และเรียกมันใน Markup เหมือนนี้:
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>
ตัวอย่างเหล่านี้ไกลจาก Perfect หากคุณต้องการเก็บ Controller Code ให้ Readable และหลีกเลี่ยง SQL Pasta ฉันแนะนำให้ดู Blog Post ก่อนหน้า นอกจากนี้ การไม่มี Logic ใน View Layer เพิ่มโอกาสที่คนอื่นจะพยายาม Build Solutions ของพวกเขา Out of it
Make Use of What Rails Gives You (ใช้สิ่งที่ Rails ให้มา)
เราจะทำสั้นๆ ที่นี่ Ruby on Rails بصفته Framework มาพร้อม Helpers สะดวกมาก โดยเฉพาะภายใน View Helpers เล็กๆ น่ารักเหล่านี้ช่วยให้คุณ Build View Layer ได้อย่างรวดเร็วและไร้ความพยายาม ในฐานะ Beginner User ของ Rails คุณอาจถูกล่อลวงให้เขียน Full HTML ภายใน ERb Files เหมือนนี้:
# 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>
ด้วย HTML นี้ คุณควรได้ Form สวยสำหรับเพลงใหม่เหมือน Screenshot ด้านล่าง:
แต่วิธี Rails คุณไม่ต้องและไม่ควรเขียน Plain HTML เหมือนนั้น เพราะ Rails มีหลังคุณอยู่ที่นั่น คุณสามารถใช้ View Helper form_with ที่จะ Generate HTML ให้คุณ form_with Introduced ใน Rails 5.1 และอยู่ที่นั่นเพื่อ Replace form_tag และ form_for ที่อาจคุ้นเคยกับบางคน มาดูว่า form_with ช่วยเราหลุดพ้นจากการเขียนโค้ดเพิ่มเติมได้อย่างไร:
<%= 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 %>
นอกจาก Generate HTML ให้เรา form_with ยัง Generate Authenticity Token ที่ป้องกัน CSRF Attacks ดังนั้นในเกือบทุกกรณี คุณ Better Off ใช้ Designated Helpers เนื่องจากพวกเขาอาจ Play Well กับ Rails Framework หากคุณพยายาม Submit Plain HTML Form มันจะ Fail เพราะไม่มี Valid Authenticity Token ที่ Submit พร้อม Request
นอกจาก form_with, label, text_area, และ submit Helpers ยังมี View Helpers อีกมากมายที่ Out-of-the-box กับ Rails พวกเขาอยู่ที่นั่นเพื่อทำให้ชีวิตคุณง่ายขึ้น และคุณควรรู้จักพวกเขาให้ดีขึ้น One ของ "All-stars" คือ link_to:
<%= link_to "Songs", songs_path %>
ซึ่งจะ Generate HTML ต่อไปนี้:
<a href="/songs">Songs</a>
ฉันจะไม่ Deep ดูแต่ละ Helper เนื่องจากโพสต์นี้จะยาวเกินไป และการผ่านทั้งหมดไม่ใช่ส่วนของ Topic วันนี้ ฉันแนะนำให้คุณผ่าน Rails Action View Helpers Guide และเลือกสิ่งที่คุณต้องการสำหรับ Website ของคุณ
Reusing และ Organizing View Code (การนำกลับมาใช้ใหม่และจัดระเบียบ View Code)
ลองจินตนาการ Perfect Web Application ใน Perfect Use-case ไม่มี If-else Statements แค่ Pure Code ที่เอา Data จาก Controller และวางระหว่าง HTML Tags Application แบบนั้นอาจมีใน Hackathons และ Dreams แต Real-world Applications มี Branches และ Conditions มากตอน Render Views
คุณควรทำอะไรเมื่อ Logic สำหรับแสดง Parts ของ Page ซับซ้อนเกินไป? คุณไปจากที่นี่ไหน? คำตอบทั่วไปคืออาจ Reach สำหรับ Modern JavaScript Library หรือ Framework และ Build สิ่งซับซ้อน แต่นี้โพสต์เกี่ยวกับ Rails Views มาดู Options ที่เรามีภายในพวกเขา
After-Market (Custom) Helpers (Helpers ที่ปรับแต่งเอง)
สมมติว่าคุณต้องการแสดง Call-to-Action (CTA) Button ใต้เพลง แต่มี Catch — Song สามารถมี Download URL หรือ เพราะเหตุผลบางอย่าง อาจ Missing เราอาจถูกล่อลวงให้ Code บางอย่างคล้ายนี้:
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> ...
หากเราดูตัวอย่างข้างต้นเป็น Isolated Presentational Logic มันไม่ดูแย่เลยใช่ไหม? แต่ถ้ามี Conditional Renders มากกว่านี้ Code จะ Less Readable มันยังเพิ่มโอกาสที่บางอย่าง ที่ไหนที่หนึ่ง จะไม่ได้ Render Properly โดยเฉพาะถ้ามี Conditions มากขึ้น
หนึ่งในวิธีต่อสู้กับสิ่งเหล่านี้คือ Extract ไป Helper แยก โชคดีที่ Rails ให้เราวิธีง่ายๆ เขียน Custom Helpers ใน app/helpers เราสามารถสร้าง SongsHelper เหมือนนี้:
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
หากเราเปิด Show Page ของเพลง เราจะยังได้ผลลัพธ์เดียวกัน อย่างไรก็ตาม เราสามารถทำตัวอย่างนี้ Better ได้ ในตัวอย่างข้างต้น เราใช้ Instance Variable @song นี่อาจไม่ Available หากเราตัดสินใจใช้ Helper นี้ที่ที่ @song เป็น nil เพื่อ Cut Off External Dependency ในรูป Instance Variable เราสามารถ Pass Argument เข้า Helper ได้:
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
จากนั้น ใน View เราสามารถเรียก Helper เหมือนด้านล่าง:
app/views/songs/show.html.erb ... <%= song_cta_link(@song) %> ...
ด้วยนั้น เราควรได้ผลลัพธ์เดียวกันใน View เหมือนก่อนหน้า ข้อดีของการใช้ Helpers คือคุณสามารถเขียน Tests สำหรับพวกเขา ให้แน่ใจว่า No Regression เกิดขึ้นเกี่ยวกับพวกเขาในอนาคต ข้อเสียคือพวกเขา Globally Defined และคุณต้อง Ensure ว่า Helper Names Unique ทั่ว App ของคุณ
หากคุณไม่ใช่ Fan ของการเขียน Rails Custom Helpers คุณสามารถ Opt-in สำหรับ View Model Pattern ด้วย Draper Gem หรือคุณสามารถ Roll Your Own View Model Pattern ที่นี่ มันไม่ควรซับซ้อนนัก หากคุณเพิ่งเริ่มต้นกับ Web App ฉันแนะนำให้เริ่มช้าๆ โดยเขียน Custom Helpers และหากนั่นนำไปสู่ Pain ให้หันไป Solution อื่นๆ
DRY up Your Views (ทำให้ Views แห้ง/ไม่ซ้ำ)
สิ่งที่ฉันชอบจริงๆ ตอนเริ่ม Rails คือความสามารถในการ DRY up Markup ของคุณได้ง่าย จนเกือบไม่น่าเชื่อ Rails ให้คุณสรאות Partials — Reusable Code Pieces ที่คุณสามารถ Include ได้ทุกที่ ตัวอย่าง หากคุณ Render เพลงในหลายที่ และมี Code เดียวกันทั่วหลาย Files มี Sense ที่จะสร้าง Song Partial
สมมติว่าคุณแสดงเพลงของคุณเหมือนด้านล่าง:
# 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 %>
แตคุณยังอยากแสดงมันบนหน้าอื่นด้วย Markup เดียวกัน จากนั้นคุณสามารถสร้าง File ใหม่ด้วย Underscore Prefix เหมือน 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) %>
และจากนั้น ที่ไหนที่คุณอยาก Include Song Partial คุณเพียงทำดังนี้:
... <%= render "song" %> ...
Rails จะทำ Auto-lookup ว่า Partial _song Exists และจะ Render มัน คล้ายกับ Example กับ Custom Helpers 最好คือเราเอา Instance Variable @song ออกจาก 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) %>
จากนั้น เราจะต้อง Pass Song Variable เข้า Partial ทำให้มัน Reusable มากขึ้นและ Suitable สำหรับ Included ในที่อื่น
... <%= render "song", song: @song %> ...
ความคิดเห็นสุดท้าย
นั่นคือทั้งหมดสำหรับโพสต์นี้ เพื่อสรุป เราผ่าน Patterns และ Anti-Patterns ไปบ้างที่คุณอาจพบใน Rails View Realm นี่คือ Takeaways ไปบ้าง:
- Avoid Complex Logic ใน UI (อย่าให้ View ทำ Powerlifting มาก)
- Learn รู้ว่า Rails ให้อะไร Out-of-the-box ในแง่ของ View Helpers
- Structure และ Reuse Code ของคุณด้วย Custom Helbles และ Partials
- อย่า Depend บน Instance Variables มากเกินไป
ในโพสต์ถัดไป เราจะครอบคลุม Rails Controller Patterns และ Anti-Patterns ที่สิ่งต่างๆ อาจค่อนข้าง Messy Stay tuned สำหรับนั้น
Until the next one, cheers!
P.S. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่เผยแพร่ สมัครรับข่าวสาร Ruby Magic newsletter และจะไม่พลาดโพสต์เด็ดๆ ไปเลย!