sasaboの生活

フィヨルドブートキャンプ卒業生が30代未経験でWebエンジニアとして生活する様子

WebpackerでJavaScriptを使ってハマった話

これは フィヨルドブートキャンプ Part 2 Advent Calendar 2020の5日目の記事です。 前日はjune29さんの成長の早いジュニア・ソフトウェアエンジニアの特徴でした。 Part1もありますので是非ご覧ください。

フィヨルドブートキャンプ Part 1 Advent Calendar 2020

自己紹介

はじめましての方もいらっしゃると思いますので改めて自己紹介しておきます。sasaboといいます。 今年の3月からフィヨルドブートキャンプというプログラミングスクールに通い10月より受託開発を行う会社にWebエンジニアとして就職しました。 今のところ元気よくエンジニア生活を送っています。

このブログは実務未経験の元自動車会社勤務のサラリーマンがエンジニアとして生活する様子を綴ったブログです。 いつもは技術的なことは書いておらず 「本当にこいつはエンジニアなのか?」とそろそろ思われてそうなので、 今日は会社の研修で困ったことを題材に技術的な話をします。

コピーボタンを作ろう

研修でRailsアプリを作成していたときのことです。 あるイベントの実施日時を入力するときに、あらかじめ入力していた予定日時をコピーできたら楽だなーと思いました。

私🐤「予定日時をコピーするボタンがあったら便利ですかね?」

メンター様👨「あ〜便利かもですね」

🐤「作ってみてもOKですか?」

👨「OKです〜 」

的な感じで、すでに入力済みの日時をコピーして別のテキストボックスに貼り付けるボタンを作ることにしました。

ここでは簡略化してテキストボックスの入力内容をコピーして表示するボタンとしましょう。

作り方

次はどうやって実現するかですね。 私はJavaScriptを使えば簡単にできると思いました。 ググるJavaScript テキストボックスの値を取得/設定するという記事をみつけました。 この通りにやればできそうだなと思いました。 そう、このときまでは…

さて、ここからは実際に作ってみましょう。 もし再現してみたい方がいたら簡単に手順を書くので一緒にやってみましょう。 ポイントとしてタイトルの通りwebpackerを使うのがミソなので 下記のやり方でやる場合はRails6を使って下さい。

準備

まずはrails new

% rails new copy_button

ボタンを表示させるTopページを作りましょう。

Topコントローラを作成します。

% rails g controller Top top

Topページを表示させるためにconfig/routes.rbを修正します。

Rails.application.routes.draw do
  get 'top/top'

  root 'top#top'
end

JavaScriptファイルを読み込む

次にビューにJavaScriptを読み込めるようにしましょう。 Webpackerはapp/javascript/packs配下のファイルをjavascript_pack_tag '<pack名>'で読み込みできるようになっています。

ビューはこんな感じに書きました。 app/views/top/top.html.erb

<%= javascript_pack_tag 'button' %>

<div class="original-text">
  <p>コピー元テキスト</p>
  <input type="text" id="original_text" value="フィヨルドブートキャンプ大好き">
</div>

<div class="by-onclick">
  <p>View側でonclickを使ってJSの関数でコピー</p>
  <input type="button" value="コピー" onclick="copyButton()">
  <span id="copied_text_by_onclick"></span>
</div>

JavaScriptはこんな感じに書きました。

app/javascript/packs/button.js

function copyButton(){
  const copiedText = document.getElementById("original_text").value;
  document.getElementById("copied_text_by_onclick").textContent = copiedText;
}

ビューのonclickボタンを押すとJavaScriptの関数copyButton()を呼び出します。 copyButton()はコピー元のテキストをコピーしてidが"copied_text_by_onclick"の要素に値をセットするので <span id="copied_text_by_onclick"></span>の箇所にコピーした値が表示されるはずです。

完璧です。動かしてみましょう!

こいつ、動かないぞ!

上記のコードを実行した様子です。 f:id:sasabo-h:20201205102456g:plain

何も変化しません。 アムロもがっくしな展開ですね。 デベロッパーツールをみるとcopyText関数が参照できないとのこと。うーん、まいった。

(index):23 Uncaught ReferenceError: copyText is not defined

誰か助けてください!

私はRailsとVue.jsを組み合わせたアプリケーションを作ったことはあるのですが Railsと素のJavaScriptを組み合わせるのは初めてでした。 なので最初は自分の書き方が何かおかしいのだと思って色々やってみましたが解決できませんでした。 そこでメンター様に相談しました。

🐤「copyTextが参照できないと起こられます。何か間違っているんでしょうか?」

👨「なるほど。とりあえずconsole.logしこんでJSファイルが読み込みできているか見てみましょう。 」

さすがメンター様。冷静な分析です。 ということでconsole.log("世界の中心で愛をさけぶ")をしこみます。

app/javascript/packs/button.js

console.log("世界の中心で愛をさけぶ")

function copyText(){
  const copiedText = document.getElementById("original_text").value;
  document.getElementById("copied_text_by_onclick").textContent = copiedText;
}

結果はこのとおりコンソールに「世界の中心で愛をさけぶ」が表示できています。 f:id:sasabo-h:20201205102807p:plain

👨「読み込みはできていますね。ではcopyText()の中にこんどはconsole.logをいれてみましょう 」

さすがメンター様。

app/javascript/packs/button.js

console.log("世界の中心で愛をさけぶ")

function copyText(){
  console.log("誰か助けて下さい!")
  const copiedText = document.getElementById("original_text").value;
  document.getElementById("copied_text_by_onclick").textContent = copiedText;
}

結果はこのとおり。予想どおりcopyText()の中のconsole.logは表示されませんでした。 f:id:sasabo-h:20201205102848p:plain

👨「エラーの通り、やっぱり参照できていませんね… 」

🐤「そうですね。JSの関数の書き方が間違っているんでしょうか?」

👨「ぱっと見は大丈夫そうですよ。う〜ん、あ!なんとなく分かりました」

🐤「!」

👨「たぶんグローバル関数として読み込めてないんじゃないでしょうか?」

🐤「なるほど。(分かってない)」

👨「windowを使って明示的にグローバル関数としてcopyText()を宣言してみましょう」

ということでメンター様のご指導のもと、下記のようにコードを書き換えました。

app/javascript/packs/button.js

window.globalcopyText = function(){
  const copiedText = document.getElementById("original_text").value;
  document.getElementById("copied_text_by_onclick_with_gloval_func").textContent = copiedText;
}

ビューも修正します。app/views/top/top.html/erb

<%= javascript_pack_tag 'button' %>

<div class="original-text">
  <p>コピー元テキスト</p>
  <input type="text" id="original_text" value="フィヨルドブートキャンプ大好き">
</div>

<div class="by-onclick-with-global-func">
  <p>View側でonclickを使ってJSで明示的にグローバル関数として宣言した関数でコピー</p>
  <input type="button" value="コピー" onclick="globalcopyText()">
  <span id="copied_text_by_onclick_with_gloval_func"></span>
</div>

結果はこのとおりです。やりました!コピーできました! f:id:sasabo-h:20201205103048g:plain

🐤「あ!できました!」

👨「Webpackerを使うとローカル関数になっちゃうんですね。Sproketsを使えば多分、sasaboさんのコードでもいけると思いますけど」

でもなんでなんだぜ?

ということでメンター様から得たヒントを元に調べるとそのものズバリな記事を見つけました。

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳)

この記事によるとWebpackerはモジュールとしてJSをコンパイルします。 グローバルを汚染しないようにモジュールの中身は無名関数の内部に封じ込められるので モジュールの中で宣言された関数はモジュールの外から関数を呼び出せない ということです。

つまりwebpackerを使うとき、ビューからはJavaScriptで宣言した関数は呼び出せないということになります。

最初に書いたコードはビューからonclickを使ってJavaScript内のcopyButton()を呼び出そうとしていました。 しかし、それはビューからモジュール内の関数を呼び出すことになるのでできなかったということです。

次にwindowを使ってみた場合ですが、windowをつかって関数を宣言すると明示的にグローバル関数として宣言できます。 グローバル関数であればビュー側からも参照できるのでコピーできたということです。

最終的にどうすべきだったか

原理は分かりました。では最終的にどのようなコードを書くのが正解だったのでしょうか。 先の記事によるとWebpackでは、欲しい振る舞いをビューではなくpackでセットアップせよ。とありました。 私は意味が分かりませんでした。記事内の例としてjQueryのコードが載っているのですがjQueryを触ったことがないのでどうすべきか分かりません。

🐤「すみません。jQueryを使わないとどうなりますか?」

素直に分からないので聞く勇気。それを私は持っている! ということで記事を噛み砕いて説明していただき一つの結論にいたりました。

ビューでonclickを使わずにJavaScript内でクリックを検知せよ!

コードに落とし込むと次のようになります。

app/views/top/top.html/erb

<%= javascript_pack_tag 'button' %>

<div class="original-text">
  <p>コピー元テキスト</p>
  <input type="text" id="original_text" value="フィヨルド大好き">
</div>

<div>
  <p>JS側でクリックを検知する関数でコピー</p>
  <input type="button" value="コピー" id="copy-button">
  <span id="copied_text_by_onclick"></span>
</div>

app/javascript/packs/button.js

function copyText(){
  console.log("俺も大好き!")
  const copiedText = document.getElementById("original_text").value;
  document.getElementById("copied_text_by_onclick").textContent = copiedText;
}

document.addEventListener('DOMContentLoaded', () => {
  const button = document.getElementById("copy-button")
  button.addEventListener("click", () => { copyText() })
})

結果はこの通り呼び出せています。今度こそ完璧です!! f:id:sasabo-h:20201205103605g:plain

ポイントはビューでonclickを使わずに button.addEventListener("click", () => { copyText() }) のようにaddEventListenerをつかってcopyText()を呼び出したことです。

まとめ

今回の現象を絵にするとこんな感じ(copy()はcopyText()に置き換えて見てね)

❌Viewでボタン押下検出 f:id:sasabo-h:20201205103729p:plain ⭕️JavaScriptでボタン押下検出 f:id:sasabo-h:20201205103747p:plain

最終的なまとめ

  • webpackerを使うときはビューからJavaScript内の関数は呼び出せない
  • なにか振る舞いを書きたいときはJavaScript内に書こう

最後に

いかがでしたか?(1度言ってみたかったw) ちゃんと私、研修で学んでいたでしょう?(笑) そんな研修も終わり実案件に12月から入っております。 実案件についてのあれこれについてはまた今度書くのでみていただけると幸いです。

今日は長文となりましたが見て下さりありがとうございました。

PS. 私はガンダムセカチューも見たことがありません。