ウェブエンジニア問題集
第20章

知っておきたいWeb API — fetch・Storage・IntersectionObserver

23
この章の目次開く

ブラウザには、JavaScript本体とは別にWeb APIが用意されています。この章で扱うのは、HTTP通信を行う fetch、ブラウザに小さなデータを保存する localStorage / sessionStorage、JavaScriptからCookieを読み書きする cookieStore、要素が画面に入ったかを検知する IntersectionObserver、DOMの変更を監視する MutationObserver です。

この章では、アプリ開発で使う頻度が高いWeb APIを、用途・構文・戻り値・注意点まで整理します。Promise やDOM操作の知識と組み合わせて読むと、ブラウザ上で「データを取得し、保存し、必要なタイミングで画面を更新する」流れがつながります。


Web APIとは — ブラウザが提供する機能

Web APIは、ブラウザがJavaScriptから呼び出せる形で提供している機能群です。JavaScriptの文法だけではHTTP通信も、画面の要素取得も、ブラウザへの保存もできません。そこをつなぐのがWeb APIです。

// JavaScriptの標準機能
const names = ['Taro', 'Hanako'].map((name) => name.toUpperCase());
 
// ブラウザのWeb API
const button = document.querySelector('button');
button.addEventListener('click', () => {
  console.log('clicked');
});
js
ブラウザ上でWeb APIを使っているイメージ

代表的なWeb APIは次の通りです。

API何をするかよく使う場面
fetchHTTPリクエストを送るAPIからJSONを取得、フォーム送信
localStorageブラウザに永続保存するテーマ設定、簡単な入力途中データ
sessionStorageタブを閉じるまで保存する一時的な画面状態、確認画面の入力値
cookieStoreJavaScriptからCookieを読み書きするテーマや言語設定など、サーバーにも送る小さな値
IntersectionObserver要素が画面に入ったか検知する遅延読み込み、無限スクロール、表示時アニメーション
MutationObserverDOMの追加・削除・属性変更を検知する外部スクリプトが書き換えるDOMの監視
URL / URLSearchParamsURLを安全に組み立てるクエリ文字列の読み書き
navigator.clipboardクリップボードを読み書きするコピーボタン、招待URLのコピー

fetch API — HTTP通信を行う

fetch はHTTPリクエストを送るためのWeb APIです。戻り値は Promise<Response> なので、await してレスポンスを受け取り、さらに response.json()response.text()response.blob() で本文を読み取ります。

構文: fetch(input, init?)

引数説明
inputURL文字列、または Request オブジェクト
init(省略可)methodheadersbodycredentialssignal を指定する設定オブジェクト

戻り値: Promise<Response>

const response = await fetch('/api/users');
const users = await response.json();
 
console.log(users);
js

HTTPエラーは自動では例外にならない

fetch がrejectするのは、ネットワーク不通、CORSエラー、リクエストの中断により、レスポンスを受け取れなかった場合です。サーバーが 404500 を返した場合、fetch 自体は成功扱いになります。

async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
 
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
 
  return response.json();
}
js

JSONをPOSTする

JSONを送るときは、methodheadersbody を指定します。body にオブジェクトを直接渡すのではなく、JSON.stringify で文字列に変換します。

const response = await fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Taro',
    role: 'admin',
  }),
});
 
if (!response.ok) {
  throw new Error(`Failed to create user: ${response.status}`);
}
 
const result = await response.json();
js
学習者学習者

body{ name: 'Taro' } をそのまま渡しちゃダメなんですか?

fetchbody は送信するデータそのものです。JSON APIに送る場合は文字列化されたJSONが必要なので、JSON.stringify を通します。JSONの変換ルールはJSONの基礎と実践を参照してください。

よく使うinitオプション

オプション役割
methodHTTPメソッド'GET', 'POST', 'PUT', 'DELETE'
headersHTTPヘッダー{ 'Content-Type': 'application/json' }
body送信本文JSON.stringify(data)
credentialsCookie送信の扱い'include', 'same-origin', 'omit'
signal中断用のシグナルcontroller.signal
cacheキャッシュ制御'no-store', 'reload'

Request / Response — fetchの入出力

fetch はURL文字列だけでなく、Request オブジェクトも受け取れます。レスポンス側は Response オブジェクトとして返ってきます。

const request = new Request('/api/users', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
  },
});
 
const response = await fetch(request);
js

Response には、ステータスやヘッダー、本文を読むためのメソッドがあります。

プロパティ / メソッド役割
response.okステータスが 200299 なら true
response.statusステータスコード
response.statusTextステータス文言
response.headersレスポンスヘッダー
response.json()本文をJSONとして読み、JavaScriptの値にする
response.text()本文を文字列として読む
response.blob()本文をBlobとして読む
const response = await fetch('/api/profile');
 
console.log(response.status); // 200
console.log(response.headers.get('content-type')); // "application/json; charset=utf-8"
 
const profile = await response.json();
js

AbortController — fetchを中断する

AbortController は、実行中の処理に「中断してよい」というシグナルを渡すためのWeb APIです。fetch では signal オプションに渡すことで、リクエストを途中でキャンセルできます。

構文: new AbortController()

プロパティ / メソッド役割
controller.signalfetch に渡す中断シグナル
controller.abort()中断を通知する
const controller = new AbortController();
 
const request = fetch('/api/search?q=javascript', {
  signal: controller.signal,
});
 
// もう結果が不要になったら中断する
controller.abort();
 
try {
  await request;
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('request was aborted');
  } else {
    throw error;
  }
}
js

検索入力のたびにAPIを呼ぶ画面では、古いリクエストを中断すると「遅れて返ってきた古い結果で画面が上書きされる」事故を防げます。

Storage API — localStorageとsessionStorage

Storage APIは、ブラウザにキーと値を保存するAPIです。localStoragesessionStorage は同じメソッドを持ちますが、保存期間が違います。

API保存期間使う場面
localStorage明示的に削除するまで残るテーマ、表示設定、最後に開いたタブ
sessionStorageタブを閉じるまで残る入力途中の一時データ、ページ遷移中だけ必要な値

構文: storage.setItem(key, value) / storage.getItem(key)

メソッド役割
setItem(key, value)文字列を保存する
getItem(key)文字列を取得する。存在しない場合は null
removeItem(key)指定キーを削除する
clear()そのオリジンのストレージを全削除する
key(index)指定位置のキー名を取得する
localStorage.setItem('theme', 'dark');
 
const theme = localStorage.getItem('theme');
console.log(theme); // "dark"
 
localStorage.removeItem('theme');
js

オブジェクトはJSON文字列にして保存する

Storageに保存できる値は文字列だけです。オブジェクトや配列を保存するときは JSON.stringify、取り出すときは JSON.parse を使います。

const settings = {
  theme: 'dark',
  sidebarOpen: true,
};
 
localStorage.setItem('settings', JSON.stringify(settings));
 
const saved = localStorage.getItem('settings');
const parsed = saved === null ? null : JSON.parse(saved);
js

Storageは同期API

localStorage.getItem()setItem() は同期的に動きます。大量データを頻繁に読み書きするとメインスレッドを止めます。保存するのは小さな設定値や軽い一時データに限定します。大きなデータや検索可能なデータを保存する場合は、IndexedDBを使います。

CookieStore API — JavaScriptからCookieを読み書きする

Cookie Store APIは、ブラウザが用意した cookieStore インスタンス からCookieを読み書きするWeb APIです。MapDate のように new CookieStore() で自分で作るのではなく、ページに最初から存在するグローバルオブジェクトを使います。

従来は document.cookie"name=value; path=/" のような文字列を代入して操作していました。文字列の組み立てとパースが面倒で、非同期処理とも相性が悪かったため、Promiseベースの cookieStore が追加されました。

構文: グローバルの cookieStore をそのまま使う

メソッド戻り値役割
get(name)Promise<CookieListItem | null>指定名のCookieを取得する
set(name, value) / set(...)Promise<undefined>Cookieを設定する
delete(name) / delete(...)Promise<undefined>Cookieを削除する
getAll()Promise<CookieListItem[]>すべてのCookieを取得する
await cookieStore.set({
  name: 'theme',
  value: 'dark',
  expires: Date.now() + 86400000,
  path: '/',
});
 
const theme = await cookieStore.get('theme');
console.log(theme?.value); // "dark"
 
await cookieStore.delete('theme');
js

set には名前と値だけを渡す短い形もあります。

await cookieStore.set('lang', 'ja');
js

取得結果の CookieListItem には namevaluedomainpathexpires などのプロパティがあります。存在しないCookieを get した場合は null が返ります。

document.cookieとの違い

document.cookiecookieStore
戻り値セミコロン区切りの文字列Promise でオブジェクトを返す
書き込み文字列を代入するset() で属性をオブジェクト指定できる
削除期限切れ日時を過去にするなど間接的delete() で明示的に削除できる
変更の監視自前でポーリングが必要change イベントで検知できる
cookieStore.addEventListener('change', (event) => {
  for (const { name, value } of event.changed) {
    console.log('changed', name, value);
  }
 
  for (const { name } of event.deleted) {
    console.log('deleted', name);
  }
});
js

IntersectionObserver — 要素が見えたかを検知する

IntersectionObserver は、対象要素が画面や親要素の表示領域に入ったかを検知するAPIです。スクロールイベントを自前で監視して座標計算するより、軽く正確に書けます。

構文: new IntersectionObserver(callback, options?)

引数説明
callback交差状態が変わったときに呼ばれる関数
options.root基準にするスクロール領域。省略時はビューポート
options.rootMargin判定領域の余白。例: '200px 0px'
options.thresholdどの割合見えたら通知するか。01
const target = document.querySelector('#load-more');
 
const observer = new IntersectionObserver(
  (entries) => {
    const entry = entries[0];
 
    if (entry.isIntersecting) {
      console.log('load next page');
    }
  },
  {
    root: null,
    rootMargin: '200px 0px',
    threshold: 0,
  },
);
 
observer.observe(target);
js

rootMargin: '200px 0px' は、画面に入る200px手前で通知する設定です。画像の遅延読み込みや無限スクロールでは、実際に見えてから動くより少し早めに処理を始める方が自然です。

表示領域に入った要素を確認するイメージ

監視を止める

一度だけ処理したい場合は、処理後に unobserve します。監視自体をすべて止める場合は disconnect します。

const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (!entry.isIntersecting) continue;
 
    entry.target.classList.add('is-visible');
    observer.unobserve(entry.target);
  }
});
 
document.querySelectorAll('.fade-in').forEach((element) => {
  observer.observe(element);
});
js
メソッド役割
observe(element)要素の監視を開始する
unobserve(element)指定要素の監視を止める
disconnect()すべての監視を止める

MutationObserver — DOMの変更を監視する

MutationObserver は、DOMツリーの変更を検知するAPIです。要素が追加された、属性が変わった、テキストが変わった、といった変更を監視できます。

構文: new MutationObserver(callback)

引数説明
callbackDOM変更が記録されたときに呼ばれる関数
const target = document.querySelector('#comments');
 
const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    if (mutation.type === 'childList') {
      console.log('DOM children changed');
    }
  }
});
 
observer.observe(target, {
  childList: true,
  subtree: true,
});
js

observe の第2引数で、何を監視するかを明示します。

オプション監視する変更
childList子要素の追加・削除
attributes属性の変更
characterDataテキストノードの変更
subtree子孫要素まで監視する
attributeFilter監視する属性名を絞る
observer.observe(target, {
  attributes: true,
  attributeFilter: ['aria-expanded', 'hidden'],
});
js

そのほかよく使うWeb API

URLとURLSearchParams

URLを文字列結合で作ると、?&、エンコード漏れで壊れます。クエリ文字列は URLURLSearchParams で扱います。

const url = new URL('/api/search', location.origin);
url.searchParams.set('q', 'JavaScript 入門');
url.searchParams.set('page', '1');
 
console.log(url.toString());
// "https://example.com/api/search?q=JavaScript+%E5%85%A5%E9%96%80&page=1"
js
const params = new URLSearchParams(location.search);
const page = Number(params.get('page') ?? '1');
js

Clipboard API

navigator.clipboard.writeText() でテキストをクリップボードへコピーできます。HTTPS上で、クリックまたはキーボード操作をきっかけに実行します。

const button = document.querySelector('#copy');
 
button.addEventListener('click', async () => {
  await navigator.clipboard.writeText(location.href);
});
js

Web Storage以外の保存先

ブラウザ内保存には複数の選択肢があります。

保存先特徴
CookieHTTPリクエストに自動で付く。認証やサーバー連携向き
cookieStoreJavaScriptからPromiseでCookieを読み書きする
localStorage文字列の小規模保存。JavaScriptから読める
sessionStorageタブ単位の一時保存
IndexedDB大きな構造化データを非同期に保存できる
Cache StorageService Workerと組み合わせてレスポンスをキャッシュする

早見表

やりたいこと使うもの
APIからJSONを取得するfetch(url)response.okresponse.json()
JSONをPOSTするfetch(url, { method: 'POST', headers, body: JSON.stringify(data) })
fetchを中断するAbortControllersignal
小さな設定値を保存するlocalStorage.setItem(key, value)
タブを閉じるまで値を保存するsessionStorage
オブジェクトをStorageに保存するJSON.stringify / JSON.parse
JavaScriptからCookieを読むawait cookieStore.get(name)
JavaScriptからCookieを書くawait cookieStore.set({ name, value, ... })
要素が表示領域に入ったら処理するIntersectionObserver
DOMの追加・削除を検知するMutationObserver
クエリ文字列を作る・読むURL / URLSearchParams
テキストをコピーするnavigator.clipboard.writeText(text)

よくあるハマりどころ

1. fetchの404をcatchで拾えると思い込む

404500 はHTTPレスポンスとして返ってきているため、fetch はrejectしません。response.ok を確認し、失敗時は自分で throw します。

2. response.json()を2回呼ぶ

レスポンス本文はストリームです。await response.json() を呼ぶと本文は消費されます。同じレスポンスでログ用と処理用に2回読む設計は避けます。

3. Storageにオブジェクトをそのまま保存する

localStorage.setItem('user', user) のようにオブジェクトを渡すと、意図したJSONにはなりません。保存前に JSON.stringify(user)、取得後に JSON.parse(text) を使います。

4. localStorageに認証トークンを入れる

StorageはJavaScriptから読めます。XSSが起きると保存した値を盗まれます。秘密情報の保存先として使いません。

5. cookieStoreでセッションIDを管理する

cookieStore はJavaScriptから読めるCookieを操作するAPIです。localStorage と同様、XSSで値を盗まれる可能性があります。ログイン状態の管理には、サーバーが設定する HttpOnly Cookieを使います。

6. Observerを解除しない

不要になった監視を放置すると、意図しない処理やメモリ使用につながります。一度だけの処理なら unobserve、画面を離れるときは disconnect します。

ちゃんと使うためのポイント

  • Web APIはブラウザが提供する機能。JavaScript標準機能とは分けて考える
  • fetchPromise<Response> を返す。HTTPエラーは response.ok で判定する
  • JSON送信では Content-Type: application/jsonJSON.stringify をセットにする
  • Response の本文は基本的に1回だけ読む
  • localStorage / sessionStorage は文字列だけを保存する。オブジェクトはJSONに変換する
  • Storageに秘密情報を入れない
  • cookieStore はグローバルインスタンスを await で操作する。HttpOnly Cookieには触れない
  • IntersectionObserver は表示検知、MutationObserver はDOM変更検知に使う
  • 監視が不要になったら unobserve または disconnect で止める

次の章では、数値とMath を扱います。Web APIから受け取った数値を丸める、範囲内に収める、ランダムに選ぶ、といった処理を整理します。

参考リンク

JavaScriptクイズに挑戦するこの章で学んだJavaScriptの知識を、4択クイズでアウトプットして定着させよう