Ruby on Railsでgonを美しく使うために〜turbolinksで踏んだバグ〜

# はじめに
Ruby on RailsでRails5まで推奨されていたsprockets環境下での開発でgonを使用する際の注意事項をまとめました。
にしてもRailsは直近フロントエンドのフレームワーク選定が非常に難しくなってきてますね...

https://qiita.com/megane42/items/bf85746b9cefaf38f473

### gonとは？

https://github.com/gazay/gon

Controller内で定義した変数をJSでアクセスできるよう勝手にセットしてくれるものです。
たとえば下記のような例では、Controller内でセットした値をビューとして表示する際にJSが即時実行されてコンソール上にgon.messageとgon.userの内容が表示されます。

Controller
```
class HomeController < ApplicationController
  def index
    gon.message = "Hello from Rails!"
    gon.user = { name: "Taro", email: "taro@example.com" }
  end
end
```
html.erb
```
<%= include_gon %> <!-- `gon` を JavaScript で使うためのヘルパー -->
<h1>Gon Test</h1>
<p>Check the console!</p>

<script>
  console.log("Gon message:", gon.message);
  console.log("Gon user:", gon.user);
</script>

```

# 概要
### バグとの遭遇
クライアント側で動作させたいパーツが多い画面を開発中のこと。
こんなコードを書きました(一部抜粋)
```
  let contentGonManager = {
    default__title: "",
    default__body: "",
    initialize() {
      this.default_title = gon.default_title || "";
      this.default_body = gon.default_body || "";
    },
  };
  //その他DOM操作時の動きをトリガーするfunction等

   $(document).ready(function () {
    contentGonManager.initialize();
    //ここでトリガーするfunctionを書く
  });
  ```

上記、このJSで管理すべき対象のgonを明示する、DOMのレンダリングが完了してからそのgonを受け取ることを保証させたいという意図があります。gonも結局HTML要素に情報ねじ込んでいるだけなので、DOMがちゃんと用意できた後に読み取らせたい。

そこで画面遷移を行うとこんなエラーが
```
Identifier has already been declared
```
あれ...?

### 原因：turbolinksが悪かった
turboinksとは、Rails環境でページ遷移の高速化のために使用されますが、こいつが悪さしていました。
当初、各リクエストごとにHTMLとそのアセットを取得すると思っていました。
```
画面：取得する内容

A画面：A画面のHTML,JS,CSS
B画面：B画面のHTML,JS,CSS
A画面：A画面のHTML,JS,CSS
```
しかしturbolinks環境下ではJS,CSSがキャッシュされHTMLの<body>のみ更新されていたため、JSが同じ実行環境で2回実行されていました。
```
画面：取得する内容

A画面：A画面のHTML,JS,CSS
B画面：B画面のHTML,JS,CSS
A画面：A画面のHTML
```

となるとJSの実行環境はAから引き継ぐため、2回アクセスした時点でブラウザ上のJSの処理は
```
//1回目のアクセス
  let contentGonManager = {
    default__title: "",
    default__body: "",
    initialize() {
      this.default_title = gon.default_title || "";
      this.default_body = gon.default_body || "";
    },
  };
  //その他DOM操作時の動きをトリガーするfunction等

   $(document).ready(function () {
    contentGonManager.initialize();
    //ここでトリガーするfunctionを書く
  });
//2回目のアクセス
  let contentGonManager = {
    default__title: "",
    default__body: "",
    initialize() {
      this.default_title = gon.default_title || "";
      this.default_body = gon.default_body || "";
    },
  };
  //その他DOM操作時の動きをトリガーするfunction等

   $(document).ready(function () {
    contentGonManager.initialize();
    //ここでトリガーするfunctionを書く
  });
  ```
  のようになっていました。
  数珠繋ぎ的に2個実行されてしまってたわけですね。回避策としてはいくつかあります。
  
  ①`turbolinks:load`イベントを使う
  ②グローバル変数を`window`に保存する
  ③turbolinksのキャッシュを無効化する
  ④スコープを1リクエスト単位に限定して多重定義を防ぐ。

  今回は④の選択肢を取りました。
  即時実行関数で閉じ、contentGonManagerが多重定義されないようにしました。
  ```
  $(function () {
  let contentGonManager = {
    default_title: "",
    default_body: "",
    initialize() {
      this.default_title = gon.default_title || "";
      this.default_body = gon.default_body || "";
    },
  };

  // DOM 操作やイベントのトリガー
  function setupTriggers() {
    $("#some-button").on("click", function () {
      console.log("Button clicked!");
      console.log("Default Title:", contentGonManager.default_title);
      console.log("Default Body:", contentGonManager.default_body);
    });
  }

  contentGonManager.initialize();
  setupTriggers();
});
```
こうしてあげると1個目のletと2個目のletは別のスコープとして認識されるのでエラーが解消されます。

# 終わりに
turbolinks使用時にはJS側の変数のスコープ意識してあげないと変なバグを踏むので気をつけたいですね。