コンテンツにスキップ

JavaScript/File API

出典: フリー教科書『ウィキブックス(Wikibooks)』

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オブジェクトを取得する例constfileInput=document.querySelector('input[type="file"]');fileInput.addEventListener('change',function(){constselectedFile=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')

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

functionshowFileInfo(file){constfileInfo=document.getElementById('fileInfo');constfileSize=file.size<1024?`${file.size} bytes`:file.size<1048576?`${(file.size/1024).toFixed(2)} KB`:`${(file.size/1048576).toFixed(2)} MB`;constlastModified=newDate(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プロパティを持ち、イテラブルです:

// 複数ファイル選択の処理例constmultiFileInput=document.querySelector('input[type="file"][multiple]');multiFileInput.addEventListener('change',function(){constfileList=this.files;console.log(`${fileList.length}個のファイルが選択されました`);// FileListを反復処理するfor(leti=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オブジェクトは、以下のコンストラクタを使用して作成できます:

constblob=newBlob(blobParts,options);

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

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

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

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

Blobのslice()メソッド

[編集]

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

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

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

// 大きなファイルを1MBごとにチャンクに分割する例functionsplitIntoChunks(blob,chunkSize){constchunks=[];constsize=blob.size;letstart=0;while(start<size){constend=Math.min(start+chunkSize,size);constchunk=blob.slice(start,end,blob.type);chunks.push(chunk);start=end;}returnchunks;}constlargeFile=document.getElementById('largeFile').files[0];constchunks=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オブジェクトの作成と使用

[編集]
constreader=newFileReader();// イベントハンドラを設定reader.onload=function(event){// 読み込み完了時の処理constcontent=event.target.result;console.log('ファイル内容:',content);};@reader.onerror=function(){console.error('ファイル読み込みエラー');};// ファイルを読み込むconstfile=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) バイナリ文字列として読み込む(非推奨) バイナリ文字列

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

constfile=document.getElementById('fileInput').files[0];constreader=newFileReader();// テキストファイルを読み込むfunctionreadAsText(){reader.onload=function(e){document.getElementById('output').textContent=e.target.result;};reader.readAsText(file,'UTF-8');// エンコーディングを指定}// 画像をプレビュー表示するfunctionpreviewImage(){reader.onload=function(e){constimg=document.getElementById('preview');img.src=e.target.result;// Data URLを直接img.srcに設定};reader.readAsDataURL(file);}// バイナリデータを操作するfunctionprocessArrayBuffer(){reader.onload=function(e){constarrayBuffer=e.target.result;constbyteArray=newUint8Array(arrayBuffer);// バイナリデータを処理...console.log(`最初の10バイト: ${byteArray.slice(0,10)}`);};reader.readAsArrayBuffer(file);}

イベント処理

[編集]

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

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

進行状況を表示する例:

functionreadLargeFile(file){constreader=newFileReader();constprogressBar=document.getElementById('progressBar');conststatus=document.getElementById('status');reader.onloadstart=function(){status.textContent='読み込み開始...';progressBar.value=0;};reader.onprogress=function(e){if(e.lengthComputable){constpercentLoaded=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を使用してより整理された方法で操作できます:

functionreadFileAsText(file){returnnewPromise((resolve,reject)=>{constreader=newFileReader();reader.onload=function(){resolve(reader.result);};reader.onerror=function(){reject(reader.error);};reader.readAsText(file);});}// 使用例asyncfunctionprocessTextFile(file){try{constcontent=awaitreadFileAsText(file);console.log('ファイル内容:',content);returncontent;}catch(error){console.error('ファイル読み込みエラー:',error);throwerror;}}// 複数のファイルを順番に処理asyncfunctionprocessMultipleFiles(fileList){constresults=[];for(constfileoffileList){constcontent=awaitreadFileAsText(file);results.push({name:file.name,content:content});}returnresults;}

5. URL APIとBlob URL

[編集]

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

URL.createObjectURL()

[編集]

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

constfile=document.getElementById('imageFile').files[0];constimageUrl=URL.createObjectURL(file);constimg=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が不要になったら必ず解放すべきです:

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

Blob URLの用途と注意点

[編集]

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

  1. ファイルプレビュー(画像、動画、オーディオなど)
  2. ダウンロードリンクの作成
  3. iframe内での表示
    // ダウンロードリンクの作成例functioncreateDownloadLink(file){constlink=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プロパティから、ドラッグされたファイルにアクセスできます。

functionsetupDragAndDrop(){constdropZone=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');constfiles=e.dataTransfer.files;if(files.length>0){handleFiles(files);}});}// ドロップされたファイルを処理する関数functionhandleFiles(files){constoutput=document.getElementById('fileList');output.innerHTML='';Array.from(files).forEach(file=>{constfileInfo=document.createElement('div');fileInfo.className='file-info';fileInfo.textContent=`${file.name} (${formatFileSize(file.size)})`;output.appendChild(fileInfo);// 画像の場合はプレビューを表示if(file.type.match('image.*')){constreader=newFileReader();reader.onload=function(e){constimg=document.createElement('img');img.src=e.target.result;img.className='preview';fileInfo.appendChild(img);};reader.readAsDataURL(file);}});}// ファイルサイズのフォーマット関数functionformatFileSize(bytes){if(bytes<1024)returnbytes+' bytes';elseif(bytes<1048576)return(bytes/1024).toFixed(1)+' KB';elsereturn(bytes/1048576).toFixed(1)+' MB';}

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

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

[編集]

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

functionsetupAdvancedDragDrop(){constdropZone=document.getElementById('advancedDropZone');constfileList=document.getElementById('advancedFileList');constuploadButton=document.getElementById('uploadButton');letdroppedFiles=[];// ドラッグ&ドロップイベントの設定['dragenter','dragover','dragleave','drop'].forEach(eventName=>{dropZone.addEventListener(eventName,preventDefaults,false);});functionpreventDefaults(e){e.preventDefault();e.stopPropagation();}// ハイライト効果['dragenter','dragover'].forEach(eventName=>{dropZone.addEventListener(eventName,highlight,false);});['dragleave','drop'].forEach(eventName=>{dropZone.addEventListener(eventName,unhighlight,false);});functionhighlight(){dropZone.classList.add('active');}functionunhighlight(){dropZone.classList.remove('active');}// ファイルのドロップ処理dropZone.addEventListener('drop',handleDrop,false);functionhandleDrop(e){constdt=e.dataTransfer;constfiles=dt.files;droppedFiles=[...files];displayFileList();uploadButton.disabled=false;}// ファイルリストの表示functiondisplayFileList(){fileList.innerHTML='';droppedFiles.forEach((file,index)=>{constitem=document.createElement('div');item.className='file-item';constinfo=document.createElement('div');info.className='file-info';info.innerHTML=`<strong>${file.name}</strong> (${formatFileSize(file.size)})`;constprogress=document.createElement('progress');progress.id=`progress-${index}`;progress.value=0;progress.max=100;conststatus=document.createElement('span');status.className='status';status.textContent='準備完了';item.appendChild(info);item.appendChild(progress);item.appendChild(status);fileList.appendChild(item);});}// アップロードボタンの処理uploadButton.addEventListener('click',()=>{uploadFiles();});// ファイルのアップロード処理(シミュレーション)functionuploadFiles(){droppedFiles.forEach((file,index)=>{constprogress=document.getElementById(`progress-${index}`);constitem=progress.parentElement;conststatus=item.querySelector('.status');status.textContent='アップロード中...';// プログレスシミュレーション(実際のアップロードコードに置き換える)letpercent=0;constinterval=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の活用

[編集]
<inputtype="file"id="fileInput"accept="image/*"><divid="preview"></div>
document.getElementById('fileInput').addEventListener('change',function(){constfile=this.files[0];if(file){constreader=newFileReader();reader.onload=function(e){constpreview=document.getElementById('preview');preview.innerHTML='';constimg=document.createElement('img');img.src=e.target.result;img.alt=file.name;img.style.maxWidth='100%';preview.appendChild(img);};reader.readAsDataURL(file);}});

複数ファイル選択

[編集]

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

<inputtype="file"id="multipleFiles"multiple><divid="multiPreview"></div>
document.getElementById('multipleFiles').addEventListener('change',function(){constfiles=this.files;constpreview=document.getElementById('multiPreview');preview.innerHTML='';if(files.length>0){Array.from(files).forEach(file=>{if(file.type.match('image.*')){constreader=newFileReader();reader.onload=function(e){constimgContainer=document.createElement('div');imgContainer.className='img-container';constimg=document.createElement('img');img.src=e.target.result;img.alt=file.name;img.className='thumbnail';constcaption=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/*'
<!-- 画像のみ --><inputtype="file"accept="image/*"><!-- PDFと特定の文書形式 --><inputtype="file"accept=".pdf,.docx,.xlsx"><!-- 複数の種類を組み合わせる --><inputtype="file"accept="image/jpeg,image/png,application/pdf">

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

functioncreateCustomFileInput(){constcontainer=document.getElementById('customFileInput');constfileInput=document.createElement('input');fileInput.type='file';fileInput.id='hiddenFileInput';fileInput.multiple=true;fileInput.accept='image/*,.pdf';fileInput.style.display='none';constbutton=document.createElement('button');button.textContent='ファイルを選択';button.className='custom-file-button';constfileList=document.createElement('div');fileList.className='custom-file-list';// ボタンクリックでファイル選択ダイアログを開くbutton.addEventListener('click',()=>{fileInput.click();});// ファイル選択時の処理fileInput.addEventListener('change',()=>{fileList.innerHTML='';if(fileInput.files.length>0){constheading=document.createElement('h3');heading.textContent='選択されたファイル:';fileList.appendChild(heading);constlist=document.createElement('ul');Array.from(fileInput.files).forEach(file=>{constitem=document.createElement('li');// ファイルタイプに応じたアイコンを表示leticon='📄';if(file.type.startsWith('image/')){icon='🖼️';}elseif(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;constevent=newEvent('change');fileInput.dispatchEvent(event);}});}

8. File APIの実践的使用例

[編集]

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

画像プレビュー

[編集]

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

functioncreateImagePreviewSystem(){constfileInput=document.getElementById('imageUpload');constpreviewContainer=document.getElementById('previewContainer');consterrorMessage=document.getElementById('errorMessage');fileInput.addEventListener('change',function(){// 前のプレビューと表示をクリアpreviewContainer.innerHTML='';errorMessage.textContent='';// 各ファイルを処理constfiles=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;}// プレビュー表示用の要素を作成constpreviewItem=document.createElement('div');previewItem.className='preview-item';// 読み込み中表示constloadingIndicator=document.createElement('div');loadingIndicator.className='loading';loadingIndicator.textContent='読み込み中...';previewItem.appendChild(loadingIndicator);// コンテナに追加previewContainer.appendChild(previewItem);// FileReaderを使って画像を読み込みconstreader=newFileReader();reader.onload=function(e){// 読み込み完了したら画像を表示previewItem.innerHTML='';constimg=document.createElement('img');img.src=e.target.result;img.className='preview-image';constfileName=document.createElement('div');fileName.className='file-name';fileName.textContent=file.name;constfileSize=document.createElement('div');fileSize.className='file-size';fileSize.textContent=formatFileSize(file.size);// 削除ボタンconstremoveButton=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='';consterrorDiv=document.createElement('div');errorDiv.className='error';errorDiv.textContent='ファイルの読み込みに失敗しました。';previewItem.appendChild(errorDiv);};// 画像を読み込むreader.readAsDataURL(file);});});}

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

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

ファイルアップロード

[編集]

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

functioncreateFileUploader(){constfileInput=document.getElementById('uploadFiles');constuploadButton=document.getElementById('startUpload');constprogressContainer=document.getElementById('progressContainer');uploadButton.addEventListener('click',asyncfunction(){constfiles=fileInput.files;if(files.length===0){alert('アップロードするファイルを選択してください。');return;}progressContainer.innerHTML='';// 各ファイルを個別にアップロードfor(constfileoffiles){awaituploadFile(file);}alert('すべてのファイルのアップロードが完了しました。');});asyncfunctionuploadFile(file){// プログレス表示用の要素を作成constprogressItem=document.createElement('div');progressItem.className='progress-item';constfileName=document.createElement('div');fileName.className='file-name';fileName.textContent=file.name;constprogressBar=document.createElement('progress');progressBar.max=100;progressBar.value=0;conststatusText=document.createElement('span');statusText.className='status';statusText.textContent='準備中...';progressItem.appendChild(fileName);progressItem.appendChild(progressBar);progressItem.appendChild(statusText);progressContainer.appendChild(progressItem);// FormDataオブジェクトを作成constformData=newFormData();formData.append('file',file);try{// アップロード処理実行constresponse=awaitnewPromise((resolve,reject)=>{constxhr=newXMLHttpRequest();// プログレスイベントの設定xhr.upload.addEventListener('progress',(e)=>{if(e.lengthComputable){constpercentComplete=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(newError(`HTTPエラー: ${xhr.status}`));}});xhr.addEventListener('error',()=>{reject(newError('ネットワークエラーが発生しました。'));});xhr.addEventListener('abort',()=>{reject(newError('アップロードが中断されました。'));});// リクエスト開始xhr.open('POST','/api/upload',true);xhr.send(formData);});// 成功時の処理progressItem.classList.add('success');statusText.textContent='アップロード完了';returnresponse;}catch(error){// エラー時の処理progressItem.classList.add('error');statusText.textContent=`エラー: ${error.message}`;throwerror;}}}

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

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

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

[編集]

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

functioncreateCSVParser(){constfileInput=document.getElementById('csvFile');constparseButton=document.getElementById('parseCSV');constresultTable=document.getElementById('resultTable');consterrorMessage=document.getElementById('csvError');parseButton.addEventListener('click',function(){constfile=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>';constreader=newFileReader();reader.onload=function(e){try{constcsvContent=e.target.result;constdata=parseCSV(csvContent);displayCSVData(data);}catch(error){errorMessage.textContent=`CSVの解析に失敗しました: ${error.message}`;resultTable.innerHTML='';}};reader.onerror=function(){errorMessage.textContent='ファイルの読み込みに失敗しました。';resultTable.innerHTML='';};reader.readAsText(file);});// CSVデータを解析する関数functionparseCSV(csvText){constlines=csvText.split(/\r\n|\n/);constresult=[];for(leti=0;i<lines.length;i++){if(lines[i].trim()==='')continue;// カンマで分割(引用符内のカンマは考慮する)constrow=[];letinQuotes=false;letcurrentValue='';for(letj=0;j<lines[i].length;j++){constchar=lines[i][j];if(char==='"'&&(j===0||lines[i][j-1]!=='\\')){inQuotes=!inQuotes;}elseif(char===','&&!inQuotes){row.push(currentValue);currentValue='';}else{currentValue+=char;}}// 最後の値を追加row.push(currentValue);result.push(row);}returnresult;}// CSVデータをテーブルとして表示functiondisplayCSVData(data){if(data.length===0){errorMessage.textContent='CSVファイルにデータがありません。';resultTable.innerHTML='';return;}lettableHTML='';// ヘッダー行tableHTML+='<tr>';data[0].forEach(header=>{tableHTML+=`<th>${escapeHTML(header)}</th>`;});tableHTML+='</tr>';// データ行for(leti=1;i<data.length;i++){tableHTML+='<tr>';data[i].forEach(cell=>{tableHTML+=`<td>${escapeHTML(cell)}</td>`;});tableHTML+='</tr>';}resultTable.innerHTML=tableHTML;}// HTMLエスケープ処理functionescapeHTML(str){returnstr.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が必要asyncfunctionuploadToRemoteServer(file){constformData=newFormData();formData.append('file',file);try{constresponse=awaitfetch('https://example.com/upload',{method:'POST',body:formData,// CORS対応サーバーへの送信mode:'cors'});if(!response.ok){thrownewError(`HTTPエラー: ${response.status}`);}returnawaitresponse.json();}catch(error){console.error('アップロードエラー:',error);throwerror;}}

File APIの制限事項

[編集]

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

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

パフォーマンス考慮点

[編集]

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

  1. メモリ使用量:大きなファイルをメモリに読み込むと、ブラウザのメモリを大量に消費する可能性があります。
functionhandleLargeFile(file){// 大きなファイルの場合はチャンク処理if(file.size>10*1024*1024){// 10MB以上processInChunks(file);}else{processEntireFile(file);}}functionprocessInChunks(file){constchunkSize=2*1024*1024;// 2MBのチャンクconstchunks=Math.ceil(file.size/chunkSize);letprocessedChunks=0;// 各チャンクを処理for(leti=0;i<chunks;i++){conststart=i*chunkSize;constend=Math.min(start+chunkSize,file.size);constchunk=file.slice(start,end);// 各チャンクを個別に処理processChunk(chunk,i).then(()=>{processedChunks++;if(processedChunks===chunks){console.log('すべてのチャンク処理が完了しました');finalizeProcessing();}});}}
  1. UIの応答性:ファイル処理中にはUIがブロックされないよう、Web Workersの使用を検討します。
    functionprocessWithWorker(file){returnnewPromise((resolve,reject)=>{constworker=newWorker('fileProcessor.js');worker.onmessage=function(e){if(e.data.error){reject(newError(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のライフサイクル管理functionmanageBlobURLs(){consturlStore=newSet();functioncreateAndStoreURL(blob){consturl=URL.createObjectURL(blob);urlStore.add(url);returnurl;}functionrevokeURL(url){if(urlStore.has(url)){URL.revokeObjectURL(url);urlStore.delete(url);returntrue;}returnfalse;}functionrevokeAllURLs(){urlStore.forEach(url=>{URL.revokeObjectURL(url);});urlStore.clear();}// ページ遷移時にすべてのURLを解放window.addEventListener('beforeunload',revokeAllURLs);return{create:createAndStoreURL,revoke:revokeURL,revokeAll:revokeAllURLs};}// 使用例constblobURLManager=manageBlobURLs();consturl=blobURLManager.create(someBlob);// URLを使用...blobURLManager.revoke(url);

10. 高度なトピック

[編集]

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

Streams APIとの連携

[編集]

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

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

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

Web Workersでのファイル処理

[編集]

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

メインスクリプト(main.js)
document.getElementById('processButton').addEventListener('click',function(){constfileInput=document.getElementById('largeFile');constfile=fileInput.files[0];if(!file){alert('ファイルを選択してください');return;}constresultDiv=document.getElementById('result');resultDiv.textContent='処理中...';// Web Workerを作成constworker=newWorker('fileWorker.js');// 進捗状況を受け取るリスナーworker.onmessage=function(e){constmessage=e.data;if(message.type==='progress'){resultDiv.textContent=`処理中... ${message.percent}%`;}elseif(message.type==='result'){resultDiv.textContent=`処理結果: ${message.data}`;worker.terminate();}elseif(message.type==='error'){resultDiv.textContent=`エラー: ${message.error}`;worker.terminate();}};// ファイルデータをWorkerに送信constreader=newFileReader();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){constmessage=e.data;if(message.type==='process'){try{constfileData=message.fileData;constfileName=message.fileName;// ArrayBufferからデータを処理constbuffer=newUint8Array(fileData);consttotalSize=buffer.length;// 進捗報告のためのカウンターletprocessedBytes=0;letlastReportedPercent=0;// 処理結果を保持する変数letresult=0;// バッファを小さなチャンクで処理constchunkSize=1024*1024;// 1MBずつ処理for(letoffset=0;offset<totalSize;offset+=chunkSize){// 現在のチャンクを取得constlimit=Math.min(offset+chunkSize,totalSize);constchunk=buffer.subarray(offset,limit);// チャンクを処理(例:バイトの平均値を計算)letsum=0;for(leti=0;i<chunk.length;i++){sum+=chunk[i];}result+=sum/chunk.length;// 進捗状況を更新processedBytes+=chunk.length;constpercent=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に渡して、メインスレッドをブロックすることなくバイトの平均値を計算しています。実際のアプリケーションでは、画像処理、テキスト分析、データ圧縮など、より複雑な処理を行うことができます。

close