# 如何在 Rails 使用 Webpacker（中）

> 如何正確的在 Rails 裡使用 Webpacker

Published: 2019-11-22
URL: https://kaochenlong.com/webpacker-with-rails-part-2

---

在[前一篇](/2019/11/21/webpacker-with-rails-part-1/)文章介紹了如何在 Rails 使用 Webpacker，接下來這篇就我們就來實際安裝幾個 JavaScript 套件來試試手感吧！

&lt;!-- more --&gt;

### 安裝 jQuery

在各種前端框架橫行的現在，[jQuery](https://jquery.com) 這種看起來相對比較樸實無華且枯燥的 JS 套件就好像沒什麼人想用了。在 Rails 5.1.0 的時候，[DHH 把以往一直預設安裝的 jQuery 給拿掉了](https://github.com/rails/rails/issues/25208)，所以如果想要使用 jQuery 的話就得自己再手動裝回來，通常會使用 [jquery-rails](https://rubygems.org/gems/jquery-rails) 這個套件。

即然都拔掉了為什麼還會想裝回來？因為還是有一些套件是依賴 jQuery 在做事的，例如在下一篇文章會介紹到的 Twitter Bootstrap 就是其中一個。

接下來我們就試著使用 Webpacker 來把 jQuery 裝回來吧！

#### Step1: 安裝 jQuery

這裡我們不使用 Gem，而是使用 `yarn` 來安裝：

    $ yarn add --dev jquery

這個指令執行完成後，會自動在根目錄的 `package.json` 加上 jQuery 相關的設定：

```json
{
  &quot;name&quot;: &quot;cc4&quot;,
  &quot;private&quot;: true,
  &quot;dependencies&quot;: {
    &quot;@rails/webpacker&quot;: &quot;^4.0.7&quot;,
    &quot;jquery&quot;: &quot;^3.4.1&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;webpack-dev-server&quot;: &quot;^3.9.0&quot;
  }
}
```

同時也會把 jQuery 套件放一份在專案裡的 `node_modules` 目錄裡。

#### Step2: 引用 jQuery

如同前面介紹的做法，如果 jQuery 想要全站都可以使用的話，可以直接把 jQuery 在 `app/javascript/pack/application.js` 裡把它引用進來：

```javascript
// 檔案：app/javascript/packs/application.js

import &#39;../ccc&#39;
import &#39;jquery&#39;
```

跟上一篇介紹到的 `import &#39;../ccc&#39;` 的相對目錄寫法不同，這裡的 `import &#39;jquery&#39;` 會去 `node_modules` 目錄裡去找 jQuery，這樣就可以把 jQuery 引用進來了。讓我們來試一下：

```javascript
// 檔案：app/javascript/ccc/smile.js

import &#39;jquery&#39;

$().ready(function(){
  console.log(&quot;You&#39;re so sweet!&quot;)
})
```

寫過 jQuery 的人應該都看過這個 `$` 符號是 jQuery 的招牌符號，`$().ready()` 則是在 DOM 載入完成之後執行裡面的那個 callback function，所以理論上上面這幾行應該可以在瀏覽器的 console 裡看到 `You&#39;re so sweet!` 的字樣，但打開瀏覽器執行一下，發現這個訊息：

    Uncaught ReferenceError: $ is not defined

咦？以前不是直接加上 `&lt;script src=&quot;...&quot;&gt;&lt;/script&gt;` 就可以用了嗎？現在怎麼不行了？就，以前你是直接用別人打包好的檔案，現在你是自己打包啊。這有幾種解決方法，第一種，就是直接在 import 的時候把 `$` 明白的寫出來：

```javascript
// 檔案：app/javascript/ccc/smile.js

import $ from &#39;jquery&#39;

$().ready(function(){
  console.log(&quot;You&#39;re so sweet!&quot;)
})
```

這樣就有 `$` 符號可以用了。另一種做法，就是直接在 Webpack 的環境設定檔上動手腳，讓 `$` 變成全站都能用：

```javascript
// 檔案：config/webpack/environment.js

const { environment } = require(&#39;@rails/webpacker&#39;)

const webpack = require(&#39;webpack&#39;)
environment.plugins.prepend(&#39;Provide&#39;,
  new webpack.ProvidePlugin({
    $: &#39;jquery&#39;,
    jQuery: &#39;jquery&#39;
  })
)

module.exports = environment
```

透過 Webpack 的 `ProvidePlugin` 來自動幫我們載入模組，就不用自己手動 `import` 或 `require` 了。

參考資料：&lt;https://webpack.js.org/plugins/provide-plugin/&gt;

改了這個設定檔，記得重開 foreman（或 rails server）才會生效。這樣一來即使沒有寫 `import &#39;jquery&#39;`，也可以直接使用 `$` 符號了。

順利引入 jQuery 之後，讓我們繼續來 View（也就是 HTML 的頁面）寫一小段 jQuery：

```html
// 檔案：app/views/pages/home.html.erb

&lt;h1&gt;Cute App&lt;/h1&gt;
&lt;p&gt;You&#39;re too smart to understand my code!&lt;/p&gt;

&lt;script&gt;
  $().ready(function(){
    console.log(&#39;Make jQuery AWESOME Again&#39;)
  })
&lt;/script&gt;
```

這應該也是很常見的寫法，就是直接在 HTML 頁面裡寫 JavaScript，我們暫且先不討論在 2019 年還這樣寫是不是適當，但重新整理之後，發現...咦？怎麼發生錯誤了：

    Uncaught ReferenceError: $ is not defined

又是 `not defined`！不是 jQuery 有安裝了嗎？而且剛剛在 `app/javascript/ccc/smile.js` 裡寫的時候在 Webpack 編譯的過程也沒事？為什麼這個 `$` 又會沒定義？

#### Step 3: 設定 `$` 符號

原本我也以為天真的以為這些 JavaScript 套件就只要 import 進來就沒事了，但看起來沒這麼單純。

前端世界真複雜 Orz

原來，的確是引入了 jQuery 沒錯，但那個 `$` 符號並不會就這樣生效，如果去翻一下 jQuery 原始碼的最後幾行是這樣寫的：

```javascript
    if ( !noGlobal ) {
      window.jQuery = window.$ = jQuery;
    }
```

資料來源：&lt;https://code.jquery.com/jquery-3.4.1.js&gt;

簡單的說，就是如果使用 Webpack 打包的情境下是沒有瀏覽器的 `window` 物件的，所以自然就沒有 `$` 或是 `jQuery` 這幾個全域變數，所以在執行到上面那段程式碼的時候才會出現 `$ is not defined` 的錯誤訊息。

那該怎麼辦？即然它沒有做 `$`，那我們就自己手動自己做。

```javascript
    // 檔案：app/javascript/packs/application.js

    import &#39;../ccc&#39;

    window.jQuery = $
    window.$ = $
```

剛剛在前面透過 Webpack 的 `ProvidePlugin` 把 `$` 變全域變數，在這邊再手動把它塞給 `window`，讓它在 View 也可以正常使用，這樣剛剛的 `$ not defined` 的問題就可以解決了。

只是，在前端越來越複雜的情況下，應該也越來越少人會在 HTML 裡塞 JavaScript 了才是？不過如果你還是有這個需求的話，上面這樣手動把 `$` 設定到 `window` 物件也是一個解法。

### 今日是何日之 Date Picker

看完了 jQuery，我們來試個漂亮一點的小玩意兒吧！這次來試個可以在網頁上點選日期的小工具：[flatpickr](https://flatpickr.js.org)。

點開網站說明，雖然它可以用 CDN 的方式安裝，但我們就來練習把它一併打包到我們的專案裡試試看。

#### Step 1：安裝 flatpickr

起手式都差不多，都是用 `yarn` 或 `npm` 把套件拉回來：

    $ yarn add --dev flatpickr

這個指令除了把套件拉回 `node_modules` 之外，同樣也會自動修改 `package.json` 的內容。

#### Step 2：串接 JavaScript

根據[手冊](https://flatpickr.js.org/getting-started/)上的記載，它的使用方法是這樣：

```javascript
    import flatpickr from &quot;flatpickr&quot;
```

這行看起來應該有點熟悉吧！接著我在專案的 `app/javascript/` 目錄裡建立一個名為 `utils` 的目錄，並在裡面新增一個名為 `datepicker.js` 的檔案，內容如下：

```javascript
    // 檔案：app/javascript/utils/datepicker.js

    import flatpickr from &#39;flatpickr&#39;

    document.addEventListener(&#39;DOMContentLoaded&#39;, function(){
      flatpickr(&quot;#theCutePicker&quot;, {})
    })
```

這裡為什麼要取名 `utils` 目錄以及 `datepicker.js` 檔案？其實沒什麼特別的原因，就只是不想把程式都寫在同一個檔案裡而已 :)

透過 `import` 語法把 `flatpickr` 套件引入，根據手冊的說明，只要呼叫 `flatpickr` 函數並把指定的 DOM 元件的 id（或 class）名稱傳給它就可以正常運作了，所以，接下來我在頁面上新增一個 id 叫做 `theCutePicker` 的 `input` 元素：

```html
    // 檔案：app/views/pages/home.html.erb

    &lt;h1&gt;Cute App&lt;/h1&gt;
    &lt;p&gt;You&#39;re too smart to understand my code!&lt;/p&gt;

    &lt;input type=&quot;text&quot; id=&quot;theCutePicker&quot;&gt;
```

這時候畫面上除了一個輸入框之外，應該還沒什麼變化，因為雖然剛剛好像在 `utils/datepicker.js` 檔案裡引入了 `flatpickr`，但還沒讓它引進整個專案，所以接下來：

```javascript
    // 檔案：app/javascript/packs/application.js

    import &#39;../ccc&#39;
    import &#39;../utils/datepicker&#39;
```

這時再回到瀏覽器應該會發現畫面有些變化了，但看起來不像是我們期望的效果啊！畫面上出現大大的箭頭、日期跟星期都跑到不對的地方，看了一下 Console 並沒有 JavaScript 的錯誤，應該是 CSS 沒設定的樣子。

是的，就是這樣，一樣再翻了一下手冊的說明，應該還要有一個 `flatpickr.min.css` 需要一併引入。其實這個 CSS 檔也一併在安裝的時候拉回 `node_modules` 裡了：

![flatpickr](/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBZZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--a8214a5e4427f66402dbea398423d00f744ba15d/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUJXa0NBQVE9IiwiZXhwIjpudWxsLCJwdXIiOiJ2YXJpYXRpb24ifX0=--578d6799c87a604ca574298502ba874c9075e929/flatpickr.png)

這個 CSS 檔就放在 `node_modules/flatpickr/dist` 目錄裡，即然知道在哪裡就簡單了！讓我們回到 `datepicker.js` 檔案加上一行：

```javascript
    // 檔案：app/javascript/utils/datepicker.js

    import flatpickr from &#39;flatpickr&#39;
    import &#39;flatpickr/dist/flatpickr.min.css&#39;

    document.addEventListener(&#39;DOMContentLoaded&#39;, function(){
      flatpickr(&quot;#theDatePicker&quot;, {})
    })
```

就這樣把它 import 進來，然後再回到瀏覽器看看：

![Datepickr](/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBZUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--2e5156bb5dd0bafc292d54f49ec939d16fae7bb9/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUJXa0NBQVE9IiwiZXhwIjpudWxsLCJwdXIiOiJ2YXJpYXRpb24ifX0=--578d6799c87a604ca574298502ba874c9075e929/datepicker.png)

搞定，瞧瞧這精美的 Date Picker！ 

等等...好像哪裡怪怪的，為什麼剛剛是在一個 JavaScript 檔案裡面 import 一個 CSS 檔案？可以這樣嗎？

這個嘛，其實對 Webpack 來說，所有的東西都是 JavaScript，而這也是下一篇文章要再說明的內容。

## 小結

基本上在 Rails 裡使用 Webpack 打包，一開始會很不習慣，特別對新手來說更是痛苦，因為這等於是一次要處理兩個生態圈，所以頭暈是正常的，不過我應該慢慢的就會習慣它了（吧）。


