[baserCMS]公開側をdefer属性に対応させるお手軽な手法


公開側でのdefer属性、よく見かけるようになってきたので、baserCMSの bc_sample テーマで用いた場合にどうなるのか調べてみました。
defer属性についてはこちら。
https://developer.mozilla.org/ja/docs/Web/HTML/Element/script#attr-defer
通常、HTML読込み → js読込み＆実行 となるところ、defer属性を付与することで、
js読込みを非同期に行い、HTML読込み中にjsも読込み、実行自体はHTML読込み完了時に行わせる手法です。

```
<script src="jquery.min.js"></script>
　　　　　　　　　　　　　↓ ↓ ↓
<script src="jquery.min.js" defer="defer"></script>
```

メリットとしては、以下の点が挙げられます。

- 実装が簡単
- 読込みが非同期となることで高速化が期待できる
- 読込みは非同期でも、実行順序は記述順序となる


## 想定している対象者
- baserCMSでウェブサイトを制作をしている方
- HTMLコーディングを主とするフロントエンジニア
- PHPer


## やってみた

js読込み箇所は以下。
app/webroot/theme/bc_sample/Layouts/default.php のhead内。

```変更後
<?php $this->BcBaser->js([
	'jquery-1.11.3.min',
	'jquery-ui-1.11.4.min',
	'jquery.bxslider-4.12.min',
	'jquery.colorbox-1.6.1.min',
	'i18n/ui.datepicker-ja',
	'jquery-accessibleMegaMenu',
	'startup'
], false,['defer' => 'defer']); ?>
```

これによる影響発生を確認できたのは以下。
Chromeとかのdeveloper toolでエラーが出ていることを確認できます。

1. ログイン中に公開側に表示されるツールバーのメニューを開く動作が効かなくなる
2. メールフォームで、入力後の確認画面から入力画面に戻れず送信される動作となる


### 環境
- PHP 5.6.40
- baserCMS 4.4.6


## 原因
defer属性が付与されているjs（この場合はjQueryが該当）の読込みは完了してますが、実行はまだです。
一方、管理ツールバーで利用されているjQuery依存のjsにはdefer属性ナシ、
また、メールフォームで利用されているjQuery依存のjsはhtml内に記述されていることで、
jQueryが未実行でも読込み後に実行されていることで正しく動作しない状態になっていることがうかがえます。


## 対処（※暫定策）
というところで、どうやったら改善できるか試してみたのが以下です。

### 公開側に表示されるツールバーのメニューを開く動作
管理ツールバー用のjsにdefer属性付与。

■ admin-second
lib/Baser/View/Elements/admin/toolbar.php をオーバーライド機構を利用します。
利用中のテーマ内にファイルをコピー -> app/webroot/theme/bc_sample/Elements/admin/toolbar.php

```変更前
$this->BcBaser->js(['admin/vendors/outerClick', 'admin/vendors/jquery.fixedMenu', 'admin/toolbar']);
```

```変更後
$this->BcBaser->js(['admin/vendors/outerClick', 'admin/vendors/jquery.fixedMenu', 'admin/toolbar'], true, ['defer' => 'defer']);
```

■ admin-third
app/webroot/theme/admin-third/Elements/admin/toolbar.php

```変更後
$this->BcBaser->js(['admin/vendors/outerClick', 'admin/vendors/jquery.fixedMenu', 'admin/toolbar']);
```

### メールフォームで、入力後の確認画面から入力画面に戻れず送信される動作となる

ポイントとしては、defer属性は「DOMContentLoaded が発生する前に実行することをブラウザーに示します。」なので、
動かしたいスクリプトに対して、jQueryのdeferが完了していることを見越したタイミングにしてやれば良さ気ということです。

■ app/webroot/theme/bc_sample/Elements/mail_form.php

```変更後
<script type="text/javascript">
window.addEventListener('DOMContentLoaded', function() {
	$(function(){
		$(".form-submit").click(function(){
			var mode = $(this).attr('id').replace('BtnMessage', '');
			$("#MailMessageMode").val(mode);
			return true;
		});
	});
});
</script>
```

■ lib/Baser/Plugin/Mail/View/Elements/mail_token.php をコピー
　 -> app/webroot/theme/bc_sample/Elements/mail_token.php

```変更後
<script type="text/javascript">
window.addEventListener('DOMContentLoaded', function() {
	$(function(){
		$('input[type="submit"]').prop('disabled', true);
	});
});
<?php if($this->request->is('ajax')): ?>
window.addEventListener('DOMContentLoaded', function() {
	$(function(){
<?php else: ?>
window.addEventListener('DOMContentLoaded', function() {
	$(window).on('load', function() {
	<?php endif ?>
		let getTokenUrl = '<?php echo $this->BcBaser->getUrl('/bc_form/ajax_get_token?requestview=false') ?>';
		$.ajaxSetup({cache: false});
		$.get(getTokenUrl, function(result) {
			$('input[name="data[_Token][key]"]').val(result);
			$('input[type="submit"]').removeAttr('disabled');
		});
	});
});
</script>
```

## 手法について
暫定策なので、コア側での正式なdefer属性対応方針が定まったらそちらの手法にしましょう。
もしくはもうちょっと良い手法が出てくると良いですね。

### 参考記事

- https://developer.mozilla.org/ja/docs/Web/HTML/Element/script#attr-defer
- https://developer.mozilla.org/ja/docs/Web/API/Window/DOMContentLoaded_event
- https://qiita.com/phanect/items/82c85ea4b8f9c373d684