COSCUP 2022/7/31
Rails 老司機帶⾶
ihower@aihao.tw
About me
• 張⽂鈿 a.k.a. ihower

• https://ihower.tw

• Rails 實戰聖經 作者

• Ruby developer since 2006

• 從 Rails 1.1 ⽤到 Rails 7 的老司機

• 愛好資訊科技 

• https://aihao.tw
關於這場 talk
• 這是⼀場進階的 Rails 演講

• 適合有 Rails 專案開發經驗的朋友聆聽

• 充滿寫 code 的主觀意⾒(偏⾒?),非 Best Practices 教學

• 我的專案開發背景

• 約5⼈左右的 Rails Developer ⼯程師開發團隊 (無前後端分離)

• ⽬前⼿上的專案時長都在五年以上 (從 Rails 3 ⼀路升級上來)
Agenda
• 關於 OOP (12min)

• 關於前端⽅案 (9min)

• 關於效能: N+1 queries (6min)

• 關於 Gem
fi
le 版號 (3min)
關於 OOP 物件導向
Application Developer 的必備知識
Case Study: 留⾔系統
(重構前)
• 可以針對不同主題的留⾔

• 使⽤了 Polymorphic association 設計

• Comment 

• commentable_type: string

• commentable_id: integer

• 還有個 thread 分類: [:unclassi
fi
ed, :ask, :share, :ask_manager]
class Comment < ApplicationRecord
def notify_users
case commentable_type
when 'Unit'
# ...
when 'User'
# ...
when 'Question'
# ...
when 'Comment'
# ...
when 'Answer'
# ....
else
# ....
end
end
end
def render_comment_subject
if commentable_type == 'Unit' && thread == 'ask'
#...
elsif comment.commentable_type == 'Unit' && comment.thread == 'share'
#...
elsif comment.commentable_type == 'Unit' && comment.thread == 'ask_manager'
#...
elsif comment.commentable_type == 'User'
#...
elsif comment.commentable_type == 'Question' && comment.thread == 'ask'
#...
elsif comment.commentable_type == 'Question' && comment.thread == 'share'
#...
elsif comment.commentable_type == 'Question' && comment.thread == 'ask_manager'
#...
elsif comment.commentable_type == 'Answer' && comment.thread == 'ask'
#...
elsif comment.commentable_type == 'Answer' && comment.thread == 'share'
#...
elsif comment.commentable_type == 'Answer' && comment.thread == 'shaask_managerre'
#...
else
#...
end
end
基本 OO 解法⽅向:
Replace Conditional with Polymorphism
⽤多型取代條件式
常⾒解法:
Replace Type Code with Subclasses
• Rails 內建就⽀持 Single Table Inheritance (STI) 

• 適合 type 不會變更的場景
class UnitShareomment < Comment
def notify_users
#...
end
def render_comment_subject
#...
end
end
class UnitAskComment < Comment
def notify_users
#...
end
def render_comment_subject
#...
end
end
稍微進階解法 :
Strategy Pattern
當 STI 不適⽤時,

這也是 Composition over inheritance 中最常⾒的技巧
什麼是 Strategy Pattern ?
• 有⼀個 method 負責挑選要⽤哪⼀個策略,然後 new Strategy 物件

• 可以是根據某⼀個 type 屬性

• 也可以是某種判斷

• 不同策略就寫在不同 Strategy class 裡⾯

• 這些 strategy class 有相同的呼叫介⾯
Case Study: 作業指派給不同助教批改
class AssignmentsController < ApplicationController
def create
@answer = Answer.create!(answer_params)
AllocationManager.new(answer).assign_to_reviewer!
redirect_to assignment_path(@answer.assignment)
end
end
class AllocationManager
def initialize(answer)
@answer = answer
self.set_strategy
end
def set_strategy
@strategy = if answer.scored?
NoNeedAllocation.new(answer_list)
elsif answer.assignment.allocation_rules.empty?
TaTagAllocation.new(program, answer_list)
else
RandomAllocation.new(program, answer_list)
end
end
def assign_to_reviewer!
@strategy.assign_to_reviewer!
end
end
class NullAllocation
def assign_to_reviewer!
#...
end
end
class TaTagAllocation
def assign_to_reviewer!
#...
end
end
class RandomAllocation
def assign_to_reviewer!
#...
end
end
OO基本原則 Open–closed principle 

對擴充開放、對修改封閉
基本 OO 解法⽅向:
Replace Conditional with Polymorphism
但要⼩⼼OO嘴砲原則
有些是吵架⽤,不是有⽤的指導原則
• 當我覺得你的解法做太多事情時,搬出 YAGNI

• 當我覺得你的解法太複雜時,搬出 KISS

• 當覺得⼀個類別太肥⼤的時候,就搬出 SRP (Single Responsibility Principle)

• ⼀個 class 應有且只有⼀個理由會使其改變

• 理由?? 多⼩的理由? 

• 最後整個結構不就變成 ⼀個 class 就⼀個 public function ?
7 Design Patterns to Refactor
MVC Components in Rails
介紹 Rails 中常⽤的 Design Patterns 的知名⽂章

https://www.sitepoint.com/7-design-patterns-to-refactor-mvc-
components-in-rails/
介紹了七種 Rails 常⽤(?)的設計模式
• 1. Service Objects

• 2. Value Objects

• 3. Form Objects

• 4. Query Objects

• 5. View Objects(Serializer/Presenter)

• 6. Policy Objects

• 7. Decorators
照做肯定 over-engineering…
• 我不推薦 

• Service Objects

• Form Objects

• View Objects(Serializer/Presenter)

• 適合場景很少

• Value Objects

• Query Objects

• 可⽤

• Policy Objects

• Decorators
Service Object
最多⼈推薦使⽤的,但我認為過譽了
什麼是 Service Object
• 這裡指的 service object,是指實作 command pattern

• 有個 class 帶有唯⼀的 public method,⽅法裡⾯就是⼀連串的指令來達成⼀
件事情

• 還有⼈出個 gem 定義了這個結構

https://github.com/nebulab/simple_command
# 出⾃ https://railsbook.tw/chapters/26-organize-your-code-advanced
class BooksController < ApplicationController
# ...[略]...
def borrow
if @book && @book.is_available?
@book.borrow!
# Slack 通知
notifier = Slack::Notifier.new ENV["WEBHOOK_URL"] do
defaults channel: ENV["NotifyChannel"], username: ENV["SlackUser"]
end
notifier.ping "#{@book.title} 已出借!"
redirect_to books_path, notice: "已完成預約(出借)!"
else
redirect_to books_path, notice: "此書⽬前無法出借!"
end
end
# ...[略]...
end
# 出⾃ https://railsbook.tw/chapters/26-organize-your-code-advanced
def borrow
if @book && @book.is_available?
@book.borrow!
# Slack 通知
service = SlackNotifyService.new("#{@book.title} 已出借!")
service.perform
redirect_to books_path, notice: "已完成預約(出借)!"
else
redirect_to books_path, notice: "此書⽬前無法出借!"
end
end
class SlackNotifyService
def initialize(message = "")
@message = message
end
def perform
SlackAPI.notify( ::Notifier.new ENV["WEBHOOK_URL"] do
defaults channel: ENV["NotifyChannel"], username: ENV["SlackUser"]
end
notifier.ping @message unless @message.empty?
end
end
Q: 假設接下來要還書也要通知?
逾期也要通知?
於是就變成 function list,只是外表是 class
這樣符合SRP喔?! 每個 class 都只有單⼀責任
• app/services/

• create_account_service.rb

• create_xxx_notify.rb

• create_yyy_notify.rb

• create_zzz_notify.rb

• update_xxx

• update_xxx

• update_xxx

• update_xxx

• update_xx

• ….

• …
我認為 Service Object 是 bad smell
• Service objects 看起來像是 OOP,因為⽤了 class 來包裹

• 但是這只是非常表⾯的, 只是把 code 搬到另⼀個地⽅,並不是好的 OO

• 真正概念上有⽤的 object 是包含內在屬性和⽅法的。如果⼀個 object 只是
procedural code 的包裹,那麼實際上並沒有 OOP 帶來的好處

• 應該⽤ OO 概念去設計 PORO (Plain Old Ruby Object) model 即可

• 把相同概念的屬性和⽅法⼀起包裹 ,⽤好的⽅法命名

• ⽽不是硬套 service object pattern,每個⽅法命名都叫做 perform ⽅法
# 我的設計
def borrow
if @book && @book.is_available?
@book.borrow!
# Slack 通知
notifier = BookNotifier.new(@book)
notifier.notify_borrow!
redirect_to books_path, notice: "已完成預約(出借)!"
else
redirect_to books_path, notice: "此書⽬前無法出借!"
end
end
class BookNotifier
def initialize(book)
@book = book
end
def notify_borrow!
#...
end
def notify_return!
#...
end
def notify_delay!
#...
end
private
def content_title
#.....
end
https://www.codewithjason.com/code-without-service-objects/
Value Objects
概念有⽤,但不常需要實作
• 當⼀個物件沒有概念上的標識 (conceptual identity),⽽只在乎它的屬性時,這
個物件就可以是個 Value Object。反之則是 Entity。

• 物件A == 物件B,要怎麼判斷兩個物件相等

• 度量衡 類型的物件很適合⽤這個概念來設計

• 例如內建的 Date, Time 都是⼀種 Value Objects

• ⼀個 Rails 沒有內建,但比較常⾒的場景: Money 

• https://github.com/Shopify/money
Form Objects
可不⽤費⼼
• 若不是 ActiveRecord 物件,⽤原始的 form_tag 去組合表單就好了

• 場景不多,不太值得/不太需要特別包裹成 Object

• 網路上的範例都太簡單,都是 “聯絡我們” 表單

• 若是多 model 的 association 的場景,想要⽤⼀個 Form Object 包裹處理

• 沒有什麼好的標準範例寫法,每個⼈都寫不⼀樣,維護起來很痛苦
View Objects
也是不推
• Serializer

• API 的話,Serializer 我不太⽤,我更喜歡 Jbuilder template 的表現⼒

• Serializer 常會混雜了query code,增加 N+1 除錯難度

• Presenter

• 缺乏好的範例。⽽且我認為 View 中就⽤ @instance 跟 Helper,不需要費⼼
整理成⼀個 Presenter 物件。代碼複⽤的機會很低,獲得的回報太少。

• 可以看看概念類似的 Rails ViewComponent 比較多⼈⽤
Query Objects
少數複雜場景才需要
• ActiveRecord relations 本⾝就是⼀種 Query object 了

• 所以你做的事情其實是 Query object of Query object,真的有需要嗎?

• 只有複雜的 Query 場景需要,但是既然複雜,就表⽰可能不太會被複⽤,因此
是否值得從 controller 中剝離出來?
Policy Objects
• 就是⽤ Pundit gem 權限設計

• https://github.com/varvet/pundit

• 不過 Pundit 給的範例,會讓你以為要圍繞在 model 設計,這是不⼀定的

• 簡單的後台權限,只需要⽤ Headless policies 即可
Decorators
• 包裹 model,提供本來 helper 在做的⼯作

• ⼜叫作 View Models,可⽤ ruby 內建的 SimpleDelegator 或是 draper gem 即可

• 不影響整體架構,就是把 model 裡⾯的⽅法拆出來放另⼀個檔案⽽已。這些不會
是太關鍵的⽅法,只是為了配合 view 畫⾯⽽已
OOP 推薦資源
• Ruby物件導向設計實踐

• Practical Object-Oriented Design in Ruby: An Agile Primer

• https://www.tenlong.com.tw/products/9780321721334

• Ruby Science

• https://github.com/thoughtbot/ruby-science

• 這PDF 就包括 重構 和 設計模式

• 初學者⽤ TDD 可以幫助學習
Rails 前端⽅案
Rails 變來變去的前端架構...
• Rails 1.0: 預設⽤ Prototype.js (http://prototypejs.org/)

• Rails 1.1: RJS template 

• Rails 3: Assets pipeline (Sprockets) 和導入 Co
ff
eescript

• Rails 4: 預設換 jQuery 和 jquery-ujs (ujs 是指 Rails 的 unobtrusive scripting adapter)

• Rails 5: Turbolinks、⽀援⽤ Yarn 和 Webpack

• Rails 6: 預設換 Webpacker, Turbolinks、移除 jQuery 換 rails-ujs、但 Assets pipeline 沒拆還在

• Rails 7: Import Map, Turbo, Stimulus。預設移除了 rails-ujs

• 把 webpacker 拆掉了,建議⽤ jsbundling-rails gem(可搭配 esbuild) 和 cssbundling-rails

• Assets pipeline 還在,但 DHH 有新出 Propshaft gem (因為有⼈離職了)
animation framework, drag and
drop, Ajax controls DOM utilities,
and unit testing.
考古 Rails 1.1 使⽤的JS框架
DHH 在想什麼?
Rails 發明⼈和 Rails 前端⽅案的決策者
• 堅持想⽤ server-side rendering 中⼼思想

• 因爲夥伴 Sam Stephenson 負責前端,所以⼀開始⽤ Prototype.js,並⽤ RJS 達成⽤ Ruby 寫 JavaScript ⽅案

• 因為 Prototype.js 不⾏了, 被迫接受 jQuery。RJS 也跟著 gg

• 曾經喜歡 Co
ff
eeScript,現在直接寫 ES6 了

• 很喜歡 Turbolinks ⽅案⾄今,⼤升級兩次: Classic Turbolink -> Turbolink 5 -> Turbo 

• 因為 Assets Pipeline(Sprockets) 功能對 ES6 和 js 模組化⽀援不⾜

• 被迫接受 Webpack ⽅案,但⼜捨不得把 Assets Pipeline 拆了。於是兩者並存... 前者處理js,後者處理css/image

• Rails 7 ⼜把 Webpack 拆了

• 因爲跟夥伴 Sam Stephenson 拆夥了,正在把 Sprockets 換掉了

• Sam Stephenson 除了離開 basecamp,同時放棄所有跟 Rails 相關的 js 套件開發⼯作,包括 Hotwire, Stimulus 等
考古: RJS template
Rails 1.1 的 Ruby JavaScript
#/app/views/rjs1/index.rhtml
<%= form_remote_tag :url => { :action =>’note’ }, :html => {:id=>’note-form’ } %>
<%= text_field_tag ‘note’,nil,:size => 40 %>
<%= submit_tag ‘submit’ %>
<%= end_form_tag %>
#/app/views/rjs1/note.rjs
page.insert_html :bottom, ‘notes’, :partial =>’note’
page.visual_effect :highlight, ‘note’
page.form.reset ‘note-form’
 考古: RJS ⽀援七種 DOM 操作
這是 rjs template code,是不是跟15年後的 Turbo Steams 很像?

都說不⽤寫 javascript
page.insert_html :bottom, 'dom_id', :partial => 'content'
page.insert_html :top, 'dom_id', :partial => 'content'
page.insert_html :before, 'dom_id', :partial => 'content'
page.insert_html :after, 'dom_id', :partial => 'content'

page.replace 'dom_id', :partial => 'content'
page.replace_html 'dom_id', :partial => 'content'
page.remove 'dom_id', :partial => 'content'
rails-ujs (或是 jquery-ujs) ⽅案
• RJS 隨著 Prototype.js gg 之後,Rails 放棄⽤ Ruby 產⽣ JavaScript 的⽅式

• 改成直接寫 js.erb,就直接寫 javascript 

• 但本質上是⼀樣的,由 server 後端傳 javascript 給前端執⾏

• 透過 link_to, button_to 或是 form_for 加上 remote: true,就會發送請求 script
到後端

• 後端回傳 javascript,前端執⾏
我的看法 (1)
• 我不喜歡 Turbolinks (Turbo Drive) ⽅案,因為他改變了瀏覽器預設⾏為

• 90%場景沒問題,但那 10% 有問題場景,對初學者特別困擾

• 和其他 JS library 搭配時,也經常會有相容性問題要處理
我的看法 (2) Turbo
• Turbo Frames 是個漂亮的 ajax 功能語法糖,⽤ HTML 標籤就讓你替換 partial 部分區塊

• 只需要改 HTML erb 即可,後端不⽤改就可實現 Ajax 部分區塊替換

• 跟 https://github.com/renderedtext/render_async 作⽤類似,可以 ajax 替換 partial

• Turbo Streams 

• ⽤ HTML 標籤,定義常⾒的 DOM 操作: 只是重新發明 RJS ⽽已...

• 語法換了,但是精神是⼀樣,HTTP payload 從 script 換成 html,但骨⼦裡仍是 DOM
操作指令

• RJS 有的架構限制,⼀樣存在
https://turbo.hotwired.dev/reference/streams
• 我認為 Server-side render 好⽤開發快效能⼜好,只是⼀條路不能走到⿊

• ⼤部分簡單的場景,我認為 Remote JS ⽅案是很好⽤

• 從 Rails 1.1 時代的 RJS

• rails-ujs 的使⽤相容性問題最⼩ (JS部分只是個簡單的 adapter) 

• 我的 Ajax 教材 https://ihower.tw/rails/fullstack-ajax-1.html

• Turbo Steams

• 這幾種運作本質上是⼀樣的,結構問題和能⼒限制也⼀樣

• 如果達到了 SPA 的場景需求,我會去採⽤ Vue.js 或是 React.js ⽅案
我的看法 (3) Remote JS
結構和能⼒限制 ?
• 結構問題

• 在有密集 Ajax 操作的 UI 組件中,會導致非常多零碎的 JS template….. 每個⼩⼩的 template 只為了調整畫⾯上
的 DOM

• 例如⼀樣是刪除 comment,因為前台 UI 不同,js template 就得不同

• ⽤ Hotwire 也是⼀樣,會有很多零碎的 .turbo_stream 檔案

• 能⼒限制

• 不在 rails-ujs 和 Turbo ⽀援的瀏覽器事件時 (超連結、表單等)

• 在需要 javascript template 的 SPA 場景時

• 還要需要⽤ jQuery ⾃⼰掛載瀏覽器事件送 Ajax,然後後端回傳 JSON 處理

• 或是搭配前端框架,例如 Stimulus.js, Vue.js, React.js 等
同個 comment 資料操作,不同UI準備不同template
好零碎啊....
• app/views/comments/create.js.erb

• app/views/comments/edit.js.erb

• app/views/comments/new.js.erb

• app/views/comments/update.js.erb

• app/views/comments/destroy.js.erb

• app/views/answers/comments/create.js.erb

• app/views/answers/comments/edit.js.erb

• app/views/answers/comments/new.js.erb

• app/views/answers/comments/update.js.erb

• app/views/answers/comments/destroy.js.erb

• app/views/peer_reviews/comments/create.js.erb

• app/views/peer_reviews/comments/edit.js.erb

• app/views/peer_reviews/comments/new.js.erb

• app/views/peer_reviews/comments/update.js.erb

• app/views/peer_reviews/comments/destroy.js.erb

• ……..
我的作法: 若有 SPA 需求可在 erb 上掛載 Vue.js
例如⼀個複雜的 UI widget,有⼀堆 ajax 操作
• Vue.js 可以不需要 build 就可以使⽤,HTML上掛上 CDN 就可⽤,後端框架導入超簡單

• Vue.js 有出⼀個精簡版 petite-vue, gzip 後只有 6.9K,之後有需要也⽅便轉成
Vue.js 單組件

• 缺點是沒有單組件(SFC)功能,這⼀定需要 build 程序(需要 webpack 或是 esbuild 等)

• 進⼀步可⽤ Vue.js 單組件掛載在 erb 上

• 此⽅案兼具 server-side rendering 開發快、速度也快的混和⽅案

• 需要 UI 組件時,再掛載前端組件即可
# erb view
<h2>⽂章標題</h2>
<p>⽂章內容</p>
……
<div id="comments-component">
<comments :comments="<%= @comments_data' %>"/>
</div>
這是erb,可以讓初始資料塞到
props 即可。Vue.js 不需要⽤ Ajax
拿。
# js 掛載 (需要透過 webpack 或是 esbuild 編譯)
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import Comments from './components/comments.vue'
document.addEventListener('DOMContentLoaded', () => {
const comments = document.querySelector('#comments-component')
if(comments){
createApp({
components: {
Comments
}
}).mount(comments);
}
})
# Comment Vue.js 單組件 (template+js+css)(組件裡還可以包組件)
<template>
<div class="compose">
....
<ul v-for="(comment, comment_index) in comments" :key="comment.id">
<Comment :comment="comment" />
</ul>
....
</div>
</template>
<script>
import Comment from './comment.vue'
export default {
name: 'Comments',
components: {
Comment
},
props: ['comments'],
data () { },
methods: { }
}
</script>
<style lang="css" scoped>
...
</style>
參考範例
• Vue.js 2 + Webpacker

• https://medium.freecodecamp.org/how-to-pass-rails-instance-variables-into-vue-
components-7fed2a14babf

• https://5xruby.tw/posts/rails-vuejs-get-started/

• Vue.js 3 + jsbundling-rails (⽤esbuild) 

• 有空我再寫 blog

• Vite Ruby ⽅案 https://github.com/ElMassimo/vite_ruby

• Vite 是 Vue.js ⾃家出的 build ⼯具
Stimulus.js 如何?
petite-vue example
我⽤ Vue.js 改寫⼀樣的範例
<div id="hello">
<input type=“text” v-model=“name">
<button @click="greet">
Greet
</button>
<span>
{{ output }}
</span>
</div>
import { createApp } from ‘petite-vue’
createApp({
name: '',
output: '',
greet(){
this.output = `Hello, ${this.name}`
}
}).mount('#hello')
• 上述 Vue.js code 只有 stimulus 的七成...

• Stimulus 仍是傳統 DOM 操作思維

• 簡化了事件掛載,提供了⼀個 controller 架構

• Vue.js 和 React.js 是現代前端的 MVVM 和 Component 組件思維

• 前端組件有⾃⼰的狀態,DOM 隨著狀態改變

• https://zh-hant.reactjs.org/docs/thinking-in-react.html

• 我認為可以 Progressive 漸進式採⽤的 Vue.js 是比較好的選擇

• 可以先採⽤不需要 build 的 Vue.js ⽅式,這比 Stimulus.js 需要 Import Map (或 build 程序),導
入成本更低

• 之後有需要再加上 build 程序,就可以⽤單組件 Component
https://rubyonrails.org/doctrine/zh_tw
• Rails 提供你整套前端+後端的架構
• 但很多元件都是可以替換的,例如
• 前端架構可換
• 測試框架可換 (很多⼈就換了 RSpec)
我的看法(5): 前後端分離?
完全不⽤ erb 後端 view template
• 我認為 Rails 的 actionview 搭配 activerecord 是做表單的強項

• 前後端完全分離,就會失去 Rails 這個優勢

• 不會整個 app 都適合⽤ SPA 去做

• 混合⽅案才是 C/P 值最⾼

• 除非你有很好的理由: 康威定律、招聘問題(前端比較好招⼈?)
就算要前後端分離,也可以不⽤這麼分離
• cookie 還是很棒,不需要⽤ JWT

• 登入⾴⽤ rails 處理,devise 繼續⽤,就不需要重新發明整個註冊、登入、忘記密碼流程

• 登入後再進入前端 SPA ⾴⾯即可

• 若可以同網域很棒,就不需要處理 CORS 跨域問題

• ⽤ nginx 處理分流即可,例如

• example.com/* ⽤ nginx reverse-proxy 到前端 app

• example.com/api/* ⽤ nginx reverse-proxy 到後端 rails app 

• example.com/admin/* ⽤ nginx reverse-proxy 到後台 rails app
不喜歡不⼀定要跟,以拖待變!!
• 我的老專案就遲遲沒⽤ webpacker 全⾯替換 sprockets 

• 拖到 webpacker 都 gg 了, sprockets 還在

• 跳過了 webpacker 時代,直接⽤ jsbundling-rails gem(可搭配 esbuild) 了

• 未來天秤⼜會回到 MPA?

• https://nolanlawson.com/2022/05/21/the-balance-has-shifted-away-from-
spas/

• MPA ⾸屏渲染更快、Core Web Vitals 分數更⾼、SEO 更好做…..
The balance has shifted away from SPAs
https://nolanlawson.com/2022/05/21/the-balance-has-shifted-away-from-spas/
• Chrome implemented back-forward caching

• 上下⾴瀏覽器本⾝就有快取

• Shared Element Transitions

• 瀏覽器⽀援跨⾴時維持固定的元素
Rails 後端 ViewComponent ⽅案
順道⼀提
我沒採⽤
• https://github.com/github/view_component

• 我覺得不值得,如果真的有需求,不如⽤ Vue.js 或 React 的前端 component ⽅
案

• View Component 會增加改進後端性能的難度,特別是 N+1 queries 

• 後端 View Component 只有處理 erb template,⽽沒有包括 javascript 跟 css

• Javascript 跟 css 仍然是沒有組建的狀態

• view component 沒有處理 css 跟 javascript,不就是做半套嗎
Fix N+1 queries
後端最常⾒的效能問題
有些實際場景單純的 includes ⽅法解不了
# model
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
scope :visible, -> { where( :status => "public") }
end
# controller
@posts = Post.includes(:comments)
# view
<% @posts.each do |post| %>
<% post.comments.visible.each{ ... } ) %>
<% end %>
1. 有 Scope 條件時
這會造成 N+1 嗎?
會
# model
class Post < ApplicationRecord
has_many :comments
has_many :visible_comments, -> { visible }, :class_name => "Comment"
end
# controller
@posts = Post.includes(:visible_comments)
# view
<% @posts.each do |post| %>
<% post.visible_comments.each{ ... } %>
<% end %>
解法
2. 預加載(Preload)資料再select
# model
def Post < ApplicationRecord
has_many :comments
def my_comments
self.comments.where( :user_id => self.user_id )
end
end
# controller
@posts = Post.includes(:comments)
# view
<% @posts.each do |post| %>
<% post.comments.each{ ... } %>
<% post.my_comments.each{ ... } %>
<% end %>
這會造成 N+1 嗎?
會
# model
def Post < ApplicationRecord
has_many :comments
def my_comments
self.comments.select{ |x| x.user_id == self.user_id }
end
end
解法
3. inverse_of 跟 N+1 queries 也有關係
class Post < ApplicationRecord
has_many :comments, :inverse_of => :post
end
class Comment < ApplicationRecord
belongs_to :post, :inverse_of => :comments
end
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
<% Post.includes(:comments).each do |post| %>
<%= post.title %>
<% post.comments.each do |comment| %>
<%= comment.content %>
<%= comment.post.title %>
<% end %>
<% end %> 這會造成 N+1 嗎?


若inverse_of 沒作⽤就會
class Post < ApplicationRecord
has_many :comments, :foreign_key => :post_id
end
# and/or
class Comment < ApplicationRecord
belongs_to :post, :foreign_key => :post_id
end
原因: ⾃動 inverse_of 可能失效
• Rails 不總是會推導出 inverse_of 屬性,例如

• 你⼿動給 , -> { where條件 } 時

• 有 :foreign_key 參數時

• 有 :through 參數時

• 此時就必須⼿動給 inverse_of
class Post < ApplicationRecord
has_many :comments, ->{ where(....) }
end
4. JSON column type 超好⽤
• PG ⽀援、MySQL 5.7 之後也⽀援 

• 不需要 MongoDB 了、舊的 ActiveRecord Serialize ⽅法也別⽤了

• 可減少了 N+1 queries 機會

• 適合場景: 不需要正規化的資料,例如

• 儲存整個 API response data

• 額外的附屬欄位資料

• has_many 關聯資料,但是其中資料卻不需要個別拿出來使⽤的

• EAV 設計,例如客製表單
Rails 實戰聖經: Rails 後端效能
• https://ihower.tw/rails/fullstack-performance-backend.html

• 我寫的效能教材

• 有上述完整的說明
Benito Serna
• https://bhserna.com/avoid-n-plus-one-queries.html

• 這 blog 是我看過研究 N+1 最深入的,例如 

• 5 ways to fetch the latest-N-of-each record on Rails

• 5 ways to
fi
x the latest-comment n+1 problem

• 作者還有出⼀本⼩PDF書,值得⼀看

• https://bhserna.com/avoid-n-plus-1-queries-on-rails.html
最後,關於 Gemfile 版本指定
• Gem
fi
le 裡⾯,盡量不要鎖定版本,那是 Gem
fi
le.lock 的事

• 只有真的真的需要指定版本才能動的版本,才需要在 Gem
fi
le 內指定版本

• 例如某 gem ⼀定要 '~> 3.0‘,不能⽤ 4.0,這種才需要指定版本

• 原因可能是⽬前 rails 不⽀援 4.0,或是 4.0 需要改 code 但還沒處理,或是 4.0 有 bug

• 若⼀定要⽤舊版,不能升級新版的原因要註解寫在 code 上⾯

• 如此⽤ bundle update 升級時,才會是正確的預期結果

• 那些有指定版本的 gem,也就是(⽬前)無法升級的 gem
gem 'rails', '6.1.4.1'
# 若升級 v2 需要補設定 manifest.js
gem 'sprockets', '~> 3.7.2'
gem 'jquery-rails'
gem 'jbuilder'
gem 'kaminari'
gem 'bootstrap4-kaminari-views'
gem "pundit"
# 此專案⽤ webpack v3
gem 'webpacker', '~> 4.x'
# 因為 omniauth-wechat-oauth2 需要 omniauth 1.3.2
gem 'omniauth', '1.3.2'
# 因為 omniauth 1.3.2 被鎖在 1.3.2
gem 'omniauth-facebook', '~> 4.0.0'
# 因為 master branch 才⽀援 wechat_qiye
gem "omniauth-wechat-oauth2", :github => "NeverMin/omniauth-wechat-oauth2"
# 因為前端⽤ Bootstrap v4
gem 'bootstrap', "~> 4.6"
舉例(真正必要才指定版本)
謝謝聆聽
技術選擇⼀定有風險,套件有 upgrade 有 deprecated
選擇前應詳閱code⽂件
https://ihower.tw

Ruby Rails 老司機帶飛

  • 1.
  • 2.
    About me • 張⽂鈿a.k.a. ihower • https://ihower.tw • Rails 實戰聖經 作者 • Ruby developer since 2006 • 從 Rails 1.1 ⽤到 Rails 7 的老司機 • 愛好資訊科技 • https://aihao.tw
  • 3.
    關於這場 talk • 這是⼀場進階的Rails 演講 • 適合有 Rails 專案開發經驗的朋友聆聽 • 充滿寫 code 的主觀意⾒(偏⾒?),非 Best Practices 教學 • 我的專案開發背景 • 約5⼈左右的 Rails Developer ⼯程師開發團隊 (無前後端分離) • ⽬前⼿上的專案時長都在五年以上 (從 Rails 3 ⼀路升級上來)
  • 4.
    Agenda • 關於 OOP(12min) • 關於前端⽅案 (9min) • 關於效能: N+1 queries (6min) • 關於 Gem fi le 版號 (3min)
  • 5.
    關於 OOP 物件導向 ApplicationDeveloper 的必備知識
  • 6.
    Case Study: 留⾔系統 (重構前) •可以針對不同主題的留⾔ • 使⽤了 Polymorphic association 設計 • Comment • commentable_type: string • commentable_id: integer • 還有個 thread 分類: [:unclassi fi ed, :ask, :share, :ask_manager]
  • 7.
    class Comment <ApplicationRecord def notify_users case commentable_type when 'Unit' # ... when 'User' # ... when 'Question' # ... when 'Comment' # ... when 'Answer' # .... else # .... end end end
  • 8.
    def render_comment_subject if commentable_type== 'Unit' && thread == 'ask' #... elsif comment.commentable_type == 'Unit' && comment.thread == 'share' #... elsif comment.commentable_type == 'Unit' && comment.thread == 'ask_manager' #... elsif comment.commentable_type == 'User' #... elsif comment.commentable_type == 'Question' && comment.thread == 'ask' #... elsif comment.commentable_type == 'Question' && comment.thread == 'share' #... elsif comment.commentable_type == 'Question' && comment.thread == 'ask_manager' #... elsif comment.commentable_type == 'Answer' && comment.thread == 'ask' #... elsif comment.commentable_type == 'Answer' && comment.thread == 'share' #... elsif comment.commentable_type == 'Answer' && comment.thread == 'shaask_managerre' #... else #... end end
  • 9.
    基本 OO 解法⽅向: ReplaceConditional with Polymorphism ⽤多型取代條件式
  • 10.
    常⾒解法: Replace Type Codewith Subclasses • Rails 內建就⽀持 Single Table Inheritance (STI) • 適合 type 不會變更的場景
  • 11.
    class UnitShareomment <Comment def notify_users #... end def render_comment_subject #... end end class UnitAskComment < Comment def notify_users #... end def render_comment_subject #... end end
  • 12.
    稍微進階解法 : Strategy Pattern 當STI 不適⽤時,
 這也是 Composition over inheritance 中最常⾒的技巧
  • 13.
    什麼是 Strategy Pattern? • 有⼀個 method 負責挑選要⽤哪⼀個策略,然後 new Strategy 物件 • 可以是根據某⼀個 type 屬性 • 也可以是某種判斷 • 不同策略就寫在不同 Strategy class 裡⾯ • 這些 strategy class 有相同的呼叫介⾯
  • 14.
    Case Study: 作業指派給不同助教批改 classAssignmentsController < ApplicationController def create @answer = Answer.create!(answer_params) AllocationManager.new(answer).assign_to_reviewer! redirect_to assignment_path(@answer.assignment) end end
  • 15.
    class AllocationManager def initialize(answer) @answer= answer self.set_strategy end def set_strategy @strategy = if answer.scored? NoNeedAllocation.new(answer_list) elsif answer.assignment.allocation_rules.empty? TaTagAllocation.new(program, answer_list) else RandomAllocation.new(program, answer_list) end end def assign_to_reviewer! @strategy.assign_to_reviewer! end end
  • 16.
    class NullAllocation def assign_to_reviewer! #... end end classTaTagAllocation def assign_to_reviewer! #... end end class RandomAllocation def assign_to_reviewer! #... end end
  • 17.
    OO基本原則 Open–closed principle
 對擴充開放、對修改封閉 基本 OO 解法⽅向: Replace Conditional with Polymorphism
  • 18.
    但要⼩⼼OO嘴砲原則 有些是吵架⽤,不是有⽤的指導原則 • 當我覺得你的解法做太多事情時,搬出 YAGNI •當我覺得你的解法太複雜時,搬出 KISS • 當覺得⼀個類別太肥⼤的時候,就搬出 SRP (Single Responsibility Principle) • ⼀個 class 應有且只有⼀個理由會使其改變 • 理由?? 多⼩的理由? • 最後整個結構不就變成 ⼀個 class 就⼀個 public function ?
  • 19.
    7 Design Patternsto Refactor MVC Components in Rails 介紹 Rails 中常⽤的 Design Patterns 的知名⽂章
 https://www.sitepoint.com/7-design-patterns-to-refactor-mvc- components-in-rails/
  • 20.
    介紹了七種 Rails 常⽤(?)的設計模式 •1. Service Objects • 2. Value Objects • 3. Form Objects • 4. Query Objects • 5. View Objects(Serializer/Presenter) • 6. Policy Objects • 7. Decorators
  • 21.
    照做肯定 over-engineering… • 我不推薦 • Service Objects • Form Objects • View Objects(Serializer/Presenter) • 適合場景很少 • Value Objects • Query Objects • 可⽤ • Policy Objects • Decorators
  • 22.
  • 23.
    什麼是 Service Object •這裡指的 service object,是指實作 command pattern • 有個 class 帶有唯⼀的 public method,⽅法裡⾯就是⼀連串的指令來達成⼀ 件事情 • 還有⼈出個 gem 定義了這個結構
 https://github.com/nebulab/simple_command
  • 24.
    # 出⾃ https://railsbook.tw/chapters/26-organize-your-code-advanced classBooksController < ApplicationController # ...[略]... def borrow if @book && @book.is_available? @book.borrow! # Slack 通知 notifier = Slack::Notifier.new ENV["WEBHOOK_URL"] do defaults channel: ENV["NotifyChannel"], username: ENV["SlackUser"] end notifier.ping "#{@book.title} 已出借!" redirect_to books_path, notice: "已完成預約(出借)!" else redirect_to books_path, notice: "此書⽬前無法出借!" end end # ...[略]... end
  • 25.
    # 出⾃ https://railsbook.tw/chapters/26-organize-your-code-advanced defborrow if @book && @book.is_available? @book.borrow! # Slack 通知 service = SlackNotifyService.new("#{@book.title} 已出借!") service.perform redirect_to books_path, notice: "已完成預約(出借)!" else redirect_to books_path, notice: "此書⽬前無法出借!" end end
  • 26.
    class SlackNotifyService def initialize(message= "") @message = message end def perform SlackAPI.notify( ::Notifier.new ENV["WEBHOOK_URL"] do defaults channel: ENV["NotifyChannel"], username: ENV["SlackUser"] end notifier.ping @message unless @message.empty? end end
  • 27.
  • 28.
    於是就變成 function list,只是外表是class 這樣符合SRP喔?! 每個 class 都只有單⼀責任 • app/services/ • create_account_service.rb • create_xxx_notify.rb • create_yyy_notify.rb • create_zzz_notify.rb • update_xxx • update_xxx • update_xxx • update_xxx • update_xx • …. • …
  • 29.
    我認為 Service Object是 bad smell • Service objects 看起來像是 OOP,因為⽤了 class 來包裹 • 但是這只是非常表⾯的, 只是把 code 搬到另⼀個地⽅,並不是好的 OO • 真正概念上有⽤的 object 是包含內在屬性和⽅法的。如果⼀個 object 只是 procedural code 的包裹,那麼實際上並沒有 OOP 帶來的好處 • 應該⽤ OO 概念去設計 PORO (Plain Old Ruby Object) model 即可 • 把相同概念的屬性和⽅法⼀起包裹 ,⽤好的⽅法命名 • ⽽不是硬套 service object pattern,每個⽅法命名都叫做 perform ⽅法
  • 30.
    # 我的設計 def borrow if@book && @book.is_available? @book.borrow! # Slack 通知 notifier = BookNotifier.new(@book) notifier.notify_borrow! redirect_to books_path, notice: "已完成預約(出借)!" else redirect_to books_path, notice: "此書⽬前無法出借!" end end
  • 31.
    class BookNotifier def initialize(book) @book= book end def notify_borrow! #... end def notify_return! #... end def notify_delay! #... end private def content_title #..... end
  • 32.
  • 33.
    Value Objects 概念有⽤,但不常需要實作 • 當⼀個物件沒有概念上的標識(conceptual identity),⽽只在乎它的屬性時,這 個物件就可以是個 Value Object。反之則是 Entity。 • 物件A == 物件B,要怎麼判斷兩個物件相等 • 度量衡 類型的物件很適合⽤這個概念來設計 • 例如內建的 Date, Time 都是⼀種 Value Objects • ⼀個 Rails 沒有內建,但比較常⾒的場景: Money • https://github.com/Shopify/money
  • 34.
    Form Objects 可不⽤費⼼ • 若不是ActiveRecord 物件,⽤原始的 form_tag 去組合表單就好了 • 場景不多,不太值得/不太需要特別包裹成 Object • 網路上的範例都太簡單,都是 “聯絡我們” 表單 • 若是多 model 的 association 的場景,想要⽤⼀個 Form Object 包裹處理 • 沒有什麼好的標準範例寫法,每個⼈都寫不⼀樣,維護起來很痛苦
  • 35.
    View Objects 也是不推 • Serializer •API 的話,Serializer 我不太⽤,我更喜歡 Jbuilder template 的表現⼒ • Serializer 常會混雜了query code,增加 N+1 除錯難度 • Presenter • 缺乏好的範例。⽽且我認為 View 中就⽤ @instance 跟 Helper,不需要費⼼ 整理成⼀個 Presenter 物件。代碼複⽤的機會很低,獲得的回報太少。 • 可以看看概念類似的 Rails ViewComponent 比較多⼈⽤
  • 36.
    Query Objects 少數複雜場景才需要 • ActiveRecordrelations 本⾝就是⼀種 Query object 了 • 所以你做的事情其實是 Query object of Query object,真的有需要嗎? • 只有複雜的 Query 場景需要,但是既然複雜,就表⽰可能不太會被複⽤,因此 是否值得從 controller 中剝離出來?
  • 37.
    Policy Objects • 就是⽤Pundit gem 權限設計 • https://github.com/varvet/pundit • 不過 Pundit 給的範例,會讓你以為要圍繞在 model 設計,這是不⼀定的 • 簡單的後台權限,只需要⽤ Headless policies 即可
  • 38.
    Decorators • 包裹 model,提供本來helper 在做的⼯作 • ⼜叫作 View Models,可⽤ ruby 內建的 SimpleDelegator 或是 draper gem 即可 • 不影響整體架構,就是把 model 裡⾯的⽅法拆出來放另⼀個檔案⽽已。這些不會 是太關鍵的⽅法,只是為了配合 view 畫⾯⽽已
  • 39.
    OOP 推薦資源 • Ruby物件導向設計實踐 •Practical Object-Oriented Design in Ruby: An Agile Primer • https://www.tenlong.com.tw/products/9780321721334 • Ruby Science • https://github.com/thoughtbot/ruby-science • 這PDF 就包括 重構 和 設計模式 • 初學者⽤ TDD 可以幫助學習
  • 40.
  • 41.
    Rails 變來變去的前端架構... • Rails1.0: 預設⽤ Prototype.js (http://prototypejs.org/) • Rails 1.1: RJS template • Rails 3: Assets pipeline (Sprockets) 和導入 Co ff eescript • Rails 4: 預設換 jQuery 和 jquery-ujs (ujs 是指 Rails 的 unobtrusive scripting adapter) • Rails 5: Turbolinks、⽀援⽤ Yarn 和 Webpack • Rails 6: 預設換 Webpacker, Turbolinks、移除 jQuery 換 rails-ujs、但 Assets pipeline 沒拆還在 • Rails 7: Import Map, Turbo, Stimulus。預設移除了 rails-ujs • 把 webpacker 拆掉了,建議⽤ jsbundling-rails gem(可搭配 esbuild) 和 cssbundling-rails • Assets pipeline 還在,但 DHH 有新出 Propshaft gem (因為有⼈離職了)
  • 42.
    animation framework, dragand drop, Ajax controls DOM utilities, and unit testing. 考古 Rails 1.1 使⽤的JS框架
  • 43.
    DHH 在想什麼? Rails 發明⼈和Rails 前端⽅案的決策者 • 堅持想⽤ server-side rendering 中⼼思想 • 因爲夥伴 Sam Stephenson 負責前端,所以⼀開始⽤ Prototype.js,並⽤ RJS 達成⽤ Ruby 寫 JavaScript ⽅案 • 因為 Prototype.js 不⾏了, 被迫接受 jQuery。RJS 也跟著 gg • 曾經喜歡 Co ff eeScript,現在直接寫 ES6 了 • 很喜歡 Turbolinks ⽅案⾄今,⼤升級兩次: Classic Turbolink -> Turbolink 5 -> Turbo • 因為 Assets Pipeline(Sprockets) 功能對 ES6 和 js 模組化⽀援不⾜ • 被迫接受 Webpack ⽅案,但⼜捨不得把 Assets Pipeline 拆了。於是兩者並存... 前者處理js,後者處理css/image • Rails 7 ⼜把 Webpack 拆了 • 因爲跟夥伴 Sam Stephenson 拆夥了,正在把 Sprockets 換掉了 • Sam Stephenson 除了離開 basecamp,同時放棄所有跟 Rails 相關的 js 套件開發⼯作,包括 Hotwire, Stimulus 等
  • 44.
    考古: RJS template Rails1.1 的 Ruby JavaScript #/app/views/rjs1/index.rhtml <%= form_remote_tag :url => { :action =>’note’ }, :html => {:id=>’note-form’ } %> <%= text_field_tag ‘note’,nil,:size => 40 %> <%= submit_tag ‘submit’ %> <%= end_form_tag %> #/app/views/rjs1/note.rjs page.insert_html :bottom, ‘notes’, :partial =>’note’ page.visual_effect :highlight, ‘note’ page.form.reset ‘note-form’
  • 45.
     考古: RJS ⽀援七種DOM 操作 這是 rjs template code,是不是跟15年後的 Turbo Steams 很像?
 都說不⽤寫 javascript page.insert_html :bottom, 'dom_id', :partial => 'content' page.insert_html :top, 'dom_id', :partial => 'content' page.insert_html :before, 'dom_id', :partial => 'content' page.insert_html :after, 'dom_id', :partial => 'content'
 page.replace 'dom_id', :partial => 'content' page.replace_html 'dom_id', :partial => 'content' page.remove 'dom_id', :partial => 'content'
  • 46.
    rails-ujs (或是 jquery-ujs)⽅案 • RJS 隨著 Prototype.js gg 之後,Rails 放棄⽤ Ruby 產⽣ JavaScript 的⽅式 • 改成直接寫 js.erb,就直接寫 javascript • 但本質上是⼀樣的,由 server 後端傳 javascript 給前端執⾏ • 透過 link_to, button_to 或是 form_for 加上 remote: true,就會發送請求 script 到後端 • 後端回傳 javascript,前端執⾏
  • 47.
    我的看法 (1) • 我不喜歡Turbolinks (Turbo Drive) ⽅案,因為他改變了瀏覽器預設⾏為 • 90%場景沒問題,但那 10% 有問題場景,對初學者特別困擾 • 和其他 JS library 搭配時,也經常會有相容性問題要處理
  • 48.
    我的看法 (2) Turbo •Turbo Frames 是個漂亮的 ajax 功能語法糖,⽤ HTML 標籤就讓你替換 partial 部分區塊 • 只需要改 HTML erb 即可,後端不⽤改就可實現 Ajax 部分區塊替換 • 跟 https://github.com/renderedtext/render_async 作⽤類似,可以 ajax 替換 partial • Turbo Streams • ⽤ HTML 標籤,定義常⾒的 DOM 操作: 只是重新發明 RJS ⽽已... • 語法換了,但是精神是⼀樣,HTTP payload 從 script 換成 html,但骨⼦裡仍是 DOM 操作指令 • RJS 有的架構限制,⼀樣存在
  • 49.
  • 50.
    • 我認為 Server-siderender 好⽤開發快效能⼜好,只是⼀條路不能走到⿊ • ⼤部分簡單的場景,我認為 Remote JS ⽅案是很好⽤ • 從 Rails 1.1 時代的 RJS • rails-ujs 的使⽤相容性問題最⼩ (JS部分只是個簡單的 adapter) • 我的 Ajax 教材 https://ihower.tw/rails/fullstack-ajax-1.html • Turbo Steams • 這幾種運作本質上是⼀樣的,結構問題和能⼒限制也⼀樣 • 如果達到了 SPA 的場景需求,我會去採⽤ Vue.js 或是 React.js ⽅案 我的看法 (3) Remote JS
  • 51.
    結構和能⼒限制 ? • 結構問題 •在有密集 Ajax 操作的 UI 組件中,會導致非常多零碎的 JS template….. 每個⼩⼩的 template 只為了調整畫⾯上 的 DOM • 例如⼀樣是刪除 comment,因為前台 UI 不同,js template 就得不同 • ⽤ Hotwire 也是⼀樣,會有很多零碎的 .turbo_stream 檔案 • 能⼒限制 • 不在 rails-ujs 和 Turbo ⽀援的瀏覽器事件時 (超連結、表單等) • 在需要 javascript template 的 SPA 場景時 • 還要需要⽤ jQuery ⾃⼰掛載瀏覽器事件送 Ajax,然後後端回傳 JSON 處理 • 或是搭配前端框架,例如 Stimulus.js, Vue.js, React.js 等
  • 52.
    同個 comment 資料操作,不同UI準備不同template 好零碎啊.... •app/views/comments/create.js.erb • app/views/comments/edit.js.erb • app/views/comments/new.js.erb • app/views/comments/update.js.erb • app/views/comments/destroy.js.erb • app/views/answers/comments/create.js.erb • app/views/answers/comments/edit.js.erb • app/views/answers/comments/new.js.erb • app/views/answers/comments/update.js.erb • app/views/answers/comments/destroy.js.erb • app/views/peer_reviews/comments/create.js.erb • app/views/peer_reviews/comments/edit.js.erb • app/views/peer_reviews/comments/new.js.erb • app/views/peer_reviews/comments/update.js.erb • app/views/peer_reviews/comments/destroy.js.erb • ……..
  • 53.
    我的作法: 若有 SPA需求可在 erb 上掛載 Vue.js 例如⼀個複雜的 UI widget,有⼀堆 ajax 操作 • Vue.js 可以不需要 build 就可以使⽤,HTML上掛上 CDN 就可⽤,後端框架導入超簡單 • Vue.js 有出⼀個精簡版 petite-vue, gzip 後只有 6.9K,之後有需要也⽅便轉成 Vue.js 單組件 • 缺點是沒有單組件(SFC)功能,這⼀定需要 build 程序(需要 webpack 或是 esbuild 等) • 進⼀步可⽤ Vue.js 單組件掛載在 erb 上 • 此⽅案兼具 server-side rendering 開發快、速度也快的混和⽅案 • 需要 UI 組件時,再掛載前端組件即可
  • 55.
    # erb view <h2>⽂章標題</h2> <p>⽂章內容</p> …… <divid="comments-component"> <comments :comments="<%= @comments_data' %>"/> </div> 這是erb,可以讓初始資料塞到 props 即可。Vue.js 不需要⽤ Ajax 拿。
  • 56.
    # js 掛載(需要透過 webpack 或是 esbuild 編譯) import { createApp } from 'vue/dist/vue.esm-bundler.js'; import Comments from './components/comments.vue' document.addEventListener('DOMContentLoaded', () => { const comments = document.querySelector('#comments-component') if(comments){ createApp({ components: { Comments } }).mount(comments); } })
  • 57.
    # Comment Vue.js單組件 (template+js+css)(組件裡還可以包組件) <template> <div class="compose"> .... <ul v-for="(comment, comment_index) in comments" :key="comment.id"> <Comment :comment="comment" /> </ul> .... </div> </template> <script> import Comment from './comment.vue' export default { name: 'Comments', components: { Comment }, props: ['comments'], data () { }, methods: { } } </script> <style lang="css" scoped> ... </style>
  • 58.
    參考範例 • Vue.js 2+ Webpacker • https://medium.freecodecamp.org/how-to-pass-rails-instance-variables-into-vue- components-7fed2a14babf • https://5xruby.tw/posts/rails-vuejs-get-started/ • Vue.js 3 + jsbundling-rails (⽤esbuild) • 有空我再寫 blog • Vite Ruby ⽅案 https://github.com/ElMassimo/vite_ruby • Vite 是 Vue.js ⾃家出的 build ⼯具
  • 59.
  • 61.
    petite-vue example 我⽤ Vue.js改寫⼀樣的範例 <div id="hello"> <input type=“text” v-model=“name"> <button @click="greet"> Greet </button> <span> {{ output }} </span> </div> import { createApp } from ‘petite-vue’ createApp({ name: '', output: '', greet(){ this.output = `Hello, ${this.name}` } }).mount('#hello')
  • 62.
    • 上述 Vue.jscode 只有 stimulus 的七成... • Stimulus 仍是傳統 DOM 操作思維 • 簡化了事件掛載,提供了⼀個 controller 架構 • Vue.js 和 React.js 是現代前端的 MVVM 和 Component 組件思維 • 前端組件有⾃⼰的狀態,DOM 隨著狀態改變 • https://zh-hant.reactjs.org/docs/thinking-in-react.html • 我認為可以 Progressive 漸進式採⽤的 Vue.js 是比較好的選擇 • 可以先採⽤不需要 build 的 Vue.js ⽅式,這比 Stimulus.js 需要 Import Map (或 build 程序),導 入成本更低 • 之後有需要再加上 build 程序,就可以⽤單組件 Component
  • 63.
    https://rubyonrails.org/doctrine/zh_tw • Rails 提供你整套前端+後端的架構 •但很多元件都是可以替換的,例如 • 前端架構可換 • 測試框架可換 (很多⼈就換了 RSpec)
  • 64.
    我的看法(5): 前後端分離? 完全不⽤ erb後端 view template • 我認為 Rails 的 actionview 搭配 activerecord 是做表單的強項 • 前後端完全分離,就會失去 Rails 這個優勢 • 不會整個 app 都適合⽤ SPA 去做 • 混合⽅案才是 C/P 值最⾼ • 除非你有很好的理由: 康威定律、招聘問題(前端比較好招⼈?)
  • 65.
    就算要前後端分離,也可以不⽤這麼分離 • cookie 還是很棒,不需要⽤JWT • 登入⾴⽤ rails 處理,devise 繼續⽤,就不需要重新發明整個註冊、登入、忘記密碼流程 • 登入後再進入前端 SPA ⾴⾯即可 • 若可以同網域很棒,就不需要處理 CORS 跨域問題 • ⽤ nginx 處理分流即可,例如 • example.com/* ⽤ nginx reverse-proxy 到前端 app • example.com/api/* ⽤ nginx reverse-proxy 到後端 rails app • example.com/admin/* ⽤ nginx reverse-proxy 到後台 rails app
  • 66.
    不喜歡不⼀定要跟,以拖待變!! • 我的老專案就遲遲沒⽤ webpacker全⾯替換 sprockets • 拖到 webpacker 都 gg 了, sprockets 還在 • 跳過了 webpacker 時代,直接⽤ jsbundling-rails gem(可搭配 esbuild) 了 • 未來天秤⼜會回到 MPA? • https://nolanlawson.com/2022/05/21/the-balance-has-shifted-away-from- spas/ • MPA ⾸屏渲染更快、Core Web Vitals 分數更⾼、SEO 更好做…..
  • 67.
    The balance hasshifted away from SPAs https://nolanlawson.com/2022/05/21/the-balance-has-shifted-away-from-spas/ • Chrome implemented back-forward caching • 上下⾴瀏覽器本⾝就有快取 • Shared Element Transitions • 瀏覽器⽀援跨⾴時維持固定的元素
  • 68.
    Rails 後端 ViewComponent⽅案 順道⼀提
  • 69.
    我沒採⽤ • https://github.com/github/view_component • 我覺得不值得,如果真的有需求,不如⽤Vue.js 或 React 的前端 component ⽅ 案 • View Component 會增加改進後端性能的難度,特別是 N+1 queries • 後端 View Component 只有處理 erb template,⽽沒有包括 javascript 跟 css • Javascript 跟 css 仍然是沒有組建的狀態 • view component 沒有處理 css 跟 javascript,不就是做半套嗎
  • 70.
  • 71.
    # model class Post< ApplicationRecord has_many :comments end class Comment < ApplicationRecord belongs_to :post scope :visible, -> { where( :status => "public") } end # controller @posts = Post.includes(:comments) # view <% @posts.each do |post| %> <% post.comments.visible.each{ ... } ) %> <% end %> 1. 有 Scope 條件時 這會造成 N+1 嗎? 會
  • 72.
    # model class Post< ApplicationRecord has_many :comments has_many :visible_comments, -> { visible }, :class_name => "Comment" end # controller @posts = Post.includes(:visible_comments) # view <% @posts.each do |post| %> <% post.visible_comments.each{ ... } %> <% end %> 解法
  • 73.
    2. 預加載(Preload)資料再select # model defPost < ApplicationRecord has_many :comments def my_comments self.comments.where( :user_id => self.user_id ) end end # controller @posts = Post.includes(:comments) # view <% @posts.each do |post| %> <% post.comments.each{ ... } %> <% post.my_comments.each{ ... } %> <% end %> 這會造成 N+1 嗎? 會
  • 74.
    # model def Post< ApplicationRecord has_many :comments def my_comments self.comments.select{ |x| x.user_id == self.user_id } end end 解法
  • 75.
    3. inverse_of 跟N+1 queries 也有關係 class Post < ApplicationRecord has_many :comments, :inverse_of => :post end class Comment < ApplicationRecord belongs_to :post, :inverse_of => :comments end
  • 76.
    class Post <ApplicationRecord has_many :comments end class Comment < ApplicationRecord belongs_to :post end
  • 77.
    <% Post.includes(:comments).each do|post| %> <%= post.title %> <% post.comments.each do |comment| %> <%= comment.content %> <%= comment.post.title %> <% end %> <% end %> 這會造成 N+1 嗎? 若inverse_of 沒作⽤就會
  • 78.
    class Post <ApplicationRecord has_many :comments, :foreign_key => :post_id end # and/or class Comment < ApplicationRecord belongs_to :post, :foreign_key => :post_id end
  • 79.
    原因: ⾃動 inverse_of可能失效 • Rails 不總是會推導出 inverse_of 屬性,例如 • 你⼿動給 , -> { where條件 } 時 • 有 :foreign_key 參數時 • 有 :through 參數時 • 此時就必須⼿動給 inverse_of class Post < ApplicationRecord has_many :comments, ->{ where(....) } end
  • 80.
    4. JSON columntype 超好⽤ • PG ⽀援、MySQL 5.7 之後也⽀援 • 不需要 MongoDB 了、舊的 ActiveRecord Serialize ⽅法也別⽤了 • 可減少了 N+1 queries 機會 • 適合場景: 不需要正規化的資料,例如 • 儲存整個 API response data • 額外的附屬欄位資料 • has_many 關聯資料,但是其中資料卻不需要個別拿出來使⽤的 • EAV 設計,例如客製表單
  • 81.
    Rails 實戰聖經: Rails後端效能 • https://ihower.tw/rails/fullstack-performance-backend.html • 我寫的效能教材 • 有上述完整的說明
  • 82.
    Benito Serna • https://bhserna.com/avoid-n-plus-one-queries.html •這 blog 是我看過研究 N+1 最深入的,例如 • 5 ways to fetch the latest-N-of-each record on Rails • 5 ways to fi x the latest-comment n+1 problem • 作者還有出⼀本⼩PDF書,值得⼀看 • https://bhserna.com/avoid-n-plus-1-queries-on-rails.html
  • 83.
    最後,關於 Gemfile 版本指定 •Gem fi le 裡⾯,盡量不要鎖定版本,那是 Gem fi le.lock 的事 • 只有真的真的需要指定版本才能動的版本,才需要在 Gem fi le 內指定版本 • 例如某 gem ⼀定要 '~> 3.0‘,不能⽤ 4.0,這種才需要指定版本 • 原因可能是⽬前 rails 不⽀援 4.0,或是 4.0 需要改 code 但還沒處理,或是 4.0 有 bug • 若⼀定要⽤舊版,不能升級新版的原因要註解寫在 code 上⾯ • 如此⽤ bundle update 升級時,才會是正確的預期結果 • 那些有指定版本的 gem,也就是(⽬前)無法升級的 gem
  • 84.
    gem 'rails', '6.1.4.1' #若升級 v2 需要補設定 manifest.js gem 'sprockets', '~> 3.7.2' gem 'jquery-rails' gem 'jbuilder' gem 'kaminari' gem 'bootstrap4-kaminari-views' gem "pundit" # 此專案⽤ webpack v3 gem 'webpacker', '~> 4.x' # 因為 omniauth-wechat-oauth2 需要 omniauth 1.3.2 gem 'omniauth', '1.3.2' # 因為 omniauth 1.3.2 被鎖在 1.3.2 gem 'omniauth-facebook', '~> 4.0.0' # 因為 master branch 才⽀援 wechat_qiye gem "omniauth-wechat-oauth2", :github => "NeverMin/omniauth-wechat-oauth2" # 因為前端⽤ Bootstrap v4 gem 'bootstrap', "~> 4.6" 舉例(真正必要才指定版本)
  • 85.
    謝謝聆聽 技術選擇⼀定有風險,套件有 upgrade 有deprecated 選擇前應詳閱code⽂件 https://ihower.tw