avatar

大兜

右手寫程式,左手寫音樂

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 之外的地方)。