Tích hợp Vue vào trong Rails

Nếu như đã từng làm việc với Laravel, chắc hẳn các bạn cũng biết Vue là framework mà Laravel lựa chọn để hỗ trợ cho phía frontend. Laravel đang là một trong những framework PHP phổ biến nhất hiện nay, vì thế sẽ không quá ngạc nhiên khi độ nổi tiếng của Vue cũng một phần do đó mà tăng lên dù bản thân Vue cũng đã mang trong mình sức mạnh, sự linh hoạt của những người đi trước như React, Angular...

Về phía Ruby on Rails, độ phổ biến cũng như sức mạnh mà framework này mang lại là điều không phải bàn cãi. Tuy nhiên tính cho đến thời điểm hiện tại, phiên bản mới nhất của Rails là 6.0.2.1 vẫn sử dụng Jquery như là một thư viện mặc định hỗ trợ cho phần frontend. Trong khi các Javascript framework ngày một chiếm ưu thế về hiệu năng cũng như sự linh hoạt thì Jquery sẽ lộ ra nhiều hạn chế nếu như đem sử dụng trong các chức năng phức tạp đòi hỏi thao tác nhiều trên DOM. Vậy nếu như bạn yêu thích Rails cũng như yêu thích sự nhỏ gọn và mạnh mẽ của Vue thì tại sao không kết hợp chúng lại với nhau. Đó chắc chắn sẽ mang lại những kết quả rất tuyệt vời.

Cài đặt

Đề bắt đầu chúng ta cần phải cài đặt webpacker. Từ đây chúng ta sẽ cài đặt Vue và các packet cần thiết khác. Thêm gem webpacker vào trong Gemfile, sau đó chạy bundle install để cài đặt:

gem "webpacker"

Để hoàn tất quá trình cài đặt Vue bạn chạy tiếp hay lệnh:

rails webpacker:install
rails webpacker:install:vue

Sau quá trình này trong project của bạn sẽ xuất hiện thêm một số file config. Trong đó hãy chú ý đến file app/javascript/packs/application.js vì đây là nơi chúng ta thực sự bắt đầu với Vue.

Kết hợp

Để sử dụng được các component của Vue bên trong các file view của Rails chúng ta cần khởi tạo một đối tượng Vue và một component toàn cục, sau đó là đăng ký các component khác vào trong component này. Thêm vào trong file app/javascript/packs/application.js:

import "core-js/stable";
import "regenerator-runtime/runtime";
import Vue from 'vue/dist/vue.esm';
import App from '../app.vue';

document.addEventListener('DOMContentLoaded', () => {
  new Vue({
    el: '#vue-app',
    components: {
      App,
    },
  });
});

Như đã nói ở trên, trong trường hợp này App sẽ là component toàn cục. Nó được định nghĩa trong file app/javascript/app.vue:

<script>
import Home from './components/home';
import Navbar from './components/header/navbar';
import PostDetail from './components/posts/detail';

export default {
  components: {
    Home,
    Navbar,
    PostDetail
  }
}
</script>

Đây là nơi chúng ta sẽ đăng ký các component để có thể sử dụng chúng ở bất cứ đâu trong view của Rails. Việc cuối cùng của quá trình kết hợp này sẽ được chúng ta thực hiện trong file app/views/layouts/application.html.erb:

<!DOCTYPE html>
<html>
  <head>
    <%= yield(:title) %>
    <%= stylesheet_link_tag "application", media: "all" %>
    <%= javascript_include_tag "application" %>
    <%= javascript_pack_tag "application" %>
  </head>

  <body>
    <div id="vue-app">
      <app inline-template>
        <div>
           <navbar />
           <h1>You can use any custom component inside the App components</h1>
           <%= yield %>
           <home />
        </div>
      </app>
    </div>
  </body>
</html>

Bên trong thể head chúng ta thêm vào tag <%= javascript_pack_tag "application" %>. Đây là một helper của gem webpacker, chức năng của nó chỉ là đưa nội dung của file application.js sau khi đã compile vào view tương tự như javascript_include_tag của Rails.

Như các bạn đã biết, Vue cần một element để có thể mount vào và trong trường hợp này là thẻ div#vue-app. Tiếp đó chúng ta sử dụng thuộc tính inline-template cho component App. Điều này là rất quan trọng bởi vì nó sẽ cho phép chúng ta có thể nhúng các vue component vào trong các file view của Rails đồng thời không làm ảnh hưởng đến các thành phần khác được yield vào từ các partial khác.

Các cài đặt cần thiết

Về cơ bản, sau bước kết hợp ở trên chúng ta đã có thể sử dụng Vue trong Rails không khác gì so với trong Laravel. Tuy nhiên có một số vấn đề mà ta cần phải giải quyết để có thể hoàn toàn thoải mái khi sử dụng các component của Vue.

I18n

Khi ứng dụng có hỗ trợ nhiều ngôn ngữ khác nhau thì sẽ thật tốt nếu như chúng ta cũng có thể áp dụng điều này trong các vue component. Rất may mắn là đã có i18n-js. Để cài đặt, đơn giản bạn chỉ cần thêm vào trong Gemfile.

gem "i18n-js"

Sau đó chạy bundle install để hoàn tất. Tiếp theo là thêm vào cuối file app/assets/javascripts/application.js:

//= require i18n/translations

Chạy lệnh rails i18n:js:export để export toàn bộ config I18n trong các file yml ra một file js. Cuối cùng là đưa chúng ra ngoài view để sử dụng. Trong file app/views/layouts/application.html.erb chúng ta thêm vào bên trong thẻ head:

<%= javascript_include_tag "i18n" %>

Để sử dụng chúng ta chỉ cần gọi:

I18n.t('posts.index.title') // => Post Detail

Tương tự như khi ta sử dụng gem rails-i18n chỉ khác là i18n-js không thể lazy loading giống như rails-i18n được.

Routes

Rails cung cấp các url helper và path helper rất thuận tiện. Nếu như bạn cũng muốn sử dụng các helper này ở trong các vue component thì js-routes sẽ là một lựa chọn tốt dành cho bạn. Đơn giản ta chỉ cần thêm dòng sau vào Gemfile:

gem "js-routes"

Chạy bundle install để cài đặt. Bước cuối cùng là require nó vào trong file app/assets/javascripts/application.js:

//= require js-routes

Cách sử dụng cũng rất đơn giản, bạn chỉ cần gọi:

Route.post_path(1, {comment_id: 2}) // => /posts/1?comment_id=2

Tích hợp vào trong Vue

Để thuận tiện hơn trong việc sử dụng hai thư viện trên trong các vue component, chúng ta sẽ đưa những hàm cần dùng vào trong một file helper sau đó sử dụng Vue.mixin để đưa chúng vào trong tất cả các component khác. Tạo ra một file app/javascript/mixins/helpers.js có nội dung là:

export default {
  methods: {
    t(key, options) {
      return window.I18n.t(key, options);
    },
    route(name, params = null) => {
      if (params && typeof(params) == 'object' && (params.slug || params.id)) {
        params = {...params, id: (params.slug || params.id)};
      }
      return params ? window.Routes[name](params) : window.Routes[name]();
    }
  }
};

Trong file app/javascript/packs/application.js chúng ta thêm đoạn code dưới đây vào ngay phía trên phần khởi tạo Vue:

import helpers from '../mixins/helpers';
Vue.mixin(helpers);

Và bây giờ ở trong một component bất kì chúng ta có thể sử dụng:

<template>
 <div>
   <h1>{{ t('page.title') }}</h1>
   <a :href="route('post_path', post)">{{ post.name }}</a>
 </div>
 </div>
</template>

<script>
export default {
 props: {
   post: {
     default: Object
   }
 }
}
</script>

Vue tag helper

Khi sử dụng các vue component trong các file view của Rails, chắc chắn sẽ có lúc bạn cần truyền dữ liệu prop cho các component đó. Bình thường chúng ta có thể làm như sau:

<div class="page-content">
  <post-detail :post="<%= @post.to_json %>" :is-voted="<%= current_user.voted? @post %>" />
</div>

Thực sự là nhìn nó không được mượt và chuyên nghiệp cho lắm, đấy là chưa kể đến những trường hợp cần phải truyền vào nhiều dữ liệu hơn thay vì như ví dụ trên. Để giải quyết vấn đề này, trong ApplicationHelper chúng ta thêm vào đoạn code sau:

module ApplicationHelper
  def vue_tag name, **props, &block
    content_tag normalize_vue_key(name), normalize_vue_props(props) do
      capture(&block) if block_given?
    end
  end

  private
  def normalize_vue_key key
    key.to_s.downcase.gsub "_", "-"
  end

  def normalize_vue_props props
    props.transform_keys do |key|
      ":#{normalize_vue_key key}"
    end.transform_values do |value|
      !!value == value ? value : value.to_json
    end
  end
end

Trở lại với ví dụ bên trên, bây giờ chúng ta có thể viết lại thành:

<div class="page-content">
   <%= vue_tag :post_detail, post: @post, is_voted: current_user.voted?(@post) do %>
       <div>Any content here</div>    
   <% end %>
</div>

Summary

Vừa rồi chúng ta đã cùng nhau hoàn thành việc kết hợp Vue vào trong Ruby on Rails. Hi vọng bài viết sẽ hữu ích cho những bạn cùng yêu cả hai framework này và mong muốn được làm việc với chúng trong cùng một project một cách hiệu quả.

Blog: https://www.dnlblog.com/posts/tich-hop-vue-vao-trong-rails

All Rights Reserved