8️⃣

普段使わないけど便利なWeb API 8選

2023/07/10に公開

MDNのWeb APIリストから、便利で、しかし普段のサービス開発ではあまり使われていない可能性のあるAPIを8個選びご紹介します。これらのAPIはあまり知られていないかもしれませんが、特定の状況や要件に対して非常に有効であることがあります。

Beacon API

Beacon APIは、非同期でブロッキングしないリクエストをWebサーバーに送信するために使用されます。このリクエストはレスポンスを期待しないため、XMLHttpRequestやFetch APIを使ったリクエストとは異なりページがアンロード(ウェブページがユーザーによって閉じられるか、別のページに移動する際)される前にブラウザがビーコンリクエストを開始し、それを完了させることを保証します。
主な使用例としては、クライアント側のイベントやセッションデータをサーバーに送信するために使用されます。このAPIは、navigator.sendBeacon()という一つのメソッドを定義しています。このメソッドはURLとリクエストに送信するデータの2つの引数を取ります。データ引数はオプションで、その型はTypedArray、DataView、Blob、文字列リテラルまたはオブジェクト、またはFormDataオブジェクトである可能性があります。ブラウザがリクエストを配信のために正常にキューに入れると、このメソッドは"true"を返し、それ以外の場合は"false"を返します。

提供されたHTMLコードは、ユーザーがページを閉じる(アンロードする)直前に、Beacon APIを使用してデータを送信するデモです。Beacon APIは、ウェブサーバーへの非同期でブロッキングしないリクエストを送信するために使用されます。この特性により、ページのアンロードを遅延させることなく、ウェブページからウェブサーバーへの小さなデータ転送を行うことができます。

以下のHTMLコードでは、navigator.sendBeaconメソッドがwindow.onbeforeunloadイベントハンドラ内で呼び出されています。これにより、ユーザーがこのページを閉じる直前に、指定されたURL("https://putsreq.herokuapp.com/4GE2nVUuDoDGsNyKES2G")
に対してデータ('Sent by a beacon!')という文字列が送信されています。

<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=yes">
  <title>Navigator.sendBeacon Demo</title>
</head>
<body>

  <h1>Navigator.sendBeacon Demo</h1>

  <p>Beacon lets you asynchronously transfer small HTTP data from the User Agent to a web server. It can be used e.g. to send analytics or diagnostics code without delaying the page's unload or affecting the performance of the next navigation.</p>

  <p>The sample below sends a beacon to a sample server at <code>https://putsreq.herokuapp.com/4GE2nVUuDoDGsNyKES2G</code> on unload. Close this page and visit the <strong><a href="https://putsreq.herokuapp.com/4GE2nVUuDoDGsNyKES2G/inspect" target="_blank" rel="noreferrer noopener">PutsReq inspect page</a></strong> to see if the beacon data was received.</p>

  <strong>JavaScript snippet</strong>
  <pre>
  &lt;script src="navigator.sendbeacon.min.js"&gt;&lt;/script&gt;
  &lt;script&gt;
    window.onbeforeunload = function(e) {
      navigator.sendBeacon(
        'https://putsreq.herokuapp.com/4GE2nVUuDoDGsNyKES2G',
        'Sent by a beacon!'
      )

      // empty return is required to not show popup
      return
    }
  &lt;/script&gt;
  </pre>

  <script src="../dist/navigator.sendbeacon.min.js"></script>
  <script>
    window.onbeforeunload = function(e) {
      navigator.sendBeacon(
        'https://putsreq.herokuapp.com/4GE2nVUuDoDGsNyKES2G',
        'Sent by a beacon!'
      )

      // empty return is required to not show popup
      return
    }
  </script>
</body>
</html>

対応できるブラウザは以下の通りです。

Page Visibility API

Page Visibility APIを使用して、Webページがユーザーに表示されているかどうかを判断し、その状態に基づいて何らかのアクションを実行することができます。以下に、簡単な例を示します。

Vercelのswrでお馴染みの挙動もこのAPIが使われて実装されているようです。

他にも以下の使用例が考えられます

  • 画像のスライドショーがあるサイトで、ユーザーが見ていない間に次のスライドに進むべきではないもの
  • 情報をダッシュボードに表示するアプリケーションで、ページが見えていないときは更新情報をサーバーへ問い合わせてほしくないもの
  • 正確なページビューをカウントできるよう、ページがプリレンダリングされている状態を検出したい。
  • 端末がスタンバイモードである (ユーザーが電源ボタンを押して、画面を消灯している) ときに、音声を止めたいサイト。
  • ウェブアプリで動画を再生している場合、ユーザーがタブをバックグラウンドにした場合に動画を一時停止させ、ユーザーがこのタブに戻ったときに再生を再開させたりする

この例では、Webページが表示されている場合には背景色を緑に設定し、非表示の場合には赤に設定します。これにより、ページの表示状態を視覚的に把握することができます。

<!DOCTYPE html>
<html>
<head>
    <title>Page Visibility API Demo</title>
</head>
<body>
    <script>
    // Function to handle visibility changes
    function handleVisibilityChange() {
        if (document.hidden) {
            document.body.style.backgroundColor = 'red';
        } else {
            document.body.style.backgroundColor = 'green';
        }
    }

    // Add the event listener
    document.addEventListener('visibilitychange', handleVisibilityChange);
    </script>
</body>
</html>

対応できるブラウザは以下の通りです。

Web Audio API

Web Audio APIを使用して、ブラウザで音声を再生、制御、操作することができます。

また、GitHubに多くのexamplesがまとまっています。

以下に、基本的な音声再生のサンプルコードを示します。

<!DOCTYPE html>
<html>
<head>
    <title>Web Audio API Demo</title>
</head>
<body>
    <button id="play">Play</button>
    <button id="stop">Stop</button>

    <script>
    // Create a new AudioContext
    var audioContext = new (window.AudioContext || window.webkitAudioContext)();

    // Create a new AudioBufferSourceNode
    var source = audioContext.createBufferSource();

    // Load a sound
    var request = new XMLHttpRequest();
    request.open('GET', 'https://mdn.github.io/webaudio-examples/audio-basics/outfoxing.mp3', true);
    request.responseType = 'arraybuffer';

    // Decode the audio once the file is loaded
    request.onload = function() {
        audioContext.decodeAudioData(request.response, function(buffer) {
            source.buffer = buffer;

            // Connect the source to the context's destination (the speakers)
            source.connect(audioContext.destination);
        });
    };

    request.send();

    // Play the sound
    document.getElementById('play').addEventListener('click', function() {
        source.start(0);
    });

    // Stop the sound
    document.getElementById('stop').addEventListener('click', function() {
        source.stop(0);
    });
    </script>
</body>
</html>

また、複雑で高度な音声編集も可能です。
簡潔で通常のウェブオーディオの使い方は、次のようになります。

  • 音声コンテキストを作成する
  • コンテキストの中で、<audio>、オシレーター、ストリームなどの音源を作成する
  • リバーブ、バイクワッドフィルター、パンナー、コンプレッサーなどのエフェクトノードを作成する
  • 最終的な音声の出力先 (destination) を選ぶ(例えばスピーカーなど)
  • 音源をエフェクトに繋げ、エフェクトを出力先に繋げる

オーディオは非常に専門性が高く音声や音楽用語に慣れない人には敷居が高く感じられるかもしれません。そのため各種チュートリアルやガイドもまとめられています。

Basic concepts behind Web Audio API

この記事では、アプリを経由した音声伝達方法の設計をする際に、十分な情報に基づいた決断をする助けとなるよう、 Web Audio API の特徴がいかに働いているか、その背後にある音声理論について説明します。この記事はあなたを熟練のサウンドエンジニアにさせることはできないものの、Web Audio API が動く理由を理解するための十分な背景を提供します。

Visualizations with Web Audio API

Web Audio API の最も興味深い機能の 1 つは、オーディオソースから周波数、波形、その他のデータを抽出し、それを使用してビジュアライゼーションを作成する機能です。この記事では、方法について説明し、いくつかの基本的な使用例を示します。

Web Audio API の使用

Web Audio API の入門を見てみましょう。ここではいくつかの概念を短く確認してから、簡単な boombox の例で、音声トラックの読み込み、再生と一時停止、音量やステレオ位置の変更の方法を学びましょう。

対応できるブラウザは以下の通りです。

Web Storage API

ユーザーのブラウザにデータを永続的に保存するためのAPIです。これは、Cookieよりも多くのデータを保存することができ、またサーバーに自動的に送信されないため、プライバシーに配慮しています。ブラウザでのデータ保存を容易にするためのAPIで、localStoragesessionStorageの2つの主要なメカニズムがあります。

Auth0Firebase AuthなどのJavaScript SDKでセッション管理として内部的に利用されていました。

ただし、JWTなど認証に必要なアクセストークンなどはHTTP Only Cookieで管理する方がJavaScriptからアクセスできないため、XSS攻撃から保護し、よりセキュアです。

以下に、Web Storage APIを使用した基本的な例を示します。

<!DOCTYPE html>
<html>
<head>
    <title>Web Storage API Demo</title>
</head>
<body>
    <h2>Web Storage API Demo</h2>
    <button id="save">Save Data</button>
    <button id="load">Load Data</button>
    <button id="clear">Clear Data</button>
    <div id="result"></div>

    <script>
    var resultDiv = document.getElementById("result");

    // Save data to localStorage
    document.getElementById("save").addEventListener("click", function() {
        localStorage.setItem("name", "John Doe");
        localStorage.setItem("email", "john.doe@example.com");
        resultDiv.textContent = "Saved data to localStorage.";
    });

    // Load data from localStorage
    document.getElementById("load").addEventListener("click", function() {
        var name = localStorage.getItem("name");
        var email = localStorage.getItem("email");
        resultDiv.textContent = "Name: " + name + ", Email: " + email;
    });

    // Clear data from localStorage
    document.getElementById("clear").addEventListener("click", function() {
        localStorage.removeItem("name");
        localStorage.removeItem("email");
        resultDiv.textContent = "Cleared data from localStorage.";
    });
    </script>
</body>
</html>

しかし、localStorageは特殊な条件下(IPTなど)だと想定通り機能しない可能性があるので使用する際は事前に確認をしておくと良いでしょう
対応できるブラウザは以下の通りです。

Vibration API

モバイルデバイスでバイブレーションを制御するためのAPIです。通知やゲームのフィードバックとして使用されます。

単一の振動:

window.navigator.vibrate(200);
window.navigator.vibrate([200]);

これらの例は、デバイスを200ミリ秒間振動させます。

振動パターン:

window.navigator.vibrate([200, 100, 200]);

この例では、デバイスを200ミリ秒間振動させ、その後100ミリ秒間休止し、再び200ミリ秒間振動させます。

既存の振動をキャンセル:

navigator.vibrate(0);

このコードは、現在進行中の振動パターンをキャンセルします。

継続的な振動:

let vibrateInterval;

// Starts vibration at passed in level
function startVibrate(duration) {
  navigator.vibrate(duration);
}

// Stops vibration
function stopVibrate() {
  // Clear interval and stop persistent vibrating
  if (vibrateInterval) clearInterval(vibrateInterval);
  navigator.vibrate(0);
}

// Start persistent vibration at given duration and interval
// Assumes a number value is given
function startPersistentVibrate(duration, interval) {
  vibrateInterval = setInterval(() => {
    startVibrate(duration);
  }, interval);
}

このコードでは、指定された期間と間隔で持続的な振動を開始します。振動は指定された期間だけ行われ、その後指定された間隔だけ休止します。このパターンは、stopVibrate関数が呼び出されるまで続きます。

対応できるブラウザは以下の通りです。

なぜSafariだけがサポートしないのだろう?と疑問に思い調べてみると、こちらの記事が見つかり以下の理由が記載されていました。

One potential risk is the identification of a particular person in real life. Imagine several people in the same room placing their devices on a table. At some point, one person’s device vibrates in specific patterns. This individual might then become marked to a potential observer.

訳すと「潜在的なリスクのひとつは、実生活における特定人物の識別である。同じ部屋にいる数人が、テーブルの上に自分のデバイスを置いているとする。ある時点で、ある人のデバイスが特定のパターンで振動する。すると、この人物は、潜在的な観察者からマークされるかもしれない。」となりプライバシー的な観点からSafariではサポートを切ったのではないかと考えられているようです。


Push API

サーバーからのプッシュメッセージをブラウザに送信するためのAPIです。これは、ユーザーに重要な更新情報を提供するために使用されます。
Webアプリケーションにプッシュ通知を送信する基本的な実装を以下に示します。

  1. Service Workerの登録
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
  .then(function(registration) {
    console.log('Service Worker registration successful with scope: ', registration.scope);
  })
  .catch(function(err) {
    console.log('Service Worker registration failed: ', err);
  });
}
  1. プッシュ通知の許可を求める
function askPermission() {
  return new Promise(function(resolve, reject) {
    const permissionResult = Notification.requestPermission(function(result) {
      resolve(result);
    });

    if (permissionResult) {
      permissionResult.then(resolve, reject);
    }
  })
  .then(function(permissionResult) {
    if (permissionResult !== 'granted') {
      throw new Error('We weren\'t granted permission.');
    }
  });
}
  1. Push Subscriptionの取得
function subscribeUserToPush() {
  return navigator.serviceWorker.register('service-worker.js')
  .then(function(registration) {
    const subscribeOptions = {
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        'BEl62iUYgUivxIkv69yViUuiCOaI9-EbH-Rj_P7fAaY5Zi6k5EN8YkkgDLoU2YYzmp0nU8MJMGUZYkUZbT7dx5A'
      )
    };

    return registration.pushManager.subscribe(subscribeOptions);
  })
  .then(function(pushSubscription) {
    console.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
    return pushSubscription;
  });
}
  1. Service Workerでプッシュイベントを処理する

Service Worker ('service-worker.js') 内部に以下のようにプッシュイベントをリスンします:

self.addEventListener('push', function(event) {
  if (event.data) {
    console.log('This push event has data: ', event.data.text());
  } else {
    console.log('This push event has no data.');
  }

  const title = 'Hello';
  const options = {
    body: 'World'
  };

  event.waitUntil(self.registration.showNotification(title, options));
});

このコードでは、プッシュイベントがService Workerに送信されるたびに、ブラウザに通知を表示します。

PushManager へのサブスクリプションを実装する場合、アプリで CSRF/XSRF 問題を起こさないように保護することが非常に重要です。以下は具体的な施策が記されているドキュメントです。

Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet & Preventing CSRF and XSRF Attacks

トークンベースの防御

一般的に最も推奨されるCSRF対策は、アプリケーションがサーバーサイドでランダムな値(トークン)を生成し、それをセッションと関連付ける方法です。このトークンは、その後の要求でクライアントによって送信され、サーバーによって検証されます。

同じサイトでのCookie

これは、ブラウザがサイト間リクエストにCookieを自動的に含めないように指示するフラグ(SameSite)を設定する防御策です。このフラグにはStrict、Lax、Noneの3つの設定があり、セキュリティと互換性の間でバランスをとる必要があります。

二重送信Cookie

このアプローチでは、ランダムなCSRFトークンがCookieと非Cookie要求パラメータ(例えば、HTTPパラメータ)の両方で送信され、サーバーサイドで両者が一致するかどうか検証されます。

両方のドキュメントとも、CSRF攻撃を防ぐための基本的な戦略は、一意のトークンの生成と検証、SameSite Cookieの使用、そしてユーザーの再認証や二要素認証の要求を通じて、不正な要求を識別し拒否することと主張しています。

SameSite Cookieとは?

SameSite Cookieは、ウェブサイトがクロスサイトのコンテキストでCookieを送信することを制御するためのCookieの属性です。これは、Cross-Site Request Forgery (CSRF)のようなセキュリティリスクを軽減するための方法として広く用いられています。

SameSite属性には3つの設定値があります:

Strict

この設定を選択すると、ブラウザはそのサイトが他のサイトにリンクしている場合やリダイレクトする場合に、そのサイトのCookieを送信しません。

Lax

これはStrictよりも柔軟なオプションで、ユーザーが直接サイトを訪れるか、そのサイトが他のサイトにリンクしている場合にはCookieを送信します。ただし、そのサイトが他のサイトからのPOSTリクエストを受け取る場合や、画像やフレームをロードする場合には、Cookieの送信は行われません。

None

この設定を選択すると、ブラウザはすべてのコンテキストでCookieを送信します。この設定を使用する場合、CookieはSecure属性も設定されている必要があり、これによりCookieはHTTPS接続を通じてのみ送信されます。(通常のCookieと似た仕様)

対応できるブラウザは以下の通りです。

api.PushEvent

api.PushMessageData

Service Workers API

Service Workerは、ウェブページの背後で実行されるスクリプトで、ウェブページとブラウザの間でネットワークリクエストを制御したり、完全にオフラインで動作する機能を提供したりすることが可能です。以下に、基本的なService Workerの登録とインストールの例を示します。

まず、Service Workerを登録するためには、次のようなコードをメインのJavaScriptファイル(通常はウェブページのHTMLからリンクされる)に書くことができます:

// Check if service workers are supported
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/service-worker.js')
    .then(function(registration) {
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    })
    .catch(function(error) {
      console.log('ServiceWorker registration failed: ', error);
    });
  });
}

上記のコードは、ブラウザがService Workerをサポートしている場合に、ページが完全にロードされたときにService Workerを登録します。登録されるService Workerは/service-worker.jsというパスで指定され、登録が成功したかどうかに応じて結果をコンソールに出力します。

次に、service-worker.jsの中でService Workerのライフサイクルイベントを制御します:

// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles/main.css',
        '/script/main.js'
      ]);
    })
  );
});

// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', function(event) {
  var cacheWhitelist = ['my-cache'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

// The fetch handler serves responses for same-origin resources from a cache.
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});

上記のservice-worker.jsでは、Service Workerのインストール時に特定のファイルをキャッシュに保存し、アクティベート時に古いキャッシュを削除し、そして各ネットワークリクエストに対してキャッシュからのレスポンスを試みる、といった処理が定義されています。

これらの基本的な例を応用して、Service Workerを用いて様々な処理を実装することが可能です。例えば、Push APIと組み合わせてプッシュ通知を実装したり、Background Sync APIと組み合わせてバックグラウンド同期を実装したりすることもできます。

サービスワーカーをもっと知ろう、または使ってみようと考えたらこちらを確認してください。

サービスワーカーのライブデモ
サービスワーカーのライブでもソースコード

IndexedDB

IndexedDBはウェブブラウザに保存されるキー-バリュー形式のデータベースシステムです。IndexedDB APIを使用してデータベースを作成し、オブジェクトを保存・検索・削除する基本的なコードは次のようになります。

firebase-js-sdkのセッション管理部分でも使用されています。

以下が基本的な使用方法の例です。
まずは、データベースの作成とオブジェクトストア(テーブルのようなもの)の作成を行います。

// データベースの名前とバージョンを設定
var dbName = 'myDatabase';
var dbVersion = 1;

// データベースを開く(または作成する)
var request = indexedDB.open(dbName, dbVersion);

request.onerror = function(event) {
  console.log('Database error: ' + event.target.errorCode);
};

request.onupgradeneeded = function(event) {
  var db = event.target.result;
  
  // "myObjects"という名前のオブジェクトストアを作成
  var objectStore = db.createObjectStore('myObjects', {keyPath: 'id'});
  
  // インデックスを作成して効率的な検索・ソートを可能にする
  objectStore.createIndex('name', 'name', {unique: false});
};

request.onsuccess = function(event) {
  console.log('Database initialised');
};

次に、オブジェクトストアにデータを追加するためのコードです。

var db;
var request = indexedDB.open('myDatabase');

request.onsuccess = function(event) {
  db = event.target.result;
  
  var transaction = db.transaction(['myObjects'], 'readwrite');
  var objectStore = transaction.objectStore('myObjects');
  var request = objectStore.add({id: 1, name: 'myObject'});
  
  request.onsuccess = function(event) {
    console.log('Object added successfully');
  };
  
  request.onerror = function(event) {
    console.log('Error adding object');
  };
};

最後に、データを取得するためのコードです。

var db;
var request = indexedDB.open('myDatabase');

request.onsuccess = function(event) {
  db = event.target.result;
  
  var transaction = db.transaction(['myObjects']);
  var objectStore = transaction.objectStore('myObjects');
  var request = objectStore.get(1);
  
  request.onsuccess = function(event) {
    var result = event.target.result;
    console.log(result); // {id: 1, name: 'myObject'}
  };
  
  request.onerror = function(event) {
    console.log('Error getting object');
  };
};

これらの基本的なコードを応用して、IndexedDBを用いてデータの保存・検索・削除・更新などを行うことが可能です。

使用する際はラッパーなどのライブラリを使用することをおすすめします。

idb

IndexedDB API をほぼ反映した小さな (~1.15k) ライブラリーですが、使いやすさを大きく変える小さな改良が加えられています。

Dexie.js

優良でシンプルな構文により高速なコード開発を可能にする、IndexedDB のラッパーです

JsStore

SQL 風の構文による IndexedDB のラッパーです。

Webストレージには各ブラウザで制限が異なります。詳しくは「Webストレージについて」を参照してください。

IndexedDBを正しく扱う

IndexedDBを使用するにあたり以下のことに配慮して開発を進められると良いでしょう

非同期処理をする

IndexedDBは非同期APIなので、データベース操作がUIをブロックしたり、アプリケーションのパフォーマンスを低下させることなく、長時間実行できます。非同期処理を活用するためには、Promiseやasync/awaitのような非同期プログラミングパターンを使用します。

トランザクションを適切に使用する

トランザクションは一連の操作をひとまとめにする方法で、これにより一部の変更が成功した場合でも他の変更が失敗すると、すべての変更がロールバック(元に戻る)されます。これにより、データの整合性が保たれます。ただし、トランザクションの使用は計画的に行う必要があり、不要なロックを避けるために一度に一つの読み取りまたは書き込みトランザクションを使用することが推奨されています。

適切なインデックス設計を行う

インデックスはデータの効率的な検索を可能にします。しかし、それぞれのインデックスはデータベースのスペースを消費し、書き込み操作を遅くします。そのため、必要最小限のインデックスのみを作成し、効率的に設計することが重要です。

データのバージョン管理を行う

onupgradeneededイベントはデータベースのスキーマ(構造)をアップデートするために使用されます。このイベントはデータベースが開かれ、そのバージョンが引数で指定したバージョンよりも古い場合に発生します。このイベントを使ってデータのバージョン管理を行うことが推奨されています。

エラーハンドリングを行う

request.onerrorまたはtransaction.onerrorイベントを使って、エラーが発生したときの処理を記述します。適切なエラーハンドリングを行うことで、データベースの問題がアプリケーションのパフォーマンスやユーザーエクスペリエンスに悪影響を及ぼすのを防ぐことができます。

Discussion