マガジン

ブラウザ保存のツールでデータが消える前に:バックアップ用の書き出し・読み込みを足す

AIを使いこなす

Claudeで執筆し、GPTで自動校閲・修正しています。

メモ帳の内容をバックアップ用紙へ退避し、復元に備える様子
記事の内容をもとに制作したAI生成イメージです。実際の製品画面や検証結果の写真ではありません。

結論から言うと、利用者が自分でデータを退避できるようにするには、「JSONファイルとして書き出すボタン」と「そのファイルを選んで戻すボタン」の2つを足せば足ります。ただし戻す側は、ファイルが壊れていたり別のアプリのものだったりする前提で作る必要があります。本記事では、書き出しファイルの仕様、AIに渡す依頼文、コードの骨格、壊れたファイルを読んだ時の確認項目を順に示します。 なお、記事内のコードとJSON例は「説明用の例」で、実際のブラウザで動かして検証した結果ではありません。公開前に、後半の動作確認の手順で自分の環境で試してください。

この記事の目次
  1. なぜ書き出しが必要なのか
  2. 書き出しファイルの仕様を先に決める
  3. AIに渡す依頼文
  4. コードの骨格(説明用の例・未実行)
  5. 壊れたファイルを読んだ時の確認項目
  6. 動作確認と、利用者への伝え方

なぜ書き出しが必要なのか

localStorageに保存したデータは、サーバーではなく利用者のブラウザの中にあります。MDNのストレージの割り当てと削除の基準によると、ブラウザの標準的な保存(best-effort)は、端末の空き容量が少ない時や利用者がブラウザ設定から削除した時などに、データが削除されることがあります。削除は、サイト(オリジン)単位でまとめて行われ、一部だけ残ることはないと説明されています。またSafariでは、トラッキング防止が有効な場合に、7日以上操作のないサイトのスクリプト生成データを削除することがあります。

navigator.storage.persist() で永続化を申請する方法もありますが、許可の扱いはブラウザごとに違います。申請が通るかどうかを、初学者向けの小さなツールで当てにするのは危険です。

加えて、利用者自身がサイトデータを消す、別のブラウザや端末に引っ越す、といった場面もあります。こうしたケースでは、アプリ側の工夫では防げません。手元にファイルがある状態が、いちばん確実な保険になります。

書き出しファイルの仕様を先に決める

AIにコードを頼む前に、ファイルの中身を人間が決めておきます。以下は編集上の提案で、メモアプリ(説明用の例)を想定しています。

  • 形式、決める内容:JSON(UTF-8、見やすい字下げ付き)、理由:利用者がテキストエディタで中身を確認できる
  • ファイル名、決める内容:アプリ名-backup-日付.json、理由:複数世代を区別できる
  • app、決める内容:アプリ識別子(例:my-memo)、理由:別アプリのファイルを弾く
  • format、決める内容:数値のバージョン(例:1)、理由:将来項目を増やした時に古いファイルを判別できる
  • exportedAt、決める内容:ISO形式の日時、理由:いつの時点のバックアップか分かる
  • data、決める内容:保存データ本体(配列)、理由:読み込み時に検証する対象
  • 読み込み方式、決める内容:全置換(既存データは自動退避)、理由:「マージ」は重複処理が増えるので初学者向けには避ける
  • サイズ上限、決める内容:5MB(例)、理由:大きすぎるファイルの読み込みを避けるためのアプリ側の制限。ブラウザの保存容量とは別に決める

書き出し例(説明用の例)です。

json
{
  "app": "my-memo",
  "format": 1,
  "exportedAt": "2026-10-27T09:00:00.000Z",
  "data": [
    { "id": "a1", "text": "買い物リスト" },
    { "id": "a2", "text": "企画メモ" }
  ]
}

ポイントは、保存データをそのまま出すのではなく、app と format を包む「封筒」を付けることです。これで、読み込み時に「このアプリのファイルか」「どの版か」を最初に判定できます。

AIに渡す依頼文

仕様が決まったら、次のように渡します。角括弧の部分は自分のアプリに合わせて置き換えてください。個人情報や本物のメモは貼らず、説明用のダミーデータにします。

text
localStorageで保存している既存のJavaScript製ツールに、
バックアップの書き出し・読み込みを足したいです。

【現状】
- 保存キー: [memo-app-data]
- 保存形式: JSON.stringifyした配列。要素は {id: 文字列, text: 文字列}
- フレームワークなし、HTMLとJS1ファイル

【仕様】
- 書き出し: {app:"my-memo", format:1, exportedAt, data} のJSONをBlobにして
  ダウンロード。ファイル名は my-memo-backup-YYYY-MM-DD.json
- 読み込み: <input type="file"> で選んだファイルを検証してから全置換。
  置換前の現在データは別キーに退避する
- 検証: 5MB超、JSONでない、appが違う、formatが未対応、dataが配列でない、
  要素の型が違う、のどれかなら、保存データを一切変更せず日本語でエラー表示

【お願い】
- コードは60行以内、コメントは日本語
- 検証関数と保存処理を分けてください
- 想定外の入力の例を5つ挙げ、それぞれ画面にどう表示されるか教えてください

最後の依頼が効きます。AIが挙げる「想定外の入力」を、そのまま後のテストケースにできるからです。

コードの骨格(説明用の例・未実行)

AIから返ってくるコードの形のイメージです。BlobとURL.createObjectURL()でファイルを作り、FileはBlobを継承するためfile.text()で文字列として読めます。text()は内容をUTF-8として扱います(MDN: Blob.text())。作ったオブジェクトURLは、使い終わったらrevokeObjectURL()で解放するのがMDNの案内です(MDN: URL.createObjectURL())。

js
const KEY = "memo-app-data";
const MAX_BYTES = 5 * 1024 * 1024;

function exportData() {
  const payload = {
    app: "my-memo", format: 1,
    exportedAt: new Date().toISOString(),
    data: JSON.parse(localStorage.getItem(KEY) ?? "[]"),
  };
  const blob = new Blob([JSON.stringify(payload, null, 2)],
    { type: "application/json" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `my-memo-backup-${payload.exportedAt.slice(0, 10)}.json`;
  a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

async function parseBackup(file) {
  if (file.size > MAX_BYTES) throw new Error("ファイルが大きすぎます");
  let obj;
  try { obj = JSON.parse(await file.text()); }
  catch { throw new Error("JSONとして読めません。壊れている可能性があります"); }
  if (obj?.app !== "my-memo") throw new Error("このアプリのバックアップではありません");
  if (obj.format !== 1) throw new Error("未対応の版のファイルです");
  if (!Array.isArray(obj.data)) throw new Error("dataの形式が正しくありません");
  const ok = obj.data.every(
    (m) => typeof m?.id === "string" && typeof m?.text === "string");
  if (!ok) throw new Error("データの中に形式の違う項目があります");
  return obj.data;
}

async function importData(file) {
  try {
    const data = await parseBackup(file);
    const current = localStorage.getItem(KEY);
    if (current !== null) localStorage.setItem(KEY + "-before-import", current);
    localStorage.setItem(KEY, JSON.stringify(data));
    alert(`${data.length}件を読み込みました`);
  } catch (e) {
    alert("読み込みを中止しました:" + e.message);
  }
}

構造上の要点は、parseBackup(検証だけ、保存はしない)とimportData(検証が通ってから保存)を分けていることです。検証の途中で保存が始まらないので、失敗しても元のデータは触られません。setItemは容量超過で例外を出すことがあるため(MDNはQuotaExceededErrorを説明しています)、保存部分もtryの中に入れています。ただし、この骨格では退避キーへの保存後に本体の保存が失敗すると、退避キーが作られたまま読み込みは失敗します。実装時は、保存前の退避に加え、本体の保存に失敗した場合の復元処理も設けてください。

壊れたファイルを読んだ時の確認項目

ここが本記事の中心です。次の表を、AIが作ったコードに対する受け入れテストとして使ってください。テスト用ファイルは、書き出したJSONをテキストエディタで手作業で壊して作れます。

  • 1、テスト用ファイルの作り方:正常に書き出したファイルをそのまま読む、期待する挙動:件数が表示され、画面に反映される、人が見るところ:件数がバックアップ時と一致するか
  • 2、テスト用ファイルの作り方:末尾の } を消す、期待する挙動:「JSONとして読めません」、人が見るところ:既存データが変わっていないか
  • 3、テスト用ファイルの作り方:空のファイル(0バイト)、期待する挙動:同上のエラー、人が見るところ:画面が固まらないか
  • 4、テスト用ファイルの作り方:app の値を別の文字列に変える、期待する挙動:「このアプリのバックアップではありません」、人が見るところ:既存データが変わっていないか
  • 5、テスト用ファイルの作り方:format を 99 にする、期待する挙動:「未対応の版」、人が見るところ:新しい版のファイルを古い版が壊さないか
  • 6、テスト用ファイルの作り方:data をオブジェクト {} にする、期待する挙動:「dataの形式が正しくありません」、人が見るところ:配列前提の処理が落ちないか
  • 7、テスト用ファイルの作り方:1件だけ text を数値にする、期待する挙動:項目の形式エラーで全体を中止、人が見るところ:「一部だけ読み込む」状態にならないか
  • 8、テスト用ファイルの作り方:画像など別形式のファイルを選ぶ、期待する挙動:JSONエラー、人が見るところ:拡張子だけで判断していないか
  • 9、テスト用ファイルの作り方:日本語・絵文字を含む正常なファイル、期待する挙動:文字化けなく復元、人が見るところ:書き出し→読み込みで文字が一致するか
  • 10、テスト用ファイルの作り方:読み込み成功後に、退避キーを確認、期待する挙動:読み込み前のデータが残っている、人が見るところ:開発者ツールのStorage欄で確認

判断の基準は単純で、失敗した場合は、保存データがバイト単位で変わっていないことです。エラーメッセージの文面よりも、これを優先して確認してください。

動作確認と、利用者への伝え方

  1. 本物のデータではなく、ダミーのメモを数件入れた状態で書き出しを押し、ダウンロードされたファイルをエディタで開いて中身を見る
  2. 上の表の1〜10を順に実行する。結果は自分で表に◯×で記録する
  3. 使っているブラウザの開発者ツールでlocalStorageを全削除し、書き出したファイルから復元できることを確かめる(「消えた状態から戻せる」ことが本来の目的です)
  4. ボタンの近くに「定期的に書き出してください。ブラウザの設定やストレージ不足でデータが消えることがあります」と一文添える

AIが返したコードを貼り付けるだけでは、4の案内文や表の確認は抜けがちです。コードの生成はAIに任せ、仕様の決定、壊れたファイルでのテスト、実ブラウザでの復元確認は人が行うと役割を分けると、安全に仕上がります。

また、書き出したJSONは平文です。実際に個人的な内容を入れて使うツールなら、利用者に「ファイルの保管場所に注意してください」と伝えておくと親切です。

出典・参考資料