Upload multiple en HTML5/JavaScript (Partie 1)

L’upload a bien évolué depuis l’arrivée de HTML5.
Auparavant il était :

  • obligatoire d’uploader vos fichier un à un (ou alors utilisé des artifice peut rpatiques)
  • impossible d’obtenir une information sur la progression (sauf à investir dans un développement coté serveur)
  • impossible de passer (sauf à développer des hack tirés par les cheveux) par un simple Drag&Drop pour sélectionner les fichier à uploader.

A travers une série d’articles je vais maqueter un code HTML5/JavaScript permetant, au final, de réaliser de l’upload multiple d’image via Drag&Drop avec pré-visualisation et et progression de chaque image.

Au lieux de vous « coller » un code source complet et insipide j’ai décidé d’avancer par itération afin de vous montrer, un à un, tous les aspects de ce module.

Le code source est en mode « maquette » : le but est de travailler sur l’upload multiple pas sur les bonnes pratiques (validateurs, gestion des exceptions…) du codage en JavaScript.

Etape 1 : Formulaire de sélection multiple.

L’étape 1 consiste en un formulaire permettant de sélectionner plusieurs fichiers à uploader.

Tous au long de l’article j’utilisera la console JavaScript afin de loger mes actions.



Hello World


		<input id="fileToUpload" type="file" multiple="multiple" />

<script type="text/javascript">// <![CDATA[
			document.querySelector('#fileToUpload').onchange = function() {
				console.log("Stat to upload");
				var filesList = this.files;

				console.log("List Files");
				for (var i = 0, filesListLength = filesList.length; i < filesListLength; i++) {
					useFile = filesList[i];
					console.log("File num : " + i + " Name : " + useFile.name + " Size : " + useFile.size + " Type : " + useFile.type);
				}
			}

// ]]></script>


La sélection des fichier est éffectué par la balise <input> de type « file » ; l’option multiple permet la sélection multiple de fichiers.

Log du script.

Stat to upload upload.html:12
List Files upload.html:15
File num : 0 Name : AAAAA.jpg Size : 56140 Type : image/jpeg upload.html:18
File num : 1 Name : Bd7769GCYAAP7wc (3e copie).jpg Size : 56140 Type : image/jpeg upload.html:18
File num : 2 Name : Bd7769GCYAAP7wc (4e copie).jpg Size : 56140 Type : image/jpeg upload.html:18
File num : 3 Name : Bd7769GCYAAP7wc (5e copie).jpg Size : 56140 Type : image/jpeg upload.html:18

Etape 2

On souhaite ajouter une fonction de preview sur les images à uploader.
Les images ne sont pas encore uploadées, en HTML4 il n’existerait pas de solution (à moins que vous connaissiez un hack magique ?) pour réaliser cette fonction.
On effet l’image n’étant pas encore uploadée elle ne possède pas d’URL.

En HTML5 nous disposons de l’objet FileReader permettant de créer une « URL locale ».

« L’objet FileReader permet aux applications Web de  lire le contenu des fichiers (ou des tampons de données brutes) stockés sur l’ordinateur de l’utilisateur de manière asynchrone »

 https://developer.mozilla.org/fr/docs/DOM/FileReader

La fonction createThubnail crée, pour un fichier (ressource obtenu par l’input),  une preview.

On va donc lire (injecter) un file dans une instance d’un FileReader : reader.readAsDataURL(file); puis associé l’URL (attribut result de l’instance file reader) à un noeud <img> que l’on inclura dans le DOM.

La lecture étant asynchrone on ne déclenche la création du noeud via l’événement onload.

La création d’un node <img> est classique, la seule ligne intéressante est : imgElement.src = this.result; associant l’URL « local » (créé par le FileReader) à l’URL du noeud image.

			function createThumbnail(file) {
				var reader = new FileReader();

				reader.readAsDataURL(file);

				reader.onload = function() {
					var imgElement = document.createElement('img');
					imgElement.style.maxWidth = '150px';
					imgElement.style.maxHeight = '150px';

					imgElement.src = this.result;
					 document.getElementById('imgPreview').appendChild(imgElement);
				}
			}

Personnellement je ne suis pas fan de la syntaxe en this.méthode Je trouve la lisibilité mauvaise : je viens du monde de l’objet et tout cela me parait bien « sale » 😉

Etape 3 : refactoring

Je vais donc remplacer ce this.maMéthode par une injection d’événement dans la fonction, cela me semble plus « propre » (avis perso je pense que l’on pourrait troller sur le thème pendant des heures)

				reader.onload = function() {
					var imgElement = document.createElement('img');
					imgElement.style.maxWidth = '150px';
					imgElement.style.maxHeight = '150px';

					imgElement.src = this.result;
					 document.getElementById('imgPreview').appendChild(imgElement);
				}

La propriété target permet de retourner l’événement qui à déclenché l’appel de la fonction (voir http://www.w3schools.com/jsref/event_target.asp)

On obtient en itération 3 le code source suivant.



Hello World</pre>
<style><!--
			.box {
				background-color: #BBBBBB;
				border: solid 3px;
				min-height: 100px;
			}
--></style>
<pre>
 <input id="fileToUpload" type="file" multiple="multiple" /></pre>
<div class="box" id="imgPreview"></div>
<pre>
<script type="text/javascript">// <![CDATA[
			function createThumbnail(file) {
				var reader = new FileReader();

				reader.readAsDataURL(file);
				/*
				 * "L'objet FileReader permet aux applications Web de
				 * lire le contenu des fichiers
				 * (ou des tampons de données brutes)
				 * stockés sur l'ordinateur de l'utilisateur de manière asynchrone"
				 *
				 * https://developer.mozilla.org/fr/docs/DOM/FileReader
				 *
				 * Asynchrone d'ou l'obligation du reader.onload
				 * (fin du chargemen du File ) avant de lancer des traitements
				 * sur ce reader
				 *
				 */

				reader.onload = function(e) {
					var imgElement = document.createElement('img');
					imgElement.style.maxWidth = '150px';
					imgElement.style.maxHeight = '150px';
					/*
					 * Plus lisble qu'un this.result que je trouve
					 * particulièrement illisible
					 */
					imgElement.src = e.target.result;
					document.getElementById('imgPreview').appendChild(imgElement);
				}
			}

			document.querySelector('#fileToUpload').onchange = function() {
				console.log("Stat to upload");
				var filesList = this.files;

				console.log("List Files");
				for (var i = 0, filesListLength = filesList.length; i < filesListLength; i++) {
					useFile = filesList[i];
					console.log("File num : " + i + " Name : " + useFile.name + " Size : " + useFile.size + " Type : " + useFile.type);
					createThumbnail(useFile);
				}
			}

// ]]></script>


Itération 4 : upload partie client.

Nous avons un formulaire permettant de sélectionner des images et de les prévisualiser.
Ajoutons à présent la partie cliente de l’upload.

Nous utilisons l’Objet FormData bien plus avantageux que l’ancienne version Ajax.
FormaData permet :

  • d’envoyer des couple clef/data : donc plus d’un fichier à la fois si on le désire et une meilleur sémantique (gràce à la clef) coté serveur
  • gestion du TimeOut permettant de stopper un upload
  • des informations sur la progression de l’Upload
			function upload(file) {
				var xhr = new XMLHttpRequest();
				xhr.open('POST', 'srv.php');

				var formData = new FormData();
				formData.append("thefile", file);

				xhr.send(formData);

				xhr.onreadystatechange = function() {
					if (xhr.readyState == xhr.DONE) {
						console.log('Upload Done / response '+xhr.responseText);
					}
				};

			}

Pour l’instant srv.php est à l’état de mock (un simple fichier vide peut faire l’affaire), dans une logique itérative je n’ajoute qu’une fonctionnalité (ou un refactoring) par étape.

Etape 5

Codons notre upload partie serveur.
La « nouveauté » est localisé au niveau de la récupération des données, l’upload reste un upload classique tel.

Nous allons utiliser $_FILES, c’est une association des clefs (dans notre cas une seule)/propriétés fichiers.

les propriétés sont les suivantes :

  • [« name »] : nom du fichier
  • [« type »] : mime type
  • [« tmp_name »] : nom du fichier temporaire uploadé
  • [« error »] : si 0 OK
  • *[« size »] : Taille du fichier

Code de srv.php


<!--?php var_dump($_FILES["thefile"]);  /*  * array(5) {  *  ["name"]=-->  string(30) "Bd7769GCYAAP7wc (7e copie).jpg"
 * ["type"]=> string(10) "image/jpeg"
 * ["tmp_name"]=> string(14) "/tmp/phpXsSyf2"
 * ["error"]=> int(0)
 * ["size"]=> int(56140)
 */

$target_Path = "/home/lde/temp/";
echo $target_Path = $target_Path.basename($_FILES['thefile']['name'] );
echo $source_Path = $_FILES['thefile']['tmp_name'];
move_uploaded_file($source_Path , $target_Path );

?>

A venir :

  • partie 2 : gestion de la progression des upload
  • partie 3 : suppression du formulaire et remplacement par un Drag&Drop