Nic Lin's Blog

喜歡在地上滾的工程師

在 Rails 內輕量使用 Vue Component 的最佳實踐

在維護一陣子的 Rails 專案上,難免會看到各處擺放的 JavaScript function 或是 jQuery 各種直接操作 DOM 之類的作法。

在這個前端技術大時代,為了因應更複雜的 UI 需求,有更多易於擴充、提升效能的作法能夠選擇,例如 React / Vue 之類的前端框架。

要解決什麼問題

  • 避免直接操作 DOM
  • 更易維護及套件管理
  • UI 易於管理 LifeCycle

在 Rails 5.1 之後,已經對 webpakcer 有相當好的支持性,在後面的 Rails 6 版本也已經變為預設的前端工具了

那麼在 Rails 專案中陳年已久的 jQuery + Asset pipeline,要怎麼樣才能夠跟這些新技術接軌呢

在有限的資源下要整個重寫,或許共存也是一種選擇。

在這篇文章中,我們的目標是將前端框架,以輕量導入的方式和 Rails 共存

這裡使用 Vue 做為示範,你也可以引入其他框架例如 React 來整合前端技術

怎麼不直接做前後端分離

前後端分離要考慮的事情很多,除非產品有強烈的需求,或是在資源狀況足夠的情況下是可以規劃的,不然很容易變成「前後端分裂」

對於 Rails 來說,你的前後端分離可能會遇到這些事情

  1. 原本好用的 session 要另外處理
  2. Server side rendering(SEO?)

以現有專案開始

加入 gem "webpacker"

直接執行指令來安裝 vue 的相依 JS 套件

rails webpacker:install:vue

這時候你會看到新的目錄

/app/javascript
├── app.vue
└── packs
    ├── application.js
    └── hello_vue.js

packs 下面放的檔案可以當作一個「入口點」,也就是和 asset pipeline 中的 Manifest 一樣。

差別在於你在 layout 中引入時語法有些不同

# Webpacker
<%= javascript_pack_tag 'application' %>

# Asset pipeline
<%= javascript_include_tag 'application' %>

我們可以在 app/javascript/packs/application.js 中修改

import Vue from 'vue/dist/vue.esm'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: '[data-behavior="vue-app"]'
  })
})

然後在 app/views/layouts/application.html.erb 中加入 <%= javascript_pack_tag 'application' %>

並且新增 data-behavior="app" 將 Vue 掛載在 DOM element 上(官方不建議掛在 body 上,別這麼懶 XD)

<body>
  <div class="container" data-behavior="vue-app">
    <%= yield %>
  </div>
</body>

打開有用到該 layout 的頁面,應該可以看到瀏覽器 console 顯示 You are running Vue in development mode.

那就可以開始來用 Vue 開發我們的 JS 元件了

Vue component

新增 app/javascript/components/Message.vue

<template>
  <div class="message">
    {{message}}
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      message: "Vue card"
    }
  }
}
</script>

並修改 app/javascript/packs/application.js

import Vue from 'vue/dist/vue.esm'
+ import Message from "../components/Message"

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
+    el: '[data-behavior="vue-app"]',
+    components: { Message }
  })
})

這時候你可以在 .erb 裡面插入 Vue 的 component,例如在 app/views/home/index.html.erb

就可以這樣寫

<message></message>

所以如果元件是 MessageCard,在 .erb 裡面就寫 message-card

也因為這樣,你在 app/javascript/ 下的 vue component 是可以和 rails 共享 CSS 的

所以你可以在 asset pipeline 寫 SCSS,在 webpakcer 裡面寫 JS 是可以共存的

直接從 Rails 給 props

有時候一些 JS 元件在初次渲染時,會沒有預設資料,這時候因為是這種 hybird 的寫法,所以你可以透過 controller 先拿好資料,在 view 的部分直接將 props 給 vue component

Ruby 部分
# Message controller
@message = Message.find(params[:id])

# Message View
<message :message="<%= @message.to_json %>"></message>

因為你拿出來的 @message 是 ruby object,這邊可以直接轉 JSON 餵給 component,要更方便的作法就是直接在 controller 內先 serialize 也是不錯的方向。

JS 部分
<template>
  <div class="message">
    <ul>
      <li>ID: {{ message.id }}</li>
      <li>email: {{ message.user.email }}</li>
      <li>content: {{ message.content }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  props: ["message"],
}
</script>

基本上到這裡,你可以在 Rails 的基礎上,使用前端框架來開發更多的 component,以現在這種 MVVM 雙向綁定加上不用直接操作 DOM 做渲染的方式,在未來協作或管理更複雜的資料流及 UI 都是有一定程度的幫助。

正常來說能寫 ES6 就不會想寫原本的 JS,能有 MVVM 就不想回去自己到處綁 element 了

Hybird 做法的優缺點

優點

這樣一來,可以慢慢抽換原本舊有的作法,不用再擔心因為到處寫 jQuery 來做 UI flow 而導致難以維護的問題

  1. 可以直接餵 props 而不用 API 請求
  2. 可以用 Rails cache
  3. 可以用 Rails session

缺點

1. I18n 處理

大致上兩種作法

  1. 和 JS 放在一起(比較推薦的作法)
  2. 全部寫在 Rails 裡面,然後倒出來用轉 JSON 餵過去,優點是只要維護一套,缺點是如果語系檔很大會非常慢,後期要單獨將前端拆出去時還要擔心語系耦合問題
2. 前後端混在一起寫

你沒辦法直接招前端工程師變成及戰力,除非他也會 Rails,不然就還要花時間帶了 XD

小結

用這種方式將 UI 邏輯丟給更適合解決問題的框架來做更方便,不管是效能或是撰寫方式都更好,不然常常看到 Rails 裡面各種綁 element 然後到處操作 DOM 真的很頭痛。

comments powered by Disqus