#073
posted on 2022.05.29 (Sun)

画像アップローダーの実装。(クライアントサイド)

CMSは必要ない仕様で画像アップローダーだけ実装したかったので、JavaScirptとPHPで実装した方法のメモ。(クライアントサイドの処理のメモ。)

※ サーバーサイドの処理は別記事を参照。

※ ローカルで選択されたファイルの画像をサイト上に表示させる方法は前の記事を参照。

 

ブラウザでデバイス内のローカルファイルを選択して、選択されたファイルをJavaScirptでPOST送信してサーバーサイドに渡す方法。

 

[ アップローダー全体の処理の流れ ]

  1. ユーザーによって画像ファイルが選択されたら、画像ファイルをform要素の中身として保持。
  2. 画像ファイルを保持したフォームをサーバーサイドにPOST送信。
  3. サーバーサイドで受信したPOSTの内容をセキュリティーチェック。
  4. 受け取った画像ファイルをサーバー内の所定のディレクトリに移動。
  5. 処理の完了を表示。

[ クライアントサイドの処理 ]

  1. HTMLのinput要素かドラッグ・ドロップでローカルファイルから画像ファイルを選択・取得。(ファイル選択の方法の詳細は前の記事を参照。)
  2. 選択された画像ファイルを、「FormData」オブジェクトを利用してform要素の中にinput要素として保持。
  3. 「XMLHttpRequest」オブジェクト(または「fetch()」メソッド)でサーバーサイドにPOST送信。
  4. サーバーサイドからレスポンスを受け取って完了を表示。

[ サーバーサイドの処理 ]

  1. POSTされてきたフォームの内容をセキュリティーチェック。
  2. 受け取った画像ファイルをリネームしてサーバー内の所定のディレクトリに移動。
  3. すべてのファイルの移動が完了したらクライアントサイドに任意のレスポンスを返す。

 

 

ローカルファイルから選択された画像をフォームとして保持

JavaScriptの「FormData」オブジェクトを利用して、ローカルファイルから選択された画像ファイルをform要素として保持する方法。

 

「FormData」オブジェクト

「FormData」オブジェクトは、フォームデータを保存したり送信したりするためのオブジェクト。

  • 「FormData」オブジェクトは、「fetch」などのネットワークメソッドのボディーに設定できる。
  • 「FormData」オブジェクトで送信データを作成すると、「enctype」属性は自動的に「multipart/form-data」の形式になる。
  • フォームの内容はエンコードされ、ファイルの送信が可能な「Content-Type: form/multipart」で送信される。
  • サーバーサイドでは、通常のフォーム送信のように扱われる。

※ HTMLでファイルをフォーム送信する場合、データのエンコード方式である「enctype」属性は「enctype=”multipart/form-data”」である必要がある。

 

画像ファイルをform要素に保持する関数を作成

「FormData」オブジェクトを利用して、画像ファイル(Fileオブジェクト)をform要素にinput要素フィールドとして保持する関数を作成する。

  1. 引数に「選択されたファイル(FileListオブジェクト)」、「Fileオブジェクトの数」を受け取るように作成。
  2. form要素に追加するときのフィールドのname属性値を指定。(下記では「ag2postfile[]」。)
  3. new演算子を付加してFormData()コンストラクターを呼び出し、「FormData」オブジェクトを生成。
  4. 選択されたファイルがすべて画像ファイルかどうかをMIMEタイプでチェック。
  5. 画像ファイルを「FormData」オブジェクトのフィールドに追加。
  6. 生成した「FormData」オブジェクトを返す。

※ 選択される画像ファイルは複数の可能性があるので、フィールドのname属性値は配列を指定する。(同名のフィールドを複数渡す場合、input要素のフィールド名には「[]」を付けて配列として渡す必要がある。)

function ag2formData(t,n){
  //設定するinputのname属性 (配列を指定)
  const ag2postname = 'ag2postfile[]';
  //FormDataオブジェクトを生成
  const ag2formData = new FormData();

  //指定のフィールド名で画像ファイルをフォームに追加
  for(let i = 0; i < n; i++){
    //ファイルのMIMEタイプをチェック
    //IE用polyfill
    if(!String.prototype.startsWith){
      Object.defineProperty(String.prototype, 'startsWith',{
        value: function(search, rawPos){
          var pos = rawPos > 0 ? rawPos|0 : 0;
          return this.substring(pos, pos + search.length) === search;
        }
      });
    }
    if(!t.files[i].type.startsWith('image/')){
      console.log('"'+t.files[i].name+'" is not a image.');
      return false;//画像以外のファイルがあれば処理をキャンセル
    }

    ag2formData.append(ag2postname, t.files[i]);
  }

  //画像を保持したFormDataオブジェクトを返す
  return ag2formData;
}

new FormData() : 新しく作成されたFormDataオブジェクトを返す。

FormDataオブジェクト.append(フィールド名, 値, ファイル名) : 指定したFormDataオブジェクトに、指定したフィールド名と指定した値(USVStringまたはBlobオブジェクトまたはFileオブジェクト)のinput要素を追加する。第3引数のファイル名は省略可で、第2引数にBlobオブジェクトまたはFileオブジェクトを指定した場合にサーバーに渡されるファイル名。(デフォルトは、Blobオブジェクトは「blob」、Fileオブジェクトは「選択したファイルの名前」。)

 

 

サーバーサイドにファイルをPOST送信

上述の関数で生成したフォーム(「FormData」オブジェクト)を、「XMLHttpRequest」オブジェクトか「fetch()」メソッドを使ってサーバーサイドにPOST送信する方法。

※ 現在は「XMLHttpRequest」よりも機能が優れる「Fetch」の方が推奨されている。(「Fetch」はIE非対応。)

※ 「XMLHttpRequest」はアップロードとダウンロードの両方の進行状況を追跡できるが、「Fetch」はダウンロードのみでアップロードの追跡はできない。

 

方法1 : 「Fetch」でPOST送信

「Fetch」を利用してサーバーサイドにPOST送信する方法。

 

「Fetch」は、HTTP通信のヘッダー、リクエスト、レスポンスを扱うためのインターフェイス。

※ Fetchの基礎知識は前の記事を参照。

 

[ fetch()メソッドを実行する関数を作成 ]

「fetch()」メソッドを使ってサーバーに設置してあるPHPファイルにPOST送信する関数を作成する。

  1. 引数に「選択されたファイル(FileListオブジェクト)」を受け取るように作成。
  2. 選択できるファイルの上限個数を設定してチェック。
  3. 上述で作成した関数「ag2formData()」を実行して、選択された画像ファイルを保持する「FormData」オブジェクトを生成・取得。
  4. 「fetch()」メソッドでサーバーのPHPファイルにPOST送信を実行。
  5. (サーバーサイドのPHPファイルの処理が実行される。)
  6. レスポンスのダウンロードの進捗状況を取得。(今回は処理完了かエラーかの簡単な文字列しか取得しないのであまり意味はない。)
  7. 処理が完了したら受け取ったレスポンスを表示。
//送信先URL
const ag2url = 'upload.php';// サーバーサイドの処理用phpファイル

function ag2uploadFetch(t){
  let thisFileNum = t.files.length;

  //一度のアップロードファイル上限数を設定
  let maxFileNum = 10;
  if(thisFileNum > maxFileNum) return console.log('一度に送信できるファイルは'+maxFileNum+'個までです。');

  //ファイルをformで保持
  let ag2form = ag2formData(t,thisFileNum);
  if(!ag2form) return console.log('画像ではないファイルが含まれています。');

  //fetchで送信
  fetch(ag2url, {
    method: 'POST',
    body: ag2form
  }).then(async (response)=>{
    //ステータスコードが200~299以外の場合はError()をスロー
    if(!response.ok) throw new Error('Network Response Error.');

    //ヘッダーからレスポンス本体の合計サイズを取得
    const totalSize = response.headers.get('Content-Length');
    //レスポンス本体のリーダーを作成
    const ag2reader = response.body.getReader();
    let receivedSize = 0;
    let chunks = [];
    //レスポンスの断片がダウンロードされるごとに進捗状況を表示
    while(true){
      const {done, value} = await ag2reader.read();
      if(done) break;
      chunks.push(value);
      receivedSize += value.length;
      let ag2percent = Math.round((receivedSize / totalSize) * 100);
      console.log('Download Progress :');
      console.log(receivedSize + ' / '+totalSize + ' bytes ('+ag2percent+' %)');
    }
    let ag2data = new Uint8Array(receivedSize);
    let arrayPos = 0;
    for(let chunk of chunks){
      ag2data.set(chunk, arrayPos);
      arrayPos += chunk.length;
    }
    //レスポンスのバッファーのバイト列を文字列にデコード
    const responseData = new TextDecoder().decode(ag2data);

    //レスポンスを渡すして解決
    return responseData;
  }).then((result)=>{
    //処理...
    console.log(result);//レスポンスの中身を表示
  }).catch((error)=>{
    //エラーの場合
    console.error('Fetch Request Error :', error);
  });
}

[ ユーザーによりファイルが選択されたら関数を実行 ]

選択ファイルがブラウザにドロップされたら上述の関数を実行する方法。

(ユーザーによりローカルファイルが所定のHTML要素にドロップされたら、上述の関数を実行してファイルをアップロードする。)

※ ドラッグ・ドロップでファイルを取得する方法は前の記事を参照。

const ag2dropArea = document.getElementById('ag2droparea');//ドロップエリアの要素

ag2dropArea.addEventListener('drop', function(){
  event.stopPropagation();
  event.preventDefault();

  ag2uploadFetch(event.dataTransfer);
});

event.stopPropagation() : eventオブジェクトのメソッド。次のDOM階層へのイベントの伝播を抑止する。

event.preventDefault() : eventオブジェクトのメソッド。発生したイベントの規定の動作を行わない。イベントの伝播は止めない。

DragEvent.dataTransfer : 読み取り専用プロパティー。ドラッグ操作のデータをDataTransferオブジェクトとして保持していて、そのDataTransferオブジェクトを返す。

 

方法2 : 「XMLHttpRequest」でPOST送信

「XMLHttpRequest」を利用してサーバーサイドにPOST送信する方法。

 

「XMLHttpRequest」は、JavaScriptでHTTPリクエストを行うための組み込みのブラウザオブジェクト。

※ XMLHttpRequestの基礎知識は前の記事を参照。

 

[ XMLHttpRequestを実行する関数を作成 ]

生成した「FormData」オブジェクトを、「XMLHttpRequest」オブジェクトを使ってサーバーに設置してあるPHPファイルにPOST送信する関数を作成する。

  1. 引数に「選択されたファイル(FileListオブジェクト)」を受け取るように作成。
  2. 選択できるファイルの上限個数を設定してチェック。
  3. 上述で作成した関数「ag2formData()」を実行して、選択ファイルから「FormData」オブジェクトを生成・取得。
  4. XMLHttpRequestオブジェクトを生成。
  5. open()メソッドで初期化し、リクエストのメインのパラメータを指定。
  6. アップロードとダウンロードの進捗状況を取得するためのイベントリスナーを登録。
  7. send()メソッドでサーバーのPHPファイルにPOST送信を実行。
  8. (サーバーサイドのPHPファイルの処理が実行される。)
  9. 処理が完了したら受け取ったレスポンスを表示。

※ XMLHttpRequestの一部のイベントは「send()」メソッドを実行する前にリスナー登録する必要があるので注意。(コードの最後でsend()するようにする。)

//送信先URL
const ag2url = 'upload.php';// サーバーサイドの処理用phpファイル

function ag2uploadXML(t){
  let thisFileNum = t.files.length;

  //一度のアップロードファイル上限数
  let maxFileNum = 10;
  if(thisFileNum > maxFileNum) return console.log('一度に送信できるファイルは'+maxFileNum+'個までです。');

  //ファイルをformで保持
  let ag2form = ag2formData(t,thisFileNum);
  if(!ag2form) return console.log('画像ではないファイルが含まれています。');

  //XMLHttpRequestオブジェクトを作成
  const ag2xhr = new XMLHttpRequest();
  //open()メソッドで初期化
  ag2xhr.open('POST', ag2url);
  //リクエストのタイムアウト(ミリ秒)
  ag2xhr.timeout = 30000; //30秒

  //アップロードの進捗
  ag2xhr.upload.addEventListener('progress', function(event){
    if(event.lengthComputable){
      let percent = Math.round((event.loaded / event.total) * 100);
      console.log('download progress :');
      console.log(event.loaded + ' bytes / '+event.total + ' bytes ('+percent+' %)');
    }
  });
  //アップロードの完了
  ag2xhr.upload.addEventListener('load', function(){
    console.log('Upload is done successfully.');
  });
  //アップロード中のエラー
  ag2xhr.upload.addEventListener('error', function(){
    console.log('xhr.upload error (during the upload) :');
    console.log(ag2xhr.status);
  });

  //レスポンスのダウンロード進捗
  ag2xhr.addEventListener('progress', function(event){
    if(event.lengthComputable){
      let ag2percent = Math.round((event.loaded / event.total) * 100);
      console.log('Download Progress :');
      console.log(event.loaded + ' bytes / '+event.total + ' bytes ('+ag2percent+' %)');
    }else{
      console.log(event.loaded + ' bytes');
    }
  });

  //処理完了後にレスポンスを取得
  ag2xhr.addEventListener('load', function(){
    if(ag2xhr.status !== 200){
      //レスポンスのHTTPステータスコードがエラーの範囲の場合
      console.log('Server Error :');
      console.log(ag2xhr.status);//レスポンスのHTTPステータスコード
      console.log(ag2xhr.statusText);////HTTPステータスメッセージ(文字列)
    }else{
      console.log(ag2xhr.response);//受け取ったレスポンス本体
    }
  });

  //エラーが発生した場合
  ag2xhr.addEventListener('error', function(){
    console.log('xhr Request Error.');
  });

  //send()メソッドでフォームを送信 (一部のイベントはsendの前にイベントリスナーを登録する必要がある)
  ag2xhr.send(ag2form);
}

[ ユーザーによりファイルが選択されたら関数を実行 ]

選択ファイルがブラウザにドロップされたら上述の関数を実行する方法。

※ ドラッグ・ドロップでファイルを取得する方法は前の記事を参照。

const ag2dropArea = document.getElementById('ag2droparea');//ドロップエリアの要素

ag2dropArea.addEventListener('drop', function(){
  event.stopPropagation();
  event.preventDefault();

  ag2uploadXML(event.dataTransfer);
});

 

 

この記事のURL

https://memo.ag2works.tokyo/post-4482/

Copyコピー
この記事のタイトル

画像アップローダーの実装。(クライアントサイド) | memo メモ [AG2WORKS]

Copyコピー
この記事のリンクタグ

<a href="https://memo.ag2works.tokyo/post-4482/" target="_blank" rel="noopener">画像アップローダーの実装。(クライアントサイド) | memo メモ [AG2WORKS]</a>

Copyコピー
※ フィールドをクリックでコピーするテキストの編集ができます。

この記事へのコメント

コメントの書き込みはまだありません。

  • コメント内のタグはエスケープ処理され、文字列として出力されます。
  • セキュリティーのため、投稿者のIPアドレスは取得されます。
  • 管理者が内容を不適切と判断したコメントは削除されます。
  • このフォームにはスパム対策として、Googleの提供するreCAPTCHAシステムが導入されています。
    (Google Privacy Policy and Terms of Service.)