在 Rails 中開始 Vue

在三大主流前端框架中,最年輕的 Vue 無疑是個爆發力最強的一個,尤其是容易上手且中文資源多,對於很多人來說 Vue 是踏入前端框架的首選。Rails 自 3.1 版使用 jQuery 並且搭配 Asset pipeline 來打包前端資源已經好一段時間,在這個前端爆炸的年代這個組合已經略顯不足,直到 Rails 5.1 開始引入了 webpack 並且可搭配 yarn 做前端套件管理後,大大的提升 Rails 對現代前端框架支援的友善度! 本篇將會以一個搞不懂前端生態(誤) 的 Rails 開發者視角來探討:

  • 手把手的寫一個簡單的 Rails 搭配 Vue 專案
  • 如何在 Rails 中應用 Vue component
  • 怎麼在 Rails 中搭配 Turbolink
  • 其他有的沒的 (本篇使用的是 Rails 5.2.2)

專案初始

以下範例程式,我們將要做一個簡單的通訊錄系統,並且嘗試用 Vue 來顯示結果。 開始之前請先確定:

  • 你的電腦上已經安裝好 node.js
  • 你的電腦上已經安裝好 yarn. Mac user 可以直接使用 homebrew ( brew install yarn ) 安裝

Rails 利用 yarn 來管理前端套件,你可以把它想像成是一個比較新潮的 npm。

接下來要新增一個「預先裝好 Webpack 並搭載 Vue 而且不裝 Turbolink 的 Rails 專案」,我們命名這個專案為 「vue_example」

$ rails new vue_example --skip-turbolinks --webpack=vue

Turbolinks 是 Rails 用來加入頁面載入的套件,其實就是透過快取來替換部分的頁面內容,但是如果搭配使用前端框架渲染頁面時就有可能產生錯誤,我們之後再說明如何結合 Turbolink 使用 Vue,目前將 Turbolink 關閉。

雖然使用 rails s 啟動專案後就可編譯 webpack 的程式碼,但是可以使用 webpack-dev-server 來做即時的自動編譯(存檔後瀏覽器就會重新 hot reload)。Webpack 已經內建了 webpack-dev-server,在 console 中輸入 bin/webpack-dev-server 就可以啟動了。但每次都要開兩個 server 也說不上很方便,所以可以順道安裝 foreman 來幫我們同時把需要的服務都開起來。

Gemfile 下新增 foreman

group :development do
  # other gem ...
  gem 'foreman'
end

存檔後記得 bundle install

接這在專案根目錄下新增一個 Procfile 檔案,把我們要一起開的服務都輸入進去

backend: bin/rails s -p 3000
frontend: bin/webpack-dev-server

存檔後,往後就可以在 console 中輸入 foreman start 來同時啟動所有需要的服務啦!

接著使用 scaffold 來快速產生我們要的範例程式

$ rails g scaffold AddressBook name:string email:string phone:string description:text
$ rails db:migrate

然後請自己在 rails console 下,自己新增一些假資料。

開始撰寫 Vue

我們來看一下安裝 webpacker 和 vue 之後, 在 app/ 資料夾多下了個 javascript 資料夾,而底下的 path/ 資料夾就是放置 webpack 的 entry point。講白話就是最後 webpack 的模組都會匯集到 entry point 裡,最後會利用神奇的魔法將模組編譯在一起,透過這個 entry point 來整合所有複雜的前端元素到我們的 html 網頁中。

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

vue 幫我們自動產生了一個 entry point - hello_vue.js 和模組 app.vue ( 有興趣可以看一下裡面在寫什麼 )

我們這裡不使用 hello_vue.js,手動建立一個自己的 entry point - main.js

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

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: '#app',
    components: { NameCard }
  })
})

[app/javascript/packs/main.js]

然後新增一個叫做 name_card.vue 的元件檔

<template>
  <div class="name-card">
    {{message}}
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      message: "this is a name card by Vue"
    }
  }
}
</script>

<style scoped>
.name-card {
  border: 1px solid rgba(0, 0, 0, 0.205);
  display: inline-block;
  padding: 10px;
  margin: 5px;
  background-color: #fff;
  border-radius: .25rem
}
</style>

接著我們要把 entry point 放到網頁裡面,最適合的地方就是在 Rails 預設的網頁 layout template: app/views/layouts/application.html.erb ,在它的<head> 標籤中加入 javascript_pack_tagstylesheet_pack_tag 來載入 entry point

<head>
  <!--...-->
  <%= stylesheet_pack_tag 'hello_vue' %>
  <%= javascript_pack_tag 'hello_vue' %> 
</head>

[app/views/layouts/application.html.erb]

index.html.erb 頁面上,加入一個 <div id="app"> 的標籤作為 Vue 的掛載點,並且把 NameCard component放在裡面

<!-- other code ...-->
<hr>
<div id="app">
  <name-card></name-card>
</div>

[app/views/address_books/index.html.erb]

存檔後啟動專案,打開瀏覽器看一下結果

name_card

其中 stylesheet_pack_tag 是用來載入 webpack 編譯完的 css style。可以試著把它拿掉,你會發現在 app.vue 內定義的 <scope style> 就失去了效果。

更好的 Vue component 掛載方式

如果覺得要使用 Vue component 都要先包一層掛載點像是 <div id="app"> 覺得很麻煩(到底是多懶?),可以考慮以下的小技巧: 在 application layout 中,用一個標籤把整個 yield 包起來

<html>
  <!--...-->
  <body>
    <div class="container" data-behavior="app">
      <%= yield %>
    </div>
  </body>
</html>

[app/views/layouts/application.html.erb]

然後修改掛載點,綁定 [data-behavior] 屬性

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

[app/javascript/packs/main.js]

這樣整個 view 都是我的 Vue 掛載點啦,是不是很方便! 這樣一來在 index.html.erb 中我們可以把 <div id="app"> 拿掉,直接把 component <name-card> 放上去就好

使用 props 來收 Rails 的資料

接著要來繼續改善我們的程式,透過 Vue 的 props 屬性,可以接收由 component 外傳傳遞進來的資訊。 修改 name_card.vue 元件

<template>
  <div class="name-card">
    <ul>
      <li>name: {{ item.name }}</li>
      <li>email: {{ item.email }}</li>
      <li>phone: {{ item.phone }}</li>
    </ul>
  </div>
</template>
<script>
export default {
  props: ["item"],
}
</script>

<!-- ... other code -->

[app/javascript/name_card.vue]

在 index.html.erb 中我們有一個 @address_books 是透過 Rails 取得的所有通訊錄資料,將資料個別取出後,轉成 json 格式就可以將每一筆的 address_book 餵給 Vue 的 NameCard 元件

<%@address_books.each do |address_book|%>
  <name-card :item="<%=address_book.to_json %>"></name-card>
<%end%>

[app/views/address_books/index.html.erb]

prop

關於 Vue 在 Rails 的自問自答

Turbolinks 一定要關掉嗎?

其實已經有相關的 Vue 套件 “vue-turbolinks” 可以讓 Vue 與 Trubolink 共存 使用 yarn 安裝 vue-turbolinks

$ yarn add vue-turbolinks

然後你的 entry point ( main.js ) 必須改成這樣

import TurbolinksAdapter from 'vue-turbolinks'
import Vue from 'vue/dist/vue.esm'
import App from '../app.vue'

Vue.use(TurbolinksAdapter)

document.addEventListener('turbolinks:load', () => {
  const app = new Vue({
    // ...
  })
})

主要是因為 Turbolinks 會暫存到 Vue 渲染過的頁面,造成快取到錯誤的頁面結果,而這個套件就是在解決這件事情。

沒有了 jQuery, 有什麼可以替代 $.ajax 呢?

webpack 好麻煩,不能繼續用 asset pipline 嗎?

如果單純的在 <script> 中來撰寫 Vue,是不需要經過 Webpack 編譯的,可以自行將Vue.js library 引入到 application.js 裡面,或者直接安裝vuejs-rails gem。

如果我想要在 Vue 元件中用 i18n 或其他 Rails 的 helper 怎麼辦?

Vue 提供了 inline template 的方式,可以直接在我們的 Rails view file 中撰寫 vue 的模板,而不是去使用原本 .vue 中的 <template> 模板

<%# 我是 .erb  %>
<name-card inline-template>
  <%# 你可以在這邊使用 Rials helper %>
</name-card>

小結

本篇著重在如何在維持 Rails MVC 的架構下,輕量的導入 Vue.js 前端框架,對於 Rails 開發者來說是相對容易與現行架構整合的一個方式。如果對於 Webpack 想要更深入探索,推薦各位可以參考五倍紅寶石工程師 Roy 寫的 新手向 Webpack 完全攻略 系列文章,而看了本篇覺得 Vue 很有趣,歡迎來參加五倍紅寶石開設的 Vue.js 與 Vuex 前端開發實戰課程喔!