如何在 Rails 使用 Webpacker(上)

如何在 Rails 使用 Webpacker(上)
credit: chuttersnap

在 Rails 專案中,所謂的「Assets」指的是 JavaScript、CSS 以及圖片檔、字型檔之類的檔案。在古老時代,Rails 是把這些檔案放在 public 目錄下,而在 Rails 3.1 之後開始引入 Assets Pipeline 後,這些檔案會被 Sprockets 來打包,這些靜態檔案最後會被打包放到 public/assets 目錄裡。

同時,像是 CoffeeScript 或是 SASS/SCSS 之類的也可順便交給 Assets Pipeline 幫忙翻譯、打包,讓開發人員可以在寫 JavaScript 或是 CSS 的時候寫得更順手一些。

就以在單純 Rails 的環境來說,Assets Pipeline 在早期其實還不錯用。但慢慢的 Assets Pipeline 慢慢浮出了一些問題:

1. 慢!

對,就是慢。Ruby 本來就不算是速度快的程式語言,在這個它不擅長的領域雖然還是做的不錯,但就是不夠快,特別是專案越大,會慢的越明顯。

另外,原本不太喜歡 JavaScript 的人可能會透過 CoffeeScript 之類的工具來做轉換,但隨著瀏覽器對 ES6 的支援程度上昇,像 CoffeeScript 之類的「二創」工具就變得有點雞肋。

2. 可能得等別人包好 Gem

因為我很懶,所以其實我就是那種會期待「這個東西有沒有強大的善心人士包好 Gem 給我用啊!」的人,例如 Bootstrap gem 就是一個例子,只要在 Gemfile 裡設定好,bundle install 指令打完就差不多可以用了。但這樣靠別人總是不太好...

雖然就算不用別人包好的 Gem 也是有別的解法(例如手工把檔案放到對應的目錄裡),只是就會有點懶(懶墯是工程師的美德啊)

3. 容易被誤會

app/assets/javascripts/application.js 檔案裡你可能曾經看過這樣的寫法:

//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require_tree .

這一眼看過去會以為是註解,有些新手可能以為這是註解就這樣順手把它刪掉,然後程式就壞了。每次上課都得提醒同學們,這個寫法是有特殊意義的...

先說,老實說我個人不覺得上面這些問題真的是什麼大問題(第一點的慢算是個問題,但我不是那麼介意就是...),所以如果大家也不覺得上面這些問題是問題的話,Assets Pipeline 還是不錯用的。

隨著前端技術越來越複雜,前端圈也有自己的打包工具,例如 GulpGruntBower 以及 Webpack 等等,其中目前 Webpack 算是市佔率比較高的。只是,Webpack 的設定檔,聽說是出了名的複雜,通常設定過一次就不會想再設定第二次(逃)...

Webpacker

好在,有善心人士把 Webpack 包成 Gem,也就是 Webpacker,減低了不少對我這種前端苦手的人的困擾。

等等,這...不就是上面提到的第二點問題嗎?好啦,我想大家就不要這麼計較了,至少這個 Gem 可以解決其它前端 Gem 的問題嘛 :)

從 Rails 5.1 開始,Webpacker 就變成選配的設定,只要在建立專案的時候加上適當的參數:

$ rails new cute_app --webpack

就可以幫你在建立專案的時候,順便把 Webpacker 相關的套件以及設定一次幫你搞定。

其實原本我 Assets Pipeline 用得好好的,也不是很想面對 Webpack 之類的前端打包工具,但從 Rails 5.1 開始變成選配,到 Rails 6 變成標準配備,不面對也不行了...

上面的指令在執行的時候,如果你仔細看,就會發現在建立專案的後半段會出現這樣的訊息:

Bundle complete! 19 Gemfile dependencies, 80 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
       rails  webpacker:install
      create  config/webpacker.yml
Copying webpack core config
      create  config/webpack
      create  config/webpack/development.js
      create  config/webpack/environment.js

...[略]...

Installing all JavaScript dependencies [4.0.7]
         run  yarn add @rails/webpacker from "."
yarn add v1.16.0
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

...[略]...

Webpacker 使用了 yarn 來安裝相關的套件,接著你就會發現有個非常沈重的資料叫做 node_modules 出現在專案裡,裡面裝了很多可能用得到或用不到的東西。

即使在建立專案的時候沒有加上 --webpack 參數也沒關係,可以後續再手動在 Gemfile 裡加上 webpacker,然後再執行:

$ rails webpacker:install

也會有一樣的效果。不只這樣,如果你想一併安裝目前最熱門的前端框架 React.js 或 Vue.js 的話,也可以在後面再加上它:

$ rails webpacker:install:react

或是:

$ rails webpacker:install:vue

這樣除了會安裝 React.js 或 Vue.js 到專案裡之外,還會順便產生一個 Hello World 的範例在 app/javascript 目錄裡,不過要怎麼寫 React.js 或是 Vue.js,那就是另一個故事了。

除了 React.js 跟 Vue.js 之外,如果執行 rails help 指令,還可以看到 webpacker:install 還支援以下選項:

$ rails help
The most common rails commands are:
 generate     Generate new code (short-cut alias: "g")
 console      Start the Rails console (short-cut alias: "c")
 ...略...
  webpacker:install:angular
  webpacker:install:coffee
  webpacker:install:elm
  webpacker:install:erb
  webpacker:install:react
  webpacker:install:stimulus
  webpacker:install:svelte
  webpacker:install:typescript
  webpacker:install:vue
 ...略...

除了 node_modules 目錄外,你也可以在 app 目錄下發現一個名為 javascript 的目錄。是的,它從以前的 app/assets/javascripts 拉上來,變成一等公民了。

是說,並不是裝了 Webpacker 這個 Gem 之後,把檔案全部丟到 app/javascript 就結束了,你還是得知道這些檔案該怎麼放比較合適,以及該怎麼引用這些檔案。

準備重新學習如何在 Rails 專案裡寫 JavaScript 了嗎?別擔心,我也是 :)

使用方式

使用 Webpacker 之後,因為打包方式不同了,使用方法也有些不同。首先,在 app/views/layout/application.html.erb 這個檔案裡:

<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

的下方再加上一行:

<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

其實就是把原本的 javascript_include_tag 換成 javascript_pack_tag

原本 javascript_include_tag 這行要留著嗎?如果你還有些檔案還是用原本的 Assets Pipeline 打包的話,那就留著它吧。

如果你使用的是 Rails 6 以上版本的話,在 layout 預設就直接是 javascript_pack_tag 而沒有 javascript_include_tag 了。

請領班幫忙

使用 Webpacker 之後,前端的東西就交給效能比較好的 JavaScript 啦,Ruby 就暫時先繼續打包原來的 Assets 就好。

所以,在開發的同時,終端機除了要開著 bin/rails server 執行應用程式,同時又得開著 bin/webpack-dev-server 來編譯、打包這些前端檔案。覺得很麻煩嗎?我們可以請領班 foreman 來幫你一次搞定。

請先在 Gemfile 的 development group 裡加上 foreman

group :development do
  # ...[略]...
  gem 'foreman', '~> 0.86.0'
end

bundle install 之後,接著在專案的根目錄新增一個名為 Procfile 的檔案,內容如下:

web: bin/rails server -p 3000
webpack: bin/webpack-dev-server

前面的 webwebpack 只是個名字,你可以取一個你自己喜歡的,像是:

app: bin/rails server -p 3000
cute: bin/webpack-dev-server

你開心就好,但建議還是取個容易辨識的名字。搞定之後,在終端機執行:

$ foreman start

仔細看看執行時候的訊息,現在不只會在 port 3000 啟動 rails server,同時也會一併執行 webpack-dev-server 了。

如果你剛才建立的檔案名稱叫做 Procfile.dev,在啟動 foreman 的時候要再加個參數,指定 Procfile 的位置:

$ foreman start -f Procfile.dev

不得不說,把打包這件事交給 JavaScript 來做,速度真的快滿多的。

如何引入其它的 JavaScript 函式庫?

講這麼多,還是來看點 code 比較實際。

我們先看一下這所謂的一等公民 app/javascript 裡面有哪些東西。要注意的是,我是使用 Rails 5.2.3 搭配 --webpack 參數建立專案,如果你是使用 Rails 6 之後的版本可能會有一些不同。

在這個目錄裡,目前可看到一個名為 packs 的目錄,裡面只有一個 application.js (如果是 Rails 6 的話還會有個 channels 目錄)。再看看這個檔案的內容,除了一行打招呼的 Hello World 之外,就是一堆的註解,這邊的註解就是真的註解啦。

而在前面 layout 裡加的那行 javascript_pack_tag 'application',到時候就是會來讀取這個 packs 目錄裡的 application.js 檔案。

接著,如果我想寫一些 JavaScript 的程式碼,可以直接就寫在 application.js 裡,但把全部的 code 都塞在這個檔案裡也不太好,應該適當的做一下模組化。例如,我想在某些頁面寫一些 JavaScript,我就可以先在 app/javascript/packs 目錄下,新增一個名為 ccc.js 的檔案,這時候的結構如下:

app
...[略]...
├── javascript
│   └── packs
│       └── application.js
│       └── ccc.js
...[略]...

然後我在 ccc.js 裡隨便寫個 console.log

// 檔案:app/javascript/packs/ccc.js

console.log('Hello, Cute App!')

在需要這段程式碼的相關 View 頁面就可以這樣寫:

// 檔案:app/views/pages/home.html.erb

<h1>Cute App</h1>
<p>You're too smart to understand the code!</p>

<%= javascript_pack_tag 'ccc' %>

這個 javascript_pack_tag 'ccc' 會去 packs 目錄裡找到 ccc.js,所以打開瀏覽器的 console 應該就會看到印出 Hello, Cute App!

不過,更多時候可能是全站都會用到的,這時候把它放在 packs 裡再一個一個用 javasccript_pack_tag 來載入就有點不太實際。例如剛剛那個 ccc.js 裡有全站都可能會用到的功能,我可以先在 app/javascript 目錄下建立一個 ccc 目錄,裡面再放一個 index.js 檔案,此時的結構如下:

app
...[略]...
├── javascript
│   ├── ccc
│   │   └── index.js
│   └── packs
│       └── application.js
...[略]...

接著,在 application.js 裡加上這行:

// 檔案:app/javascript/packs/application.js

import '../ccc'

那行 import 會去 ccc 目錄裡找 index.js 檔案,然後執行它。當然,如果這個 ccc 想要做得更模組化,像是這樣:

app
...[略]...
├── javascript
│   ├── ccc
│   │   ├── cute.js
│   │   ├── events.js
│   │   ├── index.js
│   │   └── smile.js
│   └── packs
│       └── application.js
...[略]...

我這裡把各個 JS 分別拆成 cute.jsevents.js 以及 smile.js 三個檔案,然後讓這個 index.js 來負責做「匯整」的工作:

// 檔案:app/javascript/ccc/index.js

import './cute'
import './smile'
import './events'

模組化之後,大家就可以各自散開寫各自的功能,比較不會在 merge 的時候得一直解 conflict。

其實,優秀的開發人員跟普通的開發人員,很多時候只差別在優秀的開發人員知道怎麼把檔案放在對的地方而已 :)

小結

Assets Pipeline 要留著嗎?

即使在 Rails 6 已經預設使用 Webpacker 來打包,Assets Pipeline 預設也沒有被廢除。

我個人是不會刪掉 app/assets 目錄,但為了避免混亂,如果開始使用 Webpacker 的話,我會把 JavaScript、CSS 以及圖片檔,都慢慢往 Webpacker 指定的目錄裡放。

所以,究竟該用 Webpacker 還是用 Assets Pipeline 呢?其實都好,如果你的專案只是一般的 CRUD(新增、讀取、更新、刪除)功能,沒有太複雜的前端程式的話,那 Assets Pipeline 也是很夠用的;相對的,如果在專案裡寫了許多前端相關的程式碼或是用了那些熱門的前端框架,那 Webpacker 是個不錯的選擇。

下一篇文章再繼續跟大家介紹如何使用其它 JavaScript 套件。

參考資料

工商服務

實體課程:Ruby on Rails 實戰課程
線上課程:五倍學院線上課程