レスポンシブなJavaScriptを書くならmatchMediaが便利

2018年2月10日

ウェブサイトをレスポンシブにするときよく使われる方法が、CSSやJavaScriptのメディアクエリーを使って画面サイズまたはデバイスの種類を判断してHTMLやCSSの内容を切り替える方法です。

CSSのメディアクエリーを使ったレスポンシブ

たとえばiPad(横幅768px)とスマホはモバイル向けの表示、それより大きなデバイスはPC向けの表示としたい場合、CSSを次のように記述します。

/* ▼モバイル向けのスタイルをここに記述▼ */
.container { width: 100%; }
・・・
/* ▲モバイル向けのスタイルをここに記述▲ */
@media (min-width: 769px) {
	/* ▼PC向けのスタイルをここに記述▼ */
	.container { width: 1280px; }
	・・・
	/* ▲PC向けのスタイルをここに記述▲ */
}

ごく一般的に使われている、CSSによるレスポンシブです。

モバイル向けのスタイルとPC向けのスタイルのどちらを先に(上に)記述するかは、サイトのターゲットをモバイルとPCのどちらを重要視するかによります。

iframeの埋め込みコンテンツのレスポンシブ

しかし、CSSだけでなくHTMLの構造まで切り替えなければならないような場面では、JavaScriptを使う必要があります。

たとえばYouTubeの動画をレスポンシブに対応させるには、PCとスマホでそれぞれ次のようなHTML構造にします。

まず、YouTubeのサイトで埋め込み用のコードをコピーします。

YouTubeのサイトから埋め込みコードをコピーする
そして、HTMLに貼り付けます。

<iframe width="560" height="315" src="https://www.youtube.com/embed/iTQGWlbUrCM" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>

こうすると、ブラウザには横560px縦315pxの表示エリアが確保されるので、PCではこのサイズで表示されます。

PCで表示したレスポンシブな動画

しかし、一般的なスマホの画面幅は320~400pxぐらいなので、スマホで見ると動画のフレームが画面に入りきらず、右端が切れてしまいます。

スマホで表示した動画

この問題を解決するには、iframe タグの外側をさらにタグで囲みます。

<div class="embed-responsive">
	<iframe width="560" height="315" src="https://www.youtube.com/embed/iTQGWlbUrCM" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
</div>

そして、次のようなCSSを適用します。

.embed-responsive {
    position: relative;
    display: block;
    height: 0;
    padding-top: 56.25%;
    overflow: hidden;
}
.embed-responsive iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

こうすると、iframeは横幅に対する縦幅の比率(=アスペクト比)が56.25%、つまり元の「315px:560px」と同じ比率を維持したまま、スマホの画面幅に合わせて自動的に伸縮してくれます。

このテクニックも広く知られていて、paddingの上下をパーセントで指定すると、その要素自身の高さを100とする割合を表すという性質を利用しています。一方、heightをパーセントで指定すると、その要素の親要素の高さを100とする割合になってしまうので、レスポンシブな埋め込みには使えません。

さて、上記のHTMLとCSSをスマホの場合だけ適用するには、ブラウザのサイズを広げたり縮めたりするのに応じて、自動的にHTMLの構造を変化させなければなりません。CSSではHTMLの構造を変更できないので、JavaScriptを使うことになります。

HTMLにアクセスするには、純粋なJavaScriptよりもjQueryを使うほうが簡潔に記述できるので、以下ではjQueryの構文で解説します。まず、JavaScriptファイル内に次のような関数(function)を定義しておきます。

$(function(){
	function responsive_iframe(unwrap) {
		var iframes	= $(document).find('iframe[src^="https://www.youtube.com/embed"]');
		var aspect	= 56.25;	//デフォルトのアスペクト比
		var w				= 0;			//iframeのwidth
		var h				= 0;			//iframeのheight
		var wrapper	= '<div class="embed-responsive" style="padding-top:[aspect]%">';
		$(iframes).each(function(){
			//レスポンシブなラッパーで囲む(スマホ向け)
			if (unwrap) {
				w = $(this).attr('width');
				h = $(this).attr('height');
				//widthとheightに基いてアスペクト比を計算
				if (w!=undefined && h!=undefined) {
					aspect = (h/w) * 100;
				}
				//CSSにアスペクト比を反映
				wrapper = wrapper.replace('[aspect]', aspect);
				//CSSを適用
				if(!$(this).parent().hasClass('frame-wrapper')) {
					$(this).wrap(wrapper);
				}
			}
			//ラッパーを削除(PC向け)
			else {
				if($(this).parent().hasClass('embed-responsive')) {
					$(this).unwrap();
				}
			}
		});
	}
});

responsive_iframe()関数は、引数のunwrapにtrueを指定して呼び出されたときはclass=”embed-responsive”が付いたdivでiframeタグの外側を囲み、falseを指定して呼び出されたときは囲みを外すというアイデアを、プログラムの構文に置き換えたものです。

つまり、スマホの画面サイズになったときにresponsive_iframe(true)を実行し、PCの画面サイズになったときにresponsive_iframe(false)を実行してやればよいのです。

そのためには、JavaScriptで画面サイズの変化を検出しなければなりません。

画面サイズの変化は、ブラウザのウィンドウサイズが変化するたびに発生するresize(リサイズ)イベントで検出できます。

$(window).on('resize', function(){

	if (window.matchMedia('(min-width: 769px)') {
		// PCの画面サイズになったのでiframeの外側の囲みを外す
		responsive_iframe(false);
	} else {
		// スマホの画面サイズになったのでiframeの外側を囲む
		responsive_iframe(true);
	}

});

しかし、この方法には欠点があります。

resizeイベントは画面サイズが変化するたびに発生するので、何度も連続でresponsive_iframe()関数が呼び出されてしまいます。

すると、iframeの外側を囲むタグが何重にも挿入されてしまったり、逆に、iframeの外側にあった本来のタグが削除されてしまったりして、表示が崩れてしまいます。

親タグが削除されて表示が崩れた例

一応、この例ではPC向けの表示において、削除すべき親タグかどうかを確認してから削除(unwrap)しています。

if($(this).parent().hasClass('embed-responsive')) {
	$(this).unwrap();
}

【1行目】
iframeタグを囲んでいる親要素が、embed-responsiveというclassを持っているかどうかを確認しています。
【2行目】
iframeタグを囲んでいる親要素を取り外し(unwrap)しています。

スマホ向けの表示においても、親タグが既に存在するかどうかを確認してから囲んで(wrap)しています。

if(!$(this).parent().hasClass('frame-wrapper')) {
	$(this).wrap(wrapper);
}

【1行目】
iframeタグを囲んでいる親要素が、embed-responsiveというclassを持っていないかどうかを確認しています。
【2行目】
iframeタグを囲み(wrap)ます。

しかし、このような考慮をいちいち記述するのは面倒です。

プログラムの関数というのは、決まった役目を実行する部品であるべきです。
関数の外側で行われることにまで推測をめぐらし、予防線を張るのは簡潔なプログラムとは言えません。

また、本来この処理はPCとスマホの画面サイズの境界線をまたいだ瞬間にだけ実行すればよいはずです。
リサイズイベントが発生するたびに実行するのは無駄な処理です。

JavaScriptのmatchMedia()を使ったレスポンシブ

そこで、JavaScriptのmatchMedia()の登場です。

$(function(){
	//ブレークポイントで発火する処理
	var mql = window.matchMedia('screen and (min-width: 769px)');
	function checkBreakPoint(mql) {
		//スマホ向け
		if (!mql.matches) {
			responsive_iframe(true);	//YouTubeのレスポンシブ対応
			//▼追加があればここに記述する
			//▲追加があればここに記述する
		}
		//PC向け
		else {
			responsive_iframe(false);	//YouTubeのレスポンシブ対応
			//▼追加があればここに記述する
			//▲追加があればここに記述する
		}
	}
	// ブレイクポイントの瞬間に発火
	mql.addListener(checkBreakPoint);
	// 初回の実行
	checkBreakPoint(mql);

	//YouTubeのレスポンシブ対応
	function responsive_iframe(unwrap) {
		var iframes	= $(document).find('iframe[src^="https://www.youtube.com/embed"]');
		var aspect	= 56.25;	//デフォルトのアスペクト比
		var w				= 0;			//iframeのwidth
		var h				= 0;			//iframeのheight
		var wrapper	= '<div class="embed-responsive" style="padding-top:[aspect]%">';
		$(iframes).each(function(){
			//レスポンシブなラッパーで囲む(スマホ向け)
			if (unwrap) {
				w = $(this).attr('width');
				h = $(this).attr('height');
				//widthとheightに基いてアスペクト比を計算
				if (w!=undefined && h!=undefined) {
					aspect = (h/w) * 100;
				}
				//CSSにアスペクト比を反映
				wrapper = wrapper.replace('[aspect]', aspect);
				//CSSを適用
				if(!$(this).parent().hasClass('frame-wrapper')) {
					$(this).wrap(wrapper);
				}
			}
			//ラッパーを削除(PC向け)
			else {
				if($(this).parent().hasClass('embed-responsive')) {
					$(this).unwrap();
				}
			}
		});
	}
});

【3行目】
matchMedia()は、ブラウザのウィンドウに相当するwindowオブジェクトが持つメソッドで、引数にCSSのメディアクエリーと同等の条件式を記述すると、MediaQueryListというオブジェクトを返します。CSSのメディアクエリーのJavaScript版という理解でよいでしょう。ここではMediaQueryListオブジェクトをmqlという名前の変数に保存しています。あとで何度も使うためです。

【6行目】
MediaQueryListオブジェクトは、指定された条件式にマッチしたかどうかを表すmatchesプロパティを持っています。これを参照すれば、画面サイズが条件にマッチしているかどうかを検出できます。

【19行目】
MediaQueryListオブジェクトは、指定された条件式が「成立⇒不成立」または「不成立⇒成立」に変わったときにだけ通知してくれる機能を持っています。プログラミングにおいて、何らかのタイミングで通知してくれる仕組みを「イベント」と呼び、イベントを通知する仲介役となる関数を「イベントリスナー」と呼びます。

ここでは、MediaQueryListオブジェクトのaddListenerメソッドを使って、checkBreakPoint()という自作関数をイベントリスナーとして登録しています。すると、ブラウザのウィンドウサイズが769pxを超えた瞬間と、下回った瞬間にだけ、checkBreakPoint()関数がブラウザによって自動的に起動されます。

【4行目】
そして、checkBreakPoint()関数にはあらかじめ関数の外側で保存しておいたMediaQueryListオブジェクトを引数に指定すれば、関数の中で「スマホの画面サイズに変わったのか」「PCの画面サイズに変わったのか」をmatchesプロパティによって分岐できます。これで無駄のないレスポンシブなJavaScriptを実行する仕組ができました。

【8-9行目,14-15行目】
追加の処理を行いたいときはここに任意で追加します。長いコードを入れると可読性が低下するので、なるべく関数化したほうがよいでしょう。

参考記事:
window.matchMedia をそろそろ活用してもいい頃
matchMediaのブラウザごとの対応状況