[Rails] Rails7のimportmapでGoogleMaps表示

# はじめに

- Rails7で標準装備となっているimportmapを使用しGoogleMapsを表示させます。スクリプトを単体で読み込ませる方法と全体で読み込ませる方法について解説します。
- importmap以外にもJavaScriptについても少々解説するので、"JavaScriptはこれから"という人にも分かりやすい内容になっているかもしれません。

### 実行環境

Ruby 3.2.2
Rails 7.0.4.3

### やりたいこと
[マーカーが配置された Google マップをウェブサイトに追加する](https://developers.google.com/maps/documentation/javascript/adding-a-google-map?hl=ja)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754248/13d2a57b-a12a-54c7-e892-b517f71264ea.png)

# ****TL;DR（お急ぎの方）****

まずはやりたいことを実現可能なコードを掲載し、後のパートで解説いたします。
GoogleMapsのスクリプトを読み込むには大きく以下２つのアプローチがあります。

- ページ全体で読み込む
- 特定ページのみで読み込む


## ページ全体で読み込む

GoogleMapsをページ全体で読み込ませることは少ないと思いますが、他のスクリプトも同じような記述でページ全体で同様のスクリプトを読み込ませることができます。

注意：読み込みはページ全体で行っていますが、実行はGoogleMapsAPIを叩いている特定ページのみです。

### 1. jsファイルをjavascriptフォルダに配置

```javascript:app/javascript/map.js
export function initMap() {
  const tower = { lat: 35.6586, lng: 139.7454 }; // 東京タワーを指定
  const map = new google.maps.Map(document.getElementById("map"), {
    zoom: 18,
    center: tower,
  });
  const marker = new google.maps.Marker({
    position: tower,
    map: map,
  });
}

window.initMap = initMap
```

### 2. importmapにピン留め

```ruby:config/importmap.rb
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"
pin "map" # 追加
```

### 3. Mapを埋め込むHTMLとinitMapを実行するスクリプトを記載

```html:app/views/map/index.html.erb
<h3>My Google Maps Demo</h3>
<div id="map" style="height: 100vh; width: 100%;"></div>
<script
      src="https://maps.googleapis.com/maps/api/js?key=ENV['GOOGLE_MAP_API_KEY']&callback=initMap&v=weekly"
      defer
    ></script>
```

### 4. エントリーポイントで関数読み込み

```javascript:app/javascript/application.js
import "@hotwired/turbo-rails";
import "controllers";
import "map"; // 追加
```
## 特定ページのみで読み込む

GoogleMapsは一般的には特定のページのみで表示させたいニーズが多いかと思います。

### 1. jsファイルをjavascriptフォルダに配置

```javascript:app/javascript/map.js

function initMap() {
  const tower = { lat: 35.6586, lng: 139.7454 }; // 東京タワーを指定
  const map = new google.maps.Map(document.getElementById("map"), {
    zoom: 18,
    center: tower,
  });
  const marker = new google.maps.Marker({
    position: tower,
    map: map,
  });
}
window.initMap = initMap;
```

### 2. importmapにピン留め

```ruby:config/importmap.rb
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"
pin "map" # 追加
```

#### 3. Mapを埋め込むHTMLとmap.jsを個別で読み込むためのヘルパーメソッドを記載

```html:app/views/map/index.html.erb
<% content_for :js do %>
  <%= javascript_import_module_tag "map" %>
<% end %>

<h3>My Google Maps Demo</h3>
<div id="map" style="height: 100vh; width: 100%;"></div>
<script src="https://maps.googleapis.com/maps/api/js?key=ENV['GOOGLE_MAP_API_KEY']&callback=initMap&v=weekly"
      defer></script>
```

### 4. content_forをyeild

```html:app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    略
    <%= javascript_importmap_tags %>
    <%= yield(:js) %> #追加
  </head>
  略
</html>
```

# 解説
importmapについて軽く紹介し、コードを解説します。
## Import Mapsとimportmap
Import Mapsは、そもそもRailsでなくJavaScript ES6で提供している機能で、モジュールのエイリアスとパスを紐づけ（マッピング）ます。これまではモジュールをインポートするためには`import "./map.js";`のようにインポート元のパスを書く必要がありましたが、Import Mapsを利用することによって`import "map";`のように記述することができます。ES6単体ではheadタグ内に以下のscriptを記述することでモジュールをマッピングすることができます。Rails7で導入されたimportmapではこれらのタグを生成するヘルパーメソッドなどを提供しています。

```html
<head>
	<script type="importmap" data-turbo-track="reload">{
	  "imports": {
			略
	    "map": "/assets/map/map-549e7b70a8aee6f70d68a636f7efc4d5cf27b058b36c4a32de8655c7a6a1e054.js",
			略
	  }
	}</script>
	略
	<script type="module">import "application"</script>
	<script type="module">import "map"</script>
</head>
```

## ページ全体で読み込む（解説）

importmapが何者かざっくりわかったところで、前述のコードについて解説していきます。

### 1. jsファイルをjavascriptフォルダに配置

```javascript:app/javascript/map.js
export function initMap() {
	略
}
window.initMap = initMap;
```

`export`と記述することで外部からも`initMap`関数が呼べるようにしています。また、`initMap`関数はインポートしただけではコールバック関数として呼び出すことができないため、一番大きなスコープであるグローバル関数として登録します。

### 2. importmapにピン留め

```ruby:config/importmap.rb
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"
pin "map" # 追加
pin "map", to: "map/map.js" # 相対パスで指定する場合
```

ここではマッピングする内容を一括して記載します。`pin “map”`を追記することでjavascriptディレクトリに配置したmap.jsを`”map”`というエイリアスでインポートできるようになります。`app/javascript/map/map.js`に配置する場合など、他フォルダに存在する場合は相対パスで記載します。

```html:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    略
    <%= javascript_importmap_tags %>
  </head>
  略
</html>

# 生成されたHTML
<head>
	<script type="importmap" data-turbo-track="reload">{
	  "imports": {
			略
	    "map": "/assets/map/map-549e7b70a8aee6f70d68a636f7efc4d5cf27b058b36c4a32de8655c7a6a1e054.js",
			略
	  }
	}</script>
	略
	<script type="module">import "application"</script>
</head>
```

`javascript_importmap_tags`ヘルパーにて生成されたHTMLを確認すると、先ほどマッピングした内容でインポートするscriptタグを生成しています。importmap属性のscript内では、JSON形式で`”map”`というエイリアスでアセット化されたmapスクリプトを参照していることがわかります。また、スクリプト実行にあたり、module属性のscript内でapplication.jsがエントリーポイントとしてインポートされます。

### 3. Mapを埋め込むHTMLとinitMapを実行するスクリプトを記載

```html:app/views/map/index.html.erb
<h3>My Google Maps Demo</h3>
<div id="map" style="height: 100vh; width: 100%;"></div>
<script
      src="https://maps.googleapis.com/maps/api/js?key=ENV['GOOGLE_MAP_API_KEY']&callback=initMap&v=weekly"
      defer
    ></script>
```
scriptタグ内のsrc属性ではAPIキーを環境変数[dotenv](https://github.com/bkeepers/dotenv)経由で取得するようにしています。
また、defer属性をつけることで、HTMLや他のスクリプトの読み込みが完了した後にGoogleMapsAPIを叩きます。このdefer属性がないと、コールバック関数である`initMap`の実行に失敗してしまいます。

### 4. エントリーポイントで関数読み込み

```javascript:app/javascript/application.js
import "@hotwired/turbo-rails";
import "controllers";
import "map"; # 追加
```

`map`スクリプトをインポートします。application.jsでインポートするのは、同ファイルがあらゆるスクリプトを実行するエントリーポイント（起点）となっているからです。

## 特定ページのみ読み込む（解説）

### 1. jsファイルをjavascriptフォルダに配置

```javascript:app/javascript/map.js
function initMap() {
  略
}
window.initMap = initMap;
```

今回は`application.js`など他のスクリプトから参照することはないため、`export`は不要です。

### 2. importmapにピン留め

```ruby:config/importmap.rb
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"
pin "map" # 追加
pin "map", to: "map/map.js" # 相対パスで指定する場合
```

ページ全体で読み込む場合と同様にマッピング内容を設定します。

### 3.  Mapを埋め込むHTMLとmap.jsを個別で読み込むためのヘルパーメソッドを記載

```html:app/views/map/index.html.erb
<% content_for :js do %>
  <%= javascript_import_module_tag "map" %>
<% end %>

<h3>My Google Maps Demo</h3>
<div id="map" style="height: 100vh; width: 100%;"></div>
<script src="https://maps.googleapis.com/maps/api/js?key=ENV['GOOGLE_MAP_API_KEY']&callback=initMap&v=weekly"
      defer></script>

# 生成されたHTML
<head>
	<script type="importmap" data-turbo-track="reload">{
	  "imports": {
			略
	    "map": "/assets/map/map-549e7b70a8aee6f70d68a636f7efc4d5cf27b058b36c4a32de8655c7a6a1e054.js",
			略
	  }
	}</script>
	略
	<script type="module">import "application"</script>
	<script type="module">import "map"</script>
</head>
```

ページ全体で読み込む場合と異なる箇所として`content_for`メソッドを追加しています。引数に任意のシンボルを取り（ここでは:`js`）、ブロックに`javascript_import_module_tag`ヘルパー引数`”map”`を渡すことで、map.jsを読み込むHTMLを作成しています。（正確には後述の`yield`が必要）

### 4. content_forをyeild

```html:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    略
    <%= javascript_importmap_tags %>
    <%= yield(:js) %> #追加
  </head>
  略
</html>
```

`content_for`のコンテンツを呼び出すため`yield`しています。２つの組み合わせにより、該当ページでのみ`<script type="module">import "map"</script>`が生成され、スクリプトを呼ぶことができます。

# さいごに
importmapには他にもCDNを読み込む機能なども提供しています。
以下が参考になりましたので合わせて読むことをおすすめします。

[Rails 7: importmap-rails gem README（翻訳）](https://techracho.bpsinc.jp/hachi8833/2022_06_29/112183)
[Rails 7.0 で標準になった importmap-rails とは何なのか？](https://zenn.dev/takeyuwebinc/articles/996adfac0d58fb)
[Rails 7 でファイルごとに JavaScript を分けて使えるようにする](https://zenn.dev/yama525/articles/8537244b477059)