[JavaScript] 操作を一定時間しないとタイムアウト扱いにする

Webブラウザとサーバー間で常時、または一定間隔で通信を行っている場合、ユーザーが端末の前から長時間離れた際などに通信を停止したい場合があります。最新の情報をサーバからもらってきても使う人がいなければムダですからね。

サーバ負荷的な面で実装するケースが多い印象ですが、スマホアプリの「クラッシュ・オブ・クラン」では無操作状態が続くと強制的にアプリを再起動せよと表示されます。このゲームの場合はログイン状態のときは他プレイヤーが攻め込めないというルールがあるため、それを悪用されないためだと思われます。

というわけで、今回は何らかの事情で一定の時間ユーザーが何も操作を行わなかったかどうかをクライアント側(Webブラウザ)でかんたんに判定できる方法を試してみます。

基本的な原理

原理は簡単です。 ユーザーが操作を行う度にその時刻を変数などにメモします。定期的にこの時刻をチェックし現在時間と一定秒数以上離れればタイムアウトと判定するというわけです。

具体的なコードにすると以下の通り。

// タイムアウトフラグ
FLAG = false;

// 最終操作時間を入れる
LASTTIME = Date.now();

//--------------------------------------
// イベントに仕込む
//--------------------------------------
// 指定の操作をすると最終操作時間を更新
[
  'click',
  'mousemove'
  // ここに監視するイベントを列挙
]
.forEach(type => {
  window.addEventListener(type, () => {
    LASTTIME = Date.now();
  });
});

//--------------------------------------
// 監視する
//--------------------------------------
// 最終操作時間を1秒おきにチェック
const timerid = setInterval(() => {
  // 10秒以上、無操作か判定
  FLAG = ((Date.now() - LASTTIME) > (10 * 1000));

  // タイムアウト時の処理を書く
  if( FLAG ){
    alert('操作が10秒以上無かったのでタイムアウトしました');
    clearInterval(timerid);
  }
}, 1000);

サンプル

では先ほどのコードにもう少し肉付けした状態で実際にWebブラウザ上で動かしてみます。

実行例

クリックすると新しいウィンドウが開きます。

  • 新規に開いたウィンドウ内で何もしないと約10秒程度で「タイムアウト」判定されます。
  • マウスを動かしたり、スクロールやクリックなどをするとタイムアウトまでの時間が伸びます。

ソースコード

前述の動作原理に肉付けした物です。実際に使うときはクラス化した方が良さそうですね。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf8">
  <title>一定時間無操作でタイムアウト</title>
  <link rel="stylesheet" href="css/style.css">
</head>
<body>

<h1>一定時間無操作でタイムアウトする</h1>

<div id="info">
  <dl>
    <dt>タイムアウト?</dt>
    <dd>
      <span id="istimeout">--</span>
    </dd>
    <dt>残り時間</dt>
    <dd>
      <span id="remain">--</span> msec
    </dd>
  </dl>
</div>

<script>
//----------------------------------------------------
// 定数
//----------------------------------------------------
// タイムアウト時間(ミリ秒)
const TIMEOUT_MSEC = 10 * 1000;   // 10秒経過でタイムアウト

// チェック間隔(ミリ秒)
const INTERVAL_MSEC = 1000;       // 1秒間隔

// チェックするイベント(不要な物は削除)
const WATCHEVENTS = [
  'scroll',       // ウィンドウをスクロール
  'resize',       // ウィンドウサイズを変更
  'click',        // マウスの左ボタンをクリック
  'contextmenu',  // マウスの右ボタンをクリック
  'mousemove',    // マウスポインターを移動
  'wheel',        // マウスのホイールを操作
  'keypress',     // キーボードを押下
  'touchstart',   // タッチ開始(スマホ)
  'touchend',     // タッチ終了(スマホ)
  'touchmove',    // タッチしながら移動(スマホ)
  'touchcancel'   // タッチをキャンセル(スマホ)
];

//----------------------------------------------------
// グローバル変数
//----------------------------------------------------
// タイムアウトフラグ
let TIMEOUT_FLAG = false;   // true:タイムアウト済み, false:アクティブ

// 最終操作時刻
const LAST_OPERATION = {
  type: null,         // 最終イベント名
  time: Date.now()    // 最終イベント発生時刻
};

// デバッグ表示用
const timeout = document.querySelector('#istimeout');   // フラグ表示用
const remain  = document.querySelector('#remain');    // 残り時間


/**
 * 最終操作時刻を更新する
 *
 * @param {string} type イベント名
 * @param {number} [time=null] イベント発生時刻。省略時は現時刻。
 */
function updateLastOperation(type, time=null) {
  const now = (time === null)? Date.now():time;
  LAST_OPERATION.type = type;
  LAST_OPERATION.time = now;

  // デバッグ表示
  remain.style.color = 'white';
  remain.style.backgroundColor = 'lightgreen';
  console.log('[updateLastOperation] ', type, now);
}

/**
 * ロード完了時の処理
 *
 */
window.addEventListener('load', ()=>{
  //----------------------------------------
  // ユーザー操作が発生したら時間を記録
  //----------------------------------------
  WATCHEVENTS.forEach(type => {
    window.addEventListener(type, () => {
      updateLastOperation(type);
    });
  });

  //----------------------------------------
  // 監視する
  //----------------------------------------
  const OPCHECK_TIMER = setInterval(() => {
    const now = Date.now();
    const time = LAST_OPERATION.time;

    // タイムアウト判定
    TIMEOUT_FLAG = ((now - time) > TIMEOUT_MSEC);

    // タイムアウト判定結果を表示
    if( TIMEOUT_FLAG ) {
      alert('タイムアウトしました');
      clearInterval(OPCHECK_TIMER);

      //--------------------------------
      // このあたりにタイムアウト時に
      // やりたい処理を書く
      //--------------------------------
    }

    // デバッグ表示
    timeout.innerHTML = TIMEOUT_FLAG ? 'true' : 'false';
    timeout.style.color = TIMEOUT_FLAG ? 'red' : 'blue';
    remain.innerHTML = (TIMEOUT_MSEC - (now - time)).toLocaleString();
    remain.style.color = 'black';
    remain.style.backgroundColor = 'white';
    console.log('[OPCHECK_TIMER] ', TIMEOUT_FLAG, now - time);
  }, INTERVAL_MSEC);
});
</script>
</body>
</html>

参考ページ