鼬~~哩賀,我是寫程式的山姆老弟,昨天跟大家一起用 webpacker 安裝、打包並使用了 fontawesome 和 axios 的 JS 第三方套件,前幾天也有研究了 Asset Pipeline,可以說是把 Rails 歷代的前端工具都試用過了,今天我們來試試看最新 Rails 7 所預設的 importmap,一個不同於 webpacker 的 JS 解決方案
因為 importmap 在 RailsGuide 還沒出文件,我們就先參考 Github 上的資訊,再加上自己做點實驗來驗證概念,夠夠~
確認版本 $ rails —-version
Rails 7.0.4
我實驗用的是 Rails 7.0.4 版
$ rails new test_importmap

Gemfile 安裝的 gem 有幾個新面孔:
...
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
...
sprocket-rails: 就是 Asset Pipeline,之前都是直接埋在 rails 裡,只是這次變成預設選配,也就是你可以選擇把 Asset Pipeline 取消掉,改用別的 CSS / JS 打包方式importmap-rails: 這就是今天的研究重點了,看看跟 webpacker 用起來有什麼不一樣turbo-rails: 目前還沒研究到 turbo,只知道有個 hotwire 跟 turbo 好像有點關係,但不知道差別,之後要研究應該會跟舊版的 turbolinks 一起研究stimulus-rails: 我有偷看了幾位大神在 YT 試用的影片,感覺是個類似 React 或 Vue 的使用方式,目前看起來我覺得沒有很喜歡,不是很直覺,要用這種方式,我乾脆就直接用 React 還比較好維護app/views/layouts/application.html.erb 的 js helper method,從 javascript_pack_tag,變成了 javascript_importmap_tags;而 CSS 依舊是預設由 Asset Pipeline 來處理,所以使用 stylesheet_link_tag
<!DOCTYPE html>
<html>
  <head>
    <title>TestImportmap</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>
app/javascript 資料夾底下沒有 packs 了,改成 controllers,這應該是 stimulus-rails 的結構
外層沒有 package.json 了,改成在 config/importmap.rb
# Pin npm packages by running ./bin/importmap
pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
新增個頁面 $ rails g controller home index
把新頁面變成首頁
# config/routes.rb
Rails.application.routes.draw do
  root 'home#index'
end
$ ./bin/importmap pin bootstrap jquery
Pinning "bootstrap" to https://ga.jspm.io/npm:bootstrap@5.2.1/dist/js/bootstrap.esm.js
Pinning "@popperjs/core" to https://ga.jspm.io/npm:@popperjs/core@2.11.6/lib/index.js
Pinning "jquery" to https://ga.jspm.io/npm:jquery@3.6.1/dist/jquery.js
是直接使用 CDN 的方式安裝,會在 config/importmap.rb 新增以下幾行
# config/importmap.rb
pin "bootstrap", to: "https://ga.jspm.io/npm:bootstrap@5.2.1/dist/js/bootstrap.esm.js"
pin "@popperjs/core", to: "https://ga.jspm.io/npm:@popperjs/core@2.11.6/lib/index.js"
pin "jquery", to: "https://ga.jspm.io/npm:jquery@3.6.1/dist/jquery.js"
在 app/javascript/application.js 引用 bootstrap
// app/javascript/application.js
...
import 'bootstrap'
import 'jquery'
import '@popperjs/core'
啟動 $ rails s,打開 127.0.0.1:3000,檢查一下 header,確認是有引用 bootstrap 和相依套件 popper,這樣就算使用 importmap 來安裝 bootstrap,是不是很簡單啊? 才怪…

剛才完成的只有 bootstrap 的 JS 部分而已,如果要使用 bootstrap 的 CSS 部分,還是要回歸 Asset Pipeline,心裡 OS: bootstrap 最重要的就是 CSS 啊,怎麼還是那麼麻煩!
所以要在 Gemfile 新增 gem 'bootstrap',同時把 sass 放回來,Rails 7 預設是不安裝 sass 的,並執行 $ bundle install
# Gemfile
...
# Use Sass to process CSS
gem "sassc-rails"
# Use Bootstrap
gem 'bootstrap'
為了套用 sass,把 app/assets/stylesheets/application.css 改名為 app/assets/stylesheets/application.scss,並新增 @import 'bootstrap'
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */
 @import 'bootstrap';
在 app/views/home/index.html.erb 新增幾個 bootstrap 元件:nav bar、dropdown button、tooltip button,還有 bootstrap style 的 p tag
<!-- app/views/home/index.html.erb -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" aria-label="Ninth navbar example">
  <div class="container-xl">
    <a class="navbar-brand" href="#">Container XL</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarsExample07XL" aria-controls="navbarsExample07XL" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarsExample07XL">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
        <li class="nav-item">
          <a class="nav-link active" aria-current="page" href="#">Home</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#">Link</a>
        </li>
        <li class="nav-item">
          <a class="nav-link disabled">Disabled</a>
        </li>
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</a>
          <ul class="dropdown-menu">
            <li><a class="dropdown-item" href="#">Action</a></li>
            <li><a class="dropdown-item" href="#">Another action</a></li>
            <li><a class="dropdown-item" href="#">Something else here</a></li>
          </ul>
        </li>
      </ul>
      <form role="search">
        <input class="form-control" type="search" placeholder="Search" aria-label="Search">
      </form>
    </div>
  </div>
</nav>
<h1>Home#index</h1>
<button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="top">Tooltip on top</button>
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
  Dropend button
</button>
<%= content_tag(:div, class: 'container') do %>
    <p class="bg-dark text-light">Find me in app/views/home/index.html.erb</p>
<% end %>
啟動 $ rails s,打開 127.0.0.1:3000,檢查一下,會發現 CSS 部分是正常的,但 JS 的運作有問題,只有 navbar 的展開是正常,其他像是 dropdown、tooltips 都是會報錯的

我後來看了大神的 YT 教學,終於修好了,真的是傻眼,跟我一起看一下怎麼修的吧
在 config/importmap.rb 要修一下,把 @popperjs/core 的 to 的路徑修正
pin "bootstrap", to: "https://ga.jspm.io/npm:bootstrap@5.2.1/dist/js/bootstrap.esm.js"
- pin "@popperjs/core", to: "https://ga.jspm.io/npm:@popperjs/core@2.11.6/lib/index.js"
+ pin "@popperjs/core", to: "https://unpkg.com/@popperjs/core@2.11.6/dist/esm/index.js"
我就問,到底誰會知道要這樣改!!!?
然後重新啟動、重新整理之後, navbar 的 dropdown 就好了,WTF

但下面的 tooltips 和 dropdown button 還是不能正常運作,我不懂,我真的不懂到底是發生什麼事了 Q_Q
恩… 試用 importmap 之後,讓我覺得非常傻眼,這些奇怪的 bug 遇到了都不知道怎麼修,到底誰會知道要改 library 的來源處,總之我應該之後會嘗試用別的方法,我放棄 importmap 了…,我們明天見…
Hi,
剛好最近公司的新專案是使用 Rails 7,可以透過直接下載 bootstrap 的 min.css
並且手動放進 assets/stylesheets,之後就會被 applicaiton.html.erb 內的 stylesheet_link_tag 帶進來。
若是要把 css 整理到別的資料夾內,記得在 config/intializers/asset.rb 內 Rails.application.config.assets.paths << 加入你想要的路徑 ( 通常用會 Rails.root.join 起手 )
因為沒有了 node_mudules 所以確實一開始會有點不習慣,但這種要用什麼裝什麼的感覺其實很讚
感謝分享~ 我之後再多用幾個專案來玩玩看 importmap
tooltips 需要增加 code 啟用
import "@popperjs/core"
document.addEventListener("turbo:load", () => {
  const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
  const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
  const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
  const popoverList = popoverTriggerList.map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
})
至於 dropdown button 我沒遇到問題