かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript OS の判定をしたい

ショートカットの案内を表示するのに Mac, iPhone, iPad なら それ以外は Ctrl を表示したい。という要望でクラアンと側で OS の判定をしたメモ

navigator.userAgent / navigator.platform を使う

MDN のドキュメントには navigator.userAgent, navigator.platform 共に信頼性が低いと書かれてるけど、クライアント側で判定しようとしたら現実的にこれらで判別するしかなさそう。

NavigatorID.userAgent
ユーザーエージェント文字列の検出に基づくブラウザーの識別は信頼性が低く、ユーザーエージェント文字列はユーザーが設定可能なので推奨されません。
cf. NavigatorID.userAgent - Web API | MDN

Mac OS 10.15.7 googleChrome での場合はこんな感じ

const getUserAgent = () => {
  return (navigator && navigator.userAgent) || false;
};

console.log(getUserAgent());
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
// AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36

NavigatorID.platform
ブラウザーのプラットフォームを表す文字列を返します。仕様書ではブラウザーが常に空文字列を返すことを許可していますので、信頼できる答えを得るためにこのプロパティを頼らないようにしてください。
cf. NavigatorID.platform - Web API | MDN

navigator.platform は OS の情報だけ

const getPlatform = () => {
  return (navigator && navigator.platform) || false;
};

console.log(getPlatform());
// MacIntel

OS を判定する

navigator.userAgentnavigator.platform も文字列なので、そこに特定の文字が含まれるかで判定すればいいので、indexOfString.includes を使えばOK
String.includesIE が未対応で polyfill が必要になりそうなので対応ブラウザに IE が含まれるなら indexOf を使うのが良いかも)

navigator.userAgentnavigator.platform とで微妙に含まれる文字が異なるっぽいのでどちらを使うかで別々の判定リストを作成するのが良さそう

navigator.userAgent を使って判定

const OS_LIST = {
  'Windows': 'windows nt',
  'MacOS': 'mac os',
  'iPhone': 'iphone',
  'ipad': 'iPad',
  'Android': 'android',
  'Linux': 'linux',
};

const getUserAgent = () => {
  return (navigator && navigator.userAgent) || false;
};

indexOf を使って判定

const checkIncludeText = (target) => (obj) => {
  let result = false;
  Object.entries(obj).some(([key, value]) => {
    if ( target.indexOf(value) !== -1 ) {
      result = key;
      return true;
    }
    return false;
  });

  return result;
};

const checkOS = () => {
  const ua = getUserAgent();
  let os = 'unknown';
  if (!ua) return os;
  
  return checkIncludeText(OS_LIST)(ua.toLowerCase());
};

checkOS();
// => MacOS

includes を使って判定

オブジェクトを回して判定するメソッドの中身を変えればOK

const checkIncludeText = (target) => (obj) => {
  let result = false;
  Object.entries(obj).some(([key, value]) => {
    if ( target.includes(value) ) {
      result = key;
      return true;
    }
    return false;
  });

  return result;
};

const checkOS = () => {
  const ua = getUserAgent();
  let os = 'unknown';
  if (!ua) return os;
  
  return checkIncludeText(OS_LIST)(ua.toLowerCase());
};

checkOS();
// => MacOS

navigator.platform を使って判定

navigator.platformuserAgent に含まれる文字と微妙に異なるっぽいので、こちらを使って判定する場合は別のリストを作成しておけばOK

const OS_LIST = {
  'MacOS': 'mac',
  'iPhone': 'iphone',
  'ipad': 'iPad',
  'Windows': 'windows',
  'Android': 'android',
  'Linux': 'linux',
};

const getPlatform = () => {
  return (navigator && navigator.platform) || false;
};

indexOf を使って判定

const checkIncludeText = (target) => (obj) => {
  let result = false;
  Object.entries(obj).some(([key, value]) => {
    if ( target.indexOf(value) !== -1 ) {
      result = key;
      return true;
    }
    return false;
  });

  return result;
};

const checkOS = () => {
  const platform = getPlatform();
  let os = 'unknown';
  if (!platform) return os;
  
  return checkIncludeText(OS_LIST)(platform.toLowerCase());
};

checkOS();
// => MacOS

includes を使って判定

const checkIncludeText = (target) => (obj) => {
  let result = false;
  Object.entries(obj).some(([key, value]) => {
    if ( target.includes(value) ) {
      result = key;
      return true;
    }
    return false;
  });

  return result;
};

const checkOS = () => {
  const platform = getPlatform();
  let os = 'unknown';
  if (!platform) return os;
  
  return checkIncludeText(OS_LIST)(platform.toLowerCase());
};

checkOS();
// => MacOS

複数での判定は test を使うのが良さそう

indexOf, includes は単一のテキストが含まれているかでしか判定できないので、Mac || iPhone || iPad のような複数にマッチさせたい場合は正規表現を使える test() を使うのが良さそうです。

e.g. Mac, iPhone, iPad かどうか判定したい

const IS_MAC_OR_IOS_REGEXP = /mac|iphone|ipad/;

const getUserAgent = () => {
  return (navigator && navigator.userAgent) || false;
};

const getPlatform = () => {
  return (navigator && navigator.platform) || false;
};

// userAgent で判定
IS_MAC_OR_IOS_REGEXP.test(getUserAgent());
// => true or false

// platform で判定
IS_MAC_OR_IOS_REGEXP.test(getPlatform());
// => true or false

const isMac = () => {
  return IS_MAC_OR_IOS_REGEXP.test(getPlatform());
}

const save = `${isMac() ? '⌘' : 'Ctrl'} + s`;

example

See the Pen Check OS by userAgent & platform by KIKIKI (@kikiki_kiki) on CodePen.

感想

navigator.userAgentnavigator.platform も変更が可能なので完璧な判定ができるわけではないですが、変更してる人は自分の意志なことが多いだろうしフロントで判別するならこんな感じでいいんじゃないかな〜ってイメージです。ブラウザは無視して OS だけ判定するなら navigator.platform の方がシンプルな文字列が返ってくるので扱いやすそうな印象を持ちました。
単一の判定なら indexOf()includes() を使って複数での判定なら test() を使うのが良さそうです。

まぁ自前で実装せずに判定できる信頼性のあるライブラリ入れちゃうのが簡単で良いと思いますが。(なんでこの記事書いたんでしょうねw)

おわり。


[参考]