JavaScript/File API

File API

1. File APIの概要

File APIは、Webアプリケーションがユーザーのローカルファイルシステムにアクセスし、ファイルを読み込んで操作するための標準化されたインターフェースです。この技術により、クライアントサイドのJavaScriptだけで、サーバーにアップロードする前にファイルの内容を読み取ったり、処理したりすることが可能になりました。

File APIは、主に以下のコンポーネントから構成されています:

  • File:ファイルに関するメタデータと内容にアクセスするためのインターフェース
  • FileList:複数のFileオブジェクトを表すリストインターフェース
  • Blob:バイナリデータの塊を表す基本的なインターフェース
  • FileReader:ファイルの内容を非同期に読み込むためのインターフェース

File APIの登場により、以前は不可能だったブラウザ上での高度なファイル操作が実現しました。例えば、画像のプレビュー表示、クライアントサイドでのファイル検証、ブラウザ内でのファイル編集などが、サーバーに送信する前に可能になっています。

現在のブラウザ対応状況は非常に良好で、主要なブラウザ(Chrome、Firefox、Safari、Edge)のすべてが基本的なFile API機能をサポートしています。ただし、一部の高度な機能については、ブラウザ間で実装の違いが存在することもあります。

2. Fileインターフェース

Fileインターフェースは、ユーザーのファイルシステム上のファイルを表すオブジェクトです。FileBlobを継承しており、ファイル名やファイルの最終更新日などの追加情報を提供します。

Fileオブジェクトの基本

Fileオブジェクトは、主に<input type="file">要素やドラッグ&ドロップイベントを通じて取得します。

// input要素からFileオブジェクトを取得する例
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', function() {
  const selectedFile = this.files[0]; // 最初に選択されたファイル
  console.log('選択されたファイル:', selectedFile);
});

Fileのプロパティ

Fileオブジェクトには、以下の重要なプロパティがあります:

プロパティ名 説明
name ファイル名(パスを含まない) 'document.pdf'
size ファイルサイズ(バイト単位) 2048
type MIMEタイプ 'application/pdf'
lastModified 最終更新日時(ミリ秒単位のUNIXタイムスタンプ) 1609459200000
lastModifiedDate 最終更新日時(Dateオブジェクト、非推奨) Date('2021-01-01T00:00:00.000Z')

これらのプロパティを活用して、ファイルの基本情報を取得できます:

function showFileInfo(file) {
  const fileInfo = document.getElementById('fileInfo');
  
  const fileSize = file.size < 1024 
    ? `${file.size} bytes` 
    : file.size < 1048576 
      ? `${(file.size / 1024).toFixed(2)} KB` 
      : `${(file.size / 1048576).toFixed(2)} MB`;
  
  const lastModified = new Date(file.lastModified).toLocaleString();
  
  fileInfo.innerHTML = `
    ファイル名: ${file.name}<br>
    タイプ: ${file.type || 'unknown'}<br>
    サイズ: ${fileSize}<br>
    最終更新日: ${lastModified}
  `;
}

FileListオブジェクト

FileListは、複数のFileオブジェクトを含む配列のようなオブジェクトです。主に以下の方法で取得されます:

  1. <input type="file" multiple>要素のfilesプロパティ
  2. ドラッグ&ドロップイベントのdataTransfer.filesプロパティ

FileListは配列ではありませんが、インデックスアクセスとlengthプロパティを持ち、イテラブルです:

// 複数ファイル選択の処理例
const multiFileInput = document.querySelector('input[type="file"][multiple]');
multiFileInput.addEventListener('change', function() {
  const fileList = this.files;
  console.log(`${fileList.length}個のファイルが選択されました`);
  
  // FileListを反復処理する
  for (let i = 0; i < fileList.length; i++) {
    console.log(`ファイル ${i+1}: ${fileList[i].name}`);
  }
  
  // または、配列に変換して処理する
  Array.from(fileList).forEach((file, index) => {
    console.log(`ファイル ${index+1}: ${file.name}`);
  });
});

3. Blobインターフェース

Blobは「Binary Large Object」の略で、イメージデータやオーディオデータなどの不変のバイナリデータを表します。FileオブジェクトはBlobを継承しているため、Blobのすべての機能を使用できます。

Blobコンストラクタ

新しいBlobオブジェクトは、以下のコンストラクタを使用して作成できます:

const blob = new Blob(blobParts, options);

例えば、テキストからBlobを作成する場合:

const textBlob = new Blob(['こんにちは、世界!'], {type: 'text/plain'});
console.log(`Blobのサイズ: ${textBlob.size} バイト`);
console.log(`Blobのタイプ: ${textBlob.type}`);

画像データを含むBlobを作成する例:

// Canvas要素から画像データを取得してBlobを作成
const canvas = document.getElementById('myCanvas');
canvas.toBlob(function(blob) {
  const imgUrl = URL.createObjectURL(blob);
  const img = document.createElement('img');
  img.src = imgUrl;
  document.body.appendChild(img);
}, 'image/jpeg', 0.95); // JPEG形式、品質95%

Blobのslice()メソッド

大きなBlobを小さな部分に分割するには、slice()メソッドを使用します:

const partialBlob = originalBlob.slice(start, end, contentType);
  • start: 開始バイト位置(デフォルトは0)
  • end: 終了バイト位置(デフォルトはblob.size)
  • contentType: 新しいBlobのMIMEタイプ(デフォルトは元のBlobのtype)

これは、大きなファイルを小さなチャンクに分割してアップロードする際に特に役立ちます:

// 大きなファイルを1MBごとにチャンクに分割する例
function splitIntoChunks(blob, chunkSize) {
  const chunks = [];
  const size = blob.size;
  let start = 0;
  
  while (start < size) {
    const end = Math.min(start + chunkSize, size);
    const chunk = blob.slice(start, end, blob.type);
    chunks.push(chunk);
    start = end;
  }
  
  return chunks;
}

const largeFile = document.getElementById('largeFile').files[0];
const chunks = splitIntoChunks(largeFile, 1024 * 1024); // 1MBチャンク
console.log(`ファイルは${chunks.length}個のチャンクに分割されました`);

Blobプロパティ

Blobには以下の主要なプロパティがあります:

プロパティ名 説明
size Blobのサイズ(バイト単位) 1024
type BlobのMIMEタイプ 'image/png'

4. FileReaderインターフェース

FileReaderは、Fileの内容を非同期に読み込むためのインターフェースです。ファイルの内容をさまざまな形式(テキスト、データURL、ArrayBuffer、バイナリ文字列)で読み込むことができます。

FileReaderオブジェクトの作成と使用

const reader = new FileReader();

// イベントハンドラを設定
reader.onload = function(event) {
  // 読み込み完了時の処理
  const content = event.target.result;
  console.log('ファイル内容:', content);
};@

reader.onerror = function() {
  console.error('ファイル読み込みエラー');
};

// ファイルを読み込む
const file = document.querySelector('input[type="file"]').files[0];
reader.readAsText(file); // テキストとして読み込み

読み込みメソッド

FileReaderには、ファイルを読み込むための以下のメソッドがあります:

メソッド 説明 結果の形式
readAsText(blob, [encoding]) テキストとして読み込む 文字列
readAsDataURL(blob) Data URIスキームとして読み込む data: URL文字列
readAsArrayBuffer(blob) ArrayBufferとして読み込む ArrayBuffer
readAsBinaryString(blob) バイナリ文字列として読み込む(非推奨) バイナリ文字列

それぞれの用途に応じた使用例は以下の通りです:

const file = document.getElementById('fileInput').files[0];
const reader = new FileReader();

// テキストファイルを読み込む
function readAsText() {
  reader.onload = function(e) {
    document.getElementById('output').textContent = e.target.result;
  };
  reader.readAsText(file, 'UTF-8'); // エンコーディングを指定
}

// 画像をプレビュー表示する
function previewImage() {
  reader.onload = function(e) {
    const img = document.getElementById('preview');
    img.src = e.target.result; // Data URLを直接img.srcに設定
  };
  reader.readAsDataURL(file);
}

// バイナリデータを操作する
function processArrayBuffer() {
  reader.onload = function(e) {
    const arrayBuffer = e.target.result;
    const byteArray = new Uint8Array(arrayBuffer);
    // バイナリデータを処理...
    console.log(`最初の10バイト: ${byteArray.slice(0, 10)}`);
  };
  reader.readAsArrayBuffer(file);
}

イベント処理

FileReaderには、読み込み処理の進行状況を追跡するための以下のイベントがあります:

イベント 説明
onloadstart 読み込み開始時に発生
onprogress 読み込み中に定期的に発生(進行状況の追跡に使用)
onload 読み込みが正常に完了した時に発生
onabort 読み込みが中断された時に発生
onerror 読み込み中にエラーが発生した時に発生
onloadend 読み込みが完了(成功/失敗/中断)した時に発生

進行状況を表示する例:

function readLargeFile(file) {
  const reader = new FileReader();
  const progressBar = document.getElementById('progressBar');
  const status = document.getElementById('status');
  
  reader.onloadstart = function() {
    status.textContent = '読み込み開始...';
    progressBar.value = 0;
  };
  
  reader.onprogress = function(e) {
    if (e.lengthComputable) {
      const percentLoaded = Math.round((e.loaded / e.total) * 100);
      progressBar.value = percentLoaded;
      status.textContent = `読み込み中... ${percentLoaded}%`;
    }
  };
  
  reader.onload = function() {
    progressBar.value = 100;
    status.textContent = '読み込み完了!';
    // 読み込んだデータを処理...
  };
  
  reader.onerror = function() {
    status.textContent = 'エラーが発生しました';
    console.error('FileReader error:', reader.error);
  };
  
  reader.readAsArrayBuffer(file);
}

非同期処理の扱い方

FileReaderは非同期APIであるため、Promiseを使用してより整理された方法で操作できます:

function readFileAsText(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    
    reader.onload = function() {
      resolve(reader.result);
    };
    
    reader.onerror = function() {
      reject(reader.error);
    };
    
    reader.readAsText(file);
  });
}

// 使用例
async function processTextFile(file) {
  try {
    const content = await readFileAsText(file);
    console.log('ファイル内容:', content);
    return content;
  } catch (error) {
    console.error('ファイル読み込みエラー:', error);
    throw error;
  }
}

// 複数のファイルを順番に処理
async function processMultipleFiles(fileList) {
  const results = [];
  
  for (const file of fileList) {
    const content = await readFileAsText(file);
    results.push({
      name: file.name,
      content: content
    });
  }
  
  return results;
}

5. URL APIとBlob URL

URL APIは、BlobFileオブジェクトへの参照を表すURLを作成するためのメソッドを提供します。これらのURLは、画像のプレビューやダウンロードリンクの作成などに役立ちます。

URL.createObjectURL()

この方法は、メモリ内のファイルやBlobオブジェクトを参照するURLを生成します:

const file = document.getElementById('imageFile').files[0];
const imageUrl = URL.createObjectURL(file);

const img = document.createElement('img');
img.src = imageUrl;
document.body.appendChild(img);

生成されたURLは、現在のドキュメントのライフサイクル中のみ有効です。URLの形式は、blob:http://example.com/550e8400-e29b-41d4-a716-446655440000のようになります。

URL.revokeObjectURL()

メモリリークを防ぐため、Blob URLが不要になったら必ず解放すべきです:

function createImagePreview(file) {
  const preview = document.getElementById('preview');
  const imageUrl = URL.createObjectURL(file);
  
  preview.onload = function() {
    // 画像が読み込まれたらURLを解放
    URL.revokeObjectURL(imageUrl);
  };
  
  preview.src = imageUrl;
}

Blob URLの用途と注意点

Blob URLの主な用途は以下の通りです:

  1. ファイルプレビュー(画像、動画、オーディオなど)
  2. ダウンロードリンクの作成
  3. iframe内での表示
    // ダウンロードリンクの作成例
    function createDownloadLink(file) {
      const link = document.createElement('a');
      link.href = URL.createObjectURL(file);
      link.download = file.name; // ダウンロード時のファイル名を指定
      link.textContent = `${file.name}をダウンロード`;
      
      // クリックイベントを追加
      link.addEventListener('click', function() {
        // クリック後、少し遅延させてからURLを解放
        setTimeout(() => {
          URL.revokeObjectURL(link.href);
        }, 100);
      });
      
      document.body.appendChild(link);
    }
    

注意点:

  1. Blob URLはメモリリソースを消費するため、不要になったら必ずrevokeObjectURL()で解放する
  2. revokeObjectURL()を呼び出すタイミングは、URLの使用が完全に終了した後にすること
  3. 大量のBlob URLを作成する場合は、使用後すぐに解放するよう特に注意する

6. ドラッグ&ドロップとファイル操作

ドラッグ&ドロップは、ユーザーフレンドリーなファイルアップロードインターフェースを提供するための効果的な方法です。File APIと組み合わせることで、ドラッグされたファイルに直接アクセスできます。

DataTransferインターフェース

ドラッグ&ドロップ操作では、DataTransferオブジェクトがイベントオブジェクトのdataTransferプロパティとして提供されます。このオブジェクトのfilesプロパティから、ドラッグされたファイルにアクセスできます。

function setupDragAndDrop() {
  const dropZone = document.getElementById('dropZone');
  
  // ドラッグオーバーイベントのデフォルト動作を防止
  dropZone.addEventListener('dragover', function(e) {
    e.preventDefault();
    e.stopPropagation();
    this.classList.add('highlight');
  });
  
  // ドラッグを離れたときのスタイル変更
  dropZone.addEventListener('dragleave', function() {
    this.classList.remove('highlight');
  });
  
  // ドロップ処理
  dropZone.addEventListener('drop', function(e) {
    e.preventDefault();
    e.stopPropagation();
    this.classList.remove('highlight');
    
    const files = e.dataTransfer.files;
    if (files.length > 0) {
      handleFiles(files);
    }
  });
}

// ドロップされたファイルを処理する関数
function handleFiles(files) {
  const output = document.getElementById('fileList');
  output.innerHTML = '';
  
  Array.from(files).forEach(file => {
    const fileInfo = document.createElement('div');
    fileInfo.className = 'file-info';
    fileInfo.textContent = `${file.name} (${formatFileSize(file.size)})`;
    output.appendChild(fileInfo);
    
    // 画像の場合はプレビューを表示
    if (file.type.match('image.*')) {
      const reader = new FileReader();
      reader.onload = function(e) {
        const img = document.createElement('img');
        img.src = e.target.result;
        img.className = 'preview';
        fileInfo.appendChild(img);
      };
      reader.readAsDataURL(file);
    }
  });
}

// ファイルサイズのフォーマット関数
function formatFileSize(bytes) {
  if (bytes < 1024) return bytes + ' bytes';
  else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
  else return (bytes / 1048576).toFixed(1) + ' MB';
}

このコードは、ドロップゾーンを設定し、ドロップされたファイルのリストを表示します。画像ファイルの場合は、プレビューも表示します。

ドラッグ&ドロップでのFile APIの利用法

より高度な例として、ドラッグ&ドロップで複数のファイルをアップロードし、プログレスバーで進行状況を表示する実装を見てみましょう:

function setupAdvancedDragDrop() {
  const dropZone = document.getElementById('advancedDropZone');
  const fileList = document.getElementById('advancedFileList');
  const uploadButton = document.getElementById('uploadButton');
  let droppedFiles = [];
  
  // ドラッグ&ドロップイベントの設定
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
    dropZone.addEventListener(eventName, preventDefaults, false);
  });
  
  function preventDefaults(e) {
    e.preventDefault();
    e.stopPropagation();
  }
  
  // ハイライト効果
  ['dragenter', 'dragover'].forEach(eventName => {
    dropZone.addEventListener(eventName, highlight, false);
  });
  
  ['dragleave', 'drop'].forEach(eventName => {
    dropZone.addEventListener(eventName, unhighlight, false);
  });
  
  function highlight() {
    dropZone.classList.add('active');
  }
  
  function unhighlight() {
    dropZone.classList.remove('active');
  }
  
  // ファイルのドロップ処理
  dropZone.addEventListener('drop', handleDrop, false);
  
  function handleDrop(e) {
    const dt = e.dataTransfer;
    const files = dt.files;
    droppedFiles = [...files];
    
    displayFileList();
    uploadButton.disabled = false;
  }
  
  // ファイルリストの表示
  function displayFileList() {
    fileList.innerHTML = '';
    
    droppedFiles.forEach((file, index) => {
      const item = document.createElement('div');
      item.className = 'file-item';
      
      const info = document.createElement('div');
      info.className = 'file-info';
      info.innerHTML = `<strong>${file.name}</strong> (${formatFileSize(file.size)})`;
      
      const progress = document.createElement('progress');
      progress.id = `progress-${index}`;
      progress.value = 0;
      progress.max = 100;
      
      const status = document.createElement('span');
      status.className = 'status';
      status.textContent = '準備完了';
      
      item.appendChild(info);
      item.appendChild(progress);
      item.appendChild(status);
      fileList.appendChild(item);
    });
  }
  
  // アップロードボタンの処理
  uploadButton.addEventListener('click', () => {
    uploadFiles();
  });
  
  // ファイルのアップロード処理(シミュレーション)
  function uploadFiles() {
    droppedFiles.forEach((file, index) => {
      const progress = document.getElementById(`progress-${index}`);
      const item = progress.parentElement;
      const status = item.querySelector('.status');
      
      status.textContent = 'アップロード中...';
      
      // プログレスシミュレーション(実際のアップロードコードに置き換える)
      let percent = 0;
      const interval = setInterval(() => {
        percent += 5;
        progress.value = percent;
        
        if (percent >= 100) {
          clearInterval(interval);
          status.textContent = '完了';
          item.classList.add('uploaded');
        }
      }, 200);
      
      // 実際のアップロード処理はここに実装(例:XHR/Fetch APIを使用)
    });
  }
}

7. <input type="file">とFile API

HTML5の<input type="file">要素は、File APIと組み合わせることで強力なファイル選択・操作機能を提供します。

ファイル選択UIの活用

<input type="file" id="fileInput" accept="image/*">
<div id="preview"></div>
document.getElementById('fileInput').addEventListener('change', function() {
  const file = this.files[0];
  if (file) {
    const reader = new FileReader();
    
    reader.onload = function(e) {
      const preview = document.getElementById('preview');
      preview.innerHTML = '';
      
      const img = document.createElement('img');
      img.src = e.target.result;
      img.alt = file.name;
      img.style.maxWidth = '100%';
      
      preview.appendChild(img);
    };
    
    reader.readAsDataURL(file);
  }
});

複数ファイル選択

multiple属性を追加すると、複数のファイルを選択できるようになります:

<input type="file" id="multipleFiles" multiple>
<div id="multiPreview"></div>
document.getElementById('multipleFiles').addEventListener('change', function() {
  const files = this.files;
  const preview = document.getElementById('multiPreview');
  preview.innerHTML = '';
  
  if (files.length > 0) {
    Array.from(files).forEach(file => {
      if (file.type.match('image.*')) {
        const reader = new FileReader();
        
        reader.onload = function(e) {
          const imgContainer = document.createElement('div');
          imgContainer.className = 'img-container';
          
          const img = document.createElement('img');
          img.src = e.target.result;
          img.alt = file.name;
          img.className = 'thumbnail';
          
          const caption = document.createElement('p');
          caption.textContent = file.name;
          
          imgContainer.appendChild(img);
          imgContainer.appendChild(caption);
          preview.appendChild(imgContainer);
        };
        
        reader.readAsDataURL(file);
      }
    });
  }
});

accept属性を使ったファイルタイプフィルタリング

accept属性を使用すると、特定のファイルタイプのみを許可できます:

説明
MIME タイプ 特定のMIMEタイプ 'image/jpeg', 'application/pdf'
ファイル拡張子 特定の拡張子 '.jpg', '.pdf', '.docx'
MIME タイプの一部 タイプのグループ 'image/*', 'audio/*'
<!-- 画像のみ -->
<input type="file" accept="image/*">

<!-- PDFと特定の文書形式 -->
<input type="file" accept=".pdf,.docx,.xlsx">

<!-- 複数の種類を組み合わせる -->
<input type="file" accept="image/jpeg,image/png,application/pdf">

以下は、より高度なファイル選択コンポーネントの実装例です:

function createCustomFileInput() {
  const container = document.getElementById('customFileInput');
  const fileInput = document.createElement('input');
  fileInput.type = 'file';
  fileInput.id = 'hiddenFileInput';
  fileInput.multiple = true;
  fileInput.accept = 'image/*,.pdf';
  fileInput.style.display = 'none';
  
  const button = document.createElement('button');
  button.textContent = 'ファイルを選択';
  button.className = 'custom-file-button';
  
  const fileList = document.createElement('div');
  fileList.className = 'custom-file-list';
  
  // ボタンクリックでファイル選択ダイアログを開く
  button.addEventListener('click', () => {
    fileInput.click();
  });
  
  // ファイル選択時の処理
  fileInput.addEventListener('change', () => {
    fileList.innerHTML = '';
    
    if (fileInput.files.length > 0) {
      const heading = document.createElement('h3');
      heading.textContent = '選択されたファイル:';
      fileList.appendChild(heading);
      
      const list = document.createElement('ul');
      
      Array.from(fileInput.files).forEach(file => {
        const item = document.createElement('li');
        
        // ファイルタイプに応じたアイコンを表示
        let icon = '📄';
        if (file.type.startsWith('image/')) {
          icon = '🖼️';
        } else if (file.type === 'application/pdf') {
          icon = '📑';
        }
        
        item.innerHTML = `${icon} <strong>${file.name}</strong> (${formatFileSize(file.size)})`;
        list.appendChild(item);
      });
      
      fileList.appendChild(list);
    }
  });
  
// コンテナに要素を追加
  container.appendChild(fileInput);
  container.appendChild(button);
  container.appendChild(fileList);
  
  // ドラッグ&ドロップ機能の追加
  container.addEventListener('dragover', (e) => {
    e.preventDefault();
    container.classList.add('dragover');
  });
  
  container.addEventListener('dragleave', () => {
    container.classList.remove('dragover');
  });
  
  container.addEventListener('drop', (e) => {
    e.preventDefault();
    container.classList.remove('dragover');
    
    if (e.dataTransfer.files.length > 0) {
      fileInput.files = e.dataTransfer.files;
      const event = new Event('change');
      fileInput.dispatchEvent(event);
    }
  });
}

8. File APIの実践的使用例

File APIを使用した実践的な例として、いくつかの一般的なユースケースを詳しく見ていきましょう。

画像プレビュー

ユーザーが画像ファイルを選択したときに、即座にプレビューを表示する機能は、Webアプリケーションでよく使われています。

function createImagePreviewSystem() {
  const fileInput = document.getElementById('imageUpload');
  const previewContainer = document.getElementById('previewContainer');
  const errorMessage = document.getElementById('errorMessage');
  
  fileInput.addEventListener('change', function() {
    // 前のプレビューと表示をクリア
    previewContainer.innerHTML = '';
    errorMessage.textContent = '';
    
    // 各ファイルを処理
    const files = this.files;
    Array.from(files).forEach(file => {
      // ファイルが画像かどうかをチェック
      if (!file.type.match('image.*')) {
        errorMessage.textContent = '画像ファイルのみアップロードできます。';
        return;
      }
      
      // ファイルサイズを確認(5MB以下)
      if (file.size > 5 * 1024 * 1024) {
        errorMessage.textContent = 'ファイルサイズは5MB以下にしてください。';
        return;
      }
      
      // プレビュー表示用の要素を作成
      const previewItem = document.createElement('div');
      previewItem.className = 'preview-item';
      
      // 読み込み中表示
      const loadingIndicator = document.createElement('div');
      loadingIndicator.className = 'loading';
      loadingIndicator.textContent = '読み込み中...';
      previewItem.appendChild(loadingIndicator);
      
      // コンテナに追加
      previewContainer.appendChild(previewItem);
      
      // FileReaderを使って画像を読み込み
      const reader = new FileReader();
      
      reader.onload = function(e) {
        // 読み込み完了したら画像を表示
        previewItem.innerHTML = '';
        
        const img = document.createElement('img');
        img.src = e.target.result;
        img.className = 'preview-image';
        
        const fileName = document.createElement('div');
        fileName.className = 'file-name';
        fileName.textContent = file.name;
        
        const fileSize = document.createElement('div');
        fileSize.className = 'file-size';
        fileSize.textContent = formatFileSize(file.size);
        
        // 削除ボタン
        const removeButton = document.createElement('button');
        removeButton.className = 'remove-button';
        removeButton.textContent = '削除';
        removeButton.addEventListener('click', function() {
          previewItem.remove();
        });
        
        previewItem.appendChild(img);
        previewItem.appendChild(fileName);
        previewItem.appendChild(fileSize);
        previewItem.appendChild(removeButton);
      };
      
      reader.onerror = function() {
        previewItem.innerHTML = '';
        const errorDiv = document.createElement('div');
        errorDiv.className = 'error';
        errorDiv.textContent = 'ファイルの読み込みに失敗しました。';
        previewItem.appendChild(errorDiv);
      };
      
      // 画像を読み込む
      reader.readAsDataURL(file);
    });
  });
}

この実装では、以下の機能を備えています:

  1. 画像ファイルのみ受け付ける
  2. ファイルサイズの制限(5MB)
  3. 読み込み中の表示
  4. 複数画像のプレビュー
  5. ファイル名・サイズの表示
  6. 個別の画像を削除する機能

ファイルアップロード

ファイルのアップロード処理は、通常、サーバーサイドとの連携が必要です。以下に、Fetch APIを使った実装例を示します。

function createFileUploader() {
  const fileInput = document.getElementById('uploadFiles');
  const uploadButton = document.getElementById('startUpload');
  const progressContainer = document.getElementById('progressContainer');
  
  uploadButton.addEventListener('click', async function() {
    const files = fileInput.files;
    if (files.length === 0) {
      alert('アップロードするファイルを選択してください。');
      return;
    }
    
    progressContainer.innerHTML = '';
    
    // 各ファイルを個別にアップロード
    for (const file of files) {
      await uploadFile(file);
    }
    
    alert('すべてのファイルのアップロードが完了しました。');
  });
  
  async function uploadFile(file) {
    // プログレス表示用の要素を作成
    const progressItem = document.createElement('div');
    progressItem.className = 'progress-item';
    
    const fileName = document.createElement('div');
    fileName.className = 'file-name';
    fileName.textContent = file.name;
    
    const progressBar = document.createElement('progress');
    progressBar.max = 100;
    progressBar.value = 0;
    
    const statusText = document.createElement('span');
    statusText.className = 'status';
    statusText.textContent = '準備中...';
    
    progressItem.appendChild(fileName);
    progressItem.appendChild(progressBar);
    progressItem.appendChild(statusText);
    progressContainer.appendChild(progressItem);
    
    // FormDataオブジェクトを作成
    const formData = new FormData();
    formData.append('file', file);
    
    try {
      // アップロード処理実行
      const response = await new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        
        // プログレスイベントの設定
        xhr.upload.addEventListener('progress', (e) => {
          if (e.lengthComputable) {
            const percentComplete = Math.round((e.loaded / e.total) * 100);
            progressBar.value = percentComplete;
            statusText.textContent = `${percentComplete}% 完了`;
          }
        });
        
        xhr.addEventListener('load', () => {
          if (xhr.status >= 200 && xhr.status < 300) {
            resolve(xhr.responseText);
          } else {
            reject(new Error(`HTTPエラー: ${xhr.status}`));
          }
        });
        
        xhr.addEventListener('error', () => {
          reject(new Error('ネットワークエラーが発生しました。'));
        });
        
        xhr.addEventListener('abort', () => {
          reject(new Error('アップロードが中断されました。'));
        });
        
        // リクエスト開始
        xhr.open('POST', '/api/upload', true);
        xhr.send(formData);
      });
      
      // 成功時の処理
      progressItem.classList.add('success');
      statusText.textContent = 'アップロード完了';
      return response;
      
    } catch (error) {
      // エラー時の処理
      progressItem.classList.add('error');
      statusText.textContent = `エラー: ${error.message}`;
      throw error;
    }
  }
}

この実装では、以下の機能を備えています:

  1. 複数ファイルの順次アップロード
  2. XHRを使った進行状況の表示
  3. エラーハンドリング
  4. 成功/失敗の視覚的フィードバック

クライアントサイドでのファイル処理

ファイルをサーバーにアップロードせずに、クライアントサイドで処理する例として、CSVファイルを解析して表示するケースを見てみましょう。

function createCSVParser() {
  const fileInput = document.getElementById('csvFile');
  const parseButton = document.getElementById('parseCSV');
  const resultTable = document.getElementById('resultTable');
  const errorMessage = document.getElementById('csvError');
  
  parseButton.addEventListener('click', function() {
    const file = fileInput.files[0];
    if (!file) {
      errorMessage.textContent = 'CSVファイルを選択してください。';
      return;
    }
    
    if (file.type !== 'text/csv' && !file.name.endsWith('.csv')) {
      errorMessage.textContent = 'CSVファイル形式のみ対応しています。';
      return;
    }
    
    errorMessage.textContent = '';
    resultTable.innerHTML = '<tr><td>読み込み中...</td></tr>';
    
    const reader = new FileReader();
    
    reader.onload = function(e) {
      try {
        const csvContent = e.target.result;
        const data = parseCSV(csvContent);
        displayCSVData(data);
      } catch (error) {
        errorMessage.textContent = `CSVの解析に失敗しました: ${error.message}`;
        resultTable.innerHTML = '';
      }
    };
    
    reader.onerror = function() {
      errorMessage.textContent = 'ファイルの読み込みに失敗しました。';
      resultTable.innerHTML = '';
    };
    
    reader.readAsText(file);
  });
  
  // CSVデータを解析する関数
  function parseCSV(csvText) {
    const lines = csvText.split(/\r\n|\n/);
    const result = [];
    
    for (let i = 0; i < lines.length; i++) {
      if (lines[i].trim() === '') continue;
      
      // カンマで分割(引用符内のカンマは考慮する)
      const row = [];
      let inQuotes = false;
      let currentValue = '';
      
      for (let j = 0; j < lines[i].length; j++) {
        const char = lines[i][j];
        
        if (char === '"' && (j === 0 || lines[i][j-1] !== '\\')) {
          inQuotes = !inQuotes;
        } else if (char === ',' && !inQuotes) {
          row.push(currentValue);
          currentValue = '';
        } else {
          currentValue += char;
        }
      }
      
      // 最後の値を追加
      row.push(currentValue);
      result.push(row);
    }
    
    return result;
  }
  
  // CSVデータをテーブルとして表示
  function displayCSVData(data) {
    if (data.length === 0) {
      errorMessage.textContent = 'CSVファイルにデータがありません。';
      resultTable.innerHTML = '';
      return;
    }
    
    let tableHTML = '';
    
    // ヘッダー行
    tableHTML += '<tr>';
    data[0].forEach(header => {
      tableHTML += `<th>${escapeHTML(header)}</th>`;
    });
    tableHTML += '</tr>';
    
    // データ行
    for (let i = 1; i < data.length; i++) {
      tableHTML += '<tr>';
      data[i].forEach(cell => {
        tableHTML += `<td>${escapeHTML(cell)}</td>`;
      });
      tableHTML += '</tr>';
    }
    
    resultTable.innerHTML = tableHTML;
  }
  
  // HTMLエスケープ処理
  function escapeHTML(str) {
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }
}

このCSV解析ツールは、以下の機能を備えています:

  1. ファイル形式のバリデーション
  2. CSVデータの解析(引用符対応)
  3. HTMLテーブルとしての表示
  4. エラーハンドリング
  5. XSS対策のためのHTMLエスケープ

9. セキュリティと注意点

File APIを使用する際には、いくつかのセキュリティ上の考慮事項と制限があります。

同一オリジンポリシーとの関係

File APIは基本的に同一オリジンポリシーの制約を受けません。これは、ユーザーが明示的に選択したファイルを扱うからです。ただし、生成されたBlobオブジェクトをFetch APIやXHRでアップロードする場合は、同一オリジンポリシーの対象となります。

// 異なるオリジンにファイルをアップロードする場合はCORSが必要
async function uploadToRemoteServer(file) {
  const formData = new FormData();
  formData.append('file', file);
  
  try {
    const response = await fetch('https://example.com/upload', {
      method: 'POST',
      body: formData,
      // CORS対応サーバーへの送信
      mode: 'cors'
    });
    
    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    console.error('アップロードエラー:', error);
    throw error;
  }
}

File APIの制限事項

File APIには以下のような制限があります:

  1. ファイルシステムへの書き込み不可:File APIはファイルの読み取りのみをサポートし、ファイルシステムへの直接的な書き込みはできません。
  2. ファイルパスの制限:セキュリティ上の理由から、ファイルの完全なパスは取得できません。
  3. 選択されたファイルのみアクセス可能:ユーザーが明示的に選択したファイルにのみアクセスでき、システム全体のファイルにはアクセスできません。
  4. 非同期処理のみ:特に大きなファイルを扱う場合、すべての操作は非同期で行われるため、適切な非同期パターンを使用する必要があります。

パフォーマンス考慮点

大きなファイルを扱う際のパフォーマンスに関する考慮事項は以下の通りです:

  1. メモリ使用量:大きなファイルをメモリに読み込むと、ブラウザのメモリを大量に消費する可能性があります。
   function handleLargeFile(file) {
     // 大きなファイルの場合はチャンク処理
     if (file.size > 10 * 1024 * 1024) { // 10MB以上
       processInChunks(file);
     } else {
       processEntireFile(file);
     }
   }
   
   function processInChunks(file) {
     const chunkSize = 2 * 1024 * 1024; // 2MBのチャンク
     const chunks = Math.ceil(file.size / chunkSize);
     let processedChunks = 0;
     
     // 各チャンクを処理
     for (let i = 0; i < chunks; i++) {
       const start = i * chunkSize;
       const end = Math.min(start + chunkSize, file.size);
       const chunk = file.slice(start, end);
       
       // 各チャンクを個別に処理
       processChunk(chunk, i).then(() => {
         processedChunks++;
         if (processedChunks === chunks) {
           console.log('すべてのチャンク処理が完了しました');
           finalizeProcessing();
         }
       });
     }
   }
  1. UIの応答性:ファイル処理中にはUIがブロックされないよう、Web Workersの使用を検討します。
       function processWithWorker(file) {
         return new Promise((resolve, reject) => {
           const worker = new Worker('fileProcessor.js');
           
           worker.onmessage = function(e) {
             if (e.data.error) {
               reject(new Error(e.data.error));
             } else {
               resolve(e.data.result);
             }
             worker.terminate();
           };
           
           worker.onerror = function(error) {
             reject(error);
             worker.terminate();
           };
           
           // FileオブジェクトをWorkerに渡す
           worker.postMessage({
             file: file,
             action: 'process'
           });
         });
       }
    
  2. Blob URLのリソース管理:使用後のBlob URLを適切に解放します。
       // Blob URLのライフサイクル管理
       function manageBlobURLs() {
         const urlStore = new Set();
         
         function createAndStoreURL(blob) {
           const url = URL.createObjectURL(blob);
           urlStore.add(url);
           return url;
         }
         
         function revokeURL(url) {
           if (urlStore.has(url)) {
             URL.revokeObjectURL(url);
             urlStore.delete(url);
             return true;
           }
           return false;
         }
         
         function revokeAllURLs() {
           urlStore.forEach(url => {
             URL.revokeObjectURL(url);
           });
           urlStore.clear();
         }
         
         // ページ遷移時にすべてのURLを解放
         window.addEventListener('beforeunload', revokeAllURLs);
         
         return {
           create: createAndStoreURL,
           revoke: revokeURL,
           revokeAll: revokeAllURLs
         };
       }
       
       // 使用例
       const blobURLManager = manageBlobURLs();
       const url = blobURLManager.create(someBlob);
       // URLを使用...
       blobURLManager.revoke(url);
    

10. 高度なトピック

File APIをより高度な方法で活用するためのトピックを見ていきましょう。

Streams APIとの連携

Streams APIを使用すると、大きなファイルを扱う際にメモリ効率を大幅に向上させることができます。

async function streamFileContent(file) {
  try {
    // ファイルをストリームとして読み込む
    const fileStream = file.stream();
    const reader = fileStream.getReader();
    
    let processedSize = 0;
    const contentHolder = document.getElementById('streamContent');
    contentHolder.textContent = '';
    
    // テキストデコーダーを準備
    const decoder = new TextDecoder('utf-8');
    
    // チャンクを読み込んで処理
    while (true) {
      const { done, value } = await reader.read();
      
      if (done) {
        console.log('ストリーム読み込み完了');
        break;
      }
      
      // チャンクを処理(テキストファイルの場合)
      processedSize += value.length;
      const text = decoder.decode(value, { stream: true });
      
      // 進捗表示
      const progressPercent = Math.round((processedSize / file.size) * 100);
      console.log(`処理中... ${progressPercent}%`);
      
      // テキストを表示(例えば、ログファイルの追加表示)
      const textNode = document.createTextNode(text);
      contentHolder.appendChild(textNode);
    }
    
    // 最後のデコード
    const finalText = decoder.decode();
    if (finalText) {
      const textNode = document.createTextNode(finalText);
      contentHolder.appendChild(textNode);
    }
    
  } catch (error) {
    console.error('ストリーム処理エラー:', error);
  }
}

実際のアプリケーションでは、ファイルの種類に応じて異なる処理を行うことができます。例えば、CSVファイルを行ごとに処理したり、大きな画像ファイルをチャンクで処理したりできます。

Web Workersでのファイル処理

Web Workersを使用すると、メインスレッドをブロックすることなく、大量のデータ処理を行うことができます。

メインスクリプト(main.js)
document.getElementById('processButton').addEventListener('click', function() {
  const fileInput = document.getElementById('largeFile');
  const file = fileInput.files[0];
  
  if (!file) {
    alert('ファイルを選択してください');
    return;
  }
  
  const resultDiv = document.getElementById('result');
  resultDiv.textContent = '処理中...';
  
  // Web Workerを作成
  const worker = new Worker('fileWorker.js');
  
  // 進捗状況を受け取るリスナー
  worker.onmessage = function(e) {
    const message = e.data;
    
    if (message.type === 'progress') {
      resultDiv.textContent = `処理中... ${message.percent}%`;
    } else if (message.type === 'result') {
      resultDiv.textContent = `処理結果: ${message.data}`;
      worker.terminate();
    } else if (message.type === 'error') {
      resultDiv.textContent = `エラー: ${message.error}`;
      worker.terminate();
    }
  };
  
  // ファイルデータをWorkerに送信
  const reader = new FileReader();
  reader.onload = function() {
    worker.postMessage({
      type: 'process',
      fileData: reader.result,
      fileName: file.name
    });
  };
  reader.readAsArrayBuffer(file);
});
Worker スクリプト(fileWorker.js)
// Web Worker内のコード
self.onmessage = function(e) {
  const message = e.data;
  
  if (message.type === 'process') {
    try {
      const fileData = message.fileData;
      const fileName = message.fileName;
      
      // ArrayBufferからデータを処理
      const buffer = new Uint8Array(fileData);
      const totalSize = buffer.length;
      
      // 進捗報告のためのカウンター
      let processedBytes = 0;
      let lastReportedPercent = 0;
      
      // 処理結果を保持する変数
      let result = 0;
      
      // バッファを小さなチャンクで処理
      const chunkSize = 1024 * 1024; // 1MBずつ処理
      
      for (let offset = 0; offset < totalSize; offset += chunkSize) {
        // 現在のチャンクを取得
        const limit = Math.min(offset + chunkSize, totalSize);
        const chunk = buffer.subarray(offset, limit);
        
        // チャンクを処理(例:バイトの平均値を計算)
        let sum = 0;
        for (let i = 0; i < chunk.length; i++) {
          sum += chunk[i];
        }
        result += sum / chunk.length;
        
        // 進捗状況を更新
        processedBytes += chunk.length;
        const percent = Math.round((processedBytes / totalSize) * 100);
        
        // 10%ごとに進捗を報告
        if (percent - lastReportedPercent >= 10 || percent === 100) {
          self.postMessage({
            type: 'progress',
            percent: percent
          });
          lastReportedPercent = percent;
        }
      }
      
      // 最終結果を平均化
      result = result / (totalSize / chunkSize);
      
      // 結果を返信
      self.postMessage({
        type: 'result',
        data: `平均バイト値: ${result.toFixed(2)}`
      });
      
    } catch (error) {
      self.postMessage({
        type: 'error',
        error: error.message
      });
    }
  }
};

この例では、大きなファイルをWeb Workerに渡して、メインスレッドをブロックすることなくバイトの平均値を計算しています。実際のアプリケーションでは、画像処理、テキスト分析、データ圧縮など、より複雑な処理を行うことができます。

カテゴリ:File API#* カテゴリ:JavaScript
カテゴリ:File API カテゴリ:JavaScript