ブラウザにおけるファイル操作の基礎:読み込み・アップロード・ダウンロードの実装パターン

ブラウザの機能拡張に伴い、複雑なサードパーティ製ライブラリに依存せず、標準 Web API だけでファイル入出力を実現するケースが増加しています。以下では、ネイティブ JavaScript を用いた代表的なファイル処理フローを整理します。

1. ファイル読取とフォームベース送信

ユーザーがローカルから選択したファイルを FileReader でテキスト化し、FormData でパッケージ化してサーバーへ転送する基本的なパターンです。UI は最小限のスタイリングとし、チェックボックスの状態に応じた入力フィールドの表示切り替えも実装しています。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <style>
    .upload-trigger { padding: 8px 14px; background: #eef5ff; border: 1px solid #b0c8e8; cursor: pointer; border-radius: 4px; }
    .upload-trigger input { position: absolute; opacity: 0; width: 100%; height: 100%; top: 0; left: 0; cursor: pointer; }
    .config-panel { margin-top: 12px; padding: 10px; border: 1px dashed #ccc; }
    .hidden { display: none; }
  </style>
</head>
<body>
  <div class="form-group">
    <label class="upload-trigger">
      参照ボタン
      <input type="file" id="sourceInput" accept=".json,.csv,.txt">
    </label>
    <button onclick="submitProcessedFile()">データ送信</button>
    <pre id="outputLog"></pre>
  </div>

  <div class="config-panel">
    <label><input type="checkbox" id="chkOverride" onchange="toggleMetaFields()"> 既存レコードを上書きする</label>
    <div id="extraFields" class="hidden" style="margin-top:8px;">
      <input type="text" id="targetDbName" placeholder="DB識別名" style="margin-right:8px;">
      <input type="text" id="targetLabel" placeholder="表示ラベル">
    </div>
  </div>

  <div style="margin-top:20px;">
    <input type="text" id="reqTableId" placeholder="対象テーブルID">
    <button onclick="saveMetadataLocally()">メタ情報をダウンロード</button>
  </div>

  <script>
    function toggleMetaFields() {
      const isChecked = document.getElementById('chkOverride').checked;
      document.getElementById('extraFields').classList.toggle('hidden', isChecked);
    }

    async function submitProcessedFile() {
      const fileEl = document.getElementById('sourceInput');
      const targetFile = fileEl.files[0];
      if (!targetFile) return alert('ファイルを選択してください');

      const reader = new FileReader();
      reader.onload = async (evt) => {
        try {
          const formData = new FormData();
          formData.append('payload', evt.target.result);
          formData.append('allowReplace', document.getElementById('chkOverride').checked);
          formData.append('overrideDb', 'sample_db_v2');
          formData.append('overrideLabel', '一括インポート用');

          const resp = await fetch('/api/import/config', {
            method: 'POST',
            body: formData
          });
          const json = await resp.json();
          document.getElementById('outputLog').textContent = json.statusMsg || '処理正常終了';
        } catch (err) {
          console.error('転送失敗:', err);
          document.getElementById('outputLog').textContent = 'ネットワークエラーが発生しました';
        }
      };
      reader.readAsText(targetFile);
    }

    async function saveMetadataLocally() {
      const tableId = document.getElementById('reqTableId').value;
      if (!tableId) return alert('テーブルIDを入力してください');

      try {
        const res = await fetch('/api/export/schema', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ tableId })
        });
        const data = await res.json();
        if (data.code === 200) {
          const blob = new Blob([data.content], { type: 'application/json;charset=utf-8' });
          const tempUrl = URL.createObjectURL(blob);
          const anchor = document.createElement('a');
          anchor.href = tempUrl;
          anchor.download = `schema_${tableId}.json`;
          anchor.click();
          URL.revokeObjectURL(tempUrl);
        } else {
          alert(data.errMsg || 'スキーマ取得に失敗しました');
        }
      } catch (err) {
        console.error('保存失敗:', err);
      }
    }
  </script>
</body>
</html>

2. JSONペイロード送信とバイナリエクスポート

バックエンドへフィルター条件を JSON 形式で送信し、返却されるバイナリデータ(ExcelやCSVなど)をクライアント側でダウンロードする実装です。XHR に responseType: 'blob' を設定し、生成されたオブジェクト URL を一時的に付与したアンカータグをクリックさせることで保存処理を実行します。

/**
 * 検索条件を指定してレポートデータを取得・保存
 */
async function exportReport() {
  const filterCriteria = collectQueryParams();
  const endpoint = '/reports/generate/daily';
  
  const xhr = new XMLHttpRequest();
  xhr.open('POST', endpoint, true);
  xhr.setRequestHeader('Content-Type', 'application/json');
  xhr.responseType = 'blob';

  xhr.onload = () => {
    if (xhr.status === 200 && xhr.response.size > 0) {
      const fileBlob = xhr.response;
      const objectUrl = URL.createObjectURL(fileBlob);
      
      const dlAnchor = document.createElement('a');
      dlAnchor.style.display = 'none';
      dlAnchor.href = objectUrl;
      dlAnchor.download = 'daily_report.xlsx';
      document.body.appendChild(dlAnchor);
      
      dlAnchor.click();
      
      // リソース解放防止のため DOM と URL をクリーンアップ
      setTimeout(() => {
        document.body.removeChild(dlAnchor);
        URL.revokeObjectURL(objectUrl);
      }, 100);
    } else {
      console.warn(`エクスポート応答ステータス: ${xhr.status}`);
    }
  };

  xhr.onerror = () => console.error('通信が切断されました');
  xhr.send(JSON.stringify(filterCriteria));
}

function collectQueryParams() {
  // フィルター値の収集ロジック
  return { status: 'completed', period: 'last_7_days' };
}

3. サーバーサイドのレスポンス制御(Java)

バイナリストリームを HTTP レスポンスとして送信する際、ファイル名の文字化けを防ぐため UTF-8 エンコーディングを適用するのが現代の標準的な実装です。従来ブラウザごとの分岐処理は省略し、規格に準拠したシンプルな記述に統一しています。

private void sendBinaryStream(HttpServletResponse response, String rawFilename, Workbook workbook) throws IOException {
    String encodedName = URLEncoder.encode(rawFilename, StandardCharsets.UTF_8.name());
    
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setHeader("Content-Disposition", String.format("attachment; filename*=UTF-8''%s", encodedName));

    try (OutputStream out = response.getOutputStream()) {
        workbook.write(out);
        out.flush();
    } catch (Exception e) {
        throw new RuntimeException("ファイルストリームの書き込みに失敗しました", e);
    }
}

タグ: javascript BlobAPI FormData FileReader XMLHttpRequest

5月14日 11:20 投稿