jQueryのclickがiPhoneで発火しないと思ったらasyncが原因だった話

## はじめに
こんにちは。ハリモグラ（*Tachyglossus aculeatus*）です。
こう見えてカモノハシと同じく原始的な哺乳類の仲間で、よくハリネズミに似ていると言われます。
今回、サイトを作っていたらある問題にハマってしまったので、
誰かが同じ轍を踏まないようにQiitaに投稿することにしました。初投稿です。

## 環境
 - メインPC：Windows 10 Home ver.10.0.18363.1556（Docker Desktop, VSCode）
 - メイン携帯：iOS 14.3 (iPhone 12 Pro Max)
 - サブ携帯：Android 11 (UMIDIGI A9 Pro)

## 結論
 - **目的**：モバイル専用フッターをクリック/タップしたらメニューを表示するようにしたい
 - **事案**：jQueryのClickイベントで発火するプログラムがiPhone実機でのみ動作しない
 - **原因**：asyncによりプラグインを遅延読み込みしていたため、プラグインが定義した関数が読み込めずにJS全体が止まっていた
 - **教訓**：asyncは使わない。せめてdeferを使う。webインスペクタは大事。ある事象を疑うときは、検索する前にそれを内包する上位の事象があればそれを先に検証する。

## 経過
では改めて経緯を説明していきましょう。
私は、スマホ版webサイトのユーザーエクスペリエンス(？)を向上させたいと思い、
**フッターのボタンを押したら横からメニューがシュッっと出てくるやつ**（語彙力）
を実装することにしました。
私は当時javascript（jQuery）はほとんど無学だったので、
初心者用のサイト [^1] を参考にさせてもらいながらコツコツと作りました。

その仕組みは単純で、まずメニュー自体（#nav）はマイナスの座標により見えない部分に配置して、
メニューの表示位置を見える場所に配置し直すクラス（.active）を用意しておきます。
そしてボタンが押されたらクラスの付与状態を切り替える（トグルする）ようにします。
そうすると、CSSの働きによってメニューは基本の位置からクラスの指定位置までシュッと動きます。
これをプログラムで表現すると以下のようになります。

```css:CSS
#nav {
	position: fixed;
	z-index: 2;
	top: 0;
	height: 100vh;
	transition: all cubic-bezier(0.165, 0.84, 0.44, 1) 0.8s;
	overflow: auto;
	right: -100%;
}
#nav .active {
	right: 0;
}
```

```js:JavaScript
$(function(){
	$(".button").click(function(){
        // *1
		$("#nav").toggleClass('active');
	});
});
```
HTMLは省略しますが、buttonがメニューを開閉するボタン、navがメニューです。

CSS3以前の時代はなんかもっと複雑なことをしていた気がしますが、
技術の進歩は素晴らしいもので最近はたったこれだけでスライドメニューを実装できます（すごい:hushed:）
さて、さっそくこれをモバイル環境から確認してみましょう。
手元のスマホで開発中のページを確認したい場合、
コマンドラインから```ipconfig```と叩いて、
出てきたIPアドレスのうちデフォルトゲートウェイが存在するイーサネットのIPアドレスをスマホに入力します。
（PCとスマホが同じネットワークに接続されている必要があります）

PCとAndroidでは無事に動作しました。しかし、iPhoneではボタンを押しても反応がありません。
試しに（*1）の部分に
```js:javascript
alert('動けー');
```
を追記してみても出るはずのアラートが出てこないので、
そもそも
```js:javascript
$(".button").click(function(){
    // ここ
});
```
の部分がiPhoneでのみ発火していないのだという発想に至りました。

そこで、「**jquery click iphone 動かない**」等で検索してみると、次の解決策に行きつきました。

1. clickメソッドでは動かない。```on("click touchstart")```メソッドを使うべし。
2. 親要素に**document**や**body**を指定していると動かない。対象の親要素をピンポイントで指定すべし。
3. クリックする対象に ```cursor: pointer;``` を指定すべし。

いずれも、引用元のサイトだけでなく複数のサイト [^2][^3][^4][^5] で見られた知見です。
また、②に関しては真逆のことを言っているサイトも散見されます。
iOSのバージョンの違いによるものでしょうか。

ということで実際にいろいろやってみましたが、うまく動きません。
①は実装するとPCやAndroidで挙動が不安定になってしまう上にiPhoneでは動かず、
また②と③を実装してみても、うんともすんとも動きません。
そもそも③なんておまじないレベルだと思うのですが、本当に効果があるのでしょうか……。

などと訝しがっても仕方ないので、さんざん悩んだあげく別の角度から検証することにしました。
逆に、どこからならiPhone上でjavascriptが動作するのか。
まずjQueryが動作しているかどうかを確かめるために
```js:javascript
$(document).ready(function(){
    alert("hoge");
});
```
を試してみますが、反応がありません。
なるほどそもそもjQueryが動いていないんだな。
念のため上とまったく同じ動作でjQueryを使わないスニペット、
```js:javascript
window.onload = function(){
    alert("fuga");
};
```
も試したところ、これも動きません。
jQueryどころかjavascriptそのものが動いていない。しかもiPhoneだけ。

これはiPhone上のブラウザでなんらかのFatal Errorが発生しているのだと思い、
どうにかしてそのエラーの内容を見たいと思いました。
それで「**iphone webインスペクタ**」で調べてみると、
iPhone→設定app→Safari→詳細→webインスペクタのトグルをONにすることによって、
パソコン上からwebインスペクタを確認できるとの情報が。
**ただしMacのみ。** な、なんだってー:rolling_eyes:

Windowsでもwebインスペクタを見る方法はないのかと、すがる思いで検索してみると、
ていねいに解説している個人ブログ様 [^6] を見つけました。ありがたや～:pray:
その方法は、まず管理者権限でWindows Powershellを開きます。
scoopをインストールします。scoopはCentOSでいうところのyumです。

```bash:WindowsPowershell
$ Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
$ iwr -useb get.scoop.sh | iex
```
Gitをインストールします。

```bash:WindowsPowershell
$ scoop install git
```
アプリをインストールできるように設定します。

```bash:WindowsPowershell
$ scoop bucket add extras
```
iOS WebKit Debug Proxy [^7] をインストールします。

```bash:WindowsPowershell
$ scoop install ios-webkit-debug-proxy
```
Node.jsをインストールします。

```bash:WindowsPowershell
$ scoop install nodejs
```
npmでRemoteDebug iOS WebKit Adapter [^8] をインストールします。

```bash:WindowsPowershell
$ npm install remotedebug-ios-webkit-adapter -g
```
PCにiPhoneを有線接続し、下記コマンドでアダプターを起動します（**停止するときはCtrl+C**）。
警告が出てきたらすべて許可を選択します。

```bash:WindowsPowershell
$ remotedebug_ios_webkit_adapter --port=9000
```
Google Chromeを開き、 [chrome://inspect/#devices](chrome://inspect/#devices) にアクセスします。
Discover network targetsの隣にある **Configure...** をクリックします。
```localhost:9000``` を追加して **Done** をクリックします。（設定ページは開いたまま）
iPhone上のSafariで確認したいサイトを開きます。
するとGoogle Chromeで開いている設定ページの「 **Remote Target** 」に表示しているサイト名が表示されます。
クリックするとそのサイトのリモートインスペクタが起動します。
iPhone上のSafariで一度ページをリロードします。

エラーが出てきた！:laughing:
なになに………
```
Uncaught TypeError: $(...).slidein is not a function
```
**$().slideinは関数じゃないよ** と言っていますね。そんなのjQueryにあったっけ？
検索してみると2012年に作られた古いjQuery専用のプラグインがヒットしました。
自分のサイトに戻ってHTMLのヘッダーを確認してみると、確かにそのプラグインをロードしている。
こういったプラグインは、 **jQueryより後に読み込まないと動作しません。**

```html:HTML
<head>
	<script type="text/javascript" src="./js/jquery.slidein.js" acync></script>
</head>
```
ん？:flushed:
よく見ると「**async**」というオプションがくっついてる。
これは確か、遅延読み込み（非同期ローディング）のためのオプションだったはず。
はっ。もしかして、jQueryより先にプラグインを読み込んでいるため関数が迷子になっているのでは！？
**事件は現場で起きているんじゃない！ ヘッダーで起きているんだ！**

ということで**asyncを取ったらあっさりiPhoneでも動きました**。やったー:joy:
asyncは、ページの読み込みとは別に遅延して外部ファイルを読み込むオプションですが、
その際に読み込むファイルの順番は考慮されないそうです。[^9] ~~（へー知らなかった）~~
順番に、かつ非同期に読み込みたい場合は**defer**というオプションを使います。
ある外部ファイルに関数が定義されていて、
その関数を別のファイルで使っている場合は読み込む順番も考慮しないといけないわけですね。

そして、AndroidやPCではその辺はうまいこと忖度されて読み込まれていたのでエラーは出ないものの、
iPhoneにおいては狂った順番のままロードしてしまうため、エラーが出ていたと……。
そのせいで初歩的な不具合であるにも関わらず今までずっと気づかずにいました。
~~（いままでiPhone実機で確認していないことがバレちゃったえへへ）~~
普通に検索するとjQueryの記法やiPhoneの仕様に関する知見が出てくるので、
よもやヘッダーに原因があるとは思いもしませんでした。よもやよもや

＊　　＊　　＊

以下は私見というか蛇足です。
最近の検索エンジンは優秀なので、
あいまいなキーワードを入力してもそれっぽい答えにたどり着くことができます。
しかしその「それっぽい答え」が自分にとって都合の良い情報だったりすると、
そもそも検索したキーワードという前提が誤っているかもしれないという可能性に対して盲目になりがちです。
そして「それっぽい答え」が実は役に立たない情報であったとしても、
有用性の無さに気づくのはなかなか容易ではありません。（いわゆる**確証バイアス**）

私は、プログラミングにおける不具合の特定は、
**土の中に埋め込まれた宝物を探すようなもの**だと思っています。
地面全体を原因（宝物）があるかもしれない可能性の総体として捉えたとき、
ある部分だけをピンポイントに掘って宝物を探すのは、あまり効率的とは言えません。
ではどうするべきなのか。

今回の場合は、まずjQueryのclickメソッドがうまく働かないということにまず気づきました。
しかしそのあと、jQueryそのものがうまく働いていないことに気づき、
さらにjavascriptそのものがうまく働いていないことに気づきました。
clickメソッドはjQueryの動作を前提とし、jQueryはjavascriptの動作を前提としているので、
この場合は javascriptがjQueryを内包し、jQueryがclickメソッドを内包しています。

より下位に内包された可能性は、それ自体に問題がある場合にかぎらず、
それを含む上位の可能性にエラーが起きたときにも正常に動かなくなることが間々あります。
しかし仮に上位に原因があったとしたら、下位にいくら原因を求めても解決することはありません。
したがって、より広い面積の可能性から検証していったほうが合理的であると言えます。
可能性を切り分けて、こっちには確実に宝物は無いという確信のもと、
残された可能性をどんどん狭くしていくわけです。
某クイズ番組のフィフティー・フィフティーみたいな感じですね。

今回の案件は、clickメソッドではなく、まずそれを含むjavascriptそのものの動作から疑っていれば
ここまで難航することもなかったと思います。その意味で、とても良い教訓になりました。
生粋のプログラマーにしてみれば当たり前すぎてなんてことのない話なのかもしれませんが、
初学者にはこういった考え方も時短のために大切だったりするのではないでしょうか。

ここまでお読みいただき、ありがとうございました。

[^1]: [動くwebデザインアイデア帳](https://coco-factory.jp/ugokuweb/)
[^2]: [iOS javascript clickイベントが効かないにはまる。 - かもメモ](https://chaika.hatenablog.com/entry/2017/11/24/090000)
[^3]: [【iOS】実機のiOSだとクリックイベントが動作しない件でハマった【Safari】 - Qiita](https://qiita.com/nuts_468/items/3f1da63a777d88ba2881)
[^4]: [iOS12以前のmobile Safariではwindow、document、bodyにclickイベントを設定しても発火しない - みかづきブログ・カスタム](https://blog.kimizuka.org/entry/2020/11/16/183108)
[^5]: [iOS（iPhone Safari）でclickイベントが発生しない場合の対処方法 - てらこや.work](https://www.terakoya.work/ios-iphone-safari-click-none-howto/)
[^6]: [WindowsでiOSのSafariで開いたページをデバッグする - Web活](https://webkatu.com/20200509-ios-safari-debug-on-windows/)
[^7]: [A Dev Tools proxy for iOS devices - Github](https://github.com/google/ios-webkit-debug-proxy)
[^8]: [Debug Safari and WebVies on iOS - Github](https://github.com/RemoteDebug/remotedebug-ios-webkit-adapter)
[^9]: [\<script> タグに async / defer を付けた場合のタイミング - Qiita](https://qiita.com/phanect/items/82c85ea4b8f9c373d684)