Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: importmap-rails gem README(翻訳)

概要

MITライセンスに基づいて翻訳・公開いたします。

原文は今後も更新される可能性があります。

gem名やコードなどでない一般的な表記については本記事では原則として「importmap」に統一しました。なおscript typeでは<script type="importmap">とスペースなしで書きます。

Rails: importmap-rails gem README(翻訳)

rails/importmap-rails - GitHub

importmapは、バージョン管理下のファイルやダイジェストされたファイルに対応する論理名を用いて、JavaScriptモジュールをブラウザで直接インポートできます。これによって、トランスパイルやバンドルを必要とせずに、ESモジュール(ESM)向けJavaScriptライブラリを用いてモダンなJavaScriptアプリケーションを構築できるようになります(参考: Modern web apps without JavaScript bundling or transpiling)。また、これによってWebpack、Yarn、npm、あるいはJavaScriptツールチェインのその他の部分が不要になります。必要なのは、既にRailsにあるアセットパイプラインだけです。

このアプローチでは、1個の巨大なJavaScriptファイルを送信する代わりに、小さなJavaScriptファイルを多数送信します。HTTP/2のおかげで、最初の転送時にパフォーマンス上の重大なペナルティを受けずに済み、しかもキャッシュの動作が軽快になることで長期的にも大きなメリットを得られます。従来は、巨大なバンドルに含まれるJavaScriptファイルのどれかひとつでも更新されるとバンドル全体のキャッシュが無効になりましたが、これによって単一ファイルのキャッシュだけが無効化されるようになります。

現時点ではすべてのモダンな主要ブラウザでimportmapがネイティブサポートされています(caniuse.com)。ネイティブサポートのないレガシーブラウザで動かす必要がある場合は、以下のESM shimの利用を検討できます。

guybedford/es-module-shims - GitHub

🔗 インストール

importmap for Railsは、Rails 7以降の新規アプリケーションでは自動的に同梱されますが、以下のように既存のアプリケーションにも手動インストールできます。

  1. ./bin/bundle add importmap-railsを実行する
  2. ./bin/rails importmap:installを実行する

: RailsフレームワークのAction Cable、Action Text、Active StorageなどでJavaScriptを利用するには、Rails 7以降を使わなければなりません。これは、これらのライブラリのESM互換ビルドが同梱される最初のバージョンです。

これらのライブラリは、以下のようにRailsに含まれているコンパイル済みバージョンに依存させることで、手動でピン留め(pinning)できます。

pin "@rails/actioncable", to: "actioncable.esm.js"
pin "@rails/activestorage", to: "activestorage.esm.js"
pin "@rails/actiontext", to: "actiontext.esm.js"
pin "trix"

🔗 importmapのしくみ

importmapのコアは、本質的に「ベアモジュール指定子(bare module specifier)」と呼ばれる文字列の置き換えです。ベアモジュール指定子はimport React from "react"のような見た目になります。これはESモジュールのローダーの仕様と互換性がないので、ESモジュール互換にするには、以下の3種類の指定子のいずれかを提供する必要があります。

  • 絶対パス:
import React from "/Users/DHH/projects/basecamp/node_modules/react"
  • 相対パス:
import React from "./node_modules/react"
  • HTTPパス:
import React from "https://ga.jspm.io/npm:react@17.0.1/index.js"

importmap-railsは、"react"のようなベアモジュール指定子を、ESモジュールのJavaScriptパッケージを読み込む3つの方法のいずれかに対応付けるクリーンなAPIを提供します。

# config/importmap.rb
pin "react", to: "https://ga.jspm.io/npm:react@17.0.2/index.js"

たとえば上のコードは、import React from "react"を常に以下のようにimport React from "https://ga.jspm.io/npm:react@17.0.2/index.js"に読み替えます。

import React from "react"
// => import React from "https://ga.jspm.io/npm:react@17.0.2/index.js"

🔗 利用法

importmapは、config/importmap.rbファイルの設定を介してRails.application.importmapでセットアップされます。このファイルは、開発中に更新されると自動的に再読み込みされます。ただし、ピン留め(pin)を解除して、レンダリングされたimportmapやプリロードのリストから削除する必要がある場合は、サーバーの再起動が必要である点にご注意ください。

このimportmapは、<%= javascript_importmap_tags %>を用いてアプリケーションレイアウトの<head>でインライン化されます。これにより、<script type="importmap">タグ内でJSON設定がセットアップされます。続いて<script type="module">import "application"</script>でアプリケーションのエントリポイントがインポートされます。applicationはこの論理エントリポイントであり、importmapのscriptタグでapp/javascript/application.jsファイルに対応付けられます。

importmapで定義されたモジュールのいずれかをインポートすることで、app/javascript/application.jsファイルでアプリケーションのセットアップが行われます。ESMの全機能を用いて、モジュールの特定のエクスポートをインポートすることも、すべてをインポートすることもできます。

npmで使われるパッケージ名と一致する論理名を使うようにしておけば、後でコードのトランスパイルやバンドルを使いたくなったときにモジュールのインポートを変更せずに済みます。

🔗 ローカルモジュール

app/javascript/srcや、app/javascriptの他のサブフォルダ(channelsなど)にあるローカルJSモジュールファイルをインポートしたい場合は、それらをピン留め(pin)することでインポート可能にしておく必要があります。

以下のようにpin_all_fromを用いれば、特定のフォルダ内にあるすべてのファイルを選択できるので、個別のモジュールをpinせずに済みます。

# config/importmap.rb
pin_all_from 'app/javascript/src', under: 'src', to: 'src'

# 完全性の自動チェック(高度なセキュリティ)
enable_integrity!
pin_all_from 'app/javascript/controllers', under: 'controllers', integrity: true

:toパラメータは、インポート先の論理インポート名を変更する場合にのみ必要です。この:toオプションを削除する場合は、第1パラメータの直後に:underオプションを配置しなければなりません。

enable_integrity!を呼び出すことで、完全性の算出がグローバルで有効になり、integrity: trueによってディレクトリ内のすべてのファイルで完全性ハッシュが算出されます。これにより、手動でハッシュを管理せずにセキュリティ上のメリットを得られます。

このように設定することで、以下のようにimport可能になります。

// app/javascript/application.js
import { ExampleFunction } from 'src/example_function'

この場合、app/javascript/src/example_function.jsにある関数がインポートされます。

: アセットの配信に使われてきたSprocketsはファイル名のダイジェストを必要とせず、app/javascriptsフォルダ内のファイルを論理相対パスで探索できるので、ローカルファイルをimportmapでピン留めする必要はありませんでした。しかしPropshaftの場合はこのフォールバックがないため、Propshaftを使う場合は明示的にローカルモジュールをimportmapでピン留めする必要があります。

🔗 npmパッケージをJavaScript CDN経由で利用する

Importmap for Railsは、npmパッケージの依存関係をJavaScript CDN経由でダウンロードします。JavaScript CDNではプリコンパイルされた配布用バージョンが利用可能です。

インストール時に追加される./bin/importmapコマンドを用いて、importmapのnpmパッケージのピン留め、ピン留め解除、更新が行えます。このコマンドは、デフォルトでJSPM.orgのAPIを用いてパッケージの依存関係を効率よく解決し、続いてconfig/importmap.rbファイルにピンを追加します。

./bin/importmap pin react
Pinning "react" to vendor/javascript/react.js via download from https://ga.jspm.io/npm:react@19.1.0/index.js

上を実行すると、config/importmap.rbファイルのピン留めは以下のようになります。

pin "react" # @19.1.0

--fromオプションを使うことで、unpkg.comjsdelivr.comなどの別のCDNも指定できます。

./bin/importmap pin react --from unpkg
Pinning "react" to vendor/javascript/react.js via download from https://unpkg.com/react@19.1.0/index.js
./bin/importmap pin react --from jsdelivr
Pinning "react" to vendor/javascript/react.js via download from https://cdn.jsdelivr.net/npm/react@19.1.0/index.js

このパッケージはvendor/javascriptディレクトリにダウンロードされ、Gitなどのバージョン管理にチェックイン可能になるとともに、アプリケーション自身のアセットパイプラインで配信可能になります。

ダウンロードしたピン留めを後で削除するには、以下を実行します。

./bin/importmap unpin react
Unpinning and removing "react"

🔗 サブリソース完全性(SRI)

importmap-railsでは、高度なセキュリティのために、外部CDNから読み込まれるパッケージのサブリソース完全性(SRI: Subresource Integrity)チェックをサポートしています。

🔗 ローカルアセットに対する自動完全性チェック

Railsのアセットパイプラインで配信されるローカルアセットの完全性ハッシュ自動算出を有効にするには、必ずimportmapの設定ファイル内で以下のようにenable_integrity!を最初に呼び出さなければなりません。

# config/importmap.rb

# 完全性の算出をグローバルに有効にする
enable_integrity!

# 完全性チェックを有効にすると、以下の完全性ハッシュが自動算出されるようになる
pin "application"                                               # 完全性ハッシュは自動算出される
pin "admin", to: "admin.js"                                     # 完全性ハッシュは自動算出される
pin_all_from "app/javascript/controllers", under: "controllers" # 完全性ハッシュは自動算出される

# 両方を併用する場合(明示的に完全性を制御する)
pin "cdn_package", integrity: "sha384-abc123..." # 完全性ハッシュは自動算出される
pin "no_integrity_package", integrity: false     # 完全性ハッシュを明示的に無効にする
pin "nil_integrity_package", integrity: nil      # 完全性ハッシュを明示的に無効にする

これは、特に以下の場合に有用です。

  • Railsのアセットパイプラインで管理されているローカルのJavaScriptファイル
  • pin_all_from一括操作する場合(完全性ハッシュを手動更新すると煩雑になる)
  • アセットが頻繁に更新されるdevelopment環境のワークフロー

注意: 完全性の算出はオプトインであり、利用のためにはenable_integrity!を必ず有効にしておく必要があります。この振る舞いは、個別のピン留めに対してintegrity: falseまたはintegrity: nilを設定することで無効にできます。

Propshaftユーザー向けの重要な注意事項: サブリソース完全性をサポートするには、Propshaft 1.2以降が必要です。さらに、完全性ハッシュのアルゴリズムをアプリケーションの設定ファイルで指定する必要があります。

# config/application.rb、またはconfig/environments/*.rb
config.assets.integrity_hash_algorithm = 'sha256'  # または'sha384'、'sha512'

Propshaftでは、この設定を行わないと完全性チェックがデフォルトで無効になります。Sproketsでは、完全性チェックが無設定で利用可能です。

enable_integrity!integrity: trueの出力例:

{
  "imports": {
    "application": "/assets/application-abc123.js",
    "controllers/hello_controller": "/assets/controllers/hello_controller-def456.js"
  },
  "integrity": {
    "/assets/application-abc123.js": "sha256-xyz789...",
    "/assets/controllers/hello_controller-def456.js": "sha256-uvw012..."
  }
}

🔗 完全性チェックのしくみ

完全性チェックは、importmapとモジュールプリロードタグに自動的に含まれます。

importmapのJSON:

{
  "imports": {
    "lodash": "https://ga.jspm.io/npm:lodash@4.17.21/lodash.js",
    "application": "/assets/application-abc123.js",
    "controllers/hello_controller": "/assets/controllers/hello_controller-def456.js"
  },
  "integrity": {
    "https://ga.jspm.io/npm:lodash@4.17.21/lodash.js": "sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF"
    "/assets/application-abc123.js": "sha256-xyz789...",
    "/assets/controllers/hello_controller-def456.js": "sha256-uvw012..."
  }
}

モジュールプリロードタグ:

<link rel="modulepreload" href="https://ga.jspm.io/npm:lodash@4.17.21/lodash.js" integrity="sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF">
<link rel="modulepreload" href="/assets/application-abc123.js" integrity="sha256-xyz789...">
<link rel="modulepreload" href="/assets/controllers/hello_controller-def456.js" integrity="sha256-uvw012...">

最新のブラウザでJavaScriptモジュールを読み込むとこれらの完全性ハッシュを自動的にバリデーションして、CDNファイルが変更されていないことを確認します。

🔗 ピン留めされたモジュールをプリロードする

ウォーターフォール効果(最も深くネストしたインポートに到達するまでブラウザが次々にファイルを読み込むはめになる現象)を避けるため、importmap-railsはデフォルトでリンクのmodulepreloadを利用します。依存関係をオンデマンドで読み込んで効率を高めたいという理由で依存関係をプリロードしたくない場合は、ピン留めされたモジュールでピンにpreload: falseを追加することでプリロードされないようになります。

サンプル

# config/importmap.rb
pin "@github/hotkey", to: "@github--hotkey.js" # ファイルはvendor/javascript/@github--hotkey.jsにある
pin "md5", preload: false # ファイルはvendor/javascript/md5.jsにある
# app/views/layouts/application.html.erb
<%= javascript_importmap_tags %>

# importmapがセットアップされる前に以下のリンクをインクルードする
<link rel="modulepreload" href="/assets/javascript/@github--hotkey.js">
...

以下のようにpreload:オプションに文字列または文字列の配列を渡すことで、特定の依存関係をどのエントリポイントでプリロードするかを指定することも可能です。

# config/importmap.rb
pin "@github/hotkey", to: "@github--hotkey.js", preload: 'application'
pin "md5", preload: ['application', 'alternate']
<!-- app/views/layouts/application.html.erb -->
<%= javascript_importmap_tags 'alternate' %>

<!-- 上の設定により、以下のリンクがimportmapのセットアップ前に追加されるようになる -->
<link rel="modulepreload" href="/assets/javascript/md5.js">
...

🔗 複数のimportmapを構成する

Railsは、デフォルトでimportmapの定義をアプリケーションのconfig/importmap.rbファイルから、Rails.application.importmapにあるImportmap::Mapオブジェクトに読み込みます。

Rails.application.config.importmap.pathsコンフィグにimportmap設定へのパスを追加することで、複数のimportmapを結合できます。たとえば、Railsエンジンで定義されているimportmapを追加するには以下のようにします。

# my_engine/lib/my_engine/engine.rb

module MyEngine
  class Engine < ::Rails::Engine
    # ...
    initializer "my-engine.importmap", before: "importmap" do |app|
      app.config.importmap.paths << Engine.root.join("config/importmap.rb")
      # ...
    end
  end
end

続いて、エンジンのJavaScriptモジュールを以下のようにピン留めします。

# my_engine/config/importmap.rb

pin_all_from File.expand_path("../app/assets/javascripts", __dir__)

🔗 特定のモジュールをインポートする

特定のJavaScriptモジュールを特定のページでインポートできます。

app/javascriptディレクトリにカスタムJavaScriptファイルを追加します。

// /app/javascript/checkout.js
// (checkout用のjsファイル)

このJavaScriptファイルをピン留めします。

# config/importmap.rb
# ...(他のピン留めファイル)...
pin "checkout", preload: false

このモジュールを特定のページでインポートします。なお、おそらく以下のように特定のページやパーシャルにcontent_forヘルパーのブロックを置き、それをレイアウトでyieldする形になるでしょう。

<% content_for :head do %>
  <%= javascript_import_module_tag "checkout" %>
<% end %>

重要: このjavascript_import_module_tagヘルパーは、以下のようにjavascript_importmap_tagsより後の位置に置くこと。

<%= javascript_importmap_tags %>
<%= yield(:head) %>

🔗 importmapのダイジェストを自分のETagに含める

stale?fresh_whenなどのRailsヘルパーによって生成されたETagを使っている場合は、importmapのダイジェストをこの計算に含める必要があります。さもないと、JavaScriptアセットが変更されたときにも304 Not Modifiedキャッシュレスポンスが返されてしまいます。これを避けるには以下のようにstale_when_importmap_changesメソッドを使います。

class ApplicationController < ActionController::Base
  stale_when_importmap_changes
end

これで、リクエスト形式がHTMLの場合、importmapのダイジェストがetag計算に追加されるようになります。

🔗 development環境とtest環境のキャッシュクリア

importmapのjsonやmodulepreloadsを生成するために、数百ものアセットを解決する必要が生じることもあります。これには時間がかかるため、これらの操作はキャッシュされます。しかしdevelopment環境やtest環境では、config/importmap.rbファイルとapp/javascript/ディレクトリ以下のファイルの変更を両方とも監視してこのキャッシュをクリアします。この機能は、環境の設定ファイルでconfig.importmap.sweep_cacheにブーリアン値を指定することで制御できます。

app/javascriptディレクトリの外にあるローカルファイルをピン留めする場合は、キャッシュスイーパーの設定に追加するか、それらの外部ファイルの変更時にdevelopmentサーバーを再起動する必要があります。たとえば、Railsエンジンの場合は以下のようにします。

# my_engine/lib/my_engine/engine.rb

module MyEngine
  class Engine < ::Rails::Engine
    # ...
    initializer "my-engine.importmap", before: "importmap" do |app|
      # ...
      app.config.importmap.cache_sweepers << Engine.root.join("app/assets/javascripts")
    end
  end
end

🔗 古いパッケージや脆弱なパッケージがないかどうかをチェックする(2022/06/29追加)

Railsのimportmapは、ピン留めしたパッケージをチェックする2つのコマンドを提供しています。

  • ./bin/importmap outdated: NPMレジストリに新しいバージョンがあるかどうかをチェックする
  • ./bin/importmap audit: NPMレジストリに既知のセキュリティ問題があるかどうかをチェックする

🔗 レガシーブラウザ(iOS 15のSafariなど)のサポート(2024/03/04追加)

2024/01/22にリリースされたiOS 15.8.1などのような、importmapをサポートしないブラウザ(caniuse.com)をサポートしたい場合は、以下のようにjavascript_importmap_tagsの直前にes-module-shimsを挿入します。

<script async src="https://ga.jspm.io/npm:es-module-shims@1.8.2/dist/es-module-shims.js" data-turbo-track="reload"></script>
<%= javascript_importmap_tags %>

🔗 ライセンス

Importmap for Rails is released under the MIT License.

関連記事

Rails 7 Alpha 1、2がリリースされました(リリースノート翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。