0
0
Lập trình
NM

Tạo Component trong Rails mà không cần gem

Đăng vào 3 tháng trước

• 6 phút đọc

Chủ đề:

#webdev#rails#ruby

Giới thiệu

Trong quá trình phát triển ứng dụng Rails, nhiều lập trình viên thường phải đối mặt với yêu cầu không sử dụng các thư viện bên thứ ba như ViewComponent. Trong bài viết này, tôi sẽ chia sẻ về cách xây dựng component trong Rails mà không cần sử dụng gem, giúp bạn duy trì mã nguồn sạch sẽ và dễ bảo trì hơn.

Mục lục

  1. Component Helper
  2. Components với Local Variables
  3. Components với Content Blocks
  4. Components với Decorators
  5. Class-based Components
  6. Ví dụ
  7. Thực hành tốt nhất
  8. Những cạm bẫy thường gặp
  9. Mẹo hiệu suất
  10. Giải quyết sự cố

Component Helper

Trái tim của "Component trong Rails" là component helper. Helper này cung cấp một API sạch sẽ để render các phần tử UI tái sử dụng trong ứng dụng Rails của bạn.

ruby Copy
module ComponentHelper
  def component(name, locals = {}, &block)
    return render(layout: "components/#{name}", locals: locals, &block) if block_given?

    collection = locals.delete(:collection)
    return render(partial: "components/#{name}", collection: collection, as: locals.delete(:as) || name.to_sym, locals: locals) if collection

    render(partial: "components/#{name}", locals: locals)
  end
end

Helper này bọc các phương thức render chuẩn của Rails với một API ngắn gọn hơn. Nó nhận tên component, một hash các biến địa phương, và một block tùy chọn. Helper xác định chiến lược render phù hợp dựa trên các tham số được cung cấp:

  1. Nếu có block, nó sẽ render component như một layout, truyền nội dung block vào template của component;
  2. Nếu một collection được cung cấp, nó sẽ render component cho mỗi mục trong collection;
  3. Nếu không, nó sẽ render component như một partial đơn giản.

Tất cả các component được kỳ vọng sẽ nằm trong thư mục app/views/components/, với tên file được tiền tố bằng dấu gạch dưới (theo quy tắc đặt tên partial của Rails).

Components với Local Variables

Các component đơn giản nhất chỉ là partial với một giao diện đã được định nghĩa. Rails 7.2 đã giới thiệu explicit locals, cho phép chỉ định rõ ràng các biến cần thiết cho một component.

erb Copy
<%# locals: (user:, size: :md, css: user.avatar_css) %>
<%= tag.span user.avatar.present? ? image_tag(user.avatar, class: "size-full object-cover") : user.name.first.upcase, role: :img, class: css %>

Cú pháp này làm rõ rằng tham số user là bắt buộc, trong khi sizecss có giá trị mặc định (lấy từ đối tượng user). Sử dụng component này rất đơn giản:

erb Copy
<%= component "avatar", user: User.first.decorate %>

Component cũng có thể được sử dụng với collections (giống như các partial thông thường):

erb Copy
<%= component "avatar", collection: User.all.map(&:decorate), as: :user %>

Components với Content Blocks

Một số component cần phải bao bọc nội dung. Component section (sử dụng trên trang gốc để xem trước các component) cung cấp layout nhất quán cho các phần nội dung khác nhau:

erb Copy
<%# locals: (name:) %>
<li>
  <%= tag.h2 name, class: "text-base font-bold text-gray-800" %>

  <div class="mt-1 p-4 border border-gray-200 rounded-md">
    <%= yield %>
  </div>
</li>

Component breadcrumbs (lấy từ một ứng dụng thực tế) là một ví dụ khác:

erb Copy
<%# locals: (items: []) %>
<nav aria-label="Breadcrumb">
  <ol class="flex items-center gap-x-1 text-sm font-semibold" itemscope itemtype="https://schema.org/BreadcrumbList">
    <% items.each.with_index do |item, index| %>
      <li class="flex items-center" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
        <%= link_to item[:label], item[:href], class: "text-gray-800 hover:underline", itemprop: "item" %>

        <%= tag.meta itemprop: "name", content: item[:label] %>
        <%= tag.meta itemprop: "position", content: index + 1 %>

        <span class="inline-block ml-1 font-medium text-gray-500" aria-hidden="true">/</span>
      </li>
    <% end %>

    <li class="text-gray-600" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem" aria-current="page">
      <span itemprop="name"><%= yield %></span>

      <%= tag.meta itemprop: "position", content: items.length + 1 %>
    </li>
  </ol>
</nav>

Chỉ cần truyền một hash với labelhref. Block ({ "Here" }) là mục cuối cùng của breadcrumbs:

erb Copy
<%= component("breadcrumbs", items: [{label: "Rails Designer", href: "https://railsdesigner.com"}, {label: "Articles", href: "https://railsdesigner.com/articles/"}]) { "Here" } %>

Components với Decorators

Như đã đề cập trước đó, component avatar sử dụng một decorator để cung cấp các phương thức cụ thể cho view cho model User:

ruby Copy
class User::Decorator < SimpleDelegator
  include ActionView::Helpers::TagHelper

  def avatar_css(size = :md)
    class_names(
      "flex items-center justify-center font-semibold text-gray-600 border border-gray-300/50 overflow-clip rounded-full",
      {
        "size-5 text-sm": size == :sm,
        "size-6 text-base": size == :md,
        "size-8 text-ll": size == :lg,
        "size-10 text-xl": size == :xl
      }
    )
  end
end

Decorator được áp dụng cho các đối tượng User bằng phương thức decorate. Mô hình này giúp tách biệt logic cụ thể cho view khỏi model trong khi vẫn dễ dàng truy cập từ view. Để biết thêm chi tiết về decorators, hãy tham khảo bài viết về nó trước đây.

Class-based Components

Đối với các component phức tạp hơn, việc chuyển logic vào một class Ruby dành riêng có thể hữu ích. Component badge minh họa cách tiếp cận này:

ruby Copy
class BadgeComponent
  include ActionView::Helpers::TagHelper

  def initialize(name)
    @name = name

    raise "Incorrect badge name" if NAMES.exclude? name
  end

  def name
    NAMES[@name]
  end

  def css
    class_names(
      "inline-block px-3 py-0.5",
      "text-xs font-medium",
      "ring ring-offset-0 border border-white/24 rounded-full",
      COLORS[@name]
    )
  end

  private

  NAMES = {
    webmaster: "Webmaster Supreme",
    pwru: "Power User 9000",
  }

  COLORS = {
    webmaster: "bg-blue-100 text-blue-800 ring-blue-200",
    pwru: "bg-purple-100 text-purple-800 ring-purple-200",
  }
end

Template view tương ứng rất đơn giản:

erb Copy
<%# locals: (name:, badge: BadgeComponent.new(name)) %>
<%= tag.span badge.name, class: badge.css %>

Component này được sử dụng như sau:

erb Copy
<%= component("badge", name: :webmaster) %>

Bạn thấy rằng chỉ cần truyền một name và component tự sử dụng các instance của BadgeComponent. Class Ruby có thể bao hàm logic component phức tạp hơn. Điều này có thể giúp dễ dàng kiểm tra và bảo trì. Template view chỉ tập trung vào việc render HTML của component.

Ví dụ

Kho lưu trữ VanillaComponents chứa tất cả các component ví dụ thể hiện các cách tiếp cận khác nhau trong thiết kế component:

erb Copy
<%= component "avatar", user: User.first.decorate %>
<%= component "avatar", collection: User.all.map(&:decorate), as: :user %>
<%= component("breadcrumbs", items: [{label: "Rails Designer", href: "https://railsdesigner.com"}, {label: "Articles", href: "https://railsdesigner.com/articles/"}]) { "Here" } %>
<%= component("input_toggle", value: "my secret") %>
<%= component("badge", name: :webmaster) %>

Những ví dụ này cho thấy cách sử dụng component helper để tạo ra nhiều phần tử UI khác nhau, từ avatar đơn giản đến các component tương tác phức tạp hơn.

Thực hành tốt nhất

  • Sử dụng rõ ràng các locals để giúp người khác hiểu component của bạn dễ hơn.
  • Tránh lồng ghép quá nhiều logic vào trong view, hãy giữ cho code sạch sẽ.
  • Tách biệt logic view vào decorators hoặc class khi cần thiết.

Những cạm bẫy thường gặp

  • Sử dụng helpers mà không kiểm soát được phạm vi.
  • Lạm dụng collection trong các component, gây ra hiệu suất kém.

Mẹo hiệu suất

  • Giảm thiểu số lượng render bằng cách sử dụng cache khi cần.
  • Sử dụng lazy loading cho các component nặng.

Giải quyết sự cố

  • Kiểm tra các biến được truyền vào component để đảm bảo không có lỗi do thiếu biến.
  • Sử dụng logging để theo dõi các vấn đề trong quá trình render.

Kết luận

Hy vọng rằng bài viết này đã cung cấp cho bạn cái nhìn rõ ràng về cách tạo component trong Rails mà không cần đến gem. Việc này không chỉ giúp bạn kiểm soát tốt hơn mã nguồn mà còn giảm thiểu khả năng xuất hiện lỗi trong ứng dụng của bạn. Hãy thử áp dụng những kỹ thuật này vào dự án tiếp theo của bạn và chia sẻ ý kiến của bạn dưới đây!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào