Rails 7: importmap-rails gem README(翻訳)
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の利用を検討できます。
🔗 インストール
importmap for Railsは、Rails 7以降の新規アプリケーションでは自動的に同梱されますが、以下のように既存のアプリケーションにも手動インストールできます。
./bin/bundle add importmap-rails
を実行する./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'
:to
パラメータは、インポート先の論理インポート名を変更する場合にのみ必要です。この:to
オプションを削除する場合は、第1パラメータの直後に:under
オプションを配置しなければなりません。
このように設定することで、以下のように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
ファイルにピンを追加します。このコマンドはJSPM自身の依存関係を解決できるだけでなく、unpkg.comやjsdelivr.comといった他のCDNの依存関係も解決できます。
./bin/importmap pin react
Pinning "react" to vendor/react.js via download from https://ga.jspm.io/npm:react@17.0.2/index.js
Pinning "object-assign" to vendor/object-assign.js via download from https://ga.jspm.io/npm:object-assign@4.1.1/index.js
上を実行すると、config/importmap.rb
ファイルのピン留めは以下のようになります。
pin "react" # https://ga.jspm.io/npm:react@17.0.2/index.js
pin "object-assign" # https://ga.jspm.io/npm:object-assign@4.1.1/index.js
このパッケージはvendor/javascript
ディレクトリにダウンロードされ、Gitなどのバージョン管理にチェックイン可能になるとともに、アプリケーション自身のアセットパイプラインで配信可能になります。
ダウンロードしたピン留めを後で削除するには、以下を実行します。
./bin/importmap unpin react
Unpinning and removing "react"
Unpinning and removing "object-assign"
🔗 ピン留めされたモジュールをプリロードする
ウォーターフォール効果(最も深くネストしたインポートに到達するまでブラウザが次々にファイルを読み込むはめになる現象)を避けるため、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キャッシュレスポンスが返されてしまいます。これを避けるには以下のような感じにします。
class ApplicationController < ActionController::Base
etag { Rails.application.importmap.digest(resolver: helpers) if request.format&.html? }
end
🔗 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.
概要
MITライセンスに基づいて翻訳・公開いたします。
原文は今後も更新される可能性があります。
gem名やコードなどでない一般的な表記については本記事では原則として「importmap」に統一しました。なおscript typeでは
<script type="importmap">
とスペースなしで書きます。