window.matchMedia をそろそろ活用してもいい頃

レスポンシブなページを作っていると、JavaScriptもPC向けとスマホ・タブレット向けで動作を分けたいという事はよくあります。
JavaScriptでレスポンシブ対応させるとしたら以下のような方法があるかと思います。

- `jQuery(window).width()` の値から判定
- `window.innerWidth` の値から判定
- `window.matchMedia` で判定

あと、ここでは Internet Explorer 8 以下に関しては**ないものとして**扱います。

#それぞれの動作を見てみる

それぞれにメリット/デメリットがあるのは世の常です。

##jQuery(window).width() の値から判定

はっきり言うとこの方法はダメです。何がダメかというと、`jQuery(window).width()` は**スクロールバーを含めないウィンドウの横幅**になるからです。
CSSのメディアクエリの `min-width` や `max-width` は**スクロールバーも含めたウィンドウの横幅**で判定しているので十数ピクセルの差が生まれてしまい、思うように動かない原因になります。なので、メリットはありません。

ちなみに、jQuery v3.0.0以降では `jQuery(window).outerWidth()` で `window.innerWidth` と同じ値を取得できます。

##window.innerWidth の値から判定

`window.innerWidth` はスクロールバーを含むウィンドウの横幅を取得できるプロパティです。高さなら `window.innerHeight`。

これならCSSと切替タイミングが一致するので期待通りの動作を行えます。（あえてjQueryを使わずに書いています）

```js:JavaScript
document.addEventListener('DOMContentLoaded', function () {
  function checkBreakPoint() {
    var width = window.innerWidth;

    if (width <= 780) {
      // スマホ向け
    } else {
      // PC向け
    }
  }

  // リサイズの監視
  window.addEventListener('resize', checkBreakPoint);

  // 初回チェック
  checkBreakPoint();
});
```

####メリット

- ほとんどのブラウザが対応している。
- 数値をみるだけなのでとても単純。

####デメリット

- スマホの場合頻繁に `onresize` が実行される可能性があるので、端末のレスポンスに影響が出る可能性がある。
- ブレイクポイントで切り替わる時に一度だけ実行したい処理が何度も実行される可能性がある。

###スマホに於いての onresize

iPhoneやAndroidのブラウザを使っていると、スクロール時に上部や下部でアドレスバーや操作バーが現れたり隠れたりしますよね。
その時に `onresize` イベントが毎回発火してしまうので、不必要な実行が繰り返されてしまう可能性があります。

一応その対処として実行を間引くという意味で `window.requestAnimationFrame` を使う方法などがあります。

```js:JavaScript
document.addEventListener('DOMContentLoaded', function () {
  // 対応してないIE9では setTimeout で代用
  var
    requestAnimationFrame = window.requestAnimationFrame || function (callback) {
      return setTimeout(callback, 1000 / 60);
    },
    cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout,
    requestTimer = null;

  function checkBreakPoint() {
    var width = window.innerWidth;

    if (width <= 780) {
      // スマホ向け
    } else {
      // PC向け
    }
  }

  window.addEventListener('resize', function () {
    cancelAnimationFrame(requestTimer);
    requestTimer = requestAnimationFrame(checkBreakPoint);
  });

  checkBreakPoint();
});
```

###ブレイクポイントで切り替わる時に一度だけ実行させたい

適当なフラグで管理すればいけないこともありません。

```js:JavaScript
document.addEventListener('DOMContentLoaded', function () {
  var
    requestAnimationFrame = window.requestAnimationFrame || function (callback) {
      return setTimeout(callback, 1000 / 60);
    },
    cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout,
    requestTimer = null,
    isSP = false;

  function checkBreakPoint() {
    var width = window.innerWidth;

    if (width <= 780) {
      if (isSP) {
        return;
      }
      isSP = true;

      // スマホ向け
    } else {
      if (!isSP) {
        return;
      }
      isSP = false;

      // PC向け
    }
  }

  window.addEventListener('resize', function () {
    cancelAnimationFrame(requestTimer);
    requestTimer = requestAnimationFrame(checkBreakPoint);
  });

  checkBreakPoint();
});
```

……なんだかややこしくなってきました。

##window.matchMedia で判定

`window.matchMedia` はCSSと同じ指定ができるメソッドです。返り値は `[Object MediaQueryList]` になります。

```js:JavaScript
document.addEventListener('DOMContentLoaded', function () {
  // @media screen and (max-width: 780px) と同じ
  var mql = window.matchMedia('screen and (max-width: 780px)');

  function checkBreakPoint(mql) {
    if (mql.matches) {
      // スマホ向け
    } else {
      // PC向け
    }
  }

  // ブレイクポイントの瞬間に発火
  mql.addListener(checkBreakPoint);

  // 初回チェック
  checkBreakPoint(mql);
});
```

`window.matchMedia` はメディアクエリの条件から判定するだけでなく、`MediaQueryList.addListener()` で「クエリを通知する」事ができます。
クエリを通知する、とは、要するに メディアクエリの条件に合うようになった or 合わなくなった 瞬間に実行するという事です。

####メリット

- CSSと同じ書式で指定することができる。
- 判定方法が簡潔。
- 切り替わる時に一度だけ実行できる。
- `onresize` を使わないので、無駄な関数処理がない。

####デメリット

- Internet Explorer 9 は対応していない。（CSSのメディアクエリは使える）

#おまけ：スマホの向きが変わった時に実行する方法

`window.matchMedia` を使えばCSSと同じくスマホの向きが変わっても簡単に対応できます。

```js:JavaScript
var mql = window.matchMedia('(orientation: portrait)');

function orientationChange(mql) {
  if (mql.matches) {
    // 画面が縦長い時
  } else {
    // 画面が横長い時
  }
}

orientation.addListener(orientationChange);
orientationChange(mql);
```

注意しないといけないのが、`(orientation: portrait)` は単純にウィンドウの縦横比をみているだけなので、PCのブラウザでも縦長いウィンドウにした場合にも適用されてしまいます。
スマホ表示の時のみなど、上手く条件分けしてやらないといけないですね。

#おわりに

`onresize` はスマホで扱うには結構厄介なものなので、個人的には使いたくないイベントですね。
`window.matchMedia` はIE9以下には対応していないのですが、IEでスマホ向け表示をすることはウィンドウ幅を狭くしない限り通常ないと思うので、うまく条件分けしてやればいいかもしれません。