avatar

大兜

右手寫程式,左手寫音樂

分類:程式

留言

Ruby#open 知多少?

本文同步發表於 alphacamp

先來個快問快答吧:

如果現在要你使用 Ruby 去開檔,你會想到怎麼做?

直覺是使用 File.open,但想想 File.new 似乎也可行,然後又發現不使用 File 類別,直接用 open 也能做到一樣的事。去查了 Ruby 文件結果...

繼續閱讀

留言

Rails on webpack

記得筆者在去年於 RubyConf Taiwan 講的題目「還給前端工程師一片天空」中提到如何整合 Rails 與 Node.js 世界的工具,投影片在此。雖然 webpack 正夯,但礙於筆者當時沒去研究,使用的工具仍是上一代的 gulp、bower,所幸議程也有相關的講題,如何澤清前輩的「gem 'webpack-rails'」。

只是經過筆者幾番研究之後,覺得整合這兩樣東西似乎不需要像網路上找到的各種教學文或是 gem 搞的那樣複雜,所以想藉這篇文章分享自己的做法(但並不保留 sprockets)。在那之前,先分享一些小知識:

asset_path 知多少?

#asset_path 是 Rails 中其中一個底層 API,許多 helper 如 #image_tag 等都會調用,而若沒有特別查閱原始碼,大概不多人知道 asset_path('/app.js')asset_path('app.js') 差了一個斜線會影響結果,至於差在哪就讓我們節錄部份原始碼:

def asset_path(source, options = {})
  # ...
  if source[0] != ?/
    source = compute_asset_path(source, options)
  end
  # ...
end

sprockets 其實有偷偷去複寫 #compute_asset_path,也就是說當傳入 "/app.js",assets pipeline 是不會運作的,但若傳入 "app.js",在 sprockets #compute_asset_path 的加持下結果會長的像 /assets/app-c5bd5cb45ee76432b26a5dfb28e01b59.js?body=1;反之若 "app.js" 檔案不存在,或者根本就沒安裝 sprockets,那就退回原型得到 "/app.js"

所以只要我們複寫的 #compute_asset_path 能算出 webpack 產生在 public/assets 中的正確路徑,其實不用更改任何 API,就可以輕鬆整合 webpack。而 #javascript_include_tag#stylesheet_link_tag#image_tag 等 helper 都可以照常使用。

webpack –json

sprockets 在編譯後會產生 manifest.json,裡面有 asset 原始路徑與其計算後的路徑資訊(例如 app.js 對應到 /assets/app-c5bd5cb45ee76432b26a5dfb28e01b59.js,以 Hash 儲存),好處是 Rails 可以透過讀檔取得計算後的路徑結果,不須透過 sprockets,所以為了增加效能,這個檔案在 production 環境中是必要的,而在 development 環境中,由於 assets 的路徑是及時透過 sprockets 得出,所以並不需要這個檔案。

此外 sprockets 在 controller 與 router 中也動了手腳,這也是為什麼專案中 public/assets 明明沒東西,而在 development 環境下中送出像是 /assets/ooxx.js 的請求卻仍可以正常運作的原因。

只是 webpack 畢竟是 Node.js 世界的產物,無法篡改 Rails controller 與 router,好消息是 webpack --json 會產生一個跟 manifest.json 類似的檔案(官方稱之為 stats file)。所以只要在 webpack.config.js 中加入產生 stats 的 plugin,再用 webpack --watch 來開發就已經綽綽有餘了,例如我們在專案的根目錄下產生 stats.json

plugins = [
  function() {
    this.plugin('done', function(stats) {
      require('fs').writeFileSync(__dirname + '/stats.json', JSON.stringify(stats.toJson()))
    })
  }
]

範例專案

筆者在 Github 實作了一個範例,重點只在兩個檔案: app/helpers/webpack_helper.rblib/webpack_stats.rb

webpack_stats.rb 負責載入 webpack 產生的 stats.json,經過處理之後產生一個 assets hash,例如:

{
  'app.js' => '/assets/app.js',
  'app.css' => '/assets/app-c5bd5cb45ee76432b26a5dfb28e01b59.css' # 也支援 hash 尾綴
}

(限制是 webpack 必須遵守檔名格式為 filename-hash.extname 或是 filename.extname。)

而這個 assets hash 將被用在 #compute_asset_path

# app/helpers/webpack_helper.rb
require 'webpack_stats'
module WebpackHelper
  def compute_asset_path source, options = {}
    WebpackStats.assets[source] || super
  end
end

大功告成!老實說這樣就已經能完美運作了,不用安裝什麼 gem,實測用來寫 react 也不用安裝 react-rails。而剩下的問題已經不關整合的事,像要加入 SASS、CoffeeScript 或是 Font Awesome 等,或是在 production 下要壓縮 JavaScript 並且分離 CSS 檔案等,這取決開發者對 webpack 的掌握。

至於只在 production 中分離 CSS 檔案,裡面可以這樣寫:

<%= stylesheet_link_tag 'application', media: 'all' if Rails.env.production? %>

至於為何要這樣做,可參考 extract-text-webpack-plugin 的 README 提到的優缺點。

我就是要 gem

其實筆者也是有做啦⋯⋯如果真的覺得那兩個檔案很麻煩,可以安裝 webpack_stats,這除了可以用在 Rails,也可以當一般 webpack stats 的 loader 使用(用在 Rails 之外的地方)。

繼續閱讀

留言

來自 chef.io 的小禮物

筆者在開原世界裡面也打滾一陣子,貢獻過大小專案如 Rails、Ruby 等,也收過一些獎勵如比特幣、質數幣,但這次的禮物還蠻特別的,讓人忍不住想要多發一篇網誌:

chef.io 這種方式還頗逗趣的,比起錢,筆者似乎還比較喜歡這種可愛方式(完全中招啊)。除了感謝狀之外,還有一些小貼紙,可惜筆者沒有在筆電上面加貼紙的習慣,只能收在抽屜裡了。

唯一美中不足的地方就是國名寫了「Province of China」讓人不是很舒服,如果只寫「China」或「Republic of China」筆者是還勉強可以接受,畢竟現在中國正在內戰中嘛,只是搞不清兩個政府的領土就實在有點不應該(但無論是哪一個 China,當然還是寫 Taiwan 最好聽了)。

ChefConf 2016

題外話,最近筆者收到一封寄給貢獻者的 ChefConf 2016 邀請信,早鳥票可以省 400 鎂,然後如果是 chef 貢獻者的話還可以額外省 $295 鎂,害得筆者頗心動,但即便如此筆者仍無法負擔高額的機票與住宿的費用,如果有誰需要這筆優惠的話,還請與筆者聯絡,不要讓這筆錢浪費了,感激不盡 :)

繼續閱讀

留言

對不起,我把馬英九放進 Ruby 2.3 了

這世界所有升級到 Ruby 2.3 的伺服器,皆可以得到英九的庇佑。

身為一個 Ruby 語言的教徒,這個聖誕節最令人興奮的莫過於 Ruby 2.3 的發表了,不過這次由於筆者的緣故,意外讓台灣的政治人物們走進了 Ruby 原始碼中,究竟是怎麼回事呢XD

百聞不如一見,各位不妨...

繼續閱讀

留言

PTT 每天自動登入小程式

最近大選近了,八卦版似乎將發文門檻調高到了 1000 次,帶風向的黨工少了,頓時清新了許多(不過仍然影響不到筆者)。其實養 PTT 帳號不容易,因為同一天只算登入一次,我們得每天記得登入一次 PTT,否則就平白損失了24 小時,如果沒有 PTT 成癮症,其實很容易忘記。

而身為工程師如筆...

繼續閱讀

留言

Ruby SSE Server 動手做

本篇文章同時也發表於 CodeTengu

照片是日本硬體製造商 Speedlink 在東京舉辦的 server 投擲大賽,誰能把 server 推得最遠,可以得到最高的分數,圖片中的機器值 50 萬日幣,這也是名符其實的 push server,影片在此

最近要蓋個 SSE

繼續閱讀

留言

Rails 購物車設計

封面圖片為 Minecraft 遊戲的截圖,軌道上的貨車(cart on rails),筆者也算是從 alpha 版就開始玩的老玩家呢。

隨著電子商務的崛起,網站上的購物車系統已經成了普遍的功能,這幾年 Rails 在台灣也快速竄紅,且由 Dave Thomas 與 David Heinemeier...

繼續閱讀

留言

台灣身份證字號驗證器

今天在 Ruby Taiwan 的 FB 社團上看到 adz 大大在問是否有身份證字號驗證的 gem 可以用。

這東西記得在大一時用 C、Java 各寫過一次,程式碼已遺失,反正今天寫了一天程式也累了,只好寫別的程式來放鬆一下(啥?),不過這次會用 Ruby 來寫,同樣的程式碼我也放在 Gist

繼續閱讀

留言

TJDict 兩週年紀念

其實算起來兩週年是去年八月,現在發這篇有些晚,筆者寫此文前已先懺悔一番。

說來那話兒也不長⋯⋯

2012 八月,筆者還是個飽受英文論文所苦的死大學生,總是開一堆字典交叉查詢覺得很不方便,索性做了一個工具幫助自己,本來是自用,但在朋友慫恿下放上了 Google...

繼續閱讀

留言

Rails 4.2 重點介紹

Rails 團隊終於要在聖誕節的同時釋出 Rails 4.2 版了,這次更新的重點有以下項目:

  • Active Job
  • Asynchronous mails
  • Adequate Record
  • Web Console
  • Foreign key support

Active Job

一個網站常有些較繁重的工作,並不希望在使用者提出請求時立即執行,以寄出一萬封信為例,這也許需要幾分鐘的時間,當使用者點下寄信按鈕時如果還需要等個幾分鐘才可以看到回傳頁面,這將造成糟糕的使用者體驗。

正規的作法是將這類需要長時間的工作丟到工作佇列去排程,並在背景中執行多個 worker 程序,每個 worker 都會不斷重複從佇列中取得新的工作去執行。

Rails 已經有許多 gem 可以解決這個問題,著名項目包括 ResqueSidekiqDelayedJob,其中 Resque 與 Sidekiq 使用 Redis 存放工作住列,DelayedJob 則用關聯式資料庫。

Active Job 並不是提出了一個新的實作,換句話說,使用 Rails 4.2 並不代表未來就不用安裝 Resque 之類的 gem。

它的真正意義在於統一使用介面,讓開發者在不同的 gem 之間切換時,可以不用受到 gem 的不同 API 而影響,因而降低重新改寫的成本。

看到這裡是否覺得這種作法很熟悉?它其實就是適配器模式(Adapter pattern),早在 Active Record 誕生的時候就已經使用相同的技巧,Rails 之所以能以相同的 API 介面在不同的資料庫之間遊走也是拜此所賜。

目前支援的 gem 有:

$ ls -1 activejob/lib/active_job/queue_adapters
backburner_adapter.rb
delayed_job_adapter.rb
inline_adapter.rb
qu_adapter.rb
que_adapter.rb
queue_classic_adapter.rb
resque_adapter.rb
sidekiq_adapter.rb
sneakers_adapter.rb
sucker_punch_adapter.rb
test_adapter.rb

除了 test_adapter.rb 僅用於測試,以及 inline_adapter.rb 為預設(立即執行,不會丟入背景),以外都有相對的 gem 需要安裝。

使用方式

工作的內容必須定義在 app/jobs/ 下,並繼承自 ActiveJob::Base,不過 Rails 4.2 提供了產生器,並不一定要手動新增:

$ rails g job execute_simulate
      invoke  test_unit
      create    test/jobs/execute_simulate_job_test.rb
      create  app/jobs/execute_simulate_job.rb
# app/jobs/execute_simulate_job.rb
class ExecuteSimulateJob < ActiveJob::Base
  queue_as :default

  def perform(*args)
    # Do something later
  end
end

queue_as 可以設定將此工作排進特定的佇列,預設是 default,可以透過 --queue
參數修改:

$ rails g job execute_simulate --queue urgent

使用起來像是這樣:

# 將工作丟進佇列
ExecuteSimulateJob.perform_later record

# 排程明天中午再執行
ExecuteSimulateJob.set(wait_until: Date.tomorrow.noon).perform_later(record)

# 排程一週後執行
ExecuteSimulateJob.set(wait: 1.week).perform_later(record)

# 指定特定的佇列
ExecuteSimulateJob.set(queue: :important).perform_later(record)

設定要使用的 gem:

# config/application.rb
module YourApp
  class Application < Rails::Application
    # 請確保 Gemfile 已經有安裝所要使用的 gem
    config.active_job.queue_adapter = :resque
  end
end

回呼(Callback)

共有以下 6 個註冊點:

  • before_enqueue
  • around_enqueue
  • after_enqueue
  • before_perform
  • around_perform
  • after_perform

使用方式與 controller、model 中的回呼寫法是一樣的:

class ExecuteSimulateJob < ActiveJob::Base
  queue_as :default
 
  before_enqueue do |job|
    # 在佇列前執行
  end
 
  around_perform do |job, block|
    # 在工作開始前執行
    block.call
    # 在工作完成後執行
  end
 
  def perform
    # Do something later
  end
end

Asynchronous Mails

如果寄信工作也要丟到工作佇列,先別急著寫工作檔,Rails 4.2 的 Action Mailer 內建了 DeliveryJob 類別,並提供 deliver_later 方法將寄信工作推進佇列。

你可以像這樣使用:

# 使用 #deliver_later 透過 DeliveryJob 來寄信
MyMailer.welcome(@user).deliver_later

# 若不想丟到工作住列,也有 #deliver_now 可以使用
MyMailer.welcome(@user).deliver_now

Adequate Record

由 Aaron Patterson 所作,用於提高 #find#find_by 等一些常用查詢指令的速度,可以提升 Active Record 約莫兩倍的效能。

主要是因為 Active Record 在產生 SQL 過程有很多重複的片段不斷被重新製造,這其實可利用快取將重複的片段保存起來。細節請參考 Aaron Patterson 的網誌

Web Console

如果你有用過 better_errors gem,那麼這就是類似的東西了。Rails 4.2 在開發環境下的錯誤頁面會多出一個 rails console 命令窗可以使用,除了一般 irb 的功能以外,也可以存取到該次請求中定義的實體與區域變數。

Rails 4.2 Web Console

但不只有錯誤頁面才有命令窗可以使用,也可以在任何 view 的檔案中的任何位置加入 <%= console %>,只要渲染到該檔案,就有命令窗可以使用。

外鍵(Foreign Keys)

Rails 4.2 遷移指令支援了 SQL 的外鍵定義功能,目前只有 mysql、mysql2 與 postgresql 可用。

# 將 `articles.author_id` 定義為參考 `authors.id` 的外鍵
add_foreign_key :articles, :authors

# 若命名沒有按照慣例,也可以透過設定達到
# 例如將 `articles.author_id` 定義為參考 `users.lng_id` 的外鍵
add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"

# 刪除 `accounts.branch_id` 外鍵
remove_foreign_key :accounts, :branches
 
# 刪除 `accounts.owner_id` 外鍵
remove_foreign_key :accounts, column: :owner_id

這個功能在 migration 與 model 等會產生遷移檔的產生器,當使用到 references 型別時也會自動的被使用,例如:

$ rails g migration add_user_to_posts user:references
      invoke  active_record
      create    db/migrate/20141222180048_add_user_to_posts.rb
class AddUserToPosts < ActiveRecord::Migration
  def change
    add_reference :posts, :user, index: true
    add_foreign_key :posts, :users # Rails 4.2 功能
  end
end
繼續閱讀