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

Rails 7: import-map-rails gem README(翻訳)

概要

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

rails/importmap-rails - GitHub

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

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

Rails 7: import-map-rails gem README(翻訳)

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

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

現時点ではChrome/Edge 89以降でimport mapがネイティブサポートされており(caniuse.com)、ESMを基本的にサポートしているあらゆるブラウザでESM shimがサポートされています↓。つまり、あらゆる定番ブラウザでアプリが動くようになります。

guybedford/es-module-shims - GitHub

インストール

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

  1. Gemfileにgem 'importmap-rails'を追加してimportmap-railsを使えるようにする
  2. ./bin/bundle installを実行する
  3. ./bin/rails importmap:installを実行する

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

利用法

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

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

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

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

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

Importmap for Railsは、npmパッケージの依存関係をJavaScript CDNで用いるよう設計されています。CDNではプリコンパイルされた配布用バージョンが利用可能になっていて、それらを高速かつ高効率で配信する手段を提供します。

インストール時に追加される./bin/importmapコマンドを用いて、import mapのnpmパッケージをピン留め、ピン留め解除、更新できます。このコマンドではJSPM.orgのAPIを用いてパッケージの依存関係を効率よく解決し、続いてconfig/importmap.rbファイルにピンを追加します。このコマンドはJSPM自身の依存関係を解決できるだけでなく、unpkg.comjsdelivr.comといった他のCDNの依存関係も解決できます。

./bin/importmapコマンドの動作は次のようになります。

./bin/importmap pin react react-dom
Pinning "react" to https://ga.jspm.io/npm:react@17.0.2/index.js
Pinning "react-dom" to https://ga.jspm.io/npm:react-dom@17.0.2/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js
Pinning "scheduler" to https://ga.jspm.io/npm:scheduler@0.20.2/index.js

./bin/importmap json

{
  "imports": {
    "application": "/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js",
    "react": "https://ga.jspm.io/npm:react@17.0.2/index.js",
    "react-dom": "https://ga.jspm.io/npm:react-dom@17.0.2/index.js",
    "object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js",
    "scheduler": "https://ga.jspm.io/npm:scheduler@0.20.2/index.js"
  }
}

ご覧のように、reactとreact-domという2つのパッケージの依存関係が、jspmのデフォルトで解決されるときに合計4つに解決されています。

これで、他のモジュールと同様、これらをapplication.jsのエントリポイントで利用できるようになります。

import React from "react"
import ReactDOM from "react-dom"

特定のバージョンを指定してピン留めすることも可能です。

./bin/importmap pin react@17.0.1
Pinning "react" to https://ga.jspm.io/npm:react@17.0.1/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js

ピン留めの削除も可能です。

./bin/importmap unpin react
Unpinning "react"
Unpinning "object-assign"

既にピン留めされているパッケージをピン留めすると、そのパッケージはその依存関係とともにインラインで更新されます。

ビルドがproduction(デフォルト)とdevelopmentに分かれているパッケージで、以下のようにそのパッケージの環境を制御できます。

./bin/importmap pin react --env development
Pinning "react" to https://ga.jspm.io/npm:react@17.0.2/dev.index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js

また、ピン留めの際に、サポート対象になっている別のCDNプロバイダ(unpkgjsdelivrなど)を指定することも可能です(デフォルトはjspm)。

./bin/importmap pin react --from jsdelivr
Pinning "react" to https://cdn.jsdelivr.net/npm/react@17.0.2/index.js

ただし、ピン留めをあるCDNプロバイダから別のCDNプロバイダに変更する場合は、最初のプロバイダで追加されたが2番目のプロバイダで使われていない依存関係をクリーンアップする必要が生じる可能性がある点にご留意ください。

./bin/importmapを実行するとすべてのオプションが表示されます。

このコマンドは、論理パッケージ名をCDNのURLに解決するための単なる便宜上のラッパーである点にご注意ください。CDNのURLを自分で見つけてそれらをピン留めすることも可能です。たとえば、React用のSkypackを使いたい場合、config/importmap.rbに以下を追加するだけでできます。

pin "react", to: "https://cdn.skypack.dev/react"

JavaScript CDNを使いたくない場合

単にコンパイル済みJavaScriptパッケージをダウンロードして、アプリケーションにローカル保存するオプションは常に利用可能です。これらのファイルをapp/javascript/vendorディレクトリに配置して、以下のようにローカルのpinで参照できます。

# config/importmap.rb
pin "react", to: "vendor/react@17.0.2.js"

ただし、JavaScript CDNの方が高速かつセキュアで、しかも扱いが楽です。最初はJavaScript CDNにしておきましょう。

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

ウォーターフォール効果(最も深くネストしたインポートに到達するまでブラウザが次々にファイルを読み込むはめになる現象)を避けるため、modulepreloadリンクを使います。ピン留めされたモジュールはデフォルトでプリロードされますが、以下のようにpreload: falseを指定すればオフにできます。

サンプル

# config/importmap.rb
pin "@github/hotkey", to: "https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js"
pin "md5", to: "https://cdn.jsdelivr.net/npm/md5@2.3.0/md5.js", preload: false
<!-- app/views/layouts/application.html.erb -->
<%= javascript_importmap_tags %> 

# importmapがセットアップされる前に以下のリンクをインクルードする
<link rel="modulepreload" href="https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js">
...

複数のimport mapを構成する

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

複数のimport mapをRails.application.importmapで記述することで、複数のimport mapを組み合わせられます。たとえば、Railsエンジンで定義されているimport mapを追加するには以下のようにします。

# my_engine/lib/my_engine/engine.rb

module MyEngine
  class Engine < ::Rails::Engine
    # ...
    initializer "my-engine.importmap", after: "importmap" do |app|
      app.importmap.draw(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__)

import mapのダイジェストを自分のETagに含める

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

class ApplicationController < ActionController::Base
  etag { Rails.application.importmap.digest(resolver: helpers) if request.format&.html? }
end

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

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

es-module-shimを利用中の正常なエラー

import mapはChromeとEdgeではネイティブで利用できますが、他のブラウザではshimが必要です。shimを使うブラウザではTypeError: Module specifier, 'application' does not start with "/", "./", or "../".などのJavaScriptコンソールエラーが出力されます。このエラーは正常であり、ユーザーには影響を与えません。

Firefoxでブラウザコンソールを開くと、asm.jsモジュールのlexerのビルドが最適化なしモードで実行されます(デバッガがアタッチされるため)。これによって"asm.js type error: Disabled because no suitable wasm compiler is available"というwarningメッセージが発生しますが、これは期待どおりです。コンソールを再度閉じるとasm.jsの最適化が完全に適用されます。これは、コンソールを開いてabout:configでデバッガを無効にし、ページを再起動することでも確認できます。

shimをオフにする

CIでchromedriverを使ってシステムテストを実行するような特定の環境(リソースに制約があったり特定のケースでエラーになったりする可能性がある環境)では、shimのインクルードを明示的にオフにしたいことがあります。これを行うには、バルクタグヘルパーをjavascript_importmap_tags("application", shim: false)で呼び出します。これで、shim: !ENV["CI"]のような感じで渡せるようになります。必要であれば、フルページキャッシュを行っていないことを確かめた上で、このディレクティブを( useragentなどのgemを利用して)ユーザーエージェントチェックに接続することで、ブラウザがChrome/Edge 89以降かどうかをチェックすることも可能ですが、shimはネイティブの互換ドライバと調和して動くよう設計されているので、実際にはその必要はありません。

ライセンス

Importmap for Rails is released under the MIT License.

関連記事

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


CONTACT

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